diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-05 22:16:07 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-05 22:16:07 (GMT) |
commit | 7b81ebbe4e9bd8009706310e477e03556e16bd98 (patch) | |
tree | 675a49107afdf7fb62d272638afc0f6ff15b483c | |
parent | ab7f05de4862f2eec7b77001b34f5fb78d371298 (diff) |
Update context metadata on new .xo releases
-rw-r--r-- | TODO | 2 | ||||
-rwxr-xr-x | misc/aslo-sync | 149 | ||||
-rw-r--r-- | sugar_network/client/clones.py | 9 | ||||
-rw-r--r-- | sugar_network/model/context.py | 39 | ||||
-rw-r--r-- | sugar_network/node/routes.py | 58 | ||||
-rw-r--r-- | sugar_network/toolkit/__init__.py | 37 | ||||
-rw-r--r-- | sweets.recipe | 1 | ||||
-rwxr-xr-x | tests/integration/node_client.py | 2 | ||||
-rwxr-xr-x | tests/units/node/node.py | 71 |
9 files changed, 191 insertions, 177 deletions
@@ -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 = [] |