diff --git a/tracking/README.txt b/tracking/README.txt index 0810f03..bec1e70 100644 --- a/tracking/README.txt +++ b/tracking/README.txt @@ -6,6 +6,11 @@ User tracking in the loops framework >>> 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.saveUserTrack('a001', 0, 'u1', {'somekey': 'somevalue'}) '0000001' @@ -21,6 +26,10 @@ User tracking in the loops framework >>> [str(id) for id in tracks.getTaskIds()] ['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') [] @@ -38,14 +47,14 @@ What happens if we store more than on record for one set of keys? >>> [t.data for t in t2] [{'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') 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 -for a given set of keys: +for a given set of keys. >>> tracks.saveUserTrack('a001', 0, 'u1', {'somekey': 'newvalue2'}, replace=True) '0000003' @@ -63,6 +72,30 @@ The tracks of a tracking store may be reindexed: >>> 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') + + +We still have access to older runs. + + >>> tracks.getLastUserTrack('a001', 1, 'u1') + + +We can also retrieve a run object with the run's data. + + >>> run = tracks.getRun(3) + >>> run + + Fin de partie ============= diff --git a/tracking/btree.py b/tracking/btree.py index b52f96c..6223c9b 100644 --- a/tracking/btree.py +++ b/tracking/btree.py @@ -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 # 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$ """ @@ -29,10 +29,27 @@ from zope.app.container.btree import BTreeContainer from zope.index.field import FieldIndex from persistent import Persistent -from BTrees import OOBTree +from BTrees import OOBTree, IOBTree 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 '' % ', '.join((str(self.id), + timeStamp2ISO(self.start), + timeStamp2ISO(self.end), + str(self.finished))) class TrackingStorage(BTreeContainer): @@ -48,6 +65,7 @@ class TrackingStorage(BTreeContainer): self.indexes = OOBTree.OOBTree() for idx in self.indexAttributes: self.indexes[idx] = FieldIndex() + self.runs = IOBTree.IOBTree() self.currentRuns = OOBTree.OOBTree() self.taskUsers = OOBTree.OOBTree() @@ -56,18 +74,36 @@ class TrackingStorage(BTreeContainer): def startRun(self, taskId): self.runId += 1 - self.currentRuns[taskId] = self.runId - return self.runId - - def stopRun(self, taskId): - runId = self.currentRuns.get(taskId) - if runId: - del self.currentRuns[taskId] + runId = self.runId + self.currentRuns[taskId] = runId + run = self.runs[runId] = Run(runId) + run.start = run.end = getTimeStamp() return runId + 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) + if runId: + return self.runs.get(runId) + def saveUserTrack(self, taskId, runId, userName, data, replace=False): if not runId: 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 if replace: track = self.getLastUserTrack(taskId, runId, userName) @@ -79,8 +115,7 @@ class TrackingStorage(BTreeContainer): self.trackNum += 1 trackNum = self.trackNum trackId = self.idFromNum(trackNum) - timeStamp = int(time.time()) - track = Track(taskId, runId, userName, timeStamp, data) + track = Track(taskId, runId, userName, getTimeStamp(), data) self[trackId] = track self.indexTrack(trackNum, track) return trackId @@ -161,3 +196,5 @@ class Track(Persistent): def timeStamp2ISO(ts): return time.strftime('%Y-%m-%d %H:%M', time.gmtime(ts)) +def getTimeStamp(): + return int(time.time()) diff --git a/tracking/interfaces.py b/tracking/interfaces.py index 0ca4c99..b332ceb 100644 --- a/tracking/interfaces.py +++ b/tracking/interfaces.py @@ -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 # it under the terms of the GNU General Public License as published by @@ -28,6 +28,16 @@ from zope import schema # 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): """ Result data from the interactions of a user with an task. """ @@ -41,16 +51,33 @@ class ITrackingStorage(Interface): """ 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): - """ Remove the current run entry for the task given. + def stopRun(taskId, runId=0, finish=True): + """ 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 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):