Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--addons/EmbeddedInterpreter.py30
-rw-r--r--addons/WidgetIdentifier.py35
-rw-r--r--addons/eventgenerator.py60
-rw-r--r--addons/readfile.py6
-rw-r--r--addons/timerevent.py4
-rw-r--r--data/icons/Layer 1.svg6
-rw-r--r--data/icons/clock.sugar.svg1593
-rw-r--r--tests/constraintstests.py42
-rw-r--r--tests/enginetests.py30
-rw-r--r--tests/inject.py57
-rw-r--r--tests/propertiestests.py57
-rw-r--r--tests/translatortests.py131
-rw-r--r--tests/vaulttests.py21
-rw-r--r--tutorius/actions.py1
-rw-r--r--tutorius/constraints.py39
-rw-r--r--tutorius/core.py22
-rw-r--r--tutorius/editor_interpreter.py105
-rw-r--r--tutorius/events.py36
-rw-r--r--tutorius/ipython_view.py301
-rw-r--r--tutorius/properties.py53
-rw-r--r--tutorius/translator.py186
-rw-r--r--tutorius/vault.py12
22 files changed, 2794 insertions, 33 deletions
diff --git a/addons/EmbeddedInterpreter.py b/addons/EmbeddedInterpreter.py
new file mode 100644
index 0000000..8c3522e
--- /dev/null
+++ b/addons/EmbeddedInterpreter.py
@@ -0,0 +1,30 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor_interpreter import EditorInterpreter
+from sugar.tutorius.services import ObjectStore
+
+class EmbeddedInterpreter(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = EditorInterpreter(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "EmbeddedInterpreter",
+ "display_name" : "Embedded Interpreter",
+ "icon" : "message-bubble",
+ "class" : EmbeddedInterpreter,
+ "mandatory_props" : []
+}
diff --git a/addons/WidgetIdentifier.py b/addons/WidgetIdentifier.py
new file mode 100644
index 0000000..3c559b5
--- /dev/null
+++ b/addons/WidgetIdentifier.py
@@ -0,0 +1,35 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor import WidgetIdentifier as WIPrimitive
+from sugar.tutorius.services import ObjectStore
+
+class WidgetIdentifier(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = WIPrimitive(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ # TODO elavoie 2009-07-19
+ # We should disconnect the handlers, however there seems to be an error
+ # saying that the size of the dictionary changed during the iteration
+ # We should investigate this
+ #self._dialog._disconnect_handlers()
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "WidgetIdentifier",
+ "display_name" : "Widget Identifier",
+ "icon" : "message-bubble",
+ "class" : WidgetIdentifier,
+ "mandatory_props" : []
+}
diff --git a/addons/eventgenerator.py b/addons/eventgenerator.py
new file mode 100644
index 0000000..a91ccf4
--- /dev/null
+++ b/addons/eventgenerator.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+from sugar.tutorius.actions import *
+from sugar.tutorius.gtkutils import find_widget
+
+class EventGenerator(Action):
+ source = TUAMProperty("")
+ type = TStringProperty("clicked")
+
+ def __init__(self, source=None, type="clicked"):
+ Action.__init__(self)
+
+ if source != None:
+ self.source = source
+
+ if type != "clicked":
+ self.type = type
+
+ def do(self):
+ self._activity = ObjectStore().activity
+
+ # TODO elavoie 2009-07-25 We should eventually use the UAM mecanism
+ widget = find_widget(self._activity, self.source)
+
+
+ # TODO elavoie 2009-07-25 We assume a gtk activity, it might
+ # get messier with other widget systems
+
+ # Call the signal on the widget
+ # We use introspection here to obtain the
+ # method that will send the corresponding
+ # signal on the gtk object
+ getattr(widget, self.type)()
+
+ # That's all!!!
+
+ def undo(self):
+ pass
+
+__action__ = {
+ "name" : "EventGenerator",
+ "display_name" : "Event Generator",
+ "icon" : "message-bubble",
+ "class" : EventGenerator,
+ "mandatory_props" : ["source", "type"]
+}
+
diff --git a/addons/readfile.py b/addons/readfile.py
index 4a6c54d..3cd41b6 100644
--- a/addons/readfile.py
+++ b/addons/readfile.py
@@ -16,9 +16,9 @@
import os
-from ..actions import Action
-from ..properties import TFileProperty
-from ..services import ObjectStore
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TFileProperty
+from sugar.tutorius.services import ObjectStore
class ReadFile(Action):
filename = TFileProperty(None)
diff --git a/addons/timerevent.py b/addons/timerevent.py
index 752a865..c7374d0 100644
--- a/addons/timerevent.py
+++ b/addons/timerevent.py
@@ -16,8 +16,8 @@
import gobject
-from ..filters import EventFilter
-from ..properties import TIntProperty
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TIntProperty
class TimerEvent(EventFilter):
"""
diff --git a/data/icons/Layer 1.svg b/data/icons/Layer 1.svg
new file mode 100644
index 0000000..e7d9e2b
--- /dev/null
+++ b/data/icons/Layer 1.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#666666">
+ <!ENTITY fill_color "#ffffff">
+]><svg height="48px" id="svg2597" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46" sodipodi:docname="chain.svg" sodipodi:version="0.32" width="48px" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><g display="block" id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1">
+ <path d="M 9.8339096,17.521424 L 11.225374,16.510468 L 21.895596,24.262843 L 20.957347,27.150437 L 19.395745,28.285012 L 20.561807,24.696201 L 11.225374,17.912898 L 10.202552,18.656048 L 9.8339096,17.521424 z M 21.057049,9.3673619 L 31.727272,1.6149871 L 42.397446,9.3673619 L 39.590553,18.005887 L 38.62545,17.304719 L 41.063657,9.8007699 L 31.727272,3.0174177 L 22.390838,9.8007699 L 23.597982,13.515988 L 23.001478,15.351781 L 21.057049,9.3673619 z M 24.194484,19.023319 L 24.790989,17.187573 L 25.957048,20.776336 L 37.497447,20.776336 L 37.888118,19.573873 L 38.853268,20.275089 L 38.321764,21.910912 L 25.132685,21.910912 L 24.194484,19.023319 z M 0.5551853,24.262843 L 7.9036154,18.923855 L 8.2723049,20.058432 L 1.8889674,24.696201 L 5.4551984,35.671819 L 16.995595,35.671819 L 18.202737,31.9566 L 19.764434,30.821973 L 17.819959,36.806394 L 4.6308143,36.806394 L 0.5551853,24.262843 z M 31.476438,20.209046 L 36.567095,16.510468 L 47.237269,24.262843 L 43.161633,36.806394 L 34.078493,36.806394 L 34.447138,35.671819 L 42.337264,35.671819 L 45.903479,24.696201 L 36.567095,17.912898 L 33.406728,20.209046 L 31.476438,20.209046 z M 5.3950189,9.3673619 L 16.065194,1.6149871 L 23.413707,6.9539745 L 22.44856,7.6551909 L 16.065194,3.0174177 L 6.7288079,9.8007699 L 10.29502,20.776336 L 14.201416,20.776336 L 15.763019,21.910912 L 9.4707017,21.910912 L 5.3950189,9.3673619 z M 18.061955,20.776336 L 21.835416,20.776336 L 25.401627,9.8007699 L 24.378759,9.0576215 L 25.343953,8.3564062 L 26.735416,9.3673619 L 22.659781,21.910912 L 19.623558,21.910912 L 18.061955,20.776336 z M 14.494797,37.37368 L 15.687758,37.37368 L 18.126011,44.877733 L 29.666452,44.877733 L 33.232667,33.902157 L 30.072253,31.605965 L 29.475748,29.770175 L 34.566456,33.468751 L 30.490816,46.012316 L 17.301647,46.012316 L 14.494797,37.37368 z M 25.896871,24.262843 L 28.353226,22.478199 L 30.283475,22.478199 L 27.230659,24.696201 L 30.796869,35.671819 L 32.061166,35.671819 L 31.692527,36.806394 L 29.972505,36.806394 L 25.896871,24.262843 z M 13.226011,33.468751 L 23.896234,25.716376 L 26.352544,27.501018 L 26.949047,29.336809 L 23.896234,27.118808 L 14.559799,33.902157 L 14.950472,35.104529 L 13.757512,35.104529 L 13.226011,33.468751 z" id="path2535"/>
+ </g></svg> \ No newline at end of file
diff --git a/data/icons/clock.sugar.svg b/data/icons/clock.sugar.svg
new file mode 100644
index 0000000..0334c1a
--- /dev/null
+++ b/data/icons/clock.sugar.svg
@@ -0,0 +1,1593 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="48"
+ id="svg2"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:version="0.46"
+ sodipodi:docbase="C:\Documents and Settings\Molumen\Desktop"
+ sodipodi:docname="clock.sugar.svg"
+ sodipodi:modified="true"
+ sodipodi:version="0.32"
+ version="1.0"
+ width="48">
+ <defs
+ id="defs4">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 115.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="231 : 115.5 : 1"
+ inkscape:persp3d-origin="115.5 : 77 : 1"
+ id="perspective228" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient20470"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-340,200.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20468"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-341,27.5432)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20466"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="504.125"
+ cy="468.57623"
+ fx="504.125"
+ fy="468.57623"
+ gradientTransform="matrix(1.05261,0,0,1.05261,-26.5224,-23.8951)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20464"
+ inkscape:collect="always"
+ r="2.625"
+ xlink:href="#linearGradient13172" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-744.784,-597.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20462"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20460"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient20428" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient20438"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient20428" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-744.784,-597.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19456"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-341,27.5432)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19449"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-340,200.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19446"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient19441"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19439"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19437"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="504.125"
+ cy="468.57623"
+ fx="504.125"
+ fy="468.57623"
+ gradientTransform="matrix(1.05261,0,0,1.05261,-26.5224,-23.8951)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19435"
+ inkscape:collect="always"
+ r="2.625"
+ xlink:href="#linearGradient13172" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19433"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient19431"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16295"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-781.919,-183.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16293"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16285"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-781.919,-183.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16283"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16275"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-781.919,-183.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16273"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16265"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-781.919,-183.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16263"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16249"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-781.919,-183.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16241"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient16243" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16197"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16195"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16189"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16187"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16181"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16179"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16173"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16171"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16165"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16163"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16095"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,-173.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16093"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16091"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16089"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16087"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,-173.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16085"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16083"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16081"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16079"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,-173.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16077"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16075"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16073"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16071"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,-173.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16069"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16067"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16065"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16063"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,-173.605)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16061"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient15913" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16059"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.25538,-1.25538,2.63349,2.63349,-1015.92,24.3952)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient16057"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ <radialGradient
+ cx="202.5"
+ cy="578.86218"
+ fx="202.5"
+ fy="578.86218"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13728"
+ inkscape:collect="always"
+ r="91.5"
+ xlink:href="#linearGradient10759" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13265"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13263"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13261"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="504.125"
+ cy="468.57623"
+ fx="504.125"
+ fy="468.57623"
+ gradientTransform="matrix(1.05261,0,0,1.05261,-26.5224,-23.8951)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13259"
+ inkscape:collect="always"
+ r="2.625"
+ xlink:href="#linearGradient13172" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13257"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13255"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13231"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13229"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13227"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="504.125"
+ cy="468.57623"
+ fx="504.125"
+ fy="468.57623"
+ gradientTransform="matrix(1.05261,0,0,1.05261,-26.5224,-23.8951)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13225"
+ inkscape:collect="always"
+ r="2.625"
+ xlink:href="#linearGradient13172" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13223"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13221"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13206"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13203"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13200"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13195"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13193"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13191"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13189"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13187"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <radialGradient
+ cx="504.125"
+ cy="468.57623"
+ fx="504.125"
+ fy="468.57623"
+ gradientTransform="matrix(1.05261,0,0,1.05261,-26.5224,-23.8951)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13170"
+ inkscape:collect="always"
+ r="2.625"
+ xlink:href="#linearGradient13172" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13146"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="365.95651"
+ y2="84.524567" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13143"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13140"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13137"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13133"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="426.36218"
+ y2="150.36218" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13131"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13129"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13127"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13125"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <linearGradient
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient13032"
+ inkscape:collect="always"
+ x1="302"
+ x2="302"
+ xlink:href="#linearGradient13034"
+ y1="426.36218"
+ y2="150.36218" />
+ <radialGradient
+ cx="302"
+ cy="239.93021"
+ fx="302"
+ fy="239.93021"
+ gradientTransform="matrix(3.14096,0,0,3.14096,-646.57,-549.905)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13010"
+ inkscape:collect="always"
+ r="138"
+ xlink:href="#linearGradient13012" />
+ <radialGradient
+ cx="527"
+ cy="691.20294"
+ fx="527"
+ fy="691.20294"
+ gradientTransform="matrix(1,0,0,0.231842,-1,463.219)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient13000"
+ inkscape:collect="always"
+ r="90.78125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(-0.932879,0,0,-0.244839,1018.94,683.505)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient12987"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="528"
+ cy="368.17188"
+ fx="528"
+ fy="368.17188"
+ gradientTransform="matrix(1,0,0,0.262455,-2,290.543)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient12983"
+ inkscape:collect="always"
+ r="113.53125"
+ xlink:href="#linearGradient12977" />
+ <radialGradient
+ cx="525.49945"
+ cy="467.18744"
+ fx="525.49945"
+ fy="467.18744"
+ gradientTransform="matrix(1.77314,0,0,1.77314,-405.784,-334.014)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient12959"
+ inkscape:collect="always"
+ r="138"
+ spreadMethod="pad"
+ xlink:href="#linearGradient12953" />
+ <linearGradient
+ id="linearGradient5553">
+ <stop
+ id="stop5555"
+ offset="0"
+ style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" />
+ <stop
+ id="stop5557"
+ offset="1"
+ style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5613">
+ <stop
+ id="stop5615"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.888889;" />
+ <stop
+ id="stop5617"
+ offset="0.5"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop5619"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop5621"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5675">
+ <stop
+ id="stop5677"
+ offset="0"
+ style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" />
+ <stop
+ id="stop5679"
+ offset="0.5"
+ style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" />
+ <stop
+ id="stop5681"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop5683"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5563">
+ <stop
+ id="stop5565"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.703704;" />
+ <stop
+ id="stop5571"
+ offset="0.50850612"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.189815;" />
+ <stop
+ id="stop5573"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop5567"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5537">
+ <stop
+ id="stop5539"
+ offset="0"
+ style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" />
+ <stop
+ id="stop5541"
+ offset="1"
+ style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10743">
+ <stop
+ id="stop10745"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.703704;" />
+ <stop
+ id="stop10747"
+ offset="0.50850612"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.189815;" />
+ <stop
+ id="stop10749"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10751"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10753">
+ <stop
+ id="stop10755"
+ offset="0"
+ style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" />
+ <stop
+ id="stop10757"
+ offset="1"
+ style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10759">
+ <stop
+ id="stop10761"
+ offset="0"
+ style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" />
+ <stop
+ id="stop10763"
+ offset="1"
+ style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10767">
+ <stop
+ id="stop10769"
+ offset="0"
+ style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" />
+ <stop
+ id="stop10771"
+ offset="1"
+ style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10773">
+ <stop
+ id="stop10775"
+ offset="0"
+ style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" />
+ <stop
+ id="stop10777"
+ offset="0.5"
+ style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" />
+ <stop
+ id="stop10779"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10781"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10783">
+ <stop
+ id="stop10785"
+ offset="0"
+ style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" />
+ <stop
+ id="stop10787"
+ offset="0.5"
+ style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" />
+ <stop
+ id="stop10789"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10791"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10793">
+ <stop
+ id="stop10795"
+ offset="0"
+ style="stop-color: rgb(0, 138, 0); stop-opacity: 0.890196;" />
+ <stop
+ id="stop10797"
+ offset="0.5"
+ style="stop-color: rgb(72, 143, 51); stop-opacity: 0;" />
+ <stop
+ id="stop10799"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10801"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10803">
+ <stop
+ id="stop10805"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.888889;" />
+ <stop
+ id="stop10807"
+ offset="0.5"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10809"
+ offset="0.51142859"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop10811"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10813">
+ <stop
+ id="stop10815"
+ offset="0"
+ style="stop-color: rgb(53, 155, 68); stop-opacity: 1;" />
+ <stop
+ id="stop10817"
+ offset="1"
+ style="stop-color: rgb(50, 204, 73); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient10819">
+ <stop
+ id="stop10821"
+ offset="0"
+ style="stop-color: rgb(74, 206, 96); stop-opacity: 1;" />
+ <stop
+ id="stop10823"
+ offset="1"
+ style="stop-color: rgb(180, 234, 135); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient12953">
+ <stop
+ id="stop12955"
+ offset="0"
+ style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" />
+ <stop
+ id="stop12965"
+ offset="0.47816542"
+ style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" />
+ <stop
+ id="stop12961"
+ offset="0.49808899"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop12967"
+ offset="0.50756544"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop12963"
+ offset="0.53007674"
+ style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" />
+ <stop
+ id="stop12957"
+ offset="1"
+ style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient12977">
+ <stop
+ id="stop12979"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0.319444;" />
+ <stop
+ id="stop12981"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient13012">
+ <stop
+ id="stop13014"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop13018"
+ offset="0.20165709"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop13020"
+ offset="0.32675916"
+ style="stop-color: rgb(239, 245, 251); stop-opacity: 1;" />
+ <stop
+ id="stop13016"
+ offset="1"
+ style="stop-color: rgb(4, 72, 127); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient13034">
+ <stop
+ id="stop13036"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" />
+ <stop
+ id="stop13038"
+ offset="1"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient13172">
+ <stop
+ id="stop13174"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop13176"
+ offset="1"
+ style="stop-color: rgb(0, 0, 0); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3346">
+ <stop
+ id="stop3348"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop3350"
+ offset="1"
+ style="stop-color: rgb(233, 233, 233); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3916">
+ <stop
+ id="stop3918"
+ offset="0"
+ style="stop-color: rgb(139, 139, 139); stop-opacity: 0.639175;" />
+ <stop
+ id="stop3924"
+ offset="0.44642857"
+ style="stop-color: rgb(141, 141, 141); stop-opacity: 0.206186;" />
+ <stop
+ id="stop3922"
+ offset="1"
+ style="stop-color: rgb(143, 143, 143); stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3440">
+ <stop
+ id="stop3442"
+ offset="0"
+ style="stop-color: black; stop-opacity: 1;" />
+ <stop
+ id="stop3452"
+ offset="0.3125"
+ style="stop-color: black; stop-opacity: 1;" />
+ <stop
+ id="stop3446"
+ offset="0.53727454"
+ style="stop-color: white; stop-opacity: 1;" />
+ <stop
+ id="stop3542"
+ offset="0.60522962"
+ style="stop-color: white; stop-opacity: 1;" />
+ <stop
+ id="stop3448"
+ offset="0.6964286"
+ style="stop-color: black; stop-opacity: 1;" />
+ <stop
+ id="stop3444"
+ offset="1"
+ style="stop-color: black; stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3757">
+ <stop
+ id="stop3759"
+ offset="0"
+ style="stop-color: black; stop-opacity: 1;" />
+ <stop
+ id="stop3761"
+ offset="1"
+ style="stop-color: black; stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient3584">
+ <stop
+ id="stop3586"
+ offset="0"
+ style="stop-color: white; stop-opacity: 1;" />
+ <stop
+ id="stop3592"
+ offset="1"
+ style="stop-color: white; stop-opacity: 0.498039;" />
+ <stop
+ id="stop3588"
+ offset="1"
+ style="stop-color: white; stop-opacity: 0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient15913">
+ <stop
+ id="stop15915"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop15917"
+ offset="1"
+ style="stop-color: rgb(240, 216, 35); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient16243">
+ <stop
+ id="stop16245"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop16247"
+ offset="1"
+ style="stop-color: rgb(35, 178, 240); stop-opacity: 1;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient20428">
+ <stop
+ id="stop20430"
+ offset="0"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop20432"
+ offset="0.20165709"
+ style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" />
+ <stop
+ id="stop20434"
+ offset="0.32675916"
+ style="stop-color: rgb(251, 248, 239); stop-opacity: 1;" />
+ <stop
+ id="stop20436"
+ offset="1"
+ style="stop-color: rgb(127, 98, 4); stop-opacity: 1;" />
+ </linearGradient>
+ <radialGradient
+ cx="296.26508"
+ cy="361.61154"
+ fx="296.26508"
+ fy="361.61154"
+ gradientTransform="matrix(1.03888,-1.98608,4.32211,2.26874,-1497.58,2.13654)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient2855"
+ inkscape:collect="always"
+ r="131"
+ xlink:href="#linearGradient3346" />
+ </defs>
+ <sodipodi:namedview
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ id="base"
+ inkscape:current-layer="layer1"
+ inkscape:cx="21.55618"
+ inkscape:cy="22.885983"
+ inkscape:document-units="px"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:window-height="726"
+ inkscape:window-width="1208"
+ inkscape:window-x="62"
+ inkscape:window-y="25"
+ inkscape:zoom="11.240376"
+ objecttolerance="10"
+ pagecolor="#ffffff"
+ style=""
+ showgrid="false" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF
+ style="">
+ <cc:Work
+ rdf:about=""
+ style="">
+ <dc:format
+ style="">image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage"
+ style="" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:groupmode="layer"
+ inkscape:label="Layer 1"
+ transform="translate(-436.343,-114.661)">
+ <g
+ id="g19409"
+ transform="matrix(0.1657254,0,0,0.1657254,373.12967,56.687971)">
+ <path
+ d="M 440,288.36218 A 138,138 0 1 1 164,288.36218 A 138,138 0 1 1 440,288.36218 z"
+ id="path19411"
+ sodipodi:cx="302"
+ sodipodi:cy="288.36218"
+ sodipodi:rx="138"
+ sodipodi:ry="138"
+ sodipodi:type="arc"
+ style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ transform="matrix(0.862319,0,0,0.862319,265.58,245.715)" />
+ <path
+ d="M 526,356.375 C 449.824,356.375 388,418.199 388,494.375 C 388,570.551 449.824,632.375 526,632.375 C 602.176,632.375 664,570.551 664,494.375 C 664,418.199 602.176,356.375 526,356.375 z M 526,375.375 C 591.688,375.375 645,428.687 645,494.375 C 644.99999,560.063 591.688,613.375 526,613.375 C 460.312,613.37499 407,560.063 407,494.375 C 407,428.687 460.312,375.375 526,375.375 z"
+ id="path19413"
+ style="fill:url(#radialGradient19433);fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <g
+ id="g19415">
+ <path
+ d="M 572.45442,416.66652 L 574.56216,417.96362 L 518.94889,510.35634 L 515.03453,507.94743 L 572.45442,416.66652 z"
+ id="path19417"
+ sodipodi:nodetypes="ccccc"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ d="M 470.10131,522.52645 L 468.3205,518.67564 L 539.27256,484.69517 L 541.94377,490.47138 L 470.10131,522.52645 z"
+ id="path19419"
+ sodipodi:nodetypes="ccccc"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ d="M 506.75,471.48718 A 2.625,2.625 0 1 1 501.5,471.48718 A 2.625,2.625 0 1 1 506.75,471.48718 z"
+ id="path19421"
+ sodipodi:cx="504.125"
+ sodipodi:cy="471.48718"
+ sodipodi:rx="2.625"
+ sodipodi:ry="2.625"
+ sodipodi:type="arc"
+ style="opacity:1;fill:url(#radialGradient19435);fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ transform="matrix(1.85714,0,0,1.85714,-410.232,-381.244)" />
+ </g>
+ <path
+ d="M 526,357.375 C 478.94483,357.375 437.38188,380.97729 412.46875,416.96875 C 441.06326,387.02851 481.36166,368.375 526,368.375 C 570.63834,368.375 610.93674,387.02852 639.53125,416.96875 C 614.61812,380.97729 573.05517,357.375 526,357.375 z"
+ id="path19423"
+ style="fill:url(#radialGradient19437);fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ d="M 526,623.46875 C 562.37673,623.46875 594.94742,607.12155 616.78125,581.375 C 592.50974,602.60542 560.7553,615.46875 526,615.46875 C 491.2447,615.46875 459.49026,602.60542 435.21875,581.375 C 457.05258,607.12155 489.62327,623.46875 526,623.46875 z"
+ id="path19425"
+ style="fill:url(#radialGradient19439);fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ d="M 523.53125,378.40625 L 523.53125,402.125 C 525.1769,402.07398 526.82304,402.09578 528.46875,402.125 L 528.46875,378.40625 L 523.53125,378.40625 z M 468.9375,393.40625 L 467.09375,394.46875 L 477.0625,411.75 C 477.67174,411.38819 478.28844,411.03612 478.90625,410.6875 L 468.9375,393.40625 z M 583.0625,393.40625 L 573.09375,410.6875 C 573.71156,411.03612 574.32826,411.38819 574.9375,411.75 L 584.90625,394.46875 L 583.0625,393.40625 z M 426.09375,435.46875 L 425.03125,437.3125 L 442.3125,447.28125 C 442.66112,446.66344 443.01319,446.04674 443.375,445.4375 L 426.09375,435.46875 z M 625.90625,435.46875 L 608.625,445.4375 C 608.98681,446.04674 609.33888,446.66344 609.6875,447.28125 L 626.96875,437.3125 L 625.90625,435.46875 z M 410.03125,491.90625 L 410.03125,496.84375 L 433.75,496.84375 C 433.69898,495.1981 433.72078,493.55196 433.75,491.90625 L 410.03125,491.90625 z M 618.25,491.90625 C 618.30102,493.5519 618.27922,495.19804 618.25,496.84375 L 641.96875,496.84375 L 641.96875,491.90625 L 618.25,491.90625 z M 442.3125,541.46875 L 425.03125,551.4375 L 426.09375,553.28125 L 443.375,543.3125 C 443.01319,542.70326 442.66112,542.08656 442.3125,541.46875 z M 609.6875,541.46875 C 609.33888,542.08656 608.98681,542.70326 608.625,543.3125 L 625.90625,553.28125 L 626.96875,551.4375 L 609.6875,541.46875 z M 477.0625,577 L 467.09375,594.28125 L 468.9375,595.34375 L 478.90625,578.0625 C 478.28844,577.71388 477.67174,577.36181 477.0625,577 z M 574.9375,577 C 574.32826,577.36181 573.71156,577.71388 573.09375,578.0625 L 583.0625,595.34375 L 584.90625,594.28125 L 574.9375,577 z M 523.53125,586.625 L 523.53125,610.34375 L 528.46875,610.34375 L 528.46875,586.625 C 526.8231,586.67602 525.17696,586.65422 523.53125,586.625 z"
+ id="path19427"
+ sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
+ style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
+ <path
+ d="M 440,288.36218 A 138,138 0 1 1 164,288.36218 A 138,138 0 1 1 440,288.36218 z"
+ id="path19429"
+ sodipodi:cx="302"
+ sodipodi:cy="288.36218"
+ sodipodi:rx="138"
+ sodipodi:ry="138"
+ sodipodi:type="arc"
+ style="fill:url(#linearGradient19441);fill-opacity:1;fill-rule:nonzero;stroke:#666666;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+ transform="matrix(0.728261,0,0,0.601449,306.065,286.927)" />
+ </g>
+ </g>
+</svg>
diff --git a/tests/constraintstests.py b/tests/constraintstests.py
index 4e19a92..a5ccf26 100644
--- a/tests/constraintstests.py
+++ b/tests/constraintstests.py
@@ -240,5 +240,47 @@ class FileConstraintTest(unittest.TestCase):
except FileConstraintError:
pass
+class ResourceConstraintTest(unittest.TestCase):
+ def test_valid_names(self):
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ cons = ResourceConstraint()
+
+ # All of those names should pass without exceptions
+ cons.validate(name1)
+ cons.validate(name2)
+ cons.validate(name3)
+ cons.validate(name4)
+ cons.validate(name5)
+
+ def test_invalid_names(self):
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ cons = ResourceConstraint()
+
+ try:
+ cons.validate(bad_name1)
+ assert False, "%s should not be a valid resource name" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name2)
+ assert False, "%s should not be a valid resource name" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name3)
+ assert False, "%s should not be a valid resource name" % bad_name3
+ except ResourceConstraintError:
+ pass
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/enginetests.py b/tests/enginetests.py
new file mode 100644
index 0000000..60a68f4
--- /dev/null
+++ b/tests/enginetests.py
@@ -0,0 +1,30 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import unittest
+
+import dbus
+
+session_bus = dbus.SessionBus()
+
+ENGINE_BUS_NAME = "org.tutorius.engine"
+
+LAUNCH_PATH = "/launch"
+STOP_PATH = "/stop"
+PAUSE_PATH = "/pause"
+
+class EngineInterfaceTests(unittest.TestCase):
+ pass
diff --git a/tests/inject.py b/tests/inject.py
new file mode 100644
index 0000000..d69d6ff
--- /dev/null
+++ b/tests/inject.py
@@ -0,0 +1,57 @@
+#Test event injection
+
+import gtk
+import gobject
+import time
+import types
+
+class ClickMaster():
+ def __init__(self):
+ self.event = None
+
+ def connect(self, button):
+ self.id = button.connect("pressed",self.capture_event)
+ self.id2 = button.connect("released",self.capture_event2)
+ self.id3 = button.connect("clicked",self.capture_event3)
+ self.button = button
+
+ def capture_event(self, *args):
+ print "Capture Event"
+ print args
+ self.eventPress = args[-1]
+ return False
+
+ def capture_event2(self, *args):
+ print "Capture Release"
+ print args
+ self.eventReleased = args[-1]
+ return False
+
+ def capture_event3(self, *args):
+ print "Capture Clicked"
+ print args
+ self.eventClicked = args[-1]
+ return False
+
+ def inject_event(self):
+ print "Injecting"
+ print self.event
+ #self.event.put()
+ self.button.emit("button_press_event", self.event)
+
+def print_Event(event):
+ for att in dir(event):
+ if not isinstance(att, types.MethodType):
+ print att, getattr(event, att)
+
+if __name__=='__main__':
+ w = gtk.Window()
+ b = gtk.CheckButton("Auto toggle!")
+ c=ClickMaster()
+ w.add(b)
+ b.show()
+ c.connect(b)
+
+ w.show()
+
+ gtk.main()
diff --git a/tests/propertiestests.py b/tests/propertiestests.py
index 2494ea6..f07bd43 100644
--- a/tests/propertiestests.py
+++ b/tests/propertiestests.py
@@ -579,6 +579,63 @@ class TAddonPropertyList(unittest.TestCase):
obj1.addonlist = [klass1(), klass1(), wrongAddon(), klass1()]
except ValueError:
pass
+
+class TResourcePropertyTest(unittest.TestCase):
+ def test_valid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ obj1 = klass1()
+
+ obj1.res = name1
+ assert obj1.res == name1, "Could not assign the valid name correctly : %s" % name1
+
+ obj1.res = name2
+ assert obj1.res == name2, "Could not assign the valid name correctly : %s" % name2
+
+ obj1.res = name3
+ assert obj1.res == name3, "Could not assign the valid name correctly : %s" % name3
+
+ obj1.res = name4
+ assert obj1.res == name4, "Could not assign the valid name correctly : %s" % name4
+
+ obj1.res = name5
+ assert obj1.res == name5, "Could not assign the valid name correctly : %s" % name5
+
+ def test_invalid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ obj1 = klass1()
+
+ try:
+ obj1.res = bad_name1
+ assert False, "A invalid name was accepted : %s" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name2
+ assert False, "A invalid name was accepted : %s" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name3
+ assert False, "A invalid name was accepted : %s" % bad_name3
+ except ResourceConstraintError:
+ pass
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/translatortests.py b/tests/translatortests.py
new file mode 100644
index 0000000..3b5ca6f
--- /dev/null
+++ b/tests/translatortests.py
@@ -0,0 +1,131 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import unittest
+import os
+import uuid
+
+from sugar.tutorius.translator import *
+from sugar.tutorius.properties import *
+from sugar.tutorius.tutorial import *
+from sugar.tutorius.vault import Vault
+from sugar.tutorius import addon
+
+##############################################################################
+## Helper classes
+class ResourceAction(TPropContainer):
+ resource = TResourceProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+class NestedResource(TPropContainer):
+ nested = TAddonProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested = ResourceAction()
+
+class ListResources(TPropContainer):
+ nested_list = TAddonListProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested_list = [ResourceAction(), ResourceAction()]
+
+##
+##############################################################################
+
+class ResourceTranslatorTests(unittest.TestCase):
+ temp_path = "/tmp/"
+ file_name = "file.txt"
+
+ def setUp(self):
+ # Generate a tutorial ID
+ self.tutorial_id = unicode(uuid.uuid1())
+
+ # Create a dummy fsm
+ self.fsm = Tutorial("TestTutorial1")
+ # Add a few states
+ act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450])
+ ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked")
+ act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2])
+ self.fsm.add_action("INIT", act1)
+ st2 = self.fsm.add_state((act2,))
+ self.fsm.add_transition("INIT",(ev1, st2))
+
+ # Create a dummy metadata dictionnary
+ self.test_metadata_dict = {}
+ self.test_metadata_dict['name'] = 'TestTutorial1'
+ self.test_metadata_dict['guid'] = unicode(self.tutorial_id)
+ self.test_metadata_dict['version'] = '1'
+ self.test_metadata_dict['description'] = 'This is a test tutorial 1'
+ self.test_metadata_dict['rating'] = '3.5'
+ self.test_metadata_dict['category'] = 'Test'
+ self.test_metadata_dict['publish_state'] = 'false'
+ activities_dict = {}
+ activities_dict['org.laptop.tutoriusactivity'] = '1'
+ activities_dict['org.laptop,writus'] = '1'
+ self.test_metadata_dict['activities'] = activities_dict
+
+ Vault.saveTutorial(self.fsm, self.test_metadata_dict)
+
+ try:
+ os.mkdir(self.temp_path)
+ except:
+ pass
+ abs_file_path = os.path.join(self.temp_path, self.file_name)
+ new_file = file(abs_file_path, "w")
+
+ # Add the resource in the Vault
+ self.res_name = Vault.add_resource(self.tutorial_id, abs_file_path)
+
+ # Use a dummy prob manager - we shouldn't be using it
+ self.prob_man = object()
+
+ self.translator = ResourceTranslator(self.prob_man, self.tutorial_id)
+
+ def tearDown(self):
+ Vault.deleteTutorial(self.tutorial_id)
+
+ os.unlink(os.path.join(self.temp_path, self.file_name))
+
+ def test_translate(self):
+ # Create an action with a resource property
+ res_action = ResourceAction()
+ res_action.resource = self.res_name
+
+ self.translator.translate(res_action)
+
+ assert getattr(res_action, "resource").type == "file", "Resource was not converted to file"
+
+ assert res_action.resource.default == Vault.get_resource_path(self.tutorial_id, self.res_name), "Transformed resource path is not the same as the one given by the vault"
+
+ def test_recursive_translate(self):
+ nested_action = NestedResource()
+
+ self.translator.translate(nested_action)
+
+ assert getattr(getattr(nested_action, "nested"), "resource").type == "file", "Nested resource was not converted properly"
+
+ def test_list_translate(self):
+ list_action = ListResources()
+
+ self.translator.translate(list_action)
+
+ for container in list_action.nested_list:
+ assert getattr(container, "resource").type == "file", "Element of list was not converted properly"
+
diff --git a/tests/vaulttests.py b/tests/vaulttests.py
index c6bd852..0af2d65 100644
--- a/tests/vaulttests.py
+++ b/tests/vaulttests.py
@@ -266,26 +266,17 @@ class VaultInterfaceTest(unittest.TestCase):
and return the new resource id. It also test the deletion of the resource.
"""
# Path of an image file in the test folder
- test_path = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp')
- if os.path.isdir(test_path) == True:
- shutil.rmtree(os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'tmp'))
- os.makedirs(test_path)
- file_path = os.path.join(test_path, 'test_resource.svg')
- resource_file = open(file_path, 'wt')
- resource_file.write('test')
- resource_file.close()
- #image_path = os.path.join(os.getcwd(), 'tests', 'resources', 'icon.svg')
- #assert os.path.isfile(image_path), 'cannot find the test image file'
-
+ image_path = os.path.join(os.getcwd(), 'tests', 'ressources', 'icon.svg')
+ assert os.path.isfile(image_path), 'cannot find the test image file'
+
# Create and save a tutorial
- tutorial = Tutorial('test')
- Vault.saveTutorial(tutorial, self.test_metadata_dict)
+ Vault.saveTutorial(self.fsm, self.test_metadata_dict)
bundler = TutorialBundler(self.save_test_guid)
tuto_path = bundler.get_tutorial_path(self.save_test_guid)
- # add the resource to the tutorial
- resource_id = Vault.add_resource(self.save_test_guid, file_path)
+ # add the ressource to the tutorial
+ ressource_id = Vault.add_resource(self.save_test_guid, image_path)
# Check that the image file is now in the vault
assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)), 'image file not found in vault'
diff --git a/tutorius/actions.py b/tutorius/actions.py
index bb15459..d5a8641 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -81,6 +81,7 @@ class DragWrapper(object):
"""Callback for end of drag (stolen focus)."""
self._dragging = False
+
def set_draggable(self, value):
"""Setter for the draggable property"""
if bool(value) ^ bool(self._drag_on):
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 519bce8..cd71167 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -24,6 +24,8 @@ for some properties.
# For the File Constraint
import os
+# For the Resource Constraint
+import re
class ConstraintException(Exception):
"""
@@ -214,3 +216,40 @@ class FileConstraint(Constraint):
raise FileConstraintError("Non-existing file : %s"%value)
return
+class ResourceConstraintError(ConstraintException):
+ pass
+
+class ResourceConstraint(Constraint):
+ """
+ Ensures that the value is looking like a resource name, like
+ <filename>_<GUID>[.<extension>]. We are not validating that this is a
+ valid resource for the reason that the property does not have any notion
+ of tutorial guid.
+
+ TODO : Find a way to properly validate resources by looking them up in the
+ Vault.
+ """
+
+ # Regular expression to parse a resource-like name
+ resource_regexp_text = "(.+)_([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})(\..*)?$"
+ resource_regexp = re.compile(resource_regexp_text)
+
+ def validate(self, value):
+ # TODO : Validate that we will not use an empty resource or if we can
+ # have transitory resource names
+ if value is None:
+ raise ResourceConstraintError("Resource not allowed to have a null value!")
+
+ # Special case : We allow the empty resource name for now
+ if value == "":
+ return value
+
+ # Attempt to see if the value has a resource name inside it
+ match = self.resource_regexp.search(value)
+
+ # If there was no match on the reg exp
+ if not match:
+ raise ResourceConstraintError("Resource name does not seem to be valid : %s" % value)
+
+ # If the name matched, then the value is _PROBABLY_ good
+ return value
diff --git a/tutorius/core.py b/tutorius/core.py
index bfbe07b..80e1b4f 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -616,3 +616,25 @@ class FiniteStateMachine(State):
# If we made it here, then all the states in this FSM could be matched to an
# identical state in the other FSM.
return True
+ if len(self._states) != len(otherFSM._states):
+ return False
+
+ # For each state, try to find a corresponding state in the other FSM
+ for state_name in self._states.keys():
+ state = self._states[state_name]
+ other_state = None
+ try:
+ # Attempt to use this key in the other FSM. If it's not present
+ # the dictionary will throw an exception and we'll know we have
+ # at least one different state in the other FSM
+ other_state = otherFSM._states[state_name]
+ except:
+ return False
+ # If two states with the same name exist, then we want to make sure
+ # they are also identical
+ if not state == other_state:
+ return False
+
+ # If we made it here, then all the states in this FSM could be matched to an
+ # identical state in the other FSM.
+ return True
diff --git a/tutorius/editor_interpreter.py b/tutorius/editor_interpreter.py
new file mode 100644
index 0000000..d559266
--- /dev/null
+++ b/tutorius/editor_interpreter.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2009, Tutorius.org
+# Greatly influenced by sugar/activity/namingalert.py
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+""" Tutorial Editor Interpreter Module
+"""
+
+import gtk
+import pango
+from sugar.tutorius.ipython_view import *
+
+from gettext import gettext as _
+
+class EditorInterpreter(gtk.Window):
+ """
+ Interpreter that will be shown to the user
+ """
+ __gtype_name__ = 'TutoriusEditorInterpreter'
+
+ def __init__(self, activity=None):
+ gtk.Window.__init__(self)
+
+ self._activity = activity
+
+ # Set basic properties of window
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(False)
+
+ # Connect to realize signal from ?
+ self.connect('realize', self.__realize_cb)
+
+ # Use an expander widget to allow minimizing
+ self._expander = gtk.Expander(_("Editor Interpreter"))
+ self._expander.set_expanded(True)
+ self.add(self._expander)
+ self._expander.connect("notify::expanded", self.__expander_cb)
+
+
+ # Use the IPython widget to embed
+ self.interpreter = IPythonView()
+ self.interpreter.set_wrap_mode(gtk.WRAP_CHAR)
+
+ # Expose the activity object in the interpreter
+ self.interpreter.updateNamespace({'activity':self._activity})
+
+ # Use a scroll window to permit scrolling of the interpreter prompt
+ swd = gtk.ScrolledWindow()
+ swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd.add(self.interpreter)
+ self.interpreter.show()
+
+ # Notify GTK that expander is ready
+ self._expander.add(swd)
+ self._expander.show()
+
+ # Notify GTK that the scrolling window is ready
+ swd.show()
+
+ def __expander_cb(self, *args):
+ """Callback for the window expander toggling"""
+ if self._expander.get_expanded():
+ self.__move_expanded()
+ else:
+ self.__move_collapsed()
+
+ def __move_expanded(self):
+ """Move the window to it's expanded position"""
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+ # leave room for the scrollbar at the right
+ width = swidth - 20
+ height = 200
+
+ self.set_size_request(width, height)
+ # Put at the bottom of the screen
+ self.move(0, sheight-height)
+
+ def __move_collapsed(self):
+ """Move the window to it's collapsed position"""
+ width = 150
+ height = 40
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+
+ self.set_size_request(width, height)
+ self.move(((swidth-width)/2)-150, sheight-height)
+
+ def __realize_cb(self, widget):
+ """Callback for realize"""
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+ self.__move_expanded()
diff --git a/tutorius/events.py b/tutorius/events.py
new file mode 100644
index 0000000..bf0a8b9
--- /dev/null
+++ b/tutorius/events.py
@@ -0,0 +1,36 @@
+from sugar.tutorius.properties import *
+
+class Event(TPropContainer):
+ source = TUAMProperty()
+ type = TStringProperty("clicked")
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+
+ # Providing the hash methods necessary to use events as key
+ # in a dictionary, if new properties are added we should
+ # take them into account here
+ def __hash__(self):
+ return hash(str(self.source) + str(self.type))
+
+ def __eq__(self, e2):
+ return self.source == e2.source and self.type == e2.type
+
+
+ # Adding methods for pickling and unpickling an object with
+ # properties
+ def __getstate__(self):
+ return self._props.copy()
+
+ def __setstate__(self, dict):
+ self._props.update(dict)
+
+
+# Nothing more needs to be added, the additional
+# information is in the object type
+class ClickedEvent(Event):
+ def __init__(self):
+ Event.__init__(self)
+ self.type = "clicked"
+
diff --git a/tutorius/ipython_view.py b/tutorius/ipython_view.py
new file mode 100644
index 0000000..c4294d0
--- /dev/null
+++ b/tutorius/ipython_view.py
@@ -0,0 +1,301 @@
+"""
+Backend to the console plugin.
+
+@author: Eitan Isaacson
+@organization: IBM Corporation
+@copyright: Copyright (c) 2007 IBM Corporation
+@license: BSD
+
+All rights reserved. This program and the accompanying materials are made
+available under the terms of the BSD which accompanies this distribution, and
+is available at U{http://www.opensource.org/licenses/bsd-license.php}
+"""
+# this file is a modified version of source code from the Accerciser project
+# http://live.gnome.org/accerciser
+
+import gtk
+import re
+import sys
+import os
+import pango
+from StringIO import StringIO
+
+try:
+ import IPython
+except Exception,e:
+ raise "Error importing IPython (%s)" % str(e)
+
+ansi_colors = {'0;30': 'Black',
+ '0;31': 'Red',
+ '0;32': 'Green',
+ '0;33': 'Brown',
+ '0;34': 'Blue',
+ '0;35': 'Purple',
+ '0;36': 'Cyan',
+ '0;37': 'LightGray',
+ '1;30': 'DarkGray',
+ '1;31': 'DarkRed',
+ '1;32': 'SeaGreen',
+ '1;33': 'Yellow',
+ '1;34': 'LightBlue',
+ '1;35': 'MediumPurple',
+ '1;36': 'LightCyan',
+ '1;37': 'White'}
+
+class IterableIPShell:
+ def __init__(self,argv=None,user_ns=None,user_global_ns=None,
+ cin=None, cout=None,cerr=None, input_func=None):
+ if input_func:
+ IPython.iplib.raw_input_original = input_func
+ if cin:
+ IPython.Shell.Term.cin = cin
+ if cout:
+ IPython.Shell.Term.cout = cout
+ if cerr:
+ IPython.Shell.Term.cerr = cerr
+
+ if argv is None:
+ argv=[]
+
+ # This is to get rid of the blockage that occurs during
+ # IPython.Shell.InteractiveShell.user_setup()
+ IPython.iplib.raw_input = lambda x: None
+
+ self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
+ os.environ['TERM'] = 'dumb'
+ excepthook = sys.excepthook
+ self.IP = IPython.Shell.make_IPython(argv,user_ns=user_ns,
+ user_global_ns=user_global_ns,
+ embedded=True,
+ shell_class=IPython.Shell.InteractiveShell)
+ self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
+ header='IPython system call: ',
+ verbose=self.IP.rc.system_verbose)
+ sys.excepthook = excepthook
+ self.iter_more = 0
+ self.history_level = 0
+ self.complete_sep = re.compile('[\s\{\}\[\]\(\)]')
+
+ def execute(self):
+ self.history_level = 0
+ orig_stdout = sys.stdout
+ sys.stdout = IPython.Shell.Term.cout
+ try:
+ line = self.IP.raw_input(None, self.iter_more)
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(None)
+ except KeyboardInterrupt:
+ self.IP.write('\nKeyboardInterrupt\n')
+ self.IP.resetbuffer()
+ # keep cache in sync with the prompt counter:
+ self.IP.outputcache.prompt_count -= 1
+
+ if self.IP.autoindent:
+ self.IP.indent_current_nsp = 0
+ self.iter_more = 0
+ except:
+ self.IP.showtraceback()
+ else:
+ self.iter_more = self.IP.push(line)
+ if (self.IP.SyntaxTB.last_syntax_error and
+ self.IP.rc.autoedit_syntax):
+ self.IP.edit_syntax_error()
+ if self.iter_more:
+ self.prompt = str(self.IP.outputcache.prompt2).strip()
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(self.IP.pre_readline)
+ else:
+ self.prompt = str(self.IP.outputcache.prompt1).strip()
+ sys.stdout = orig_stdout
+
+ def historyBack(self):
+ self.history_level -= 1
+ return self._getHistory()
+
+ def historyForward(self):
+ self.history_level += 1
+ return self._getHistory()
+
+ def _getHistory(self):
+ try:
+ rv = self.IP.user_ns['In'][self.history_level].strip('\n')
+ except IndexError:
+ self.history_level = 0
+ rv = ''
+ return rv
+
+ def updateNamespace(self, ns_dict):
+ self.IP.user_ns.update(ns_dict)
+
+ def complete(self, line):
+ split_line = self.complete_sep.split(line)
+ possibilities = self.IP.complete(split_line[-1])
+ if possibilities:
+ common_prefix = reduce(self._commonPrefix, possibilities)
+ completed = line[:-len(split_line[-1])]+common_prefix
+ else:
+ completed = line
+ return completed, possibilities
+
+ def _commonPrefix(self, str1, str2):
+ for i in range(len(str1)):
+ if not str2.startswith(str1[:i+1]):
+ return str1[:i]
+ return str1
+
+ def shell(self, cmd,verbose=0,debug=0,header=''):
+ stat = 0
+ if verbose or debug: print header+cmd
+ # flush stdout so we don't mangle python's buffering
+ if not debug:
+ input, output = os.popen4(cmd)
+ print output.read()
+ output.close()
+ input.close()
+
+class ConsoleView(gtk.TextView):
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ self.modify_font(pango.FontDescription('Mono'))
+ self.set_cursor_visible(True)
+ self.text_buffer = self.get_buffer()
+ self.mark = self.text_buffer.create_mark('scroll_mark',
+ self.text_buffer.get_end_iter(),
+ False)
+ for code in ansi_colors:
+ self.text_buffer.create_tag(code,
+ foreground=ansi_colors[code],
+ weight=700)
+ self.text_buffer.create_tag('0')
+ self.text_buffer.create_tag('notouch', editable=False)
+ self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
+ self.line_start = \
+ self.text_buffer.create_mark('line_start',
+ self.text_buffer.get_end_iter(), True
+ )
+ self.connect('key-press-event', self._onKeypress)
+ self.last_cursor_pos = 0
+
+ def write(self, text, editable=False):
+ segments = self.color_pat.split(text)
+ segment = segments.pop(0)
+ start_mark = self.text_buffer.create_mark(None,
+ self.text_buffer.get_end_iter(),
+ True)
+ self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
+
+ if segments:
+ ansi_tags = self.color_pat.findall(text)
+ for tag in ansi_tags:
+ i = segments.index(tag)
+ self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
+ segments[i+1], tag)
+ segments.pop(i)
+ if not editable:
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(start_mark),
+ self.text_buffer.get_end_iter())
+ self.text_buffer.delete_mark(start_mark)
+ self.scroll_mark_onscreen(self.mark)
+
+ def showPrompt(self, prompt):
+ self.write(prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+
+ def changeLine(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
+ self.write(text, True)
+
+ def getCurrentLine(self):
+ rv = self.text_buffer.get_slice(self.text_buffer.get_iter_at_mark(self.line_start),
+ self.text_buffer.get_end_iter(), False)
+ return rv
+
+ def showReturned(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(self.line_start),
+ iter)
+ self.write('\n'+text)
+ if text:
+ self.write('\n')
+ self.showPrompt(self.prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+ self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
+
+ def _onKeypress(self, obj, event):
+ if not event.string:
+ return
+ insert_mark = self.text_buffer.get_insert()
+ insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
+ selection_mark = self.text_buffer.get_selection_bound()
+ selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
+ start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ if start_iter.compare(insert_iter) <= 0 and \
+ start_iter.compare(selection_iter) <= 0:
+ return
+ elif start_iter.compare(insert_iter) > 0 and \
+ start_iter.compare(selection_iter) > 0:
+ self.text_buffer.place_cursor(start_iter)
+ elif insert_iter.compare(selection_iter) < 0:
+ self.text_buffer.move_mark(insert_mark, start_iter)
+ elif insert_iter.compare(selection_iter) > 0:
+ self.text_buffer.move_mark(selection_mark, start_iter)
+
+
+class IPythonView(ConsoleView, IterableIPShell):
+ def __init__(self):
+ ConsoleView.__init__(self)
+ self.cout = StringIO()
+ IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
+ input_func=self.raw_input)
+ self.connect('key_press_event', self.keyPress)
+ self.execute()
+ self.cout.truncate(0)
+ self.showPrompt(self.prompt)
+ self.interrupt = False
+
+ def raw_input(self, prompt=''):
+ if self.interrupt:
+ self.interrupt = False
+ raise KeyboardInterrupt
+ return self.getCurrentLine()
+
+ def keyPress(self, widget, event):
+ if event.state & gtk.gdk.CONTROL_MASK and event.keyval == 99:
+ self.interrupt = True
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Return:
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Up:
+ self.changeLine(self.historyBack())
+ return True
+ elif event.keyval == gtk.keysyms.Down:
+ self.changeLine(self.historyForward())
+ return True
+ elif event.keyval == gtk.keysyms.Tab:
+ if not self.getCurrentLine().strip():
+ return False
+ completed, possibilities = self.complete(self.getCurrentLine())
+ if len(possibilities) > 1:
+ slice = self.getCurrentLine()
+ self.write('\n')
+ for symbol in possibilities:
+ self.write(symbol+'\n')
+ self.showPrompt(self.prompt)
+ self.changeLine(completed or slice)
+ return True
+
+ def _processLine(self):
+ self.history_pos = 0
+ self.execute()
+ rv = self.cout.getvalue()
+ if rv: rv = rv.strip('\n')
+ self.showReturned(rv)
+ self.cout.truncate(0)
+
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 5422532..c7af821 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -24,7 +24,8 @@ from copy import copy, deepcopy
from .constraints import Constraint, \
UpperLimitConstraint, LowerLimitConstraint, \
MaxSizeConstraint, MinSizeConstraint, \
- ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint
+ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \
+ ResourceConstraint
class TPropContainer(object):
"""
@@ -89,6 +90,21 @@ class TPropContainer(object):
except AttributeError:
return object.__setattr__(self, name, value)
+ def replace_property(self, prop_name, new_prop):
+ """
+ Changes the content of a property. This is done in order to support
+ the insertion of executable properties in the place of a portable
+ property. The typical exemple is that a resource property needs to
+ be changed into a file property with the correct file name, since the
+ installation location will be different on every platform.
+
+ @param prop_name The name of the property to be changed
+ @param new_prop The new property to insert
+ @raise AttributeError of the mentionned property doesn't exist
+ """
+ props = object.__getattribute__(self, "_props")
+ props.__setitem__(prop_name, new_prop)
+
def get_properties(self):
"""
Return the list of property names.
@@ -261,8 +277,6 @@ class TFileProperty(TutoriusProperty):
For now, the path may be relative or absolute, as long as it exists on
the local machine.
- TODO : Make sure that we have a file scheme that supports distribution
- on other computers (LP 355197)
"""
TutoriusProperty.__init__(self)
@@ -351,7 +365,7 @@ class TEventType(TutoriusProperty):
class TAddonListProperty(TutoriusProperty):
"""
- Reprensents an embedded tutorius Addon List Component.
+ Represents an embedded tutorius Addon List Component.
See TAddonProperty
"""
def __init__(self):
@@ -367,3 +381,34 @@ class TAddonListProperty(TutoriusProperty):
return value
raise ValueError("Value proposed to TAddonListProperty is not a list")
+class TResourceProperty(TutoriusProperty):
+ """
+ Represents a resource in the tutorial. A resource is a file with a specific
+ name that exists under the tutorials folder. It is distributed alongside the
+ tutorial itself.
+
+ When the system encounters a resource, it knows that it refers to a file in
+ the resource folder and that it should translate this resource name to an
+ absolute file name before it is executed.
+
+ E.g. An image is added to a tutorial in an action. On doing so, the creator
+ adds a resource to the tutorial, then saves its name in the resource
+ property of that action. When this tutorial is executed, the Engine
+ replaces all the TResourceProperties inside the action by their equivalent
+ TFileProperties with absolute paths, so that they can be used on any
+ machine.
+ """
+ def __init__(self, resource_name=""):
+ """
+ Creates a new resource pointing to an existing resource.
+
+ @param resource_name The file name of the resource (should be only the
+ file name itself, no directory information)
+ """
+ TutoriusProperty.__init__(self)
+ self.type = "resource"
+
+ self.resource_cons = ResourceConstraint()
+
+ self.default = self.validate("")
+
diff --git a/tutorius/translator.py b/tutorius/translator.py
new file mode 100644
index 0000000..626e2c9
--- /dev/null
+++ b/tutorius/translator.py
@@ -0,0 +1,186 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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 2 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import logging
+import copy
+
+logger = logging.getLogger("ResourceTranslator")
+
+from .properties import *
+# TODO : Uncomment this line upon integration with the Vault
+from .vault import Vault
+
+class ResourceTranslator(object):
+ """
+ Handles the conversion of resource properties into file
+ properties before action execution. This class works as a decorator
+ to the ProbeManager class, as it is meant to be a transparent layer
+ before sending the action to execution.
+
+ An architectural note : every different type of translation should have its
+ own method (translate_resource, etc...), and this function must be called
+ from the translate method, under the type test. The translate_* method
+ must take in the input property and give the output property that should
+ replace it.
+ """
+
+ def __init__(self, probe_manager, tutorial_id):
+ """
+ Creates a new ResourceTranslator for the given tutorial. This
+ translator is tasked with replacing resource properties of the
+ incoming action into actually usable file properties pointing
+ to the correct resource file. This is done by querying the vault
+ for all the resources and creating a new file property from the
+ returned path.
+
+ @param probe_manager The probe manager to decorate
+ @param tutorial_id The ID of the current tutorial
+ """
+ self._probe_manager = probe_manager
+ self._tutorial_id = tutorial_id
+
+ def translate_resource(self, res_value):
+ """
+ Replace the TResourceProperty in the container by their
+ runtime-defined file equivalent. Since the resources are stored
+ in a relative manner in the vault and that only the Vault knows
+ which is the current folder for the current tutorial, it needs
+ to transform the resource identifier into the absolute path for
+ the process to be able to use it properly.
+
+ @param res_prop The resource property's value to be translated
+ @return The TFileProperty corresponding to this resource, containing
+ an absolute path to the resource
+ """
+ # We need to replace the resource by a file representation
+ filepath = Vault.get_resource_path(self._tutorial_id, res_value)
+
+ # Create the new file representation
+ file_prop = TFileProperty(filepath)
+
+ return file_prop
+
+ def translate(self, prop_container):
+ """
+ Applies the required translation to be able to send the container to an
+ executing endpoint (like a Probe). For each type of property that
+ requires it, there is translation function that will take care of
+ mapping the property to its executable form.
+
+ This function does not return anything, but its post-condition is
+ that all the properties of the input container have been replaced
+ by their corresponding executable versions.
+
+ An example of translation is taking a resource (a relative path to
+ a file under the tutorial folder) and transforming it into a file
+ (a full absolute path) to be able to load it when the activity receives
+ the action.
+
+ @param prop_container The property container in which we want to
+ replace all the resources for file properties and
+ to recursively do so for addon and addonlist
+ properties.
+ """
+ for propname in prop_container.get_properties():
+ prop_value = getattr(prop_container, propname)
+ prop_type = getattr(type(prop_container), propname).type
+
+ # If the property is a resource, then we need to query the
+ # vault to create its correspondent
+ if prop_type == "resource":
+ # Apply the translation
+ file_prop = self.translate_resource(prop_value)
+ # Set the property with the new value
+ prop_container.replace_property(propname, file_prop)
+
+ # If the property is an addon, then its value IS a
+ # container too - we need to translate it
+ elif prop_type == "addon":
+ # Translate the sub properties
+ self.translate(prop_value)
+
+ # If the property is an addon list, then we need to translate all
+ # the elements of the list
+ elif prop_type == "addonlist":
+ # Now, for each sub-container in the list, we will apply the
+ # translation processing. This is done by popping the head of
+ # the list, translating it and inserting it back at the end.
+ for index in range(0, len(prop_value)):
+ # Pop the head of the list
+ container = prop_value[0]
+ del prop_value[0]
+ # Translate the sub-container
+ self.translate(container)
+
+ # Put the value back in the list
+ prop_value.append(container)
+ # Change the list contained in the addonlist property, since
+ # we got a copy of the list when requesting it
+ prop_container.replace_property(propname, prop_value)
+
+ ### ProbeManager interface for decorator ###
+
+ ## Unchanged functions ##
+ def setCurrentActivity(self, activity_id):
+ self._probe_manager.setCurrentActivity(activity_id)
+
+ def getCurrentActivity(self):
+ return self._probe_manager.getCurrentActivity()
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ self._probe_manager.attach(activity_id)
+
+ def detach(self, activity_id):
+ self._probe_manager.detach(activity_id)
+
+ def subscribe(self, event, callback):
+ return self._probe_manager.subscribe(event, callback)
+
+ def unsubscribe(self, event, callback):
+ return self._probe_manager.unsubscribe(event, callback)
+
+ def unsubscribe_all(self):
+ return self._probe_manager.unsubscribe_all()
+
+ ## Decorated functions ##
+ def install(self, action):
+ # Make a new copy of the action that we want to install,
+ # because translate() changes the action and we
+ # don't want to modify the caller's action representation
+ new_action = copy.deepcopy(action)
+ # Execute the replacement
+ self.translate(new_action)
+
+ # Send the new action to the probe manager
+ return self._probe_manager.install(new_action)
+
+ def update(self, action):
+ new_action = copy.deepcopy(action)
+ self.translate(new_action)
+
+ return self._probe_manager.update(new_action)
+
+ def uninstall(self, action):
+ new_action = copy.deepcopy(action)
+ self.translate(new_action)
+
+ return self._probe_manager.uninstall(new_action)
+
+ def uninstall_all(self):
+ return self._probe_manager.uninstall_all()
+
diff --git a/tutorius/vault.py b/tutorius/vault.py
index 7ec0a23..4d817ec 100644
--- a/tutorius/vault.py
+++ b/tutorius/vault.py
@@ -325,14 +325,14 @@ class Vault(object):
@staticmethod
- def deleteTutorial(Guid):
+ def deleteTutorial(tutorial_id):
"""
Removes the tutorial from the Vault. It will unpublish the tutorial if need be,
and it will also wipe it from the persistent storage.
@returns true is the tutorial was deleted from the Vault
"""
- bundle = TutorialBundler(Guid)
- bundle_path = bundle.get_tutorial_path(Guid)
+ bundle = TutorialBundler(tutorial_id)
+ bundle_path = bundle.get_tutorial_path(tutorial_id)
# TODO : Need also to unpublish tutorial, need to interact with webservice module
@@ -982,9 +982,3 @@ class TutorialBundler(object):
xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY)
serializer.save_tutorial(fsm, xml_filename, self.Path)
- @staticmethod
- def add_resources(typename, file):
- """
- Add resources to metadata.
- """
- raise NotImplementedError("add_resources not implemented")