Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius
diff options
context:
space:
mode:
authormike <michael.jmontcalm@gmail.com>2009-10-19 04:38:32 (GMT)
committer mike <michael.jmontcalm@gmail.com>2009-10-19 04:38:32 (GMT)
commit6584510d390a37153c20974da6704a907058fea0 (patch)
treea0649a77b36b63885774e0af25ec752192a5c404 /tutorius
parent2aef185e57f6c6c38670a5eea74f7889b3d56944 (diff)
parent3b9bff2ef1826987d95815ff03c235052cea9aae (diff)
Merge gitorious@git.sugarlabs.org:tutorius/michaeljm-dev into merge_michaeljm-dev
Diffstat (limited to 'tutorius')
-rw-r--r--tutorius/actions.py151
-rw-r--r--tutorius/addon.py7
-rw-r--r--tutorius/bundler.py163
-rw-r--r--tutorius/core.py136
-rw-r--r--tutorius/filters.py108
-rw-r--r--tutorius/properties.py105
-rw-r--r--tutorius/store.py173
-rw-r--r--tutorius/uam/__init__.py3
8 files changed, 532 insertions, 314 deletions
diff --git a/tutorius/actions.py b/tutorius/actions.py
index 4269cd7..cd34976 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -18,14 +18,10 @@ This module defines Actions that can be done and undone on a state
"""
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"""
@@ -176,149 +172,4 @@ class Action(TPropContainer):
x, y = self._drag.position
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 51791d1..15612c8 100644
--- a/tutorius/addon.py
+++ b/tutorius/addon.py
@@ -56,7 +56,12 @@ def create(name, *args, **kwargs):
if not _cache:
_reload_addons()
try:
- return _cache[name]['class'](*args, **kwargs)
+ comp_metadata = _cache[name]
+ try:
+ return comp_metadata['class'](*args, **kwargs)
+ except:
+ logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs)))
+ return None
except KeyError:
logging.error("Addon not found for class '%s'", name)
return None
diff --git a/tutorius/bundler.py b/tutorius/bundler.py
index 8808d93..56bbf3e 100644
--- a/tutorius/bundler.py
+++ b/tutorius/bundler.py
@@ -48,6 +48,8 @@ INI_XML_FSM_PROPERTY = "FSM_FILENAME"
INI_FILENAME = "meta.ini"
TUTORIAL_FILENAME = "tutorial.xml"
NODE_COMPONENT = "Component"
+NODE_SUBCOMPONENT = "property"
+NODE_SUBCOMPONENTLIST = "listproperty"
class TutorialStore(object):
@@ -150,6 +152,71 @@ class XMLSerializer(Serializer):
eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc))
return statesList
+ def _create_addon_component_node(self, parent_attr_name, comp, doc):
+ """
+ Takes a component that is embedded in another component (e.g. the content
+ of a OnceWrapper) and encapsulate it in a node with the property name.
+
+ e.g.
+ <Component Class="OnceWrapper">
+ <property name="addon">
+ <Component Class="BubbleMessage" message="'Hi!'" position="[12,32]"/>
+ </property>
+ </Component>
+
+ When reloading this node, we should look up the property name for the parent
+ in the attribute of the node, then examine the subnode to create the addon
+ object itself.
+
+ @param parent_attr_name The name of the parent's attribute for this addon
+ e.g. the OnceWrapper has the action attribute, which corresponds to a
+ sub-action it must execute once.
+ @param comp The component node itself
+ @param doc The XML document root (only used to create the nodes)
+ @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node
+ that represents another component.
+ """
+ subCompNode = doc.createElement(NODE_SUBCOMPONENT)
+ subCompNode.setAttribute("name", parent_attr_name)
+
+ subNode = self._create_component_node(comp, doc)
+
+ subCompNode.appendChild(subNode)
+
+ return subCompNode
+
+ def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc):
+ """
+ Takes a list of components that are embedded in another component (ex. the
+ content of a ChainAction) and encapsulate them in a node with the property
+ name.
+
+ e.g.
+ <Component Class="ChainAction">
+ <listproperty name="actions">
+ <Component Class="BubbleMessage" message="'Hi!'" position="[15,35]"/>
+ <Component Class="DialogMessage" message="'Multi-action!'" position="[45,10]"/>
+ </listproperty>
+ </Component>
+
+ When reloading this node, we should look up the property name for the parent
+ in the the attribute of the node, then rebuild the list by appending the
+ content of all the subnodes.
+
+ @param parent_attr_name The name of the parent component's property
+ @param comp_list A list of components that comprise the property
+ @param doc The XML document root (only for creating new nodes)
+ @returns A NODE_SUBCOMPONENTLIST node with the property attribute
+ """
+ subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST)
+ subCompListNode.setAttribute("name", parent_attr_name)
+
+ for comp in comp_list:
+ compNode = self._create_component_node(comp, doc)
+ subCompListNode.appendChild(compNode)
+
+ return subCompListNode
+
def _create_component_node(self, comp, doc):
"""
Takes a single component (action or eventfilter) and transforms it
@@ -169,10 +236,10 @@ class XMLSerializer(Serializer):
for propname in comp.get_properties():
propval = getattr(comp, propname)
if getattr(type(comp), propname).type == "addonlist":
- for subval in propval:
- compNode.appendChild(self._create_component_node(subval, doc))
- elif getattr(type(comp), propname).type == "addonlist":
- compNode.appendChild(self._create_component_node(subval, doc))
+ compNode.appendChild(self._create_addonlist_component_node(propname, propval, doc))
+ elif getattr(type(comp), propname).type == "addon":
+ #import rpdb2; rpdb2.start_embedded_debugger('pass')
+ compNode.appendChild(self._create_addon_component_node(propname, propval, doc))
else:
# repr instead of str, as we want to be able to eval() it into a
# valid object.
@@ -282,6 +349,27 @@ class XMLSerializer(Serializer):
# Error : none of these directories contain the tutorial
raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID %s"%guid)
+ def _get_direct_descendants_by_tag_name(self, node, name):
+ """
+ Searches in the list of direct descendants of a node to find all the node
+ that have the given name.
+
+ This is used because the Document.getElementsByTagName() function returns the
+ list of all the descendants (whatever their distance to the start node) that
+ have that name. In the case of complex components, we absolutely need to inspect
+ a single layer of the tree at the time.
+
+ @param node The node from which we want the direct descendants with a particular
+ name
+ @param name The name of the node
+ @returns A list, possibly empty, of direct descendants of node that have this name
+ """
+ return_list = []
+ for childNode in node.childNodes:
+ if childNode.nodeName == name:
+ return_list.append(childNode)
+ return return_list
+
def _load_xml_properties(self, properties_elem):
"""
Changes a list of properties into fully instanciated properties.
@@ -298,7 +386,7 @@ class XMLSerializer(Serializer):
@param filters_elem An XML Element representing a list of event filters
"""
reformed_event_filters_list = []
- event_filter_element_list = filters_elem.getElementsByTagName(NODE_COMPONENT)
+ 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:
@@ -309,6 +397,42 @@ class XMLSerializer(Serializer):
return reformed_event_filters_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 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.
+ """
+ subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT)
+
+ for subComp in subCompList:
+ property_name = subComp.getAttribute("name")
+ internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0]
+ internal_comp = self._load_xml_component(internal_comp_node)
+ properties[str(property_name)] = internal_comp
+
+ def _load_xml_subcomponent_lists(self, node, properties):
+ """
+ Loads all the subcomponent lists below the given node and stores them
+ under the correct property name for that node.
+
+ @param node The node from which we want to read the subComponent lists
+ @param properties The dictionnary that will contain the mapping of prop->subCompList
+ @returns Nothing. The values are returns inside the properties dict.
+ """
+ listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST)
+ for subCompListNode in listOf_subCompListNode:
+ property_name = subCompListNode.getAttribute("name")
+ subCompList = []
+ for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT):
+ subComp = self._load_xml_component(subCompNode)
+ subCompList.append(subComp)
+ properties[str(property_name)] = subCompList
+
def _load_xml_component(self, node):
"""
Loads a single addon component instance from an Xml node.
@@ -318,20 +442,23 @@ class XMLSerializer(Serializer):
@return The addon component object of the correct type according to the XML
description
"""
- new_action = addon.create(node.getAttribute("Class"))
- if not new_action:
- return None
+ class_name = node.getAttribute("Class")
+
+ properties = {}
- for attrib in node.attributes.keys():
- if attrib == "Class": continue
- # security note: keep sandboxed
- setattr(new_action, attrib, eval(node.getAttribute(attrib), {}, {}))
+ for prop in node.attributes.keys():
+ if prop == "Class" : continue
+ # security : keep sandboxed
+ properties[str(prop)] = eval(node.getAttribute(prop))
- # recreate complex attributes
- for sub in node.childNodes:
- name = getattr(new_action, sub.nodeName)
- if name == "addon":
- setattr(new_action, sub.getAttribute("Name"), self._load_xml_action(sub))
+ # Read the complex attributes
+ self._load_xml_subcomponents(node, properties)
+ self._load_xml_subcomponent_lists(node, properties)
+
+ new_action = addon.create(class_name, **properties)
+
+ if not new_action:
+ return None
return new_action
@@ -342,7 +469,7 @@ class XMLSerializer(Serializer):
@param actions_elem An XML Element representing a list of Actions
"""
reformed_actions_list = []
- actions_element_list = actions_elem.getElementsByTagName(NODE_COMPONENT)
+ actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT)
for action in actions_element_list:
new_action = self._load_xml_component(action)
diff --git a/tutorius/core.py b/tutorius/core.py
index dd2435e..4376315 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -89,18 +89,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
@@ -141,9 +129,6 @@ class State(object):
self._actions = action_list or []
- # Unused for now
- #self.tests = []
-
self._event_filters = event_filter_list or []
self.tutorial = tutorial
@@ -205,15 +190,13 @@ class State(object):
# 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
@@ -258,6 +241,60 @@ class State(object):
tutorial.
"""
self._event_filters = []
+
+ def __eq__(self, otherState):
+ """
+ Compares two states and tells whether they contain the same states with the
+ same actions and event filters.
+
+ @param otherState The other State that we wish to match
+ @returns True if every action in this state has a matching action in the
+ other state with the same properties and values AND if every
+ event filters in this state has a matching filter in the
+ other state having the same properties and values AND if both
+ states have the same name.
+` """
+ if not isinstance(otherState, State):
+ return False
+ if self.name != otherState.name:
+ return False
+
+ # Do they have the same actions?
+ if len(self._actions) != len(otherState._actions):
+ return False
+
+ if len(self._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 == 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?
+ 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 == otherEvent:
+ found = True
+ break
+ 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):
"""
@@ -348,7 +385,7 @@ class FiniteStateMachine(State):
# Flag the FSM level setup as done
self._fsm_setup_done = True
# Execute all the FSM level actions
- for action in self.actions:
+ for action in self._actions:
action.do()
# Then, we need to run the setup of the current state
@@ -413,7 +450,7 @@ class FiniteStateMachine(State):
# Flag the FSM teardown as not needed anymore
self._fsm_teardown_done = True
# Undo all the FSM level actions here
- for action in self.actions:
+ for action in self._actions:
action.undo()
# TODO : It might be nice to have a start() and stop() method for the
@@ -526,3 +563,58 @@ class FiniteStateMachine(State):
for st in self._states.itervalues():
out_string += st.name + ", "
return out_string
+
+ def __eq__(self, otherFSM):
+ """
+ Compares the elements of two FSM to ensure and returns true if they have the
+ same set of states, containing the same actions and the same event filters.
+
+ @returns True if the two FSMs have the same content, False otherwise
+ """
+ if not isinstance(otherFSM, FiniteStateMachine):
+ return False
+
+ # Make sure they share the same name
+ if not (self.name == otherFSM.name) or \
+ not (self.start_state_name == otherFSM.start_state_name):
+ return False
+
+ # Ensure they have the same number of FSM-level actions
+ if len(self._actions) != len(otherFSM._actions):
+ return False
+
+ # Test that we have all the same FSM level actions
+ for act in self._actions:
+ found = False
+ # For every action in the other FSM, try to match it with the
+ # current one.
+ for otherAct in otherFSM._actions:
+ if act == otherAct:
+ found = True
+ break
+ if found == False:
+ return False
+
+ # Make sure we have the same number of states in both FSMs
+ if len(self._states) != len(otherFSM._states):
+ return False
+
+ # For each state, try to find a corresponding state in the other FSM
+ for state_name in self._states.keys():
+ state = self._states[state_name]
+ other_state = None
+ try:
+ # Attempt to use this key in the other FSM. If it's not present
+ # the dictionary will throw an exception and we'll know we have
+ # at least one different state in the other FSM
+ other_state = otherFSM._states[state_name]
+ except:
+ return False
+ # If two states with the same name exist, then we want to make sure
+ # they are also identical
+ if not state == other_state:
+ return False
+
+ # If we made it here, then all the states in this FSM could be matched to an
+ # identical state in the other FSM.
+ return True
diff --git a/tutorius/filters.py b/tutorius/filters.py
index aa8c997..0055763 100644
--- a/tutorius/filters.py
+++ b/tutorius/filters.py
@@ -94,111 +94,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/properties.py b/tutorius/properties.py
index abf76e5..4c34511 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -95,6 +95,89 @@ class TPropContainer(object):
"""
return object.__getattribute__(self, "_props").keys()
+ def __eq__(self, otherContainer):
+ """
+ Compare this property container to the other one and returns True only if
+ the every property of the first one can be found in the other container, with
+ the same name and the same value.
+
+ @param otherContainer The other container that we wish to test for equality.
+ @returns True if every property in the first container can be found with the same
+ value and the same name in the second container.
+ """
+ # Make sure both have the same number of properties
+ if len(self._props) != len(otherContainer._props):
+ return False
+
+ if not(type(self) == type(otherContainer)):
+ return False
+
+ # For every property in this container
+ for prop in self._props.keys():
+ found = False
+ # Try to match it with another property present in the other container
+ for otherProp in otherContainer._props.keys():
+ # If we were able to match the name, then we look up the value
+ 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 is an addon list, then we need to make sure that
+ # every element of the list is also present in the other list
+ if this_type == "addonlist":
+ if not self._are_lists_identical(self._props[prop], otherContainer._props[prop]):
+ return False
+ found = True
+ break
+
+ # If this is just an embedded / decorated container, then we want to
+ # make sure the sub component are identical.
+ elif this_type == "addon":
+ if not (self._props[prop] == otherContainer._props[prop]):
+ return False
+ found = True
+ break
+ else:
+ if self._props[prop] == otherContainer._props[prop]:
+ found = True
+ break
+ # If we arrive here, then we couldn't find any property in the second
+ # container that matched the current one. We know that the two containers are
+ # not equal.
+ if found == False:
+ return False
+ return True
+
+ def _are_lists_identical(self, myList, otherList):
+ """
+ Compares two lists of property containers to see if they are identical (
+ they have the same properties
+
+ @param myList The first list of properties containers
+ @param otherList The second list of properties containers
+ @return True if all of the properties inside the list are identical. False otherwise.
+ """
+ # For each property in the first list,
+ for container in myList:
+ found = False
+ # Attempt to match it with every property in the other list
+ for other_container in otherList:
+ # If the containers are identical,
+ if container == other_container:
+ # We found a matching container. We don't need to search in the
+ # second list anymore, so we break
+ found = True
+ break
+ # In the case the property was not found inside the second list
+ if found == False:
+ # We know right away that the two lists are not identical
+ return False
+ # If we were able to match each property in the first list, then we
+ # can say the lists are equal.
+ return True
+
class TutoriusProperty(object):
"""
The base class for all actions' properties. The interface is the following :
@@ -145,19 +228,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
@@ -317,8 +387,15 @@ class TAddonListProperty(TutoriusProperty):
See TAddonProperty
"""
def __init__(self):
- super(TAddonProperty, self).__init__()
+ TutoriusProperty.__init__(self)
self.type = "addonlist"
self.default = []
+ def validate(self, value):
+ if isinstance(value, list):
+ for component in value:
+ if not (isinstance(component, TPropContainer)):
+ raise ValueError("Expected a list of TPropContainer instances inside TAddonListProperty value, got a %s" % (str(type(component))))
+ return value
+ raise ValueError("Value proposed to TAddonListProperty is not a list")
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")
diff --git a/tutorius/uam/__init__.py b/tutorius/uam/__init__.py
index 7cf5671..bcd67e1 100644
--- a/tutorius/uam/__init__.py
+++ b/tutorius/uam/__init__.py
@@ -65,7 +65,8 @@ for subscheme in [".".join([SCHEME,s]) for s in __parsers]:
class SchemeError(Exception):
def __init__(self, message):
Exception.__init__(self, message)
- self.message = message
+ ## Commenting this line as it is causing an error in the tests
+ ##self.message = message
def parse_uri(uri):