Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-10-21 04:34:27 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-10-21 04:34:27 (GMT)
commitb4e9ca55fc02458a9df04fa7df4d882b79d752be (patch)
tree1d5c075046d260580c9eeb27d0d7d09045a7d4b5
parent6ef85a852323beaf08ffe89194d388e210e1ce3d (diff)
parent912528253fcf1fc43c1a2d02ffe6e540fe60d8e7 (diff)
Merge branch 'master' of git://git.sugarlabs.org/tutorius/mainline
-rw-r--r--addons/bubblemessage.py11
-rw-r--r--addons/chainaction.py44
-rw-r--r--addons/clickaction.py52
-rw-r--r--addons/dialogmessage.py2
-rw-r--r--addons/disablewidget.py59
-rw-r--r--addons/gtkwidgeteventfilter.py10
-rw-r--r--addons/gtkwidgettypefilter.py100
-rw-r--r--addons/oncewrapper.py59
-rw-r--r--addons/readfile.py56
-rw-r--r--addons/timerevent.py73
-rw-r--r--addons/triggereventfilter.py46
-rw-r--r--addons/typetextaction.py57
-rw-r--r--addons/widgetidentifyaction.py47
-rw-r--r--tests/addontests.py50
-rw-r--r--tests/coretests.py217
-rw-r--r--tests/probetests.py63
-rw-r--r--tests/propertiestests.py171
-rwxr-xr-xtests/run-tests.py74
-rw-r--r--tests/serializertests.py4
-rw-r--r--tests/storetests.py107
-rw-r--r--tutorius/TProbe.py506
-rw-r--r--tutorius/actions.py153
-rw-r--r--tutorius/addon.py1
-rw-r--r--tutorius/bundler.py121
-rw-r--r--tutorius/constraints.py5
-rw-r--r--tutorius/core.py190
-rw-r--r--tutorius/creator.py14
-rw-r--r--tutorius/dbustools.py41
-rw-r--r--tutorius/engine.py48
-rw-r--r--tutorius/filters.py132
-rw-r--r--tutorius/linear_creator.py3
-rw-r--r--tutorius/overlayer.py6
-rw-r--r--tutorius/properties.py85
-rw-r--r--tutorius/service.py85
-rw-r--r--tutorius/services.py3
-rw-r--r--tutorius/store.py173
36 files changed, 2276 insertions, 592 deletions
diff --git a/addons/bubblemessage.py b/addons/bubblemessage.py
index c499bdb..2bd2d31 100644
--- a/addons/bubblemessage.py
+++ b/addons/bubblemessage.py
@@ -13,14 +13,17 @@
# 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.actions import Action, DragWrapper
+from sugar.tutorius.properties import TStringProperty, TArrayProperty
+from sugar.tutorius import overlayer
+from sugar.tutorius.services import ObjectStore
class BubbleMessage(Action):
message = TStringProperty("Message")
# Create the position as an array of fixed-size 2
- position = TArrayProperty([0,0], 2, 2)
+ position = TArrayProperty((0,0), 2, 2)
# Do the same for the tail position
- tail_pos = TArrayProperty([0,0], 2, 2)
+ tail_pos = TArrayProperty((0,0), 2, 2)
def __init__(self, message=None, position=None, speaker=None, tail_pos=None):
"""
@@ -94,7 +97,7 @@ class BubbleMessage(Action):
def exit_editmode(self, *args):
x,y = self._drag.position
- self.position = [int(x), int(y)]
+ self.position = (int(x), int(y))
if self._drag:
self._drag.draggable = False
self._drag = None
diff --git a/addons/chainaction.py b/addons/chainaction.py
new file mode 100644
index 0000000..43c4fa4
--- /dev/null
+++ b/addons/chainaction.py
@@ -0,0 +1,44 @@
+# 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 *
+
+class ChainAction(Action):
+ actions = TAddonListProperty()
+
+ """Utility class to allow executing actions in a specific order"""
+ def __init__(self, actions=[]):
+ """ChainAction(action1, ... ) builds a chain of actions"""
+ Action.__init__(self)
+ self.actions = actions
+
+ def do(self,**kwargs):
+ """do() each action in the chain"""
+ for act in self.actions:
+ act.do(**kwargs)
+
+ def undo(self):
+ """undo() each action in the chain, starting with the last"""
+ for act in reversed(self.actions):
+ act.undo()
+
+__action__ = {
+ 'name': 'ChainAction',
+ 'display_name' : 'Chain of actions',
+ 'icon' : 'chain',
+ 'class' : ChainAction,
+ 'mandatory_props' : ['actions']
+}
diff --git a/addons/clickaction.py b/addons/clickaction.py
new file mode 100644
index 0000000..828dd75
--- /dev/null
+++ b/addons/clickaction.py
@@ -0,0 +1,52 @@
+# 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 import gtkutils
+from sugar.tutorius.actions import *
+
+class ClickAction(Action):
+ """
+ Action that simulate a click on a widget
+ Work on any widget that implements a clicked() method
+
+ @param widget The threehish representation of the widget
+ """
+ widget = TStringProperty("")
+ def __init__(self, widget):
+ Action.__init__(self)
+ self.widget = widget
+
+ def do(self):
+ """
+ click the widget
+ """
+ realWidget = gtkutils.find_widget(ObjectStore().activity, self.widget)
+ if hasattr(realWidget, "clicked"):
+ realWidget.clicked()
+
+ def undo(self):
+ """
+ No undo
+ """
+ pass
+
+__action__ = {
+ 'name' : 'ClickAction',
+ 'display_name' : 'Click',
+ 'icon' : 'format-justify-center',
+ 'class' : ClickAction,
+ 'mandatory_props' : ['widget']
+}
diff --git a/addons/dialogmessage.py b/addons/dialogmessage.py
index 298466a..f15f256 100644
--- a/addons/dialogmessage.py
+++ b/addons/dialogmessage.py
@@ -20,7 +20,7 @@ from sugar.tutorius.actions import *
class DialogMessage(Action):
message = TStringProperty("Message")
- position = TArrayProperty([0, 0], 2, 2)
+ position = TArrayProperty((0, 0), 2, 2)
def __init__(self, message=None, position=None):
"""
diff --git a/addons/disablewidget.py b/addons/disablewidget.py
new file mode 100644
index 0000000..ce3f235
--- /dev/null
+++ b/addons/disablewidget.py
@@ -0,0 +1,59 @@
+# 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 import gtkutils
+from sugar.tutorius.services import ObjectStore
+
+class DisableWidgetAction(Action):
+ target = TStringProperty("0")
+
+ def __init__(self, target):
+ """Constructor
+ @param target target treeish
+ """
+ Action.__init__(self)
+ if target is not None:
+ self.target = target
+ self._widget = None
+
+ def do(self):
+ """Action do"""
+ os = ObjectStore()
+ if os.activity:
+ self._widget = gtkutils.find_widget(os.activity, self.target)
+ if self._widget:
+ # If we have an object whose sensitivity we can query, we will
+ # keep it to reset it in the undo() method
+ if hasattr(self._widget, 'get_sensitive') and callable(self._widget.get_sensitive):
+ self._previous_sensitivity = self._widget.get_sensitive()
+ self._widget.set_sensitive(False)
+
+ def undo(self):
+ """Action undo"""
+ if self._widget:
+ if hasattr(self, '_previous_sensitivity'):
+ self._widget.set_sensitive(self._previous_sensitivity)
+ else:
+ self._widget.set_sensitive(True)
+
+__action__ = {
+ 'name' : 'DisableWidgetAction',
+ 'display_name' : 'Disable Widget',
+ 'icon' : 'stop',
+ 'class' : DisableWidgetAction,
+ 'mandatory_props' : ['target']
+}
diff --git a/addons/gtkwidgeteventfilter.py b/addons/gtkwidgeteventfilter.py
index cbfb00c..5497af4 100644
--- a/addons/gtkwidgeteventfilter.py
+++ b/addons/gtkwidgeteventfilter.py
@@ -13,8 +13,9 @@
# 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.filters import *
-from sugar.tutorius.properties import *
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TUAMProperty, TStringProperty
+from sugar.tutorius.gtkutils import find_widget
class GtkWidgetEventFilter(EventFilter):
"""
@@ -23,13 +24,12 @@ class GtkWidgetEventFilter(EventFilter):
object_id = TUAMProperty()
event_name = TStringProperty("clicked")
- def __init__(self, next_state=None, object_id=None, event_name=None):
+ def __init__(self, object_id=None, event_name=None):
"""Constructor
- @param next_state default EventFilter param, passed on to EventFilter
@param object_id object fqdn-style identifier
@param event_name event to attach to
"""
- super(GtkWidgetEventFilter,self).__init__(next_state)
+ super(GtkWidgetEventFilter,self).__init__()
self._callback = None
self.object_id = object_id
self.event_name = event_name
diff --git a/addons/gtkwidgettypefilter.py b/addons/gtkwidgettypefilter.py
new file mode 100644
index 0000000..16673c1
--- /dev/null
+++ b/addons/gtkwidgettypefilter.py
@@ -0,0 +1,100 @@
+# 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.filters import *
+from sugar.tutorius.properties import *
+from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.gtkutils import find_widget
+
+import logging
+logger = logging.getLogger("GtkWidgetTypeFilter")
+
+class GtkWidgetTypeFilter(EventFilter):
+ """
+ Event Filter that listens for keystrokes on a widget
+ """
+ object_id = TStringProperty("")
+ text = TStringProperty("")
+ strokes = TArrayProperty([])
+
+ def __init__(self, next_state, object_id, text=None, strokes=None):
+ """Constructor
+ @param next_state default EventFilter param, passed on to EventFilter
+ @param object_id object tree-ish identifier
+ @param text resulting text expected
+ @param strokes list of strokes expected
+
+ At least one of text or strokes must be supplied
+ """
+ super(GtkWidgetTypeFilter, self).__init__(next_state)
+ self.object_id = object_id
+ self.text = text
+ self._captext = ""
+ self.strokes = strokes
+ self._capstrokes = []
+ self._widget = None
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install handlers
+ @param callback default EventFilter callback arg
+ """
+ super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
+ logger.debug("~~~GtkWidgetTypeFilter install")
+ activity = ObjectStore().activity
+ if activity is None:
+ logger.error("No activity")
+ raise RuntimeWarning("no activity in the objectstore")
+
+ self._widget = find_widget(activity, self.object_id)
+ if self._widget:
+ self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
+ logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self.object_id) )
+
+ def remove_handlers(self):
+ """remove handlers"""
+ super(GtkWidgetTypeFilter, self).remove_handlers()
+ #if an event was connected, disconnect it
+ if self._handler_id:
+ self._widget.handler_disconnect(self._handler_id)
+ self._handler_id=None
+
+ def __keypress_cb(self, widget, event, *args):
+ """keypress callback"""
+ logger.debug("~~~keypressed!")
+ key = event.keyval
+ keystr = event.string
+ logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
+ self._capstrokes += [key]
+ #TODO Treat other stuff, such as arrows
+ if key == gtk.keysyms.BackSpace:
+ self._captext = self._captext[:-1]
+ else:
+ self._captext = self._captext + keystr
+
+ logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
+ if not self.strokes is None and self.strokes in self._capstrokes:
+ self.do_callback()
+ if not self.text is None and self.text in self._captext:
+ self.do_callback()
+
+__event__ = {
+ 'name' : 'GtkWidgetTypeFilter',
+ 'display_name' : 'Widget Filter',
+ 'icon' : '',
+ 'class' : GtkWidgetTypeFilter,
+ 'mandatory_props' : ['next_state', 'object_id']
+}
diff --git a/addons/oncewrapper.py b/addons/oncewrapper.py
new file mode 100644
index 0000000..97f4752
--- /dev/null
+++ b/addons/oncewrapper.py
@@ -0,0 +1,59 @@
+# 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 *
+
+class OnceWrapper(Action):
+ """
+ Wraps a class to perform an action once only
+
+ This ConcreteActions's do() method will only be called on the first do()
+ and the undo() will be callable after do() has been called
+ """
+
+ action = TAddonProperty()
+
+ def __init__(self, action):
+ Action.__init__(self)
+ self._called = False
+ self._need_undo = False
+ self.action = action
+
+ def do(self):
+ """
+ Do the action only on the first time
+ """
+ if not self._called:
+ self._called = True
+ self.action.do()
+ self._need_undo = True
+
+ def undo(self):
+ """
+ Undo the action if it's been done
+ """
+ if self._need_undo:
+ self.action.undo()
+ self._need_undo = False
+
+
+__action__ = {
+ 'name' : 'OnceWrapper',
+ 'display_name' : 'Execute an action only once',
+ 'icon' : 'once_wrapper',
+ 'class' : OnceWrapper,
+ 'mandatory_props' : ['action']
+}
diff --git a/addons/readfile.py b/addons/readfile.py
new file mode 100644
index 0000000..0d276b9
--- /dev/null
+++ b/addons/readfile.py
@@ -0,0 +1,56 @@
+# 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
+
+from sugar.tutorius.actions import Action
+from sugar.tutorius.properties import TFileProperty
+from sugar.tutorius.services import ObjectStore
+
+class ReadFile(Action):
+ filename = TFileProperty(None)
+
+ def __init__(self, filename=None):
+ """
+ Calls activity.read_file to restore a specified state to an activity
+ like when restored from the journal.
+ @param filename Path to the file to read
+ """
+ Action.__init__(self)
+
+ if filename:
+ self.filename=filename
+
+ def do(self):
+ """
+ Perform the action, call read_file on the activity
+ """
+ if os.path.isfile(str(self.filename)):
+ ObjectStore().activity.read_file(self.filename)
+
+ def undo(self):
+ """
+ Not undoable
+ """
+ pass
+
+__action__ = {
+ "name" : "ReadFile",
+ "display_name" : "Read File",
+ "icon" : "message-bubble", #FIXME
+ "class" : ReadFile,
+ "mandatory_props" : ["filename"]
+}
diff --git a/addons/timerevent.py b/addons/timerevent.py
new file mode 100644
index 0000000..c7374d0
--- /dev/null
+++ b/addons/timerevent.py
@@ -0,0 +1,73 @@
+# 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 gobject
+
+from sugar.tutorius.filters import EventFilter
+from sugar.tutorius.properties import TIntProperty
+
+class TimerEvent(EventFilter):
+ """
+ TimerEvent is a special EventFilter that uses gobject
+ timeouts to trigger a state change after a specified amount
+ of time. It must be used inside a gobject main loop to work.
+ """
+ timeout = TIntProperty(15, 0)
+
+ def __init__(self, timeout=None):
+ """Constructor.
+
+ @param timeout timeout in seconds
+ """
+ super(TimerEvent,self).__init__()
+ if timeout:
+ self.timeout = timeout
+ self._handler_id = None
+
+ def install_handlers(self, callback, **kwargs):
+ """install_handlers creates the timer and starts it"""
+ super(TimerEvent,self).install_handlers(callback, **kwargs)
+ #Create the timer
+ self._handler_id = gobject.timeout_add_seconds(self.timeout, self._timeout_cb)
+
+ def remove_handlers(self):
+ """remove handler removes the timer"""
+ super(TimerEvent,self).remove_handlers()
+ if self._handler_id:
+ try:
+ #XXX What happens if this was already triggered?
+ #remove the timer
+ gobject.source_remove(self._handler_id)
+ except:
+ pass
+
+ def _timeout_cb(self):
+ """
+ _timeout_cb triggers the eventfilter callback.
+
+ It is necessary because gobject timers only stop if the callback they
+ trigger returns False
+ """
+ self.do_callback()
+ return False #Stops timeout
+
+__event__ = {
+ "name" : "TimerEvent",
+ "display_name" : "Timed transition",
+ "icon" : "clock",
+ "class" : TimerEvent,
+ "mandatory_props" : ["timeout"]
+}
diff --git a/addons/triggereventfilter.py b/addons/triggereventfilter.py
new file mode 100644
index 0000000..06c0995
--- /dev/null
+++ b/addons/triggereventfilter.py
@@ -0,0 +1,46 @@
+# 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.filters import *
+from sugar.tutorius.properties import *
+
+class TriggerEventFilter(EventFilter):
+ """
+ This event filter can be triggered by simply calling its do_callback function.
+
+ Used to fake events and see the effect on the FSM.
+ """
+ def __init__(self, next_state):
+ EventFilter.__init__(self, next_state)
+ self.toggle_on_callback = False
+
+ def install_handlers(self, callback, **kwargs):
+ """
+ Forsakes the incoming callback function and just set the inner one.
+ """
+ self._callback = self._inner_cb
+
+ def _inner_cb(self, event_filter):
+ self.toggle_on_callback = not self.toggle_on_callback
+
+__event__ = {
+ 'name' : 'TriggerEventFilter',
+ 'display_name' : 'Triggerable event filter (test only)',
+ 'icon' : '',
+ 'class' : TriggerEventFilter,
+ 'mandatory_props' : ['next_state'],
+ 'test' : True
+}
diff --git a/addons/typetextaction.py b/addons/typetextaction.py
new file mode 100644
index 0000000..fee66e5
--- /dev/null
+++ b/addons/typetextaction.py
@@ -0,0 +1,57 @@
+# 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 import gtkutils
+
+class TypeTextAction(Action):
+ """
+ Simulate a user typing text in a widget
+ Work on any widget that implements a insert_text method
+
+ @param widget The treehish representation of the widget
+ @param text the text that is typed
+ """
+ widget = TStringProperty("")
+ text = TStringProperty("")
+
+ def __init__(self, widget, text):
+ Action.__init__(self)
+
+ self.widget = widget
+ self.text = text
+
+ def do(self, **kwargs):
+ """
+ Type the text
+ """
+ widget = gtkutils.find_widget(ObjectStore().activity, self.widget)
+ if hasattr(widget, "insert_text"):
+ widget.insert_text(self.text, -1)
+
+ def undo(self):
+ """
+ no undo
+ """
+ pass
+
+__action__ = {
+ 'name' : 'TypeTextAction',
+ 'display_name' : 'Type text',
+ 'icon' : 'format-justify-center',
+ 'class' : TypeTextAction,
+ 'mandatory_props' : ['widgetUAM', 'text']
+}
diff --git a/addons/widgetidentifyaction.py b/addons/widgetidentifyaction.py
new file mode 100644
index 0000000..3c66211
--- /dev/null
+++ b/addons/widgetidentifyaction.py
@@ -0,0 +1,47 @@
+# 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.editor import WidgetIdentifier
+
+class WidgetIdentifyAction(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 = WidgetIdentifier(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : 'WidgetIdentifyAction',
+ "display_name" : 'Widget Identifier',
+ "icon" : 'viewmag1',
+ "class" : WidgetIdentifyAction,
+ "mandatory_props" : [],
+ 'test' : True
+}
diff --git a/tests/addontests.py b/tests/addontests.py
new file mode 100644
index 0000000..5fb4f61
--- /dev/null
+++ b/tests/addontests.py
@@ -0,0 +1,50 @@
+# Copyright (C) 2009, Tutorius.org
+# Copyright (C) 2009, Simon Poirier <simpoir@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
+
+import unittest
+
+from sugar.tutorius import addon
+
+class AddonTest(unittest.TestCase):
+ def test_create_constructor_fail(self):
+ try:
+ obj = addon.create("BubbleMessage", wrong_param=True, second_wrong="This", last_wrong=12, unknown=13.4)
+ assert False, "Constructor with wrong parameter should raise an exception"
+ except:
+ pass
+
+ def test_create_wrong_addon(self):
+ try:
+ obj = addon.create("Non existing addon name")
+ assert False, "Addon creator should raise an exception when the requested addon is unknown"
+ except:
+ pass
+
+ def test_create(self):
+ obj = addon.create("BubbleMessage", message="Hi!", position=[12,31])
+
+ assert obj is not None
+
+ def test_reload_addons(self):
+ addon._cache = None
+ assert len(addon.list_addons()) > 0, "Addons should be reloaded upon cache clear"
+
+ def test_get_addon_meta(self):
+ addon._cache = None
+ meta = addon.get_addon_meta("BubbleMessage")
+ assert meta.keys() == ['mandatory_props', 'class', 'display_name', 'name', 'icon',]
diff --git a/tests/coretests.py b/tests/coretests.py
index f90374f..4f564c8 100644
--- a/tests/coretests.py
+++ b/tests/coretests.py
@@ -28,6 +28,7 @@ and event filters. Those are in their separate test module
import unittest
+import copy
import logging
from sugar.tutorius.actions import *
from sugar.tutorius.addon import *
@@ -49,6 +50,28 @@ class SimpleTutorial(Tutorial):
def set_state(self, name):
self.current_state_name = name
+class TutorialTest(unittest.TestCase):
+ """Tests the tutorial functions that are not covered elsewhere."""
+ def test_detach(self):
+ class Activity(object):
+ name = "this"
+
+ activity1 = Activity()
+ activity2 = Activity()
+
+ fsm = FiniteStateMachine("Sample example")
+
+ tutorial = Tutorial("Test tutorial", fsm)
+
+ assert tutorial.activity == None, "There is a default activity in the tutorial"
+
+ tutorial.attach(activity1)
+
+ assert tutorial.activity == activity1, "Activity should have been associated to this tutorial"
+
+ tutorial.attach(activity2)
+ assert tutorial.activity == activity2, "Activity should have been changed to activity2"
+
class TutorialWithFSM(Tutorial):
"""
Fake tutorial, but associated with a FSM.
@@ -159,14 +182,11 @@ class StateTest(unittest.TestCase):
assert state.add_action(act2), "Could not add the second action"
assert state.add_action(act3), "Could not add the third action"
- # Try to add a second time an action that was already inserted
- assert state.add_action(act1) == False, "Not supposed to insert an action twice"
-
# Fetch the associated actions
actions = state.get_action_list()
# Make sure all the actions are present in the state
- assert act1 in actions and act2 in actions and act3 in actions,\
+ assert act1 in actions and act2 in actions and act3 in actions, \
"The actions were not properly inserted in the state"
# Clear the list
@@ -201,7 +221,80 @@ class StateTest(unittest.TestCase):
assert len(state.get_event_filter_list()) == 0, \
"Could not clear the event filter list properly"
+
+ def test_eq_simple(self):
+ """
+ Two empty states with the same name must be identical
+ """
+ st1 = State("Identical")
+ st2 = State("Identical")
+
+ assert st1 == st2, "Empty states with the same name should be identical"
+
+ def test_eq(self):
+ """
+ Test whether two states share the same set of actions and event filters.
+ """
+ st1 = State("Identical")
+ st2 = State("Identical")
+
+ non_state = object()
+
+ act1 = addon.create("BubbleMessage", message="Hi", position=[132,450])
+ act2 = addon.create("BubbleMessage", message="Hi", position=[132,450])
+
+ event1 = addon.create("GtkWidgetEventFilter", "nextState", "0.0.0.1.1.2.3.1", "clicked")
+
+ act3 = addon.create("DialogMessage", message="Hello again.", position=[200, 400])
+
+ # Build the first state
+ st1.add_action(act1)
+ st1.add_action(act3)
+ st1.add_event_filter(event1)
+
+ # Build the second state
+ st2.add_action(act2)
+ st2.add_action(act3)
+ st2.add_event_filter(event1)
+
+ # Make sure that they are identical for now
+ assert st1 == st2, "States should be considered as identical"
+ assert st2 == st1, "States should be considered as identical"
+ # Modify the second bubble message action
+ act2.message = "New message"
+
+ # Since one action changed in the second state, this should indicate that the states
+ # are not identical anymore
+ assert not (st1 == st2), "Action was changed and states should be different"
+ assert not (st2 == st1), "Action was changed and states should be different"
+
+ # Make sure that trying to find identity with something else than a State object fails properly
+ assert not (st1 == non_state), "Passing a non-State object should fail for identity"
+
+ st2.name = "Not identical anymore"
+ assert not(st1 == st2), "Different state names should give different states"
+ st2.name = "Identical"
+
+ st3 = copy.deepcopy(st1)
+ st3.add_action(addon.create("BubbleMessage", "Hi!", [128,264]))
+
+ assert not (st1 == st3), "States having a different number of actions should be different"
+
+ st4 = copy.deepcopy(st1)
+ st4.add_event_filter(addon.create("GtkWidgetEventFilter", "next_state", "0.0.1.1.2.2.3", "clicked"))
+
+ assert not (st1 == st4), "States having a different number of events should be different"
+
+ st5 = copy.deepcopy(st1)
+ st5._event_filters = []
+
+ st5.add_event_filter(addon.create("GtkWidgetEventFilter", "other_state", "0.1.2.3.4.1.2", "pressed"))
+
+ #import rpdb2; rpdb2.start_embedded_debugger('pass')
+ assert not (st1 == st5), "States having the same number of event filters" \
+ + " but those being different should be different"
+
class FSMTest(unittest.TestCase):
"""
This class needs to text the interface and functionality of the Finite
@@ -246,6 +339,7 @@ class FSMTest(unittest.TestCase):
assert act_second.active == False, "FSM did not teardown SECOND properly"
+
def test_state_insert(self):
"""
This is a simple test to insert, then find a state.
@@ -337,10 +431,10 @@ class FSMTest(unittest.TestCase):
# Make sure that there is no link to the removed state in the rest
# of the FSM
- assert "second" not in fsm.get_following_states("INIT"),\
+ assert "second" not in fsm.get_following_states("INIT"), \
"The link to second from INIT still exists after removal"
- assert "second" not in fsm.get_following_states("third"),\
+ assert "second" not in fsm.get_following_states("third"), \
"The link to second from third still exists after removal"
def test_set_same_state(self):
@@ -367,8 +461,116 @@ class FSMTest(unittest.TestCase):
"The action was triggered a second time, do_count = %d"%do_count
undo_count = fsm.get_state_by_name("INIT").get_action_list()[0].undo_count
- assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0,\
+ assert fsm.get_state_by_name("INIT").get_action_list()[0].undo_count == 0, \
"The action has been undone unappropriately, undo_count = %d"%undo_count
+
+ def test_setup(self):
+ fsm = FiniteStateMachine("New state machine")
+
+ try:
+ fsm.setup()
+ assert False, "fsm should throw an exception when trying to setup and not bound to a tutorial"
+ except UnboundLocalError:
+ pass
+
+ def test_setup_actions(self):
+ tut = SimpleTutorial()
+
+ states_dict = {"INIT": State("INIT")}
+ fsm = FiniteStateMachine("New FSM", state_dict=states_dict)
+
+ act = CountAction()
+ fsm.add_action(act)
+
+ fsm.set_tutorial(tut)
+
+ fsm.setup()
+
+ # Let's also test the current state name
+ assert fsm.get_current_state_name() == "INIT", "Initial state should be INIT"
+
+ assert act.do_count == 1, "Action should have been called during setup"
+
+ fsm._fsm_has_finished = True
+
+ fsm.teardown()
+
+ assert act.undo_count == 1, "Action should have been undone"
+
+ def test_string_rep(self):
+ fsm = FiniteStateMachine("Testing machine")
+
+ st1 = State("INIT")
+ st2 = State("Other State")
+ st3 = State("Final State")
+
+ st1.add_action(addon.create("BubbleMessage", "Hi!", [132,312]))
+
+ fsm.add_state(st1)
+ fsm.add_state(st2)
+ fsm.add_state(st3)
+
+ assert str(fsm) == "INIT, Final State, Other State, "
+
+ def test_eq_(self):
+ fsm = FiniteStateMachine("Identity test")
+
+ non_fsm_object = object()
+
+ assert not (fsm == non_fsm_object), "Testing with non FSM object should not give identity"
+
+ # Compare FSMs
+ act1 = CountAction()
+
+ fsm.add_action(act1)
+
+ fsm2 = copy.deepcopy(fsm)
+
+ assert fsm == fsm2
+
+ act2 = CountAction()
+ fsm2.add_action(act2)
+
+ assert not(fsm == fsm2), \
+ "FSMs having a different number of actions should be different"
+
+ fsm3 = FiniteStateMachine("Identity test")
+
+ act3 = addon.create("BubbleMessage", "Hi!", [123,312])
+ fsm3.add_action(act3)
+
+ assert not(fsm3 == fsm), \
+ "Actions having the same number of actions but different ones should be different"
+
+ st1 = State("INIT")
+
+ st2 = State("OtherState")
+
+ fsm.add_state(st1)
+ fsm.add_state(st2)
+
+ fsm4 = copy.deepcopy(fsm)
+
+ assert fsm == fsm4
+
+ st3 = State("Last State")
+
+ fsm4.add_state(st3)
+
+ assert not (fsm == fsm4), "FSMs having a different number of states should not be identical"
+
+ fsm4.remove_state("OtherState")
+
+ assert not (fsm == fsm4), "FSMs having different states should be different"
+
+ fsm4.remove_state("Last State")
+
+ st5 = State("OtherState")
+ st5.add_action(CountAction())
+
+ fsm4.add_state(st5)
+
+ assert not(fsm == fsm4), "FSMs having states with same name but different content should be different"
class FSMExplorationTests(unittest.TestCase):
def setUp(self):
@@ -425,6 +627,5 @@ class FSMExplorationTests(unittest.TestCase):
self.validate_previous_states("Fourth", ("Second"))
-
if __name__ == "__main__":
unittest.main()
diff --git a/tests/probetests.py b/tests/probetests.py
new file mode 100644
index 0000000..a440334
--- /dev/null
+++ b/tests/probetests.py
@@ -0,0 +1,63 @@
+# 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
+"""
+Probe Tests
+
+"""
+
+import unittest
+import os, sys
+import gtk
+import time
+
+from dbus.mainloop.glib import DBusGMainLoop
+import dbus
+
+from sugar.tutorius.TProbe import TProbe, ProbeProxy
+
+
+class FakeActivity(object):
+ def __init__(self):
+ self.top = gtk.Window(type=gtk.WINDOW_TOPLEVEL)
+ self.top.set_name("Top")
+
+ hbox = gtk.HBox()
+ self.top.add(hbox)
+ hbox.show()
+
+ btn1 = gtk.Button()
+ btn1.set_name("Button1")
+ hbox.pack_start(btn1)
+ btn1.show()
+ self.button = btn1
+
+class ProbeTest(unittest.TestCase):
+ def test_ping(self):
+ m = DBusGMainLoop(set_as_default=True)
+ dbus.set_default_main_loop(m)
+
+ activity = FakeActivity()
+ probe = TProbe("localhost.unittest.ProbeTest", activity.top)
+
+ #Parent, ping the probe
+ proxy = ProbeProxy("localhost.unittest.ProbeTest")
+ res = probe.ping()
+
+ assert res == "alive", "Probe should be alive"
+
+if __name__ == "__main__":
+ unittest.main()
+
diff --git a/tests/propertiestests.py b/tests/propertiestests.py
index e1f6f4b..0b8251a 100644
--- a/tests/propertiestests.py
+++ b/tests/propertiestests.py
@@ -17,6 +17,7 @@
import unittest
import uuid
import os
+import copy
from sugar.tutorius.constraints import *
from sugar.tutorius.properties import *
@@ -83,7 +84,128 @@ class BasePropertyTest(unittest.TestCase):
obj.prop = 2
assert obj.prop == 2, "Unable to set a value on base class"
+
+ def test_eq_(self):
+ class klass(TPropContainer):
+ prop = TutoriusProperty()
+ obj = klass()
+
+ obj2 = klass()
+
+ assert obj == obj2, "Base property containers should be identical"
+class AdvancedPropertyTest(unittest.TestCase):
+ def test_properties_groups(self):
+ """
+ Tests complex properties containers for identity.
+ """
+
+ class klass1(TPropContainer):
+ message = TutoriusProperty()
+ property = TutoriusProperty()
+ data = TutoriusProperty()
+
+ class klass3(TPropContainer):
+ property = TutoriusProperty()
+ message = TutoriusProperty()
+ data = TutoriusProperty()
+ extra_prop = TutoriusProperty()
+
+ class klass4(TPropContainer):
+ property = TutoriusProperty()
+ message = TutoriusProperty()
+ data = TFloatProperty(13.0)
+
+ obj1 = klass1()
+ obj1.property = 12
+ obj1.message = "Initial message"
+ obj1.data = [132, 208, 193, 142]
+
+ obj2 = klass1()
+ obj2.property = 12
+ obj2.message = "Initial message"
+ obj2.data = [132, 208, 193, 142]
+
+ obj3 = klass3()
+ obj3.property = 12
+ obj3.message = "Initial message"
+ obj3.data = [132, 208, 193, 142]
+ obj3.extra_prop = "Suprprise!"
+
+ obj4 = klass4()
+ obj4.property = 12
+ obj4.message = "Initial message"
+ obj4.data = 13.4
+
+ # Ensure that both obj1 and obj2 are identical (they have the same list of
+ # properties and they have the same values
+ assert obj1 == obj1, "Identical objects were considered as different"
+
+ # Ensure that obj1 is different from obj3, since obj3 has an extra property
+ assert not (obj1 == obj3), "Objects should not be identical since obj3 has more props"
+ assert not (obj3 == obj1), "Objects should not be identical since obj3 has more properties"
+
+ # Ensure that properties of different type are considered as different
+ assert not (obj1 == obj4), "Properties of different type should not be equal"
+
+ def test_addon_properties(self):
+ """Test an addon property.
+
+ This tests creates a class with a single addon property (klass1) and
+ assigns a new addon to it (inner1)."""
+ class klass1(TPropContainer):
+ addon = TAddonProperty()
+
+ class inner1(TPropContainer):
+ internal = TutoriusProperty()
+ def __init__(self, value):
+ TPropContainer.__init__(self)
+ self.internal = value
+
+ obj1 = klass1()
+ obj1.addon = inner1("Hi!")
+
+ obj2 = klass1()
+ obj2.addon = inner1("Hi!")
+
+ assert obj1 == obj2, "Identical objects with addon properties were treated as different"
+
+ obj3 = klass1()
+ obj3.addon = inner1("Hello!")
+
+ assert not (obj1 == obj3), "Objects with addon property having a different value should be considered different"
+
+ def test_addonlist_properties(self):
+ class klass1(TPropContainer):
+ addon_list = TAddonListProperty()
+
+ class inner1(TPropContainer):
+ message = TutoriusProperty()
+ data = TutoriusProperty()
+ def __init__(self, message, data):
+ TPropContainer.__init__(self)
+ self.message = message
+ self.data = data
+
+ class inner2(TPropContainer):
+ message = TutoriusProperty()
+ other_data = TutoriusProperty()
+ def __init__(self, message, data):
+ TPropContainer.__init__(self)
+ self.message = message
+ self.other_data = data
+
+ obj1 = klass1()
+ obj1.addon_list = [inner1('Hi!', 12), inner1('Hello.', [1,2])]
+ obj2 = klass1()
+ obj2.addon_list = [inner1('Hi!', 12), inner1('Hello.', [1,2])]
+
+ assert obj1 == obj2, "Addon lists with the same containers were considered different"
+
+ obj3 = klass1()
+ obj3.addon_list = [inner1('Hi!', 12), inner2('Hello.', [1,2])]
+ assert not (obj1 == obj3), "Differently named properties should be considered different in the addon list tests"
+
class TIntPropertyTest(unittest.TestCase):
def test_int_property(self):
class klass(TPropContainer):
@@ -379,12 +501,18 @@ class TEnumPropertyTest(unittest.TestCase):
try_wrong_values(self.obj)
class TFilePropertyTest(unittest.TestCase):
+ root_folder = "/tmp/tutorius"
+
def setUp(self):
+ try:
+ os.mkdir(self.root_folder)
+ except:
+ pass
# Create some sample, unique files for the tests
- self.temp_filename1 = "sample_file1_" + str(uuid.uuid1()) + ".txt"
+ self.temp_filename1 = os.path.join(self.root_folder, "sample_file1_" + str(uuid.uuid1()) + ".txt")
self.temp_file1 = file(self.temp_filename1, "w")
self.temp_file1.close()
- self.temp_filename2 = "sample_file2_" + str(uuid.uuid1()) + ".txt"
+ self.temp_filename2 = os.path.join(self.root_folder, "sample_file2_" + str(uuid.uuid1()) + ".txt")
self.temp_file2 = file(self.temp_filename2, "w")
self.temp_file2.close()
@@ -412,6 +540,45 @@ class TFilePropertyTest(unittest.TestCase):
except FileConstraintError:
pass
+class TAddonPropertyTest(unittest.TestCase):
+ def test_wrong_value(self):
+ class klass1(TPropContainer):
+ addon = TAddonProperty()
+
+ class wrongAddon(object):
+ pass
+
+ obj1 = klass1()
+ obj1.addon = klass1()
+
+ try:
+ obj1.addon = wrongAddon()
+ assert False, "Addon Property should not accept non-TPropContainer values"
+ except ValueError:
+ pass
+
+class TAddonPropertyList(unittest.TestCase):
+ def test_wrong_value(self):
+ class klass1(TPropContainer):
+ addonlist = TAddonListProperty()
+
+ class wrongAddon(object):
+ pass
+
+ obj1 = klass1()
+
+ obj1.addonlist = [klass1(), klass1()]
+
+ try:
+ obj1.addonlist = klass1()
+ assert False, "TAddonPropeprty shouldn't accept anything else than a list"
+ except ValueError:
+ pass
+
+ try:
+ obj1.addonlist = [klass1(), klass1(), wrongAddon(), klass1()]
+ except ValueError:
+ pass
if __name__ == "__main__":
unittest.main()
diff --git a/tests/run-tests.py b/tests/run-tests.py
deleted file mode 100755
index d41aa0a..0000000
--- a/tests/run-tests.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/python
-# This is a dumb script to run tests on the sugar-jhbuild installed files
-# The path added is the default path for the jhbuild build
-
-INSTALL_PATH="../../../../../../install/lib/python2.5/site-packages/"
-
-import os, sys
-sys.path.insert(0,
- os.path.abspath(INSTALL_PATH)
-)
-
-FULL_PATH = os.path.join(INSTALL_PATH,"sugar/tutorius")
-SUBDIRS = ["uam"]
-GLOB_PATH = os.path.join(FULL_PATH,"*.py")
-import unittest
-from glob import glob
-def report_files():
- ret = glob(GLOB_PATH)
- for dir in SUBDIRS:
- ret += glob(os.path.join(FULL_PATH,dir,"*.py"))
- return ret
-
-import sys
-if __name__=='__main__':
- if "--coverage" in sys.argv:
- sys.argv=[arg for arg in sys.argv if arg != "--coverage"]
- import coverage
- coverage.erase()
- #coverage.exclude('raise NotImplementedError')
- coverage.start()
-
- import coretests
- import servicestests
- import gtkutilstests
- #import overlaytests # broken
- import linear_creatortests
- import actiontests
- import uamtests
- import filterstests
- import constraintstests
- import propertiestests
- import serializertests
- suite = unittest.TestSuite()
- suite.addTests(unittest.findTestCases(coretests))
- suite.addTests(unittest.findTestCases(servicestests))
- suite.addTests(unittest.findTestCases(gtkutilstests))
- #suite.addTests(unittest.findTestCases(overlaytests)) # broken
- suite.addTests(unittest.findTestCases(linear_creatortests))
- suite.addTests(unittest.findTestCases(actiontests))
- suite.addTests(unittest.findTestCases(uamtests))
- suite.addTests(unittest.findTestCases(filterstests))
- suite.addTests(unittest.findTestCases(constraintstests))
- suite.addTests(unittest.findTestCases(propertiestests))
- suite.addTests(unittest.findTestCases(serializertests))
- runner = unittest.TextTestRunner()
- runner.run(suite)
- coverage.stop()
- coverage.report(report_files())
- coverage.erase()
- else:
- from coretests import *
- from servicestests import *
- from gtkutilstests import *
- #from overlaytests import * # broken
- from actiontests import *
- from linear_creatortests import *
- from uamtests import *
- from filterstests import *
- from constraintstests import *
- from propertiestests import *
- from actiontests import *
- from serializertests import *
-
- unittest.main()
diff --git a/tests/serializertests.py b/tests/serializertests.py
index c939b7a..2f2e287 100644
--- a/tests/serializertests.py
+++ b/tests/serializertests.py
@@ -164,7 +164,7 @@ class XMLSerializerTest(unittest.TestCase):
self.test_save()
reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
- assert self.fsm.is_identical(reloaded_fsm), "Expected equivalence before saving vs after loading."
+ assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
def test_all_filters(self):
"""
@@ -190,7 +190,7 @@ class XMLSerializerTest(unittest.TestCase):
reloaded_fsm = xml_ser.load_fsm(str(self.uuid))
- assert self.fsm.is_identical(reloaded_fsm), "Expected equivalence before saving vs after loading."
+ assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading."
if __name__ == "__main__":
unittest.main()
diff --git a/tests/storetests.py b/tests/storetests.py
new file mode 100644
index 0000000..da20c00
--- /dev/null
+++ b/tests/storetests.py
@@ -0,0 +1,107 @@
+# 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
+
+from sugar.tutorius.store import *
+
+g_tutorial_id = '114db454-b2a1-11de-8cfc-001f5bf747dc'
+g_other_id = '47efc6ee-b2a3-11de-8cfc-001f5bf747dc'
+
+class StoreProxyTest(unittest.TestCase):
+ def setUp(self):
+ self.store = StoreProxy()
+
+ def tearDown(self):
+ pass
+
+ def test_get_categories(self):
+ categories = self.store.get_categories()
+
+ assert isinstance(categories, list), "categories should be a list"
+
+ def test_get_tutorials(self):
+ self.store.get_tutorials()
+
+ def test_get_tutorial_collection(self):
+ collection_list = self.store.get_tutorial_collection('top5_rating')
+
+ assert isinstance(collection_list, list), "get_tutorial_collection should return a list"
+
+ def test_get_latest_version(self):
+ version_dict = self.store.get_latest_version([])
+
+ assert isinstance(version_dict, dict)
+
+ def test_download_tutorial(self):
+ tutorial = self.store.download_tutorial(g_tutorial_id)
+
+ assert tutorial is not None
+
+ def test_login(self):
+ assert self.store.login("unknown_user", "random_password")
+
+ def test_register_new_user(self):
+ user_info = {
+ 'name' : "Albert",
+ 'last_name' : "The Tester",
+ 'location' : 'Mozambique',
+ 'email' : 'albertthetester@mozambique.org'
+ }
+
+ assert self.store.register_new_user(user_info)
+
+
+class StoreProxyLoginTest(unittest.TestCase):
+ def setUp(self):
+ self.store = StoreProxy()
+ self.store.login("unknown_user", "random_password")
+
+ def tearDown(self):
+ session_id = self.store.get_session_id()
+
+ if session_id is not None:
+ self.store.close_session()
+
+ def test_close_session(self):
+ assert self.store.close_session()
+
+ def test_get_session_id(self):
+ session_id = self.store.get_session_id()
+
+ assert session_id is not None
+
+ def test_rate(self):
+ assert self.store.rate(5, g_tutorial_id)
+
+ def test_publish(self):
+ # TODO : We need to send in a real tutorial loaded from
+ # the Vault
+ assert self.store.publish(['This should be a real tutorial...'])
+
+ def test_unpublish(self):
+ # TODO : We need to send in a real tutorial loaded from
+ # the Vault
+ self.store.publish([g_tutorial_id, 'Fake tutorial'])
+
+ assert self.store.unpublish(g_tutorial_id)
+
+ def test_update_published_tutorial(self):
+ # TODO : Run these tests with files from the Vault
+ self.store.publish([g_tutorial_id, 'Fake tutorial'])
+
+ assert self.store.update_published_tutorial(g_tutorial_id, [g_tutorial_id, 'This is an updated tutorial'])
+
diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py
new file mode 100644
index 0000000..6d7b6e2
--- /dev/null
+++ b/tutorius/TProbe.py
@@ -0,0 +1,506 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.TProbe")
+import os
+
+import gobject
+
+import dbus
+import dbus.service
+import cPickle as pickle
+
+import sugar.tutorius.addon as addon
+
+from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.properties import TPropContainer
+
+from sugar.tutorius.dbustools import remote_call, save_args
+import copy
+
+"""
+ --------------------
+ | ProbeManager |
+ --------------------
+ |
+ V
+ -------------------- ----------
+ | ProbeProxy |<---- DBus ---->| TProbe |
+ -------------------- ----------
+
+"""
+#TODO Add stub error handling for remote calls in the classes so that it will
+# be clearer how errors can be handled in the future.
+
+
+class TProbe(dbus.service.Object):
+ """ Tutorius Probe
+ Defines an entry point for Tutorius into activities that allows
+ performing actions and registering events onto an activity via
+ a DBUS Interface.
+ """
+
+ def __init__(self, activity_name, activity):
+ """
+ Create and register a TProbe for an activity.
+
+ @param activity_name unique activity_id
+ @param activity activity reference, must be a gtk container
+ """
+ LOGGER.debug("TProbe :: Creating TProbe for %s (%d)", activity_name, os.getpid())
+ LOGGER.debug("TProbe :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("TProbe :: Current gobject depth: %s", str(gobject.main_depth()))
+ # Moving the ObjectStore assignment here, in the meantime
+ # the reference to the activity shouldn't be share as a
+ # global variable but passed by the Probe to the objects
+ # that requires it
+ self._activity = activity
+
+ ObjectStore().activity = activity
+
+ self._activity_name = activity_name
+ self._session_bus = dbus.SessionBus()
+
+ # Giving a new name because _name is already used by dbus
+ self._name2 = dbus.service.BusName(activity_name, self._session_bus)
+ dbus.service.Object.__init__(self, self._session_bus, "/tutorius/Probe")
+
+ # Add the dictionary we will use to store which actions and events
+ # are known
+ self._installedActions = {}
+ self._subscribedEvents = {}
+
+ def start(self):
+ """
+ Optional method to call if the probe is not inserted into an
+ existing activity. Starts a gobject mainloop
+ """
+ mainloop = gobject.MainLoop()
+ print "Starting Probe for " + self._activity_name
+ mainloop.run()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def registered(self, service):
+ print ("Registered with: " + str(service))
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='', out_signature='s')
+ def ping(self):
+ """
+ Allows testing the connection to a Probe
+ @return string "alive"
+ """
+ return "alive"
+
+ # ------------------ Action handling --------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def install(self, pickled_action):
+ """
+ Install an action on the Activity
+ @param pickled_action string pickled action
+ @return string address of installed action
+ """
+ loaded_action = pickle.loads(str(pickled_action))
+ action = addon.create(loaded_action.__class__.__name__)
+
+ address = self._generate_action_reference(action)
+
+ self._installedActions[address] = action
+
+ if action._props:
+ action._props.update(loaded_action._props)
+
+ action.do()
+
+ return address
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='ss', out_signature='')
+ def update(self, address, action_props):
+ """
+ Update an already registered action
+ @param address string address returned by install()
+ @param action_props pickled action properties
+ @return None
+ """
+ action = self._installedActions[address]
+
+ if action._props:
+ props = pickle.loads(str(action_props))
+ action._props.update(props)
+ action.undo()
+ action.do()
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def uninstall(self, address):
+ """
+ Uninstall an action
+ @param address string address returned by install()
+ @return None
+ """
+ if self._installedActions.has_key(address):
+ action = self._installedActions[address]
+ action.undo()
+ self._installedActions.pop(address)
+
+
+ # ------------------ Event handling ---------------------------------------
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='s')
+ def subscribe(self, pickled_event):
+ """
+ Subscribe to an Event
+ @param pickled_event string pickled EventFilter
+ @return string unique name of registered event
+ """
+ #TODO Perform event unmapping once Tutorials use abstract events
+ # instead of concrete EventFilters that are tied to their
+ # implementation.
+ eventfilter = pickle.loads(str(pickled_event))
+
+ # The callback uses the event defined previously and each
+ # successive call to subscribe will register a different
+ # callback that references a different event
+ def callback(*args):
+ self.notify(eventfilter)
+
+ eventfilter.install_handlers(callback, activity=self._activity)
+
+ name = self._generate_event_reference(eventfilter)
+ self._subscribedEvents[name] = eventfilter
+
+ return name
+
+ @dbus.service.method("org.tutorius.ProbeInterface",
+ in_signature='s', out_signature='')
+ def unsubscribe(self, address):
+ """
+ Remove subscription to an event
+ @param address string adress returned by subscribe()
+ @return None
+ """
+
+ if self._subscribedEvents.has_key(address):
+ eventfilter = self._subscribedEvents[address]
+ eventfilter.remove_handlers()
+ self._subscribedEvents.pop(address)
+
+ @dbus.service.signal("org.tutorius.ProbeInterface")
+ def eventOccured(self, event):
+ # We need no processing now, the signal will be sent
+ # when the method exit
+ pass
+
+ # The actual method we will call on the probe to send events
+ def notify(self, event):
+ LOGGER.debug("TProbe :: notify event %s", str(event))
+ self.eventOccured(pickle.dumps(event))
+
+ # Return a unique name for this action
+ def _generate_action_reference(self, action):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = action.__class__.__name__
+ suffix = 1
+
+ while self._installedActions.has_key(name+str(suffix)):
+ suffix += 1
+
+ return name + str(suffix)
+
+
+ # Return a unique name for this event
+ def _generate_event_reference(self, event):
+ # TODO elavoie 2009-07-25 Should return a universal address
+ name = event.__class__.__name__
+ #Keep the counter to avoid looping all the time
+ suffix = getattr(self, '_event_ref_suffix', 0 ) + 1
+
+ while self._subscribedEvents.has_key(name+str(suffix)):
+ suffix += 1
+
+ #setattr(self, '_event_ref_suffix', suffix)
+
+ return name + str(suffix)
+
+class ProbeProxy:
+ """
+ ProbeProxy is a Proxy class for connecting to a remote TProbe.
+
+ It provides an object interface to the TProbe, which requires pickled
+ strings, across a DBus communication.
+ """
+ def __init__(self, activityName):
+ """
+ Constructor
+ @param activityName unique activity id. Must be a valid dbus bus name.
+ """
+ LOGGER.debug("ProbeProxy :: Creating ProbeProxy for %s (%d)", activityName, os.getpid())
+ LOGGER.debug("ProbeProxy :: Current gobject context: %s", str(gobject.main_context_default()))
+ LOGGER.debug("ProbeProxy :: Current gobject depth: %s", str(gobject.main_depth()))
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(activityName, "/tutorius/Probe")
+ self._probe = dbus.Interface(self._object, "org.tutorius.ProbeInterface")
+
+ self._actions = {}
+ # We keep those two data structures to be able to have multiple callbacks
+ # for the same event and be able to remove them independently
+ # _subscribedEvents holds a list of callback addresses's for each event
+ # _registeredCallbacks holds the functions to call for each address
+ self._subscribedEvents = {}
+ self._registeredCallbacks = {}
+
+
+ self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface")
+
+ def _handle_signal(self, pickled_event):
+ event = pickle.loads(str(pickled_event))
+ LOGGER.debug("ProbeProxy :: Received Event : %s %s", str(event), str(event._props.items()))
+
+ LOGGER.debug("ProbeProxy :: Currently %d events registered", len(self._registeredCallbacks))
+ if self._registeredCallbacks.has_key(event):
+ for callback in self._registeredCallbacks[event].values():
+ callback(event)
+ else:
+ for event in self._registeredCallbacks.keys():
+ LOGGER.debug("==== %s", str(event._props.items()))
+ LOGGER.debug("ProbeProxy :: Event does not appear to be registered")
+
+ def isAlive(self):
+ try:
+ return self._probe.ping() == "alive"
+ except:
+ return False
+
+ def __update_action(self, action, address):
+ LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address))
+ self._actions[action] = str(address)
+
+ def __clear_action(self, action):
+ self._actions.pop(action, None)
+
+ def install(self, action, block=False):
+ """
+ Install an action on the TProbe's activity
+ @param action Action to install
+ @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),
+ block=block)
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action 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:
+ raise RuntimeWarning("Action not installed")
+ #TODO Check error handling
+ return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block)
+
+ def uninstall(self, action, block=False):
+ """
+ Uninstall an installed action
+ @param action 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)
+
+ def __update_event(self, event, callback, address):
+ LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address))
+ # Since multiple callbacks could be associated to the same
+ # event signature, we will store multiple callbacks
+ # in a dictionary indexed by the unique address
+ # given for this subscribtion and access this
+ # dictionary from another one indexed by event
+ address = str(address)
+
+ # We use the event object as a key
+ if not self._registeredCallbacks.has_key(event):
+ self._registeredCallbacks[event] = {}
+
+ # TODO elavoie 2009-07-25 decide on a proper exception
+ # taxonomy
+ if self._registeredCallbacks[event].has_key(address):
+ # Oups, how come we have two similar addresses?
+ # send the bad news!
+ raise Exception("Probe subscribe exception, the following address already exists: " + str(address))
+
+ self._registeredCallbacks[event][address] = callback
+
+ # We will keep another dictionary to remember the
+ # event that was associated to this unique address
+ # Let's copy to make sure that even if the event
+ # passed in is modified later it won't screw up
+ # our dictionary (python pass arguments by reference)
+ self._subscribedEvents[address] = copy.copy(event)
+
+ return address
+
+ def __clear_event(self, address):
+ LOGGER.debug("ProbeProxy :: Unregistering adress %s", str(address))
+ # Cleanup everything
+ if self._subscribedEvents.has_key(address):
+ event = self._subscribedEvents[address]
+
+ if self._registeredCallbacks.has_key(event)\
+ and self._registeredCallbacks[event].has_key(address):
+ self._registeredCallbacks[event].pop(address)
+
+ if self._registeredCallbacks[event] == {}:
+ self._registeredCallbacks.pop(event)
+
+ self._subscribedEvents.pop(address)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s inconsistency : not registered", address)
+
+ def subscribe(self, event, callback, block=True):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @param block Force a synchroneous dbus call if True (Not allowed yet)
+ @return address identifier used for unsubscribing
+ """
+ LOGGER.debug("ProbeProxy :: Registering event %s", str(hash(event)))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+
+ # 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
+ return remote_call(self._probe.subscribe, (pickle.dumps(event),),
+ save_args(self.__update_event, event, callback),
+ block=block)
+
+ def unsubscribe(self, address, block=True):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address))
+ if not block:
+ raise RuntimeError("This function does not allow non-blocking mode yet")
+ if address in self._subscribedEvents.keys():
+ remote_call(self._probe.unsubscribe, (address,),
+ return_cb=save_args(self.__clear_event, address),
+ block=block)
+ else:
+ LOGGER.debug("ProbeProxy :: unsubsribe address %s failed : not registered", address)
+
+ def detach(self, block=False):
+ """
+ Detach the ProbeProxy from it's TProbe. All installed actions and
+ subscribed events should be removed.
+ """
+ for action in self._actions.keys():
+ self.uninstall(action, block)
+
+ for address in self._subscribedEvents.keys():
+ self.unsubscribe(address, block)
+
+
+class ProbeManager(object):
+ """
+ The ProbeManager provides multiplexing across multiple activity ProbeProxies
+
+ For now, it only handles one at a time, though.
+ Actually it doesn't do much at all. But it keeps your encapsulation happy
+ """
+ def __init__(self):
+ self._probes = {}
+ self._current_activity = None
+
+ def setCurrentActivity(self, activity_id):
+ if not activity_id in self._probes:
+ raise RuntimeError("Activity not attached")
+ self._current_activity = activity_id
+
+ def getCurrentActivity(self):
+ return self._current_activity
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ if activity_id in self._probes:
+ raise RuntimeWarning("Activity already attached")
+
+ self._probes[activity_id] = ProbeProxy(activity_id)
+ #TODO what do we do with this? Raise something?
+ if self._probes[activity_id].isAlive():
+ print "Alive!"
+ else:
+ print "FAil!"
+
+ def detach(self, activity_id):
+ if activity_id in self._probes:
+ probe = self._probes.pop(activity_id)
+ probe.detach()
+
+ def install(self, action, block=False):
+ """
+ Install an action on the current activity
+ @param action Action to install
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].install(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def update(self, action, newaction, block=False):
+ """
+ Update an already installed action's properties and run it again
+ @param action Action to update
+ @param newaction Action to update it with
+ @param block Force a synchroneous dbus call if True
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].update(action, newaction, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def uninstall(self, action, 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._probes[self.currentActivity].uninstall(action, block)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def subscribe(self, event, callback):
+ """
+ Register an event listener
+ @param event Event to listen for
+ @param callback callable that will be called when the event occurs
+ @return address identifier used for unsubscribing
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].subscribe(event, callback)
+ else:
+ raise RuntimeWarning("No activity attached")
+
+ def unsubscribe(self, address):
+ """
+ Unregister an event listener
+ @param address identifier given by subscribe()
+ @return None
+ """
+ if self.currentActivity:
+ return self._probes[self.currentActivity].unsubscribe(address)
+ else:
+ raise RuntimeWarning("No activity attached")
+
diff --git a/tutorius/actions.py b/tutorius/actions.py
index 0db7988..08f55cd 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -16,16 +16,14 @@
"""
This module defines Actions that can be done and undone on a state
"""
+import gtk
+
from gettext import gettext as _
-from sugar.tutorius import gtkutils, addon
-from dialog import TutoriusDialog
-import overlayer
-from sugar.tutorius.editor import WidgetIdentifier
+from sugar.tutorius import addon
from sugar.tutorius.services import ObjectStore
from sugar.tutorius.properties import *
from sugar.graphics import icon
-import gtk.gdk
class DragWrapper(object):
"""Wrapper to allow gtk widgets to be dragged around"""
@@ -177,148 +175,3 @@ class Action(TPropContainer):
self.position = [int(x), int(y)]
self.__edit_img.destroy()
-##class OnceWrapper(Action):
-## """
-## Wraps a class to perform an action once only
-##
-## This ConcreteActions's do() method will only be called on the first do()
-## and the undo() will be callable after do() has been called
-## """
-##
-## _action = TAddonProperty()
-##
-## def __init__(self, action):
-## Action.__init__(self)
-## self._called = False
-## self._need_undo = False
-## self._action = action
-##
-## def do(self):
-## """
-## Do the action only on the first time
-## """
-## if not self._called:
-## self._called = True
-## self._action.do()
-## self._need_undo = True
-##
-## def undo(self):
-## """
-## Undo the action if it's been done
-## """
-## if self._need_undo:
-## self._action.undo()
-## self._need_undo = False
-##
-##class WidgetIdentifyAction(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 = WidgetIdentifier(self.activity)
-## self._dialog.show()
-
-
-## def undo(self):
-## if self._dialog:
-## self._dialog.destroy()
-
-##class ChainAction(Action):
-## """Utility class to allow executing actions in a specific order"""
-## def __init__(self, *actions):
-## """ChainAction(action1, ... ) builds a chain of actions"""
-## Action.__init__(self)
-## self._actions = actions
-##
-## def do(self,**kwargs):
-## """do() each action in the chain"""
-## for act in self._actions:
-## act.do(**kwargs)
-##
-## def undo(self):
-## """undo() each action in the chain, starting with the last"""
-## for act in reversed(self._actions):
-## act.undo()
-
-##class DisableWidgetAction(Action):
-## def __init__(self, target):
-## """Constructor
-## @param target target treeish
-## """
-## Action.__init__(self)
-## self._target = target
-## self._widget = None
-
-## def do(self):
-## """Action do"""
-## os = ObjectStore()
-## if os.activity:
-## self._widget = gtkutils.find_widget(os.activity, self._target)
-## if self._widget:
-## self._widget.set_sensitive(False)
-
-## def undo(self):
-## """Action undo"""
-## if self._widget:
-## self._widget.set_sensitive(True)
-
-
-##class TypeTextAction(Action):
-## """
-## Simulate a user typing text in a widget
-## Work on any widget that implements a insert_text method
-##
-## @param widget The treehish representation of the widget
-## @param text the text that is typed
-## """
-## def __init__(self, widget, text):
-## Action.__init__(self)
-##
-## self._widget = widget
-## self._text = text
-##
-## def do(self, **kwargs):
-## """
-## Type the text
-## """
-## widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
-## if hasattr(widget, "insert_text"):
-## widget.insert_text(self._text, -1)
-##
-## def undo(self):
-## """
-## no undo
-## """
-## pass
-##
-##class ClickAction(Action):
-## """
-## Action that simulate a click on a widget
-## Work on any widget that implements a clicked() method
-##
-## @param widget The threehish representation of the widget
-## """
-## def __init__(self, widget):
-## Action.__init__(self)
-## self._widget = widget
-##
-## def do(self):
-## """
-## click the widget
-## """
-## widget = gtkutils.find_widget(ObjectStore().activity, self._widget)
-## if hasattr(widget, "clicked"):
-## widget.clicked()
-##
-## def undo(self):
-## """
-## No undo
-## """
-## pass
-
diff --git a/tutorius/addon.py b/tutorius/addon.py
index e311a65..15612c8 100644
--- a/tutorius/addon.py
+++ b/tutorius/addon.py
@@ -62,7 +62,6 @@ def create(name, *args, **kwargs):
except:
logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs)))
return None
- return _cache[name]['class'](*args, **kwargs)
except KeyError:
logging.error("Addon not found for class '%s'", name)
return None
diff --git a/tutorius/bundler.py b/tutorius/bundler.py
index 734c679..c9558b1 100644
--- a/tutorius/bundler.py
+++ b/tutorius/bundler.py
@@ -24,6 +24,7 @@ import logging
import os
import uuid
import xml.dom.minidom
+from xml.dom import NotFoundErr
from sugar.tutorius import addon
from sugar.tutorius.core import Tutorial, State, FiniteStateMachine
@@ -37,8 +38,10 @@ def _get_store_root():
return os.path.join(os.getenv("HOME"),
".sugar",profile_name,"tutorius","data")
# this is where activity bundled tutorials should be, under the activity bundle
-def _get_bundle_root():
- return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
+def _get_bundle_root(base_path=None):
+ base_path = base_path or os.getenv("SUGAR_BUNDLE_PATH")
+ if base_path:
+ return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data")
INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES"
INI_METADATA_SECTION = "GENERAL_METADATA"
@@ -48,46 +51,9 @@ INI_XML_FSM_PROPERTY = "FSM_FILENAME"
INI_FILENAME = "meta.ini"
TUTORIAL_FILENAME = "tutorial.xml"
NODE_COMPONENT = "Component"
-NODE_SUBCOMPONENT = "SubComponent"
-NODE_SUBCOMPONENTLIST = "SubComponentList"
-
-class Vault(object):
- """
- The Vault is the primary interface for the storage and installation of tutorials
- on the machine. It needs to accomplish the following tasks :
- - query() : Lists the
- - installTutorial() :
- - deleteTutorial() :
- - readTutorial() :
- - saveTutorial() :
- """
- def query(keyword="", category="", start_index=0, num_results=10):
- """
- Returns a list of tutorial meta-data corresponding to the keywords
- and category mentionned.
-
- @param keyword The keyword to look for in the tutorial title and description.
- @param category The category in which to look for tutorials
- @param start_index The first result to be shown (e.g. )
- @param num_results The number of results to show
- @return The list of tutorial metadata that corresponds to the query parameters.
- """
- raise NotImplementedError("The query function on the Vault is not implemented")
-
- def installTutorial(path ,force_install=False):
- """
- Inserts the tutorial inside the Vault. Once installed, it will show up
- """
- raise NotImplementedError("Installation in the Vault not supported yet")
-
- def deleteTutorial(tutorial_id):
- raise NotImplementedError("")
-
- def readTutorial(tutorial_id):
- raise NotImplementedError("")
-
- def saveTutorial(tutorial, metadata, resource_list):
- raise NotImplementedError("")
+NODE_SUBCOMPONENT = "property"
+NODE_SUBCOMPONENTLIST = "listproperty"
+NEXT_STATE_ATTR = "next_state"
class TutorialStore(object):
@@ -99,7 +65,7 @@ class TutorialStore(object):
@returns a map of tutorial {names : GUID}.
"""
# check both under the activity data and user installed folders
- paths = [_get_store_root(), _get_bundle_root()]
+ paths = [p for p in [_get_store_root(), _get_bundle_root()] if p ]
tutoGuidName = {}
@@ -127,7 +93,7 @@ class TutorialStore(object):
return tutoGuidName
- def load_tutorial(self, Guid):
+ def load_tutorial(self, Guid, bundle_path=None):
"""
Rebuilds a tutorial object from it's serialized state.
Common storing paths will be scanned.
@@ -135,15 +101,15 @@ class TutorialStore(object):
@param Guid the generic identifier of the tutorial
@returns a Tutorial object containing an FSM
"""
- bundle = TutorialBundler(Guid)
- bundle_path = bundle.get_tutorial_path()
+ bundler = TutorialBundler(Guid, bundle_path=bundle_path)
+ bundler_path = bundler.get_tutorial_path()
config = SafeConfigParser()
- config.read(os.path.join(bundle_path, INI_FILENAME))
+ config.read(os.path.join(bundler_path, INI_FILENAME))
serializer = XMLSerializer()
name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY)
- fsm = serializer.load_fsm(Guid)
+ fsm = serializer.load_fsm(Guid, bundler.Path)
tuto = Tutorial(name, fsm)
return tuto
@@ -163,13 +129,13 @@ class Serializer(object):
exception occur. If no GUID is provided, FSM is written in a new file
in the store root.
"""
- NotImplementedError
+ return NotImplementedError()
def load_fsm(self):
"""
Load fsm from disk.
"""
- NotImplementedError
+ return NotImplementedError()
class XMLSerializer(Serializer):
"""
@@ -197,9 +163,9 @@ class XMLSerializer(Serializer):
e.g.
<Component Class="OnceWrapper">
- <SubComponent property="addon">
+ <property name="addon">
<Component Class="BubbleMessage" message="'Hi!'" position="[12,32]"/>
- </SubComponent>
+ </property>
</Component>
When reloading this node, we should look up the property name for the parent
@@ -215,7 +181,7 @@ class XMLSerializer(Serializer):
that represents another component.
"""
subCompNode = doc.createElement(NODE_SUBCOMPONENT)
- subCompNode.setAttribute("property", parent_attr_name)
+ subCompNode.setAttribute("name", parent_attr_name)
subNode = self._create_component_node(comp, doc)
@@ -231,10 +197,10 @@ class XMLSerializer(Serializer):
e.g.
<Component Class="ChainAction">
- <SubComponentList property="actions">
+ <listproperty name="actions">
<Component Class="BubbleMessage" message="'Hi!'" position="[15,35]"/>
<Component Class="DialogMessage" message="'Multi-action!'" position="[45,10]"/>
- </SubComponentList>
+ </listproperty>
</Component>
When reloading this node, we should look up the property name for the parent
@@ -247,7 +213,7 @@ class XMLSerializer(Serializer):
@returns A NODE_SUBCOMPONENTLIST node with the property attribute
"""
subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST)
- subCompListNode.setAttribute("property", parent_attr_name)
+ subCompListNode.setAttribute("name", parent_attr_name)
for comp in comp_list:
compNode = self._create_component_node(comp, doc)
@@ -308,8 +274,9 @@ class XMLSerializer(Serializer):
Create and return a xml Node from a event filters.
"""
eventFiltersList = doc.createElement("EventFiltersList")
- for event_f in event_filters:
- eventFilterNode = self._create_component_node(event_f, doc)
+ for event, state in event_filters:
+ eventFilterNode = self._create_component_node(event, doc)
+ eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state))
eventFiltersList.appendChild(eventFilterNode)
return eventFiltersList
@@ -423,24 +390,29 @@ class XMLSerializer(Serializer):
@param filters_elem An XML Element representing a list of event filters
"""
- reformed_event_filters_list = []
+ transition_list = []
event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT)
new_event_filter = None
for event_filter in event_filter_element_list:
+ next_state = event_filter.getAttribute(NEXT_STATE_ATTR)
+ try:
+ event_filter.removeAttribute(NEXT_STATE_ATTR)
+ except NotFoundErr:
+ next_state = None
new_event_filter = self._load_xml_component(event_filter)
if new_event_filter is not None:
- reformed_event_filters_list.append(new_event_filter)
+ transition_list.append((new_event_filter, next_state))
- return reformed_event_filters_list
+ return transition_list
def _load_xml_subcomponents(self, node, properties):
"""
Loads all the subcomponent node below the given node and inserts them with
the right property name inside the properties dictionnary.
- @param node The parent node that contains one or many SubComponent nodes.
+ @param node The parent node that contains one or many property nodes.
@param properties A dictionnary where the subcomponent property names
and the instantiated components will be stored
@returns Nothing. The properties dict will contain the property->comp mapping.
@@ -448,7 +420,7 @@ class XMLSerializer(Serializer):
subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT)
for subComp in subCompList:
- property_name = subComp.getAttribute("property")
+ 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)
properties[str(property_name)] = internal_comp
@@ -464,7 +436,7 @@ class XMLSerializer(Serializer):
"""
listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST)
for subCompListNode in listOf_subCompListNode:
- property_name = subCompListNode.getAttribute("property")
+ 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)
@@ -568,18 +540,18 @@ class XMLSerializer(Serializer):
# Load the event filters
events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0])
- for event in events:
- fsm.add_event_filter(event)
+ for event, next_state in events:
+ fsm.add_event_filter(event, next_state)
return fsm
- def load_fsm(self, guid):
+ def load_fsm(self, guid, path=None):
"""
Load fsm from xml file whose .ini file guid match argument guid.
"""
# Fetch the directory (if any)
- tutorial_dir = self._find_tutorial_dir_with_guid(guid)
+ tutorial_dir = path or self._find_tutorial_dir_with_guid(guid)
# Open the XML file
tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME)
@@ -597,7 +569,7 @@ class TutorialBundler(object):
editor.
"""
- def __init__(self,generated_guid = None):
+ def __init__(self,generated_guid = None, bundle_path=None):
"""
Tutorial_bundler constructor. If a GUID is given in the parameter, the
Tutorial_bundler object will be associated with it. If no GUID is given,
@@ -606,6 +578,7 @@ class TutorialBundler(object):
self.Guid = generated_guid or str(uuid.uuid1())
+ #FIXME: Look for the bundle in the activity first (more specific)
#Look for the file in the path if a uid is supplied
if generated_guid:
#General store
@@ -614,9 +587,13 @@ class TutorialBundler(object):
self.Path = os.path.dirname(store_path)
else:
#Bundle store
- bundle_path = os.path.join(_get_bundle_root(), generated_guid, INI_FILENAME)
- if os.path.isfile(bundle_path):
- self.Path = os.path.dirname(bundle_path)
+ base_bundle_path = _get_bundle_root(bundle_path)
+ if base_bundle_path:
+ bundle_path = os.path.join(base_bundle_path, generated_guid, INI_FILENAME)
+ if os.path.isfile(bundle_path):
+ self.Path = os.path.dirname(bundle_path)
+ else:
+ raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
else:
raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid)
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 36abdfb..e91f23a 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -200,7 +200,10 @@ class FileConstraint(Constraint):
def validate(self, value):
# TODO : Decide on the architecture for file retrieval on disk
# Relative paths? From where? Support macros?
- #
+ # FIXME This is a hack to make cases where a default file is not valid
+ # work. It allows None values to be validated, though
+ if value is None:
+ return
if not os.path.isfile(value):
raise FileConstraintError("Non-existing file : %s"%value)
return
diff --git a/tutorius/core.py b/tutorius/core.py
index 41089f1..6030457 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -21,14 +21,12 @@ This module contains the core classes for tutorius
"""
-import gtk
import logging
-import copy
import os
-from sugar.tutorius.dialog import TutoriusDialog
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
+from sugar.tutorius.TProbe import ProbeManager
+from sugar.tutorius.dbustools import save_args
+from sugar.tutorius import addon
logger = logging.getLogger("tutorius")
@@ -36,8 +34,11 @@ 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):
+ def __init__(self, name, fsm, filename=None):
"""
Creates an unattached tutorial.
"""
@@ -51,21 +52,22 @@ class Tutorial (object):
self.state = None
self.handlers = []
- self.activity = None
+ self._probeMgr = ProbeManager()
+ self._activity_id = None
#Rest of initialisation happens when attached
- def attach(self, activity):
+ def attach(self, activity_id):
"""
Attach to a running activity
- @param activity the activity to attach to
+ @param activity_id the id of the activity to attach to
"""
#For now, absolutely detach if a previous one!
- if self.activity:
+ if self._activity_id:
self.detach()
- self.activity = activity
- ObjectStore().activity = activity
- ObjectStore().tutorial = self
+ self._activity_id = activity_id
+ self._probeMgr.attach(activity_id)
+ self._probeMgr.currentActivity = activity_id
self._prepare_activity()
self.state_machine.set_state("INIT")
@@ -77,9 +79,9 @@ class Tutorial (object):
# Uninstall the whole FSM
self.state_machine.teardown()
- #FIXME There should be some amount of resetting done here...
- self.activity = None
-
+ if not self._activity_id is None:
+ self._probeMgr.detach(self._activity_id)
+ self._activity_id = None
def set_state(self, name):
"""
@@ -89,18 +91,6 @@ class Tutorial (object):
self.state_machine.set_state(name)
-
- # Currently unused -- equivalent function is in each state
- def _eventfilter_state_done(self, eventfilter):
- """
- Callback handler for eventfilter to notify
- when we must go to the next state.
- """
- #XXX Tests should be run here normally
-
- #Swith to the next state pointed by the eventfilter
- self.set_state(eventfilter.get_next_state())
-
def _prepare_activity(self):
"""
Prepare the activity for the tutorial by loading the saved state and
@@ -112,9 +102,11 @@ class Tutorial (object):
#of the activity root directory
filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\
self.activity_init_state_filename
- if os.path.exists(filename):
- self.activity.read_file(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):
"""
@@ -141,10 +133,9 @@ class State(object):
self._actions = action_list or []
- # Unused for now
- #self.tests = []
+ self._transitions= dict(event_filter_list or [])
- self._event_filters = event_filter_list or []
+ self._installedEvents = set()
self.tutorial = tutorial
@@ -168,12 +159,11 @@ class State(object):
Install the state itself, by first registering the event filters
and then triggering the actions.
"""
- for eventfilter in self._event_filters:
- eventfilter.install_handlers(self._event_filter_state_done_cb,
- activity=self.tutorial.activity)
+ 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:
- action.do()
+ self.tutorial.probeManager.install(action)
def teardown(self):
"""
@@ -182,38 +172,37 @@ class State(object):
removing dialogs that were displayed, removing highlights, etc...
"""
# Remove the handlers for the all of the state's event filters
- for event_filter in self._event_filters:
- event_filter.remove_handlers()
+ 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:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
- def _event_filter_state_done_cb(self, event_filter):
+ 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 event_filter The event filter that was called
+ @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(event_filter.get_next_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 (only if it wasn't added before)
+ Adds an action to the state
@param new_action The new action to execute when in this state
@return True if added, False otherwise
"""
- if new_action not in self._actions:
- self._actions.append(new_action)
- return True
- return False
+ 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
@@ -229,19 +218,21 @@ class State(object):
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_filter):
+ 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_filter The new event filter that will trigger a transition
+ @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_filter not in self._event_filters:
- self._event_filters.append(event_filter)
+ if event not in self._transitions.keys():
+ self._transitions[event]=next_state
return True
return False
@@ -249,7 +240,7 @@ class State(object):
"""
@return The list of event filters associated with this state.
"""
- return self._event_filters
+ return self._transitions.items()
def clear_event_filters(self):
"""
@@ -257,12 +248,19 @@ class State(object):
was just cleared will become a sink and will be the end of the
tutorial.
"""
- self._event_filters = []
+ self._transitions = {}
- def is_identical(self, otherState):
+ def __eq__(self, otherState):
"""
- Compares two states and tells whether they contain the same states and
+ 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
@@ -272,27 +270,38 @@ class State(object):
# Do they have the same actions?
if len(self._actions) != len(otherState._actions):
return False
+
+ if len(self._event_filters) != len(otherState._event_filters):
+ 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.is_identical(otherAct):
+ 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 len(self._actions) != len(otherState._actions):
- return False
for event in self._event_filters:
found = False
+ # For every event filter in the other state, try to match it with
+ # the current filter. We just need to find one with the right
+ # properties and values.
for otherEvent in otherState._event_filters:
- if event.is_identical(otherEvent):
+ if event == otherEvent:
found = True
break
- if found == False:
+ if found == False:
+ # We could not find the given event filter in the other state.
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):
@@ -385,7 +394,7 @@ class FiniteStateMachine(State):
self._fsm_setup_done = True
# Execute all the FSM level actions
for action in self.actions:
- action.do()
+ self.tutorial.probeManager.install(action)
# Then, we need to run the setup of the current state
self.current_state.setup()
@@ -450,7 +459,7 @@ class FiniteStateMachine(State):
self._fsm_teardown_done = True
# Undo all the FSM level actions here
for action in self.actions:
- action.undo()
+ self.tutorial.probeManager.uninstall(action)
# TODO : It might be nice to have a start() and stop() method for the
# FSM.
@@ -506,9 +515,9 @@ class FiniteStateMachine(State):
#TODO : Move this code inside the State itself - we're breaking
# encap :P
- for event_filter in st._event_filters:
- if event_filter.get_next_state() == state_name:
- st._event_filters.remove(event_filter)
+ for event, state in st._transitions:
+ if state == state_name:
+ del st._transitions[event]
# Remove the state from the dictionary
del self._states[state_name]
@@ -526,8 +535,8 @@ class FiniteStateMachine(State):
next_states = set()
- for event_filter in state._event_filters:
- next_states.add(event_filter.get_next_state())
+ for event, state in state._transitions:
+ next_states.add(state)
return tuple(next_states)
@@ -549,9 +558,9 @@ class FiniteStateMachine(State):
states = []
# Walk through the list of states
for st in self._states.itervalues():
- for event_filter in st._event_filters:
- if event_filter.get_next_state() == state_name:
- states.append(event_filter.get_next_state())
+ for event, state in st._transitions:
+ if state == state_name:
+ states.append(state)
continue
return tuple(states)
@@ -563,42 +572,57 @@ class FiniteStateMachine(State):
out_string += st.name + ", "
return out_string
- def is_identical(self, otherFSM):
+ 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
+ @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.is_identical(otherAct):
+ 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 state in self._states.itervalues():
- found = False
- for otherState in otherFSM._states.itervalues():
- if state.is_identical(otherState):
- found = True
- break
- if found == 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/creator.py b/tutorius/creator.py
index 513e312..efa17c3 100644
--- a/tutorius/creator.py
+++ b/tutorius/creator.py
@@ -114,7 +114,6 @@ class Creator(object):
"""
self.introspecting = False
eventfilter = addon.create('GtkWidgetEventFilter',
- next_state=None,
object_id=self._selected_widget,
event_name=event_name)
# undo actions so they don't persist through step editing
@@ -207,9 +206,13 @@ class Creator(object):
had_introspect = True
self.introspecting = True
elif isinstance(prop, properties.TStringProperty):
- dlg = TextInputDialog(title="Mandatory property",
+ dlg = TextInputDialog(text="Mandatory property",
field=propname)
setattr(action, propname, dlg.pop())
+ elif isinstance(prop, properties.TIntProperty):
+ dlg = TextInputDialog(text="Mandatory property",
+ field=propname)
+ setattr(action, propname, int(dlg.pop()))
else:
raise NotImplementedError()
@@ -240,7 +243,7 @@ class Creator(object):
Quit editing and cleanup interface artifacts.
"""
self.introspecting = False
- eventfilter = filters.EventFilter(None)
+ eventfilter = filters.EventFilter()
# undo actions so they don't persist through step editing
for action in self._tutorial.current_actions:
action.exit_editmode()
@@ -396,7 +399,10 @@ class EditToolBox(gtk.Window):
def _list_prop_changed(self, widget, evt, action, propname, idx):
try:
- getattr(action, propname)[idx] = int(widget.get_text())
+ #Save props as tuples so that they can be hashed
+ attr = list(getattr(action, propname))
+ attr[idx] = int(widget.get_text())
+ setattr(action, propname, tuple(attr))
except ValueError:
widget.set_text(str(getattr(action, propname)[idx]))
self.__parent._creator._action_refresh_cb(None, None, action)
diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py
new file mode 100644
index 0000000..1b685d7
--- /dev/null
+++ b/tutorius/dbustools.py
@@ -0,0 +1,41 @@
+import logging
+LOGGER = logging.getLogger("sugar.tutorius.dbustools")
+
+def save_args(callable, *xargs, **xkwargs):
+ def __call(*args, **kwargs):
+ kw = dict()
+ kw.update(kwargs)
+ kw.update(xkwargs)
+ return callable(*(xargs+args), **kw)
+ return __call
+
+def ignore(*args):
+ LOGGER.debug("Unhandled asynchronous dbus call response with arguments: %s", str(args))
+
+def logError(error):
+ LOGGER.error("Unhandled asynchronous dbus call error: %s", error)
+
+def remote_call(callable, args, return_cb=None, error_cb=None, block=False):
+ reply_cb = return_cb or ignore
+ errhandler_cb = error_cb or logError
+ if block:
+ try:
+ ret_val = callable(*args)
+ LOGGER.debug("remote_call return arguments: %s", str(ret_val))
+ except Exception, e:
+ #Use the specified error handler even for blocking calls
+ errhandler_cb(e)
+
+ #Return value signature might be :
+ if ret_val is None:
+ #Nothing
+ return reply_cb()
+ elif type(ret_val) in (list, tuple):
+ #Several parameters
+ return reply_cb(*ret_val)
+ else:
+ #One parameter
+ return reply_cb(ret_val)
+ else:
+ callable(*args, reply_handler=reply_cb, error_handler=errhandler_cb)
+
diff --git a/tutorius/engine.py b/tutorius/engine.py
new file mode 100644
index 0000000..dda9f3f
--- /dev/null
+++ b/tutorius/engine.py
@@ -0,0 +1,48 @@
+import logging
+import dbus.mainloop.glib
+from jarabe.model import shell
+
+from sugar.tutorius.bundler import TutorialStore
+from sugar.bundle.activitybundle import ActivityBundle
+
+class Engine:
+ """
+ Driver for the execution of tutorials
+ """
+
+ def __init__(self):
+ # FIXME Probe management should be in the probe manager
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ #FIXME shell.get_model() will only be useful in the shell process
+ self._shell = shell.get_model()
+ self._tutorial = None
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._tutorial:
+ self._tutorial.detach()
+ self._tutorial = None
+
+ store = TutorialStore()
+
+ #Get the active activity from the shell
+ activity = self._shell.get_active_activity()
+ self._tutorial = store.load_tutorial(tutorialID, bundle_path=activity.get_bundle_path())
+
+ #TProbes automatically use the bundle id, available from the ActivityBundle
+ bundle = ActivityBundle(activity.get_bundle_path())
+ self._tutorial.attach(bundle.get_bundle_id())
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._tutorial.detach()
+ self._tutorial = None
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ raise NotImplementedError("Unable to store tutorial state")
+
diff --git a/tutorius/filters.py b/tutorius/filters.py
index fc58562..44621d5 100644
--- a/tutorius/filters.py
+++ b/tutorius/filters.py
@@ -15,13 +15,9 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import gobject
-import gtk
import logging
logger = logging.getLogger("filters")
-from sugar.tutorius.gtkutils import find_widget
-from sugar.tutorius.services import ObjectStore
from sugar.tutorius import properties
@@ -30,31 +26,13 @@ class EventFilter(properties.TPropContainer):
Base class for an event filter
"""
- next_state = properties.TStringProperty("None")
-
- def __init__(self, next_state=None):
+ def __init__(self):
"""
Constructor.
- @param next_state name of the next state
"""
super(EventFilter, self).__init__()
- if next_state:
- self.next_state = next_state
self._callback = None
- def get_next_state(self):
- """
- Getter for the next state
- """
- return self.next_state
-
- def set_next_state(self, new_next_name):
- """
- Setter for the next state. Should only be used during construction of
- the event_fitler, not while the tutorial is running.
- """
- self.next_state = new_next_name
-
def install_handlers(self, callback, **kwargs):
"""
install_handlers is called for eventfilters to setup all
@@ -94,111 +72,3 @@ class EventFilter(properties.TPropContainer):
if self._callback:
self._callback(self)
-##class TimerEvent(EventFilter):
-## """
-## TimerEvent is a special EventFilter that uses gobject
-## timeouts to trigger a state change after a specified amount
-## of time. It must be used inside a gobject main loop to work.
-## """
-## def __init__(self,next_state,timeout_s):
-## """Constructor.
-##
-## @param next_state default EventFilter param, passed on to EventFilter
-## @param timeout_s timeout in seconds
-## """
-## super(TimerEvent,self).__init__(next_state)
-## self._timeout = timeout_s
-## self._handler_id = None
-##
-## def install_handlers(self, callback, **kwargs):
-## """install_handlers creates the timer and starts it"""
-## super(TimerEvent,self).install_handlers(callback, **kwargs)
-## #Create the timer
-## self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb)
-##
-## def remove_handlers(self):
-## """remove handler removes the timer"""
-## super(TimerEvent,self).remove_handlers()
-## if self._handler_id:
-## try:
-## #XXX What happens if this was already triggered?
-## #remove the timer
-## gobject.source_remove(self._handler_id)
-## except:
-## pass
-##
-## def _timeout_cb(self):
-## """
-## _timeout_cb triggers the eventfilter callback.
-##
-## It is necessary because gobject timers only stop if the callback they
-## trigger returns False
-## """
-## self.do_callback()
-## return False #Stops timeout
-##
-##class GtkWidgetTypeFilter(EventFilter):
-## """
-## Event Filter that listens for keystrokes on a widget
-## """
-## def __init__(self, next_state, object_id, text=None, strokes=None):
-## """Constructor
-## @param next_state default EventFilter param, passed on to EventFilter
-## @param object_id object tree-ish identifier
-## @param text resulting text expected
-## @param strokes list of strokes expected
-##
-## At least one of text or strokes must be supplied
-## """
-## super(GtkWidgetTypeFilter, self).__init__(next_state)
-## self._object_id = object_id
-## self._text = text
-## self._captext = ""
-## self._strokes = strokes
-## self._capstrokes = []
-## self._widget = None
-## self._handler_id = None
-##
-## def install_handlers(self, callback, **kwargs):
-## """install handlers
-## @param callback default EventFilter callback arg
-## """
-## super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs)
-## logger.debug("~~~GtkWidgetTypeFilter install")
-## activity = ObjectStore().activity
-## if activity is None:
-## logger.error("No activity")
-## raise RuntimeWarning("no activity in the objectstore")
-##
-## self._widget = find_widget(activity, self._object_id)
-## if self._widget:
-## self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb)
-## logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) )
-##
-## def remove_handlers(self):
-## """remove handlers"""
-## super(GtkWidgetTypeFilter, self).remove_handlers()
-## #if an event was connected, disconnect it
-## if self._handler_id:
-## self._widget.handler_disconnect(self._handler_id)
-## self._handler_id=None
-##
-## def __keypress_cb(self, widget, event, *args):
-## """keypress callback"""
-## logger.debug("~~~keypressed!")
-## key = event.keyval
-## keystr = event.string
-## logger.debug("~~~Got key: " + str(key) + ":"+ keystr)
-## self._capstrokes += [key]
-## #TODO Treat other stuff, such as arrows
-## if key == gtk.keysyms.BackSpace:
-## self._captext = self._captext[:-1]
-## else:
-## self._captext = self._captext + keystr
-##
-## logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext))
-## if not self._strokes is None and self._strokes in self._capstrokes:
-## self.do_callback()
-## if not self._text is None and self._text in self._captext:
-## self.do_callback()
-
diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py
index 91b11f4..78e94ce 100644
--- a/tutorius/linear_creator.py
+++ b/tutorius/linear_creator.py
@@ -58,9 +58,8 @@ class LinearCreator(object):
# Set the next state name - there is no way the caller should have
# to deal with that.
next_state_name = "State %d" % (self.nb_state+1)
- event_filter.set_next_state(next_state_name)
state = State(self.state_name, action_list=self.current_actions,
- event_filter_list=[event_filter])
+ event_filter_list=[(event_filter, next_state_name),])
self.state_name = next_state_name
self.nb_state += 1
diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py
index 6b1b948..0a3d542 100644
--- a/tutorius/overlayer.py
+++ b/tutorius/overlayer.py
@@ -157,7 +157,7 @@ class TextBubble(gtk.Widget):
A CanvasDrawableWidget drawing a round textbox and a tail pointing
to a specified widget.
"""
- def __init__(self, text, speaker=None, tailpos=[0,0]):
+ def __init__(self, text, speaker=None, tailpos=(0,0)):
"""
Creates a new cairo rendered text bubble.
@@ -199,7 +199,7 @@ class TextBubble(gtk.Widget):
# TODO fetch speaker coordinates
# draw bubble tail if present
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
@@ -228,7 +228,7 @@ class TextBubble(gtk.Widget):
context.fill()
# bubble painting. Redrawing the inside after the tail will combine
- if self.tailpos != [0,0]:
+ if self.tailpos != (0,0):
context.move_to(xradius-width/4, yradius)
context.line_to(self.tailpos[0], self.tailpos[1])
context.line_to(xradius+width/4, yradius)
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 6d30a8d..78e3c2b 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,39 +95,25 @@ class TPropContainer(object):
"""
return object.__getattribute__(self, "_props").keys()
- def is_identical(self, otherContainer):
- for prop in self._props.keys():
- found = False
- for otherProp in otherContainer._props.keys():
- if prop == otherProp:
- this_type = getattr(type(self), prop).type
- other_type = getattr(type(otherContainer), prop).type
- if this_type != other_type:
- return False
- if this_type == "addonlist":
- for inner_cont in self._props[prop]:
- inner_found = False
- for other_inner in otherContainer._props[prop]:
- if inner_cont.is_identical(other_inner):
- inner_found = True
- break
- if inner_found == False:
- return False
- found = True
- break
- elif this_type == "addon":
- if not self._props[prop].is_identical(otherContainer._props[prop]):
- return False
- found = True
- break
- else:
- if self._props[prop]== otherContainer._props[prop]:
- found = True
- break
- if found == False:
- return False
- return True
-
+ # Providing the hash methods necessary to use TPropContainers
+ # in a dictionary, according to their properties
+ def __hash__(self):
+ #Return a hash of properties (key, value) sorted by key
+ #We need to transform the list of property key, value lists into
+ # a tuple of key, value tuples
+ return hash(tuple(map(tuple,sorted(self._props.items(), cmp=lambda x, y: cmp(x[0], y[0])))))
+
+ def __eq__(self, e2):
+ return self._props == e2._props
+
+ # 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)
+
class TutoriusProperty(object):
"""
The base class for all actions' properties. The interface is the following :
@@ -178,19 +164,6 @@ class TAddonListProperty(TutoriusProperty):
"""
pass
-
- def get_constraints(self):
- """
- Returns the list of constraints associated to this property.
- """
- if self._constraints is None:
- self._constraints = []
- for i in dir(self):
- typ = getattr(self, i)
- if isinstance(typ, Constraint):
- self._constraints.append(i)
- return self._constraints
-
class TIntProperty(TutoriusProperty):
"""
Represents an integer. Can have an upper value limit and/or a lower value
@@ -240,8 +213,20 @@ class TArrayProperty(TutoriusProperty):
self.type = "array"
self.max_size_limit = MaxSizeConstraint(max_size_limit)
self.min_size_limit = MinSizeConstraint(min_size_limit)
- self.default = self.validate(value)
+ self.default = tuple(self.validate(value))
+ #Make this thing hashable
+ def __setstate__(self, state):
+ self.max_size_limit = MaxSizeConstraint(state["max_size_limit"])
+ self.min_size_limit = MinSizeConstraint(state["min_size_limit"])
+ self.value = state["value"]
+
+ def __getstate__(self):
+ return dict(
+ max_size_limit=self.max_size_limit.limit,
+ min_size_limit=self.min_size_limit.limit,
+ value=self.value,
+ )
class TColorProperty(TutoriusProperty):
"""
Represents a RGB color with 3 8-bit integer values.
@@ -320,8 +305,10 @@ class TUAMProperty(TutoriusProperty):
"""
Represents a widget of the interface by storing its UAM.
"""
- # TODO : Pending UAM check-in (LP 355199)
- pass
+ def __init__(self, value=None):
+ TutoriusProperty.__init__(self)
+
+ self.type = "uam"
class TAddonProperty(TutoriusProperty):
"""
diff --git a/tutorius/service.py b/tutorius/service.py
new file mode 100644
index 0000000..21f0cf1
--- /dev/null
+++ b/tutorius/service.py
@@ -0,0 +1,85 @@
+from engine import Engine
+import dbus
+
+from dbustools import remote_call
+
+_DBUS_SERVICE = "org.tutorius.Service"
+_DBUS_PATH = "/org/tutorius/Service"
+_DBUS_SERVICE_IFACE = "org.tutorius.Service"
+
+class Service(dbus.service.Object):
+ """
+ Global tutorius entry point to control the whole system
+ """
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ bus_name = dbus.service.BusName(_DBUS_SERVICE, bus=bus)
+ dbus.service.Object.__init__(self, bus_name, _DBUS_PATH)
+
+ self._engine = None
+
+ def start(self):
+ """ Start the service itself
+ """
+ # For the moment there is nothing to do
+ pass
+
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="s", out_signature="")
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ if self._engine == None:
+ self._engine = Engine()
+ self._engine.launch(tutorialID)
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ self._engine.stop()
+
+ @dbus.service.method(_DBUS_SERVICE_IFACE,
+ in_signature="", out_signature="")
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ self._engine.pause()
+
+class ServiceProxy:
+ """ Proxy to connect to the Service object, abstracting the DBus interface"""
+
+ def __init__(self):
+ bus = dbus.SessionBus()
+ self._object = bus.get_object(_DBUS_SERVICE,_DBUS_PATH)
+ self._service = dbus.Interface(self._object, _DBUS_SERVICE_IFACE)
+
+ def launch(self, tutorialID):
+ """ Launch a tutorial
+ @param tutorialID unique tutorial identifier used to retrieve it from the disk
+ """
+ remote_call(self._service.launch, (tutorialID, ), block=False)
+
+ def stop(self):
+ """ Stop the current tutorial
+ """
+ remote_call(self._service.stop, (), block=False)
+
+ def pause(self):
+ """ Interrupt the current tutorial and save its state in the journal
+ """
+ remote_call(self._service.pause, (), block=False)
+
+if __name__ == "__main__":
+ import dbus.mainloop.glib
+ import gobject
+
+ loop = gobject.MainLoop()
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+ s = Service()
+ loop.run()
+
diff --git a/tutorius/services.py b/tutorius/services.py
index 9ed2e50..e7b17d8 100644
--- a/tutorius/services.py
+++ b/tutorius/services.py
@@ -22,6 +22,9 @@ This module supplies services to be used by States, FSMs, Actions and Filters.
Services provided are:
-Access to the running activity
-Access to the running tutorial
+
+TODO: Passing the activity reference should be done by the Probe instead
+of being a global variable.
"""
diff --git a/tutorius/store.py b/tutorius/store.py
new file mode 100644
index 0000000..480c81b
--- /dev/null
+++ b/tutorius/store.py
@@ -0,0 +1,173 @@
+# 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 urllib
+
+class StoreProxy(object):
+ """
+ Implements a communication channel with the Tutorius Store, where tutorials
+ are shared from around the world. This proxy is meant to offer a one-stop
+ shop to implement all the requests that could be made to the Store.
+ """
+
+ def get_categories(self):
+ """
+ Returns all the categories registered in the store. Categories are used to
+ classify tutorials according to a theme. (e.g. Mathematics, History, etc...)
+
+ @return The list of category names stored on the server.
+ """
+ raise NotImplementedError("get_categories() not implemented")
+
+ def get_tutorials(self, keywords=None, category=None, startIndex=0, numResults=10, sortBy='name'):
+ """
+ Returns the list of tutorials that correspond to the given search criteria.
+
+ @param keywords The list of keywords that should be matched inside the tutorial title
+ or description. If None, the search will not filter the results
+ according to the keywords.
+ @param category The category in which to restrict the search.
+ @param startIndex The index in the result set from which to return results. This is
+ used to allow applications to fetch results one set at a time.
+ @param numResults The max number of results that can be returned
+ @param sortBy The field on which to sort the results
+ @return A list of tutorial meta-data that corresponds to the query
+ """
+ raise NotImplementedError("get_tutorials() not implemented")
+
+ def get_tutorial_collection(self, collection_name):
+ """
+ Returns a list of tutorials corresponding to the given collection name.
+ Collections can be groups like '5 most downloaded' or 'Top 10 ratings'.
+
+ @param collection_name The name of the collection from which we want the
+ meta-data
+ @return A list of tutorial meta-data corresponding to the given group
+ """
+ raise NotImplementedError("get_tutorial_collection() not implemented... yet!")
+
+ def get_latest_version(self, tutorial_id_list):
+ """
+ Returns the latest version number on the server, for each tutorial ID
+ in the list.
+
+ @param tutorial_id_list The list of tutorial IDs from which we want to
+ known the latest version number.
+ @return A dictionary having the tutorial ID as the key and the version
+ as the value.
+ """
+ raise NotImplementedError("get_latest_version() not implemented")
+
+ def download_tutorial(self, tutorial_id, version=None):
+ """
+ Fetches the tutorial file from the server and returns the
+
+ @param tutorial_id The tutorial that we want to get
+ @param version The version number that we want to download. If None,
+ the latest version will be downloaded.
+ @return The downloaded file itself (an in-memory representation of the file,
+ not a path to it on the disk)
+
+ TODO : We should decide if we're saving to disk or in mem.
+ """
+ raise NotImplementedError("downloadTutorial() not implemented")
+
+ def login(self, username, password):
+ """
+ Logs in the user on the store and saves the login status in the proxy
+ state. After a successful logon, the operation requiring a login will
+ be successful.
+
+ @return True if the login was successful, False otherwise
+ """
+ raise NotImplementedError("login() not implemented yet")
+
+ def close_session(self):
+ """
+ Ends the user's session on the server and changes the state of the proxy
+ to disallow the calls to the store that requires to be logged in.
+
+ @return True if the user was disconnected, False otherwise
+ """
+ raise NotImplementedError("close_session() not implemented yet")
+
+ def get_session_id(self):
+ """
+ Gives the current session ID cached in the Store Proxy, or returns
+ None is the user is not logged yet.
+
+ @return The current session's ID, or None if the user is not logged
+ """
+ raise NotImplementedError("get_session_id() not implemented yet")
+
+ def rate(self, value, tutorial_store_id):
+ """
+ Sends a rating for the given tutorial.
+
+ This function requires the user to be logged in.
+
+ @param value The value of the rating. It must be an integer with a value
+ from 1 to 5.
+ @param tutorial_store_id The ID of the tutorial that was rated
+ @return True if the rating was sent to the Store, False otherwise.
+ """
+ raise NotImplementedError("rate() not implemented")
+
+ def publish(self, tutorial):
+ """
+ Sends a tutorial to the store.
+
+ This function requires the user to be logged in.
+
+ @param tutorial The tutorial file to be sent. Note that this is the
+ content itself and not the path to the file.
+ @return True if the tutorial was sent correctly, False otherwise.
+ """
+ raise NotImplemetedError("publish() not implemented")
+
+ def unpublish(self, tutorial_store_id):
+ """
+ Removes a tutorial from the server. The user in the current session
+ needs to be the creator for it to be unpublished. This will remove
+ the file from the server and from all its collections and categories.
+
+ This function requires the user to be logged in.
+
+ @param tutorial_store_id The ID of the tutorial to be removed
+ @return True if the tutorial was properly removed from the server
+ """
+ raise NotImplementedError("unpublish() not implemeted")
+
+ def update_published_tutorial(self, tutorial_id, tutorial):
+ """
+ Sends the new content for the tutorial with the given ID.
+
+ This function requires the user to be logged in.
+
+ @param tutorial_id The ID of the tutorial to be updated
+ @param tutorial The bundled tutorial file content (not a path!)
+ @return True if the tutorial was sent and updated, False otherwise
+ """
+ raise NotImplementedError("update_published_tutorial() not implemented yet")
+
+ def register_new_user(self, user_info):
+ """
+ Creates a new user from the given user information.
+
+ @param user_info A structure containing all the data required to do a login.
+ @return True if the new account was created, false otherwise
+ """
+ raise NotImplementedError("register_new_user() not implemented")