Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2013-08-05 22:16:07 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2013-08-05 22:16:07 (GMT)
commit7b81ebbe4e9bd8009706310e477e03556e16bd98 (patch)
tree675a49107afdf7fb62d272638afc0f6ff15b483c
parentab7f05de4862f2eec7b77001b34f5fb78d371298 (diff)
Update context metadata on new .xo releases
-rw-r--r--TODO2
-rwxr-xr-xmisc/aslo-sync149
-rw-r--r--sugar_network/client/clones.py9
-rw-r--r--sugar_network/model/context.py39
-rw-r--r--sugar_network/node/routes.py58
-rw-r--r--sugar_network/toolkit/__init__.py37
-rw-r--r--sweets.recipe1
-rwxr-xr-xtests/integration/node_client.py2
-rwxr-xr-xtests/units/node/node.py71
9 files changed, 191 insertions, 177 deletions
diff --git a/TODO b/TODO
index 407cd85..58b9661 100644
--- a/TODO
+++ b/TODO
@@ -4,10 +4,8 @@
- Remove temporal security hole with speciying guid in POST,
it was added as a fast hack to support offline creation (with later pushing to a node)
-- get all localized strings from activity.info while populating local contexts
- activities migth need MIME registering while checking-in
- changed pulls should take into account accept_length
-- i18n activity.info's strings
- handle DELETE while calculating per-object node stats
- unstall activities on checking out and on initial syncing
- increase granularity for sync.chunked_encode()
diff --git a/misc/aslo-sync b/misc/aslo-sync
index a260632..2e32b53 100755
--- a/misc/aslo-sync
+++ b/misc/aslo-sync
@@ -30,7 +30,7 @@ from sugar_network.node import data_root
from sugar_network.client.bundle import Bundle
from sugar_network.resources.volume import Volume
from sugar_network.node.slave import SlaveCommands
-from sugar_network.node.commands import load_activity_bundle
+from sugar_network.node.routes import load_bundle
from sugar_network.toolkit import util, licenses, application, spec, Option
@@ -41,19 +41,6 @@ SUGAR_GUID = 'sugar'
SN_GUID = 'sugar-network'
PACKAGES_GUID = 'packages'
-SUGAR_RELEASES = {
- (0, 82): 'sugar-0.82',
- (0, 84): 'sugar-0.84',
- (0, 86): 'sugar-0.86',
- (0, 88): 'sugar-0.88',
- (0, 90): 'sugar-0.90',
- (0, 92): 'sugar-0.92',
- (0, 94): 'sugar-0.94',
- (0, 96): 'sugar-0.96',
- (0, 98): 'sugar-0.98',
- (0, 100): 'sugar-0.100',
- }
-
CATEGIORIES_TO_TAGS = {
'Search & Discovery': 'discovery',
'Documents': 'documents',
@@ -218,45 +205,6 @@ class Application(application.Application):
self.sync_activities()
@application.command(
- 'pull activities metadata from activities.sugarlabs.org')
- def pull_metadata(self):
- sql = """
- SELECT
- id,
- guid
- FROM
- addons
- WHERE
- status > 0 AND status < 5
- """
- for addon_id in self.args:
- sql += ' AND id = %s' % addon_id
-
- for addon_id, bundle_id in self.sqlexec(sql):
- impls, __ = self.volume['implementation'].find(
- context=bundle_id, order_by='-version', limit=1)
- for impl in impls:
- version = impl['version']
- break
- else:
- continue
- rows = self.sqlexec("""
- SELECT
- files.filename
- FROM versions
- INNER JOIN files ON files.version_id = versions.id
- WHERE
- versions.addon_id = %s AND versions.version = '%s'
- ORDER BY
- versions.id DESC
- LIMIT
- 1
- """ % (addon_id, version))
- if not rows:
- continue
- self.sync_context_metadata(bundle_id, addon_id, rows[0][0])
-
- @application.command(
'submit pulled activities.sugarlabs.org content to '
'Sugar Network server')
def push(self):
@@ -374,9 +322,6 @@ class Application(application.Application):
versions.id DESC
""" % addon_id
- recent_version = None
- recent_filename = None
-
for version_id, version, license_id, alicense, release_date, \
releasenotes, filename, sugar_min, sugar_max \
in self.sqlexec(sql):
@@ -439,88 +384,10 @@ class Application(application.Application):
(version, bundle_id, error)
continue
- if parsed_version > recent_version:
- recent_version = parsed_version
- recent_filename = filename
-
- if recent_version:
- self.sync_context_metadata(bundle_id, addon_id, recent_filename)
-
for guid in existing_versions:
print '-- Hide %s %r version deleted on ASLO' % (bundle_id, guid)
directory.update(guid, {'layer': ['deleted']})
- def sync_context_metadata(self, bundle_id, addon_id, filename):
- bundle = Bundle(join(ACTIVITIES_PATH, str(addon_id), filename))
- spec = bundle.get_spec()
-
- props = {}
- for prop in ('homepage', 'mime_types'):
- if spec[prop]:
- props[prop] = spec[prop]
-
- try:
- svg = bundle.extractfile(join(bundle.rootdir, spec['icon']))
- icon = props['artifact_icon'] = svg.read()
- png = svg_to_png(icon, '--width=55', '--height=55')
- if png:
- props['icon'] = png
- png = svg_to_png(icon, '--width=160', '--height=120')
- if png:
- props['preview'] = png
- except Exception, error:
- print '-- Cannot find activity icon in %r: %s' % (filename, error)
-
- msgids = {}
- for prop, confname in [
- ('title', 'name'),
- ('summary', 'summary'),
- ('description', 'description'),
- ]:
- if spec[confname]:
- msgids[prop] = spec[confname]
-
- title, summary, description = self.sqlexec("""
- SELECT
- addons.name,
- addons.summary,
- addons.description
- FROM
- addons
- WHERE
- addons.id = %s
- """ % addon_id)[0]
- if 'title' not in msgids:
- props['title'] = AbsDict(self.get_i18n_field(title))
- if 'summary' not in msgids:
- props['summary'] = AbsDict(self.get_i18n_field(summary))
- if 'description' not in msgids:
- props['description'] = AbsDict(self.get_i18n_field(description))
-
- tmpdir = tempfile.mkdtemp()
- try:
- for path in bundle.get_names():
- if not path.endswith('.mo'):
- continue
- locale_path = path.strip(os.sep).split(os.sep)
- if len(locale_path) != 5 or locale_path[1] != 'locale':
- continue
- lang = locale_path[2]
- bundle.extract(path, tmpdir)
- i18n = gettext.translation(bundle_id,
- join(tmpdir, *locale_path[:2]), [lang])
- for prop, value in msgids.items():
- msgstr = i18n.gettext(value)
- if msgstr != value or lang == 'en':
- props.setdefault(prop, AbsDict())[lang] = msgstr
- except Exception, error:
- print '-- Failed to read locales from %r: %s' % (filename, error)
- finally:
- shutil.rmtree(tmpdir)
-
- print '-- Update %r metadata from %r' % (bundle_id, filename)
- self.volume['context'].update(bundle_id, props)
-
def sync_context(self, addon_id, bundle_id):
if not self.volume['context'].exists(bundle_id):
self.volume['context'].create({
@@ -601,8 +468,9 @@ class Application(application.Application):
def sync_implementaiton(self, context, addon_id, filename,
sugar_min, sugar_max, status, license, notes, date):
bundle_path = join(ACTIVITIES_PATH, str(addon_id), filename)
- with load_activity_bundle(self.volume, bundle_path,
- 'sugar>=%s; sugar<=%s' % (sugar_min, sugar_max)) as impl:
+ with load_bundle(self.volume, bundle_path, {
+ 'requires': 'sugar>=%s; sugar<=%s' % (sugar_min, sugar_max),
+ }) as impl:
if impl['license'] == spec.EMPTY_LICENSE:
if not license and context in LICENSES_MAP:
license = LICENSES_MAP[context]
@@ -680,15 +548,6 @@ class Application(application.Application):
return result
-def svg_to_png(svg, *args):
- try:
- return util.assert_call(
- ('rsvg-convert', '--keep-aspect-ratio') + args,
- stdin=svg)
- except Exception, error:
- print '-- Cannot convert SVG icon: %s' % error
-
-
mysql_server = Option(
'MySQL server',
default='localhost', name='mysql_server')
diff --git a/sugar_network/client/clones.py b/sugar_network/client/clones.py
index 34ff8c6..b7c8ec8 100644
--- a/sugar_network/client/clones.py
+++ b/sugar_network/client/clones.py
@@ -22,6 +22,7 @@ from os.path import join, exists, lexists, relpath, dirname, basename, isdir
from os.path import abspath, islink
from sugar_network import db, client, toolkit
+from sugar_network.model.context import Context
from sugar_network.toolkit.spec import Spec
from sugar_network.toolkit.inotify import Inotify, \
IN_DELETE_SELF, IN_CREATE, IN_DELETE, IN_CLOSE_WRITE, \
@@ -168,12 +169,8 @@ class _Inotify(Inotify):
icon_path = join(spec.root, spec['icon'])
if exists(icon_path):
- with file(icon_path, 'rb') as f:
- self._contexts.update(context,
- {'artifact_icon': {'blob': f}})
- with toolkit.NamedTemporaryFile() as f:
- toolkit.svg_to_png(icon_path, f.name, 32, 32)
- self._contexts.update(context, {'icon': {'blob': f.name}})
+ with file(icon_path, 'rb') as svg:
+ self._contexts.update(context, Context.image_props(svg))
self._checkin_activity(spec)
diff --git a/sugar_network/model/context.py b/sugar_network/model/context.py
index 772a71a..41bf46b 100644
--- a/sugar_network/model/context.py
+++ b/sugar_network/model/context.py
@@ -13,6 +13,7 @@
# 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 cStringIO import StringIO
from os.path import join
from sugar_network import db, model, static
@@ -129,3 +130,41 @@ class Context(db.Resource):
@db.stored_property(typecast=dict, default={}, acl=ACL.PUBLIC | ACL.LOCAL)
def packages(self, value):
return value
+
+ @staticmethod
+ def image_props(svg):
+ icon = StringIO(svg.read())
+ return {'artifact_icon': {
+ 'blob': icon,
+ 'mime_type': 'image/svg+xml',
+ },
+ 'icon': {
+ 'blob': _svg_to_png(icon.getvalue(), 55, 55),
+ 'mime_type': 'image/png',
+ },
+ 'preview': {
+ 'blob': _svg_to_png(icon.getvalue(), 160, 120),
+ 'mime_type': 'image/png',
+ },
+ }
+
+
+def _svg_to_png(data, w, h):
+ import rsvg
+ import cairo
+
+ svg = rsvg.Handle(data=data)
+ surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
+ context = cairo.Context(surface)
+
+ scale = min(float(w) / svg.props.width, float(h) / svg.props.height)
+ context.translate(
+ int(w - svg.props.width * scale) / 2,
+ int(h - svg.props.height * scale) / 2)
+ context.scale(scale, scale)
+ svg.render_cairo(context)
+
+ result = StringIO()
+ surface.write_to_png(result)
+ result.seek(0)
+ return result
diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py
index 8457be6..3e457c8 100644
--- a/sugar_network/node/routes.py
+++ b/sugar_network/node/routes.py
@@ -15,6 +15,7 @@
import os
import shutil
+import gettext
import logging
import hashlib
from contextlib import contextmanager
@@ -23,13 +24,14 @@ from os.path import join, isdir, exists
from sugar_network import db, node, toolkit, model
from sugar_network.node import stats_node, stats_user
+from sugar_network.model.context import Context
# pylint: disable-msg=W0611
from sugar_network.toolkit.router import route, preroute, postroute
from sugar_network.toolkit.router import ACL, fallbackroute
from sugar_network.toolkit.spec import EMPTY_LICENSE
from sugar_network.toolkit.spec import parse_requires, ensure_requires
from sugar_network.toolkit.bundle import Bundle
-from sugar_network.toolkit import http, coroutine, enforce
+from sugar_network.toolkit import http, coroutine, exception, enforce
_MAX_STATS_LENGTH = 100
@@ -312,8 +314,8 @@ class NodeRoutes(db.Routes, model.Routes):
'Operation is permitted only for superusers')
@postroute
- def postroute(self, request, response, result, exception):
- if exception is None or isinstance(exception, http.StatusPass):
+ def postroute(self, request, response, result, error):
+ if error is None or isinstance(error, http.StatusPass):
if self._stats is not None:
self._stats.log(request)
@@ -408,6 +410,7 @@ def load_bundle(volume, bundle_path, impl=None):
impl = {}
data = impl.setdefault('data', {})
data['blob'] = bundle_path
+ context_updates = {}
try:
bundle = Bundle(bundle_path, mime_type='application/zip')
@@ -423,6 +426,7 @@ def load_bundle(volume, bundle_path, impl=None):
unpack_size += bundle.getmember(arcname).size
spec = bundle.get_spec()
extract = bundle.rootdir
+ context_updates = _load_context_metadata(bundle, spec)
if 'requires' in impl:
spec.requires.update(parse_requires(impl.pop('requires')))
impl['context'] = spec['context']
@@ -460,6 +464,52 @@ def load_bundle(volume, bundle_path, impl=None):
volume['implementation'].update(i.guid, {'layer': layer})
impl['guid'] = volume['implementation'].create(impl)
+ if context_updates:
+ volume['context'].update(impl['context'], context_updates)
+
+
+def _load_context_metadata(bundle, spec):
+ result = {}
+ for prop in ('homepage', 'mime_types'):
+ if spec[prop]:
+ result[prop] = spec[prop]
+
+ try:
+ with bundle.extractfile(join(bundle.rootdir, spec['icon'])) as svg:
+ result.update(Context.image_props(svg))
+ 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:
+ i18n = gettext.translation(spec['context'],
+ join(tmpdir, *mo_path[:2]), [lang])
+ for prop, value in msgids.items():
+ msgstr = i18n.gettext(value)
+ if msgstr != value or lang == 'en':
+ result[prop][lang] = msgstr
+ except Exception:
+ exception(_logger, 'Gettext failed to read %r', mo_path[-1])
+
+ return result
+
def _load_pubkey(pubkey):
pubkey = pubkey.strip()
@@ -472,7 +522,7 @@ def _load_pubkey(pubkey):
['ssh-keygen', '-f', key_file.name, '-e', '-m', 'PKCS8'])
except Exception:
message = 'Cannot read DSS public key gotten for registeration'
- toolkit.exception(message)
+ exception(_logger, message)
if node.trust_users.value:
logging.warning('Failed to read registration pubkey, '
'but we trust users')
diff --git a/sugar_network/toolkit/__init__.py b/sugar_network/toolkit/__init__.py
index b1f7c81..e586d31 100644
--- a/sugar_network/toolkit/__init__.py
+++ b/sugar_network/toolkit/__init__.py
@@ -17,6 +17,7 @@ import os
import sys
import json
import errno
+import shutil
import logging
import tempfile
import collections
@@ -328,23 +329,6 @@ def symlink(src, dst):
os.symlink(src, dst)
-def svg_to_png(src_path, dst_path, width, height):
- import rsvg
- import cairo
-
- svg = rsvg.Handle(src_path)
-
- surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
- context = cairo.Context(surface)
- scale = min(
- float(width) / svg.props.width,
- float(height) / svg.props.height)
- context.scale(scale, scale)
- svg.render_cairo(context)
-
- surface.write_to_png(dst_path)
-
-
def assert_call(cmd, stdin=None, **kwargs):
"""Variant of `call` method with raising exception of errors.
@@ -423,8 +407,6 @@ def cptree(src, dst):
path to the new directory
"""
- import shutil
-
if abspath(src) == abspath(dst):
return
@@ -504,6 +486,23 @@ def unique_filename(root, filename):
return path
+class mkdtemp(str):
+
+ def __new__(cls, **kwargs):
+ if cachedir.value:
+ if not exists(cachedir.value):
+ os.makedirs(cachedir.value)
+ kwargs['dir'] = cachedir.value
+ result = tempfile.mkdtemp(**kwargs)
+ return str.__new__(cls, result)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ shutil.rmtree(self)
+
+
def TemporaryFile(*args, **kwargs):
if cachedir.value:
if not exists(cachedir.value):
diff --git a/sweets.recipe b/sweets.recipe
index 37b497a..6ecd64d 100644
--- a/sweets.recipe
+++ b/sweets.recipe
@@ -13,6 +13,7 @@ stability = developer
requires = xapian-bindings-python; m2crypto; rrdtool-python; gevent >= 1
dbus-python; openssh-client; sugar-network-webui; sugar-network-hub
+ rsvg; cairo
replaces = sugar-network-server; sweets-recipe; active-document
pylru; requests
diff --git a/tests/integration/node_client.py b/tests/integration/node_client.py
index db41138..ec30271 100755
--- a/tests/integration/node_client.py
+++ b/tests/integration/node_client.py
@@ -123,7 +123,7 @@ class NodeClientTest(tests.Test):
assert not exists(pubkey_path)
self.assertEqual(
sorted(['dep1.rpm', 'dep2.rpm', 'dep3.rpm']),
- sorted(deplist.split('\n')))
+ sorted([i.strip() for i in deplist.split('\n')]))
self.cli(['PUT', '/context/context', '--anonymous', 'cmd=clone', 'nodeps=1', 'stability=stable', '-jd', '1'])
assert not exists(privkey_path)
diff --git a/tests/units/node/node.py b/tests/units/node/node.py
index 8a9ce5f..014ecfe 100755
--- a/tests/units/node/node.py
+++ b/tests/units/node/node.py
@@ -1,9 +1,11 @@
#!/usr/bin/env python
+# -*- coding: utf-8 -*-
# sugar-lint: disable
import os
import time
import json
+import base64
from email.utils import formatdate, parsedate
from cStringIO import StringIO
from os.path import exists
@@ -749,6 +751,75 @@ class NodeTest(tests.Test):
self.assertEqual([], volume['implementation'].get(guid4)['layer'])
self.assertEqual(bundle3, conn.get(['context', 'bundle_id'], cmd='clone'))
+ def test_release_LoadMetadata(self):
+ volume = self.start_master()
+ conn = Connection()
+
+ conn.post(['context'], {
+ 'guid': 'org.laptop.ImageViewerActivity',
+ 'type': 'activity',
+ 'title': {'en': ''},
+ 'summary': {'en': ''},
+ 'description': {'en': ''},
+ })
+ svg = '\n'.join([
+ '<?xml version="1.0" encoding="UTF-8"?>',
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [',
+ ' <!ENTITY fill_color "#FFFFFF">',
+ ' <!ENTITY stroke_color "#010101">',
+ ']>',
+ '<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">',
+ ' <rect x="3" y="7" width="44" height="36" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:3"/>',
+ ' <polyline points="15,7 25,1 35,7" style="fill:none;;stroke:&stroke_color;;stroke-width:1.25"/>',
+ ' <circle cx="14" cy="19" r="4.5" style="fill:&stroke_color;;stroke:&stroke_color;;stroke-width:1.5"/>',
+ ' <polyline points="3,36 16,32 26,35" style="fill:none;;stroke:&stroke_color;;stroke-width:2.5"/>',
+ ' <polyline points="15,43 37,28 47,34 47,43" style="fill:&stroke_color;;stroke:&stroke_color;;stroke-width:3"/>',
+ ' <polyline points="22,41.5 35,30 27,41.5" style="fill:&fill_color;;stroke:none;;stroke-width:0"/>',
+ ' <polyline points="26,23 28,25 30,23" style="fill:none;;stroke:&stroke_color;;stroke-width:.9"/>',
+ ' <polyline points="31.2,20 33.5,17.7 35.8,20" style="fill:none;;stroke:&stroke_color;;stroke-width:1"/>',
+ ' <polyline points="36,13 38.5,15.5 41,13" style="fill:none;;stroke:&stroke_color;;stroke-width:1"/>',
+ '</svg>',
+ ])
+ bundle = self.zips(
+ ('ImageViewer.activity/activity/activity.info', '\n'.join([
+ '[Activity]',
+ 'bundle_id = org.laptop.ImageViewerActivity',
+ 'name = Image Viewer',
+ 'summary = The Image Viewer activity is a simple and fast image viewer tool',
+ 'description = It has features one would expect of a standard image viewer, like zoom, rotate, etc.',
+ 'homepage = http://wiki.sugarlabs.org/go/Activities/Image_Viewer',
+ 'activity_version = 22',
+ 'license = GPLv2+',
+ 'icon = activity-imageviewer',
+ 'exec = true',
+ 'mime_types = image/bmp;image/gif',
+ ])),
+ ('ImageViewer.activity/locale/ru/LC_MESSAGES/org.laptop.ImageViewerActivity.mo',
+ base64.b64decode('3hIElQAAAAAMAAAAHAAAAHwAAAARAAAA3AAAAAAAAAAgAQAADwAAACEBAAAOAAAAMQEAAA0AAABAAQAACgAAAE4BAAAMAAAAWQEAAA0AAABmAQAAJwAAAHQBAAAUAAAAnAEAABAAAACxAQAABwAAAMIBAAAIAAAAygEAANEBAADTAQAAIQAAAKUDAAATAAAAxwMAABwAAADbAwAAFwAAAPgDAAAhAAAAEAQAAB0AAAAyBAAAQAAAAFAEAAA9AAAAkQQAADUAAADPBAAAFAAAAAUFAAAQAAAAGgUAAAEAAAACAAAABwAAAAAAAAADAAAAAAAAAAwAAAAJAAAAAAAAAAoAAAAEAAAAAAAAAAAAAAALAAAABgAAAAgAAAAFAAAAAENob29zZSBkb2N1bWVudABEb3dubG9hZGluZy4uLgBGaXQgdG8gd2luZG93AEZ1bGxzY3JlZW4ASW1hZ2UgVmlld2VyAE9yaWdpbmFsIHNpemUAUmV0cmlldmluZyBzaGFyZWQgaW1hZ2UsIHBsZWFzZSB3YWl0Li4uAFJvdGF0ZSBhbnRpY2xvY2t3aXNlAFJvdGF0ZSBjbG9ja3dpc2UAWm9vbSBpbgBab29tIG91dABQcm9qZWN0LUlkLVZlcnNpb246IFBBQ0tBR0UgVkVSU0lPTgpSZXBvcnQtTXNnaWQtQnVncy1UbzogClBPVC1DcmVhdGlvbi1EYXRlOiAyMDEyLTA5LTI3IDE0OjU3LTA0MDAKUE8tUmV2aXNpb24tRGF0ZTogMjAxMC0wOS0yMiAxMzo1MCswMjAwCkxhc3QtVHJhbnNsYXRvcjoga3JvbTlyYSA8a3JvbTlyYUBnbWFpbC5jb20+Ckxhbmd1YWdlLVRlYW06IExBTkdVQUdFIDxMTEBsaS5vcmc+Ckxhbmd1YWdlOiAKTUlNRS1WZXJzaW9uOiAxLjAKQ29udGVudC1UeXBlOiB0ZXh0L3BsYWluOyBjaGFyc2V0PVVURi04CkNvbnRlbnQtVHJhbnNmZXItRW5jb2Rpbmc6IDhiaXQKUGx1cmFsLUZvcm1zOiBucGx1cmFscz0zOyBwbHVyYWw9KG4lMTA9PTEgJiYgbiUxMDAhPTExID8gMCA6IG4lMTA+PTIgJiYgbiUxMDw9NCAmJiAobiUxMDA8MTAgfHwgbiUxMDA+PTIwKSA/IDEgOiAyKTsKWC1HZW5lcmF0b3I6IFBvb3RsZSAyLjAuMwoA0JLRi9Cx0LXRgNC40YLQtSDQtNC+0LrRg9C80LXQvdGCANCX0LDQs9GA0YPQt9C60LAuLi4A0KPQvNC10YHRgtC40YLRjCDQsiDQvtC60L3QtQDQn9C+0LvQvdGL0Lkg0Y3QutGA0LDQvQDQn9GA0L7RgdC80L7RgtGAINC60LDRgNGC0LjQvdC+0LoA0JjRgdGC0LjQvdC90YvQuSDRgNCw0LfQvNC10YAA0J/QvtC70YPRh9C10L3QuNC1INC40LfQvtCx0YDQsNC20LXQvdC40LksINC/0L7QtNC+0LbQtNC40YLQtS4uLgDQn9C+0LLQtdGA0L3Rg9GC0Ywg0L/RgNC+0YLQuNCyINGH0LDRgdC+0LLQvtC5INGB0YLRgNC10LvQutC4ANCf0L7QstC10YDQvdGD0YLRjCDQv9C+INGH0LDRgdC+0LLQvtC5INGB0YLRgNC10LvQutC1ANCf0YDQuNCx0LvQuNC30LjRgtGMANCe0YLQtNCw0LvQuNGC0YwA')),
+ ('ImageViewer.activity/activity/activity-imageviewer.svg', svg),
+ )
+ impl = json.load(conn.request('POST', ['implementation'], bundle, params={'cmd': 'release'}).raw)
+
+ context = volume['context'].get('org.laptop.ImageViewerActivity')
+ self.assertEqual({
+ 'en': 'Image Viewer',
+ 'ru': u'Просмотр картинок',
+ },
+ context['title'])
+ self.assertEqual({
+ 'en': 'The Image Viewer activity is a simple and fast image viewer tool',
+ },
+ context['summary'])
+ self.assertEqual({
+ 'en': 'It has features one would expect of a standard image viewer, like zoom, rotate, etc.',
+ },
+ context['description'])
+ self.assertEqual(svg, file(context['artifact_icon']['blob']).read())
+ assert 'blob' in context['icon']
+ assert 'blob' in context['preview']
+ self.assertEqual('http://wiki.sugarlabs.org/go/Activities/Image_Viewer', context['homepage'])
+ self.assertEqual(['image/bmp', 'image/gif'], context['mime_types'])
+
def call(routes, method, document=None, guid=None, prop=None, principal=None, cmd=None, content=None, **kwargs):
path = []