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/inject.py57
-rw-r--r--tests/probetests.py64
-rw-r--r--tests/propertiestests.py57
-rw-r--r--tests/translatortests.py131
-rw-r--r--tests/vaulttests.py21
-rw-r--r--tutorius/TProbe.py46
-rw-r--r--tutorius/actions.py1
-rw-r--r--tutorius/constraints.py39
-rw-r--r--tutorius/core.py618
-rw-r--r--tutorius/editor_interpreter.py105
-rw-r--r--tutorius/engine.py37
-rw-r--r--tutorius/events.py36
-rw-r--r--tutorius/ipython_view.py301
-rw-r--r--tutorius/properties.py53
-rw-r--r--tutorius/translator.py189
-rw-r--r--tutorius/tutorial.py8
-rw-r--r--tutorius/vault.py25
25 files changed, 2845 insertions, 719 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/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/probetests.py b/tests/probetests.py
index 59072e5..8321e19 100644
--- a/tests/probetests.py
+++ b/tests/probetests.py
@@ -20,6 +20,7 @@ Probe Tests
import unittest
import pickle
+import functools
from dbus.mainloop.glib import DBusGMainLoop
from dbus.mainloop import NULL_MAIN_LOOP
@@ -85,28 +86,37 @@ class MockProbeProxy(object):
@param activityName unique activity id. Must be a valid dbus bus name.
"""
self.MockAction = None
+ self.MockActionAddress = None
self.MockActionUpdate = None
self.MockEvent = None
self.MockCB = None
self.MockAlive = True
self.MockEventAddr = None
+
+ self._address_nb = 0
def isAlive(self):
return self.MockAlive
- def install(self, action, block=False):
+ def install(self, action, callback, block=False):
self.MockAction = action
+ action_name = 'new_action' + str(self._address_nb)
+ self._address_nb = self._address_nb + 1
+ callback(action_name)
+
self.MockActionUpdate = None
return None
- def update(self, action, newaction, block=False):
- self.MockAction = action
+ def update(self, action_address, newaction, block=False):
+ self.MockActionAddress = action_address
self.MockActionUpdate = newaction
return None
- def uninstall(self, action, block=False):
- self.MockAction = None
- self.MockActionUpdate = None
+ def uninstall(self, action_address, block=False):
+ if self.MockActionAddress == action_address:
+ self.MockActionAddress = None
+ self.MockAction = None
+ self.MockActionUpdate = None
return None
def subscribe(self, event, callback, block=True):
@@ -124,11 +134,13 @@ class MockProbeProxy(object):
def detach(self, block=False):
self.MockAction = None
+ self.MockActionAddress = None
self.MockActionUpdate = None
self.MockEvent = None
self.MockCB = None
self.MockAlive = False
self.MockEventAddr = None
+ self._address_nb = 0
return None
class MockProxyObject(object):
@@ -183,6 +195,9 @@ class ProbeTest(unittest.TestCase):
self.activity = MockActivity()
self.probe = TProbe(self.activity, MockServiceProxy())
+ # Assigned addresses for the registered actions
+ self._registered_actions = {}
+
#Override the eventOccured on the Probe...
self.old_eO = self.probe.eventOccured
def newEo(event):
@@ -223,12 +238,16 @@ class ProbeTest(unittest.TestCase):
address = self.probe.install(pickle.dumps(action))
assert type(address) == str, "install should return a string"
assert message_box == (5, "woot"), "message box should have (i, s)"
+ #assert self._registered_actions['action1'] == 'new_action0', "Callback should give back the address"
+ #address = self._registered_actions['action1']
#install 2
action.i, action.s = (10, "ahhah!")
address2 = self.probe.install(pickle.dumps(action))
assert message_box == (10, "ahhah!"), "message box should have changed"
assert address != address2, "action addresses should be different"
+ #assert self._registered_actions['action2'] == 'new_action1', "Callback should give back the address"
+ #address2 = self._registered_actions['action2']
#uninstall 2
self.probe.uninstall(address2)
@@ -297,6 +316,7 @@ class ProbeManagerTest(unittest.TestCase):
def setUp(self):
MockProbeProxy._MockProxyCache = {}
self.probeManager = ProbeManager(proxy_class=MockProbeProxy)
+ self._registered_actions = {}
def test_register_probe(self):
assert len(self.probeManager.get_registered_probes_list()) == 0
@@ -336,6 +356,9 @@ class ProbeManagerTest(unittest.TestCase):
assert len(self.probeManager.get_registered_probes_list("act1")) == 0
assert self.probeManager.get_registered_probes_list("act1") == []
+ def _register_action(self, action_name, action_address):
+ self._registered_actions[action_name] = action_address
+
def test_actions(self):
self.probeManager.register_probe("act1", "unique_id_1")
self.probeManager.register_probe("act2", "unique_id_2")
@@ -345,27 +368,28 @@ class ProbeManagerTest(unittest.TestCase):
ad1 = MockAddon()
#ErrorCase: install, update, uninstall without currentActivity
#Action functions should do a warning if there is no activity
- self.assertRaises(RuntimeWarning, self.probeManager.install, ad1)
- self.assertRaises(RuntimeWarning, self.probeManager.update, ad1, ad1)
- self.assertRaises(RuntimeWarning, self.probeManager.uninstall, ad1)
+ self.assertRaises(RuntimeWarning, self.probeManager.install, ad1, functools.partial(self._register_action, "action1"))
+ self.assertRaises(RuntimeWarning, self.probeManager.update, "No Name", ad1)
+ self.assertRaises(RuntimeWarning, self.probeManager.uninstall, "No Name")
assert act1.MockAction is None, "Action should not be installed on inactive proxy"
assert act2.MockAction is None, "Action should not be installed on inactive proxy"
self.probeManager.currentActivity = "act1"
- self.probeManager.install(ad1)
+ self.probeManager.install(ad1, functools.partial(self._register_action, "action1"))
assert act1.MockAction == ad1, "Action should have been installed"
+ assert self._registered_actions["action1"] == 'new_action0', "Address for the action should have been registered"
assert act2.MockAction is None, "Action should not be installed on inactive proxy"
- self.probeManager.update(ad1, ad1)
+ self.probeManager.update(self._registered_actions["action1"], ad1)
assert act1.MockActionUpdate == ad1, "Action should have been updated"
assert act2.MockActionUpdate is None, "Should not update on inactive"
self.probeManager.currentActivity = "act2"
- self.probeManager.uninstall(ad1)
+ self.probeManager.uninstall(self._registered_actions["action1"])
assert act1.MockAction == ad1, "Action should still be installed"
self.probeManager.currentActivity = "act1"
- self.probeManager.uninstall(ad1)
+ self.probeManager.uninstall(self._registered_actions["action1"])
assert act1.MockAction is None, "Action should be uninstalled"
def test_events(self):
@@ -406,6 +430,8 @@ class ProbeProxyTest(unittest.TestCase):
self.mockObj = MockProxyObject("unittest.TestCase", "/tutorius/Probe/unique_id_1")
self.probeProxy = ProbeProxy("unittest.TestCase", "unique_id_1")
+ self._registered_actions = {}
+
def tearDown(self):
dbus.SessionBus = old_SessionBus
MockProxyObject._MockProxyObjects = {}
@@ -417,6 +443,9 @@ class ProbeProxyTest(unittest.TestCase):
self.mockObj.MockRet["ping"] = "anything else"
assert self.probeProxy.isAlive() == False, "Alive should return False"
+ def _register_action(self, action_name, action_address):
+ self._registered_actions[action_name] = action_address
+
def test_actions(self):
action = MockAddon()
action.i, action.s = 5, "action"
@@ -427,24 +456,25 @@ class ProbeProxyTest(unittest.TestCase):
address = "Addr1"
#Set the return value of probe install
self.mockObj.MockRet["install"] = address
- self.probeProxy.install(action, block=True)
+ callback = functools.partial(self._register_action, "action1")
+ self.probeProxy.install(action, callback, block=True)
assert pickle.loads(self.mockObj.MockCall["install"]["args"][0]) == action, "1 argument, the action"
#ErrorCase: Update should fail on noninstalled actions
self.assertRaises(RuntimeWarning, self.probeProxy.update, action2, action2, block=True)
#Test the update
- self.probeProxy.update(action, action2, block=True)
+ self.probeProxy.update(address, action2, block=True)
args = self.mockObj.MockCall["update"]["args"]
assert args[0] == address, "arg 1 should be the action address"
assert pickle.loads(args[1]) == action2._props, "arg2 should be the new action properties"
#ErrorCase: Uninstall on not installed action (silent fail)
#Test the uninstall
- self.probeProxy.uninstall(action2, block=True)
+ self.probeProxy.uninstall("wrong address", block=True)
assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed"
- self.probeProxy.uninstall(action, block=True)
+ self.probeProxy.uninstall(address, block=True)
assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address"
def test_events(self):
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..9bd0525 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)
+ resource_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'
@@ -303,8 +294,6 @@ class VaultInterfaceTest(unittest.TestCase):
# Check that the resource is not in the vault anymore
assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)) == False, 'image file found in vault when it should have been deleted.'
-
-
def tearDown(self):
folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data');
for file in os.listdir(folder):
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
index 0c79690..cfa734b 100644
--- a/tutorius/TProbe.py
+++ b/tutorius/TProbe.py
@@ -297,46 +297,47 @@ class ProbeProxy:
except:
return False
- def __update_action(self, action, address):
+ def __update_action(self, action, callback, address):
LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
- self._actions[action] = str(address)
+ callback(address)
- def __clear_action(self, action):
- self._actions.pop(action, None)
+ def __clear_action(self, action_address):
+ self._actions.pop(action_address, None)
- def install(self, action, block=False):
+ def install(self, action, callback, block=False):
"""
Install an action on the TProbe's activity
@param action Action to install
+ @param callback The function to call to propagate the address
@param block Force a synchroneous dbus call if True
@return None
"""
return remote_call(self._probe.install, (pickle.dumps(action),),
- save_args(self.__update_action, action),
+ save_args(self.__update_action, action, callback),
block=block)
- def update(self, action, newaction, block=False):
+ def update(self, action_address, newaction, block=False):
"""
Update an already installed action's properties and run it again
- @param action Action to update
+ @param action_name The name of the action to update
@param newaction Action to update it with
@param block Force a synchroneous dbus call if True
@return None
"""
#TODO review how to make this work well
- if not action in self._actions:
+ if not action_address in self._actions.keys():
raise RuntimeWarning("Action not installed")
#TODO Check error handling
- return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block)
+ return remote_call(self._probe.update, (self._actions[action_address], pickle.dumps(newaction._props)), block=block)
- def uninstall(self, action, block=False):
+ def uninstall(self, action_address, block=False):
"""
Uninstall an installed action
- @param action Action to uninstall
+ @param action_name The name of the action to uninstall
@param block Force a synchroneous dbus call if True
"""
- if action in self._actions:
- remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block)
+ if action_name in self._actions.keys():
+ remote_call(self._probe.uninstall,(self._actions.pop(action_name),), block=block)
def __update_event(self, event, callback, address):
LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
@@ -400,7 +401,7 @@ class ProbeProxy:
# TODO elavoie 2009-07-25 When we will allow for patterns both
# for event types and sources, we will need to revise the lookup
- # mecanism for which callback function to call
+ # mechanism for which callback function to call
return remote_call(self._probe.subscribe, (pickle.dumps(event),),
save_args(self.__update_event, event, callback),
block=block)
@@ -465,7 +466,7 @@ class ProbeManager(object):
currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
- def install(self, action, block=False):
+ def install(self, action_name, action, block=False):
"""
Install an action on the current activity
@param action Action to install
@@ -473,11 +474,11 @@ class ProbeManager(object):
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).install(action, block)
+ return self._first_proxy(self.currentActivity).install(action_name, action, block)
else:
raise RuntimeWarning("No activity attached")
- def update(self, action, newaction, block=False):
+ def update(self, action_name, newaction, block=False):
"""
Update an already installed action's properties and run it again
@param action Action to update
@@ -486,18 +487,18 @@ class ProbeManager(object):
@return None
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).update(action, newaction, block)
+ return self._first_proxy(self.currentActivity).update(action_name, newaction, block)
else:
raise RuntimeWarning("No activity attached")
- def uninstall(self, action, block=False):
+ def uninstall(self, action_name, block=False):
"""
Uninstall an installed action
@param action Action to uninstall
@param block Force a synchroneous dbus call if True
"""
if self.currentActivity:
- return self._first_proxy(self.currentActivity).uninstall(action, block)
+ return self._first_proxy(self.currentActivity).uninstall(action_name, block)
else:
raise RuntimeWarning("No activity attached")
@@ -541,7 +542,6 @@ class ProbeManager(object):
self._probes[process_name] = [(unique_id,self._ProxyClass(process_name, unique_id))]
else:
self._probes[process_name].append((unique_id,self._ProxyClass(process_name, unique_id)))
-
def unregister_probe(self, unique_id):
""" Remove a probe from the known probes.
@@ -570,8 +570,6 @@ class ProbeManager(object):
else:
return []
-
-
def _first_proxy(self, process_name):
"""
Returns the oldest probe connected under the process_name
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
deleted file mode 100644
index bfbe07b..0000000
--- a/tutorius/core.py
+++ /dev/null
@@ -1,618 +0,0 @@
-# Copyright (C) 2009, Tutorius.org
-# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com>
-#
-# 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
-"""
-Core
-
-This module contains the core classes for tutorius
-
-"""
-
-import logging
-import os
-
-from .TProbe import ProbeManager
-from .dbustools import save_args
-from . import addon
-
-logger = logging.getLogger("tutorius")
-
-class Tutorial (object):
- """
- Tutorial Class, used to run through the FSM.
- """
- #Properties
- probeManager = property(lambda self: self._probeMgr)
- activityId = property(lambda self: self._activity_id)
-
- def __init__(self, name, fsm, filename=None):
- """
- Creates an unattached tutorial.
- """
- object.__init__(self)
- self.name = name
- self.activity_init_state_filename = filename
-
- self.state_machine = fsm
- self.state_machine.set_tutorial(self)
-
- self.state = None
-
- self.handlers = []
- self._probeMgr = ProbeManager()
- self._activity_id = None
- #Rest of initialisation happens when attached
-
- def attach(self, activity_id):
- """
- Attach to a running activity
-
- @param activity_id the id of the activity to attach to
- """
- #For now, absolutely detach if a previous one!
- if self._activity_id:
- self.detach()
- self._activity_id = activity_id
- self._probeMgr.attach(activity_id)
- self._probeMgr.currentActivity = activity_id
- self._prepare_activity()
- self.state_machine.set_state("INIT")
-
- def detach(self):
- """
- Detach from the current activity
- """
-
- # Uninstall the whole FSM
- self.state_machine.teardown()
-
- if not self._activity_id is None:
- self._probeMgr.detach(self._activity_id)
- self._activity_id = None
-
- def set_state(self, name):
- """
- Switch to a new state
- """
- logger.debug("==== NEW STATE: %s ====" % name)
-
- self.state_machine.set_state(name)
-
- def _prepare_activity(self):
- """
- Prepare the activity for the tutorial by loading the saved state and
- emitting gtk signals
- """
- #Load the saved activity if any
- if self.activity_init_state_filename is not None:
- #For now the file will be saved in the data folder
- #of the activity root directory
- filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\
- self.activity_init_state_filename
- readfile = addon.create("ReadFile", filename=filename)
- if readfile:
- self._probeMgr.install(readfile)
- #Uninstall now while we have the reference handy
- self._probeMgr.uninstall(readfile)
-
-class State(object):
- """
- This is a step in a tutorial. The state represents a collection of actions
- to undertake when entering the state, and a series of event filters
- with associated actions that point to a possible next state.
- """
-
- def __init__(self, name="", action_list=None, event_filter_list=None, tutorial=None):
- """
- Initializes the content of the state, like loading the actions
- that are required and building the correct tests.
-
- @param action_list The list of actions to execute when entering this
- state
- @param event_filter_list A list of tuples of the form
- (event_filter, next_state_name), that explains the outgoing links for
- this state
- @param tutorial The higher level container of the state
- """
- object.__init__(self)
-
- self.name = name
-
- self._actions = action_list or []
-
- self._transitions= dict(event_filter_list or [])
-
- self._installedEvents = set()
-
- self.tutorial = tutorial
-
- def set_tutorial(self, tutorial):
- """
- Associates this state with a tutorial. A tutorial must be set prior
- to executing anything in the state. The reason for this is that the
- states need to have access to the activity (via the tutorial) in order
- to properly register their callbacks on the activities' widgets.
-
- @param tutorial The tutorial that this state runs under.
- """
- if self.tutorial == None :
- self.tutorial = tutorial
- else:
- raise RuntimeWarning(\
- "The state %s was already associated with a tutorial." % self.name)
-
- def setup(self):
- """
- Install the state itself, by first registering the event filters
- and then triggering the actions.
- """
- for (event, next_state) in self._transitions.items():
- self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state )))
-
- for action in self._actions:
- self.tutorial.probeManager.install(action)
-
- def teardown(self):
- """
- Uninstall all the event filters that were active in this state.
- Also undo every action that was installed for this state. This means
- removing dialogs that were displayed, removing highlights, etc...
- """
- # Remove the handlers for the all of the state's event filters
- while len(self._installedEvents) > 0:
- self.tutorial.probeManager.unsubscribe(self._installedEvents.pop())
-
- # Undo all the actions related to this state
- for action in self._actions:
- self.tutorial.probeManager.uninstall(action)
-
- def _event_filter_state_done_cb(self, next_state, event):
- """
- Callback for event filters. This function needs to inform the
- tutorial that the state is over and tell it what is the next state.
-
- @param next_state The next state for the transition
- @param event The event that occured
- """
- # Run the tests here, if need be
-
- # Warn the higher level that we wish to change state
- self.tutorial.set_state(next_state)
-
- # Model manipulation
- # These functions are used to simplify the creation of states
- def add_action(self, new_action):
- """
- Adds an action to the state
-
- @param new_action The new action to execute when in this state
- @return True if added, False otherwise
- """
- self._actions.append(new_action)
- return True
-
- # remove_action - We did not define names for the action, hence they're
- # pretty hard to remove on a precise basis
-
- def get_action_list(self):
- """
- @return A list of actions that the state will execute
- """
- return self._actions
-
- def clear_actions(self):
- """
- Removes all the action associated with this state. A cleared state will
- not do anything when entered or exited.
- """
- #FIXME What if the action is currently installed?
- self._actions = []
-
- def add_event_filter(self, event, next_state):
- """
- Adds an event filter that will cause a transition from this state.
-
- The same event filter may not be added twice.
-
- @param event The event that will trigger a transition
- @param next_state The state to which the transition will lead
- @return True if added, False otherwise
- """
- if event not in self._transitions.keys():
- self._transitions[event]=next_state
- return True
- return False
-
- def get_event_filter_list(self):
- """
- @return The list of event filters associated with this state.
- """
- return self._transitions.items()
-
- def clear_event_filters(self):
- """
- Removes all the event filters associated with this state. A state that
- was just cleared will become a sink and will be the end of the
- tutorial.
- """
- self._transitions = {}
-
- def __eq__(self, otherState):
- """
- Compares two states and tells whether they contain the same states with the
- same actions and event filters.
-
- @param otherState The other State that we wish to match
- @returns True if every action in this state has a matching action in the
- other state with the same properties and values AND if every
- event filters in this state has a matching filter in the
- other state having the same properties and values AND if both
- states have the same name.
-` """
- if not isinstance(otherState, State):
- return False
- if self.name != otherState.name:
- return False
-
- # Do they have the same actions?
- if len(self._actions) != len(otherState._actions):
- return False
-
- if len(self._transitions) != len(otherState._transitions):
- return False
-
- for act in self._actions:
- found = False
- # For each action in the other state, try to match it with this one.
- for otherAct in otherState._actions:
- if act == otherAct:
- found = True
- break
- if found == False:
- # If we arrive here, then we could not find an action with the
- # same values in the other state. We know they're not identical
- return False
-
- # Do they have the same event filters?
- if self._transitions != otherState._transitions:
- return False
-
- # If nothing failed up to now, then every actions and every filters can
- # be found in the other state
- return True
-
-class FiniteStateMachine(State):
- """
- This is a collection of states, with a start state and an end callback.
- It is used to simplify the development of the various tutorials by
- encapsulating a collection of states that represent a given learning
- process.
-
- For now, we will consider that there can only be states
- inserted in the FSM, and that there are no nested FSM inside.
- """
-
- def __init__(self, name, tutorial=None, state_dict=None, start_state_name="INIT", action_list=None):
- """
- The constructor for a FSM. Pass in the start state and the setup
- actions that need to be taken when the FSM itself start (which may be
- different from what is done in the first state of the machine).
-
- @param name A short descriptive name for this FSM
- @param tutorial The tutorial that will execute this FSM. If None is
- attached on creation, then one must absolutely be attached before
- executing the FSM with set_tutorial().
- @param state_dict A dictionary containing the state names as keys and
- the state themselves as entries.
- @param start_state_name The name of the starting state, if different
- from "INIT"
- @param action_list The actions to undertake when initializing the FSM
- """
- State.__init__(self, name)
-
- self.name = name
- self.tutorial = tutorial
-
- # Dictionnary of states contained in the FSM
- self._states = state_dict or {}
-
- self.start_state_name = start_state_name
- # Set the current state to None - we are not executing anything yet
- self.current_state = None
-
- # Register the actions for the FSM - They will be processed at the
- # FSM level, meaning that when the FSM will start, it will first
- # execute those actions. When the FSM closes, it will tear down the
- # inner actions of the state, then close its own actions
- self.actions = action_list or []
-
- # Flag to mention that the FSM was initialized
- self._fsm_setup_done = False
- # Flag that must be raised when the FSM is to be teared down
- self._fsm_teardown_done = False
- # Flag used to declare that the FSM has reached an end state
- self._fsm_has_finished = False
-
- def set_tutorial(self, tutorial):
- """
- This associates the FSM to the given tutorial. It MUST be associated
- either in the constructor or with this function prior to executing the
- FSM.
-
- @param tutorial The tutorial that will execute this FSM.
- """
- # If there was no tutorial associated
- if self.tutorial == None:
- # Associate it with this FSM and all the underlying states
- self.tutorial = tutorial
- for state in self._states.itervalues():
- state.set_tutorial(tutorial)
- else:
- raise RuntimeWarning(\
- "The FSM %s is already associated with a tutorial."%self.name)
-
- def setup(self):
- """
- This function initializes the FSM the first time it is called.
- Then, every time it is called, it initializes the current state.
- """
- # Are we associated with a tutorial?
- if self.tutorial == None:
- raise UnboundLocalError("No tutorial was associated with FSM %s" % self.name)
-
- # If we never initialized the FSM itself, then we need to run all the
- # actions associated with the FSM.
- if self._fsm_setup_done == False:
- # Remember the initial state - we might want to reset
- # or rewind the FSM at a later moment
- self.start_state = self._states[self.start_state_name]
- self.current_state = self.start_state
- # Flag the FSM level setup as done
- self._fsm_setup_done = True
- # Execute all the FSM level actions
- for action in self.actions:
- self.tutorial.probeManager.install(action)
-
- # Then, we need to run the setup of the current state
- self.current_state.setup()
-
- def set_state(self, new_state_name):
- """
- This functions changes the current state of the finite state machine.
-
- @param new_state The identifier of the state we need to go to
- """
- # TODO : Since we assume no nested FSMs, we don't set state on the
- # inner States / FSMs
-## # Pass in the name to the internal state - it might be a FSM and
-## # this name will apply to it
-## self.current_state.set_state(new_state_name)
-
- # Make sure the given state is owned by the FSM
- if not self._states.has_key(new_state_name):
- # If we did not recognize the name, then we do not possess any
- # state by that name - we must ignore this state change request as
- # it will be done elsewhere in the hierarchy (or it's just bogus).
- return
-
- if self.current_state != None:
- if new_state_name == self.current_state.name:
- # If we already are in this state, we do not need to change
- # anything in the current state - By design, a state may not point
- # to itself
- return
-
- new_state = self._states[new_state_name]
-
- # Undo the actions of the old state
- self.teardown()
-
- # Insert the new state
- self.current_state = new_state
-
- # Call the initial actions in the new state
- self.setup()
-
- def get_current_state_name(self):
- """
- Returns the name of the current state.
-
- @return A string representing the name of the current state
- """
- return self.current_state.name
-
- def teardown(self):
- """
- Revert any changes done by setup()
- """
- # Teardown the current state
- if self.current_state is not None:
- self.current_state.teardown()
-
- # If we just finished the whole FSM, we need to also call the teardown
- # on the FSM level actions
- if self._fsm_has_finished == True:
- # Flag the FSM teardown as not needed anymore
- self._fsm_teardown_done = True
- # Undo all the FSM level actions here
- for action in self.actions:
- self.tutorial.probeManager.uninstall(action)
-
- # TODO : It might be nice to have a start() and stop() method for the
- # FSM.
-
- # Data manipulation section
- # These functions are dedicated to the building and editing of a graph.
- def add_state(self, new_state):
- """
- Inserts a new state in the FSM.
-
- @param new_state The State object that will now be part of the FSM
- @raise KeyError In the case where a state with this name already exists
- """
- if self._states.has_key(new_state.name):
- raise KeyError("There is already a state by this name in the FSM")
-
- self._states[new_state.name] = new_state
-
- # Not such a great name for the state accessor... We already have a
- # set_state name, so get_state would conflict with the notion of current
- # state - I would recommend having a set_current_state instead.
- def get_state_by_name(self, state_name):
- """
- Fetches a state from the FSM, based on its name. If there is no
- such state, the method will throw a KeyError.
-
- @param state_name The name of the desired state
- @return The State object having the given name
- """
- return self._states[state_name]
-
- def remove_state(self, state_name):
- """
- Removes a state from the FSM. Raises a KeyError when the state is
- not existent.
-
- Warning : removing a state will also remove all the event filters that
- point to this given name, to preserve the FSM's integrity. If you only
- want to edit a state, you would be better off fetching this state with
- get_state_by_name().
-
- @param state_name A string being the name of the state to remove
- @raise KeyError When the state_name does not a represent a real state
- stored in the dictionary
- """
-
- state_to_remove = self._states[state_name]
-
- # Remove the state from the states' dictionnary
- for st in self._states.itervalues():
- # Iterate through the list of event filters and remove those
- # that point to the state that will be removed
-
- #TODO : Move this code inside the State itself - we're breaking
- # encap :P
- for event in st._transitions:
- if st._transitions[event] == state_name:
- del st._transitions[event]
-
- # Remove the state from the dictionary
- del self._states[state_name]
-
- # Exploration methods - used to know more about a given state
- def get_following_states(self, state_name):
- """
- Returns a tuple of the names of the states that point to the given
- state. If there is no such state, the function raises a KeyError.
-
- @param state_name The name of the state to analyse
- @raise KeyError When there is no state by this name in the FSM
- """
- state = self._states[state_name]
-
- next_states = set()
-
- for event, state in state._transitions.items():
- next_states.add(state)
-
- return tuple(next_states)
-
- def get_previous_states(self, state_name):
- """
- Returns a tuple of the names of the state that can transition to
- the given state. If there is no such state, the function raises a
- KeyError.
-
- @param state_name The name of the state that the returned states might
- transition to.
- """
- # This might seem a bit funny, but we don't verify if the given
- # state is present or not in the dictionary.
- # This is due to the fact that when building a graph, we might have a
- # prototypal state that has not been inserted yet. We could not know
- # which states are pointing to it until we insert it in the graph.
-
- states = []
- # Walk through the list of states
- for st in self._states.itervalues():
- for event, state in st._transitions.items():
- if state == state_name:
- states.append(state)
- continue
-
- return tuple(states)
-
- # Convenience methods to see the content of a FSM
- def __str__(self):
- out_string = ""
- for st in self._states.itervalues():
- out_string += st.name + ", "
- return out_string
-
- def __eq__(self, otherFSM):
- """
- Compares the elements of two FSM to ensure and returns true if they have the
- same set of states, containing the same actions and the same event filters.
-
- @returns True if the two FSMs have the same content, False otherwise
- """
- if not isinstance(otherFSM, FiniteStateMachine):
- return False
-
- # Make sure they share the same name
- if not (self.name == otherFSM.name) or \
- not (self.start_state_name == otherFSM.start_state_name):
- return False
-
- # Ensure they have the same number of FSM-level actions
- if len(self._actions) != len(otherFSM._actions):
- return False
-
- # Test that we have all the same FSM level actions
- for act in self._actions:
- found = False
- # For every action in the other FSM, try to match it with the
- # current one.
- for otherAct in otherFSM._actions:
- if act == otherAct:
- found = True
- break
- if found == False:
- return False
-
- # Make sure we have the same number of states in both FSMs
- 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/engine.py b/tutorius/engine.py
index c945e49..ec281b3 100644
--- a/tutorius/engine.py
+++ b/tutorius/engine.py
@@ -8,6 +8,7 @@ from .TProbe import ProbeManager
from .dbustools import save_args
from .tutorial import Tutorial, AutomaticTransitionEvent
+from .translator import ResourceTranslator
class TutorialRunner(object):
"""
@@ -25,8 +26,7 @@ class TutorialRunner(object):
self._state = None
self._sEvents = set() #Subscribed Events
- #Cached objects
- self._actions = {}
+ self._installed_actions = {}
#Temp FIX until event/actions have an activity id
self._activity_id = None
@@ -55,15 +55,18 @@ class TutorialRunner(object):
return
#Clear the current actions
- for action in self._actions.values():
- self._pM.uninstall(action)
- self._actions = {}
+ for action_address in self._installed_actions.values():
+ self._pM.uninstall(action_address)
+ self._installed_actions = {}
#Clear the EventFilters
for event in self._sEvents:
self._pM.unsubscribe(event)
self._sEvents.clear()
+ def __save_address(self, action_name, action_address):
+ self._installed_actions[action_name] = action_address
+
def _setupState(self):
if self._state is None:
raise RuntimeError("Attempting to setupState without a state")
@@ -71,19 +74,25 @@ class TutorialRunner(object):
# Handle the automatic event
state_name = self._state
- self._actions = self._tutorial.get_action_dict(self._state)
+ actions = self._tutorial.get_action_dict(self._state)
transitions = self._tutorial.get_transition_dict(self._state)
+ # Verify if we have an automatic transition in the state - if so, we
+ # will skip installing the actions and events and go straight to the
+ # next state
for (event, next_state) in transitions.values():
if isinstance(event, AutomaticTransitionEvent):
state_name = next_state
- break
+ return state_name
+
+ # Install all the actions first
+ for (action_name, action) in actions.items():
+ self._pM.install(action, save_args(self.__save_address, action_name), block=True)
+ # Install the event filters
+ for (event, next_state) in transitions.values():
self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state)))
- for action in self._actions.values():
- self._pM.install(action)
-
return state_name
def enterState(self, state_name):
@@ -112,9 +121,6 @@ class TutorialRunner(object):
# transition in the state definition
self.enterState(self._setupState())
-
-
-
class Engine:
"""
Driver for the execution of tutorials
@@ -128,6 +134,7 @@ class Engine:
#FIXME shell.get_model() will only be useful in the shell process
self._shell = shell.get_model()
self._probeManager = probeManager or ProbeManager()
+
self._tutorial = None
def launch(self, tutorialID):
@@ -137,7 +144,9 @@ class Engine:
if self._tutorial:
self.stop()
- self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager)
+ # Insert the resource translation layer into the
+ translator_layer = ResourceTranslator(self._probeManager, tutorialID)
+ self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), translator_layer)
#Get the active activity from the shell
activity = self._shell.get_active_activity()
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..335a461
--- /dev/null
+++ b/tutorius/translator.py
@@ -0,0 +1,189 @@
+# 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 as copy_module
+
+logger = logging.getLogger("ResourceTranslator")
+
+from .properties import *
+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.currentActivity = activity_id
+
+ def getCurrentActivity(self):
+ return self._probe_manager.currentActivity
+
+ 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, address):
+ return self._probe_manager.unsubscribe(address)
+
+ def register_probe(self, process_name, unique_id):
+ self._probe_manager.register_probe(process_name, unique_id)
+
+ def unregister_probe(self, unique_id):
+ self._probe_manager.unregister_probe(unique_id)
+
+ def get_registered_probes_list(self, process_name=None):
+ return self._probe_manager.get_registered_probes_list(process_name)
+
+ ## Decorated functions ##
+ def install(self, action, callback, block=False):
+ # 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_module.deepcopy(action)
+ # Execute the replacement
+ self.translate(new_action)
+
+ # Send the new action to the probe manager
+ return self._probe_manager.install(new_action, callback, block)
+
+ def update(self, action_address, newaction, block=False):
+ # TODO : Repair this as it currently doesn't work.
+ # Actions are being copied, then translated in install(), so they
+ # won't be addressable via the same object that is in the Tutorial
+ # Runner.
+ translated_new_action = copy_module.deepcopy(newaction)
+ self.translate(translated_new_action)
+
+ return self._probe_manager.update(action_address, translated_new_action, block)
+
+ def uninstall(self, action_address, block=False):
+ return self._probe_manager.uninstall(action_address, block)
+
diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py
index b45363f..793d6f2 100644
--- a/tutorius/tutorial.py
+++ b/tutorius/tutorial.py
@@ -88,7 +88,7 @@ class Tutorial(object):
self._state_name_nb = 0
- def add_state(self, action_list=(), transition_list=()):
+ def add_state(self, action_dict={}, transition_list=()):
"""
Add a new state to the state machine. The state is
initialized with the action list and transition list
@@ -98,19 +98,19 @@ class Tutorial(object):
The transitions are added using add_transition.
- @param action_list The list of valid actions for this state
+ @param action_dict The dictionary of valid action_name:actions for this state
@param transition_list The list of valid transitions
@return unique name for this state
"""
name = self._generate_unique_state_name()
- for action in action_list:
+ for (action_name, action) in action_dict.items():
self._validate_action(action)
for transition in transition_list:
self._validate_transition(transition)
- state = State(name, action_list, transition_list)
+ state = State(name, action_dict, transition_list)
self._state_dict[name] = state
diff --git a/tutorius/vault.py b/tutorius/vault.py
index 7ec0a23..af00539 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
@@ -659,13 +659,11 @@ class XMLSerializer(Serializer):
new_transition = None
for transition in transition_element_list:
- #start_state = transition.getAttribute(START_STATE_ATTR)
next_state = transition.getAttribute(NEXT_STATE_ATTR)
transition_name = transition.getAttribute(NAME_ATTR)
try:
- #The attributes must be removed so that they are not
+ # The attributes must be removed so that they are not
# viewed as a property in load_xml_component
- # transition.removeAttribute(START_STATE_ATTR)
transition.removeAttribute(NEXT_STATE_ATTR)
transition.removeAttribute(NAME_ATTR)
except NotFoundErr:
@@ -683,13 +681,11 @@ class XMLSerializer(Serializer):
new_transition = None
for transition in transition_element_list:
- #start_state = transition.getAttribute(START_STATE_ATTR)
next_state = transition.getAttribute(NEXT_STATE_ATTR)
transition_name = transition.getAttribute(NAME_ATTR)
try:
#The attributes must be removed so that they are not
# viewed as a property in load_xml_component
- # transition.removeAttribute(START_STATE_ATTR)
transition.removeAttribute(NEXT_STATE_ATTR)
transition.removeAttribute(NAME_ATTR)
except NotFoundErr:
@@ -752,7 +748,8 @@ class XMLSerializer(Serializer):
properties = {}
for prop in node.attributes.keys():
- if prop == "Class" : continue
+ if prop == "Class" or prop[:2] == '__': continue
+ logger.debug("property to be inserted is : " + prop)
# security : keep sandboxed
properties[str(prop)] = eval(node.getAttribute(prop))
@@ -810,10 +807,10 @@ class XMLSerializer(Serializer):
stateName = state.getAttribute("Name")
# Using item 0 in the list because there is always only one
# Actions and EventFilterList element per State node.
- actions_list = cls._load_xml_actions(state.getElementsByTagName(ELEM_ACTIONS)[0])
+ actions_dict = cls._load_xml_actions(state.getElementsByTagName(ELEM_ACTIONS)[0])
transitions_list = cls._load_xml_transitions(state.getElementsByTagName(ELEM_TRANS)[0])
- state_dict[stateName] = State(stateName, actions_list, transitions_list)
+ state_dict[stateName] = State(stateName, actions_dict, transitions_list)
return state_dict
@@ -982,9 +979,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")