work in progress: tracking statistics: overview report

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2988 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2008-11-14 15:04:45 +00:00
parent a6559dfdf9
commit 6b62e383ac
7 changed files with 208 additions and 9 deletions

View file

@ -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()

View file

@ -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;

View file

@ -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

View file

@ -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)

View file

@ -28,6 +28,12 @@
<!-- views -->
<browser:page
name="tracking_stats.html"
for="loops.interfaces.IConcept"
class="loops.organize.tracking.report.TrackingStats"
permission="zope.View" />
<zope:adapter
for="loops.organize.tracking.change.IChangeRecord
zope.publisher.interfaces.browser.IBrowserRequest"
@ -43,11 +49,11 @@
permission="zope.View" />
<browser:page
name="load_access_records"
for="loops.interfaces.ILoops"
class="loops.organize.tracking.access.AccessRecordManager"
attribute="loadRecordsFromLog"
permission="zope.Public" />
name="load_access_records"
for="loops.interfaces.ILoops"
class="loops.organize.tracking.access.AccessRecordManager"
attribute="loadRecordsFromLog"
permission="zope.Public" />
<zope:adapter factory="loops.organize.tracking.setup.SetupManager"
name="organize.tracking" />

View file

@ -0,0 +1,31 @@
<metal:report define-macro="report"
tal:define="info item/getData">
<h2 i18n:translate="">Statistics Report</h2>
<metal:listing use-macro="info/macro" />
</metal:report>
<metal:overview define-macro="overview">
<table class="listing">
<tr>
<th i18n:translate="">Period</th>
<th i18n:translate="">access</th>
<th i18n:translate="">changes</th>
<th i18n:translate="">additions</th>
<th i18n:translate="">total</th>
</tr>
<tr tal:repeat="row info/data"
tal:attributes="class python: repeat['row'].odd() and 'even' or 'odd'">
<td tal:content="row/period"></td>
<td class="number"
tal:content="row/access"></td>
<td class="number"
tal:content="row/changed"></td>
<td class="number"
tal:content="row/new"></td>
<td class="number"
tal:content="row/count"></td>
</tr>
</table>
</metal:overview>

122
organize/tracking/report.py Normal file
View file

@ -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]