diff options
-rw-r--r-- | addons/EmbeddedInterpreter.py | 30 | ||||
-rw-r--r-- | addons/WidgetIdentifier.py | 35 | ||||
-rw-r--r-- | addons/eventgenerator.py | 60 | ||||
-rw-r--r-- | addons/readfile.py | 6 | ||||
-rw-r--r-- | addons/timerevent.py | 4 | ||||
-rw-r--r-- | data/icons/Layer 1.svg | 6 | ||||
-rw-r--r-- | data/icons/clock.sugar.svg | 1593 | ||||
-rw-r--r-- | tests/constraintstests.py | 42 | ||||
-rw-r--r-- | tests/inject.py | 57 | ||||
-rw-r--r-- | tests/probetests.py | 64 | ||||
-rw-r--r-- | tests/propertiestests.py | 57 | ||||
-rw-r--r-- | tests/translatortests.py | 131 | ||||
-rw-r--r-- | tests/vaulttests.py | 21 | ||||
-rw-r--r-- | tutorius/TProbe.py | 46 | ||||
-rw-r--r-- | tutorius/actions.py | 1 | ||||
-rw-r--r-- | tutorius/constraints.py | 39 | ||||
-rw-r--r-- | tutorius/core.py | 618 | ||||
-rw-r--r-- | tutorius/editor_interpreter.py | 105 | ||||
-rw-r--r-- | tutorius/engine.py | 37 | ||||
-rw-r--r-- | tutorius/events.py | 36 | ||||
-rw-r--r-- | tutorius/ipython_view.py | 301 | ||||
-rw-r--r-- | tutorius/properties.py | 53 | ||||
-rw-r--r-- | tutorius/translator.py | 189 | ||||
-rw-r--r-- | tutorius/tutorial.py | 8 | ||||
-rw-r--r-- | tutorius/vault.py | 25 |
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") |