diff options
Diffstat (limited to 'tutorius/vault.py')
-rw-r--r-- | tutorius/vault.py | 982 |
1 files changed, 982 insertions, 0 deletions
diff --git a/tutorius/vault.py b/tutorius/vault.py new file mode 100644 index 0000000..9576de9 --- /dev/null +++ b/tutorius/vault.py @@ -0,0 +1,982 @@ +# Copyright (C) 2009, Tutorius.org +# Copyright (C) 2009, Jean-Christophe Savard <savard.jean.christophe@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +""" +This module contains all the data handling class of Tutorius +""" + +import logging +import os +import shutil +import tempfile +import uuid +import xml.dom.minidom +from xml.dom import NotFoundErr +import zipfile +from ConfigParser import SafeConfigParser + +from . import addon +from .tutorial import Tutorial, State, AutomaticTransitionEvent + +logger = logging.getLogger("tutorius") + +# this is where user installed/generated tutorials will go +def _get_store_root(): + profile_name = os.getenv("SUGAR_PROFILE") or "default" + return os.path.join(os.getenv("HOME"), + ".sugar",profile_name,"tutorius","data") +# this is where activity bundled tutorials should be, under the activity bundle +def _get_bundle_root(): + """ + Return the path of the bundled activity, or None if not applicable. + """ + if os.getenv("SUGAR_BUNDLE_PATH") != None: + return os.path.join(os.getenv("SUGAR_BUNDLE_PATH"),"data","tutorius","data") + else: + return None + +INI_ACTIVITY_SECTION = "RELATED_ACTIVITIES" +INI_METADATA_SECTION = "GENERAL_METADATA" +INI_GUID_PROPERTY = "guid" +INI_NAME_PROPERTY = "name" +INI_XML_FSM_PROPERTY = "fsm_filename" +INI_VERSION_PROPERTY = 'version' +INI_FILENAME = "meta.ini" +TUTORIAL_FILENAME = "tutorial.xml" + +###################################################################### +# XML Tag names and attributes +###################################################################### +ELEM_FSM = "FSM" +ELEM_STATES = "States" +ELEM_STATE = "State" +ELEM_ACTIONS = "Actions" +ELEM_TRANS = "Transitions" +ELEM_AUTOTRANS = "AutomaticTransition" +NODE_COMPONENT = "Component" +NODE_SUBCOMPONENT = "property" +NODE_SUBCOMPONENTLIST = "listproperty" +NAME_ATTR = "__name__" +NEXT_STATE_ATTR = "__next_state__" +START_STATE_ATTR = "__start_state__" +RESSOURCES_FOLDER = 'ressources' + +class Vault(object): + + ## Vault internal functions : + @staticmethod + def list_available_tutorials(activity_name = None, activity_vers = 0): + """ + Generate the list of all tutorials present on disk for a + given activity. + + @param activity_name the name of the activity associated with this tutorial. None means ALL activities + @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. If activity_name is None, version number is not used + @returns a map of tutorial {names : GUID}. + """ + # check both under the activity data and user installed folders + if _get_bundle_root() != None: + paths = [_get_store_root(), _get_bundle_root()] + else: + paths = [_get_store_root()] + + tutoGuidName = {} + + for repository in paths: + # (our) convention dictates that tutorial folders are named + # with their GUID (for unicity) + try: + for tuto in os.listdir(repository): + parser = SafeConfigParser() + file = parser.read(os.path.join(repository, tuto, INI_FILENAME)) + if file != []: + # If parser can read at least section + guid = parser.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) + name = parser.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + activities = parser.options(INI_ACTIVITY_SECTION) + # enforce matching activity name AND version, as UI changes + # break tutorials. We may lower this requirement when the + # UAM gets less dependent on the widget order. + # Also note property names are always stored lowercase. + if (activity_name != None) and (activity_name.lower() in activities): + version = parser.get(INI_ACTIVITY_SECTION, activity_name) + if (activity_vers == version) or (activity_vers == 0): + tutoGuidName[guid] = name + elif (activity_name == None): + tutoGuidName[guid] = name + except OSError: + # the repository may not exist. Continue scanning + pass + + return tutoGuidName + + ## Vault interface functions : + @staticmethod + def installTutorials(path, zip_file_name, forceinstall=False): + """ + Extract the tutorial files in the ZIPPED tutorial archive at the + specified path and add them inside the vault. This should remove any previous + version of this tutorial, if there's any. On the opposite, if we are + trying to install an earlier version, the function will return 1 if + forceInstall is not set to true. + + @params path The path where the zipped tutorial archive is present + @params forceinstall A flag that indicate if we need to force overwrite + of a tutorial even if is version number is lower than the existing one. + + @returns 0 if it worked, 1 if the user needs to confirm the installation + and 2 to mean an error happened + """ + # TODO : Check with architecture team for exception vs error returns + + # test if the file is a valid pkzip file + if zipfile.is_zipfile(os.path.join(path, zip_file_name)) != True: + assert False, "Error : The given file is not a valid PKZip file" + + # unpack the zip archive + zfile = zipfile.ZipFile(os.path.join(path, zip_file_name), "r" ) + + temp_path = tempfile.mkdtemp(dir=_get_store_root()) + zfile.extractall(temp_path) + + # get the tutorial file + ini_file_path = os.path.join(temp_path, INI_FILENAME) + ini_file = SafeConfigParser() + ini_file.read(ini_file_path) + + # get the tutorial guid + guid = ini_file.get(INI_METADATA_SECTION, INI_GUID_PROPERTY) + + # Check if tutorial already exist + tutorial_path = os.path.join(_get_store_root(), guid) + if os.path.isdir(tutorial_path) == False: + # Copy the tutorial in the Vault + shutil.copytree(temp_path, tutorial_path) + + else: + # Check the version of the existing tutorial + existing_version = ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY) + # Check the version of the new tutorial + new_ini_file = SafeConfigParser() + new_ini_file.read(os.path.join(tutorial_path, INI_FILENAME)) + new_version = new_ini_file.get(INI_METADATA_SECTION, INI_VERSION_PROPERTY) + + if new_version < existing_version and forceinstall == False: + # Version of new tutorial is older and forceinstall is false, return exception + return 1 + else : + # New tutorial is newer or forceinstall flag is set, can overwrite the existing tutorial + shutil.rmtree(tutorial_path) + shutil.copytree(temp_path, tutorial_path) + + # Remove temp data + shutil.rmtree(temp_path) + + return 0 + + @staticmethod + def query(keyword=[], relatedActivityNames=[], category=[]): + """ + Returns the list of tutorials that corresponds to the specified parameters. + + @returns a list of Tutorial meta-data (TutorialID, Description, + Rating, Category, PublishState, etc...) + TODO : Search for tuto caracterised by the entry : OR between [], and between each + + The returned dictionnary is of this format : key = property name, value = property value + The dictionnary also contain one dictionnary element whose key is the string 'activities' + and whose value is another dictionnary of this form : key = related activity name, + value = related activity version number + """ + + # Temp solution for returning all tutorials metadata + + tutorial_list = [] + tuto_guid_list = [] + ini_file = SafeConfigParser() + if keyword == [] and relatedActivityNames == [] and category == []: + # get all tutorials tuples (name:guid) for all activities and version + tuto_dict = Vault.list_available_tutorials() + for id in tuto_dict.keys(): + tuto_guid_list.append(id) + + # Find .ini metadata files with the guid list + + # Get the guid from the tuto tuples + for guid in tuto_guid_list: + # Create a dictionnary containing the metadata and also + # another dictionnary containing the tutorial Related Acttivities, + # and add it to a list + + # Create a TutorialBundler object from the guid + bundler = TutorialBundler(guid) + # Find the .ini file path for this guid + ini_file_path = bundler.get_tutorial_path(guid) + # Read the .ini file + ini_file.read(os.path.join(ini_file_path, 'meta.ini')) + + metadata_dictionnary = {} + related_act_dictionnary = {} + metadata_list = ini_file.options(INI_METADATA_SECTION) + for metadata_name in metadata_list: + # Create a dictionnary of tutorial metadata + metadata_dictionnary[metadata_name] = ini_file.get(INI_METADATA_SECTION, metadata_name) + # Get Related Activities data from.ini files + related_act_list = ini_file.options(INI_ACTIVITY_SECTION) + for related_act in related_act_list: + # For related activites, the format is : key = activity name, value = activity version + related_act_dictionnary[related_act] = ini_file.get(INI_ACTIVITY_SECTION, related_act) + + # Add Related Activities dictionnary to metadata dictionnary + metadata_dictionnary['activities'] = related_act_dictionnary + + # Add this dictionnary to tutorial list + tutorial_list.append(metadata_dictionnary) + + # Return tutorial list + return tutorial_list + + @staticmethod + def loadTutorial(Guid): + """ + Creates an executable version of a tutorial from its saved representation. + @param Guid Unique identifier of the tutorial + @returns Tutorial object + """ + + bundle = TutorialBundler(Guid) + bundle_path = bundle.get_tutorial_path(Guid) + config = SafeConfigParser() + config.read(os.path.join(bundle_path, INI_FILENAME)) + + serializer = XMLSerializer() + + name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) + + # Open the XML file + tutorial_file = os.path.join(bundle_path, TUTORIAL_FILENAME) + with open(tutorial_file, 'r') as tfile: + tutorial = serializer.load_tutorial(tfile) + + return tutorial + + @staticmethod + def saveTutorial(tutorial, metadata_dict): + """ + Creates a persistent version of a tutorial in the Vault. + @param tutorial Tutorial + @param metadata_dict dictionary of metadata for the Tutorial + @returns true if the tutorial was saved correctly + """ + + # Get the tutorial guid from metadata dictionnary + guid = metadata_dict[INI_GUID_PROPERTY] + + # Check if tutorial already exist + tutorial_path = os.path.join(_get_store_root(), guid) + if os.path.isdir(tutorial_path) == False: + + # Serialize the tutorial and write it to disk + xml_ser = XMLSerializer() + os.makedirs(tutorial_path) + + with open(os.path.join(tutorial_path, TUTORIAL_FILENAME), 'w') as fsmfile: + xml_ser.save_tutorial(tutorial, fsmfile) + + # Create the metadata file + ini_file_path = os.path.join(tutorial_path, "meta.ini") + parser = SafeConfigParser() + parser.add_section(INI_METADATA_SECTION) + for key, value in metadata_dict.items(): + if key != 'activities': + parser.set(INI_METADATA_SECTION, key, value) + else: + related_activities_dict = value + parser.add_section(INI_ACTIVITY_SECTION) + for related_key, related_value in related_activities_dict.items(): + parser.set(INI_ACTIVITY_SECTION, related_key, related_value) + + # Write the file to disk + with open(ini_file_path, 'wb') as configfile: + parser.write(configfile) + + else: + # Error, tutorial already exist + return False + + # TODO : wait for Ben input on how to unpublish tuto before coding this function + # For now, no unpublishing will occur. + + + @staticmethod + def deleteTutorial(tutorial_id): + """ + Removes the tutorial from the Vault. It will unpublish the tutorial if need be, + and it will also wipe it from the persistent storage. + @returns true is the tutorial was deleted from the Vault + """ + bundle = TutorialBundler(tutorial_id) + bundle_path = bundle.get_tutorial_path(tutorial_id) + + # TODO : Need also to unpublish tutorial, need to interact with webservice module + + shutil.rmtree(bundle_path) + if os.path.isdir(bundle_path) == False: + return True + else: + return False + + + @staticmethod + def add_resource(tutorial_guid, file_path): + """ + Returns a unique name for this resource composed from the original name of the file + and a suffix to make it unique ( ex: name_1.jpg ) and add it to the resources for the tutorial. + @param tutorial_guid The guid of the tutorial + @param file_path the file path of the ressource to add + @returns the ressource_id of the ressource + """ + + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Get the file name + fname_splitted = file_path.rsplit('/') + file_name = fname_splitted[fname_splitted.__len__() - 1] + base_name, extension = os.path.splitext(file_name) + # Append unique name to file name + file_name_appended = base_name + '_' + str(uuid.uuid1()) + extension + # Check if the ressource file already exists + new_file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, file_name_appended) + if os.path.isfile(new_file_path) == False: + # Copy the ressource file in the vault + if os.path.isdir(os.path.join(tutorial_path, RESSOURCES_FOLDER)) == False: + os.makedirs(os.path.join(tutorial_path, RESSOURCES_FOLDER)) + assert os.path.isfile(file_path) + shutil.copyfile(file_path, new_file_path) + + return file_name_appended + + + @staticmethod + def delete_resource(tutorial_guid, ressource_id): + """ + Delete the resource from the resources of the tutorial. + @param tutorial_guid the guid of the tutorial + @param ressource_id the ressource id of the ressource to delete + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the ressource file exists + file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, ressource_id) + if os.path.isfile(file_path): + # Delete the ressource + os.remove(file_path) + else: + print('File not found, no delete took place') + + @staticmethod + def get_resource_path(tutorial_guid, ressource_id): + """ + Returns the absolute file path to the resourceID + @param tutorial_guid the guid of the tutorial + @param ressource_id the ressource id of the ressource to find the path for + @returns the absolute path of the ressource file + """ + # Get the tutorial path + bundler = TutorialBundler(tutorial_guid) + tutorial_path = bundler.get_tutorial_path(tutorial_guid) + # Check if the ressource file exists + file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, ressource_id) + if os.path.isfile(file_path): + return file_path + else: + return None + + +class Serializer(object): + """ + Interface that provide serializing and deserializing of the FSM + used in the tutorials to/from disk. Must be inherited. + """ + + def save_tutorial(self,fsm): + """ + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are overwritten. If the GUId is not found, an + exception occur. If no GUID is provided, FSM is written in a new file + in the store root. + """ + raise NotImplementedError() + + def load_tutorial(self): + """ + Load fsm from disk. + """ + raise NotImplementedError() + +class XMLSerializer(Serializer): + """ + Class that provide serializing and deserializing of the FSM + used in the tutorials to/from a .xml file. Inherit from Serializer + """ + + @classmethod + def _create_state_dict_node(cls, state_dict, doc): + """ + Create and return a xml Node from a State dictionnary. + @param state_dict dictionary of State objects + @param doc The XML document root (used to create nodes only + @return xml Element containing the states + """ + statesList = doc.createElement(ELEM_STATES) + for state_name, state in state_dict.items(): + stateNode = doc.createElement(ELEM_STATE) + statesList.appendChild(stateNode) + stateNode.setAttribute("Name", state_name) + actionsList = stateNode.appendChild(cls._create_action_list_node(state.get_action_dict(), doc)) + transitionsList = stateNode.appendChild(cls._create_transitions_node(state.get_transition_dict(), doc)) + return statesList + + @classmethod + def _create_addon_component_node(cls, parent_attr_name, comp, doc): + """ + Takes a component that is embedded in another component (e.g. the content + of a OnceWrapper) and encapsulate it in a node with the property name. + + e.g. + <Component Class="OnceWrapper"> + <property name="addon"> + <Component Class="BubbleMessage" message="'Hi!'" position="[12,32]"/> + </property> + </Component> + + When reloading this node, we should look up the property name for the parent + in the attribute of the node, then examine the subnode to create the addon + object itself. + + @param parent_attr_name The name of the parent's attribute for this addon + e.g. the OnceWrapper has the action attribute, which corresponds to a + sub-action it must execute once. + @param comp The component node itself + @param doc The XML document root (only used to create the nodes) + @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node + that represents another component. + """ + subCompNode = doc.createElement(NODE_SUBCOMPONENT) + subCompNode.setAttribute("name", parent_attr_name) + + subNode = cls._create_component_node(comp, doc) + + subCompNode.appendChild(subNode) + + return subCompNode + + @classmethod + def _create_addonlist_component_node(cls, parent_attr_name, comp_list, doc): + """ + Takes a list of components that are embedded in another component (ex. the + content of a ChainAction) and encapsulate them in a node with the property + name. + + e.g. + <Component Class="ChainAction"> + <listproperty name="actions"> + <Component Class="BubbleMessage" message="'Hi!'" position="[15,35]"/> + <Component Class="DialogMessage" message="'Multi-action!'" position="[45,10]"/> + </listproperty> + </Component> + + When reloading this node, we should look up the property name for the parent + in the the attribute of the node, then rebuild the list by appending the + content of all the subnodes. + + @param parent_attr_name The name of the parent component's property + @param comp_list A list of components that comprise the property + @param doc The XML document root (only for creating new nodes) + @returns A NODE_SUBCOMPONENTLIST node with the property attribute + """ + subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST) + subCompListNode.setAttribute("name", parent_attr_name) + + for comp in comp_list: + compNode = cls._create_component_node(comp, doc) + subCompListNode.appendChild(compNode) + + return subCompListNode + + @classmethod + def _create_component_node(cls, comp, doc): + """ + Takes a single component (action or eventfilter) and transforms it + into a xml node. + + @param comp A single component + @param doc The XML document root (used to create nodes only + @return A XML Node object with the component tag name + """ + compNode = doc.createElement(NODE_COMPONENT) + + # Write down just the name of the Action class as the Class + # property -- + compNode.setAttribute("Class",type(comp).__name__) + + # serialize all tutorius properties + for propname in comp.get_properties(): + propval = getattr(comp, propname) + if getattr(type(comp), propname).type == "addonlist": + compNode.appendChild(cls._create_addonlist_component_node(propname, propval, doc)) + elif getattr(type(comp), propname).type == "addon": + #import rpdb2; rpdb2.start_embedded_debugger('pass') + compNode.appendChild(cls._create_addon_component_node(propname, propval, doc)) + else: + # repr instead of str, as we want to be able to eval() it into a + # valid object. + compNode.setAttribute(propname, repr(propval)) + + return compNode + + @classmethod + def _create_action_list_node(cls, action_dict, doc): + """ + Create and return a xml Node from a Action list. + + @param action_dict Dictionary of actions with names as keys + @param doc The XML document root (used to create new nodes only) + @return A XML Node object with the Actions tag name and a serie of + Action children + """ + actionsList = doc.createElement(ELEM_ACTIONS) + for name, action in action_dict.items(): + # Create the action node + actionNode = cls._create_component_node(action, doc) + actionNode.setAttribute(NAME_ATTR, name) + # Append it to the list + actionsList.appendChild(actionNode) + + return actionsList + + @classmethod + def _create_transitions_node(cls, transition_dict, doc): + """ + Create and return a xml Node from a transition dictionary. + @param transition_dict dictionary of (event, next_state) transitions. + @param doc The XML document root (used to create nodes only + @return xml Element containing the transitions + """ + eventFiltersList = doc.createElement(ELEM_TRANS) + for transition_name, (event, end_state) in transition_dict.items(): + #start_state = transition_name.split(Tutorial._NAME_SEPARATOR)[0] + #XXX The addon is not in the cache and cannot be loaded so we + # store it differently for now + if type(event) == AutomaticTransitionEvent: + eventFilterNode = doc.createElement(ELEM_AUTOTRANS) + else: + eventFilterNode = cls._create_component_node(event, doc) + #eventFilterNode.setAttribute(START_STATE_ATTR, unicode(start_state)) + eventFilterNode.setAttribute(NEXT_STATE_ATTR, unicode(end_state)) + eventFilterNode.setAttribute(NAME_ATTR, transition_name) + eventFiltersList.appendChild(eventFilterNode) + + return eventFiltersList + + @classmethod + def save_tutorial(cls, fsm, file_obj): + """ + Save fsm to file + + @param fsm Tutorial to save + @param file_obj file-like object in which the serialized fsm is saved + + Side effects: + A serialized version of the Tutorial is written to file_obj. + The file is not closed automatically. + """ + doc = xml.dom.minidom.Document() + fsm_element = doc.createElement(ELEM_FSM) + doc.appendChild(fsm_element) + + fsm_element.setAttribute("Name", fsm.name) + + states = cls._create_state_dict_node(fsm.get_state_dict(), doc) + fsm_element.appendChild(states) + + file_obj.write(doc.toprettyxml()) + + @classmethod + def _get_direct_descendants_by_tag_name(cls, node, name): + """ + Searches in the list of direct descendants of a node to find all the node + that have the given name. + + 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 + + @classmethod + def _load_xml_transitions(cls, filters_elem): + """ + Loads up a list of Event Filters. + + @param filters_elem An XML Element representing a list of event filters + @return dict of (event, next_state) transitions, keyed by name + """ + transition_dict = {} + + #Retrieve normal transitions + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) + new_transition = None + + for transition in transition_element_list: + #start_state = transition.getAttribute(START_STATE_ATTR) + next_state = transition.getAttribute(NEXT_STATE_ATTR) + transition_name = transition.getAttribute(NAME_ATTR) + try: + #The attributes must be removed so that they are not + # viewed as a property in load_xml_component + # transition.removeAttribute(START_STATE_ATTR) + transition.removeAttribute(NEXT_STATE_ATTR) + transition.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue + + new_transition = cls._load_xml_component(transition) + + if new_transition is not None: + transition_dict[transition_name] = (new_transition, next_state) + + #Retrieve automatic transitions + # XXX This is done differently as the AutomaticTransitionEvent + # cannot be loaded dynamically (yet?) + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, ELEM_AUTOTRANS) + new_transition = None + + for transition in transition_element_list: + #start_state = transition.getAttribute(START_STATE_ATTR) + next_state = transition.getAttribute(NEXT_STATE_ATTR) + transition_name = transition.getAttribute(NAME_ATTR) + try: + #The attributes must be removed so that they are not + # viewed as a property in load_xml_component + # transition.removeAttribute(START_STATE_ATTR) + transition.removeAttribute(NEXT_STATE_ATTR) + transition.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue + + transition_dict[transition_name] = (AutomaticTransitionEvent(), next_state) + + return transition_dict + + @classmethod + def _load_xml_subcomponents(cls, node, properties): + """ + Loads all the subcomponent node below the given node and inserts them with + the right property name inside the properties dictionnary. + + @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 = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) + + for subComp in subCompList: + property_name = subComp.getAttribute("name") + internal_comp_node = cls._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] + internal_comp = cls._load_xml_component(internal_comp_node) + properties[str(property_name)] = internal_comp + + @classmethod + def _load_xml_subcomponent_lists(cls, node, properties): + """ + Loads all the subcomponent lists below the given node and stores them + under the correct property name for that node. + + @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 = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) + for subCompListNode in listOf_subCompListNode: + property_name = subCompListNode.getAttribute("name") + subCompList = [] + for subCompNode in cls._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): + subComp = cls._load_xml_component(subCompNode) + subCompList.append(subComp) + properties[str(property_name)] = subCompList + + @classmethod + def _load_xml_component(cls, node): + """ + Loads a single addon component instance from an Xml node. + + @param node The component XML Node to transform + object + @return The addon component object of the correct type according to the XML + description + """ + class_name = node.getAttribute("Class") + + properties = {} + + for prop in node.attributes.keys(): + if prop == "Class" : continue + # security : keep sandboxed + properties[str(prop)] = eval(node.getAttribute(prop)) + + # Read the complex attributes + cls._load_xml_subcomponents(node, properties) + cls._load_xml_subcomponent_lists(node, properties) + + new_action = addon.create(class_name, **properties) + + if not new_action: + return None + + return new_action + + @classmethod + def _load_xml_actions(cls, actions_elem): + """ + Transforms an Actions element into a dict of instanciated Action. + + @param actions_elem An XML Element representing a list of Actions + @return dictionary of actions keyed by name + """ + action_dict = {} + actions_element_list = cls._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) + + for action in actions_element_list: + action_name = action.getAttribute(NAME_ATTR) + try: + #The name attribute must be removed so that it is not + # viewed as a property in load_xml_component + action.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue + new_action = cls._load_xml_component(action) + + action_dict[action_name] = new_action + + return action_dict + + @classmethod + def _load_xml_states(cls, states_elem): + """ + Takes in a States element and fleshes out a complete list of State + objects. + + @param states_elem An XML Element that represents a list of States + @return dictionary of States + """ + state_dict = {} + # item(0) because there is always only one <States> tag in the xml file + # so states_elem should always contain only one element + states_element_list = states_elem.item(0).getElementsByTagName(ELEM_STATE) + + for state in states_element_list: + stateName = state.getAttribute("Name") + # Using item 0 in the list because there is always only one + # Actions and EventFilterList element per State node. + actions_list = cls._load_xml_actions(state.getElementsByTagName(ELEM_ACTIONS)[0]) + transitions_list = cls._load_xml_transitions(state.getElementsByTagName(ELEM_TRANS)[0]) + + state_dict[stateName] = State(stateName, actions_list, transitions_list) + + return state_dict + + @classmethod + def load_xml_tutorial(cls, fsm_elem): + """ + Takes in an XML element representing an FSM and returns the fully + crafted FSM. + + @param fsm_elem The XML element that describes a FSM + @return Tutorial loaded from xml element + """ + # Load the FSM's name and start state's name + fsm_name = fsm_elem.getAttribute("Name") + + # Load the states + states_dict = cls._load_xml_states(fsm_elem.getElementsByTagName(ELEM_STATES)) + fsm = Tutorial(fsm_name, states_dict) + + return fsm + + @classmethod + def load_tutorial(cls, tutorial_file): + """ + Load fsm from xml file + @param tutorial_file file-like object to read the fsm from + @return Tutorial object that was loaded from the file + """ + xml_dom = xml.dom.minidom.parse(tutorial_file) + + fsm_elem = xml_dom.getElementsByTagName(ELEM_FSM)[0] + + return cls.load_xml_tutorial(fsm_elem) + +class TutorialBundler(object): + """ + This class provide the various data handling methods useable by the tutorial + editor. + """ + + def __init__(self,generated_guid = None, bundle_path=None): + """ + Tutorial_bundler constructor. If a GUID is given in the parameter, the + Tutorial_bundler object will be associated with it. If no GUID is given, + a new GUID will be generated, + """ + + self.Guid = generated_guid or str(uuid.uuid1()) + + #FIXME: Look for the bundle in the activity first (more specific) + #Look for the file in the path if a uid is supplied + if generated_guid: + #General store + store_path = os.path.join(_get_store_root(), str(generated_guid), INI_FILENAME) + if os.path.isfile(store_path): + self.Path = os.path.dirname(store_path) + elif _get_bundle_root() != None: + #Bundle store + bundle_path = os.path.join(_get_bundle_root(), str(generated_guid), INI_FILENAME) + if os.path.isfile(bundle_path): + self.Path = os.path.dirname(bundle_path) + else: + raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid) + else: + raise IOError(2,"Unable to locate metadata file for guid '%s'" % generated_guid) + + else: + #Create the folder, any failure will go through to the caller for now + store_path = os.path.join(_get_store_root(), self.Guid) + os.makedirs(store_path) + self.Path = store_path + + def write_metadata_file(self, tutorial): + """ + Write metadata to the property file. + @param tutorial Tutorial for which to write metadata + """ + #Create the Config Object and populate it + cfg = SafeConfigParser() + cfg.add_section(INI_METADATA_SECTION) + cfg.set(INI_METADATA_SECTION, INI_GUID_PROPERTY, self.Guid) + cfg.set(INI_METADATA_SECTION, INI_NAME_PROPERTY, tutorial.name) + cfg.set(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY, TUTORIAL_FILENAME) + cfg.add_section(INI_ACTIVITY_SECTION) + if os.environ['SUGAR_BUNDLE_NAME'] != None and os.environ['SUGAR_BUNDLE_VERSION'] != None: + cfg.set(INI_ACTIVITY_SECTION, os.environ['SUGAR_BUNDLE_NAME'], + os.environ['SUGAR_BUNDLE_VERSION']) + else: + cfg.set(INI_ACTIVITY_SECTION, 'not_an_activity', '0') + + #Write the ini file + cfg.write( file( os.path.join(self.Path, INI_FILENAME), 'w' ) ) + + + @staticmethod + def get_tutorial_path(guid): + """ + Finds the tutorial with the associated GUID. If it is found, return + the path to the tutorial's directory. If it doesn't exist, raise an + IOError. + + A note : if there are two tutorials with this GUID in the folders, + they will both be inspected and the one with the highest version + number will be returned. If they have the same version number, the one + from the global store will be returned. + + @param guid The GUID of the tutorial that is to be loaded. + """ + # Attempt to find the tutorial's directory in the global directory + global_dir = os.path.join(_get_store_root(),str(guid)) + # Then in the activty's bundle path + if _get_bundle_root() != None: + activity_dir = os.path.join(_get_bundle_root(), str(guid)) + else: + activity_dir = '' + + # If they both exist + if os.path.isdir(global_dir) and os.path.isdir(activity_dir): + # Inspect both metadata files + global_meta = os.path.join(global_dir, "meta.ini") + activity_meta = os.path.join(activity_dir, "meta.ini") + + # Open both config files + global_parser = SafeConfigParser() + global_parser.read(global_meta) + + activity_parser = SafeConfigParser() + activity_parser.read(activity_meta) + + # Get the version number for each tutorial + global_version = global_parser.get(INI_METADATA_SECTION, "version") + activity_version = activity_parser.get(INI_METADATA_SECTION, "version") + + # If the global version is higher or equal, we'll take it + if global_version >= activity_version: + return global_dir + else: + return activity_dir + + # Do we just have the global directory? + if os.path.isdir(global_dir): + return global_dir + + # Or just the activity's bundle directory? + if os.path.isdir(activity_dir): + return activity_dir + + # 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 write_fsm(self, fsm): + + """ + Save fsm to disk. If a GUID parameter is provided, the existing GUID is + located in the .ini files in the store root and bundle root and + the corresponding FSM is/are created or overwritten. If the GUID is not + found, an exception occur. + """ + + config = SafeConfigParser() + + serializer = XMLSerializer() + path = os.path.join(self.Path, "meta.ini") + config.read(path) + xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) + serializer.save_tutorial(fsm, xml_filename, self.Path) + |