diff options
Diffstat (limited to 'sugar_network/model/__init__.py')
-rw-r--r-- | sugar_network/model/__init__.py | 275 |
1 files changed, 262 insertions, 13 deletions
diff --git a/sugar_network/model/__init__.py b/sugar_network/model/__init__.py index 167eb30..7278d10 100644 --- a/sugar_network/model/__init__.py +++ b/sugar_network/model/__init__.py @@ -13,34 +13,283 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -from sugar_network.model.routes import VolumeRoutes, FrontRoutes +import os +import gettext +import logging +from os.path import join + +import xapian + +from sugar_network import toolkit, db +from sugar_network.db import files +from sugar_network.model.routes import FrontRoutes +from sugar_network.toolkit.spec import parse_version, parse_requires +from sugar_network.toolkit.spec import EMPTY_LICENSE +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit.bundle import Bundle +from sugar_network.toolkit.router import ACL +from sugar_network.toolkit import i18n, http, exception, enforce CONTEXT_TYPES = [ 'activity', 'group', 'package', 'book', ] + POST_TYPES = [ - 'review', # Review the Context - 'object', # Object generated by Context application - 'question', # Q&A request - 'answer', # Q&A response - 'issue', # Propblem with the Context - 'announce', # General announcement - 'update', # Auto-generated Post for updates within the Context - 'feedback', # Review parent Post - 'comment', # Dependent Post + 'review', # Review the Context + 'object', # Object generated by Context application + 'question', # Q&A request + 'answer', # Q&A response + 'issue', # Propblem with the Context + 'announce', # General announcement + 'notification', # Auto-generated Post for updates within the Context + 'feedback', # Review parent Post + 'post', # General purpose dependent Post ] STABILITIES = [ 'insecure', 'buggy', 'developer', 'testing', 'stable', ] -RATINGS = [0, 1, 2, 3, 4, 5] - RESOURCES = ( 'sugar_network.model.context', 'sugar_network.model.post', - 'sugar_network.model.release', 'sugar_network.model.report', 'sugar_network.model.user', ) + +_logger = logging.getLogger('model') + + +class Rating(db.List): + + def __init__(self, **kwargs): + db.List.__init__(self, db.Numeric(), default=[0, 0], **kwargs) + + def slotting(self, value): + rating = float(value[1]) / value[0] if value[0] else 0 + return xapian.sortable_serialise(rating) + + +class Release(object): + + def typecast(self, rel): + if this.resource.exists and \ + 'activity' not in this.resource['type'] and \ + 'book' not in this.resource['type']: + return rel + if not isinstance(rel, dict): + __, rel = load_bundle(files.post(rel), context=this.request.guid) + return rel['spec']['*-*']['bundle'], rel + + def teardown(self, rel): + if this.resource.exists and \ + 'activity' not in this.resource['type'] and \ + 'book' not in this.resource['type']: + return + for spec in rel['spec'].values(): + files.delete(spec['bundle']) + + def encode(self, value): + return [] + + +def generate_node_stats(volume): + + def calc_rating(**kwargs): + rating = [0, 0] + alldocs, __ = volume['post'].find(**kwargs) + for post in alldocs: + if post['vote']: + rating[0] += 1 + rating[1] += post['vote'] + return rating + + alldocs, __ = volume['context'].find() + for context in alldocs: + rating = calc_rating(type='review', context=context.guid) + volume['context'].update(context.guid, {'rating': rating}) + + alldocs, __ = volume['post'].find(topic='') + for topic in alldocs: + rating = calc_rating(type='feedback', topic=topic.guid) + volume['post'].update(topic.guid, {'rating': rating}) + + +def populate_context_images(props, svg): + if 'guid' in props: + from sugar_network.toolkit.sugar import color_svg + svg = color_svg(svg, props['guid']) + props['artifact_icon'] = files.post( + svg, + {'mime_type': 'image/svg+xml'}, + ).digest + props['icon'] = files.post( + toolkit.svg_to_png(svg, 55, 55), + {'mime_type': 'image/png'}, + ).digest + props['logo'] = files.post( + toolkit.svg_to_png(svg, 140, 140), + {'mime_type': 'image/png'}, + ).digest + + +def load_bundle(blob, context=None, initial=False, extra_deps=None): + contexts = this.volume['context'] + context_type = None + context_meta = None + release_notes = None + release = {} + blob_meta = {} + + try: + bundle = Bundle(blob.path, mime_type='application/zip') + except Exception: + context_type = 'book' + if not context: + context = this.request['context'] + release['version'] = this.request['version'] + if 'license' in this.request: + release['license'] = this.request['license'] + if isinstance(release['license'], basestring): + release['license'] = [release['license']] + release['spec'] = {'*-*': { + 'bundle': blob.digest, + }} + blob_meta['mime_type'] = this.request.content_type + else: + context_type = 'activity' + unpack_size = 0 + + with bundle: + changelog = join(bundle.rootdir, 'CHANGELOG') + for arcname in bundle.get_names(): + if changelog and arcname == changelog: + with bundle.extractfile(changelog) as f: + release_notes = f.read() + changelog = None + unpack_size += bundle.getmember(arcname).size + spec = bundle.get_spec() + context_meta = _load_context_metadata(bundle, spec) + + if not context: + context = spec['context'] + else: + enforce(context == spec['context'], + http.BadRequest, 'Wrong context') + if extra_deps: + spec.requires.update(parse_requires(extra_deps)) + + release['version'] = spec['version'] + release['stability'] = spec['stability'] + if spec['license'] is not EMPTY_LICENSE: + release['license'] = spec['license'] + release['requires'] = requires = [] + for dep_name, dep in spec.requires.items(): + found = False + for version in dep.versions_range(): + requires.append('%s-%s' % (dep_name, version)) + found = True + if not found: + requires.append(dep_name) + release['spec'] = {'*-*': { + 'bundle': blob.digest, + 'commands': spec.commands, + 'requires': spec.requires, + }} + release['unpack_size'] = unpack_size + blob_meta['mime_type'] = 'application/vnd.olpc-sugar' + + enforce(context, http.BadRequest, 'Context is not specified') + enforce(release['version'], http.BadRequest, 'Version is not specified') + release['release'] = parse_version(release['version']) + if initial and not contexts.exists(context): + enforce(context_meta, http.BadRequest, 'No way to initate context') + context_meta['guid'] = context + context_meta['type'] = [context_type] + this.call(method='POST', path=['context'], content=context_meta) + else: + enforce(context_type in contexts[context]['type'], + http.BadRequest, 'Inappropriate bundle type') + context_obj = contexts[context] + + releases = context_obj['releases'] + if 'license' not in release: + enforce(releases, http.BadRequest, 'License is not specified') + recent = max(releases, key=lambda x: releases[x]['release']) + release['license'] = releases[recent]['license'] + + _logger.debug('Load %r release: %r', context, release) + + if this.request.principal in context_obj['author']: + diff = context_obj.patch(context_meta) + if diff: + this.call(method='PUT', path=['context', context], content=diff) + context_obj.props.update(diff) + # TRANS: Release notes title + title = i18n._('%(name)s %(version)s release') + else: + # TRANS: 3rd party release notes title + title = i18n._('%(name)s %(version)s third-party release') + release['announce'] = this.call(method='POST', path=['post'], + content={ + 'context': context, + 'type': 'notification', + 'title': i18n.encode(title, + name=context_obj['title'], + version=release['version'], + ), + 'message': release_notes or '', + }, + content_type='application/json') + + filename = ''.join(i18n.decode(context_obj['title']).split()) + blob_meta['name'] = '%s-%s' % (filename, release['version']) + files.update(blob.digest, blob_meta) + + return context, release + + +def _load_context_metadata(bundle, spec): + result = {} + for prop in ('homepage', 'mime_types'): + if spec[prop]: + result[prop] = spec[prop] + result['guid'] = spec['context'] + + try: + icon_file = bundle.extractfile(join(bundle.rootdir, spec['icon'])) + populate_context_images(result, icon_file.read()) + icon_file.close() + except Exception: + exception(_logger, 'Failed to load icon') + + msgids = {} + for prop, confname in [ + ('title', 'name'), + ('summary', 'summary'), + ('description', 'description'), + ]: + if spec[confname]: + msgids[prop] = spec[confname] + result[prop] = {'en': spec[confname]} + with toolkit.mkdtemp() as tmpdir: + for path in bundle.get_names(): + if not path.endswith('.mo'): + continue + mo_path = path.strip(os.sep).split(os.sep) + if len(mo_path) != 5 or mo_path[1] != 'locale': + continue + lang = mo_path[2] + bundle.extract(path, tmpdir) + try: + translation = gettext.translation(spec['context'], + join(tmpdir, *mo_path[:2]), [lang]) + for prop, value in msgids.items(): + msgstr = translation.gettext(value).decode('utf8') + if lang == 'en' or msgstr != value: + result[prop][lang] = msgstr + except Exception: + exception(_logger, 'Gettext failed to read %r', mo_path[-1]) + + return result |