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
+
+
+
+
+
+
+
+ Period |
+ access |
+ changes |
+ additions |
+ total |
+
+
+ |
+ |
+ |
+ |
+ |
+
+
+
+
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]