# =========================================================================== # eXe # Copyright 2004-2005, University of Auckland # Copyright 2004-2008 eXe Project, http://eXeLearning.org/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # =========================================================================== """ Exports an eXe package as a SCORM package """ import logging import re import time from cgi import escape from zipfile import ZipFile, ZIP_DEFLATED from exe.webui import common from exe.webui.blockfactory import g_blockFactory from exe.engine.error import Error from exe.engine.path import Path, TempDirPath from exe.export.pages import Page, uniquifyNames from exe.engine.uniqueidgenerator import UniqueIdGenerator log = logging.getLogger(__name__) # =========================================================================== class Manifest(object): """ Represents an imsmanifest xml file """ def __init__(self, config, outputDir, package, pages, scormType): """ Initialize 'outputDir' is the directory that we read the html from and also output the mainfest.xml """ self.config = config self.outputDir = outputDir self.package = package self.idGenerator = UniqueIdGenerator(package.name, config.exePath) self.pages = pages self.itemStr = "" self.resStr = "" self.scormType = scormType self.dependencies = {} def createMetaData(self, template): """ if user did not supply metadata title, description or creator then use package title, description, or creator in imslrm if they did not supply a package title, use the package name if they did not supply a date, use today """ lrm = self.package.dublinCore.__dict__.copy() if lrm.get('title', '') == '': lrm['title'] = self.package.title if lrm['title'] == '': lrm['title'] = self.package.name if lrm.get('description', '') == '': lrm['description'] = self.package.description if lrm['description'] == '': lrm['description'] = self.package.name if lrm.get('creator', '') == '': lrm['creator'] = self.package.author if lrm['date'] == '': lrm['date'] = time.strftime('%Y-%m-%d') # if they don't look like VCARD entries, coerce to fn: for f in ('creator', 'publisher', 'contributors'): if re.match('.*[:;]', lrm[f]) == None: lrm[f] = u'FN:' + lrm[f] xml = template % lrm return xml def save(self, filename): """ Save a imsmanifest file to self.outputDir """ out = open(self.outputDir/filename, "w") if filename == "imsmanifest.xml": out.write(self.createXML().encode('utf8')) out.close() if self.scormType == "scorm1.2": templateFilename = self.config.xulDir/'templates'/'imslrm.xml' template = open(templateFilename, 'rb').read() xml = self.createMetaData(template) out = open(self.outputDir/'imslrm.xml', 'wb') out.write(xml.encode('utf8')) out.close() def createXML(self): """ returning XLM string for manifest file """ manifestId = unicode(self.idGenerator.generate()) orgId = unicode(self.idGenerator.generate()) # Add the namespaces if self.scormType == "scorm1.2": xmlStr = u'\n' xmlStr += u'\n' xmlStr += u' \n" xmlStr += u" \n" xmlStr += u" ADL SCORM \n" xmlStr += u" 1.2 \n" xmlStr += u" imslrm.xml" xmlStr += u" \n" xmlStr += u" \n" elif self.scormType == "scorm2004": xmlStr = u'\n' xmlStr += u'\n' xmlStr += u' \n" elif self.scormType == "commoncartridge": xmlStr = u''' \n''' % manifestId templateFilename = self.config.xulDir/'templates'/'cc.xml' template = open(templateFilename, 'rb').read() xmlStr += self.createMetaData(template) # Metadata if self.scormType == "commoncartridge": xmlStr += u''' \n''' % (orgId, unicode(self.idGenerator.generate())) else: xmlStr += u" \n" xmlStr += u' \n' % orgId if self.package.title != '': title = escape(self.package.title) else: title = escape(self.package.root.titleShort) xmlStr += u""+title+"\n" if self.scormType == "commoncartridge": # FIXME flatten hierarchy for page in self.pages: self.genItemResStr(page) self.itemStr += "\n" else: depth = 0 for page in self.pages: while depth >= page.depth: self.itemStr += "\n" depth -= 1 self.genItemResStr(page) depth = page.depth while depth >= 1: self.itemStr += "\n" depth -= 1 xmlStr += self.itemStr if self.scormType == "commoncartridge": xmlStr += " \n" xmlStr += " \n" xmlStr += "\n" xmlStr += "\n" xmlStr += self.resStr xmlStr += "\n" xmlStr += "\n" return xmlStr def genItemResStr(self, page): """ Returning xml string for items and resources """ itemId = "ITEM-"+unicode(self.idGenerator.generate()) resId = "RES-"+unicode(self.idGenerator.generate()) filename = page.name+".html" self.itemStr += '\n' self.itemStr += " " self.itemStr += escape(page.node.titleShort) self.itemStr += "\n" self.resStr += " """ % (filename, filename) if page.node.package.backgroundImg: self.resStr += '\n ' % \ page.node.package.backgroundImg.basename() self.dependencies["base.css"] = True self.dependencies["content.css"] = True self.dependencies["popup_bg.gif"] = True else: self.resStr += "adlcp:scormtype=\"sco\" " self.resStr += "href=\""+filename+"\"> \n" self.resStr += """\ """ % filename self.resStr += "\n" fileStr = "" for resource in page.node.getResources(): fileStr += " \n" self.dependencies[resource] = True self.resStr += fileStr self.resStr += " \n" # =========================================================================== class ScormPage(Page): """ This class transforms an eXe node into a SCO """ def __init__(self, name, depth, node, scormType="scorm1.2"): self.scormType = scormType super(ScormPage, self).__init__(name, depth, node) def save(self, outputDir): """ This is the main function. It will render the page and save it to a file. 'outputDir' is the name of the directory where the node will be saved to, the filename will be the 'self.node.id'.html or 'index.html' if self.node is the root node. 'outputDir' must be a 'Path' instance """ out = open(outputDir/self.name+".html", "w") out.write(self.render()) out.close() def render(self): """ Returns an XHTML string rendering this page. """ html = common.docType() html += u"\n" html += u"\n" html += u""+_("eXe")+"\n" html += u"\n"; html += u"\n" html += u"\n" html += u'\n' html += u"\n" if self.scormType == 'commoncartridge': html += u"" else: html += u"\n" html += u"\n" html += u'' html += u"
\n" html += u"
\n" html += u"
\n" html += u"

