added explicit 'run' management, will basis for SCORM-compliant API, e.g. for yeepa

git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@1705 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2007-05-01 09:04:42 +00:00
parent b79c15740d
commit 747e65e265
3 changed files with 117 additions and 20 deletions

View file

@ -6,6 +6,11 @@ User tracking in the loops framework
>>> from cybertools.tracking.btree import TrackingStorage >>> from cybertools.tracking.btree import TrackingStorage
Let's create a tracking storage and store a few tracks in it. A track
is basically an arbitrary mapping. (In the following examples we
ignore the ``run`` argument and use a 0 value for it; thus we are just
working with the current run of a task.)
>>> tracks = TrackingStorage() >>> tracks = TrackingStorage()
>>> tracks.saveUserTrack('a001', 0, 'u1', {'somekey': 'somevalue'}) >>> tracks.saveUserTrack('a001', 0, 'u1', {'somekey': 'somevalue'})
'0000001' '0000001'
@ -21,6 +26,10 @@ User tracking in the loops framework
>>> [str(id) for id in tracks.getTaskIds()] >>> [str(id) for id in tracks.getTaskIds()]
['a001'] ['a001']
We can query the tracking storage using the tracks' metadata. These
are mapped to btree indexes, so we get fast access to the resulting
track data.
>>> tracks.query(taskId='a001') >>> tracks.query(taskId='a001')
[<Track ['a001', 1, 'u1', '...-...-... ...:...']: {'somekey': 'somevalue'}>] [<Track ['a001', 1, 'u1', '...-...-... ...:...']: {'somekey': 'somevalue'}>]
@ -38,14 +47,14 @@ What happens if we store more than on record for one set of keys?
>>> [t.data for t in t2] >>> [t.data for t in t2]
[{'somekey': 'somevalue'}, {'somekey': 'newvalue'}] [{'somekey': 'somevalue'}, {'somekey': 'newvalue'}]
It is also possible to retrieve the last entry for a set of keys directliy: It is also possible to retrieve the last entry for a set of keys directly.
>>> tracks.getLastUserTrack('a001', 0, 'u1') >>> tracks.getLastUserTrack('a001', 0, 'u1')
<Track ['a001', 1, 'u1', ...]: {'somekey': 'newvalue'}> <Track ['a001', 1, 'u1', ...]: {'somekey': 'newvalue'}>
Instead of creating a new track object for each call one can also replace Instead of creating a new track object for each call one can also replace
an existing one (if present). The replaced entry is always the last one an existing one (if present). The replaced entry is always the last one
for a given set of keys: for a given set of keys.
>>> tracks.saveUserTrack('a001', 0, 'u1', {'somekey': 'newvalue2'}, replace=True) >>> tracks.saveUserTrack('a001', 0, 'u1', {'somekey': 'newvalue2'}, replace=True)
'0000003' '0000003'
@ -63,6 +72,30 @@ The tracks of a tracking store may be reindexed:
>>> tracks.reindexTracks() >>> tracks.reindexTracks()
Runs
----
We may explicitly start a new run for a given task. This will also replace
the task's current run.
>>> tracks.startRun('a001')
3
>>> tracks.saveUserTrack('a001', 0, 'u1', {'k1': 'value1'})
'0000005'
>>> tracks.getLastUserTrack('a001', 0, 'u1')
<Track ['a001', 3, 'u1', ...]: {'k1': 'value1'}>
We still have access to older runs.
>>> tracks.getLastUserTrack('a001', 1, 'u1')
<Track ['a001', 1, 'u1', ...]: {'somekey': 'newvalue2'}>
We can also retrieve a run object with the run's data.
>>> run = tracks.getRun(3)
>>> run
<Run 3, ..., ..., False>
Fin de partie Fin de partie
============= =============

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de # Copyright (c) 2007 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -17,7 +17,7 @@
# #
""" """
BTree-based implementation of user tracking. BTree-based implementation of user interaction tracking.
$Id$ $Id$
""" """
@ -29,10 +29,27 @@ from zope.app.container.btree import BTreeContainer
from zope.index.field import FieldIndex from zope.index.field import FieldIndex
from persistent import Persistent from persistent import Persistent
from BTrees import OOBTree from BTrees import OOBTree, IOBTree
from BTrees.IFBTree import intersection from BTrees.IFBTree import intersection
from interfaces import ITrackingStorage, ITrack from interfaces import IRun, ITrackingStorage, ITrack
class Run(object):
implements(IRun)
id = start = end = 0
finished = False
def __init__(self, id):
self.id = id
def __repr__(self):
return '<Run %s>' % ', '.join((str(self.id),
timeStamp2ISO(self.start),
timeStamp2ISO(self.end),
str(self.finished)))
class TrackingStorage(BTreeContainer): class TrackingStorage(BTreeContainer):
@ -48,6 +65,7 @@ class TrackingStorage(BTreeContainer):
self.indexes = OOBTree.OOBTree() self.indexes = OOBTree.OOBTree()
for idx in self.indexAttributes: for idx in self.indexAttributes:
self.indexes[idx] = FieldIndex() self.indexes[idx] = FieldIndex()
self.runs = IOBTree.IOBTree()
self.currentRuns = OOBTree.OOBTree() self.currentRuns = OOBTree.OOBTree()
self.taskUsers = OOBTree.OOBTree() self.taskUsers = OOBTree.OOBTree()
@ -56,18 +74,36 @@ class TrackingStorage(BTreeContainer):
def startRun(self, taskId): def startRun(self, taskId):
self.runId += 1 self.runId += 1
self.currentRuns[taskId] = self.runId runId = self.runId
return self.runId self.currentRuns[taskId] = runId
run = self.runs[runId] = Run(runId)
run.start = run.end = getTimeStamp()
return runId
def stopRun(self, taskId): def stopRun(self, taskId, runId=0, finish=True):
currentRun = self.currentRuns.get(taskId)
runId = runId or currentRun
if runId and runId == currentRun:
del self.currentRuns[taskId]
run = self.getRun(runId)
if run is not None:
run.end = getTimeStamp()
run.finished = finish
return runId
def getRun(self, runId=0, taskId=None):
if taskId and not runId:
runId = self.currentRuns.get(taskId) runId = self.currentRuns.get(taskId)
if runId: if runId:
del self.currentRuns[taskId] return self.runs.get(runId)
return runId
def saveUserTrack(self, taskId, runId, userName, data, replace=False): def saveUserTrack(self, taskId, runId, userName, data, replace=False):
if not runId: if not runId:
runId = self.currentRuns.get(taskId) or self.startRun(taskId) runId = self.currentRuns.get(taskId) or self.startRun(taskId)
run = self.getRun(runId)
if run is None:
raise ValueError('Invalid run: %i.' % runId)
run.end = getTimeStamp()
trackNum = 0 trackNum = 0
if replace: if replace:
track = self.getLastUserTrack(taskId, runId, userName) track = self.getLastUserTrack(taskId, runId, userName)
@ -79,8 +115,7 @@ class TrackingStorage(BTreeContainer):
self.trackNum += 1 self.trackNum += 1
trackNum = self.trackNum trackNum = self.trackNum
trackId = self.idFromNum(trackNum) trackId = self.idFromNum(trackNum)
timeStamp = int(time.time()) track = Track(taskId, runId, userName, getTimeStamp(), data)
track = Track(taskId, runId, userName, timeStamp, data)
self[trackId] = track self[trackId] = track
self.indexTrack(trackNum, track) self.indexTrack(trackNum, track)
return trackId return trackId
@ -161,3 +196,5 @@ class Track(Persistent):
def timeStamp2ISO(ts): def timeStamp2ISO(ts):
return time.strftime('%Y-%m-%d %H:%M', time.gmtime(ts)) return time.strftime('%Y-%m-%d %H:%M', time.gmtime(ts))
def getTimeStamp():
return int(time.time())

