From 2c8fe66c0f7490c8aaaae27b4977b987001c6b71 Mon Sep 17 00:00:00 2001 From: mike Date: Tue, 01 Dec 2009 20:11:43 +0000 Subject: Merge branch 'master' of git://git.sugarlabs.org/tutorius/simpoirs-clone Conflicts: src/extensions/tutoriusremote.py tutorius/TProbe.py tutorius/creator.py --- (limited to 'tutorius') diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 1521eab..7021f80 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -1,10 +1,25 @@ +# 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 1 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 logging LOGGER = logging.getLogger("sugar.tutorius.TProbe") import os import gobject -import dbus import dbus.service import cPickle as pickle @@ -12,7 +27,6 @@ import cPickle as pickle from . import addon from . import properties from .services import ObjectStore -from .properties import TPropContainer from .dbustools import remote_call, save_args import copy @@ -78,8 +92,6 @@ class TProbe(dbus.service.Object): LOGGER.debug("TProbe :: registering '%s' with unique_id '%s'", self._activity_name, activity.get_id()) self._service_proxy.register_probe(self._activity_name, self._unique_id) - - def start(self): """ @@ -184,32 +196,12 @@ class TProbe(dbus.service.Object): in_signature='s', out_signature='s') def create_event(self, addon_name): # avoid recursive imports - from .creator import WidgetSelector, SignalInputDialog, TextInputDialog - event = addon.create(addon_name) addonname = type(event).__name__ meta = addon.get_addon_meta(addonname) for propname in meta['mandatory_props']: prop = getattr(type(event), propname) - if isinstance(prop, properties.TUAMProperty): - selector = WidgetSelector(self._activity) - setattr(event, propname, selector.select()) - elif isinstance(prop, properties.TEventType): - try: - dlg = SignalInputDialog(self._activity, - text="Mandatory property", - field=propname, - addr=event.object_id) - setattr(event, propname, dlg.pop()) - except AttributeError: - pass - elif isinstance(prop, properties.TStringProperty): - dlg = TextInputDialog(self._activity, - text="Mandatory property", - field=propname) - setattr(event, propname, dlg.pop()) - else: - raise NotImplementedError() + prop.widget_class.run_dialog(self._activity, event, propname) return pickle.dumps(event) @@ -322,7 +314,6 @@ class ProbeProxy: self._subscribedEvents = {} self._registeredCallbacks = {} - self._object.connect_to_signal("eventOccured", self._handle_signal, dbus_interface="org.tutorius.ProbeInterface") def _handle_signal(self, pickled_event): @@ -344,58 +335,57 @@ class ProbeProxy: except: return False - def __update_action(self, action, address): + def __update_action(self, action, callback, address): LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) - self._actions[action] = str(address) + self._actions[address] = action + callback(address) - def __clear_action(self, action): - self._actions.pop(action, None) + def __clear_action(self, address): + self._actions.pop(address, None) - def install(self, action, block=False, is_editing=False): + def install(self, action, action_installed_cb, error_cb, is_editing=False): """ Install an action on the TProbe's activity @param action Action to install - @param block Force a synchroneous dbus call if True + @param action_installed_cb The callback function to call once the action is installed + @param error_cb The callback function to call when an error happens @param is_editing whether this action comes from the editor @return None """ - return remote_call(self._probe.install, - (pickle.dumps(action), is_editing), - save_args(self.__update_action, action), - block=block) + self._probe.install(pickle.dumps(action), is_editing, + reply_handler=save_args(self.__update_action, action, action_installed_cb), + error_handler=save_args(error_cb, action)) - def update(self, action, newaction, block=False, is_editing=False): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again - @param action Action to update + @param action_address The address of the action to update. This is + provided by the install callback method. @param newaction Action to update it with @param block Force a synchroneous dbus call if True @param is_editing whether this action comes from the editor @return None """ #TODO review how to make this work well - if not action in self._actions.keys(): + if not action_address in self._actions.keys(): raise RuntimeWarning("Action not installed") #TODO Check error handling - return remote_call(self._probe.update, - (self._actions[action], - pickle.dumps(newaction._props), - is_editing), - block=block) + return self._probe.update(action_address, pickle.dumps(newaction._props), is_editing, + reply_handler=ignore, + error_handler=logError) - def uninstall(self, action, block=False, is_editing=False): + def uninstall(self, action_address, is_editing): """ Uninstall an installed action - @param action Action to uninstall - @param block Force a synchroneous dbus call if True + @param action_address The address of the action to uninstall. This address was given + on action installation @param is_editing whether this action comes from the editor """ - if action in self._actions.keys(): - remote_call(self._probe.uninstall, - (self._actions.pop(action), is_editing), - block=block) + if action_address in self._actions: + self._actions.pop(action_address, None) + self._probe.uninstall(action_address, is_editing, reply_handler=ignore, error_handler=logError) - def __update_event(self, event, callback, address): + def __update_event(self, event, callback, event_subscribed_cb, 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 @@ -411,7 +401,7 @@ class ProbeProxy: # 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? + # Oops, how come we have two similar addresses? # send the bad news! raise Exception("Probe subscribe exception, the following address already exists: " + str(address)) @@ -424,6 +414,7 @@ class ProbeProxy: # our dictionary (python pass arguments by reference) self._subscribedEvents[address] = copy.copy(event) + event_subscribed_cb(address) return address def __clear_event(self, address): @@ -453,24 +444,26 @@ class ProbeProxy: """ return pickle.loads(str(self._probe.create_event(addon_name))) - def subscribe(self, event, callback, block=True): + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + """ 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) + @param notification_cb callable that will be called when the event occurs + @param event_installed_cb callable that will be called once the event is subscribed to + @param error_cb callable that will be called if the subscription fails @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") + #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) + self._probe.subscribe(pickle.dumps(event), + reply_handler=save_args(self.__update_event, event, notification_cb, event_subscribed_cb), + error_handler=save_args(error_cb, event)) def unsubscribe(self, address, block=True): """ @@ -481,9 +474,10 @@ class ProbeProxy: """ LOGGER.debug("ProbeProxy :: Unregister adress %s issued", str(address)) if address in self._subscribedEvents.keys(): - remote_call(self._probe.unsubscribe, (address,), - return_cb=save_args(self.__clear_event, address), - block=block) + self.__clear_event(address) + self._probe.unsubscribe(address, + reply_handler=save_args(self.__clear_event, address), + error_handler=logError) else: LOGGER.debug("ProbeProxy :: unsubscribe address %s failed : not registered", address) @@ -493,10 +487,10 @@ class ProbeProxy: subscribed events should be removed. """ for action_addr in self._actions.keys(): - self.uninstall(action_addr, block) + self.uninstall(action_addr) for address in self._subscribedEvents.keys(): - self.unsubscribe(address, block) + self.unsubscribe(address) class ProbeManager(object): @@ -536,43 +530,48 @@ class ProbeManager(object): currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, block=False, is_editing=False): + def install(self, action, action_installed_cb, error_cb, is_editing=False): """ Install an action on the current activity @param action Action to install + @param action_installed_cb The callback to call once the action is installed + @param error_cb The callback that will be called if there is an error during installation @param block Force a synchroneous dbus call if True @param is_editing whether this action comes from the editor @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, block, is_editing) + return self._first_proxy(self.currentActivity).install( + action=action, + is_editing=is_editing, + action_installed_cb=action_installed_cb, + error_cb=error_cb) else: raise RuntimeWarning("No activity attached") - def update(self, action, newaction, block=False, is_editing=False): + def update(self, action_address, newaction, is_editing=False): """ Update an already installed action's properties and run it again - @param action Action to update + @param action_address Action to update @param newaction Action to update it with @param block Force a synchroneous dbus call if True @param is_editing whether this action comes from the editor @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).update(action, newaction, block, is_editing) + return self._first_proxy(self.currentActivity).update(action_address, newaction, is_editing) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action, block=False, is_editing=False): + def uninstall(self, action_address, is_editing=False): """ Uninstall an installed action - @param action Action to uninstall + @param action_address Action to uninstall @param block Force a synchroneous dbus call if True @param is_editing whether this action comes from the editor """ if self.currentActivity: - # return self._probes[self.currentActivity].uninstall(action, block, is_editing) - return self._first_proxy(self.currentActivity).uninstall(action, block, is_editing) + return self._first_proxy(self.currentActivity).uninstall(action_address, is_editing) else: raise RuntimeWarning("No activity attached") @@ -585,19 +584,24 @@ class ProbeManager(object): @returns: an eventfilter instance """ if self.currentActivity: - return self._first_proxy(self.currentActivity).uninstall(action, block) + return self._first_proxy(self.currentActivity).create_event(addon_name) else: raise RuntimeWarning("No activity attached") - def subscribe(self, event, callback): + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): """ Register an event listener @param event Event to listen for - @param callback callable that will be called when the event occurs + @param notification_cb callable that will be called when the event occurs + @param subscribe_cb callable that will be called once the action has been + installed + @param error_cb callable that will be called if an error happens during + installation @return address identifier used for unsubscribing """ if self.currentActivity: - return self._first_proxy(self.currentActivity).subscribe(event, callback) + return self._first_proxy(self.currentActivity).subscribe(event, notification_cb,\ + event_subscribed_cb, error_cb) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/addon.py b/tutorius/addon.py index 21ebffa..6e3d8b9 100644 --- a/tutorius/addon.py +++ b/tutorius/addon.py @@ -66,10 +66,10 @@ def create(name, *args, **kwargs): 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))) + logging.debug("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) + logging.debug("Addon not found for class '%s'", name) return None def list_addons(): diff --git a/tutorius/constraints.py b/tutorius/constraints.py index 519bce8..cd71167 100644 --- a/tutorius/constraints.py +++ b/tutorius/constraints.py @@ -24,6 +24,8 @@ for some properties. # For the File Constraint import os +# For the Resource Constraint +import re class ConstraintException(Exception): """ @@ -214,3 +216,40 @@ class FileConstraint(Constraint): raise FileConstraintError("Non-existing file : %s"%value) return +class ResourceConstraintError(ConstraintException): + pass + +class ResourceConstraint(Constraint): + """ + Ensures that the value is looking like a resource name, like + _[.]. We are not validating that this is a + valid resource for the reason that the property does not have any notion + of tutorial guid. + + TODO : Find a way to properly validate resources by looking them up in the + Vault. + """ + + # Regular expression to parse a resource-like name + resource_regexp_text = "(.+)_([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})(\..*)?$" + resource_regexp = re.compile(resource_regexp_text) + + def validate(self, value): + # TODO : Validate that we will not use an empty resource or if we can + # have transitory resource names + if value is None: + raise ResourceConstraintError("Resource not allowed to have a null value!") + + # Special case : We allow the empty resource name for now + if value == "": + return value + + # Attempt to see if the value has a resource name inside it + match = self.resource_regexp.search(value) + + # If there was no match on the reg exp + if not match: + raise ResourceConstraintError("Resource name does not seem to be valid : %s" % value) + + # If the name matched, then the value is _PROBABLY_ good + return value diff --git a/tutorius/creator.py b/tutorius/creator.py index e8182b0..54e2912 100644 --- a/tutorius/creator.py +++ b/tutorius/creator.py @@ -37,6 +37,8 @@ from . import viewer from .propwidgets import TextInputDialog from . import TProbe +from functools import partial + from dbus import SessionBus from dbus.service import method, Object, BusName @@ -75,6 +77,7 @@ class Creator(Object): self.is_authoring = False Creator._instance = self self._probe_mgr = TProbe.ProbeManager.default_instance + self._installed_actions = list() def start_authoring(self, tutorial=None): """ @@ -177,8 +180,8 @@ class Creator(Object): .get(action, None) if not action_obj: return False - #action_obj.exit_editmode() - self._probe_mgr.uninstall(action_obj, is_editing=True) + + self._probe_mgr.uninstall(action_obj.address) self._tutorial.delete_action(action) self._overview.win.queue_draw() return True @@ -225,47 +228,43 @@ class Creator(Object): or state_name == self._tutorial.END: return - for action in self._tutorial.get_action_dict(self._state).values(): - #action.exit_editmode() - self._probe_mgr.uninstall(action, is_editing=True) + for action in self._installed_actions: + self._probe_mgr.uninstall(action.address, + is_editing=True) + self._installed_actions = [] self._state = state_name state_actions = self._tutorial.get_action_dict(self._state).values() + for action in state_actions: - action.enter_editmode() - action._drag._eventbox.connect_after( - "button-release-event", self._action_refresh_cb, action) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True) if state_actions: + # I'm really lazy right now and to keep things simple I simply + # always select the first action when + # we change state. we should really select the clicked block + # in the overview instead. FIXME self._propedit.action = state_actions[0] else: self._propedit.action = None self._overview.win.queue_draw() - - def _evfilt_cb(self, menuitem, event): - """ - This will get called once the user has selected a menu item from the - event filter popup menu. This should add the correct event filter - to the FSM and increment states. - """ - # undo actions so they don't persist through step editing - for action in self._state.get_action_list(): - self._probe_mgr.uninstall(action, is_editing=True) - #action.exit_editmode() - self._propedit.action = None - #self._activity.queue_draw() - def _add_action_cb(self, widget, path): """Callback for the action creation toolbar tool""" action_type = self._propedit.actions_list[path][ToolBox.ICON_NAME] action = addon.create(action_type) - self._probe_mgr.install(action, is_editing=True) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True) self._tutorial.add_action(self._state, action) - # FIXME: replace following with event catching - #action._drag._eventbox.connect_after( - # "button-release-event", self._action_refresh_cb, action) + self._propedit.action = action self._overview.win.queue_draw() def _add_event_cb(self, widget, path): @@ -290,12 +289,7 @@ class Creator(Object): """ event_type = self._propedit.events_list[path][ToolBox.ICON_NAME] - event = addon.create(event_type) - addonname = type(event).__name__ - meta = addon.get_addon_meta(addonname) - for propname in meta['mandatory_props']: - prop = getattr(type(event), propname) - prop.widget_class.run_dialog(None, event, propname) + event = self._probe_mgr.create_event(event_type) event_filters = self._tutorial.get_transition_dict(self._state) @@ -328,14 +322,13 @@ class Creator(Object): Callback for refreshing properties values and notifying the property dialog of the new values. """ - self._probe_mgr.uninstall(action, is_editing=True) - #action.exit_editmode() - self._probe_mgr.install(action, is_editing=True) - #action.enter_editmode() - #self._activity.queue_draw() - # TODO: replace following with event catching - #action._drag._eventbox.connect_after( - # "button-release-event", self._action_refresh_cb, action) + self._probe_mgr.uninstall(action.address, + is_editing=True) + return_cb = partial(self._action_installed_cb, action) + self._probe_mgr.install(action, + action_installed_cb=return_cb, + error_cb=self._dbus_exception, + is_editing=True) self._propedit.action = action self._overview.win.queue_draw() @@ -348,12 +341,12 @@ class Creator(Object): """ # undo actions so they don't persist through step editing for action in self._tutorial.get_action_dict(self._state).values(): - #action.exit_editmode() - self._probe_mgr.uninstall(action, is_editing=True) + self._probe_mgr.uninstall(action.address, + is_editing=True) if kwargs.get('force', False): dialog = gtk.MessageDialog( - parent=None, + parent=self._overview.win, flags=gtk.DIALOG_MODAL, type=gtk.MESSAGE_QUESTION, buttons=gtk.BUTTONS_YES_NO, @@ -398,6 +391,23 @@ class Creator(Object): assert False, "REMOVE THIS CALL!!!" launch = staticmethod(launch) + def _action_installed_cb(self, action, address): + """ + This is a callback intented to be use to receive actions addresses + after they are installed. + @param address: the address of the newly installed action + """ + action.address = address + self._installed_actions.append(action) + + def _dbus_exception(self, exception): + """ + This is a callback intented to be use to receive exceptions on remote + DBUS calls. + @param exception: the exception thrown by the remote process + """ + pass + class ToolBox(object): """ diff --git a/tutorius/dbustools.py b/tutorius/dbustools.py index 02acd3d..0973164 100644 --- a/tutorius/dbustools.py +++ b/tutorius/dbustools.py @@ -1,3 +1,19 @@ +# 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 1 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 logging import gobject diff --git a/tutorius/engine.py b/tutorius/engine.py index c945e49..198fa11 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -1,4 +1,21 @@ +# 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 1 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 logging +from heapq import heappush, heappop import dbus.mainloop.glib from jarabe.model import shell from sugar.bundle.activitybundle import ActivityBundle @@ -7,7 +24,21 @@ from .vault import Vault from .TProbe import ProbeManager from .dbustools import save_args from .tutorial import Tutorial, AutomaticTransitionEvent +from .translator import ResourceTranslator + +# Priority values for queuable messages +STOP_MSG_PRIORITY = 5 +EVENT_NOTIFICATION_MSG_PRIORITY = 10 +# List of runner states +RUNNER_STATE_IDLE = "idle" +RUNNER_STATE_SETUP_ACTIONS = "setup_actions" +RUNNER_STATE_SETUP_EVENTS = "setup_events" +RUNNER_STATE_AWAITING_NOTIFICATIONS = "awaiting_notification" +RUNNER_STATE_UNINSTALLING_ACTIONS = "uninstalling_actions" +RUNNER_STATE_UNSUBSCRIBING_EVENTS = "unsubscribing_events" + +LOGGER = logging.getLogger("sugar.tutorius.engine") class TutorialRunner(object): """ @@ -21,12 +52,27 @@ class TutorialRunner(object): self._tutorial = tutorial self._pM = probeManager + # The tutorial runner's state. For more details, see : + # https://docs.google.com/Doc?docid=0AVT_nzmWT2B2ZGN3dDd2MzRfNTBka3J4bW1kaA&hl=en + self._runner_state = RUNNER_STATE_IDLE + + # The message queue is a heap, so only heap operations should be done + # on it like heappush, heappop, etc... + # The stocked messages are actually a list of parameters that should be + # passed to the appropriate function. E.g. When raising an event notification, + # it saves the (next_state, event) in the message. + self._message_queue = [] + #State self._state = None - self._sEvents = set() #Subscribed Events #Cached objects - self._actions = {} + self._installed_actions = {} + self._installation_errors = {} + + # Subscribed Events + self._subscribed_events = {} + self._subscription_errors = {} #Temp FIX until event/actions have an activity id self._activity_id = None @@ -34,86 +80,216 @@ class TutorialRunner(object): #Temp FIX until event, actions have an activity id def setCurrentActivity(self): self._pM.currentActivity = self._activity_id - + + ########################################################################### + # Incoming messages def start(self): self.setCurrentActivity() #Temp Hack until activity in events/actions self.enterState(self._tutorial.INIT) def stop(self): + if self._runner_state == RUNNER_STATE_SETUP_ACTIONS or \ + self._runner_state == RUNNER_STATE_SETUP_EVENTS: + heappush(self._message_queue, (STOP_MSG_PRIORITY, None)) + elif self._runner_state != RUNNER_STATE_IDLE: + self._execute_stop() + + def action_installed(self, action_name, address): + LOGGER.debug("TutorialRunner :: Action %s received address %s"%(action_name, address)) + self._installed_actions[action_name] = address + # Verify if we just completed the installation of the actions for this state + self._verify_action_install_state() + + def install_error(self, action_name, action, exception): + # TODO : Fix this as it doesn't warn the user about the problem or anything + LOGGER.debug("TutorialRunner :: Action could not be installed %s, exception was : %s"%(str(action), str(exception))) + self._installation_errors[action_name] = exception + self._verify_action_install_state() + + def event_subscribed(self, event_name, event_address): + LOGGER.debug("TutorialRunner :: Event %s was subscribed to, located at address %s"%(event_name, event_address)) + self._subscribed_events[event_name] = event_address + + # Verify if we just completed the subscription of all the events for this state + self._verify_event_install_state() + + def subscribe_error(self, event_name, exception): + # TODO : Do correct error handling here + LOGGER.debug("TutorialRunner :: Could not subscribe to event %s, got exception : %s"%(event_name, str(exception))) + self._subscription_errors[event_name] = exception + + # Verify if we just completed the subscription of all the events for this state + self._verify_event_install_state() + + def all_actions_installed(self): + self._runner_state = RUNNER_STATE_SETUP_EVENTS + # Process the messages that might have been stored + self._process_pending_messages() + + # If we processed a message that changed the runner state, we need to stop + # processing + if self._runner_state != RUNNER_STATE_SETUP_EVENTS: + return + + # Start subscribing to events + transitions = self._tutorial.get_transition_dict(self._state) + + # If there are no transitions, raise the All Events Subscribed message + if len(transitions) == 0: + self.all_events_subscribed() + return + + # Send all the event registration + for (event_name, (event, next_state)) in transitions.items(): + self._pM.subscribe(event, + save_args(self._handleEvent, next_state), + save_args(self.event_subscribed, event_name), + save_args(self.subscribe_error, event_name)) + + def all_events_subscribed(self): + self._runner_state = RUNNER_STATE_AWAITING_NOTIFICATIONS + self._process_pending_messages() + + ########################################################################### + # Helper functions + def _execute_stop(self): self.setCurrentActivity() #Temp Hack until activity in events/actions - self.enterState(self._tutorial.END) self._teardownState() self._state = None + self._runner_state = RUNNER_STATE_IDLE def _handleEvent(self, next_state, event): - #FIXME sanity check, log event that was not installed and ignore - self.enterState(next_state) + # Look if we are actually receiving notifications + if self._runner_state == RUNNER_STATE_AWAITING_NOTIFICATIONS: + LOGGER.debug("TutorialRunner :: Received event notification in AWAITING_NOTIFICATIONS for %s"%str(event)) + transitions = self._tutorial.get_transition_dict(self._state) + for (this_event, this_next_state_name) in transitions.values(): + if event == this_event and next_state == this_next_state_name: + self.enterState(next_state) + break + elif self._runner_state == RUNNER_STATE_SETUP_EVENTS: + LOGGER.debug("TutorialRunner :: Queuing event notification to go to state %s"%next_state) + # Push the message on the queue + heappush(self._message_queue, (EVENT_NOTIFICATION_MSG_PRIORITY, (next_state, event))) + # Ignore the message for all other states def _teardownState(self): if self._state is None: #No state, no teardown return + self._remove_installed_actions() + self._remove_subscribed_events() + def _remove_installed_actions(self): #Clear the current actions - for action in self._actions.values(): - self._pM.uninstall(action) - self._actions = {} + for (action_name, action_address) in self._installed_actions.items(): + LOGGER.debug("TutorialRunner :: Uninstalling action %s with address %s"%(action_name, action_address)) + self._pM.uninstall(action_address) + self._installed_actions.clear() + self._installation_errors.clear() + def _remove_subscribed_events(self): #Clear the EventFilters - for event in self._sEvents: - self._pM.unsubscribe(event) - self._sEvents.clear() - - def _setupState(self): - if self._state is None: - raise RuntimeError("Attempting to setupState without a state") - - # Handle the automatic event - state_name = self._state - - self._actions = self._tutorial.get_action_dict(self._state) + for (event_name, event_address) in self._subscribed_events.items(): + self._pM.unsubscribe(event_address) + self._subscribed_events.clear() + self._subscription_errors.clear() + + def _verify_action_install_state(self): + actions = self._tutorial.get_action_dict(self._state) + + # Do the check to see if we have finished installing all the actions by either having + # received a address for it or an error message + install_complete = True + for (this_action_name, this_action) in actions.items(): + if not this_action_name in self._installed_actions.keys() and \ + not this_action_name in self._installation_errors.keys(): + # There's at least one uninstalled action, so we still wait + install_complete = False + break + + if install_complete: + LOGGER.debug("TutorialRunner :: All actions installed!") + # Raise the All Actions Installed event for the TutorialRunner state + self.all_actions_installed() + + def _verify_event_install_state(self): transitions = self._tutorial.get_transition_dict(self._state) - for (event, next_state) in transitions.values(): - if isinstance(event, AutomaticTransitionEvent): - state_name = next_state + # Check to see if we completed all the event subscriptions + subscribe_complete = True + for (this_event_name, (this_event, next_state)) in transitions.items(): + if not this_event_name in self._subscribed_events.keys() and \ + not this_event_name in self._subscription_errors.keys(): + subscribe_complete = False break + + if subscribe_complete: + LOGGER.debug("TutorialRunner : Subscribed to all events!") + self.all_events_subscribed() + + def _process_pending_messages(self): + while len(self._message_queue) != 0: + (priority, message) = heappop(self._message_queue) + + if priority == STOP_MSG_PRIORITY: + LOGGER.debug("TutorialRunner :: Stop message taken from message queue") + # We can safely ignore the rest of the events + self._message_queue = [] + self._execute_stop() + elif priority == EVENT_NOTIFICATION_MSG_PRIORITY: + LOGGER.debug("TutorialRunner :: Handling stored event notification for next_state %s"%message[0]) + self._handle_event(*message) - self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state))) + def _setupState(self): + if self._state is None: + raise RuntimeError("Attempting to setupState without a state") - for action in self._actions.values(): - self._pM.install(action) + actions = self._tutorial.get_action_dict(self._state) + + if len(actions) == 0: + self.all_actions_installed() + return - return state_name + for (action_name, action) in actions.items(): + LOGGER.debug("TutorialRunner :: Installed action %s"%(action_name)) + self._pM.install(action, + save_args(self.action_installed, action_name), + save_args(self.install_error, action_name)) def enterState(self, state_name): """ - Starting from the state_name, the runner execute states until - no automatic transition are found and will wait for an external - event to occur. + Starting from the state_name, the runner execute states from the + tutorial until no automatic transitions are found and will wait + for an external event to occur. - When entering the state, actions and events from the previous + When entering the sate, actions and events from the previous state are respectively uninstalled and unsubscribed and actions and events from the state_name will be installed and subscribed. @param state_name The name of the state to enter in """ self.setCurrentActivity() #Temp Hack until activity in events/actions + # Set the runner state to actions setup + self._runner_state = RUNNER_STATE_SETUP_ACTIONS - # Recursive base case - if state_name == self._state: - #Nothing to do - return + real_next_state = None + skip_to_state = state_name + + # As long as we do have automatic transitions, skip them to go to the + # next state + while skip_to_state != real_next_state: + real_next_state = skip_to_state + transitions = self._tutorial.get_transition_dict(skip_to_state) + for (event, next_state) in transitions.values(): + if isinstance(event, AutomaticTransitionEvent): + skip_to_state = next_state + break self._teardownState() - self._state = state_name - - # Recursively call the enterState in case there was an automatic - # transition in the state definition - self.enterState(self._setupState()) - - + self._state = real_next_state + self._setupState() class Engine: """ @@ -137,7 +313,8 @@ class Engine: if self._tutorial: self.stop() - self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), self._probeManager) + translator_decorator = ResourceTranslator(self._probeManager, tutorialID) + self._tutorial = TutorialRunner(Vault.loadTutorial(tutorialID), translator_decorator) #Get the active activity from the shell activity = self._shell.get_active_activity() diff --git a/tutorius/linear_creator.py b/tutorius/linear_creator.py deleted file mode 100644 index f664c49..0000000 --- a/tutorius/linear_creator.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Greatly influenced by sugar/activity/namingalert.py -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -from copy import deepcopy - -from .core import * -from .actions import * -from .filters import * - -class LinearCreator(object): - """ - This class is used to create a FSM from a linear sequence of orders. The - orders themselves are meant to be either an action or a transition. - """ - - def __init__(self): - self.fsm = FiniteStateMachine("Sample Tutorial") - self.current_actions = [] - self.nb_state = 0 - self.state_name = "INIT" - - def set_name(self, name): - """ - Sets the name of the generated FSM. - """ - self.fsm.name = name - - def action(self, action): - """ - Adds an action to execute in the current state. - """ - self.current_actions.append(action) - - def event(self, event_filter): - """ - Adds a transition to another state. When executing this, all the actions - previously called will be bundled in a single state, with the exit - condition of this state being the transition just added. - - Whatever the name of the next state you inserted in the event, it will - be replaced to point to the next event in the line. - """ - if len(self.current_actions) != 0: - # 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) - state = State(self.state_name, action_list=self.current_actions, - event_filter_list=[(event_filter, next_state_name),]) - self.state_name = next_state_name - - self.nb_state += 1 - self.fsm.add_state(state) - - # Clear the actions from the list - self.current_actions = [] - - def generate_fsm(self): - """ - Returns a finite state machine corresponding to the sequence of calls - that were made from this point on. - """ - # Copy the whole FSM that was generated yet - new_fsm = deepcopy(self.fsm) - - # Generate the final state - state = None - if len(self.current_actions) != 0: - state = State("State" + str(self.nb_state), action_list=self.current_actions) - # Don't increment the nb_state here - we would break the linearity - # because we might generate more stuff with this creator later. - # Since we rely on linearity for continuity when generating the - # next state's name on an event filter, we cannot increment here. - else: - state = State("State" + str(self.nb_state)) - - # Insert the state in the copy of the FSM - new_fsm.add_state(state) - - return new_fsm - diff --git a/tutorius/overlayer.py b/tutorius/overlayer.py index b967739..9e4adbf 100644 --- a/tutorius/overlayer.py +++ b/tutorius/overlayer.py @@ -149,8 +149,8 @@ class Overlayer(gtk.Layout): # Since widget is laid out in a Layout box, the Layout will honor the # requested size. Using size_allocate could make a nasty nested loop in # some cases. - self._overlayed.set_size_request(allocation.width, allocation.height) - + if self._overlayed: + self._overlayed.set_size_request(allocation.width, allocation.height) class TextBubble(gtk.Widget): """ @@ -319,6 +319,198 @@ class TextBubble(gtk.Widget): gobject.type_register(TextBubble) +class TextBubbleWImg(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), imagepath=None): + """ + Creates a new cairo rendered text bubble. + + @param text the text to render in the bubble + @param speaker the widget to compute the tail position from + @param tailpos (optional) position relative to the bubble to use as + the tail position, if no speaker + """ + gtk.Widget.__init__(self) + + # FIXME: ensure previous call does not interfere with widget stacking, + # as using a gtk.Layout and stacking widgets may reveal a screwed up + # order with the cairo widget on top. + self.__label = None + + self.label = text + self.speaker = speaker + self.tailpos = tailpos + self.line_width = 5 + self.padding = 20 + + # image painting + self.filename = imagepath + self.pixbuf = gtk.gdk.pixbuf_new_from_file(self.filename) + self.imgsize = (self.pixbuf.get_width(), self.pixbuf.get_height()) + + self._no_expose = False + self.__exposer = None + + def draw_with_context(self, context): + """ + Draw using the passed cairo context instead of creating a new cairo + context. This eases blending between multiple cairo-rendered + widgets. + """ + context.translate(self.allocation.x, self.allocation.y) + width = self.allocation.width + height = self.allocation.height + xradius = width/2 + yradius = height/2 + width -= self.line_width + height -= self.line_width + # + # TODO fetch speaker coordinates + + # draw bubble tail if present + 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) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_line_color) + context.stroke_preserve() + + # bubble border + context.move_to(width-self.padding, 0.0) + context.line_to(self.padding, 0.0) + context.arc_negative(self.padding, self.padding, self.padding, + 3*pi/2, pi) + context.line_to(0.0, height-self.padding) + context.arc_negative(self.padding, height-self.padding, self.padding, + pi, pi/2) + context.line_to(width-self.padding, height) + context.arc_negative(width-self.padding, height-self.padding, + self.padding, pi/2, 0) + context.line_to(width, self.padding) + context.arc_negative(width-self.padding, self.padding, self.padding, + 0.0, -pi/2) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_line_color) + context.stroke_preserve() + context.set_source_rgb(*xo_fill_color) + context.fill() + + # bubble painting. Redrawing the inside after the tail will combine + 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) + context.set_line_width(self.line_width) + context.set_source_rgb(*xo_fill_color) + context.fill() + + # draw/write text + context.set_source_rgb(1.0, 1.0, 1.0) + pangoctx = pangocairo.CairoContext(context) + self._text_layout.set_markup(self.__label) + text_size = self._text_layout.get_pixel_size() + pangoctx.move_to( + int((self.allocation.width-text_size[0])/2), + int(self.line_width+self.imgsize[1]+self.padding/2)) + pangoctx.show_layout(self._text_layout) + + # create a new cairo surface to place the image on + #surface = cairo.ImageSurface(0,x,y) + # create a context to the new surface + #ct = cairo.Context(surface) + + # paint image + context.set_source_pixbuf( + self.pixbuf, + int((self.allocation.width-self.imgsize[0])/2), + int(self.line_width+self.padding/2)) + + context.paint() + + # work done. Be kind to next cairo widgets and reset matrix. + context.identity_matrix() + + def do_realize(self): + """ Setup gdk window creation. """ + self.set_flags(gtk.REALIZED | gtk.NO_WINDOW) + self.window = self.get_parent_window() + if not self._no_expose: + self.__exposer = self.connect_after("expose-event", \ + self.__on_expose) + + def __on_expose(self, widget, event): + """Redraw event callback.""" + ctx = self.window.cairo_create() + + self.draw_with_context(ctx) + + return True + + def _set_label(self, value): + """Sets the label and flags the widget to be redrawn.""" + self.__label = "%s"%value + if not self.parent: + return + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(value) + del pangoctx, ctx#, surf + + def do_size_request(self, requisition): + """Fill requisition with size occupied by the widget.""" + ctx = self.parent.window.cairo_create() + pangoctx = pangocairo.CairoContext(ctx) + self._text_layout = pangoctx.create_layout() + self._text_layout.set_markup(self.__label) + + width, height = self._text_layout.get_pixel_size() + + max_width = width + if self.imgsize[0] > width: + max_width = self.imgsize[0] + requisition.width = int(2*self.padding+max_width) + + requisition.height = int(2*self.padding+height+self.imgsize[1]) + + def do_size_allocate(self, allocation): + """Save zone allocated to the widget.""" + self.allocation = allocation + + def _get_label(self): + """Getter method for the label property""" + return self.__label[3:-4] + + def _set_no_expose(self, value): + """setter for no_expose property""" + self._no_expose = value + if not (self.flags() and gtk.REALIZED): + return + + if self.__exposer and value: + self.parent.disconnect(self.__exposer) + self.__exposer = None + elif (not self.__exposer) and (not value): + self.__exposer = self.parent.connect_after("expose-event", + self.__on_expose) + + def _get_no_expose(self): + """getter for no_expose property""" + return self._no_expose + + no_expose = property(fset=_set_no_expose, fget=_get_no_expose, + doc="Whether the widget should handle exposition events or not.") + + label = property(fget=_get_label, fset=_set_label, + doc="Text label which is to be painted on the top of the widget") + +gobject.type_register(TextBubbleWImg) + + class Rectangle(gtk.Widget): """ A CanvasDrawableWidget drawing a rectangle over a specified widget. diff --git a/tutorius/properties.py b/tutorius/properties.py index 01cd2c0..cc76748 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -25,7 +25,9 @@ from copy import copy, deepcopy from .constraints import Constraint, \ UpperLimitConstraint, LowerLimitConstraint, \ MaxSizeConstraint, MinSizeConstraint, \ - ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint + ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \ + ResourceConstraint + from .propwidgets import PropWidget, \ StringPropWidget, \ UAMPropWidget, \ @@ -99,6 +101,21 @@ class TPropContainer(object): except AttributeError: return object.__setattr__(self, name, value) + def replace_property(self, prop_name, new_prop): + """ + Changes the content of a property. This is done in order to support + the insertion of executable properties in the place of a portable + property. The typical exemple is that a resource property needs to + be changed into a file property with the correct file name, since the + installation location will be different on every platform. + + @param prop_name The name of the property to be changed + @param new_prop The new property to insert + @raise AttributeError of the mentionned property doesn't exist + """ + props = object.__getattribute__(self, "_props") + props.__setitem__(prop_name, new_prop) + def get_properties(self): """ Return the list of property names. @@ -289,6 +306,37 @@ class TFileProperty(TutoriusProperty): self.default = self.validate(path) +class TResourceProperty(TutoriusProperty): + """ + Represents a resource in the tutorial. A resource is a file with a specific + name that exists under the tutorials folder. It is distributed alongside the + tutorial itself. + + When the system encounters a resource, it knows that it refers to a file in + the resource folder and that it should translate this resource name to an + absolute file name before it is executed. + + E.g. An image is added to a tutorial in an action. On doing so, the creator + adds a resource to the tutorial, then saves its name in the resource + property of that action. When this tutorial is executed, the Engine + replaces all the TResourceProperties inside the action by their equivalent + TFileProperties with absolute paths, so that they can be used on any + machine. + """ + def __init__(self, resource_name=""): + """ + Creates a new resource pointing to an existing resource. + + @param resource_name The file name of the resource (should be only the + file name itself, no directory information) + """ + TutoriusProperty.__init__(self) + self.type = "resource" + + self.resource_cons = ResourceConstraint() + + self.default = self.validate("") + class TEnumProperty(TutoriusProperty): """ Represents a value in a given enumeration. This means that the value will diff --git a/tutorius/translator.py b/tutorius/translator.py new file mode 100644 index 0000000..4f29078 --- /dev/null +++ b/tutorius/translator.py @@ -0,0 +1,200 @@ +# Copyright (C) 2009, Tutorius.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import os +import logging +import copy as copy_module + +logger = logging.getLogger("ResourceTranslator") + +from .properties import * +from .vault import Vault +from .dbustools import save_args + +class ResourceTranslator(object): + """ + Handles the conversion of resource properties into file + properties before action execution. This class works as a decorator + to the ProbeManager class, as it is meant to be a transparent layer + before sending the action to execution. + + An architectural note : every different type of translation should have its + own method (translate_resource, etc...), and this function must be called + from the translate method, under the type test. The translate_* method + must take in the input property and give the output property that should + replace it. + """ + + def __init__(self, probe_manager, tutorial_id): + """ + Creates a new ResourceTranslator for the given tutorial. This + translator is tasked with replacing resource properties of the + incoming action into actually usable file properties pointing + to the correct resource file. This is done by querying the vault + for all the resources and creating a new file property from the + returned path. + + @param probe_manager The probe manager to decorate + @param tutorial_id The ID of the current tutorial + """ + self._probe_manager = probe_manager + self._tutorial_id = tutorial_id + + def translate_resource(self, res_value): + """ + Replace the TResourceProperty in the container by their + runtime-defined file equivalent. Since the resources are stored + in a relative manner in the vault and that only the Vault knows + which is the current folder for the current tutorial, it needs + to transform the resource identifier into the absolute path for + the process to be able to use it properly. + + @param res_prop The resource property's value to be translated + @return The TFileProperty corresponding to this resource, containing + an absolute path to the resource + """ + # We need to replace the resource by a file representation + filepath = Vault.get_resource_path(self._tutorial_id, res_value) + logger.debug("ResourceTranslator :: Matching resource %s to file %s" % (res_value, filepath)) + + # Create the new file representation + file_prop = TFileProperty(filepath) + + return file_prop + + def translate(self, prop_container): + """ + Applies the required translation to be able to send the container to an + executing endpoint (like a Probe). For each type of property that + requires it, there is translation function that will take care of + mapping the property to its executable form. + + This function does not return anything, but its post-condition is + that all the properties of the input container have been replaced + by their corresponding executable versions. + + An example of translation is taking a resource (a relative path to + a file under the tutorial folder) and transforming it into a file + (a full absolute path) to be able to load it when the activity receives + the action. + + @param prop_container The property container in which we want to + replace all the resources for file properties and + to recursively do so for addon and addonlist + properties. + """ + for propname in prop_container.get_properties(): + prop_value = getattr(prop_container, propname) + prop_type = getattr(type(prop_container), propname).type + + # If the property is a resource, then we need to query the + # vault to create its correspondent + if prop_type == "resource": + # Apply the translation + file_prop = self.translate_resource(prop_value) + # Set the property with the new value + prop_container.replace_property(propname, file_prop) + + # If the property is an addon, then its value IS a + # container too - we need to translate it + elif prop_type == "addon": + # Translate the sub properties + self.translate(prop_value) + + # If the property is an addon list, then we need to translate all + # the elements of the list + elif prop_type == "addonlist": + # Now, for each sub-container in the list, we will apply the + # translation processing. This is done by popping the head of + # the list, translating it and inserting it back at the end. + for index in range(0, len(prop_value)): + # Pop the head of the list + container = prop_value[0] + del prop_value[0] + # Translate the sub-container + self.translate(container) + + # Put the value back in the list + prop_value.append(container) + # Change the list contained in the addonlist property, since + # we got a copy of the list when requesting it + prop_container.replace_property(propname, prop_value) + + ########################################################################### + ### ProbeManager interface for decorator + + # Unchanged functions + def setCurrentActivity(self, activity_id): + self._probe_manager.currentActivity = activity_id + + def getCurrentActivity(self): + return self._probe_manager.currentActivity + + currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) + def attach(self, activity_id): + self._probe_manager.attach(activity_id) + + def detach(self, activity_id): + self._probe_manager.detach(activity_id) + + def subscribe(self, event, notification_cb, event_subscribed_cb, error_cb): + return self._probe_manager.subscribe(event, notification_cb, event_subscribed_cb, error_cb) + + def unsubscribe(self, address): + return self._probe_manager.unsubscribe(address) + + def register_probe(self, process_name, unique_id): + self._probe_manager.register_probe(process_name, unique_id) + + def unregister_probe(self, unique_id): + self._probe_manager.unregister_probe(unique_id) + + def get_registered_probes_list(self, process_name=None): + return self._probe_manager.get_registered_probes_list(process_name) + + ########################################################################### + + def action_installed(self, action_installed_cb, address): + # Callback to the upper layers to inform them that the action + # was installed + action_installed_cb(address) + + def action_install_error(self, install_error_cb, old_action, exception): + # Warn the upper layer that the installation failed + install_error_cb(old_action, exception) + + # Decorated functions + def install(self, action, action_installed_cb, error_cb): + # Make a new copy of the action that we want to install, + # because translate() changes the action and we + # don't want to modify the caller's action representation + new_action = copy_module.deepcopy(action) + # Execute the replacement + self.translate(new_action) + + # Send the new action to the probe manager + self._probe_manager.install(new_action, save_args(self.action_installed, action_installed_cb), + save_args(self.action_install_error, error_cb, new_action)) + + def update(self, action_address, newaction): + translated_new_action = copy_module.deepcopy(newaction) + self.translate(translated_new_action) + + self._probe_manager.update(action_address, translated_new_action, block) + + def uninstall(self, action_address): + self._probe_manager.uninstall(action_address) + diff --git a/tutorius/vault.py b/tutorius/vault.py index a3b84a4..1c1e33c 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -251,26 +251,25 @@ class Vault(object): addition_flag = False # Check if at least one keyword of the list is present for key in keyword: - for value in metadata_dictionnary.values(): - if isinstance(value, str) == True: - if value.lower().count(key.lower()) > 0: - addition_flag = True - else: + if key != None: + for value in metadata_dictionnary.values(): + if isinstance(value, str): + if value.lower().count(key.lower()) > 0: + addition_flag = True # Check one layer of depth in the metadata to find the keyword # (for exemple, related activites are a dictionnary stocked - # in a value of the main dictionnary) - if isinstance(value, dict) == True: + # in a value of the main dictionnary) + elif isinstance(value, dict): for inner_key, inner_value in value.items(): - if isinstance(inner_value, str) == True and isinstance(inner_key, str) == True: - if inner_value.lower().count(key.lower()) > 0 or inner_key.count(key.lower()) > 0: - addition_flag = True + if isinstance(inner_value, str) and isinstance(inner_key, str) and (inner_value.lower().count(key.lower()) > 0 or inner_key.count(key.lower()) > 0): + addition_flag = True # Filter tutorials for related activities if relatedActivityNames != []: addition_flag = False # Check if at least one element of the list is present for related in relatedActivityNames: - if related.lower() in related_act_dictionnary.keys(): + if related != None and related.lower() in related_act_dictionnary.keys(): addition_flag = True # Filter tutorials for categories @@ -278,9 +277,8 @@ class Vault(object): addition_flag = False # Check if at least one element of the list is present for cat in category: - if metadata_dictionnary.has_key(INI_CATEGORY_PROPERTY): - if metadata_dictionnary[INI_CATEGORY_PROPERTY].lower() == cat.lower(): - addition_flag = True + if cat != None and metadata_dictionnary.has_key(INI_CATEGORY_PROPERTY) and metadata_dictionnary[INI_CATEGORY_PROPERTY].lower() == cat.lower(): + addition_flag = True # Add this dictionnary to tutorial list if it has not been filtered if addition_flag == True: @@ -524,6 +522,7 @@ class Vault(object): tutorial_path = bundler.get_tutorial_path(tutorial_guid) # Check if the resource file exists file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) + logger.debug("Vault :: Assigning resource %s to file %s "%(resource_id, file_path)) if os.path.isfile(file_path): return file_path else: -- cgit v0.9.1