Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/model
diff options
context:
space:
mode:
Diffstat (limited to 'sugar_network/model')
-rw-r--r--sugar_network/model/__init__.py275
-rw-r--r--sugar_network/model/context.py156
-rw-r--r--sugar_network/model/post.py69
-rw-r--r--sugar_network/model/release.py83
-rw-r--r--sugar_network/model/report.py42
-rw-r--r--sugar_network/model/routes.py154
-rw-r--r--sugar_network/model/user.py6
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