Skip to content
Browse files

Initial Commit

  • Loading branch information...
0 parents commit d73d31350c20fbc5f1c90f25bd3a8fac4cdf27eb @bracken bracken committed Jan 22, 2010
BIN IMSLogo.bmp
Binary file not shown.
29 LICENSE.txt
@@ -0,0 +1,29 @@
+Copyright (c) 2004-2008, University of Cambridge.
+GUI Code Copyright (c) 2004-2008, Pierre Gorissen
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ''AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
BIN QTIMigrate.ico
Binary file not shown.
10 QTIMigrationTool.iml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module relativePaths="true" type="PYTHON_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="jdk" jdkName="Python 2.6.4" jdkType="Python SDK" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
+
443 README.txt
@@ -0,0 +1,443 @@
+QTI Migration Tool
+------------------
+
+This is the QTI migration tool, use is governed by the license at the bottom of
+this file.
+
+For up to date information about the migration tool and related initiatives go
+to http://qtitools.caret.cam.ac.uk/ - Please direct any comments to
+swl10@cam.ac.uk
+
+
+Installation
+------------
+
+The migration tool can work either in batch mode activated from the command line
+or using a graphical user interface (GUI).
+
+* Windows
+
+Simply download the installer program and run it.
+
+* Mac OS X
+
+Simply download the application disk image. To install the application just
+drag it from the mounted disk image to the Applications folder.
+
+* Unix/Linux
+
+Follow the source release installation instructions below.
+
+* Installing the Source
+
+To install the source distribution you will need a version 2 python interpreter
+to be installed on your system. Python is available as a simple installer for
+most popular platforms, for more details see:
+
+http://www.python.org/
+
+The GUI mode requires wxPython to be installed. wxPython is available for
+Windows, Mac and Unix/Linux based systems. The installation is straightforward
+on most modern versions of these operating systems. For details of how to
+download and install it for your system see:
+
+http://www.wxpython.org/
+
+Finally, if you install the vobject package into your Python interpreter the
+migration tool can use it to enhance the --qmdextensions option. This tool
+has been tested against version 0.6.0 of vobject, for more details, see:
+
+http://vobject.skyhouseconsulting.com/
+
+Once you have installed Python (and optionally, wxPython and vobject) you should
+simply unpack the source distribution into the desired location in the file
+system.
+
+
+Running
+-------
+
+On some systems you may be able to open the migration tool directly from your
+GUI just by double-clicking the "migrate.py" file. However, the migration tool
+can always be run from the command line. On Windows machines you must use the
+*DOS* command line, on MacOS X you should use the terminal. Windows users are
+advised to familiarize themselves with the instructions for running python
+scripts from the DOS command line:
+
+http://www.python.org/doc/faq/windows/#how-do-i-run-a-python-program-under-windows
+
+To run the tool you must change to the directory in which the tool resides and
+then run the migrate.py script.
+
+By default, the tool launches in GUI mode if it can. If you don't have wxPython
+installed, or you use the --nogui option, the tool uses batch mode instead. When
+running in GUI mode command line options can be used to set the initial states
+of the corresponding controls.
+
+* Batch mode
+
+To force batch mode, pass --nogui an argument to the migrate.py command.
+
+You pass the paths of the QTI version 1 files to process as arguments. If you
+pass the name of a directory then it is scanned recursively for QTI xml files
+(which are assumed to be all files with the extension ".xml").
+
+The tool writes some messages to the standard output, mainly to report on
+progress and to flag unsupported conversion features.
+
+By default, the migration tool just examines the version 1 items for problems
+without generating an output content package. To actually convert the items you
+will need to pass it the path of a directory in which to write its output files,
+this is passed as a special path prefixed with the string --cpout= (see example
+below).
+
+migrate.py --nogui MyQTIv1Items.xml --cpout=Package
+
+This example uses batch mode to convert all the items in the XML file
+MyQTIv1Items.xml and places them in a directory called Package. In QTI version
+1, a single XML document could contain many items. In version 2.0 multiple
+items are grouped together using an IMS Content Package instead. The migration
+tool generates an appropriate manifest file automatically and places it in the
+output directory.
+
+* GUI Mode
+
+The GUI allows you to select a "Single QTIv1.2 File" or a "Full Directory" to
+process. To generate a content package you must select a directory using the
+control in the "Save files to..." box.
+
+To convert the files use the "Convert..." button.
+
+You can control the destination of the status messages and error reports by
+toggling the "Show Output Log" menu item in the "File" menu.
+
+* Options
+
+You can use options to tweak the behaviour of the migration tool. Options can
+be specified on the command line. In GUI mode they are available from the
+Options menu. A full list is given below.
+
+Once you have got a basic migration of your content, you might like to use the
+--qmdextensions and --ucvars options as these usually improve the resulting
+output at the risk of deviating slightly from the proper interpretatin of the
+original (version 1.x) QTI specification. Similarly, use of --lang is highly
+recommended for improving the quality of the resulting metadata.
+
+
+Troubleshooting
+---------------
+
+I've tried to make the migration tool as tolerant as possible so that it
+produces some output for all items, even if the migration is incomplete.
+However, sometimes the tool refuses to parse the input files completely and
+just stops. Have a look at the section on problems with graphical characters
+(below) and do check that your input files are at least well formed XML files.
+
+Check the comments that the tool writes at the top of each version 2 file it
+generates, they sometimes contain important information about decisions made
+during the migration.
+
+If you are dissatisfied with the result of migrating your QTI data or the tool
+exits with an error without generating any output *please* do send me a sample
+QTI (version 1.x) file demonstrating the problem so that I can use it to improve
+future versions. It is often possible to fix migration problems quickly and to
+incorporate changes for the next release so that everyone benefits.
+
+
+Migration Tool Options
+----------------------
+
+--cpout=<path to package directory>
+"Save files to... Select Directory" in the GUI
+
+Used to specify an output directory for the content package to be written to.
+Without this option the migration tool will only examine the version 1 files and
+report problems without generating a content package.
+
+--ucvars
+"Options: Force Uppercase" in the GUI
+
+Version 1 of the QTI specification was unclear on whether or not variable names
+would be treated case sensitively or not. From version 2.0 onwards all variable
+names are treated case sensitively. Some old content may assume that
+comparisons are case-insensitive. Watch out for errors reported in the output
+like "reference to undeclared outcome" - this may indicate a case-mismatch
+between references and declared variables. The tool auto-declares missing
+outcomes so these errors are not fatal but the resulting output won't work as
+expected! If you specify this option, the migration tool will force all
+identifiers to upper-case before processing them.
+
+--qmdextensions
+"Options: Allow Metadata Extensions" in the GUI
+
+Some metadata tags are in use that were not part of QTI version 1 itself. You
+can turn on support for these metadata extensions with this flag. Specifically,
+the following tags are then recognized:
+
+ <qmd_keywords>
+ <qmd_domain> (treated as a keyword)
+ <qmd_description>
+ <qmd_title>
+ <qmd_author> (requires vobject support, see above)
+ <qmd_organisation> (requires vobject support, see above)
+
+--lang=<language>
+"Options: Default Language" in the GUI
+
+This option allows you to provide a default language for items migrated by the
+tool. If there is no language specified for an item then it is treated as if
+<language> was specified instead. Note that this option also affects metadata
+which is read from the item tag itself in version 1 of QTI. In fact, one of its
+main purposes is to ensure that the metadata values are appropriately language
+tagged when migrating them into the manifest of the content package.
+
+--nocomment
+"Options: Suppress Comment" in the GUI
+
+The migration tool generates warning messages during conversion which it adds
+to the top of each output files in the form of an XML comment immediately
+following the XML declaration. This option suppresses the generation of these
+comments.
+
+--dtdloc=<path>
+"Options: Force DTD Location" in the GUI
+
+Files that contain a SYSTEM idenifier pointing at a non-existent file will cause
+the migration tool to fail. The only ways to overcome this are (a) to put a
+copy of the required DTD in the specified location, (b) remove the SYSTEM
+identifier from the DOCTYPE declaration at the top of the file or (c) use the
+--dtdloc option to tell the migration tool the *directory* where it can find
+the DTD.
+
+--forcefibfloat
+"Options: Force Float" in the GUI
+
+Some implementations blurred the distinction between string and numeric
+variables. As a result, some content creates fill-in-the-blank or 'fib'
+questions that obtain string type values from the user that are then treated as
+numbers during response processing. There is no general fix for this in the
+migration tool but the --forcefibfloat option forces all fibs to be treated as
+if they had been declared as floats instead. Only use this option if you know
+that your content suffers from this problem.
+
+--nogui
+
+As of version 20080610 the migration tool will launch in GUI mode if wxPython is
+installed. You can force the tool to run in batch mode with this option.
+
+--help
+
+Print a help message (implies --nogui)
+
+--version
+
+Print version information (implies --nogui)
+
+
+Graphic Character Problems: fixwinchars.py
+------------------------------------------
+
+Sometimes, the migration tool fails with an "Invalid Token" message, generated
+by the XML parser. These messages might be caused by the additional graphic
+characters often used by Windows. These characters, including 'smart' quotes,
+are not portable between systems and cannot be included in XML files without an
+appropriate XML declaration. The source release contains a simple script to
+help solve this problem. You pass it the name of a file (or files) and it will
+examine them for special graphical characters. If it finds some, it will
+replace them with XML character entity references to the appropriate Unicode
+characters.
+
+BE CAREFUL: files are changed in place so only ever run this script on a copy of
+your files! For safety, fixwinchars will only process files that start with a
+'<' character.
+
+fixwinchars.py MyWindowsFile.xml
+
+The output of the script might look like something this:
+
+Fixing 2 chars in file: MyWindowsFile.xml
+chr(0x96) -> &#x2013;
+chr(0x96) -> &#x2013;
+
+If there is nothing to be done, no output is generated.
+
+You can force all non-ascii characters to be encoded by passing the --ascii
+option, you should use this option if your file is missing the XML declaration
+all together (or has been incorrectly labelled as being UTF-8):
+
+fixwinchars.py --ascii MyWindowsFile.xml
+
+
+Building the Binary Executables
+-------------------------------
+
+The source release contains all the files necessary to build the binary
+executable files.
+
+For Windows, you will need py2exe. The compile.bat file contains the command
+required to trigger the build given a typical Python 2.5 installation:
+
+python setup_migrate.py py2exe --packages encodings
+
+The executable files are created in the dist directory. These can be packaged into
+an installer using the Inno Setup tool, a suitable installation script is provided in
+migrate_w32.iss.
+
+For Mac OS, you will need to install py2app and setuptools. You can then build
+the binary application using the command:
+
+python setup.py py2app
+
+To build the binary releases you *must* have VObject installed.
+
+python: http://www.python.org/download/
+py2exe: http://www.py2exe.org/
+Inno Setup: http://www.jrsoftware.org/isinfo.php
+VObject: http://vobject.skyhouseconsulting.com/
+
+All needed tools are available for free download.
+
+Change Log
+----------
+
+
+Version: 20080612
+
+New features:
+
+* Integrated Pierre Gorissen's GUI interface code into the source release to
+speed up production of Windows (and now Mac) binary distributions.
+
+* Added --nocomment option
+
+
+Version: 20080604
+
+Bug fix release to correct "Non-ASCII character" problem.
+
+
+Version: 20080602
+
+New features:
+
+* Added rudimentary (but effective) support for the parsing of embedded RTF
+ code in mattext
+
+* Significantly improved parsing of embedded HTML code in mattext, including
+support for HTML code with missing CDATA sections delimitters, better code to
+handle omitted tags and a wider range of supported tags (including basic
+tables and img elements in mattext).
+
+* Added support for copying media files into the content package, previously
+ this had to be done by hand after the migration.
+
+* Added automated generation of QTI specific metadata in the manifest and added
+support for <qtimetadata> and the following legacy tags:
+ <qmd_itemtype>
+ <qmd_levelofdifficulty>
+ <qmd_maximumscore>
+ <qmd_status>
+ <qmd_toolvendor>
+ <qmd_topic>
+
+* Improved generated XML to ensure validation against latest IMS schemas.
+
+* Relaxed various validation issues to generate warnings instead of terminating
+the migration process.
+
+* Reduced verbosity when skipping unsupported tags
+
+* Added --ucvars option
+
+* Added --qmdextensions option
+
+* Added --forcefibfloag option
+
+* Added --lang option
+
+* Added --dtdloc option
+
+* Bundled the related fixwinchars script with this release
+
+Bugs fixed:
+
+* Fixed a bug that allowed invalid identifiers to be migrated unchanged.
+
+* Fixed a bug which occasionally caused truncation of content when processing
+embedded formatting tags.
+
+* Fixed a bug caused by the use of language tags on the top level questestinterp.
+
+* Fixed a bug caused by the use of multiple response processing sections in a
+single item
+
+
+Version: 20070424
+
+Changed the errors trapped by the XML parser to prevent the tool exiting out
+when faced with badly formed input files during a batch run.
+
+
+Version: 20060915
+
+Added support for the unanswered tag to accompany my XSLT pre-processor for
+converting content from the QAed authoring tool.
+
+Changed the QTI schema location hint in the output to point to the version of
+the schema published on the IMS website.
+
+
+Version: 20050610
+
+This version of the tool represents an improvement on the earlier version
+distributed as a Windows executable through SURF SiX. Though the modifications
+are fairly minor.
+
+This version is functionally identical to the one that was used at the
+CETIS/LIFE project CodeBash event in Bolton, April 2005.
+
+
+Acknowledgements
+----------------
+
+Thanks to Pierre Gorissen for providing the GUI code and the Windows binary
+executable and installer.
+
+Thanks to all those people who have offered me content to test the migration
+process on. In particular, thanks to all the attendees at the CETIS Code Bashes
+with a special thank you to Dick Bacon for loaning me the Physical Sciences item
+bank.
+
+
+License
+-------
+
+Copyright (c) 2004-2008, University of Cambridge.
+GUI Code Copyright (c) 2004-2008, Pierre Gorissen
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
50 checkgremlins.py
@@ -0,0 +1,50 @@
+#! /usr/bin/env python
+
+"""Copyright (c) 2008, University of Cambridge.
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
+
+import os, sys, string
+
+def CheckFile(fname):
+ f=file(fname,"rb")
+ data=f.read()
+ f.close()
+ pos=0
+ for c in data:
+ if ord(c)>=0x7F or (ord(c)<=0x1F and c not in "\r\n\t"):
+ print 'Found chr(0x%2X) at after:\n%s'%(ord(c),data[pos-80:pos])
+ pos+=1
+
+if __name__ == '__main__':
+ fileNames=[]
+ for x in sys.argv[1:]:
+ fileNames.append(x)
+ for f in fileNames:
+ print "Checking file: %s"%f
+ CheckFile(f)
1 compile.bat
@@ -0,0 +1 @@
+c:\python25\python setup.py py2exe --packages encodings
109 fixwinchars.py
@@ -0,0 +1,109 @@
+#! /usr/bin/env python
+
+"""Copyright (c) 2008, University of Cambridge.
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
+
+import os, sys, string
+
+CharFixMap={
+ 0x80:"&#x20AC;",
+ 0x82:"&#x201A;",
+ 0x83:"&#x192;",
+ 0x84:"&#x201E;",
+ 0x85:"&#x2026;",
+ 0x86:"&#x2020;",
+ 0x87:"&#x2021;",
+ 0x88:"&#x2C6;",
+ 0x89:"&#x2030;",
+ 0x8A:"&#x160;",
+ 0x8B:"&#x2039;",
+ 0x8C:"&#x152;",
+ 0x8E:"&#x17D;",
+ 0x91:"&#x2018;",
+ 0x92:"&#x2019;",
+ 0x93:"&#x201C;",
+ 0x94:"&#x201D;",
+ 0x95:"&#x2022;",
+ 0x96:"&#x2013;",
+ 0x97:"&#x2014;",
+ 0x98:"&#x2DC;",
+ 0x99:"&#x2122;",
+ 0x9A:"&#x161;",
+ 0x9B:"&#x203A;",
+ 0x9C:"&#x153;",
+ 0x9E:"&#x17E;",
+ 0x9F:"&#x178;",
+ }
+
+def FixFile(fname,asciiMode,forceMode=0):
+ f=file(fname,"rb")
+ header=f.read(4)
+ if len(header)<4:
+ # ignore very short files
+ return
+ if (ord(header[0]) in (0x00, 0xFE, 0xFF,0xEF)) or ord(header[1])==0:
+ # File probably starts with a BOM or is in a 16-bit wide format, so skip it.
+ return
+ if ord(header[0])!=0x3C and not forceMode:
+ return
+ data=header+f.read()
+ f.close()
+ fix=0
+ for c in data:
+ if ord(c)>=0x80 and ord(c)<=0x9F:
+ fix+=1
+ elif asciiMode and ord(c)>=0xA0:
+ fix+=1
+ if not fix:
+ return
+ print "Fixing %i chars in file: %s"%(fix,fname)
+ output=[]
+ for c in data:
+ cout=CharFixMap.get(ord(c),c)
+ if asciiMode and c==cout and ord(c)>=0x80:
+ cout="&#x%2X;"%ord(c)
+ output.append(cout)
+ if c!=cout:
+ print "chr(0x%2X) -> %s"%(ord(c),cout)
+ outStr=string.join(output,'')
+ f=file(fname,"wb")
+ f.write(outStr)
+ f.close()
+
+if __name__ == '__main__':
+ fileNames=[]
+ asciiMode=0
+ for x in sys.argv[1:]:
+ # check for options here
+ if x.lower()=="--ascii":
+ asciiMode=1
+ else:
+ fileNames.append(x)
+ for f in fileNames:
+ FixFile(f,asciiMode)
BIN folder.ico
Binary file not shown.
420 lib/gui.py
@@ -0,0 +1,420 @@
+#! /usr/bin/env python
+"""
+GUI Script Code Copyright (c) 2004 - 2008, Pierre Gorissen
+All Other Code is Copyright (c) 2004 - 2008, University of Cambridge.
+"""
+
+# Global options
+# --------------
+GUI_VERSION='Version: 2008-06-07'
+
+import sys, os, time, string
+import wx
+import wx.lib.filebrowsebutton as filebrowse
+
+import imsqtiv1
+
+
+# ==============================================================================
+# some global info
+#
+# file filter
+wildcard = "QTI files (*.xml)|*.xml"
+
+# global ID's
+ID_LOG = 100
+ID_ABOUT = 101
+ID_EXIT = 102
+ID_UCVARS = 103
+ID_QMDEXTENSIONS = 104
+ID_FORCEFIBFLOAT = 105
+ID_FORCEDTD = 106
+ID_FORCELANG = 107
+ID_NOCOMMENT = 108
+
+
+# ==============================================================================
+#
+# MyFrame Class
+#
+# Description: this class constructs the main window
+#
+class MyFrame(wx.Frame):
+
+ def __init__(self, parent, ID, title):
+ self.app=wx.GetApp()
+ self.options=self.app.options
+ wx.Frame.__init__(self, parent, ID, title,
+ wx.DefaultPosition, wx.Size(600, 300), style=wx.DEFAULT_DIALOG_STYLE|wx.CAPTION|wx.SYSTEM_MENU)
+ self.panel=wx.Panel(self)
+ self.CreateStatusBar()
+ self.SetStatusText("Program initialised")
+ self.SetBackgroundColour(wx.Colour(192,192,192))
+ # build menu
+ self.menu1 = wx.Menu()
+ self.menu1.Append(ID_LOG, 'Show Output &Log',
+ 'Redirect log statements to a window',wx.ITEM_CHECK)
+
+ self.menu1.Append(ID_ABOUT, "&About",
+ "More information about this program")
+ self.menu1.AppendSeparator()
+ self.menu1.Append(ID_EXIT, "E&xit", "Terminate the program")
+
+ menu2 = wx.Menu()
+ menu2.Append(ID_QMDEXTENSIONS, 'Allow Metadata Extensions',
+ 'Allow Metadata Extensions',
+ wx.ITEM_CHECK)
+ menu2.Append(ID_UCVARS, 'Force &Uppercase',
+ 'Force all identifiers to upper-case',
+ wx.ITEM_CHECK)
+
+ menu2.Append(ID_FORCEFIBFLOAT, 'Force &Float',
+ 'Force FIBs to be treated as floats',
+ wx.ITEM_CHECK)
+ menu2.Append(ID_FORCEDTD, 'Force DTD Location',
+ 'Overrule the DTD location found in the XML file')
+ menu2.Append(ID_FORCELANG, 'Default Language',
+ 'Provide a default language setting for converted items')
+ menu2.Append(ID_NOCOMMENT, 'Suppress Comments',
+ 'Suppress output of diagnostic comments in converted items',
+ wx.ITEM_CHECK)
+
+ # Set the options
+ menu2.Check(ID_QMDEXTENSIONS, self.options.qmdExtensions)
+ menu2.Check(ID_UCVARS, self.options.ucVars)
+ menu2.Check(ID_FORCEFIBFLOAT, self.options.forceFloat)
+ menu2.Check(ID_NOCOMMENT, self.options.noComment)
+
+ menuBar = wx.MenuBar()
+ menuBar.Append(self.menu1, "&File");
+ menuBar.Append(menu2, "&Options");
+ self.SetMenuBar(menuBar)
+ # events for menu items
+ wx.EVT_MENU(self, ID_ABOUT, self.OnAbout)
+ wx.EVT_MENU(self, ID_EXIT, self.TimeToQuit)
+ wx.EVT_MENU(self, ID_LOG, self.OnToggleRedirect)
+ wx.EVT_MENU(self, ID_QMDEXTENSIONS, self.OnQMDExtensions)
+ wx.EVT_MENU(self, ID_UCVARS, self.OnUCVars)
+ wx.EVT_MENU(self, ID_FORCEFIBFLOAT, self.OnForceFIBFloat)
+ wx.EVT_MENU(self, ID_FORCEDTD, self.OnForceDTD)
+ wx.EVT_MENU(self, ID_FORCELANG, self.OnForceLANG)
+ wx.EVT_MENU(self, ID_NOCOMMENT, self.OnSuppressComments)
+
+ # close event for main window
+ self.Bind(wx.EVT_CLOSE, self.TimeToQuit)
+
+ # Group FileBrowser and DirectoryBrowser
+ # Only one is to be made active
+
+ box1_title = wx.StaticBox( self.panel, 107, "Convert..." )
+ self.grp1_ctrls = []
+ radio1 = wx.RadioButton(self.panel, 103, "", style = wx.RB_SINGLE )
+ ffb1 = filebrowse.FileBrowseButton(self.panel, 104, size = (450, -1),
+ labelText = "Single QTIv1.2 File : ",
+ buttonText = "Browse..",
+ startDirectory = os.getcwd(),
+ fileMask = wildcard,
+ fileMode = wx.OPEN,
+ changeCallback = self.ffb1Callback)
+
+ radio1.SetValue(1)
+ ffb1.Enable(True)
+
+ radio2 = wx.RadioButton(self.panel, 105, "", style = wx.RB_SINGLE )
+ dbb1 = filebrowse.DirBrowseButton(self.panel, 106, size = (450, -1),
+ labelText = "Full Directory : ",
+ buttonText = "Browse..",
+ startDirectory = os.getcwd(),
+ changeCallback = self.dbb1Callback)
+
+ radio2.SetValue(0)
+ dbb1.Enable(False)
+
+ self.grp1_ctrls.append((radio1, ffb1))
+ self.grp1_ctrls.append((radio2, dbb1))
+
+ box1 = wx.StaticBoxSizer( box1_title, wx.VERTICAL )
+ grid1 = wx.FlexGridSizer( 0, 2, 0, 0 )
+ for radio, browseButton in self.grp1_ctrls:
+ grid1.Add( radio, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT|wx.TOP, 5 )
+ grid1.Add( browseButton, 0, wx.ALIGN_CENTRE|wx.LEFT|wx.RIGHT|wx.TOP, 5 )
+ box1.Add( grid1, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+
+ # == Save files to...
+ box2_title = wx.StaticBox( self.panel, 109, "Save files to..." )
+ self.grp2_ctrls = []
+ dbb2 = filebrowse.DirBrowseButton(self.panel, 108, size = (475, -1),
+ labelText = "Select Directory : ",
+ buttonText = "Browse..",
+ startDirectory = os.getcwd(),
+ changeCallback = self.dbb2Callback)
+ dbb2.SetValue(self.options.cpPath,0)
+
+ self.grp2_ctrls.append(dbb2)
+ box2 = wx.StaticBoxSizer( box2_title, wx.VERTICAL )
+ box2.Add( dbb2, 0, wx.ALIGN_CENTRE|wx.ALL, 5 )
+
+ # == define Convert and Cancel (close) buttons
+ button1 = wx.Button(self.panel, 1003, "Convert..")
+ self.Bind(wx.EVT_BUTTON, self.ConvertMe, button1)
+ button2 = wx.Button(self.panel, 1004, "Close")
+ self.Bind(wx.EVT_BUTTON, self.TimeToQuit, button2)
+
+ # Now add everything to the border
+ # First define a button row
+ buttonBorder = wx.BoxSizer(wx.HORIZONTAL)
+ buttonBorder.Add(button1, 0, wx.ALL, 5)
+ buttonBorder.Add(button2, 0, wx.ALL, 5)
+ # Add it all
+ border = wx.BoxSizer(wx.VERTICAL)
+ border.Add(box1, 0, wx.ALL, 5)
+ border.Add(box2, 0, wx.ALL, 5)
+ border.Add(buttonBorder, 0, wx.ALL, 5)
+ self.SetSizer(border)
+ self.SetAutoLayout(True)
+
+ # Setup event handling and initial state for controls:
+ for radio, browseButton in self.grp1_ctrls:
+ self.Bind(wx.EVT_RADIOBUTTON, self.OnGroup1Select, radio )
+ # radio.SetValue(0)
+ # browseButton.Enable(False)
+
+ self.app.SetMainFrame(self)
+
+ # ===============================
+ # reads input file or input directory from ctrls
+ # checks to see if one of them is set
+ # reads output directory from ctrl
+ # checks to see if it is set
+ # calls the parser to do the actual work
+ #
+ def ConvertMe(self, event):
+ cpInput=None
+ fileNames=[]
+ self.SetStatusText("Starting Conversion")
+ for radio, browseButton in self.grp1_ctrls:
+ if radio.GetValue() == 1:
+ cpInput = browseButton.GetValue()
+ print "Input: " + cpInput
+ if (cpInput !=""):
+ fileNames.append(cpInput)
+ else:
+ cpInput=None
+ print "No Input file or folder defined. Aborting..."
+ break
+ for browseButton in self.grp2_ctrls:
+ #handled by call back now
+ #self.options.cpPath=browseButton.GetValue()
+ if (self.options.cpPath!=""):
+ self.options.cpPath=os.path.abspath(self.options.cpPath)
+ if os.path.exists(self.options.cpPath):
+ if os.path.isdir(self.options.cpPath):
+ print "Output Directory is valid...."
+ else:
+ print "No Output Directory selected. Aborting..."
+ self.options.cpPath=''
+ break
+ else:
+ self.options.cpPath=''
+ if fileNames:
+ self.ProcessFiles(fileNames)
+ else:
+ print "No input set. Parser aborted..."
+ self.SetStatusText("Parser aborted...")
+
+ def ProcessFiles(self,fileNames):
+ self.SetStatusText("Parsing input files...")
+ parser=imsqtiv1.QTIParserV1(self.options)
+ parser.ProcessFiles(os.getcwd(),fileNames)
+ if self.options.cpPath:
+ print "Parsing complete"
+ self.SetStatusText("Creating content package...")
+ parser.DumpCP()
+ print "Migration completed..."
+ self.SetStatusText("")
+ else:
+ print "No output set. Parsing complete"
+ self.SetStatusText("Parsing complete (dry run)")
+ parser=None
+
+ # ===============================
+ # toggles visibility of either
+ # file input option ctrls or
+ # directory input option ctrls
+ #
+ def OnGroup1Select( self, event ):
+ radio_selected = event.GetEventObject()
+
+ for radio, browseButton in self.grp1_ctrls:
+ if radio is radio_selected:
+ browseButton.Enable(True)
+ radio.SetValue(1)
+ else:
+ browseButton.Enable(False)
+ radio.SetValue(0)
+ # ===============================
+ # displays about box
+ #
+ def OnAbout(self, event):
+ msg=string.join(self.app.splashLog,'\n')
+ dlg = wx.MessageDialog(self, msg,
+ "About Me", wx.OK | wx.ICON_INFORMATION)
+ dlg.ShowModal()
+ dlg.Destroy()
+
+ # ===============================
+ # check / uncheck qmdextensions option
+ #
+ def OnQMDExtensions(self, event):
+ self.options.qmdExtensions=abs(self.options.qmdExtensions-1)
+
+ # ===============================
+ # check / uncheck ucvars option
+ #
+ def OnUCVars(self, event):
+ self.options.ucVars=abs(self.options.ucVars-1)
+
+ # ===============================
+ # check / uncheckforcefibfloat option
+ #
+ def OnForceFIBFloat(self, event):
+ self.options.forceFloat=abs(self.options.forceFloat-1)
+
+ # ===============================
+ # displays DTD location entry box
+ #
+ def OnForceDTD(self, event):
+
+ dlg = wx.TextEntryDialog(
+ self, 'Enter the full DTD location that needs to be used.\nLeave empty to use the location provided in the QTI File.',
+ 'Override DTD location in QTI files', '')
+
+ dlg.SetValue(self.options.dtdDir)
+
+ if dlg.ShowModal() == wx.ID_OK:
+ self.options.dtdDir = dlg.GetValue()
+ self.options.dtdDir=os.path.abspath(self.options.dtdDir)
+ print "DTD_LOCATION = "+self.options.dtdDir
+
+ dlg.Destroy()
+
+ # ===============================
+ # displays Language entry box
+ #
+ def OnForceLANG(self, event):
+
+ dlg = wx.TextEntryDialog(
+ self, 'Enter the language code to use.\nLeave empty to use the one provided in the QTI File.',
+ 'Override language in QTI files', '')
+
+ dlg.SetValue(self.options.lang)
+
+ if dlg.ShowModal() == wx.ID_OK:
+ self.options.lang = dlg.GetValue()
+
+ dlg.Destroy()
+
+ # ===============================
+ # check / uncheck noComment option
+ #
+ def OnSuppressComments(self, event):
+ self.options.noComment=abs(self.options.noComment-1)
+
+
+ # ===============================
+ # callback functions for browse buttons
+ #
+ def ffb1Callback(self, evt):
+ print 'FileBrowseButton: %s\n' % evt.GetString()
+
+ def dbb1Callback(self, evt):
+ print 'DirBrowseButton: %s\n' % evt.GetString()
+
+ def dbb2Callback(self, evt):
+ self.options.cpPath=evt.GetString()
+ print 'CPOutBrowseButton: %s\n' % evt.GetString()
+
+ # ===============================
+ # let's go home
+ #
+ def TimeToQuit(self, event):
+ self.app.RestoreStdio()
+ sys.exit(1)
+
+ # ===============================
+ # switch between output to
+ # command box or ourput windows
+ #
+ def SetRedirect(self):
+ # Force output to be visible
+ self.menu1.Check(ID_LOG, True)
+ self.app.RedirectStdio()
+
+ def OnToggleRedirect(self, event):
+ if event.Checked():
+ self.app.RedirectStdio()
+ print "Log statements will be directed to this window.\n\n"
+ else:
+ self.app.RestoreStdio()
+ print "Log statements will be directed to this window.\n\n"
+
+# ==============================================================================
+
+def opj(path):
+ """Convert paths to the platform-specific separator"""
+ return apply(os.path.join, tuple(path.split('/')))
+
+# ==============================================================================
+#
+# MySplashScreen Class
+#
+# Description: show the splashscreen during startup
+#
+class MySplashScreen(wx.SplashScreen):
+ def __init__(self):
+ bmp = wx.Image(opj("IMSLogo.bmp")).ConvertToBitmap()
+ wx.SplashScreen.__init__(self, bmp,
+ wx.SPLASH_CENTRE_ON_SCREEN | wx.SPLASH_TIMEOUT,
+ 3000, None, -1)
+ self.Bind(wx.EVT_CLOSE, self.OnClose)
+
+ def OnClose(self, evt):
+ self.Hide()
+ # Open the main window
+ frame = MyFrame(None, -1, "QTI Migration Tool")
+ frame.Show(True)
+ frame.SetFocus()
+
+
+# ==============================================================================
+#
+# MyApp Class
+#
+# Description: handles display of splashscreen and creation of main window
+#
+
+class MyApp(wx.App):
+ def __init__(self,splashLog,options,fileNames):
+ self.splashLog=splashLog
+ self.options=options
+ self.mainframe=None
+ self.fileNames=fileNames
+ wx.App.__init__(self,0)
+
+ def SetMainFrame(self,mainframe):
+ print self.fileNames
+ self.mainframe=mainframe
+ # Turn on IO redirection
+ if self.fileNames:
+ self.mainframe.SetRedirect()
+ self.mainframe.ProcessFiles(self.fileNames)
+
+ def OnInit(self):
+ splash = MySplashScreen()
+ splash.Show()
+ self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
+ return True
+
+ def OnCloseWindow(self, event):
+ self.Destroy()
+
272 lib/imscp.py
@@ -0,0 +1,272 @@
+"""Copyright (c) 2004-2008, University of Cambridge.
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
+
+from lom import *
+from imsqti import QTIMetadata
+import StringIO
+import os
+from shutil import copyfile
+
+IMSCP_NAMESPACE="http://www.imsglobal.org/xsd/imscp_v1p1"
+IMSMD_NAMESPACE="http://www.imsglobal.org/xsd/imsmd_v1p2"
+IMSQTI_NAMESPACE="http://www.imsglobal.org/xsd/imsqti_v2p1"
+
+SCHEMA_LOCATION="""http://www.imsglobal.org/xsd/imscp_v1p1 http://www.imsglobal.org/xsd/imscp_v1p2.xsd
+http://www.imsglobal.org/xsd/imsmd_v1p2 http://www.imsglobal.org/xsd/imsmd_v1p2p4.xsd
+http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/imsqti_v2p1.xsd"""
+
+class ContentPackage:
+ def __init__ (self):
+ self.id=None
+ self.idSpace={}
+ self.fileSpace={}
+ self.resources=[]
+ self.lom=None
+
+ def GetUniqueID (self,baseStr):
+ idStr=baseStr
+ idExtra=1
+ while self.idSpace.has_key(idStr):
+ idStr=baseStr+'-'+str(idExtra)
+ idExtra=idExtra+1
+ return idStr
+
+ def GetUniqueFileName (self,fName,dataHash=None):
+ nameParts=fName.split(".")
+ stem=nameParts[0]
+ if not stem:
+ stem="file"
+ i=0
+ while 1:
+ nameParts[0]=stem
+ if i:
+ nameParts[0]='%s-%i'%(stem,i)
+ else:
+ nameParts[0]=stem
+ fName=string.join(nameParts,'.')
+ if self.fileSpace.has_key(fName):
+ if dataHash and self.fileSpace[fName]==dataHash:
+ break
+ i+=1
+ continue
+ break
+ self.fileSpace[fName]=dataHash
+ return fName
+
+ def AddResource (self,r):
+ if not r.id or self.idSpace.has_key(r.id):
+ r.AutoSetID(self)
+ self.idSpace[r.id]=r
+ self.resources.append(r)
+
+ def GetLOM (self):
+ if not self.lom:
+ self.lom=LOM()
+ return self.lom
+
+ def DumpToDirectory (self,path):
+ if not os.path.exists(path):
+ os.mkdir(path)
+ assert os.path.isdir(path)
+ manifestPath=os.path.join(path,'imsmanifest.xml')
+ f=open(manifestPath,'w')
+ print "Writing manifest file: "+manifestPath
+ self.WriteManifestXML(f)
+ f.close()
+ for r in self.resources:
+ r.DumpToDirectory(path)
+
+ def WriteManifestXML (self,f):
+ f.write('<?xml version="1.0"?>')
+ f.write('\n<manifest')
+ if self.id:
+ f.write(' identifier="'+XMLString(self.id)+'"')
+ else:
+ f.write(' identifier="'+self.GetUniqueID("manifest")+'"')
+ f.write('\n\txmlns="%s"'%IMSCP_NAMESPACE)
+ f.write('\n\txmlns:imsmd="%s"'%IMSMD_NAMESPACE)
+ f.write('\n\txmlns:imsqti="%s"'%IMSQTI_NAMESPACE)
+ f.write('\n\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
+ f.write('\n\txsi:schemaLocation="%s">'%SCHEMA_LOCATION)
+ if self.lom:
+ f.write('\n<metadata>\n\t<schema>IMS Content</schema>\n\t<schemaversion>1.1.3</schemaversion>')
+ self.lom.WriteIMSXML(f,"imsmd:")
+ f.write('\n</metadata>')
+ f.write('\n<organizations/>')
+ if self.resources:
+ f.write('\n<resources>')
+ for r in self.resources:
+ r.WriteManifestXML(f)
+ f.write('\n</resources>')
+ f.write('\n</manifest>')
+
+
+class CPResource:
+ def __init__ (self):
+ self.id=None
+ self.type='webcontent'
+ self.lom=None
+ self.qtiMD=None
+ self.files=[]
+ self.entryPoint=None
+
+ def SetIdentifier (self,identifier):
+ self.id=identifier
+ self.FixIdentifier()
+
+ def FixIdentifier (self):
+ # Must match correct syntax
+ newID=""
+ for c in self.id:
+ if not c in NMTOKEN_CHARS:
+ c='_'
+ if not newID and not (c in NMSTART_CHARS):
+ newID="ID_"
+ newID=newID+c
+ self.id=newID
+
+ def AutoSetID (self,cp):
+ self.id=None
+ if self.lom:
+ self.id=self.lom.SuggestXMLID()
+ self.FixIdentifier()
+ else:
+ self.id="resource"
+ self.id=cp.GetUniqueID(self.id)
+
+ def SetType (self,type):
+ self.type=type
+
+ def GetLOM (self):
+ if not self.lom:
+ self.lom=LOM()
+ return self.lom
+
+ def GetQTIMD (self):
+ if not self.qtiMD:
+ self.qtiMD=QTIMetadata()
+ return self.qtiMD
+
+ def AddFile (self,cpf,entryPoint=0):
+ self.files.append(cpf)
+ if entryPoint:
+ self.entryPoint=cpf
+
+ def DumpToDirectory (self,path):
+ for f in self.files:
+ f.DumpToDirectory(path)
+
+ def WriteManifestXML (self,f):
+ f.write('\n\t<resource identifier="'+self.id+'"')
+ if self.type:
+ f.write(' type="'+self.type+'"')
+ if self.entryPoint:
+ self.entryPoint.WriteResourceHREF(f)
+ if self.files or self.lom:
+ f.write('>')
+ if self.lom or self.qtiMD:
+ f.write('\n\t\t<metadata>')
+ if self.lom:
+ self.lom.WriteIMSXML(f,"imsmd:")
+ if self.qtiMD:
+ self.qtiMD.WriteXML(f,"imsqti:")
+ f.write('\n\t\t</metadata>')
+ for cpf in self.files:
+ cpf.WriteManifestXML(f)
+ f.write('\n\t</resource>')
+ else:
+ f.write('/>')
+
+class CPFile:
+ def __init__ (self):
+ self.href=None
+ self.lom=None
+ self.data=None
+ self.dataPath=None
+
+ def SetHREF (self,href):
+ self.href=href
+
+ def SetData (self,data):
+ self.data=data
+
+ def SetDataPath (self,dataPath):
+ self.dataPath=dataPath
+
+ def DumpToDirectory (self,path):
+ if RelativeURL(self.href):
+ filepath=ResolveCPURI(path,self.href)
+ print "Writing file: "+filepath
+ if self.dataPath is None:
+ f=open(filepath,'w')
+ f.write(self.data)
+ f.close()
+ else:
+ try:
+ copyfile(self.dataPath,filepath)
+ except IOError:
+ print 'Problem copying "%s" -> "%s"'%(self.dataPath,filepath)
+ f=open(filepath,'w')
+ f.write("Data Missing\n")
+ f.close()
+
+ def WriteResourceHREF (self,f):
+ if self.href:
+ f.write(' href="'+XMLString(self.href)+'"')
+
+ def WriteManifestXML (self,f):
+ f.write('\n\t\t<file')
+ if self.href:
+ f.write(' href="'+XMLString(self.href)+'"')
+ if self.lom:
+ f.write('>')
+ if self.lom:
+ f.write('\n\t\t\t<metadata>\n\t\t\t\t<schema>IMS Content</schema>\n\t\t\t\t<schemaversion>1.1.3</schemaversion>')
+ self.lom.WriteIMSXML(f,"imsmd:")
+ f.write('\n\t\t\t</metadata>')
+ f.write('\n\t\t</file>')
+ else:
+ f.write('/>')
+
+def ResolveCPURI (cpPath,uri):
+ walk=0
+ path=cpPath
+ segments=string.split(uri,'/')
+ for segment in segments:
+ if segment==".":
+ continue
+ elif segment=="..":
+ if walk:
+ path,discard=os.path.split(path)
+ walk=walk-1
+ print "Warning: relative URL tried to leave the CP directory"
+ else:
+ walk=walk+1
+ path=os.path.join(path,DecodePathSegment(segment))
+ return path
1,483 lib/imsqti.py
@@ -0,0 +1,1483 @@
+"""Copyright (c) 2004-2008, University of Cambridge.
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms
+(where applicable), with or without modification, are permitted
+provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions, and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions, and the following
+ disclaimer in the documentation and/or other materials provided with
+ the distribution.
+
+ * Neither the name of the University of Cambridge, nor the names of
+ any other contributors to the software, may be used to endorse or
+ promote products derived from this software without specific prior
+ written permission.
+
+THIS SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""
+
+from xmlutils import *
+
+class QTIMetadata:
+ def __init__ (self):
+ self.itemTemplate=None
+ self.timeDependent=None
+ self.composite=None
+ self.interactionTypes={}
+ self.feedbackType=None
+ self.solutionAvailable=None
+ self.toolName=None
+ self.toolVersion=None
+ self.toolVendor=None
+
+ def SetItemTemplate(self,itemTemplate):
+ self.itemTemplate=itemTemplate
+
+ def SetTimeDependent(self,timeDependent):
+ self.timeDependent=timeDependent
+
+ def SetComposite(self,composite):
+ self.composite=composite
+
+ def AddInteractionType(self,interactionType):
+ self.interactionTypes[interactionType]=1
+
+ def SetFeedbackType(self,feedbackType):
+ self.feedbackType=feedbackType
+
+ def SetSolutionAvailable(self,solutionAvailable):
+ self.solutionAvailable=solutionAvailable
+
+ def SetToolName(self,name):
+ self.toolName=name
+
+ def SetToolVersion(self,version):
+ self.toolVersion=version
+
+ def SetToolVendor(self,vendor):
+ self.toolVendor=vendor
+
+ def WriteXML (self,f,ns):
+ if ns:
+ f.write('\n<'+ns+'qtiMetadata>')
+ else:
+ f.write('\n<qtiMetadata xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"\
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\
+ xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 imsqti_v2p1.xsd">')
+ if self.itemTemplate is not None:
+ if self.itemTemplate:
+ value="true"
+ else:
+ value="false"
+ f.write('\n<'+ns+'itemTemplate>'+value+'</'+ns+'itemTemplate>')
+ if self.timeDependent is not None:
+ if self.timeDependent:
+ value="true"
+ else:
+ value="false"
+ f.write('\n<'+ns+'timeDependent>'+value+'</'+ns+'timeDependent>')
+ if self.composite is not None:
+ if self.composite:
+ value="true"
+ else:
+ value="false"
+ f.write('\n<'+ns+'composite>'+value+'</'+ns+'composite>')
+ for interactionType in self.interactionTypes.keys():
+ f.write('\n<'+ns+'interactionType>'+XMLString(interactionType)+'</'+ns+'interactionType>')
+ if self.feedbackType is not None:
+ f.write('\n<'+ns+'feedbackType>'+XMLString(self.feedbackType)+'</'+ns+'feedbackType>')
+ if self.solutionAvailable is not None:
+ if self.solutionAvailable:
+ value="true"
+ else:
+ value="false"
+ f.write('\n<'+ns+'solutionAvailable>'+value+'</'+ns+'solutionAvailable>')
+ if self.toolName:
+ f.write('\n<'+ns+'toolName>'+XMLString(self.toolName)+'</'+ns+'toolName>')
+ if self.toolVersion:
+ f.write('\n<'+ns+'toolVersion>'+XMLString(self.toolVersion)+'</'+ns+'toolVersion>')
+ if self.toolVendor:
+ f.write('\n<'+ns+'toolVendor>'+XMLString(self.toolVendor)+'</'+ns+'toolVendor>')
+ f.write('\n</'+ns+'qtiMetadata>')
+
+class AssessmentItem:
+ def __init__ (self):
+ self.identifier=""
+ self.title=""
+ self.label=None
+ self.language=None
+ self.adaptive=0
+ self.timeDependent=0
+ self.toolName=None
+ self.toolVersion=None
+ self.variables={}
+ self.itemBody=None
+ self.responseProcessing=None
+ self.modalFeedback=[]
+
+ def SetIdentifier (self,identifier):
+ self.identifier=identifier
+
+ def SetTitle (self,title):
+ self.title=title
+
+ def SetLabel (self,label):
+ self.label=label
+
+ def SetLanguage (self,language):
+ self.language=language
+
+ def DeclareVariable (self,variable):
+ if self.variables.has_key(variable.GetIdentifier()):
+ raise QTIException(eDuplicateVariable,variable.GetIdentifier())
+ self.variables[variable.GetIdentifier()]=variable
+
+ def ResetResponseProcessing (self):
+ self.responseProcessing=None
+ for vName in self.variables.keys():
+ v=self.variables[vName]
+ if isinstance(v,OutcomeDeclaration):
+ del self.variables[vName]
+
+ def GetItemBody (self):
+ if not self.itemBody:
+ self.itemBody=ItemBody()
+ return self.itemBody
+
+ def GetResponseProcessing (self):
+ if not self.responseProcessing:
+ self.responseProcessing=ResponseProcessing()
+ return self.responseProcessing
+
+ def AddModalFeedback (self,feedback):
+ self.modalFeedback.append(feedback)
+
+ def HasModalFeedback (self):
+ return len(self.modalFeedback)>0
+
+ def WriteXML (self,f):
+ f.write('<assessmentItem')
+ f.write('\n\txmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"')
+ f.write('\n\txmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"')
+ f.write('\n\txsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 http://www.imsglobal.org/xsd/imsqti_v2p1.xsd"')
+ f.write(' identifier="'+XMLString(self.identifier)+'"')
+ f.write('\n title="'+XMLString(self.title)+'"')
+ if self.label:
+ f.write('\n label="'+XMLString(self.label)+'"')
+ if self.language:
+ f.write('\n xml:lang="'+XMLString(self.language)+'"')
+ if self.adaptive:
+ f.write('\n adaptive="true"')
+ else:
+ f.write('\n adaptive="false"')
+ if self.timeDependent:
+ f.write('\n timeDependent="true"')
+ else:
+ f.write('\n timeDependent="false"')
+ if self.toolName:
+ f.write('\n toolName="'+XMLString(self.toolName)+'"')
+ if self.toolVersion:
+ f.write('\n toolVersion="'+XMLString(self.toolVersion)+'"')
+ f.write('>')
+ vars=self.variables.keys()
+ vars.sort()
+ for var in vars:
+ varDeclaration=self.variables[var]
+ if isinstance(varDeclaration,ResponseDeclaration):
+ varDeclaration.WriteXML(f)
+ for var in vars:
+ varDeclaration=self.variables[var]
+ if isinstance(varDeclaration,OutcomeDeclaration):
+ varDeclaration.WriteXML(f)
+ # templateDeclarations
+ # templateProcessing
+ if self.itemBody:
+ self.itemBody.WriteXML(f)
+ if self.responseProcessing:
+ self.responseProcessing.WriteXML(f)
+ for feedback in self.modalFeedback:
+ feedback.WriteXML(f)
+ f.write('\n</assessmentItem>\n')
+
+
+class VariableDeclaration:
+ def __init__ (self,identifier,cardinality,baseType):
+ self.identifier=identifier
+ self.cardinality=cardinality
+ self.baseType=baseType
+ self.default=None
+
+ def GetIdentifier (self):
+ return self.identifier
+
+ def GetCardinality (self):
+ return self.cardinality
+
+ def GetBaseType (self):
+ return self.baseType
+
+ def SetDefaultValue (self,value):
+ self.default=value
+
+ def GetDefaultValue (self):
+ return value
+
+
+class ResponseDeclaration(VariableDeclaration):
+ def WriteXML (self,f):
+ f.write('\n<responseDeclaration identifier="'+self.identifier+
+ '" cardinality="'+self.cardinality+
+ '" baseType="'+self.baseType+'"')
+ if self.default:
+ f.write('>')
+ self.default.WriteXML(f)
+ f.write('</responseDeclaration>')
+ else:
+ f.write('/>')
+
+
+class OutcomeDeclaration(VariableDeclaration):
+ def __init__ (self,identifier,cardinality,baseType):
+ VariableDeclaration.__init__(self,identifier,cardinality,baseType)
+ self.interpretation=None
+ self.normalMaximum=None
+
+ def SetInterpretation (self,value):
+ self.interpretation=value
+
+ def SetNormalMaximum (self,value):
+ self.normalMaximum=value
+
+ def WriteXML (self,f):
+ f.write('\n<outcomeDeclaration identifier="'+self.identifier+
+ '" cardinality="'+self.cardinality+
+ '" baseType="'+self.baseType+'"')
+ if self.interpretation:
+ f.write(' interpretation="'+XMLString(self.interpretation)+'"')
+ if self.normalMaximum:
+ f.write(' normalMaximum="'+str(self.normalMaximum)+'"')
+ if self.default:
+ f.write('>')
+ self.default.WriteXML(f)
+ f.write('</outcomeDeclaration>')
+ else:
+ f.write('/>')
+
+class DefaultValue:
+ def __init__ (self,value=""):
+ self.value=value
+ self.interpretation=None
+
+ def SetInterpretation (self,interpretation):
+ self.interpretation=interpretation
+
+ def WriteXML (self,f):
+ f.write('\n<defaultValue')
+ if self.interpretation:
+ f.write(' interpretation="'+XMLString(self.interpretation)+'">')
+ else:
+ f.write(">")
+ f.write('<value>'+XMLString(self.value)+'</value>')
+ f.write("</defaultValue>")
+
+
+class BodyElement:
+ def __init__ (self):
+ self.id=None
+ self.classid=None
+ self.language=None
+ self.label=None
+
+ def SetID (self,id):
+ self.id=id
+
+ def SetClass (self,classid):
+ self.classid=classid
+
+ def SetLanguage (self,language):
+ self.language=language
+
+ def SetLabel (self,label):
+ self.label=label
+
+ def ExtractText (self):
+ self.PrintWarning("Warning: trying to extract text from non-text object")
+ return "<missing object>"
+
+ def ExtractImages (self):
+ return []
+
+ def WriteXMLAttributes (self,f):
+ if self.id:
+ f.write(' id="'+XMLString(self.id)+'"')
+ if self.classid:
+ f.write(' class="'+XMLString(self.classid)+'"')
+ if self.language:
+ f.write(' xml:lang="'+XMLString(self.language)+'"')
+ if self.label:
+ f.write(' label="'+XMLString(self.label)+'"')
+
+ def WriteXML (self,f):
+ pass
+
+class ObjectFlow: pass
+
+class Flow(BodyElement,ObjectFlow): pass
+
+class Block(Flow): pass
+
+class Inline(Flow): pass
+
+SimpleInlineNames=['a','abbr','acronym','b','big','cite','code','dfn','em','i','kbd',
+ 'q','samp','small','span','strong','sub','sup','tt','var']
+
+class SimpleInline(Inline):
+ def __init__ (self,name):
+ BodyElement.__init__(self)
+ self.name=name
+ self.elements=[]
+
+ def AppendElement(self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write("\n<"+self.name)
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write(">")
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</"+self.name+">")
+
+ def ExtractText (self):
+ text=""
+ for element in self.elements:
+ text=text+element.ExtractText()
+ return text
+
+ def ExtractImages (self):
+ return ExtractImages(self.elements)
+
+
+class AtomicBlock(Block):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.elements=[]
+
+ def AppendElement(self,element):
+ self.elements.append(element)
+
+ def ExtractText (self):
+ text=""
+ for element in self.elements:
+ text=text+element.ExtractText()
+ return text
+
+ def ExtractImages (self):
+ return ExtractImages(self.elements)
+
+
+class SimpleBlock(Block):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.elements=[]
+
+ def AppendElement(self,element):
+ self.elements.append(element)
+
+ def ExtractText (self):
+ text=""
+ for element in self.elements:
+ text=text+element.ExtractText()
+ return text
+
+ def ExtractImages (self):
+ return ExtractImages(self.elements)
+
+
+class ItemBody(BodyElement):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.blocks=[]
+
+ def AppendBlock (self,block):
+ self.blocks.append(block)
+
+ def WriteXML (self,f):
+ f.write('\n<itemBody')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for block in self.blocks:
+ block.WriteXML(f)
+ f.write('\n</itemBody>')
+
+ def ExtractImages (self):
+ return ExtractImages(self.blocks)
+
+
+class RubricBlock(BodyElement):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.blocks=[]
+ self.view=[]
+
+ def AppendView (self,view):
+ if not view in self.view:
+ self.view.append(view)
+
+ def AppendElement (self,block):
+ self.blocks.append(block)
+
+ def WriteXML (self,f):
+ if not self.view:
+ view=['all']
+ else:
+ view=self.view
+ f.write('\n<rubricBlock')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write(' view="'+string.join(view,' ')+'">')
+ for block in self.blocks:
+ block.WriteXML(f)
+ f.write('\n</rubricBlock>')
+
+class ModalFeedback(BodyElement):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.outcomeIdentifier="FEEDBACK"
+ self.identifier=None
+ self.showHide='show'
+ self.title=None
+ self.elements=[]
+
+ def SetOutcomeIdentifier (self,identifier):
+ self.outcomeIdentifier=identifier
+
+ def SetShowHide (self,showHide):
+ self.showHide=showHide
+
+ def SetIdentifier (self,identifier):
+ self.identifier=identifier
+
+ def SetTitle (self,title):
+ self.title=title
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<modalFeedback')
+ f.write(' outcomeIdentifier="'+XMLString(self.outcomeIdentifier)+'"')
+ f.write(' showHide="'+XMLString(self.showHide)+'"')
+ f.write(' identifier="'+XMLString(self.identifier)+'"')
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write('</modalFeedback>')
+
+def ExtractImages (elements):
+ images=[]
+ i=0
+ while i<len(elements):
+ element=elements[i]
+ if isinstance(element,xhtml_object) and element.type[:6]=='image/':
+ images.append(element)
+ del elements[i]
+ else:
+ images=images+element.ExtractImages()
+ i=i+1
+ return images
+
+class xhtml_div(Block):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.elements=[]
+
+ def AppendElement (self,element):
+ assert not (element is None),"Adding None to xhtml_div"
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<div')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write('</div>')
+
+ def ExtractImages (self):
+ return ExtractImages(self.elements)
+
+class xhtml_blockquote(SimpleBlock):
+
+ def __init__(self):
+ SimpleBlock.__init__(self)
+
+ def WriteXML (self,f):
+ f.write("\n<blockquote")
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write(">")
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</blockquote>")
+
+
+class xhtml_ul(Block):
+ def __init__ (self,name="ul"):
+ BodyElement.__init__(self)
+ self.listItems=[]
+ self.name=name
+
+ def AppendElement (self,element):
+ if isinstance(element,xhtml_text):
+ assert not element.text.strip(),"PCDATA in <"+self.name+">"
+ else:
+ assert isinstance(element,xhtml_li),"Adding non-list item to list: %s"%repr(element)
+ self.listItems.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<'+self.name)
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for listItem in self.listItems:
+ listItem.WriteXML(f)
+ f.write('</'+self.name+'>')
+
+class xhtml_li(BodyElement):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.elements=[]
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<li')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write('</li>')
+
+class xhtml_p(AtomicBlock):
+
+ def __init__(self):
+ AtomicBlock.__init__(self)
+
+ def WriteXML (self,f):
+ f.write("\n<p")
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write(">")
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</p>")
+
+class xhtml_pre(AtomicBlock):
+
+ def __init__(self):
+ AtomicBlock.__init__(self)
+
+ def WriteXML (self,f):
+ f.write("\n<pre")
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write(">")
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</pre>")
+
+class xhtml_object(Inline):
+ def __init__ (self):
+ self.data=None
+ self.type=None
+ self.height=None
+ self.width=None
+
+ def SetData (self,data):
+ self.data=data
+
+ def SetType (self,type):
+ self.type=type
+
+ def SetHeight (self,height):
+ self.height=height
+
+ def SetWidth (self,width):
+ self.width=width
+
+ def WriteXML (self,f):
+ f.write("<object")
+ if self.data:
+ f.write(' data="'+XMLString(self.data)+'"')
+ if self.type:
+ f.write(' type="'+XMLString(self.type)+'"')
+ if self.width:
+ f.write(' width="'+str(self.width)+'"')
+ if self.width:
+ f.write(' height="'+str(self.height)+'"')
+ f.write("/>")
+
+class xhtml_table(Block):
+ def __init__(self):
+ BodyElement.__init__(self)
+ self.tableBody=[]
+ self.summary=None
+
+ def SetSummary (self,summary):
+ self.summary=summary
+
+ def AppendElement (self,element):
+ if isinstance(element,xhtml_text):
+ assert not element.text.strip(),"PCDATA in <table>"
+ elif isinstance(element,xhtml_tbody):
+ self.tableBody.append(element)
+ else:
+ assert isinstance(element,xhtml_tr),"Adding non-table tag to table: %s"%repr(element)
+ if not self.tableBody:
+ # An implied table body
+ self.tableBody.append(xhtml_tbody())
+ self.tableBody[0].AppendElement(element)
+
+ def WriteXML (self,f):
+ f.write('\n<table')
+ BodyElement.WriteXMLAttributes(self,f)
+ if self.summary:
+ f.write(' summary="'+XMLString(self.summary)+'"')
+ f.write('>')
+ for tbody in self.tableBody:
+ tbody.WriteXML(f)
+ f.write('</table>')
+
+class xhtml_tbody(BodyElement):
+ def __init__(self):
+ BodyElement.__init__(self)
+ self.rows=[]
+
+ def AppendElement (self,element):
+ if isinstance(element,xhtml_text):
+ assert not element.text.strip(),"PCDATA in <tbody>"
+ else:
+ assert isinstance(element,xhtml_tr),"Adding non-table tag to tbody: %s"%repr(element)
+ self.rows.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<tbody')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for tr in self.rows:
+ tr.WriteXML(f)
+ f.write('</tbody>')
+
+class xhtml_tr(BodyElement):
+ def __init__(self):
+ BodyElement.__init__(self)
+ self.cells=[]
+
+ def AppendElement (self,element):
+ if isinstance(element,xhtml_text):
+ assert not element.text.strip(),"PCDATA in <tr>"
+ else:
+ assert isinstance(element,TableCell),"Adding non-table cell tag to tr: %s"%repr(element)
+ self.cells.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<tr')
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for tcell in self.cells:
+ tcell.WriteXML(f)
+ f.write('</tr>')
+
+class TableCell(BodyElement):
+ def __init__(self,name="td"):
+ BodyElement.__init__(self)
+ self.name=name
+ self.elements=[]
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<'+self.name)
+ BodyElement.WriteXMLAttributes(self,f)
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write('</'+self.name+'>')
+
+
+class xhtml_img(Inline):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ self.src=None
+ self.alt=""
+ self.longdesc=None
+ self.height=None
+ self.width=None
+
+ def SetSrc (self,src):
+ self.src=src
+
+ def SetAlt (self,alt):
+ self.alt=alt
+
+ def SetLongDesc (self,longdesc):
+ self.longdesc=None
+
+ def SetHeight (self,height):
+ self.height=height
+
+ def SetWidth (self,width):
+ self.width=width
+
+ def WriteXML (self,f):
+ f.write("<img")
+ if self.src:
+ f.write(' src="'+XMLString(self.src)+'"')
+ f.write(' alt="'+XMLString(self.alt)+'"')
+ if self.longdesc:
+ f.write(' longdesc="'+XMLString(self.longdesc)+'"')
+ if self.width:
+ f.write(' width="'+str(self.width)+'"')
+ if self.width:
+ f.write(' height="'+str(self.height)+'"')
+ f.write("/>")
+
+class xhtml_br(Inline):
+ def WriteXML (self,f):
+ f.write("<br/>\n")
+
+class xhtml_text(Inline):
+ def __init__ (self,text=""):
+ self.text=text
+
+ def SetText (self, text):
+ self.text=text
+
+ def WriteXML (self,f):
+ f.write(XMLString(self.text))
+
+ def ExtractText (self):
+ return self.text
+
+class Interaction:
+ def __init__ (self):
+ self.response=None
+
+ def BindResponse (self,response):
+ self.response=response
+
+ def WriteXMLAttributes (self,f):
+ f.write(' responseIdentifier="'+self.response+'"')
+
+class BlockInteraction(Block,Interaction):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ Interaction.__init__(self)
+ self.prompt=None
+
+ def GetPrompt (self):
+ if not self.prompt:
+ self.prompt=Prompt()
+ return self.prompt
+
+class InlineInteraction(Inline,Interaction): pass
+
+class Prompt:
+ def __init__ (self):
+ self.elements=[]
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write('\n<prompt>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write('</prompt>')
+
+class ChoiceInteraction(BlockInteraction):
+ def __init__ (self):
+ BlockInteraction.__init__(self)
+ self.shuffle=0
+ self.maxChoices=1
+ self.choices=[]
+
+ def SetShuffle (self,shuffle):
+ self.shuffle=shuffle
+
+ def SetMaxChoices (self,maxChoices):
+ self.maxChoices=maxChoices
+
+ def WriteXML (self,f):
+ f.write("\n<choiceInteraction")
+ Interaction.WriteXMLAttributes(self,f)
+ BodyElement.WriteXMLAttributes(self,f)
+ if self.shuffle:
+ f.write(' shuffle="true"')
+ else:
+ f.write(' shuffle="false"')
+ f.write(' maxChoices="'+str(self.maxChoices)+'"')
+ f.write('>')
+ if self.prompt:
+ self.prompt.WriteXML(f)
+ for choice in self.choices:
+ choice.WriteXML(f)
+ f.write('\n</choiceInteraction>')
+
+ def AddChoice (self,choice):
+ self.choices.append(choice)
+
+class OrderInteraction(BlockInteraction):
+ def __init__ (self):
+ BlockInteraction.__init__(self)
+ self.shuffle=0
+ self.choices=[]
+
+ def SetShuffle (self,shuffle):
+ self.shuffle=shuffle
+
+ def WriteXML (self,f):
+ f.write("\n<orderInteraction")
+ Interaction.WriteXMLAttributes(self,f)
+ BodyElement.WriteXMLAttributes(self,f)
+ if self.shuffle:
+ f.write(' shuffle="true"')
+ else:
+ f.write(' shuffle="false"')
+ f.write('>')
+ if self.prompt:
+ self.prompt.WriteXML(f)
+ for choice in self.choices:
+ choice.WriteXML(f)
+ f.write('\n</orderInteraction>')
+
+ def AddChoice (self,choice):
+ self.choices.append(choice)
+
+class AssociateInteraction(BlockInteraction):
+ def __init__ (self):
+ BlockInteraction.__init__(self)
+ self.shuffle=0
+ self.maxAssociations=1
+ self.choices=[]
+
+ def SetShuffle (self,shuffle):
+ self.shuffle=shuffle
+
+ def SetMaxAssociations (self,maxAssociations):
+ self.maxAssociations=maxAssociations
+
+ def WriteXML (self,f):
+ f.write("\n<associateInteraction")
+ Interaction.WriteXMLAttributes(self,f)
+ BodyElement.WriteXMLAttributes(self,f)
+ if self.shuffle:
+ f.write(' shuffle="true"')
+ else:
+ f.write(' shuffle="false"')
+ f.write(' maxAssociations="'+str(self.maxAssociations)+'"')
+ f.write('>')
+ if self.prompt:
+ self.prompt.WriteXML(f)
+ for choice in self.choices:
+ choice.WriteXML(f)
+ f.write('\n</associateInteraction>')
+
+ def AddChoice (self,choice):
+ self.choices.append(choice)
+
+class Choice:
+ def __init__ (self):
+ self.identifier=""
+ self.fixed=None
+
+ def SetIdentifier (self,identifier):
+ assert not (identifier is None)
+ self.identifier=identifier
+
+ def GetIdentifier (self):
+ return self.identifier
+
+ def SetFixed (self,fixed):
+ self.fixed=fixed
+
+ def WriteXMLAttributes (self,f):
+ f.write(' identifier="'+XMLString(self.identifier)+'"')
+ if self.fixed:
+ f.write(' fixed="true"')
+ elif not (self.fixed is None):
+ f.write(' fixed="false"')
+
+class SimpleChoice(Choice,BodyElement):
+ def __init__ (self):
+ BodyElement.__init__(self)
+ Choice.__init__(self)
+ self.elements=[]
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def WriteXML (self,f):
+ f.write("\n<simpleChoice")
+ BodyElement.WriteXMLAttributes(self,f)
+ Choice.WriteXMLAttributes(self,f)
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</simpleChoice>")
+
+class AssociableChoice(Choice):
+ def __init__ (self):
+ Choice.__init__(self)
+ self.elements=[]
+ self.matchMax=0
+ self.matchGroup=[]
+
+ def AppendElement (self,element):
+ self.elements.append(element)
+
+ def SetMatchMax (self,max):
+ self.matchMax=max
+
+ def SetMatchGroup (self,group):
+ self.matchGroup=group
+
+ def WriteXMLAttributes (self,f):
+ Choice.WriteXMLAttributes(self,f)
+ f.write(' matchMax="'+str(self.matchMax)+'"')
+ if self.matchGroup:
+ f.write(' matchGroup="'+string.join(self.matchGroup,' ')+'"')
+
+class SimpleAssociableChoice(AssociableChoice):
+ def __init__ (self):
+ AssociableChoice.__init__(self)
+
+ def WriteXML (self,f):
+ f.write("\n<simpleAssociableChoice")
+ AssociableChoice.WriteXMLAttributes(self,f)
+ f.write('>')
+ for element in self.elements:
+ element.WriteXML(f)
+ f.write("</simpleAssociableChoice>")
+
+class StringInteraction:
+ def __init__(self):
+ self.base=None
+ self.stringIdentifier=None
+ self.expectedLength=None
+
+ def SetBase (self,base):
+ self.base=base
+
+ def SetStringIdentifier (self,identifier):
+ self.stringIdentifier=identifier
+
+ def SetExpectedLength (self,length):
+ self.expectedLength=length
+
+ def WriteXMLAttributes (self,f):
+ if self.stringIdentifier:
+ f.write(' stringIdentifier="'+self.stringIdentifier+'"')
+ if self.base:
+ f.write(' base="'+str(self.base)+'"')
+ if self.expectedLength:
+ f.write(' expectedLength="'+str(self.expectedLength)+'"')
+
+class ExtendedTextInteraction(BlockInteraction,StringInteraction):
+ def __init__(self):
+ BlockInteraction.__init__(self)
+ StringInteraction.__init__(self)
+ self.maxStrings=None
+
+ def SetMaxStrings (self,maxStrings):
+ self.maxStrings=maxStrings
+
+ def WriteXML (self,f):
+ f.write("\n<extendedTextInteraction")
+ Interaction.WriteXMLAttributes(self,f)
+ BodyElement.WriteXMLAttributes(self,f)
+ StringInteraction.WriteXMLAttributes(self,f)
+ if self.maxStrings:
+ f.write(' maxStrings="'+str(self.maxStrings)+'"')
+ if self.prompt:
+ f.write(">")
+ self.prompt.WriteXML(f)
+ f.write("</extendedTextInteraction>")
+ else:
+ f.write("/>")
+
+class TextEntryInteraction (InlineInteraction,StringInteraction):
+ def __init__(self):
+ BodyElement.__init__(self)
+ Interaction.__init__(self)
+ StringInteraction.__init__(self)
+
+ def WriteXML (self,f):
+ f.write("\n<textEntryInteraction")
+ Interaction.WriteXMLAttributes(self,f)
+ BodyElement.WriteXMLAttributes(self,f)
+ StringInteraction.WriteXMLAttributes(self,f)
+ f.write("/>")
+
+class GraphicInteraction (BlockInteraction):
+ def __init__(self):
+ BlockInteraction.__init__(self)
+ self.graphic=None