View file

@ -1,5 +1,5 @@
# #
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de # Copyright (c) 2007 Helmut Merz helmutm@cy55.de
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -28,6 +28,16 @@ from zope import schema
# user interaction tracking # user interaction tracking
class IRun(Interface):
""" A set of interactions, sort of a session.
"""
id = Attribute('A unique integer that identifies a run within a tracking storage')
start = Attribute('Timestamp of run creation')
end = Attribute('Timestamp of last interaction or of stopping the run')
finished = Attribute('Boolean that is set to True if run was finished explicitly')
class ITrack(Interface): class ITrack(Interface):
""" Result data from the interactions of a user with an task. """ Result data from the interactions of a user with an task.
""" """
@ -41,16 +51,33 @@ class ITrackingStorage(Interface):
""" """
def startRun(taskId): def startRun(taskId):
""" Creates a new run for the task given and return its id. """ Create a new run for the task given and return its id.
""" """
def stopRun(taskId): def stopRun(taskId, runId=0, finish=True):
""" Remove the current run entry for the task given. """ Stop/finish a run.
If the runId is 0 use the task's current run.
If the run is the task's current one remove it from the set
of current runs.
Set the run's ``finished`` flag to the value of the ``finish``
argument.
Return the real runId.
""" """
def saveUserTrack(taskId, runId, userName, data): def getRun(runId, taskId=None):
""" Return the run object identified by ``runId``. Return None
if there is no corresponding run.
If ``runId`` is 0 and a ``taskId`` is given return the
current run of the task.
"""
def saveUserTrack(taskId, runId, userName, data, replace=False):
""" Save the data given (typically a mapping object) to the user track """ Save the data given (typically a mapping object) to the user track
corresponding to the user name, task id, and run id given. corresponding to the user name, task id, and run id given.
If the runId is 0 use the task's current run.
If the ``replace`` flag is set, the new track replaces the last
one for the given set of keys.
Return the new track item's id.
""" """
def query(**criteria): def query(**criteria):