From 926238a2c54daae80d4c561b4cda8546d40173a7 Mon Sep 17 00:00:00 2001 From: JCTutorius Date: Wed, 21 Oct 2009 05:06:48 +0000 Subject: vault merge --- (limited to 'tutorius/vault.py') diff --git a/tutorius/vault.py b/tutorius/vault.py new file mode 100644 index 0000000..9215e8d --- /dev/null +++ b/tutorius/vault.py @@ -0,0 +1,861 @@ +# 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 sugar.tutorius import addon +from sugar.tutorius.core import Tutorial, State, FiniteStateMachine +from ConfigParser import SafeConfigParser + +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. Ifactivity_ame 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) + + + @staticmethod + def write_fsm(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") -- cgit v0.9.1