diff --git a/scorm/README.txt b/scorm/README.txt index c0b0e46..e23b61f 100644 --- a/scorm/README.txt +++ b/scorm/README.txt @@ -37,11 +37,11 @@ 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 additional track for all the other elements. - >>> for t in sorted(tracks.values(), key=lambda x: x.timeStamp): + >>> for t in sorted(tracks.values(), key=lambda x: x.data['recnum']): ... print t.data - {'id': 'q007', 'key_prefix': 'cmi.interactions.0', 'result': 'correct'} - {'cmi.comments_from_learner.comment': 'Hello SCORM', 'key_prefix': ''} - {'id': 'q009', 'key_prefix': 'cmi.interactions.1', 'result': 'incorrect'} + {'recnum': -1, 'cmi.comments_from_learner.comment': 'Hello SCORM'} + {'cmi.interactions.0.id': 'q007', 'cmi.interactions.0.result': 'correct', 'recnum': 0} + {'cmi.interactions.1.result': 'incorrect', 'cmi.interactions.1.id': 'q009', 'recnum': 1} Using the getValue() method we can retrieve certain values without having to care about the storage in different track objects. @@ -69,12 +69,12 @@ We can also query special elements like _count and _children. We may also update existing tracks using the ``setValue()`` method. >>> rc = api.setValue('cmi.comments_from_learner.location', 'q007') - >>> for t in sorted(tracks.values(), key=lambda x: x.timeStamp): + >>> for t in sorted(tracks.values(), key=lambda x: x.data['recnum']): ... print t.data - {'id': 'q007', 'key_prefix': 'cmi.interactions.0', 'result': 'correct'} - {'cmi.comments_from_learner.location': 'q007', - 'cmi.comments_from_learner.comment': 'Hello SCORM', 'key_prefix': ''} - {'id': 'q009', 'key_prefix': 'cmi.interactions.1', 'result': 'incorrect'} + {'recnum': -1, 'cmi.comments_from_learner.comment': 'Hello SCORM', + 'cmi.comments_from_learner.location': 'q007'} + {'cmi.interactions.0.id': 'q007', 'cmi.interactions.0.result': 'correct', 'recnum': 0} + {'cmi.interactions.1.result': 'incorrect', 'cmi.interactions.1.id': 'q009', 'recnum': 1} With the ``setValues()`` method we may set more than one element with one call. (This is not a SCORM-compliant call but is provided for efficiency @@ -86,14 +86,14 @@ XML-RPC call.) ... } >>> rc = api.setValues(data) - >>> for t in sorted(tracks.values(), key=lambda x: x.timeStamp): + >>> for t in sorted(tracks.values(), key=lambda x: x.data['recnum']): ... print t.data - {'id': 'q007', 'key_prefix': 'cmi.interactions.0', 'result': 'correct'} - {'cmi.comments_from_learner.location': 'q007', - 'cmi.comments_from_learner.comment': 'Hello SCORM', 'key_prefix': ''} - {'id': 'q009', 'key_prefix': 'cmi.interactions.1', 'result': 'incorrect'} - {'result': 'correct', 'key_prefix': 'cmi.interactions.2', - 'learner_response': 'my answer'} + {'recnum': -1, 'cmi.comments_from_learner.comment': 'Hello SCORM', + 'cmi.comments_from_learner.location': 'q007'} + {'cmi.interactions.0.id': 'q007', 'cmi.interactions.0.result': 'correct', 'recnum': 0} + {'cmi.interactions.1.result': 'incorrect', 'cmi.interactions.1.id': 'q009', 'recnum': 1} + {'cmi.interactions.2.learner_response': 'my answer', + 'cmi.interactions.2.result': 'correct', 'recnum': 2} >>> api.getValue('cmi.interactions.2.result') ('correct', '0') diff --git a/scorm/base.py b/scorm/base.py index d9041c4..3d3cb0e 100644 --- a/scorm/base.py +++ b/scorm/base.py @@ -33,7 +33,9 @@ from cybertools.tracking.interfaces import ITrackingStorage OK = '0' -_children = { +scormInteractionsPrefixes = ['cmi.interactions.'] + +scormChildren = { 'cmi.comments_from_learner': ('comment', 'location', 'timestamp'), 'cmi.comments_from_lms': ('comment', 'location', 'timestamp'), 'cmi.interactions': ('id', 'type', 'objectives', 'timestamp', @@ -43,10 +45,12 @@ _children = { 'delivery_speed', 'audio_captioning'), 'cmi.objectives': ('id', 'score', 'success_status', 'completion_status', 'description'), - 'score': ('scaled', 'raw', 'min', 'max'), + ('cmi', 'score'): ('scaled', 'raw', 'min', 'max'), } + + class ScormAPI(object): """ ScormAPI objects are temporary adapters created by browser or XML-RPC views. @@ -79,12 +83,12 @@ class ScormAPI(object): def setValue(self, element, value): tracks = self.context.getUserTracks(self.taskId, self.runId, self.userId) - prefix, key = self._splitKey(element) - track = self._getTrack(tracks, prefix) + recnum = self._getRecnum(element) + track = self._getTrack(tracks, recnum) data = track is not None and track.data or {} - data[key] = value + data[element] = value if track is None: - data['key_prefix'] = prefix + data['recnum'] = recnum self.context.saveUserTrack(self.taskId, self.runId, self.userId, data) else: self.context.updateTrack(track, data) @@ -103,23 +107,23 @@ class ScormAPI(object): tracks = self.context.getUserTracks(self.taskId, self.runId, self.userId) if element.endswith('._count'): base = element[:-len('._count')] - if element.startswith('cmi.interactions.'): - return self._countSubtracks(tracks, base), OK - else: - track = self._getTrack(tracks, '') - if track is None: - return 0, OK - return self._countSubelements(track.data, base), OK + for prefix in scormInteractionsPrefixes: + if element.startswith(prefix): + return self._countInteractionTracks(tracks), OK + track = self._getTrack(tracks, -1) + if track is None: + return 0, OK + return self._countSubelements(track.data, base), OK if element.endswith('_children'): base = element[:-len('._children')] return self._getChildren(base) - prefix, key = self._splitKey(element) - track = self._getTrack(tracks, prefix) + recnum = self._getRecnum(element) + track = self._getTrack(tracks, recnum) if track is None: return '', '403' data = track.data - if key in data: - return data[key], OK + if element in data: + return data[element], OK else: return '', '403' @@ -131,15 +135,22 @@ class ScormAPI(object): # helper methods + def _getRecnum(self, element): + for prefix in scormInteractionsPrefixes: + if element.startswith(prefix): + # interaction record + return int(element[len(prefix):].split('.', 1)[0]) + return -1 # base record + def _splitKey(self, element): if element.startswith('cmi.interactions.'): parts = element.split('.') return '.'.join(parts[:3]), '.'.join(parts[3:]) return '', element - def _getTrack(self, tracks, prefix): - for tr in reversed(sorted(tracks, key=lambda x: x.timeStamp)): - if tr and tr.data.get('key_prefix', None) == prefix: + def _getTrack(self, tracks, recnum): + for tr in tracks: + if tr.data['recnum'] == recnum: return tr return None @@ -150,14 +161,19 @@ class ScormAPI(object): result.add(key) return len(result) - def _countSubtracks(self, tracks, base): - return len([tr for tr in tracks if tr.data.get('key_prefix').startswith(base)]) + def _countInteractionTracks(self, tracks): + return len([tr for tr in tracks + if tr.data.get('recnum', -1) >= 0]) def _getChildren(self, base): - if base.endswith('.score'): - base = 'score' - if base in _children: - return _children[base], OK - else: - return '', '401' + if base in scormChildren: + return scormChildren[base], OK + parts = base.split('.') + if len(parts) >= 2: + # this may be somewhat simplistic, but should cover the + # most common cases + key = (parts[0], parts[-1]) + if key in scormChildren: + return scormChildren[key], OK + return '', '401'