diff options
Diffstat (limited to 'sugar_network/model')
-rw-r--r-- | sugar_network/model/__init__.py | 275 | ||||
-rw-r--r-- | sugar_network/model/context.py | 156 | ||||
-rw-r--r-- | sugar_network/model/post.py | 69 | ||||
-rw-r--r-- | sugar_network/model/release.py | 83 | ||||
-rw-r--r-- | sugar_network/model/report.py | 42 | ||||
-rw-r--r-- | sugar_network/model/routes.py | 154 | ||||
-rw-r--r-- | sugar_network/model/user.py | 6 |
7 files changed, 411 insertions, 374 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 diff --git a/sugar_network/model/context.py b/sugar_network/model/context.py index 1763d65..6bac120 100644 --- a/sugar_network/model/context.py +++ b/sugar_network/model/context.py @@ -13,42 +13,33 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import hashlib -from cStringIO import StringIO - -from sugar_network import db, model, static, toolkit -from sugar_network.toolkit.router import Blob, ACL +from sugar_network import db, model +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit.router import ACL class Context(db.Resource): - @db.indexed_property(prefix='T', full_text=True, - typecast=[model.CONTEXT_TYPES]) + @db.indexed_property(db.List, prefix='T', full_text=True, + subtype=db.Enum(model.CONTEXT_TYPES)) def type(self, value): return value @type.setter def type(self, value): - if value and 'package' in value and 'common' not in self['layer']: - self['layer'] = tuple(self['layer']) + ('common',) - if 'artifact_icon' not in self: - for name in ('activity', 'book', 'group'): - if name not in self.type: - continue - with file(static.path('images', name + '.svg')) as f: - Context.populate_images(self, f.read()) - break - return value - - @db.indexed_property(slot=1, prefix='S', full_text=True, localized=True) + if 'package' in value and 'common' not in self['layer']: + self.post('layer', self['layer'] + ['common']) + return value + + @db.indexed_property(db.Localized, slot=1, prefix='S', full_text=True) def title(self, value): return value - @db.indexed_property(prefix='R', full_text=True, localized=True) + @db.indexed_property(db.Localized, prefix='R', full_text=True) def summary(self, value): return value - @db.indexed_property(prefix='D', full_text=True, localized=True) + @db.indexed_property(db.Localized, prefix='D', full_text=True) def description(self, value): return value @@ -56,72 +47,49 @@ class Context(db.Resource): def homepage(self, value): return value - @db.indexed_property(prefix='Y', default=[], typecast=[], full_text=True) + @db.indexed_property(db.List, prefix='Y', default=[], full_text=True) def mime_types(self, value): return value - @db.blob_property(mime_type='image/png') + @db.stored_property(db.Blob, mime_type='image/png', default='missing.png') def icon(self, value): - if value: - return value - if 'package' in self['type']: - return Blob({ - 'url': '/static/images/package.png', - 'blob': static.path('images', 'package.png'), - 'mime_type': 'image/png', - }) - else: - return Blob({ - 'url': '/static/images/missing.png', - 'blob': static.path('images', 'missing.png'), - 'mime_type': 'image/png', - }) - - @db.blob_property(mime_type='image/svg+xml') + return value + + @db.stored_property(db.Blob, mime_type='image/svg+xml', + default='missing.svg') def artifact_icon(self, value): - if value: - return value - if 'package' in self['type']: - return Blob({ - 'url': '/static/images/package.svg', - 'blob': static.path('images', 'package.svg'), - 'mime_type': 'image/png', - }) - else: - return Blob({ - 'url': '/static/images/missing.svg', - 'blob': static.path('images', 'missing.svg'), - 'mime_type': 'image/svg+xml', - }) - - @db.blob_property(mime_type='image/png') + return value + + @db.stored_property(db.Blob, mime_type='image/png', + default='missing-logo.png') def logo(self, value): - if value: - return value - if 'package' in self['type']: - return Blob({ - 'url': '/static/images/package-logo.png', - 'blob': static.path('images', 'package-logo.png'), - 'mime_type': 'image/png', - }) - else: - return Blob({ - 'url': '/static/images/missing-logo.png', - 'blob': static.path('images', 'missing-.png'), - 'mime_type': 'image/png', - }) - - @db.indexed_property(slot=2, default=0, acl=ACL.READ | ACL.CALC) - def downloads(self, value): return value - @db.indexed_property(slot=3, typecast=[], default=[0, 0], - sortable_serialise=lambda x: float(x[1]) / x[0] if x[0] else 0, + @db.stored_property(db.Aggregated, subtype=db.Blob()) + def previews(self, value): + return value + + @db.stored_property(db.Aggregated, subtype=model.Release(), + acl=ACL.READ | ACL.INSERT | ACL.REMOVE | ACL.REPLACE) + def releases(self, value): + return value + + @releases.setter + def releases(self, value): + if value or this.request.method != 'POST': + self.invalidate_solutions() + return value + + @db.indexed_property(db.Numeric, slot=2, default=0, acl=ACL.READ | ACL.CALC) + def downloads(self, value): + return value + + @db.indexed_property(model.Rating, slot=3, acl=ACL.READ | ACL.CALC) def rating(self, value): return value - @db.stored_property(typecast=[], default=[], acl=ACL.PUBLIC | ACL.LOCAL) + @db.stored_property(db.List, default=[], acl=ACL.PUBLIC | ACL.LOCAL) def dependencies(self, value): """Software dependencies. @@ -131,32 +99,20 @@ class Context(db.Resource): """ return value - @db.stored_property(typecast=dict, default={}, - acl=ACL.PUBLIC | ACL.LOCAL) - def aliases(self, value): - return value - - @db.stored_property(typecast=dict, default={}, acl=ACL.PUBLIC | ACL.LOCAL) - def packages(self, value): + @dependencies.setter + def dependencies(self, value): + if value or this.request.method != 'POST': + self.invalidate_solutions() return value - @staticmethod - def populate_images(props, svg): - if 'guid' in props: - from sugar_network.toolkit.sugar import color_svg - svg = color_svg(svg, props['guid']) + def deleted(self): + self.invalidate_solutions() - def convert(w, h): - png = toolkit.svg_to_png(svg, w, h) - return {'blob': png, - 'mime_type': 'image/png', - 'digest': hashlib.sha1(png.getvalue()).hexdigest(), - } + def restored(self): + self.invalidate_solutions() - props['artifact_icon'] = { - 'blob': StringIO(svg), - 'mime_type': 'image/svg+xml', - 'digest': hashlib.sha1(svg).hexdigest(), - } - props['icon'] = convert(55, 55) - props['logo'] = convert(140, 140) + def invalidate_solutions(self): + this.broadcast({ + 'event': 'release', + 'seqno': this.volume.releases_seqno.next(), + }) diff --git a/sugar_network/model/post.py b/sugar_network/model/post.py index 88c6956..107f354 100644 --- a/sugar_network/model/post.py +++ b/sugar_network/model/post.py @@ -13,39 +13,31 @@ # 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 import db, model, static -from sugar_network.toolkit.router import Blob, ACL +from sugar_network import db, model +from sugar_network.toolkit.router import ACL +from sugar_network.toolkit.coroutine import this class Post(db.Resource): - @db.indexed_property(prefix='C', - acl=ACL.CREATE | ACL.READ) + @db.indexed_property(prefix='C', acl=ACL.CREATE | ACL.READ) def context(self, value): return value - @db.indexed_property(prefix='A', default='', - acl=ACL.CREATE | ACL.READ) + @db.indexed_property(prefix='A', default='', acl=ACL.CREATE | ACL.READ) def topic(self, value): return value - @topic.setter - def topic(self, value): - if value and not self['context']: - post = self.volume['post'].get(value) - self['context'] = post['context'] - return value - - @db.indexed_property(prefix='T', typecast=model.POST_TYPES) + @db.indexed_property(db.Enum, prefix='T', items=model.POST_TYPES) def type(self, value): return value - @db.indexed_property(slot=1, prefix='N', full_text=True, localized=True, + @db.indexed_property(db.Localized, slot=1, prefix='N', full_text=True, acl=ACL.CREATE | ACL.READ) def title(self, value): return value - @db.indexed_property(prefix='M', full_text=True, localized=True, + @db.indexed_property(db.Localized, prefix='M', full_text=True, acl=ACL.CREATE | ACL.READ) def message(self, value): return value @@ -54,40 +46,45 @@ class Post(db.Resource): def solution(self, value): return value - @db.indexed_property(prefix='V', typecast=model.RATINGS, default=0, + @db.indexed_property(db.Enum, prefix='V', items=range(5), default=0, acl=ACL.CREATE | ACL.READ) def vote(self, value): return value - @db.indexed_property(prefix='D', typecast=db.AggregatedType, - full_text=True, default=db.AggregatedType(), - fmt=lambda x: [i.get('message') for i in x.values()], - acl=ACL.READ | ACL.INSERT | ACL.REMOVE) + @vote.setter + def vote(self, value): + if value: + if self['topic']: + resource = this.volume['post'] + guid = self['topic'] + else: + resource = this.volume['context'] + guid = self['context'] + orig = resource[guid]['rating'] + resource.update(guid, {'rating': [orig[0] + 1, orig[1] + value]}) + return value + + @db.indexed_property(db.Aggregated, prefix='D', full_text=True, + subtype=db.Localized()) def comments(self, value): return value - @db.blob_property(mime_type='image/png') + @db.stored_property(db.Blob, mime_type='image/png', + default='missing-logo.png') def preview(self, value): - if value: - return value - return Blob({ - 'url': '/static/images/missing-logo.png', - 'blob': static.path('images', 'missing-logo.png'), - 'mime_type': 'image/png', - }) - - @db.blob_property() - def data(self, value): + return value + + @db.stored_property(db.Aggregated, subtype=db.Blob()) + def attachments(self, value): if value: value['name'] = self['title'] return value - @db.indexed_property(slot=2, default=0, acl=ACL.READ | ACL.CALC) + @db.indexed_property(db.Numeric, slot=2, default=0, + acl=ACL.READ | ACL.CALC) def downloads(self, value): return value - @db.indexed_property(slot=3, typecast=[], default=[0, 0], - sortable_serialise=lambda x: float(x[1]) / x[0] if x[0] else 0, - acl=ACL.READ | ACL.CALC) + @db.indexed_property(model.Rating, slot=3, acl=ACL.READ | ACL.CALC) def rating(self, value): return value diff --git a/sugar_network/model/release.py b/sugar_network/model/release.py deleted file mode 100644 index 46eeaae..0000000 --- a/sugar_network/model/release.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright (C) 2012-2013 Aleksey Lim -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import xapian - -from sugar_network import db, model -from sugar_network.toolkit.router import ACL -from sugar_network.toolkit.licenses import GOOD_LICENSES -from sugar_network.toolkit.spec import parse_version - - -class Release(db.Resource): - - @db.indexed_property(prefix='C', - acl=ACL.CREATE | ACL.READ) - def context(self, value): - return value - - @context.setter - def context(self, value): - if self.request.principal: - authors = self.volume['context'].get(value)['author'] - if self.request.principal in authors: - self['layer'] = ('origin',) + tuple(self.layer) - return value - - @db.indexed_property(prefix='L', full_text=True, typecast=[GOOD_LICENSES], - acl=ACL.CREATE | ACL.READ) - def license(self, value): - return value - - @db.indexed_property(slot=1, prefix='V', fmt=lambda x: _fmt_version(x), - acl=ACL.CREATE | ACL.READ) - def version(self, value): - return value - - @db.indexed_property(prefix='S', default='stabile', - acl=ACL.CREATE | ACL.READ, typecast=model.STABILITIES) - def stability(self, value): - return value - - @db.indexed_property(prefix='N', full_text=True, localized=True, - default='', acl=ACL.CREATE | ACL.READ) - def notes(self, value): - return value - - @db.indexed_property(prefix='R', typecast=[], default=[], - acl=ACL.CREATE | ACL.READ) - def requires(self, value): - return value - - @db.blob_property() - def data(self, value): - return value - - -def _fmt_version(version): - version = parse_version(version) - # Convert to [(`version`, `modifier`)] - version = zip(*([iter(version)] * 2)) - major, modifier = version.pop(0) - - result = sum([(rank % 10000) * pow(10000, 3 - i) - for i, rank in enumerate((major + [0, 0])[:3])]) - result += (5 + modifier) * 1000 - if modifier and version: - minor, __ = version.pop(0) - if minor: - result += (minor[0] % 1000) - - return xapian.sortable_serialise(result) diff --git a/sugar_network/model/report.py b/sugar_network/model/report.py index 84db43a..980c3ff 100644 --- a/sugar_network/model/report.py +++ b/sugar_network/model/report.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Aleksey Lim +# Copyright (C) 2012-2014 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,6 +17,19 @@ from sugar_network import db from sugar_network.toolkit.router import ACL +class _Solution(db.Property): + + def __init__(self, **kwargs): + db.Property.__init__(self, default=[], **kwargs) + + def typecast(self, value): + return [] if value is None else list(value) + + def encode(self, value): + for i in value: + yield i[0] + + class Report(db.Resource): @db.indexed_property(prefix='C', acl=ACL.CREATE | ACL.READ) @@ -24,28 +37,27 @@ class Report(db.Resource): return value @db.indexed_property(prefix='V', default='', acl=ACL.CREATE | ACL.READ) - def release(self, value): + def version(self, value): return value - @release.setter - def release(self, value): - if value and 'version' not in self.props and 'release' in value: - version = self.volume['release'].get(value) - self['version'] = version['version'] + @db.indexed_property(prefix='E', full_text=True, acl=ACL.CREATE | ACL.READ) + def error(self, value): return value - @db.stored_property(default='', acl=ACL.CREATE | ACL.READ) - def version(self, value): + @db.indexed_property(prefix='U', full_text=True, acl=ACL.CREATE | ACL.READ) + def uname(self, value): return value - @db.stored_property(typecast=dict, default={}, acl=ACL.CREATE | ACL.READ) - def environ(self, value): + @db.indexed_property(db.Dict, prefix='L', full_text=True, + acl=ACL.CREATE | ACL.READ) + def lsb_release(self, value): return value - @db.indexed_property(prefix='T', acl=ACL.CREATE | ACL.READ) - def error(self, value): + @db.indexed_property(_Solution, prefix='S', full_text=True, + acl=ACL.CREATE | ACL.READ) + def solution(self, value): return value - @db.blob_property() - def data(self, value): + @db.stored_property(db.Aggregated, subtype=db.Blob()) + def logs(self, value): return value diff --git a/sugar_network/model/routes.py b/sugar_network/model/routes.py index c8f8da6..ff0377f 100644 --- a/sugar_network/model/routes.py +++ b/sugar_network/model/routes.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013 Aleksey Lim +# Copyright (C) 2013-2014 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,55 +14,21 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import logging -import mimetypes -from os.path import split -from sugar_network import static, db -from sugar_network.toolkit.router import route, fallbackroute, Blob, ACL +from sugar_network.db import files +from sugar_network.toolkit.router import route +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import coroutine _logger = logging.getLogger('model.routes') -class VolumeRoutes(db.Routes): - - @route('GET', ['context', None], cmd='feed', - mime_type='application/json') - def feed(self, request, distro): - context = self.volume['context'].get(request.guid) - releases = self.volume['release'] - versions = [] - - impls, __ = releases.find(context=context.guid, - not_layer='deleted', **request) - for impl in impls: - version = impl.properties([ - 'guid', 'ctime', 'layer', 'author', 'tags', - 'version', 'stability', 'license', 'notes', - ]) - if context['dependencies']: - requires = version.setdefault('requires', {}) - for i in context['dependencies']: - requires.setdefault(i, {}) - version['data'] = data = impl.meta('data') - for key in ('mtime', 'seqno', 'blob'): - if key in data: - del data[key] - versions.append(version) - - result = {'releases': versions} - if distro: - aliases = context['aliases'].get(distro) - if aliases and 'binary' in aliases: - result['packages'] = aliases['binary'] - return result - - class FrontRoutes(object): def __init__(self): - self._pooler = _Pooler() + self._spooler = coroutine.Spooler() + this.broadcast = self._broadcast @route('GET', mime_type='text/html') def hello(self): @@ -80,34 +46,14 @@ class FrontRoutes(object): response.content_length = 0 @route('GET', cmd='subscribe', mime_type='text/event-stream') - def subscribe(self, request=None, response=None, ping=False, **condition): + def subscribe(self, request=None, response=None, **condition): """Subscribe to Server-Sent Events.""" if request is not None and not condition: condition = request if response is not None: response.content_type = 'text/event-stream' response['Cache-Control'] = 'no-cache' - return self._pull_events(request, ping, condition) - - @route('POST', cmd='broadcast', - mime_type='application/json', acl=ACL.LOCAL) - def broadcast(self, event=None, request=None): - if request is not None: - event = request.content - _logger.debug('Broadcast event: %r', event) - self._pooler.notify_all(event) - - @fallbackroute('GET', ['static']) - def get_static(self, request): - path = static.path(*request.path[1:]) - if not mimetypes.inited: - mimetypes.init() - mime_type = mimetypes.types_map.get('.' + path.rsplit('.', 1)[-1]) - return Blob({ - 'blob': path, - 'filename': split(path)[-1], - 'mime_type': mime_type, - }) + return self._pull_events(request, condition) @route('GET', ['robots.txt'], mime_type='text/plain') def robots(self, request, response): @@ -115,34 +61,29 @@ class FrontRoutes(object): @route('GET', ['favicon.ico']) def favicon(self, request, response): - return Blob({ - 'blob': static.path('favicon.ico'), - 'mime_type': 'image/x-icon', - }) - - def _pull_events(self, request, ping, condition): - _logger.debug('Start subscription, total=%s', self._pooler.waiters + 1) - - if ping: - # XXX The whole commands' kwargs handling should be redesigned - if 'ping' in condition: - condition.pop('ping') - # If non-greenlet application needs only to initiate - # a subscription and do not stuck in waiting for the first event, - # it should pass `ping` argument to return fake event to unblock - # `GET /?cmd=subscribe` call. - yield {'event': 'pong'} - - rfile = None + return files.get('favicon.ico') + + def _broadcast(self, event): + _logger.debug('Broadcast event: %r', event) + self._spooler.notify_all(event) + + def _pull_events(self, request, condition): + _logger.debug('Start %s-nth subscription', self._spooler.waiters + 1) + + # Unblock `GET /?cmd=subscribe` call to let non-greenlet application + # initiate a subscription and do not stuck in waiting for the 1st event + yield {'event': 'pong'} + + subscription = None if request is not None: - rfile = request.content_stream - if rfile is not None: - coroutine.spawn(self._waiter_for_closing, rfile) + subscription = request.content_stream + if subscription is not None: + coroutine.spawn(self._wait_for_closing, subscription) while True: - event = self._pooler.wait() + event = self._spooler.wait() if not isinstance(event, dict): - if event is rfile: + if event is subscription: break else: continue @@ -155,48 +96,13 @@ class FrontRoutes(object): else: yield event - _logger.debug('Stop subscription, total=%s', self._pooler.waiters) + _logger.debug('Stop %s-nth subscription', self._spooler.waiters) - def _waiter_for_closing(self, rfile): + def _wait_for_closing(self, rfile): try: coroutine.select([rfile.fileno()], [], []) finally: - self._pooler.notify_all(rfile) - - -class _Pooler(object): - """One-producer-to-many-consumers events delivery.""" - - def __init__(self): - self._value = None - self._waiters = 0 - self._ready = coroutine.Event() - self._open = coroutine.Event() - self._open.set() - - @property - def waiters(self): - return self._waiters - - def wait(self): - self._open.wait() - self._waiters += 1 - try: - self._ready.wait() - finally: - self._waiters -= 1 - if self._waiters == 0: - self._ready.clear() - self._open.set() - return self._value - - def notify_all(self, value=None): - self._open.wait() - if not self._waiters: - return - self._open.clear() - self._value = value - self._ready.set() + self._spooler.notify_all(rfile) _HELLO_HTML = """\ diff --git a/sugar_network/model/user.py b/sugar_network/model/user.py index 69d0d42..b44093e 100644 --- a/sugar_network/model/user.py +++ b/sugar_network/model/user.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2013 Aleksey Lim +# Copyright (C) 2012-2014 Aleksey Lim # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,10 +27,10 @@ class User(db.Resource): def location(self, value): return value - @db.indexed_property(slot=2, prefix='B', default=0, typecast=int) + @db.indexed_property(db.Numeric, slot=2, prefix='B', default=0) def birthday(self, value): return value - @db.blob_property(acl=ACL.CREATE, mime_type='text/plain') + @db.stored_property(db.Blob, acl=ACL.CREATE, mime_type='text/plain') def pubkey(self, value): return value |