\n" html += escape(self.node.titleLong) html += u'

\n' for idevice in self.node.idevices: html += u'
\n' % (idevice.klass, idevice.id) block = g_blockFactory.createBlock(None, idevice) if not block: log.critical("Unable to render iDevice.") raise Error("Unable to render iDevice.") if hasattr(idevice, "isQuiz"): html += block.renderJavascriptForScorm() html += self.processInternalLinks( block.renderView(self.node.package.style)) html += u'
\n' # iDevice div html += u"
\n" html += u"
\n" if self.node.package.scolinks: html += u'
' html += u'%s | %s' % _('Next') html += u'
' html += self.renderLicense() html += self.renderFooter() html += u"\n" html = html.encode('utf8') return html def processInternalLinks(self, html): """ take care of any internal links which are in the form of: href="exe-node:Home:Topic:etc#Anchor" For this SCORM Export, go ahead and remove the link entirely, leaving only its text, since such links are not to be in the LMS. """ return common.removeInternalLinks(html) # =========================================================================== class ScormExport(object): """ Exports an eXe package as a SCORM package """ def __init__(self, config, styleDir, filename, scormType): """ Initialize 'styleDir' is the directory from which we will copy our style sheets (and some gifs) """ self.config = config self.imagesDir = config.webDir/"images" self.scriptsDir = config.webDir/"scripts" self.templatesDir = config.webDir/"templates" self.schemasDir = config.webDir/"schemas" self.styleDir = Path(styleDir) self.filename = Path(filename) self.pages = [] self.hasForum = False self.scormType = scormType def export(self, package): """ Export SCORM package """ # First do the export to a temporary directory outputDir = TempDirPath() # Export the package content self.pages = [ ScormPage("index", 1, package.root, scormType=self.scormType) ] self.generatePages(package.root, 2) uniquifyNames(self.pages) for page in self.pages: page.save(outputDir) if not self.hasForum: for idevice in page.node.idevices: if hasattr(idevice, "isForum"): if idevice.forum.lms.lms == "moodle": self.hasForum = True break # Create the manifest file manifest = Manifest(self.config, outputDir, package, self.pages, self.scormType) manifest.save("imsmanifest.xml") if self.hasForum: manifest.save("discussionforum.xml") # Copy the style sheet files to the output dir # But not nav.css styleFiles = [self.styleDir/'..'/'base.css'] styleFiles += [self.styleDir/'..'/'popup_bg.gif'] styleFiles += [f for f in self.styleDir.files("*.css") if f.basename() <> "nav.css"] styleFiles += self.styleDir.files("*.jpg") styleFiles += self.styleDir.files("*.gif") styleFiles += self.styleDir.files("*.png") styleFiles += self.styleDir.files("*.js") styleFiles += self.styleDir.files("*.html") # FIXME for now, only copy files referenced in Common Cartridge # this really should apply to all exports, but without a manifest # of the files needed by an included stylesheet it is too restrictive if self.scormType == "commoncartridge": for sf in styleFiles[:]: if sf.basename() not in manifest.dependencies: styleFiles.remove(sf) self.styleDir.copylist(styleFiles, outputDir) # copy the package's resource files package.resourceDir.copyfiles(outputDir) # Copy the scripts if self.scormType == "commoncartridge": self.scriptsDir.copylist(('libot_drag.js', 'common.js'), outputDir) else: self.scriptsDir.copylist(('APIWrapper.js', 'SCOFunctions.js', 'libot_drag.js', 'common.js'), outputDir) schemasDir = "" if self.scormType == "scorm1.2": schemasDir = self.schemasDir/"scorm1.2" schemasDir.copylist(('imscp_rootv1p1p2.xsd', 'imsmd_rootv1p2p1.xsd', 'adlcp_rootv1p2.xsd', 'ims_xml.xsd'), outputDir) elif self.scormType == "scorm2004": schemasDir = self.schemasDir/"scorm2004" schemasDir.copylist(('imscp_rootv1p1p2.xsd', 'imsmd_rootv1p2p1.xsd', 'adlcp_rootv1p2.xsd', 'ims_xml.xsd'), outputDir) # copy players for media idevices. hasFlowplayer = False hasMagnifier = False hasXspfplayer = False isBreak = False for page in self.pages: if isBreak: break for idevice in page.node.idevices: if (hasFlowplayer and hasMagnifier and hasXspfplayer): isBreak = True break if not hasFlowplayer: if 'flowPlayer.swf' in idevice.systemResources: hasFlowplayer = True if not hasMagnifier: if 'magnifier.swf' in idevice.systemResources: hasMagnifier = True if not hasXspfplayer: if 'xspf_player.swf' in idevice.systemResources: hasXspfplayer = True if hasFlowplayer: videofile = (self.templatesDir/'flowPlayer.swf') videofile.copyfile(outputDir/'flowPlayer.swf') if hasMagnifier: videofile = (self.templatesDir/'magnifier.swf') videofile.copyfile(outputDir/'magnifier.swf') if hasXspfplayer: videofile = (self.templatesDir/'xspf_player.swf') videofile.copyfile(outputDir/'xspf_player.swf') if self.scormType == "scorm1.2" or self.scormType == "scorm2004": if package.license == "GNU Free Documentation License": # include a copy of the GNU Free Documentation Licence (self.templatesDir/'fdl.html').copyfile(outputDir/'fdl.html') # Zip it up! self.filename.safeSave(self.doZip, _('EXPORT FAILED!\nLast succesful export is %s.'), outputDir) # Clean up the temporary dir outputDir.rmtree() def doZip(self, fileObj, outputDir): """ Actually does the zipping of the file. Called by 'Path.safeSave' """ # Zip up the scorm package zipped = ZipFile(fileObj, "w") for scormFile in outputDir.files(): zipped.write(scormFile, scormFile.basename().encode('utf8'), ZIP_DEFLATED) zipped.close() def generatePages(self, node, depth): """ Recursive function for exporting a node. 'node' is the node that we are making a page for 'depth' is the number of ancestors that the page has +1 (ie. root is 1) """ for child in node.children: pageName = child.titleShort.lower().replace(" ", "_") pageName = re.sub(r"\W", "", pageName) if not pageName: pageName = "__" page = ScormPage(pageName, depth, child, scormType=self.scormType) self.pages.append(page) self.generatePages(child, depth + 1) # ===========================================================================