Creating custom iDevices in Python
Creating custom iDevices in Python
Although it is hoped that the material below will be beneficial to others, it has not been produced by the original developers of eXe, and as such may include bad practice and/or errors. Please use at your own discretion. All instructions relate to Windows XP.
The most likely reason for experimenting with the eXe source code is to create custom iDevices. Although it is technically possible to do this without adjusting the main source code, you will ultimately benefit from doing so; particularly for iDevice loading.
Setting up Eclipse
The following general instructions relate to setting up the eXe source code in the Eclipse 3.4.2 IDE. Other Python IDEs will obviously work, and will follow similar steps.
- Follow instructions for setting up Python, Eclipse, and pydev. You should find these on the pydev website.
- Install Twisted, pycrypto, PIL, and py2exe libraries
- Download and extract the eXe source
- Start Eclipse, and create a new Pydev project called eXe: set Python grammar to 2.5, and ensure "Add src folder" is unticked.
- Right click on the new project, select properties->Pydev - PYTHONPATH, then "Add source folder" and accept pre-selected directory (but note where it is).
- Move the exe source into the project. You need to copy the source folders in so it has the README etc in the top.
- Copy a Firefox folder to /exe/webui/Mozilla Firefox. If you have an installed version of eXe then you can copy the directory from there. Compatibility with later versions of Firefox has not been tested.
- In Eclipse, right click on the project and select "Refresh"
- Copy "exe/exe" to the root of the project, and rename the copy to "run.py" This is for convenience.
- Open the new "exe/run.py" and press F9 to start.
- It runs!
Debugging of the eXe source code is achieved mainly through log files, and very rarely will any errors encountered appear in the IDE. The log files can be found in the directory:
C:\Documents and Settings\*userid*\Application Data\exe\
The latest log file is name exe.log, and the preceeding 10 log files are also retained.
By default eXe will look for custom iDevices in a hidden system folder:
C:\Documents and Settings\*userid*\Application Data\exe\idevices\
You can copy your iDevice files here, and eXe should load them, but this is less than ideal for both development and deployment. The following instructions describe how to adjust the source code so that iDevices are loaded from within the eXe directory.
The file to change is /exe/engine/idevicestore.py In the loadExtended routine the call to self.__loadUserExtended() may be commented out. Uncomment it.
In the loadUserExtended routine replace the line:
idevicePath = self.config.configDir/'idevices'
idevicePath = self.config.exePath.dirname()/'idevices'
Create a folder in the eXe source code root directory named idevices, and place your files within it.
Creating an iDevice base
iDevices are described by a pair of files:
- nameidevice.py : Describes the component types, default values etc.
- nameblock.py : Describes how the content is rendered as XHTML
In the iDevice source folder: /exe/idevices/, you will find two example files that serve as a base for your own iDevices, and should be copied to the idevice folder. Certain parts then need to be renamed to create your new iDevice:
- the filenames: they must end in idevice.py and block.py
- in the iDevice, rename the class and the call in the register function
- in the block, rename the class, change the filename and class name of the iDevice in the register function
Main functions in the block are:
- renderEdit: view when editing the iDevice, mainly calls to component edit methods
- renderPreview: view of the iDevice in eXe when not editing
- renderView: view of the iDevice when exported to webpage etc.
These source files will be compiled when the eXe application starts, creating pyc files in the idevices folder. If eXe fails to load, then there is most likely a problem in one of the iDevice files.
Elements and Fields
field.py and element.py are roughly equivalent files that are loaded into the idevice and block respectively. Both describe UI objects (e.g. text areas), with field.py describing the storage of the information, and element.py describing the presentation of the information.
The methods for the element objects are intended to present information in a HTML format. As such they generally return text formatted with HTML tags. If you want to access the values of the elements (e.g. the actual text in a TextArea element), then you will need to add extra methods. These can be very simple, such as:
def getContent (self): return self.field.content_w_resourcePaths
Use the render methods as a guide to see how to access the properties.
One way (probably not the correct way) of doing this is by adding the following code (originally taken from the applet iDevice):
In the iDevice, import the required functionality:
from exe.engine.resource import Resource
Again in the iDevice, add a method:
def includeFile(self, filePath): """ Store the upload files in the package Needs to be in a package to work. """ log.debug(u"uploadFile "+unicode(filePath)) resourceFile = Path(filePath) assert(self.parentNode, _('file %s has no parentNode') % self.id) assert(self.parentNode.package, _('iDevice %s has no package') % self.parentNode.id) if resourceFile.isfile(): self.message = "" Resource(self, resourceFile) else: log.error('File %s is not a file' % resourceFile)
Files can now be registered from the block using the code:
The uncertainty is where to add the code in the block file. Adding it to the renderPreview code works, but an attempt will be made to add the file everytime the preview is generated. eXe includes several safeguards for checking if files have been added or if 'zombie' files are present (i.e. files that are no longer attached to an iDevice), but it is probably not a good idea to rely on these. If the file only needs to be added once, then consider having a boolean variable to flag if it has been added or not.
Including additional UI components
Additional UI components can be added to iDevices with relative ease. There are however several specific pieces of code that must be added in several places for it to work correctly.
Depending on the element that you wish to add, you must import the relevant field object:
from exe.engine.field import TextAreaField, TextField
Within the __init__ method you must then create an instance of that object:
self.titleText = TextField(_(u"Title"), _(u"The title for your exercise"), "") self.titleText.idevice = self
titleText is the variable name that you define yourself.
In the block you must import the matching element object:
from exe.engine.field import TextAreaField, TextField
Within the __init__ method you create an instance of this object, and associate it with the idevice field:
self.titleText = TextElement(idevice.titleText)
In the process block, you must add code to catch a submission of the form (when the user has finished editing), and save the data.
titleText = self.titleText.process(request) if titleText: self.idevice.titleText = titleText
In each render function you must now add code to display the component. These calls generally match the name of the render function, for example in the renderEdit method you would use the call:
html += self.titleText.renderEdit()
Dynamically adding UI components
In some situations you may need a varying number of UI components; for example in the supplied MCQ iDevice you can create additional question and option fields as required. The method for doing this is again quite straight-forward, so long as all the pieces are in-place. The general method of doing this is to have an array in both the idevice and block files, to which new objects are added once created.
In the __init__ method define an array to hold the fields:
self.pairs = 
Include a method that will add objects into this array; for example:
def addPair(self): """ Add a new question to this iDevice. """ pair = TextField(_(u"Pairs"), _(u"Add the matching pairs here"), "") self.pairs.append(pair)
Rather than going through the code in the order that it appears in the file, we will go through it in the order that it is processed. This will hopefully make it easier to understand the purpose of each part.
In the __init__ method define an array to hold the elements:
self.pairElements = 
In the renderEdit method, you will need to include an option for adding a new UI element (be sure to import the common library from exe.webui):
html += common.submitButton("addPair"+unicode(self.id), "Add a pair")
This will display a button during edit, which when clicked will send an addPair request to the server (eXe runs as a paired browser and server). To catch this call, add the code below to the process method.
if ("addPair"+unicode(self.id)) in request.args: self.idevice.addPair() self.idevice.edit = True
Returning to the __init__ method, we will add code to create the element objects as required (add underneath the previously added array code: self.pairElements = ).
for pairs in idevice.pairs: self.pairElements.append(TextElement(pairs))
To display elements once they have been created you will need to loop through the array. The example below would be added to the renderEdit method:
for element in self.pairElements: html += element.renderEdit() html += "<br/>"
If you add the code up to this point, then the elements would appear when editing, but their contents would not be saved. The method to resolve this is similar to that used for static elements. Add the following code to the process method:
for element in self.pairElements: element.process(request)
Utilising a Flash front-end
The Flash movie will be embedded using code in the format:
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0" width="550" height="400"> <param name="play" value="false" /> <param name="scale" value="exactfit" /> <param name="src" value="resources/flashmovie.swf?params" /> <param name="width" value="550" /> <param name="height" value="400" /> <embed type="application/x-shockwave-flash" play="false" scale="exactfit" src="resources/flashmovie.swf?params" width="550" height="400"></embed> </object>
The important part is the emboldened text params. It is here that values can be passed to the flash movie, in the format:
You would use this format when constructing the html string in the render methods. For example:
html += '<param name="src" value="resources/dragdrop2.swf?' swfRef = 'title=Test title' for i,x in enumerate(self.txtElements): swfRef += '&it%d=%s' % (i,x.renderView()) swfRef = swfRef.replace(" ","%20") html += swfRef
The replace function is important, since the appended string is technically a URL, and so must conform that that standard. In the example above spaces are replaced with %20. If you are expecting other non-allowed characters, then you should replace those as well.
Note that there are two locations where the parameter code should be appended, and that the Flash movie must be added to the resources folder (see Adding Resources).
In Flash (AS3) this information can be accessed through:
You can now use these values to configure your Flash front-end, for example a pair-matching drag and drop exercise (see screen shot below).