# Copyright (C) 2009, Tutorius.org # Copyright (C) 2009, Jean-Christophe Savard # # 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" RESOURCES_FOLDER = 'resources' ###################################################################### # 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__" 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 tutorial 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(Guid): """ 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(Guid) bundle_path = bundle.get_tutorial_path(Guid) # 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): """ Add given resource file in the vault and 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 ). @param tutorial_guid The guid of the tutorial @param file_path the file path of the resource to add @returns the resource_id of the resource """ assert os.path.isfile(file_path) # Get the tutorial path bundler = TutorialBundler(tutorial_guid) tutorial_path = bundler.get_tutorial_path(tutorial_guid) # Get the file name file_name = os.path.basename(file_path) #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 resource file already exists new_file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, file_name_appended) if os.path.isfile(new_file_path) == False: # Copy the resource file in the vault if os.path.isdir(os.path.join(tutorial_path, RESOURCES_FOLDER)) == False: os.makedirs(os.path.join(tutorial_path, RESOURCES_FOLDER)) shutil.copyfile(file_path, new_file_path) return file_name_appended @staticmethod def delete_resource(tutorial_guid, resource_id): """ Delete the resource from the resources of the tutorial. @param tutorial_guid the guid of the tutorial @param resource_id the resource id of the resource to delete """ # Get the tutorial path bundler = TutorialBundler(tutorial_guid) 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) if os.path.isfile(file_path): # Delete the resource os.remove(file_path) else: logging.info('File not found, no delete took place') @staticmethod def get_resource_path(tutorial_guid, resource_id): """ Returns the absolute file path to the resourceID @param tutorial_guid the guid of the tutorial @param resource_id the resource id of the resource to find the path for @returns the absolute path of the resource file """ # Get the tutorial path bundler = TutorialBundler(tutorial_guid) 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) 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. 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. 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 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) @staticmethod def add_resources(typename, file): """ Add resources to metadata. """ raise NotImplementedError("add_resources not implemented")