Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/sugar_network/model/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'sugar_network/model/__init__.py')
-rw-r--r--sugar_network/model/__init__.py275
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