diff --git a/browser/common.py b/browser/common.py index ce37cbb..c806aec 100644 --- a/browser/common.py +++ b/browser/common.py @@ -209,6 +209,10 @@ class BaseView(GenericView, I18NView): def conceptManager(self): return self.loopsRoot.getConceptManager() + @Lazy + def resourceManager(self): + return self.loopsRoot.getResourceManager() + @Lazy def typePredicate(self): return self.conceptManager.getTypePredicate() diff --git a/browser/loops.css b/browser/loops.css index bd26a79..5c44389 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -45,6 +45,10 @@ table.listing td { vertical-align: middle; } +table.listing td.number { + text-align: right; +} + table.listing td.checkbox { text-align: center; width: 10px; diff --git a/organize/tracking/README.txt b/organize/tracking/README.txt index 4694982..f2c34d8 100644 --- a/organize/tracking/README.txt +++ b/organize/tracking/README.txt @@ -59,6 +59,13 @@ Recording changes to objects >>> len(changes) 1 + >>> from zope.lifecycleevent import ObjectCreatedEvent, ObjectModifiedEvent + >>> from zope.event import notify + >>> resources['d001.txt'].title = 'Change Doc 001' + >>> notify(ObjectModifiedEvent(resources['d001.txt'])) + >>> len(changes) + 2 + Recording assignment changes ---------------------------- @@ -68,14 +75,18 @@ Recording assignment changes >>> t01.assignChild(johnC) >>> len(changes) - 2 + 3 Tracking Object Access ====================== Access records are not directly stored in the ZODB (in order to avoid -conflict errors) but first stored to a log file. +conflict errors) but are first stored to a log file. + +Even this logging is a two-step process: the data to be logged are first collected +in the request; all collected data are then written triggered by the +EndRequestEvent. >>> from loops.organize.tracking.access import logfile_option, record, logAccess >>> from loops.organize.tracking.access import AccessRecordManager @@ -94,20 +105,40 @@ conflict errors) but first stored to a log file. ... node=util.getUidForObject(home), ... target=util.getUidForObject(resources['d001.txt']), ... ) + >>> logAccess(EndRequestEvent(NodeView(home, request), request), testDir) + >>> record(request, principal='users.john', view='render', ... node=util.getUidForObject(home), ... target=util.getUidForObject(resources['d002.txt']), ... ) - >>> logAccess(EndRequestEvent(NodeView(home, request), request), testDir) -They can then be read in via an AccessRecordManager object, i.e. a view +The access log can then be read in via an AccessRecordManager object, i.e. a view that may be called via ``wget`` using a crontab entry or some other kind of job control. + >>> access = records['access'] + >>> len(access) + 0 + >>> rm = AccessRecordManager(loopsRoot, TestRequest()) >>> rm.baseDir = testDir >>> rm.loadRecordsFromLog() + >>> len(access) + 2 + + +Tracking Reports +================ + + >>> from loops.organize.tracking.report import TrackingStats + + >>> view = TrackingStats(home, TestRequest()) + >>> result = view.getData() + >>> result['macro'][4][1][u'define-macro'] + u'overview' + >>> result['data'] + [{'access': 2, 'new': 0, 'changed': 1, 'period': '2008-11', 'count': 3}] Fin de partie diff --git a/organize/tracking/access.py b/organize/tracking/access.py index c4955ef..3ac479e 100644 --- a/organize/tracking/access.py +++ b/organize/tracking/access.py @@ -56,6 +56,7 @@ def record(request, **kw): data = request.annotations.setdefault(request_key, {}) for k, v in kw.items(): data[k] = v + # ???: better to collect data in a list of dictionaries? @adapter(IEndRequestEvent) diff --git a/organize/tracking/configure.zcml b/organize/tracking/configure.zcml index d465d4a..d452e10 100644 --- a/organize/tracking/configure.zcml +++ b/organize/tracking/configure.zcml @@ -28,6 +28,12 @@ + + + name="load_access_records" + for="loops.interfaces.ILoops" + class="loops.organize.tracking.access.AccessRecordManager" + attribute="loadRecordsFromLog" + permission="zope.Public" /> diff --git a/organize/tracking/report.pt b/organize/tracking/report.pt new file mode 100644 index 0000000..8adc543 --- /dev/null +++ b/organize/tracking/report.pt @@ -0,0 +1,31 @@ + +

Statistics Report

+ +
+ + + + + + + + + + + + + + + + + + +
Periodaccesschangesadditionstotal
+
+ diff --git a/organize/tracking/report.py b/organize/tracking/report.py new file mode 100644 index 0000000..e7ce476 --- /dev/null +++ b/organize/tracking/report.py @@ -0,0 +1,122 @@ +# +# Copyright (c) 2008 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 +# + +""" +Adapter and view class(es) for statistics reporting. + +$Id$ +""" + +from datetime import date, datetime +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy +from zope.traversing.browser import absoluteURL +from zope.traversing.api import getName + +from loops.browser.common import BaseView +from loops.interfaces import IResource +from loops import util + + +report_macros = ViewPageTemplateFile('report.pt') + + +class TrackingStats(BaseView): + + template = report_macros + + @Lazy + def macro(self): + return self.macros['report'] + + @Lazy + def macros(self): + return self.template.macros + + @Lazy + def accessRecords(self): + return self.filter(reversed(self.loopsRoot.getRecordManager()['access'].values())) + + @Lazy + def changeRecords(self): + return self.filter(reversed(self.loopsRoot.getRecordManager()['changes'].values())) + + def filter(self, tracks): + for tr in tracks: + try: + if IResource.providedBy(util.getObjectForUid(tr.taskId)): + yield tr + except KeyError: + pass + + def getData(self): + form = self.request.form + period = form.get('period') + uid = form.get('id') + if period is not None: + result = self.getPeriodDetails(period) + macroName = 'period' + elif uid is not None: + result = self.getObjectDetails(uid) + macroName = 'object' + else: + result = self.getOverview() + macroName = 'overview' + macro = self.macros[macroName] + return dict(data=result, macro=macro) + + def getOverview(self): + """ Period-based (monthly) listing of the numbers of object accesses, new, + changed, [deleted] objects, number of resources at end of month. + """ + periods = {} + for track in self.accessRecords: + ts = datetime.fromtimestamp(track.timeStamp) + p = date(ts.year, ts.month, 1) + periods.setdefault(p, dict(access=0, new=0, changed=0)) + periods[p]['access'] += 1 + for track in self.changeRecords: + ts = datetime.fromtimestamp(track.timeStamp) + p = date(ts.year, ts.month, 1) + periods.setdefault(p, dict(access=0, new=0, changed=0)) + #periods[p]['new'] += 1 + if track.data['action'] == 'modify': + periods[p]['changed'] += 1 + elif track.data['action'] == 'add': + periods[p]['new'] += 1 + result = [dict(period=formatAsMonth(p), **periods[p]) + for p in reversed(sorted(periods))] + num = len(self.resourceManager) + for data in result: + data['count'] = num + num = num - data['new'] + return result + + def getPeriodDetails(period): + """ Listing of accessed, new, changed, [deleted] objects during + the period given. + """ + + def getObjectDetails(uid): + """ Listing of last n accesses and changes of the object specified by + the uid given. + """ + + +def formatAsMonth(d): + return d.isoformat()[:7]