diff options
-rw-r--r-- | addons/bubblemessage.py | 2 | ||||
-rw-r--r-- | addons/changecolor.py | 127 | ||||
-rw-r--r-- | addons/clickaction.py | 2 | ||||
-rw-r--r-- | addons/dialogmessage.py | 2 | ||||
-rw-r--r-- | addons/disablewidget.py | 2 | ||||
-rw-r--r-- | addons/messagebuttonnext.py | 171 | ||||
-rw-r--r-- | addons/oncewrapper.py | 2 | ||||
-rw-r--r-- | addons/readfile.py | 2 | ||||
-rw-r--r-- | addons/widgetidentifyaction.py | 2 | ||||
-rw-r--r-- | tests/ressources/icon.svg | 21 | ||||
-rw-r--r-- | tests/vaulttests.py | 239 | ||||
-rw-r--r-- | tutorius/TProbe.py | 4 | ||||
-rw-r--r-- | tutorius/gtkutils.py | 6 | ||||
-rw-r--r-- | tutorius/tutorial.py | 55 | ||||
-rw-r--r-- | tutorius/vault.py | 420 |
15 files changed, 757 insertions, 300 deletions
diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py index 6572a6a..1ed1fe0 100644 --- a/addons/bubblemessage.py +++ b/addons/bubblemessage.py @@ -48,7 +48,7 @@ class BubbleMessage(Action): self._bubble = None self._speaker = None - def do(self): + def do(self, **kwargs): """ Show the dialog """ diff --git a/addons/changecolor.py b/addons/changecolor.py new file mode 100644 index 0000000..460da32 --- /dev/null +++ b/addons/changecolor.py @@ -0,0 +1,127 @@ +# 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 time + +import gobject + +import gtk, gtk.gdk + +from sugar.tutorius.actions import Action +from sugar.tutorius.properties import TUAMProperty +from sugar.tutorius.gtkutils import find_widget + +from sugar import profile + +# for easy profile access +xo_line_color = profile.get_color().get_stroke_color() +xo_fill_color = profile.get_color().get_fill_color() + +class ChangeColor(Action): + """ + ChangeColorEvent + """ + # widget address property + widaddr = TUAMProperty("0") + + # set timeout + timeout = 500 + + def __init__(self, widaddr=None): + """Constructor - Change a widget color + @param widaddr: the widget for which you want to change the color (UAM) + """ + Action.__init__(self) + + if widaddr: self.widaddr = widaddr + + self.init_style = None + self._new_color = None + + self.wid = None + + self._handler_id = None + + def do(self, **kwargs): + """ + do. + Change the color of the widaddr widget with the chosen color + """ + + if not "activity" in kwargs: + raise TypeError("activity argument is Mandatory") + + # get widget instance + self.wid = find_widget(kwargs["activity"], self.widaddr, ignore_errors=False) + + if not self.wid: + raise NameError("widget not found") + + # we have to get the initial color in the sugar rc theme + self.init_style = self.wid.rc_get_style() + + # define new color + self._new_color = gtk.gdk.color_parse(xo_fill_color) + + # set change color timeout (flash) + self._handler_id = gobject.timeout_add(ChangeColor.timeout, self._timeout_cb) + + def undo(self): + """ + Remove timer and go back to the original color + """ + + if self._handler_id: + try: + #remove the timer + gobject.source_remove(self._handler_id) + except: + pass + + # modify bg color (go back to original color) + self.wid.modify_bg(gtk.STATE_NORMAL, self.init_style.bg[gtk.STATE_NORMAL]) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self.init_style.bg[gtk.STATE_PRELIGHT]) + self.wid.modify_bg(gtk.STATE_ACTIVE, self.init_style.bg[gtk.STATE_ACTIVE]) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self.init_style.bg[gtk.STATE_INSENSITIVE]) + + def _timeout_cb(self): + """ + _timeout_cb triggers the eventfilter callback. + """ + + if self.wid.rc_get_style().bg[gtk.STATE_NORMAL] == self._new_color: + # modify bg color (go back to original color) + self.wid.modify_bg(gtk.STATE_NORMAL, self.init_style.bg[gtk.STATE_NORMAL]) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self.init_style.bg[gtk.STATE_PRELIGHT]) + self.wid.modify_bg(gtk.STATE_ACTIVE, self.init_style.bg[gtk.STATE_ACTIVE]) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self.init_style.bg[gtk.STATE_INSENSITIVE]) + else: + # modify bg color (to new color) + self.wid.modify_bg(gtk.STATE_NORMAL, self._new_color) + self.wid.modify_bg(gtk.STATE_PRELIGHT, self._new_color) + self.wid.modify_bg(gtk.STATE_ACTIVE, self._new_color) + self.wid.modify_bg(gtk.STATE_INSENSITIVE, self._new_color) + + return True + +__action__ = { + "name" : "ChangeColor", + "display_name" : "Change widget color", + "icon" : "message-bubble", + "class" : ChangeColor, + "mandatory_props" : ["widaddr"] +} + diff --git a/addons/clickaction.py b/addons/clickaction.py index 88c5519..071af28 100644 --- a/addons/clickaction.py +++ b/addons/clickaction.py @@ -29,7 +29,7 @@ class ClickAction(Action): Action.__init__(self) self.widget = widget - def do(self): + def do(self, **kwargs): """ click the widget """ diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py index 9250693..fad6d2c 100644 --- a/addons/dialogmessage.py +++ b/addons/dialogmessage.py @@ -36,7 +36,7 @@ class DialogMessage(Action): self.message = message if position: self.position = position - def do(self): + def do(self, **kwargs): """ Show the dialog """ diff --git a/addons/disablewidget.py b/addons/disablewidget.py index fd88303..b3d9ae6 100644 --- a/addons/disablewidget.py +++ b/addons/disablewidget.py @@ -30,7 +30,7 @@ class DisableWidgetAction(Action): self.target = target self._widget = None - def do(self): + def do(self, **kwargs): """Action do""" os = ObjectStore() if os.activity: diff --git a/addons/messagebuttonnext.py b/addons/messagebuttonnext.py new file mode 100644 index 0000000..74ce1bb --- /dev/null +++ b/addons/messagebuttonnext.py @@ -0,0 +1,171 @@ +# 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 gtk, gtk.gdk + +from sugar.tutorius.filters import EventFilter +from sugar.tutorius.properties import TStringProperty, TArrayProperty +from sugar.tutorius import overlayer + +from sugar import profile + +xo_line_color_profile = profile.get_color().get_stroke_color() +xo_fill_color_profile = profile.get_color().get_fill_color() + +class MessageButtonNext(EventFilter): + """ + MessageButtonNext + """ + # set message + message = TStringProperty("Message") + + # create the position as an array of fixed-size 2 + position = TArrayProperty((0,0), 2, 2) + + # set padding + padding = 40 + + def __init__(self, message=None, position=None, center_pos=False): + """Constructor. + + @param message message to display + @param position message position + """ + super(MessageButtonNext,self).__init__() + + if position: + self.position = position + else: + # TODO: to be removed when creator supports editing properties on events + self.position = (300, 200) + + if message: + self.message = message + + self.overlay = None + self.msgnext = None + + def install_handlers(self, callback, **kwargs): + """install_handlers creates the message button next and shows it""" + super(MessageButtonNext,self).install_handlers(callback, **kwargs) + + if not "activity" in kwargs: + raise TypeError("activity argument is Mandatory") + + # get activity instance + self.activity = kwargs["activity"] + + # get or inject overlayer + self.overlay = self.activity._overlayer + + if not self.overlay: + self.overlay = self.activity._overlayer + + btntext = "NEXT" + + self.msgnext = MsgNext(text=self.message,btntext=btntext) + self.msgnext._btnnext.connect("clicked", self.btnnext_clicked) + + # add space around minimum need size + wid_width, wid_height = self.msgnext.size_request() + self.msgnext.set_size_request(wid_width+MessageButtonNext.padding, wid_height+MessageButtonNext.padding) + + # set position + x, y = self.position + + self.msgnext.show() + self.overlay.put(self.msgnext, x, y) + self.overlay.queue_draw() + + def remove_handlers(self): + """remove handler removes the message button next""" + super(MessageButtonNext,self).remove_handlers() + + if self.msgnext: + self.msgnext.destroy() + self.msgnext = None + + def btnnext_clicked(self, widget): + self.do_callback() + +__event__ = { + "name" : "MessageButtonNext", + "display_name" : "Message button next", + "icon" : "message-bubble", + "class" : MessageButtonNext, + "mandatory_props" : ["message"] +} + +class MsgNext(gtk.EventBox): + """ + Create an EventBox + """ + def __init__(self, text, btntext): + """ + Creates an Event Box + """ + gtk.EventBox.__init__(self) + + self.message = text + self.btnmessage = btntext + + self.set_visible_window(True) + + # create a vbox + self.box = gtk.VBox() + + # create a label (set message to display) + self._label = gtk.Label() + self._text = "<b>%s</b>" % self.message + self._label.set_markup(self._text) + self._label.set_line_wrap(True) + + self._colortext = gtk.gdk.color_parse("white") + self._label.modify_fg(gtk.STATE_NORMAL, self._colortext) + self._label.modify_fg(gtk.STATE_PRELIGHT, self._colortext) + self._label.modify_fg(gtk.STATE_ACTIVE, self._colortext) + self._label.modify_fg(gtk.STATE_INSENSITIVE, self._colortext) + + self._label.show() + + # create a hbox (holding button) + self._hbox = gtk.HBox() + + # create a button inside hbox + self._btnnext = gtk.Button(self.btnmessage) + + self._colorbtn = gtk.gdk.color_parse(xo_fill_color_profile) + + self._btnnext.modify_bg(gtk.STATE_NORMAL, self._colorbtn) + self._btnnext.modify_bg(gtk.STATE_PRELIGHT, self._colorbtn) + self._btnnext.modify_bg(gtk.STATE_ACTIVE, self._colorbtn) + + self._btnnext.show() + + self._hbox.pack_end(self._btnnext, expand=False) + + self._hbox.show() + + self.box.pack_start(self._label, expand=True) + self.box.pack_start(self._hbox, expand=True) + + self.box.show() + + self.add(self.box) + + self._colormsgnext = gtk.gdk.color_parse(xo_fill_color_profile) + self.modify_bg(gtk.STATE_NORMAL, self._colormsgnext) + diff --git a/addons/oncewrapper.py b/addons/oncewrapper.py index 5db3b60..c404ae4 100644 --- a/addons/oncewrapper.py +++ b/addons/oncewrapper.py @@ -32,7 +32,7 @@ class OnceWrapper(Action): self._need_undo = False self.action = action - def do(self): + def do(self, **kwargs): """ Do the action only on the first time """ diff --git a/addons/readfile.py b/addons/readfile.py index 9fe2f81..4a6c54d 100644 --- a/addons/readfile.py +++ b/addons/readfile.py @@ -34,7 +34,7 @@ class ReadFile(Action): if filename: self.filename=filename - def do(self): + def do(self, **kwargs): """ Perform the action, call read_file on the activity """ diff --git a/addons/widgetidentifyaction.py b/addons/widgetidentifyaction.py index 3df244b..c44964b 100644 --- a/addons/widgetidentifyaction.py +++ b/addons/widgetidentifyaction.py @@ -24,7 +24,7 @@ class WidgetIdentifyAction(Action): self.activity = None self._dialog = None - def do(self): + def do(self, **kwargs): os = ObjectStore() if os.activity: self.activity = os.activity diff --git a/tests/ressources/icon.svg b/tests/ressources/icon.svg new file mode 100644 index 0000000..bb28f04 --- /dev/null +++ b/tests/ressources/icon.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" ?><!-- Created with Inkscape (http://www.inkscape.org/) --><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [ + <!ENTITY stroke_color "#000000"> + <!ENTITY fill_color "#ffffff"> +]><svg height="55px" id="svg2393" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.47pre1 r21720" sodipodi:docname="tutortool.svg" sodipodi:version="0.32" version="1.1" width="55px" 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"> + <defs id="defs2395"> + <inkscape:perspective id="perspective2401" inkscape:persp3d-origin="16 : 10.666667 : 1" inkscape:vp_x="0 : 16 : 1" inkscape:vp_y="0 : 1000 : 0" inkscape:vp_z="32 : 16 : 1" sodipodi:type="inkscape:persp3d"/> + </defs> + <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="layer1" inkscape:cx="3.7661233" inkscape:cy="33.132055" inkscape:document-units="px" inkscape:grid-bbox="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="675" inkscape:window-maximized="0" inkscape:window-width="1057" inkscape:window-x="108" inkscape:window-y="45" inkscape:zoom="3.9590209" pagecolor="#ffffff" showgrid="true"/> + <metadata id="metadata2398"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + </cc:Work> + </rdf:RDF> + </metadata> + <g id="layer1" inkscape:groupmode="layer" inkscape:label="Layer 1" transform="translate(0,23)"> + <path d="m 38.01548,1.5478747 c 0,7.1837999 -7.3667,13.0141283 -16.443525,13.0141283 -2.269208,0 -8.124729,3.152936 -13.9608513,4.252763 C 13.382177,14.110994 11.434521,11.926642 9.9463815,10.748864 6.9701032,8.3933076 5.1284282,5.1397735 5.1284282,1.5478747 c 0,-7.1837994 7.3666998,-13.0141297 16.4435268,-13.0141297 9.076825,0 16.443525,5.8303303 16.443525,13.0141297 z" id="path2403" sodipodi:nodetypes="cscsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> + <path d="m 50.150276,6.4721386 c 0,2.621116 -1.428036,4.9953144 -3.735846,6.7142344 -1.153905,0.85946 -1.824287,2.434433 1.398853,6.784273 -6.258422,-3.991066 -8.65379,-4.001712 -10.413335,-4.001712 -7.03818,0 -12.750327,-4.254565 -12.750327,-9.4967954 0,-5.2422321 5.712147,-9.4967971 12.750327,-9.4967971 7.038182,0 12.750328,4.254565 12.750328,9.4967971 z" id="path3175" sodipodi:nodetypes="cccsssc" style="fill:&fill_color;;fill-opacity:1;fill-rule:nonzero;stroke:&stroke_color;;stroke-width:1.96931934;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> + </g> +</svg>
\ No newline at end of file diff --git a/tests/vaulttests.py b/tests/vaulttests.py index 02c34e8..c6bd852 100644 --- a/tests/vaulttests.py +++ b/tests/vaulttests.py @@ -29,9 +29,10 @@ import unittest import os import shutil import zipfile +import cStringIO from sugar.tutorius import addon -from sugar.tutorius.core import State, FiniteStateMachine, Tutorial +from sugar.tutorius.tutorial import Tutorial from sugar.tutorius.actions import * from sugar.tutorius.filters import * from sugar.tutorius.vault import Vault, XMLSerializer, Serializer, TutorialBundler @@ -100,18 +101,14 @@ class VaultInterfaceTest(unittest.TestCase): ini_file2.close() # Create a dummy fsm - self.fsm = FiniteStateMachine("testingMachine") + 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]) - st1 = State("INIT") - st1.add_action(act1) - st1.add_event_filter(ev1, 'Second') - st2 = State("Second") - st2.add_action(act2) - self.fsm.add_state(st1) - self.fsm.add_state(st2) + self.fsm.add_action("INIT", act1) + st2 = self.fsm.add_state((act2,)) + self.fsm.add_transition("INIT",(ev1, st2)) self.tuto_guid = uuid1() # Create a dummy metadata dictionnary @@ -146,7 +143,8 @@ class VaultInterfaceTest(unittest.TestCase): # Creat a dummy tutorial .xml file serializer = XMLSerializer() - serializer.save_fsm(self.fsm, 'tutorial.xml', test_path) + with file(os.path.join(test_path, 'tutorial.xml'), 'w') as fsmfile: + serializer.save_tutorial(self.fsm, fsmfile) # Create a dummy tutorial metadata file self.create_test_metadata_file(os.path.join(test_path, 'meta.ini'), self.tuto_guid) @@ -238,16 +236,8 @@ class VaultInterfaceTest(unittest.TestCase): reloaded_tuto = vault.loadTutorial(self.tuto_guid) # Compare the two FSMs - reloaded_fsm = reloaded_tuto.state_machine - - assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ + assert reloaded_tuto.get_state_dict().keys() == self.fsm.get_state_dict().keys(), \ 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" def test_saveTutorial(self): """ @@ -256,29 +246,65 @@ class VaultInterfaceTest(unittest.TestCase): # Save the tutorial in the vault vault = Vault() - tutorial = Tutorial('test', self.fsm) + tutorial = self.fsm vault.saveTutorial(tutorial, self.test_metadata_dict) # Get the tutorial back reloaded_tuto = vault.loadTutorial(self.save_test_guid) # Compare the two FSMs - reloaded_fsm = reloaded_tuto.state_machine - - assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ + assert reloaded_tuto.get_state_dict().keys() == self.fsm.get_state_dict().keys(), \ 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" # TODO : Compare the initial and reloaded metadata when vault.Query() will accept specifc argument # (so we can specifiy that we want only the metadata for this particular tutorial - - + + def test_add_delete_get_path_resource(self): + """ + This test verify that the vault interface function add_resource succesfully add resource in the vault + 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' + + # Create and save a tutorial + tutorial = Tutorial('test') + Vault.saveTutorial(tutorial, 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) + + # 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' + + # Check if get_resource_path Vault interface function is working + vault_path = Vault.get_resource_path(self.save_test_guid, resource_id) + + assert os.path.isfile(vault_path), 'path returned is not a file' + basename, extension = os.path.splitext(vault_path) + assert extension == '.svg', 'wrong file path have been returned' + + # Delete the resource + Vault.delete_resource(self.save_test_guid, resource_id) + + # 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): @@ -298,8 +324,8 @@ class SerializerInterfaceTest(unittest.TestCase): ser = Serializer() try: - ser.save_fsm(None) - assert False, "save_fsm() should throw an unimplemented error" + ser.save_tutorial(None) + assert False, "save_tutorial() should throw an unimplemented error" except: pass @@ -307,8 +333,8 @@ class SerializerInterfaceTest(unittest.TestCase): ser = Serializer() try: - ser.load_fsm(str(uuid.uuid1())) - assert False, "load_fsm() should throw an unimplemented error" + ser.load_tutorial(str(uuid.uuid1())) + assert False, "load_tutorial() should throw an unimplemented error" except: pass @@ -318,100 +344,61 @@ class XMLSerializerTest(unittest.TestCase): """ def setUp(self): - - os.environ["SUGAR_BUNDLE_PATH"] = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path') - path = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path') - if os.path.isdir(path) != True: - os.makedirs(path) - # Create the sample FSM - self.fsm = FiniteStateMachine("testingMachine") + 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]) - st1 = State("INIT") - st1.add_action(act1) - st1.add_event_filter(ev1, 'Second') - - st2 = State("Second") - - st2.add_action(act2) - - self.fsm.add_state(st1) - self.fsm.add_state(st2) + self.fsm.add_action("INIT",act1) + st2 = self.fsm.add_state((act2,)) + self.fsm.add_transition("INIT",(ev1, st2)) self.uuid = uuid1() - # Flag to set to True if the output can be deleted after execution of - # the test - self.remove = True - def tearDown(self): """ - Removes the created files, if need be. + Nothing to do anymore. """ - if self.remove == True: - shutil.rmtree(os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')) - - folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data'); - for file in os.listdir(folder): - file_path = os.path.join(folder, file) - shutil.rmtree(file_path) + pass - def create_test_metadata(self, ini_file_path, guid): - ini_file = open(ini_file_path, 'wt') - ini_file.write("[GENERAL_METADATA]\n") - ini_file.write('guid=' + str(guid) + '\n') - ini_file.write('name=TestTutorial1\n') - ini_file.write('version=1\n') - ini_file.write('description=This is a test tutorial 1\n') - ini_file.write('rating=3.5\n') - ini_file.write('category=Test\n') - ini_file.write('publish_state=false\n') - ini_file.write('[RELATED_ACTIVITIES]\n') - ini_file.write('org.laptop.TutoriusActivity = 1\n') - ini_file.write('org.laptop.Writus = 1\n') - ini_file.close() - - def test_save(self): - """ - Writes an FSM to disk, then compares the file to the expected results. - "Remove" boolean argument specify if the test data must be removed or not - """ - xml_ser = XMLSerializer() - os.makedirs(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid))) - xml_ser.save_fsm(self.fsm, sugar.tutorius.vault.TUTORIAL_FILENAME, os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid))) - self.create_test_metadata(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid), 'meta.ini'), self.uuid) - + def create_test_metadata(self, file_obj, guid): + file_obj.write("[GENERAL_METADATA]\n") + file_obj.write('guid=' + str(guid) + '\n') + file_obj.write('name=TestTutorial1\n') + file_obj.write('version=1\n') + file_obj.write('description=This is a test tutorial 1\n') + file_obj.write('rating=3.5\n') + file_obj.write('category=Test\n') + file_obj.write('publish_state=false\n') + file_obj.write('[RELATED_ACTIVITIES]\n') + file_obj.write('org.laptop.TutoriusActivity = 1\n') + file_obj.write('org.laptop.Writus = 1\n') def test_save_and_load(self): """ + Writes an FSM to disk, then compares the file to the expected results. Load up the written FSM and compare it with the object representation. """ - self.test_save() xml_ser = XMLSerializer() - - loaded_fsm = xml_ser.load_fsm(str(self.uuid)) + tuto_file = cStringIO.StringIO() + xml_ser.save_tutorial(self.fsm, tuto_file) + + xml_ser = XMLSerializer() + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) + loaded_fsm = xml_ser.load_tutorial(load_tuto_file) # Compare the two FSMs - assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert loaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" + assert loaded_fsm == self.fsm, "Loaded FSM differs from original one" def test_all_actions(self): """ Inserts all the known action types in a FSM, then attempt to load it. """ - st = State("INIT") - + fsm = Tutorial("TestActions") + tuto_file = cStringIO.StringIO() act1 = addon.create('BubbleMessage', "Hi!", position=[10,120], tail_pos=[-12,30]) act2 = addon.create('DialogMessage', "Hello again.", position=[120,10]) act3 = addon.create('WidgetIdentifyAction') @@ -423,26 +410,24 @@ class XMLSerializerTest(unittest.TestCase): actions = [act1, act2, act3, act4, act5, act6, act7, act8] for action in actions: - st.add_action(action) + fsm.add_action("INIT", action) - self.fsm.remove_state("Second") - self.fsm.remove_state("INIT") - self.fsm.add_state(st) - xml_ser = XMLSerializer() + xml_ser.save_tutorial(fsm, tuto_file) + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) - self.test_save() - - reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) - - # TODO : Cannot do object equivalence, must check equality of all underlying object - # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + reloaded_fsm = xml_ser.load_tutorial(load_tuto_file) + # Compare the two FSMs + assert reloaded_fsm == fsm, "Loaded FSM differs from original one" + assert fsm.get_action_dict() == reloaded_fsm.get_action_dict(), \ + "Actions should be the same" def test_all_filters(self): """ Inserts all the known action filters in a FSM, then attempt to load it. """ - st = State("INIT") + fsm = Tutorial("TestFilters") + tuto_file = cStringIO.StringIO() ev1 = addon.create('TimerEvent', 1000) ev2 = addon.create('GtkWidgetEventFilter', object_id="0.0.1.1.0.0.1", event_name="clicked") @@ -450,20 +435,18 @@ class XMLSerializerTest(unittest.TestCase): ev4 = addon.create('GtkWidgetTypeFilter', "0.0.1.1.1.2.3", strokes="acbd") filters = [ev1, ev2, ev3, ev4] - for filter in filters: - st.add_event_filter(filter, 'Second') + for efilter in filters: + fsm.add_transition("INIT", (efilter, 'END')) - self.fsm.remove_state("INIT") - self.fsm.add_state(st) - xml_ser = XMLSerializer() - - self.test_save() - - reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) - - # TODO : Cannot do object equivalence, must check equality of all underlying object - # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + xml_ser.save_tutorial(fsm, tuto_file) + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) + + reloaded_fsm = xml_ser.load_tutorial(load_tuto_file) + # Compare the two FSMs + assert reloaded_fsm == fsm, "Loaded FSM differs from original one" + assert fsm.get_transition_dict() == reloaded_fsm.get_transition_dict(), \ + "Transitions should be the same" class TutorialBundlerTests(unittest.TestCase): @@ -472,7 +455,7 @@ class TutorialBundlerTests(unittest.TestCase): This module contains all the tests for the storage mecanisms for tutorials This mean testing saving and loading tutorial, .ini file management and - adding ressources to tutorial + adding resources to tutorial """ def setUp(self): @@ -513,4 +496,4 @@ class TutorialBundlerTests(unittest.TestCase): if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index f55547c..dbab86a 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -110,8 +110,8 @@ class TProbe(dbus.service.Object): if action._props: action._props.update(loaded_action._props) - action.do() - + action.do(activity=self._activity) + return address @dbus.service.method("org.tutorius.ProbeInterface", diff --git a/tutorius/gtkutils.py b/tutorius/gtkutils.py index 1a9cb0f..c96a73f 100644 --- a/tutorius/gtkutils.py +++ b/tutorius/gtkutils.py @@ -33,7 +33,7 @@ def raddr_lookup(widget): return ".".join(name) -def find_widget(base, target_fqdn): +def find_widget(base, target_fqdn, ignore_errors=True): """Find a widget by digging into a parent widget's children tree @param base the parent widget @param target_fqdn fqdn-style target object name @@ -57,7 +57,9 @@ def find_widget(base, target_fqdn): try: obj = get_children(obj)[int(path.pop(0))] except: - break + if ignore_errors: + break + return None return obj diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py index 9831a7b..b45363f 100644 --- a/tutorius/tutorial.py +++ b/tutorius/tutorial.py @@ -67,7 +67,8 @@ class Tutorial(object): self.add_transition(Tutorial.INIT, \ (AutomaticTransitionEvent(), Tutorial.END)) else: - raise NotImplementedError("Tutorial: Initilization from a dictionary is not supported yet") + self._state_dict = state_dict + # Minimally check for the presence of an INIT and an END @@ -528,15 +529,20 @@ class Tutorial(object): def _generate_unique_state_name(self): name = "State" + str(self._state_name_nb) - self._state_name_nb += 1 + while name in self._state_dict: + self._state_name_nb += 1 + name = "State" + str(self._state_name_nb) return name + # Python Magic Methods def __str__(self): """ Return a string representation of the tutorial """ return str(self._state_dict) + def __eq__(self, other): + return isinstance(other, type(self)) and self.get_state_dict() == other.get_state_dict() class State(object): """ @@ -548,16 +554,20 @@ class State(object): inputs, the validation should be done by the containing class. """ - def __init__(self, name, action_list=(), transition_list=()): + def __init__(self, name, actions={}, transitions={}): """ Initializes the content of the state, such as loading the actions that are required and building the correct transitions. - @param action_list The list of actions to execute when entering this + @param actions list or dict of actions to perform when entering the state - @param transition_list A list of tuples of the form + @param transitions list or dict of tuples of the form (event, next_state_name), that explains the outgoing links for this state + + For actions and transitions, dictionaries allow specifying the name. + If lists are given, their contents will be added with add_action or + add_transition """ object.__init__(self) @@ -567,13 +577,19 @@ class State(object): self.action_name_nb = 0 self.transition_name_nb = 0 - self._actions = {} - for action in action_list: - self.add_action(action) - - self._transitions = {} - for transition in transition_list: - self.add_transition(transition) + if type(actions) is dict: + self._actions = dict(actions) + else: + self._actions = {} + for action in actions: + self.add_action(action) + + if type(transitions) is dict: + self._transitions = dict(transitions) + else: + self._transitions = {} + for transition in transitions: + self.add_transition(transition) # Action manipulations @@ -741,7 +757,9 @@ class State(object): # to make it easier to debug and know what we are # manipulating name = self.name + _NAME_SEPARATOR + "action" + str(self.action_name_nb) - self.action_name_nb += 1 + while name in self._actions: + self.action_name_nb += 1 + name = self.name + _NAME_SEPARATOR + "action" + str(self.action_name_nb) return name def _generate_unique_transition_name(self, transition): @@ -757,7 +775,9 @@ class State(object): # generate a name to make it easier to debug and know # what we are manipulating name = self.name + _NAME_SEPARATOR + "transition" + str(self.transition_name_nb) - self.transition_name_nb += 1 + while name in self._transitions: + self.transition_name_nb += 1 + name = self.name + _NAME_SEPARATOR + "transition" + str(self.transition_name_nb) return name def __eq__(self, otherState): @@ -775,12 +795,15 @@ class State(object): @param otherState The state that will be compared to this one @return True if the states are the same, False otherwise ` """ - raise NotImplementedError + return isinstance(otherState, type(self)) and \ + self.get_action_dict() == otherState.get_action_dict() and \ + self.get_transition_dict() == otherState.get_transition_dict() #TODO: Define the automatic transition in the same way as # other events class AutomaticTransitionEvent(TPropContainer): - pass + def __repr__(self): + return str(self.__class__.__name__) ################## Error Handling and Exceptions ############################## diff --git a/tutorius/vault.py b/tutorius/vault.py index b455a52..7ec0a23 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -31,7 +31,7 @@ import zipfile from ConfigParser import SafeConfigParser from . import addon -from .core import Tutorial, State, FiniteStateMachine +from .tutorial import Tutorial, State, AutomaticTransitionEvent logger = logging.getLogger("tutorius") @@ -58,10 +58,23 @@ INI_XML_FSM_PROPERTY = "fsm_filename" INI_VERSION_PROPERTY = 'version' INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" +RESOURCES_FOLDER = 'resources' + +###################################################################### +# XML Tag names and attributes +###################################################################### +ELEM_FSM = "FSM" +ELEM_STATES = "States" +ELEM_STATE = "State" +ELEM_ACTIONS = "Actions" +ELEM_TRANS = "Transitions" +ELEM_AUTOTRANS = "AutomaticTransition" NODE_COMPONENT = "Component" NODE_SUBCOMPONENT = "property" NODE_SUBCOMPONENTLIST = "listproperty" -NEXT_STATE_ATTR = "next_state" +NAME_ATTR = "__name__" +NEXT_STATE_ATTR = "__next_state__" +START_STATE_ATTR = "__start_state__" class Vault(object): @@ -73,7 +86,7 @@ class Vault(object): given activity. @param activity_name the name of the activity associated with this tutorial. None means ALL activities - @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. If activity_name is None, version number is not used + @param activity_vers the version number of the activity to find tutorial for. 0 means find for ANY version. If activity_name is None, version number is not used @returns a map of tutorial {names : GUID}. """ # check both under the activity data and user installed folders @@ -237,12 +250,14 @@ class Vault(object): # Return tutorial list return tutorial_list + @staticmethod def loadTutorial(Guid): """ Creates an executable version of a tutorial from its saved representation. - @returns an executable representation of a tutorial + @param Guid Unique identifier of the tutorial + @returns Tutorial object """ bundle = TutorialBundler(Guid) @@ -253,15 +268,20 @@ class Vault(object): serializer = XMLSerializer() name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) - fsm = serializer.load_fsm(Guid, bundle_path) - tuto = Tutorial(name, fsm) - return tuto + # Open the XML file + tutorial_file = os.path.join(bundle_path, TUTORIAL_FILENAME) + with open(tutorial_file, 'r') as tfile: + tutorial = serializer.load_tutorial(tfile) + + return tutorial @staticmethod def saveTutorial(tutorial, metadata_dict): """ Creates a persistent version of a tutorial in the Vault. + @param tutorial Tutorial + @param metadata_dict dictionary of metadata for the Tutorial @returns true if the tutorial was saved correctly """ @@ -275,7 +295,9 @@ class Vault(object): # Serialize the tutorial and write it to disk xml_ser = XMLSerializer() os.makedirs(tutorial_path) - xml_ser.save_fsm(tutorial.state_machine, TUTORIAL_FILENAME, tutorial_path) + + with open(os.path.join(tutorial_path, TUTORIAL_FILENAME), 'w') as fsmfile: + xml_ser.save_tutorial(tutorial, fsmfile) # Create the metadata file ini_file_path = os.path.join(tutorial_path, "meta.ini") @@ -303,7 +325,7 @@ class Vault(object): @staticmethod - def deleteTutorial(Tutorial): + def deleteTutorial(Guid): """ Removes the tutorial from the Vault. It will unpublish the tutorial if need be, and it will also wipe it from the persistent storage. @@ -321,13 +343,82 @@ class Vault(object): return False + @staticmethod + def add_resource(tutorial_guid, file_path): + """ + Add given resource file in the vault and returns a unique name for this resource + composed from the original name of the file and a suffix to make it unique + ( ex: name_1.jpg ). + @param tutorial_guid The guid of the tutorial + @param file_path the file path of the resource to add + @returns the resource_id of the resource + """ + assert os.path.isfile(file_path) + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Get the file name + file_name = os.path.basename(file_path) + #fname_splitted = file_path.rsplit('/') + #file_name = fname_splitted[fname_splitted.__len__() - 1] + base_name, extension = os.path.splitext(file_name) + # Append unique name to file name + file_name_appended = base_name + '_' + str(uuid.uuid1()) + extension + # Check if the resource file already exists + new_file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, file_name_appended) + if os.path.isfile(new_file_path) == False: + # Copy the resource file in the vault + if os.path.isdir(os.path.join(tutorial_path, RESOURCES_FOLDER)) == False: + os.makedirs(os.path.join(tutorial_path, RESOURCES_FOLDER)) + shutil.copyfile(file_path, new_file_path) + + return file_name_appended + + + @staticmethod + def delete_resource(tutorial_guid, resource_id): + """ + Delete the resource from the resources of the tutorial. + @param tutorial_guid the guid of the tutorial + @param resource_id the resource id of the resource to delete + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + if os.path.isfile(file_path): + # Delete the resource + os.remove(file_path) + else: + logging.info('File not found, no delete took place') + + @staticmethod + def get_resource_path(tutorial_guid, resource_id): + """ + Returns the absolute file path to the resourceID + @param tutorial_guid the guid of the tutorial + @param resource_id the resource id of the resource to find the path for + @returns the absolute path of the resource file + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + if os.path.isfile(file_path): + return file_path + else: + return None + + class Serializer(object): """ Interface that provide serializing and deserializing of the FSM used in the tutorials to/from disk. Must be inherited. """ - def save_fsm(self,fsm): + def save_tutorial(self,fsm): """ Save fsm to disk. If a GUID parameter is provided, the existing GUID is located in the .ini files in the store root and bundle root and @@ -337,7 +428,7 @@ class Serializer(object): """ raise NotImplementedError() - def load_fsm(self): + def load_tutorial(self): """ Load fsm from disk. """ @@ -348,21 +439,26 @@ class XMLSerializer(Serializer): Class that provide serializing and deserializing of the FSM used in the tutorials to/from a .xml file. Inherit from Serializer """ - - def _create_state_dict_node(self, state_dict, doc): + + @classmethod + def _create_state_dict_node(cls, state_dict, doc): """ Create and return a xml Node from a State dictionnary. + @param state_dict dictionary of State objects + @param doc The XML document root (used to create nodes only + @return xml Element containing the states """ - statesList = doc.createElement("States") + statesList = doc.createElement(ELEM_STATES) for state_name, state in state_dict.items(): - stateNode = doc.createElement("State") + stateNode = doc.createElement(ELEM_STATE) statesList.appendChild(stateNode) stateNode.setAttribute("Name", state_name) - actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc)) - eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc)) + actionsList = stateNode.appendChild(cls._create_action_list_node(state.get_action_dict(), doc)) + transitionsList = stateNode.appendChild(cls._create_transitions_node(state.get_transition_dict(), doc)) return statesList - - def _create_addon_component_node(self, parent_attr_name, comp, doc): + + @classmethod + def _create_addon_component_node(cls, parent_attr_name, comp, doc): """ Takes a component that is embedded in another component (e.g. the content of a OnceWrapper) and encapsulate it in a node with the property name. @@ -389,13 +485,14 @@ class XMLSerializer(Serializer): subCompNode = doc.createElement(NODE_SUBCOMPONENT) subCompNode.setAttribute("name", parent_attr_name) - subNode = self._create_component_node(comp, doc) + subNode = cls._create_component_node(comp, doc) subCompNode.appendChild(subNode) return subCompNode - def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc): + @classmethod + def _create_addonlist_component_node(cls, parent_attr_name, comp_list, doc): """ Takes a list of components that are embedded in another component (ex. the content of a ChainAction) and encapsulate them in a node with the property @@ -422,12 +519,13 @@ class XMLSerializer(Serializer): subCompListNode.setAttribute("name", parent_attr_name) for comp in comp_list: - compNode = self._create_component_node(comp, doc) + compNode = cls._create_component_node(comp, doc) subCompListNode.appendChild(compNode) return subCompListNode - def _create_component_node(self, comp, doc): + @classmethod + def _create_component_node(cls, comp, doc): """ Takes a single component (action or eventfilter) and transforms it into a xml node. @@ -446,68 +544,86 @@ class XMLSerializer(Serializer): for propname in comp.get_properties(): propval = getattr(comp, propname) if getattr(type(comp), propname).type == "addonlist": - compNode.appendChild(self._create_addonlist_component_node(propname, propval, doc)) + compNode.appendChild(cls._create_addonlist_component_node(propname, propval, doc)) elif getattr(type(comp), propname).type == "addon": #import rpdb2; rpdb2.start_embedded_debugger('pass') - compNode.appendChild(self._create_addon_component_node(propname, propval, doc)) + compNode.appendChild(cls._create_addon_component_node(propname, propval, doc)) else: # repr instead of str, as we want to be able to eval() it into a # valid object. compNode.setAttribute(propname, repr(propval)) return compNode - - def _create_action_list_node(self, action_list, doc): + + @classmethod + def _create_action_list_node(cls, action_dict, doc): """ Create and return a xml Node from a Action list. - @param action_list A list of actions + @param action_dict Dictionary of actions with names as keys @param doc The XML document root (used to create new nodes only) @return A XML Node object with the Actions tag name and a serie of Action children """ - actionsList = doc.createElement("Actions") - for action in action_list: + actionsList = doc.createElement(ELEM_ACTIONS) + for name, action in action_dict.items(): # Create the action node - actionNode = self._create_component_node(action, doc) + actionNode = cls._create_component_node(action, doc) + actionNode.setAttribute(NAME_ATTR, name) # Append it to the list actionsList.appendChild(actionNode) return actionsList - - def _create_event_filters_node(self, event_filters, doc): - """ - Create and return a xml Node from an event filters. + + @classmethod + def _create_transitions_node(cls, transition_dict, doc): """ - eventFiltersList = doc.createElement("EventFiltersList") - for event, state in event_filters: - eventFilterNode = self._create_component_node(event, doc) - eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state)) + Create and return a xml Node from a transition dictionary. + @param transition_dict dictionary of (event, next_state) transitions. + @param doc The XML document root (used to create nodes only + @return xml Element containing the transitions + """ + eventFiltersList = doc.createElement(ELEM_TRANS) + for transition_name, (event, end_state) in transition_dict.items(): + #start_state = transition_name.split(Tutorial._NAME_SEPARATOR)[0] + #XXX The addon is not in the cache and cannot be loaded so we + # store it differently for now + if type(event) == AutomaticTransitionEvent: + eventFilterNode = doc.createElement(ELEM_AUTOTRANS) + else: + eventFilterNode = cls._create_component_node(event, doc) + #eventFilterNode.setAttribute(START_STATE_ATTR, unicode(start_state)) + eventFilterNode.setAttribute(NEXT_STATE_ATTR, unicode(end_state)) + eventFilterNode.setAttribute(NAME_ATTR, transition_name) eventFiltersList.appendChild(eventFilterNode) return eventFiltersList - def save_fsm(self, fsm, xml_filename, path): + @classmethod + def save_tutorial(cls, fsm, file_obj): """ - Save fsm to disk, in the xml file specified by "xml_filename", in the - "path" folder. If the specified file doesn't exist, it will be created. + Save fsm to file + + @param fsm Tutorial to save + @param file_obj file-like object in which the serialized fsm is saved + + Side effects: + A serialized version of the Tutorial is written to file_obj. + The file is not closed automatically. """ - self.doc = doc = xml.dom.minidom.Document() - fsm_element = doc.createElement("FSM") + doc = xml.dom.minidom.Document() + fsm_element = doc.createElement(ELEM_FSM) doc.appendChild(fsm_element) + fsm_element.setAttribute("Name", fsm.name) - fsm_element.setAttribute("StartStateName", fsm.start_state_name) - statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc)) - - fsm_actions_node = self._create_action_list_node(fsm.actions, doc) - fsm_actions_node.tagName = "FSMActions" - actionsList = fsm_element.appendChild(fsm_actions_node) - - file_object = open(os.path.join(path, xml_filename), "w") - file_object.write(doc.toprettyxml()) - file_object.close() - def _get_direct_descendants_by_tag_name(self, node, name): + states = cls._create_state_dict_node(fsm.get_state_dict(), doc) + fsm_element.appendChild(states) + + file_obj.write(doc.toprettyxml()) + + @classmethod + def _get_direct_descendants_by_tag_name(cls, node, name): """ Searches in the list of direct descendants of a node to find all the node that have the given name. @@ -528,40 +644,63 @@ class XMLSerializer(Serializer): return_list.append(childNode) return return_list - -## def _load_xml_properties(self, properties_elem): -## """ -## Changes a list of properties into fully instanciated properties. -## -## @param properties_elem An XML element reprensenting a list of -## properties -## """ -## return [] - - def _load_xml_event_filters(self, filters_elem): + @classmethod + def _load_xml_transitions(cls, filters_elem): """ Loads up a list of Event Filters. @param filters_elem An XML Element representing a list of event filters + @return dict of (event, next_state) transitions, keyed by name """ - transition_list = [] - event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) - new_event_filter = None + transition_dict = {} + + #Retrieve normal transitions + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) + new_transition = None - for event_filter in event_filter_element_list: - next_state = event_filter.getAttribute(NEXT_STATE_ATTR) + 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: + continue + + new_transition = cls._load_xml_component(transition) + + if new_transition is not None: + transition_dict[transition_name] = (new_transition, next_state) + + #Retrieve automatic transitions + # XXX This is done differently as the AutomaticTransitionEvent + # cannot be loaded dynamically (yet?) + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, ELEM_AUTOTRANS) + 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: - event_filter.removeAttribute(NEXT_STATE_ATTR) + #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: - next_state = None - new_event_filter = self._load_xml_component(event_filter) + continue - if new_event_filter is not None: - transition_list.append((new_event_filter, next_state)) + transition_dict[transition_name] = (AutomaticTransitionEvent(), next_state) - return transition_list - - def _load_xml_subcomponents(self, node, properties): + return transition_dict + + @classmethod + def _load_xml_subcomponents(cls, node, properties): """ Loads all the subcomponent node below the given node and inserts them with the right property name inside the properties dictionnary. @@ -571,15 +710,16 @@ class XMLSerializer(Serializer): and the instantiated components will be stored @returns Nothing. The properties dict will contain the property->comp mapping. """ - subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) + subCompList = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) for subComp in subCompList: property_name = subComp.getAttribute("name") - internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] - internal_comp = self._load_xml_component(internal_comp_node) + internal_comp_node = cls._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] + internal_comp = cls._load_xml_component(internal_comp_node) properties[str(property_name)] = internal_comp - def _load_xml_subcomponent_lists(self, node, properties): + @classmethod + def _load_xml_subcomponent_lists(cls, node, properties): """ Loads all the subcomponent lists below the given node and stores them under the correct property name for that node. @@ -588,16 +728,17 @@ class XMLSerializer(Serializer): @param properties The dictionnary that will contain the mapping of prop->subCompList @returns Nothing. The values are returns inside the properties dict. """ - listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) + listOf_subCompListNode = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) for subCompListNode in listOf_subCompListNode: property_name = subCompListNode.getAttribute("name") subCompList = [] - for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): - subComp = self._load_xml_component(subCompNode) + for subCompNode in cls._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): + subComp = cls._load_xml_component(subCompNode) subCompList.append(subComp) properties[str(property_name)] = subCompList - def _load_xml_component(self, node): + @classmethod + def _load_xml_component(cls, node): """ Loads a single addon component instance from an Xml node. @@ -616,8 +757,8 @@ class XMLSerializer(Serializer): properties[str(prop)] = eval(node.getAttribute(prop)) # Read the complex attributes - self._load_xml_subcomponents(node, properties) - self._load_xml_subcomponent_lists(node, properties) + cls._load_xml_subcomponents(node, properties) + cls._load_xml_subcomponent_lists(node, properties) new_action = addon.create(class_name, **properties) @@ -625,99 +766,88 @@ class XMLSerializer(Serializer): return None return new_action - - def _load_xml_actions(self, actions_elem): + + @classmethod + def _load_xml_actions(cls, actions_elem): """ - Transforms an Actions element into a list of instanciated Action. + Transforms an Actions element into a dict of instanciated Action. @param actions_elem An XML Element representing a list of Actions + @return dictionary of actions keyed by name """ - reformed_actions_list = [] - actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) + action_dict = {} + actions_element_list = cls._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) for action in actions_element_list: - new_action = self._load_xml_component(action) + action_name = action.getAttribute(NAME_ATTR) + try: + #The name attribute must be removed so that it is not + # viewed as a property in load_xml_component + action.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue + new_action = cls._load_xml_component(action) - reformed_actions_list.append(new_action) + action_dict[action_name] = new_action - return reformed_actions_list - - def _load_xml_states(self, states_elem): + return action_dict + + @classmethod + def _load_xml_states(cls, states_elem): """ Takes in a States element and fleshes out a complete list of State objects. @param states_elem An XML Element that represents a list of States + @return dictionary of States """ - reformed_state_list = [] + state_dict = {} # item(0) because there is always only one <States> tag in the xml file # so states_elem should always contain only one element - states_element_list = states_elem.item(0).getElementsByTagName("State") + states_element_list = states_elem.item(0).getElementsByTagName(ELEM_STATE) for state in states_element_list: 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 = self._load_xml_actions(state.getElementsByTagName("Actions")[0]) - event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0]) - reformed_state_list.append(State(stateName, actions_list, event_filters_list)) + actions_list = 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) - return reformed_state_list + return state_dict - def load_xml_fsm(self, fsm_elem): + @classmethod + def load_xml_tutorial(cls, fsm_elem): """ Takes in an XML element representing an FSM and returns the fully crafted FSM. @param fsm_elem The XML element that describes a FSM + @return Tutorial loaded from xml element """ # Load the FSM's name and start state's name fsm_name = fsm_elem.getAttribute("Name") - fsm_start_state_name = None - try: - fsm_start_state_name = fsm_elem.getAttribute("StartStateName") - except: - pass - - fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name) - # Load the states - states = self._load_xml_states(fsm_elem.getElementsByTagName("States")) - for state in states: - fsm.add_state(state) - - # Load the actions on this FSM - actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0]) - for action in actions: - fsm.add_action(action) - - # Load the event filters - events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0]) - for event, next_state in events: - fsm.add_event_filter(event, next_state) - - return fsm + states_dict = cls._load_xml_states(fsm_elem.getElementsByTagName(ELEM_STATES)) + fsm = Tutorial(fsm_name, states_dict) - - def load_fsm(self, guid, path=None): + return fsm + + @classmethod + def load_tutorial(cls, tutorial_file): """ - Load fsm from xml file whose .ini file guid match argument guid. + Load fsm from xml file + @param tutorial_file file-like object to read the fsm from + @return Tutorial object that was loaded from the file """ - # Fetch the directory (if any) - bundler = TutorialBundler(guid) - tutorial_dir = bundler.get_tutorial_path(guid) - - # Open the XML file - tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME) - xml_dom = xml.dom.minidom.parse(tutorial_file) - fsm_elem = xml_dom.getElementsByTagName("FSM")[0] - - return self.load_xml_fsm(fsm_elem) + fsm_elem = xml_dom.getElementsByTagName(ELEM_FSM)[0] - + return cls.load_xml_tutorial(fsm_elem) + class TutorialBundler(object): """ This class provide the various data handling methods useable by the tutorial @@ -850,11 +980,11 @@ class TutorialBundler(object): path = os.path.join(self.Path, "meta.ini") config.read(path) xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) - serializer.save_fsm(fsm, xml_filename, self.Path) + serializer.save_tutorial(fsm, xml_filename, self.Path) @staticmethod def add_resources(typename, file): """ - Add ressources to metadata. + Add resources to metadata. """ raise NotImplementedError("add_resources not implemented") |