diff --git a/scorm/README.txt b/scorm/README.txt new file mode 100644 index 0000000..d479f49 --- /dev/null +++ b/scorm/README.txt @@ -0,0 +1,40 @@ +=================== +A generic SCORM API +=================== + + ($Id$) + +In order to work with the SCORM API we first need a tracking storage. + + >>> from cybertools.tracking.btree import TrackingStorage + >>> tracks = TrackingStorage() + +We can now create a ScormAPI adapter. Note that this adapter is stateless +as it is usually created anew upon each request. + + >>> from cybertools.scorm.base import ScormAPI + >>> api = ScormAPI(tracks, 'a001', 0, 'user1') + +The first step is always the initialize() call - though in our case it +does not do anything. + + >>> api.initialize() + '0' + +Then we can set some values. + + >>> rc = api.setValue('cmi.interactions.0.id', 'q007') + >>> rc = api.setValue('cmi.interactions.0.result', 'correct') + >>> rc = api.setValue('cmi.comments_from_learner', 'Hello SCORM') + >>> rc = api.setValue('cmi.interactions.1.id', 'q009') + >>> rc = api.setValue('cmi.interactions.1.result', 'incorrect') + +Depending on the data elements the values entered are kept together in +one track or stored in separate track objects. So there is a separate +track for each interaction and one track for all the other elements. + + >>> for t in sorted(tracks.values(), key=lambda x: x.timeStamp): + ... print t.data + {'id': 'q007', 'key_prefix': 'cmi.interactions.0', 'result': 'correct'} + {'cmi.comments_from_learner': 'Hello SCORM', 'key_prefix': ''} + {'id': 'q009', 'key_prefix': 'cmi.interactions.1', 'result': 'incorrect'} \ No newline at end of file diff --git a/scorm/__init__.py b/scorm/__init__.py new file mode 100644 index 0000000..4bc90fb --- /dev/null +++ b/scorm/__init__.py @@ -0,0 +1,4 @@ +""" +$Id$ +""" + diff --git a/scorm/base.py b/scorm/base.py new file mode 100644 index 0000000..5ba6484 --- /dev/null +++ b/scorm/base.py @@ -0,0 +1,110 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Base classes for providing a generic SCORM-compliant API. + +$Id$ +""" + +from zope import interface +from zope.interface import implements + +from cybertools.scorm.interfaces import IScormAPI +from cybertools.tracking.btree import TrackingStorage + + +OK = '0' + + +class ScormAPI(object): + """ ScormAPI objects are temporary adapters created by + browser or XML-RPC views. + """ + + implements(IScormAPI) + + def __init__(self, storage, taskId, runId, userId): + self.taskId = taskId + self.runId = runId + self.userId = userId + self.storage = storage + + def initialize(self, parameter=''): + # Note that the run has already been started upon SCO launch, the runId + # usually being part of the URI or XML-RPC call arguments. + return OK + + def terminate(self, parameter=''): + rc = self.commit() + if rc == OK: + self.storage.stopRun(self.taskId, self.runId) + return rc + + def commit(self, parameter=''): + return OK + + def setValue(self, element, value): + tracks = self.storage.getUserTracks(self.taskId, self.runId, self.userId) + prefix, key = self._splitKey(element) + data = self._getTrackData(tracks, prefix) or {} + update = bool(data) + data['key_prefix'] = prefix + data.update({key: value}) + self.storage.saveUserTrack(self.taskId, self.runId, self.userId, data, + update=update) + return OK + + def setValues(self, mapping={}, **kw): + mapping.update(kw) + # TODO: optimize, i.e. retrieve existing tracks only once. + for key, value in mapping: + rc = self.setValue(key, value) + if rc != OK: + return rc + return OK + + def getValue(self, element): + tracks = self.storage.getUserTracks(self.taskId, self.runId, self.userId) + prefix, key = self._splitKey(element) + data = self._getTrackData(tracks, prefix) or {} + if key in data: + return data[key], OK + else: + return '', '403' + + def getErrorString(self, errorCode): + return '' + + def getDiagnostic(self, code): + return '' + + # helper methods + + def _splitKey(self, element): + if element.startswith('cmi.interactions.'): + parts = element.split('.') + return '.'.join(parts[:3]), '.'.join(parts[3:]) + return '', element + + def _getTrackData(self, tracks, prefix): + track = None + for tr in reversed(sorted(tracks, key=lambda x: x.timeStamp)): + if tr and tr.data.get('key_prefix', None) == prefix: + return tr.data + return {} diff --git a/scorm/interfaces.py b/scorm/interfaces.py new file mode 100644 index 0000000..039e26b --- /dev/null +++ b/scorm/interfaces.py @@ -0,0 +1,96 @@ +# +# Copyright (c) 2007 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +SCORM interface definitions for API_1484_11. + +$Id$ +""" + +from zope.interface import Interface, Attribute +from zope import schema + + +class IScormAPI(Interface): + """ This interface represents a server-side adapter object for a + tracking storage and a set of key/meta data that identify a + learner session with one or more track objects. IScormAPI objects + are stateless, so they don't remember any values between calls. + + In addition to the standard SCORM RTS methods there is a setValues() + method that allows setting more than one value in one call, + probably during execution of a Commit() call on the client + side. + + There is no method corresponding to GetLastError() as the + methods immediately return an appropriate CMIErrorCode, + i.e. a '0' when OK. + + Note that the names of the methods have been slightly modified + to correspond to the Python programming style guides. + """ + + taskId = Attribute('Task ID') + runId = Attribute('Run ID (integer)') + userId = Attribute('User ID') + + def initialize(parameter): + """ Corresponds to API.Initialize(''). + Return CMIErrorCode. + """ + + def commit(parameter): + """ Corresponds to API.Commit(''). + Return CMIErrorCode. + """ + + def terminate(parameter): + """ Corresponds to API.Initialize(''). + Mark the run as finished. + Return CMIErrorCode. + """ + + def setValue(element, value): + """ Corresponds to API.SetValue(element, value). + Return CMIErrorCode. + """ + + def setValues(mapping={}, **kw): + """ Combine the mapping and kw arguments setting up a series of + element-value mappings that will in turn be applied to a + series of setValue() calls. + Return CMIErrorCode. + """ + + def getValue(element): + """ Corresponds to API.GetValue(element). + Return a tuple with the current value of the element given + (a string, '' if not present) and a CMIErrorCode. + """ + + def getErrorString(errorCode): + """ Corresponds to API.GetErrorString(errorCode). + Return the error text belonging to the errorCode + (a CMIErrorCode value) given. + """ + + def getDiagnostic(code): + """ Corresponds to API.GetDiagnostic(code). + Return an LMS-specific information text related to the code given; + code may but need not be a CMIErrorCode value. + """ diff --git a/scorm/tests.py b/scorm/tests.py new file mode 100755 index 0000000..87e6e46 --- /dev/null +++ b/scorm/tests.py @@ -0,0 +1,22 @@ +# $Id$ + +import unittest, doctest +from zope.testing.doctestunit import DocFileSuite + + +class Test(unittest.TestCase): + "Basic tests for the cybertools.scorm package." + + def testBasics(self): + pass + + +def test_suite(): + flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + return unittest.TestSuite(( + unittest.makeSuite(Test), + DocFileSuite('README.txt', optionflags=flags), + )) + +if __name__ == '__main__': + unittest.main(defaultTest='test_suite')