From 6584510d390a37153c20974da6704a907058fea0 Mon Sep 17 00:00:00 2001 From: mike Date: Mon, 19 Oct 2009 04:38:32 +0000 Subject: Merge gitorious@git.sugarlabs.org:tutorius/michaeljm-dev into merge_michaeljm-dev --- (limited to 'tutorius') 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. + + + + + + + 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. + + + + + + + + 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): -- cgit v0.9.1