2012-06-19
What is ipsative assessment and why would I use it? | Getting Results -- The Questionmark Blog
Having recently registered for the eAssessment Scotland conference I was reminded that last year I learnt a new word there: ipsative assessment.
This is a nice summary on the subject from John Kleeman.
'via Blog this'
2012-06-15
Viewing OData $metadata with XSLT
In a recent post I talked about work I've been doing on OData. See Atom, OData and Binary Blobs for a primer on Atom and some examples of OData feeds.
In a nutshell, OData adds a definition language for database-like Entities (think SQL tables) to Atom and provides conventions for representing their properties (think SQL columns) in XML. Their definition language is based on ADO.NET, yes they could have chosen different bindings which would have made more work for their engineers, less for the rest of us and improved the chances of widespread adoption. But it is what it is (a phrase I seem to be hearing a lot of recently).
One of the OData-defined conventions is that data services can publish a metadata document which describes the entities that you are likely to encounter in the Atom feeds it publishes. This can be useful if you want to POST a new entry and you don't know what type of data property X is supposed to have. To get the metadata document you just get a special URL, for the sample data service published by Microsoft:
http://services.odata.org/OData/OData.svc/$metadata
To see what this might look like in the real world you can also look at the $metadata document published as part of the Netflix OData service I used as the source of my examples last time.
http://odata.netflix.com/v2/Catalog/$metadata
Wouldn't it be nice to have a simple documented form of this information? The schema on which it is based even allows annotations and Documentation elements. I browsed the web a bit but couldn't find anyone who had done this so I wrote a little XSLT myself. Here is a picture of part of the output from transforming the Netflix feed
Now there is an issue here. One of the things that I've commented on before is the annoying habit of specification writers to change the namespace they use when a new version is published. I can see why some people might do this but when 90% of the spec is the same it just causes frustration as tools which look for one namespace have to have significant revisions just to work with a minor addition of some optional elements.
As a result, I've published two versions of the XSLT that I used to create the above picture. If somebody out there knows how I can do all of this in one XSL file without lots of copying and pasting I'd love to know.
The first xslt uses the namespace from CSDL 1.1 and can be used to transform the metadata from the sample OData service published by Microsoft. The second xslt uses the namespace from CSDL 2.0 and must be used to transform the metadata from Netflix. If you get a blank HTML file with just a heading then try the other one. When clicking on these links your browser may attempt to render them as HTML, you can "View Source" to see the XSLT as plain text.
Here is how I've used these files on my Mac:
$ curl http://services.odata.org/OData/OData.svc/\$metadata > sample.xml $ xsltproc odata2html-v1p1.xsl sample.xml > sample.html $ curl http://odata.netflix.com/v2/Catalog/\$metadata > netflix.xml $ xsltproc odata2html-v2.xsl netflix.xml > netflix.html
This transform could obviously be improved a lot, it only shows you the Entities, Complex Types and Associations though I am rather proud of the hyperlinking between them.
2012-06-07
Launching learning activities with AICC CMI-5: GET or POST?
With the AICC working on a new specification to update the existing use of the CMI data model and replace the ageing (but still popular!) HACP it seems like a good opportunity to fix the concept of launch via a web browser to harmonise learning technology standards with best practice in mainstream applications. By the way, if you are curious HACP is defined in the 1993 document: [CMI001] CMI Guidelines for Interoperability AICC.
The Learning Management System (LMS) typically does the launch by creating a special page that is delivered to the user's web browser containing a form (or a naked hyperlink with query parameters) to trigger the activity in a remote system. There are some subtle things going on here because best practice in HTML has been based on the assumption that the server that generated the form is the one that will receive the submission whereas our community tends to rely on cross-site form submission (XSFS perhaps?).
Anyway, you can read my recommendation to the AICC in the open forum created by them to discuss the CMI-5 draft specification. And of course, if you haven't had a look yet I'd encourage you to have a quick look over the specification yourself. The more people review and comment on these documents the better.
Link: Topic: 8.1 Web (Browser) Environment
The topic I just started with my new proposal on how the LMS should launch a learning activity in future. I'm recommending that the POST method is used with GET supported for backwards compatibility only so if you support AICC standards please read and review my proposal because you might have to do some work on your launch code to support CMI-5 if my proposal is accepted.
Link: CMI-5 Forum Home Page
The home page of the AICC CMI-5 public forum, see all threads about the proposed specification here. Note that only registered users can see the interactive specification viewer in the forums but there is a general public download on the main CMI-5 page:
Link: CMI-5 Home Page
You can download the full specification as a PDF from here.
2012-06-05
Lion, wxPython and py2app: targeting Carbon & Cocoa
About this time last year I wrote a blog entry on installing wxPython and py2app on my Mac running Snow Leopard. Well I have since upgraded to Lion and the same installation seemed to keep working just fine. This weekend I've actually upgraded to a new Mac and I thought this would be an excellent opportunity to grapple with this installation again and perhaps advance my understanding of what I'm doing.
Apple's Migration Assistant (a.k.a. Setup Assistant) had other ideas. You have to hand it to them, it took about 2-3 hours with the two machines plugged together and everything was copied across and working without any intervention. They really do make it easy to buy a new Mac and get productive straight away.
So is this blog post the Python equivalent of the "pass" statement? Just a no-op?
Well not quite. At the moment, I'm building my QTI migration tool using the Carbon version of wxPython which means forcing Python to work in 32bit mode. That's getting a bit out-dated now and I surely can't take advantage of my new 8GB Mac? I need to embrace 64bit builds, I need to figure out how to build for the Cocoa version of wxPython and I need to figure out how to do this while retaining my ability to create the 32-bit build for older hardware/versions of Mac OS.
So here is how I now recommend doing this...
Step 1: Install Python
The lesson from last time was that you can't rely on the python versions installed by Apple to do these builds for you. If you run python on a clean Lion install you'll get a 64-bit version of python 2.7.1:
$ which python /usr/bin/python $ python Python 2.7.1 (r271:86832, Jul 31 2011, 19:30:53) [GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import sys;"%X"%sys.maxsize '7FFFFFFFFFFFFFFF'
Thanks to How to tell if my python shell is executing in 32bit or 64bit mode? for the tip on printing maxsize.
I want to keep python pointing here because the command-line version of the migration tool, and the supporting Pyslet package (which can be used independently) need to work 'out-of-the-box'. The Migration Assistant had helpfully copied over my .bash_profile which included modifications made by my custom Mac binary install of Python 2.7 last year. The modifications are well commented and help to explain the different paths we'll be dealing with:
# Setting PATH for Python 2.7
# The orginal version is saved in .bash_profile.pysave
PATH="/Library/Frameworks/Python.framework/Versions/2.7/bin:${PATH}"
export PATH
Firstly, note that the Mac binaries install in /Library/Frameworks/ whereas the pre-loaded Apple installations are all in /System/Library/Frameworks/. This is a fairly subtle difference so a little bit of concentration is required to prevent mistakes. Anyway, as per the instructions above I restored my .bash_profile from .bash_profile.pysave and confirmed (as above) that I was getting the Apple python.
It seems like 2.7.3 is the latest version available as a Mac binary build from the main python download page. This will make it a bit easier to check I'm running the right interpreter! So I downloaded the dmg from the following link and ran the installer: http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.6.dmg. For me this was an upgrade rather than a clean install. The resulting binaries are put on the path in /usr/local/bin. By default, the interpreter runs in 64bit mode but it can be invoked in 32-bit mode too:
$ /usr/local/bin/python Python 2.7.3 (v2.7.3:70274d53c1dd, Apr 9 2012, 20:52:43) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import sys;"%X"%sys.maxsize '7FFFFFFFFFFFFFFF'
$ which python-32 /usr/local/bin/python-32 $ python-32 Python 2.7.3 (v2.7.3:70274d53c1dd, Apr 9 2012, 20:52:43) [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import sys;"%X"%sys.maxsize '7FFFFFFF'
This might be enough, but the wxPython home page does recommend that you use different python installations if you want to run both the Carbon and Cocoa versions. So I'll repeat the installation with a python 2.6 build. The current binary build is 2.6.6, this is missing some important security fixes that have been included in 2.6.8 but it looks safe for the migration tool. I downloaded the 2.6 installer from here. When I ran the installer I made sure I only installed the framework, I don't want everything else getting in the way.
I now have a python 2.6 installation in /Library/Frameworks/Python.framework/Versions/2.6
$ /Library/Frameworks/Python.framework/Versions/2.6/bin/python2.6 Python 2.6.6 (r266:84374, Aug 31 2010, 11:00:51) [GCC 4.0.1 (Apple Inc. build 5493)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import sys;"%X"%sys.maxsize '7FFFFFFF'
Notice that this is a 32-bit build. Apple actually ship a 64-bit build of 2.6.7 so care will be needed, typing python2.6 at the terminal will not bring up this new installation.
To make life a little bit easier I always create a bin directory in my home directory and add it to the path in my .bash_profile using lines like these:
PATH=~/bin:${PATH}
export PATH
This is going to come in very handy in the next step.
Step 2: setuptools and easy_install
setuptools is required by lots of Python packages, it is designed to make your life very easy but it takes a bit of fiddling to get it working with these custom installations. It's an egg which means it runs magically from the command line, I'll show you the process of installing it on python 2.6 but the instructions for putting it in 2.7 are almost identical (it's just a different egg).
I downloaded the egg from here: http://pypi.python.org/packages/2.6/s/setuptools/setuptools-0.6c11-py2.6.egg#md5=bfa92100bd772d5a213eedd356d64086 and then took a peek at the top of the script:
$ head -n 8 setuptools-0.6c11-py2.6.egg
#!/bin/sh
if [ `basename $0` = "setuptools-0.6c11-py2.6.egg" ]
then exec python2.6 -c "import sys, os; sys.path.insert(0, os.path.abspath('$0')); from setuptools.command.easy_install import bootstrap; sys.exit(bootstrap())" "$@"
else
echo $0 is not the correct name for this egg file.
echo Please rename it back to setuptools-0.6c11-py2.6.egg and try again.
exec false
fi
I've only shown the top 8 lines here, the rest is binary encoded gibberish. The thing to notice is that, on line 3 it invokes python2.6 directly so if I want to control which python installation setuptools is installed for I need to ensure that python2.6 invokes the correct interpreter. That's where my local bin directory and path manipulation comes in handy.
$ cd ~/bin $ ln -s /Library/Frameworks/Python.framework/Versions/2.6/bin/python2.6 python2.6 $ ln -s /Library/Frameworks/Python.framework/Versions/2.6/bin/python2.7 python2.7
Now for me, and anyone who inherits my $PATH, invoking python2.6 will start my custom MacPython install.
$ sudo -l python2.6 /Users/swl10/bin/python2.6
Fortunately sudo is configured to inherit my environment. It was worth checking as this is configurable. I can now install setuptools from the egg:
$ sudo sh setuptools-0.6c11-py2.6.egg Password: [I had to type my root password here] Processing setuptools-0.6c11-py2.6.egg Copying setuptools-0.6c11-py2.6.egg to /Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages ...
I did the same for python 2.7 (note that you need a different egg) and then added links to easy_install to my bin directory:
$ cd ~/bin ln -s /Library/Frameworks/Python.framework/Versions/2.7/bin/easy_install easy_install-2.7 ln -s /Library/Frameworks/Python.framework/Versions/2.6/bin/easy_install easy_install-2.6
Step 3: wxPython
wxPython is a binary installer tied to a particular python version. However, I believe it uses a scatter gun approach to search for python installations and will install itself everywhere with a single click. That is the reason why it is better to run completely different versions of python if you want completely different versions of wxPython. In fact, if you find yourself with multiple installs there is a wiki page that explains how to switch between versions. But a little playing reveals that this refers to Major.Minor version numbers. It can't cope with the subtlety of switching between builds or switching between Carbon and Cocoa as far as I can tell so this won't help us.
My plan is to install the Carbon wxPython (which is 32bit only) for python 2.6 and the newer Cocoa wxPython for python 2.7. The wxPython download page has a stable and unstable version but to get Cocoa I'll need to use the unstable version. The stability referred to is that of the API, rather than the quality of the code. Being cautious I downloaded the stable 2.8 (Carbon) installer for python 2.6 and the unstable 2.9 Cocoa installer for python 2.7. Installation is easy but look out for a useful script on the disk image which allows you to review and manage your installations. To invoke the script you can just run it from the command line:
$ /Volumes/wxPython2.9-osx-2.9.3.1-cocoa-py2.7/uninstall_wxPython.py
When I was done with the installations it reported the following configurations as being present:
1. wxPython2.8-osx-unicode-universal-py2.6 2.8.12.1 2. wxPython2.9-osx-cocoa-py2.7 2.9.3.1
(If, like me, you are upgrading from previous installations you may have to clean up older builds here.) At this point I tested my wxPython based programs and confirmed that they were working OK. I was impressed that the Cocoa version seems to work unchanged.
Step 4: py2app
With the groundwork done right, the last step is very simple. The symlinks we put in for easy_install make it easy to install py2app.
$ sudo easy_install-2.6 -U py2app Password: [type your root password here] Searching for py2app Reading http://pypi.python.org/simple/py2app/ Reading http://undefined.org/python/#py2app Reading http://bitbucket.org/ronaldoussoren/py2app Best match: py2app 0.6.4 ...[snip]... Installed /Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/altgraph-0.9-py2.6.egg Finished processing dependencies for py2app
The plot is very similar for python 2.7.
I've now added the following to my setup script:
import wx if "cocoa" in wx.version(): suffix="-Cocoa" else: suffix="-Carbon"
The suffix is then appended to the name passed to the setup call itself. The following command results in a 32bit, Carbon binary, compatible with OS X 10.3 onwards.
$ python2.6 setup.py py2app
While this command creates a Cocoa based 64bit binary for 10.5 and later.
$ python2.7 setup.py py2app
And that is how to target both Carbon and Cocoa in your wxPython projects.
2012-06-01
Atom, OData and Binary Blobs
I've been doing a lot of work on Atom and OData recently. I'm a real fan of Atom and the related Atom Publishing Protocol (APP for short). OData is a specification from Microsoft which builds on these two basic building blocks of the internet to provide standard conventions for querying feeds and representing properties using a SQL-like model.
Given that OData can be used to easily expose data currently residing in SQL databases it is not surprising that the issue of binary blobs is one that takes a little research to figure out. At first sight it isn't obvious how OData deals with them, in fact, it isn't even obvious how APP deals with them!
Atom Primer
Most of us are familiar with the idea of an RSS feed for following news articles and blogs like this one (this article prompted me to add the gadget to my blogger templates to make it easier to subscribe). Atom is a slightly more formal definition of the same concept and is available as an option for subscribing to this blog too. Understanding the origins of Atom helps when trying to understand the Atom data model, especially if you are coming to Atom from a SQL/OData point of view.
Atom is all about feeds (lists) of entries. The data you want, be it a news article, blog post or a row in your database table is an entry. A feed might be everything, such as all the articles in your blog or all the rows in your database table, or it may be a filtered subset such as all the articles in your blog with a particular tag or all the rows in your table that match a certain query.
Atom adheres closely to the REST-based service concept. Each entry has its own unique URI. Feeds also have their own URIs. For example, the Atom feed URL for this blog is:
http://swl10.blogspot.com/feeds/posts/default
But if you are only interested in the Python language then you might want to use a different feed:
http://swl10.blogspot.com/feeds/posts/default/-/Python
Obviously the first feed contains all the entries in the second feed too!
Atom is XML-based so an entry is represented by an <entry> element and the content of an entry is represented by a <content> child element. Here's an abbreviated example from this blog's Atom feed. Note that the atom-defined metadata elements appear as siblings of the content...
<entry>
<id>tag:blogger.com,1999:blog-8659912959976079554.post-4875480159917130568</id>
<published>2011-07-17T16:00:00.000+01:00</published>
<updated>2011-07-17T16:00:06.090+01:00</updated>
<category scheme="http://www.blogger.com/atom/ns#" term="QTI"/>
<category scheme="http://www.blogger.com/atom/ns#" term="Python"/>
<title type="text">Using gencodec to make a custom character mapping</title>
<content type="html">One of the problems I face...</content>
<link rel="edit" type="application/atom+xml"
href="http://www.blogger.com/feeds/8659912959976079554/posts/default/4875480159917130568"/>
<link rel="self" type="application/atom+xml"
href="http://www.blogger.com/feeds/8659912959976079554/posts/default/4875480159917130568"/>
</entry>
For blog articles, this content is typically html text (yes, horribly escaped to allow it to pass through XML parsers). Atom actually defines three types of native content, 'html', 'text' and 'xhtml'. It also allows the content element to contain a single child element corresponding to other XML media types. OData uses this method to represent the property name/value pairs that might correspond to the column names and values for a row in the database table your are exposing.
Here's another abbreviated example taken from the Netflix OData People feed:
<entry>
<id>http://odata.netflix.com/v2/Catalog/People(189)</id>
<title type="text">Bruce Abbott</title>
<updated>2012-06-01T07:55:17Z</updated>
<category term="Netflix.Catalog.v2.Person" scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type="application/xml">
<m:properties>
<d:Id m:type="Edm.Int32">189</d:Id>
<d:Name>Bruce Abbott</d:Name>
</m:properties>
</content>
</entry>
Notice the application/xml content type and the single properties element from Microsoft's metadata schema.
Any other type of content is considered to be external media. But Atom can still describe it, it can still associate metadata with it and it can still organize it into feeds...
Binary Blobs as Media
There is nothing stopping the content of an entry from being a non-text binary blob of data. You just change the type attribute to be your favourite blob format and add a src attribute to point to an external file or base-64 encode it and include it in the entry itself (this second method is rarely used I think).
Obviously the URL of the entry (the XML document containing the <entry> tag) is not the same as the URL of the media resource, but they are closely related. The entry is referred to as a Media Link because it contains the metadata about the media file (such as the title, updated date etc) and it links to it. The media file itself is known as a media resource.
There's a problem with OData though. OData requires the child of the content element to be the properties element (see example above) and the type attribute to be application/xml. But Atom says there can only be one content element per entry. So how can OData be used for binary blobs?
The answer is a bit of a hack. When the entry is a media link entry the properties move into the metadata area of the entry. Here's another abbreviated example from Netflix which illustrates the technique:
<entry>
<id>http://odata.netflix.com/v2/Catalog/Titles('13aly')</id>
<title type="text">Red Hot Chili Peppers: Funky Monks</title>
<summary type="html">Lead singer Anthony Kiedis...</summary>
<updated>2012-01-31T09:45:16Z</updated>
<author>
<name />
</author>
<category term="Netflix.Catalog.v2.Title"
scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme" />
<content type="image/jpeg" src="http://cdn-0.nflximg.com/en_us/boxshots/large/5632678.jpg" />
<m:properties xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices">
<d:Id>13aly</d:Id>
<d:Name>Red Hot Chili Peppers: Funky Monks</d:Name>
<d:ShortName>Red Hot Chili Peppers: Funky Monks</d:ShortName>
<d:Synopsis>Lead singer Anthony Kiedis...</d:Synopsis>
<d:ReleaseYear m:type="Edm.Int32">1991</d:ReleaseYear>
<d:Url>http://www.netflix.com/Movie/Red_Hot_Chili_Peppers_Funky_Monks/5632678</d:Url>
<!-- more properties.... -->
</m:properties>
</entry>
This entry is taken from the Titles feed, notice that the entry is a media-link to the large box graphic for the film.
Binary Blobs and APP
APP adds a protocol for publishing information to Atom feeds and OData builds on APP to allow data feeds to be writable, not just read-only streams. You can't upload your own titles to Netflix as far as I know so I don't have an example here. The details are all in section 9.6 of RFC 5023 but in a nutshell, if you post a binary blob to a feed the server should store the blob and create a media link entry that points to it (populated with a minimal set of metadata). Once created, you can then update the metadata with HTTP's PUT method on the media link's edit URI directly, or update the binary blob by using HTTP's PUT method on the edit-media URI of the media resource. (These links are given in the <link> elements in the entries, see the first example for examples.)
There is no reason why binary blobs can't be XML files of course. Many of the technical standards for education that I work with are very data-centric. They define the format of XML documents such as QTI, which are designed to be opaque to management systems like item banks (an item bank is essentially a special-purpose content management system for questions used in assessment).
So publishing feeds using OData or APP from an item bank would most likely use these techniques for making the underlying content available to third party systems. Questions often contain media resources (e.g., images) of course but even the question content itself is typically marked up using XML, as it is in QTI. This data is not easy to represent as a simple list of property values and would typically be stored as a blob in a database or as a file in a repository. Therefore, it is probably better to think of this data as a media resource when exposing it via APP/OData.
2012-05-23
Standards Working Together: An e-Learning Perspective - HR-XML Consortium - Leading the Development of HR Integration Standards
As part of last week's whirlwind tour of the standards bodies I wrote this guest post on the HR-XML blog.
'via Blog this'
2012-05-22
Common Cartridge, Namespaces and Dependency Injection
This post is about coping with a significant change to the newer (public) versions of the IMS Common Cartridge specification. This change won't affect everyone the same way, your implementation may just shrug it off. However, I found I had to make an important change to the QTI migration tool code to make it possible to read QTI version 1 files from the newer form of cartridges.
There have been three versions of this specification now, versions 1.0, 1.1 and most recently version 1.2. The significant change for me was between versions 1.0 (published October 2008) and 1.1 (revised May 2011).
Changing Namespaces
The key change between 1.0 and 1.1 was to the namespaces used in the XML files. In version 1.0, the default namespace for content packaging elements is used in the manifest file: http://www.imsglobal.org/xsd/imscp_v1p1.
Content Packaging has also been through several revisions. The v1p1 namespace (above) was defined in the widely used Content Packaging 1.1 (now on revision 4). The same namespace was used for most of the elements in the (public draft) of the newer IMS Content Packaging version 1.2 specification too. In this case, the decision was made to augment the revised specification with a new schema containing definitions of the new elements only. The existing elements would stay in the 1.1 namespace to ensure that tools that recognise version 1.1 packages continue to work, ignoring the unrecognised extension elements.
Confusingly though, the schema definition provided with the content packaging specification is located here: http://www.imsglobal.org/xsd/imscp_v1p1.xsd whereas the schema definition provided with the common cartridge specification (1.0), for the same namespace, is located here: http://www.imsglobal.org/profile/cc/ccv1p0/derived_schema/imscp_v1p2.xsd. That's two different definition files for the same namespace. Given this discrepancy it is not surprising that newer revisions of common cartridge have chosen to use a new namespace entirely. In the case of 1.1, the namespace used for the basic content packaging elements was changed to http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1.
But this decision is not without consequences. The decision to retain a consistent namespace in the various revisions of the Content Packaging specification enabled existing tools to continue working. Sure enough, the decision to change the namespace in Common Cartridge means that some tools will not continue working. Including my Python libraries used in the QTI migration tool.
From Parser to Python Class
In the early days of XML, you could identify an element within a document by its name, scoped perhaps by the PUBLIC identifier given in the document type definition. The disadvantage being that all elements had to be defined in the same scope. Namespace prefixes were used to help sort this mess out. A namespace aware parser splits off the namespace prefix (everything up to the colon) from the element name and uses it to identify the element by a pair of strings: the namespace (a URI) and the remainder of the element name.
The XML parser at the heart of my python libraries uses these namespace/name pairs as keys into a dictionary which it uses to look up the class object it should use to represent the element. The advantage of this approach is that I can add behaviour to the XML elements when they are deserialized from their XML representations through the methods defined on the corresponding classes. Furthermore, a rich class hierarchy can be defined allowing concepts such as XHTML's organization of elements into groups like 'inline elements' to be represented directly in the class hierarchy.
If I need two different XML definitions to map to the same class I can easily do this by adding multiple entries to the dictionary and mapping them to the same class. So at first glance I seem to have avoided some of the problems inherent with tight-coupling of classes. The following two elements could be mapped to the same Manifest class in my program:
('http://www.imsglobal.org/xsd/imscp_v1p1', 'manifest')
('http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1', 'manifest')
This would work fine when reading the manifest from the XML stream but what about writing manifests? How does my Manifest class know which namespace to use when I'm creating a new manifest? The following code snippet from the python interpreter shows me creating an instance of a Manifest (I pass None as the element's parent). The instance knows which namespace it should be in:
>>> import pyslet.imscpv1p2 as cp >>> m=cp.Manifest(None) >>> print m <manifest xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"> <organizations/> <resources/> </manifest>
This clearly won't work for the new common cartridges. The Manifest class 'knows' the namespace it is supposed to be in because its canonical XML name is provided as a class attribute on its definition. The obvious solution is to wrap the class with a special common cartridge Manifest that overrides this attribute. That is relatively easy to do, here is the updated definition:
class Manifest(cp.Manifest):
XMLNAME=("http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1",'manifest')
Unfortunately, this doesn't do enough. Continuing to use the python interpreter....
>>> class Manifest(cp.Manifest):
... XMLNAME=("http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1",'manifest')
...
>>> m=Manifest(None)
>>> print m
<manifest xmlns="http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1">
<organizations xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"/>
<resources xmlns="http://www.imsglobal.org/xsd/imscp_v1p1"/>
</manifest>
Now we've got the namespace correct on the manifest but the required organizations and resources elements are still created in the old namespace.
The Return of Tight Coupling
If I'm going to fix this issue I'm going to have to wrap the classes used for all the elements in the Content Packaging specification. That sounds like a bit of a chore but remember that the reason why the namespace has changed is because Common Cartridge has added some additional constraints to the specification so we're likely to have to override at least some of the behaviours too.
Unfortunately, wrapping the classes still isn't enough. In the above example the organizations and resources elements are required children of the manifest. So when I created my instance of the Manifest class the Manifest's constructor needed to create instances of the related Organizations and Resources classes and it does this using the default implementations, not the wrapped versions I've defined in my Common Cartridge module. This is known as tight coupling, and the solution is to adopt a dependency injection solution. For a more comprehensive primer on common solutions to this pattern you could do worse than reading Martin Fowler's article Inversion of Control Containers and the Dependency Injection pattern.
The important point here is that the logic inside my Manifest class, including the logic that takes place during construction, needs to be decoupled from the decision to use a particular class object to instantiate the Organizations and Resources elements. These dependencies need to be injected into the code somehow.
I must admit, I find the example solutions in Java frameworks confusing because the additional coding required to satisfy the compiler makes it harder to see what is really going on. There aren't many good examples of how to solve the problem in python. The python wiki points straight to an article called Dependency Injection The Python Way. But this article describes a full feature broker (like the service locator solution) which seems like overkill for my coupling problem.
A simpler solution is to pass dependencies in (in my case on the constructor) following a pattern similar to the one in this blogpost. In fact, this poster is trying to solve a related problem of module-level dependeny but the basic idea is the same. I could pass the wrapped class objects to the constructor.
Dependency Injection using Class Attributes
The spirit of the python language is certainly one of adopting the simplest solution that solves the problem. So here is my dependency injection solution to this specific case of tight coupling.
I start by adding class attributes to set class dependencies. My base Manifest class now looks something like this:
class Manifest:
XMLNAME=("http://www.imsglobal.org/xsd/imscp_v1p1",'manifest')
MetadataClass=Metadata
OrganizationsClass=Organizations
ResourcesClass=Resources
# method definitions and other attributes follow...
And in my Common Cartridge module it is overridden like this:
class Manifest(cp.Manifest):
XMLNAME=("http://www.imsglobal.org/xsd/imsccv1p1/imscp_v1p1",'manifest')
MetadataClass=Metadata
OrganizationsClass=Organizations
ResourcesClass=Resources
Although these look similar, in the first case the Metadata, Organizations and Resources names refer to classes in the base Content Packaging module whereas in the second definition they refer to overrides in the Common Cartridge Module (note the use of cp.Manifest to select the base class from the original Content Packaging module).
Now the original Manifest's constructor is modified to use these class attributes to create the required child elements:
def __init__(self,parent):
self.Metadata=None
self.Organizations=self.OrganizationsClass(self)
self.Resources=self.ResourcesClass(self)
The upshot is that when I create an instance of the Common Cartridge Manifest I don't need to override the constructor just to solve the dependency problem. The base class constructor will now create the correct Organizations and Resources members using the overridden class attributes.
I've abbreviated the code a bit, if you want to see the full implementation you can see it in the trunk of the pyslet framework.

