# 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 .core import Tutorial, State, FiniteStateMachine 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" NODE_COMPONENT = "Component" NODE_SUBCOMPONENT = "property" NODE_SUBCOMPONENTLIST = "listproperty" NEXT_STATE_ATTR = "next_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 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. @returns an executable representation of a tutorial """ 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) fsm = serializer.load_fsm(Guid, bundle_path) tuto = Tutorial(name, fsm) return tuto @staticmethod def saveTutorial(tutorial, metadata_dict): """ Creates a persistent version of a tutorial in the Vault. @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) xml_ser.save_fsm(tutorial.state_machine, TUTORIAL_FILENAME, tutorial_path) # 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): """ 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 class Serializer(object): """ Interface that provide serializing and deserializing of the FSM used in the tutorials to/from disk. Must be inherited. """ def save_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 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_fsm(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 """ def _create_state_dict_node(self, state_dict, doc): """ Create and return a xml Node from a State dictionnary. """ statesList = doc.createElement("States") for state_name, state in state_dict.items(): stateNode = doc.createElement("State") statesList.appendChild(stateNode) stateNode.setAttribute("Name", state_name) actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc)) eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc)) return statesList def _create_addon_component_node(self, parent_attr_name, comp, doc): """ Takes a component that is embedded in another component (e.g. the content of a OnceWrapper) and encapsulate it in a node with the property name. e.g. When reloading this node, we should look up the property name for the parent in the attribute of the node, then examine the subnode to create the addon object itself. @param parent_attr_name The name of the parent's attribute for this addon e.g. the OnceWrapper has the action attribute, which corresponds to a sub-action it must execute once. @param comp The component node itself @param doc The XML document root (only used to create the nodes) @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node that represents another component. """ subCompNode = doc.createElement(NODE_SUBCOMPONENT) subCompNode.setAttribute("name", parent_attr_name) subNode = self._create_component_node(comp, doc) subCompNode.appendChild(subNode) return subCompNode def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc): """ Takes a list of components that are embedded in another component (ex. the content of a ChainAction) and encapsulate them in a node with the property name. e.g. When reloading this node, we should look up the property name for the parent in the the attribute of the node, then rebuild the list by appending the content of all the subnodes. @param parent_attr_name The name of the parent component's property @param comp_list A list of components that comprise the property @param doc The XML document root (only for creating new nodes) @returns A NODE_SUBCOMPONENTLIST node with the property attribute """ subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST) subCompListNode.setAttribute("name", parent_attr_name) for comp in comp_list: compNode = self._create_component_node(comp, doc) subCompListNode.appendChild(compNode) return subCompListNode def _create_component_node(self, comp, doc): """ Takes a single component (action or eventfilter) and transforms it 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(self._create_addonlist_component_node(propname, propval, doc)) elif getattr(type(comp), propname).type == "addon": #import rpdb2; rpdb2.start_embedded_debugger('pass') compNode.appendChild(self._create_addon_component_node(propname, propval, doc)) else: # repr instead of str, as we want to be able to eval() it into a # valid object. compNode.setAttribute(propname, repr(propval)) return compNode def _create_action_list_node(self, action_list, doc): """ Create and return a xml Node from a Action list. @param action_list A list of actions @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("Actions") for action in action_list: # Create the action node actionNode = self._create_component_node(action, doc) # Append it to the list actionsList.appendChild(actionNode) return actionsList def _create_event_filters_node(self, event_filters, doc): """ Create and return a xml Node from an event filters. """ eventFiltersList = doc.createElement("EventFiltersList") for event, state in event_filters: eventFilterNode = self._create_component_node(event, doc) eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state)) eventFiltersList.appendChild(eventFilterNode) return eventFiltersList def save_fsm(self, fsm, xml_filename, path): """ Save fsm to disk, in the xml file specified by "xml_filename", in the "path" folder. If the specified file doesn't exist, it will be created. """ self.doc = doc = xml.dom.minidom.Document() fsm_element = doc.createElement("FSM") doc.appendChild(fsm_element) fsm_element.setAttribute("Name", fsm.name) fsm_element.setAttribute("StartStateName", fsm.start_state_name) statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc)) fsm_actions_node = self._create_action_list_node(fsm.actions, doc) fsm_actions_node.tagName = "FSMActions" actionsList = fsm_element.appendChild(fsm_actions_node) file_object = open(os.path.join(path, xml_filename), "w") file_object.write(doc.toprettyxml()) file_object.close() def _get_direct_descendants_by_tag_name(self, node, name): """ Searches in the list of direct descendants of a node to find all the node that have the given name. This is used because the Document.getElementsByTagName() function returns the list of all the descendants (whatever their distance to the start node) that have that name. In the case of complex components, we absolutely need to inspect a single layer of the tree at the time. @param node The node from which we want the direct descendants with a particular name @param name The name of the node @returns A list, possibly empty, of direct descendants of node that have this name """ return_list = [] for childNode in node.childNodes: if childNode.nodeName == name: return_list.append(childNode) return return_list ## def _load_xml_properties(self, properties_elem): ## """ ## Changes a list of properties into fully instanciated properties. ## ## @param properties_elem An XML element reprensenting a list of ## properties ## """ ## return [] def _load_xml_event_filters(self, filters_elem): """ Loads up a list of Event Filters. @param filters_elem An XML Element representing a list of event filters """ transition_list = [] event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) new_event_filter = None for event_filter in event_filter_element_list: next_state = event_filter.getAttribute(NEXT_STATE_ATTR) try: event_filter.removeAttribute(NEXT_STATE_ATTR) except NotFoundErr: next_state = None new_event_filter = self._load_xml_component(event_filter) if new_event_filter is not None: transition_list.append((new_event_filter, next_state)) return transition_list def _load_xml_subcomponents(self, node, properties): """ Loads all the subcomponent node below the given node and inserts them with the right property name inside the properties dictionnary. @param node The parent node that contains one or many property nodes. @param properties A dictionnary where the subcomponent property names and the instantiated components will be stored @returns Nothing. The properties dict will contain the property->comp mapping. """ subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) for subComp in subCompList: property_name = subComp.getAttribute("name") internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] internal_comp = self._load_xml_component(internal_comp_node) properties[str(property_name)] = internal_comp def _load_xml_subcomponent_lists(self, node, properties): """ Loads all the subcomponent lists below the given node and stores them under the correct property name for that node. @param node The node from which we want to read the subComponent lists @param properties The dictionnary that will contain the mapping of prop->subCompList @returns Nothing. The values are returns inside the properties dict. """ listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) for subCompListNode in listOf_subCompListNode: property_name = subCompListNode.getAttribute("name") subCompList = [] for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): subComp = self._load_xml_component(subCompNode) subCompList.append(subComp) properties[str(property_name)] = subCompList def _load_xml_component(self, node): """ Loads a single addon component instance from an Xml node. @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 self._load_xml_subcomponents(node, properties) self._load_xml_subcomponent_lists(node, properties) new_action = addon.create(class_name, **properties) if not new_action: return None return new_action def _load_xml_actions(self, actions_elem): """ Transforms an Actions element into a list of instanciated Action. @param actions_elem An XML Element representing a list of Actions """ reformed_actions_list = [] actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) for action in actions_element_list: new_action = self._load_xml_component(action) reformed_actions_list.append(new_action) return reformed_actions_list def _load_xml_states(self, 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 """ reformed_state_list = [] # 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("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 = self._load_xml_actions(state.getElementsByTagName("Actions")[0]) event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0]) reformed_state_list.append(State(stateName, actions_list, event_filters_list)) return reformed_state_list def load_xml_fsm(self, 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 """ # Load the FSM's name and start state's name fsm_name = fsm_elem.getAttribute("Name") fsm_start_state_name = None try: fsm_start_state_name = fsm_elem.getAttribute("StartStateName") except: pass fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name) # Load the states states = self._load_xml_states(fsm_elem.getElementsByTagName("States")) for state in states: fsm.add_state(state) # Load the actions on this FSM actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0]) for action in actions: fsm.add_action(action) # Load the event filters events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0]) for event, next_state in events: fsm.add_event_filter(event, next_state) return fsm def load_fsm(self, guid, path=None): """ Load fsm from xml file whose .ini file guid match argument guid. """ # Fetch the directory (if any) bundler = TutorialBundler(guid) tutorial_dir = bundler.get_tutorial_path(guid) # Open the XML file tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME) xml_dom = xml.dom.minidom.parse(tutorial_file) fsm_elem = xml_dom.getElementsByTagName("FSM")[0] return self.load_xml_fsm(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_fsm(fsm, xml_filename, self.Path) @staticmethod def add_resources(typename, file): """ Add ressources to metadata. """ raise NotImplementedError("add_resources not implemented")