provide 'recent changes' view
git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@3031 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
		
							parent
							
								
									40cee5d212
								
							
						
					
					
						commit
						bc538f2fd7
					
				
					 8 changed files with 155 additions and 8 deletions
				
			
		
							
								
								
									
										11
									
								
								CHANGES.txt
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								CHANGES.txt
									
										
									
									
									
								
							|  | @ -6,10 +6,17 @@ $Id$ | ||||||
| 0.9 | 0.9 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
|  | New features | ||||||
|  | 
 | ||||||
| - basic job management: a job executor view calls job managers specified | - basic job management: a job executor view calls job managers specified | ||||||
|   by loops root option ``organize.job.managers`` |   by loops root option ``organize.job.managers`` | ||||||
| - allow ``__getitem__`` on Loops and ViewManager, this is a prerequisite for | - allow ``__getitem__`` on Loops and ViewManager, this is a prerequisite for | ||||||
|   using virtual hosts over more than one path element (e.g. leading to |   using virtual hosts over more than one path element (e.g. leading to | ||||||
|   views/home) on protected sites; this also allows calling of job processiong |   views/home) on protected sites; this also allows calling of (public) | ||||||
|   views via wget without login credentials |   job processiong views via wget without login credentials | ||||||
| - add definition of loops package version (see loops/version.py) | - add definition of loops package version (see loops/version.py) | ||||||
|  | 
 | ||||||
|  | Bug fixes | ||||||
|  | 
 | ||||||
|  | - use select version on file, image and media asset links | ||||||
|  |   (via ``?version=this`` in the URL) | ||||||
|  |  | ||||||
|  | @ -160,6 +160,19 @@ class BaseView(GenericView, I18NView): | ||||||
|         # allow for having a separate object the view acts upon |         # allow for having a separate object the view acts upon | ||||||
|         return self.context |         return self.context | ||||||
| 
 | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def viewAnnotations(self): | ||||||
|  |         return self.request.annotations.get('loops.view', {}) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def node(self): | ||||||
|  |         return self.viewAnnotations.get('node') | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def nodeView(self): | ||||||
|  |         ann = self.request.annotations.get('loops.view', {}) | ||||||
|  |         return self.viewAnnotations.get('nodeView') | ||||||
|  | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def params(self): |     def params(self): | ||||||
|         result = {} |         result = {} | ||||||
|  |  | ||||||
|  | @ -76,6 +76,8 @@ class NodeView(BaseView): | ||||||
| 
 | 
 | ||||||
|     def __init__(self, context, request): |     def __init__(self, context, request): | ||||||
|         super(NodeView, self).__init__(context, request) |         super(NodeView, self).__init__(context, request) | ||||||
|  |         viewAnnotations = request.annotations.setdefault('loops.view', {}) | ||||||
|  |         viewAnnotations['nodeView'] = self | ||||||
|         viewConfig = getViewConfiguration(context, request) |         viewConfig = getViewConfiguration(context, request) | ||||||
|         self.setSkin(viewConfig.get('skinName')) |         self.setSkin(viewConfig.get('skinName')) | ||||||
| 
 | 
 | ||||||
|  | @ -738,6 +740,8 @@ class NodeTraverser(ItemTraverser): | ||||||
|     component.adapts(INode) |     component.adapts(INode) | ||||||
| 
 | 
 | ||||||
|     def publishTraverse(self, request, name): |     def publishTraverse(self, request, name): | ||||||
|  |         viewAnnotations = request.annotations.setdefault('loops.view', {}) | ||||||
|  |         viewAnnotations['node'] = self.context | ||||||
|         if self.context.nodeType == 'menu': |         if self.context.nodeType == 'menu': | ||||||
|             setViewConfiguration(self.context, request) |             setViewConfiguration(self.context, request) | ||||||
|         if name == '.loops': |         if name == '.loops': | ||||||
|  | @ -761,8 +765,6 @@ class NodeTraverser(ItemTraverser): | ||||||
|                 target = self.context.target |                 target = self.context.target | ||||||
|             if target is not None: |             if target is not None: | ||||||
|                 # remember self.context in request |                 # remember self.context in request | ||||||
|                 viewAnnotations = request.annotations.setdefault('loops.view', {}) |  | ||||||
|                 viewAnnotations['node'] = self.context |  | ||||||
|                 if request.method == 'PUT': |                 if request.method == 'PUT': | ||||||
|                     # we have to use the target object directly |                     # we have to use the target object directly | ||||||
|                     return target |                     return target | ||||||
|  |  | ||||||
|  | @ -133,6 +133,9 @@ of job control. | ||||||
| Tracking Reports | Tracking Reports | ||||||
| ================ | ================ | ||||||
| 
 | 
 | ||||||
|  | Overview (cumulative) statistics | ||||||
|  | -------------------------------- | ||||||
|  | 
 | ||||||
|   >>> from loops.organize.tracking.report import TrackingStats |   >>> from loops.organize.tracking.report import TrackingStats | ||||||
| 
 | 
 | ||||||
|   >>> view = TrackingStats(home, TestRequest()) |   >>> view = TrackingStats(home, TestRequest()) | ||||||
|  | @ -142,6 +145,28 @@ Tracking Reports | ||||||
|   >>> result['data'] |   >>> result['data'] | ||||||
|   [{'access': 2, 'new': 0, 'changed': 1, 'period': '...', 'count': 3}] |   [{'access': 2, 'new': 0, 'changed': 1, 'period': '...', 'count': 3}] | ||||||
| 
 | 
 | ||||||
|  | Recent changes | ||||||
|  | -------------- | ||||||
|  | 
 | ||||||
|  |   >>> from loops.organize.tracking.report import RecentChanges | ||||||
|  |   >>> view = RecentChanges(home, TestRequest()) | ||||||
|  |   >>> result = view.getData() | ||||||
|  |   >>> result['macro'][4][1][u'define-macro'] | ||||||
|  |   u'recent_changes' | ||||||
|  | 
 | ||||||
|  |   >>> data = result['data'] | ||||||
|  |   >>> data | ||||||
|  |   [<ChangeRecord ['27', 2, '33', '...']: {'action': 'modify'}>] | ||||||
|  | 
 | ||||||
|  |   >>> data[0].timeStamp | ||||||
|  |   u'... ...:...' | ||||||
|  |   >>> data[0].object | ||||||
|  |   {'url': '', 'object': <loops.resource.Resource ...>, 'title': 'Change Doc 001'} | ||||||
|  |   >>> data[0].user | ||||||
|  |   {'url': '', 'object': <loops.concept.Concept ...>, 'title': u'john'} | ||||||
|  |   >>> data[0].action | ||||||
|  |   'modify' | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| Fin de partie | Fin de partie | ||||||
| ============= | ============= | ||||||
|  |  | ||||||
|  | @ -42,6 +42,12 @@ | ||||||
|       class="loops.organize.tracking.report.TrackingStats" |       class="loops.organize.tracking.report.TrackingStats" | ||||||
|       permission="zope.View" /> |       permission="zope.View" /> | ||||||
| 
 | 
 | ||||||
|  |   <browser:page | ||||||
|  |       name="recent_changes.html" | ||||||
|  |       for="loops.interfaces.IConcept" | ||||||
|  |       class="loops.organize.tracking.report.RecentChanges" | ||||||
|  |       permission="zope.View" /> | ||||||
|  | 
 | ||||||
|   <zope:adapter |   <zope:adapter | ||||||
|       for="loops.organize.tracking.change.IChangeRecord |       for="loops.organize.tracking.change.IChangeRecord | ||||||
|            zope.publisher.interfaces.browser.IBrowserRequest" |            zope.publisher.interfaces.browser.IBrowserRequest" | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| <metal:report define-macro="report" | <metal:report define-macro="report" | ||||||
|               tal:define="info item/getData"> |               tal:define="info item/getData"> | ||||||
|   <h2 i18n:translate="">Statistics Report</h2> |   <h2 i18n:translate="" | ||||||
|  |       tal:content="item/title|default">Statistics Report</h2><br /> | ||||||
|   <metal:listing use-macro="info/macro" /> |   <metal:listing use-macro="info/macro" /> | ||||||
| </metal:report> | </metal:report> | ||||||
| 
 | 
 | ||||||
|  | @ -29,3 +30,27 @@ | ||||||
|   </table> |   </table> | ||||||
| </metal:overview> | </metal:overview> | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | <metal:recent define-macro="recent_changes"> | ||||||
|  |   <table class="listing"> | ||||||
|  |     <tr> | ||||||
|  |       <th i18n:translate="">Title</th> | ||||||
|  |       <th i18n:translate="">User</th> | ||||||
|  |       <th i18n:translate="">Date/Time</th> | ||||||
|  |       <th i18n:translate="">Change</th> | ||||||
|  |     </tr> | ||||||
|  |     <tr tal:repeat="row info/data" | ||||||
|  |         tal:attributes="class python: repeat['row'].odd() and 'even' or 'odd'"> | ||||||
|  |       <td tal:define="url row/object/url"> | ||||||
|  |         <a tal:attributes="href url" | ||||||
|  |            tal:omit-tag="not:url" | ||||||
|  |            tal:content="row/object/title" /></td> | ||||||
|  |       <td tal:define="url row/user/url"> | ||||||
|  |         <a tal:attributes="href url" | ||||||
|  |            tal:omit-tag="not:url" | ||||||
|  |            tal:content="row/user/title" /></td> | ||||||
|  |       <td tal:content="row/timeStamp"></td> | ||||||
|  |       <td tal:content="row/action"></td> | ||||||
|  |     </tr> | ||||||
|  |   </table> | ||||||
|  | </metal:recent> | ||||||
|  |  | ||||||
|  | @ -28,9 +28,11 @@ from zope.cachedescriptors.property import Lazy | ||||||
| from zope.traversing.browser import absoluteURL | from zope.traversing.browser import absoluteURL | ||||||
| from zope.traversing.api import getName | from zope.traversing.api import getName | ||||||
| 
 | 
 | ||||||
|  | from cybertools.util import format | ||||||
| from loops.browser.common import BaseView | from loops.browser.common import BaseView | ||||||
| from loops.interfaces import IResource | from loops.interfaces import IResource | ||||||
| from loops import util | from loops import util | ||||||
|  | from loops.util import _ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| report_macros = ViewPageTemplateFile('report.pt') | report_macros = ViewPageTemplateFile('report.pt') | ||||||
|  | @ -39,6 +41,7 @@ report_macros = ViewPageTemplateFile('report.pt') | ||||||
| class TrackingStats(BaseView): | class TrackingStats(BaseView): | ||||||
| 
 | 
 | ||||||
|     template = report_macros |     template = report_macros | ||||||
|  |     title = _(u'Statistics Report') | ||||||
| 
 | 
 | ||||||
|     @Lazy |     @Lazy | ||||||
|     def macro(self): |     def macro(self): | ||||||
|  | @ -112,11 +115,75 @@ class TrackingStats(BaseView): | ||||||
|             the period given. |             the period given. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
|     def getObjectDetails(uid): |     def getObjectDetails(uid, period=None): | ||||||
|         """ Listing of last n accesses and changes of the object specified by |         """ Listing of (last n?) accesses and changes of the object specified by | ||||||
|             the uid given. |             the uid given, optionally limited to the period (month) given. | ||||||
|         """ |         """ | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class RecentChanges(TrackingStats): | ||||||
|  | 
 | ||||||
|  |     title = _(u'Recent Changes') | ||||||
|  | 
 | ||||||
|  |     def getData(self): | ||||||
|  |         length = self.request.form.get('length', 15) | ||||||
|  |         new = {} | ||||||
|  |         changed = {} | ||||||
|  |         result = [] | ||||||
|  |         for track in self.changeRecords: | ||||||
|  |             if track.data['action'] == 'add' and track.taskId not in new: | ||||||
|  |                 new[track.taskId] = track | ||||||
|  |                 result.append(track) | ||||||
|  |                 continue | ||||||
|  |             if track.data['action'] == 'modify' and track.taskId not in changed: | ||||||
|  |                 sameNew = new.get(track.taskId) | ||||||
|  |                 if sameNew and sameNew.timeStamp > track.timeStamp - 60: | ||||||
|  |                     # change immediate after creation | ||||||
|  |                     continue | ||||||
|  |                 changed[track.taskId] = track | ||||||
|  |                 result.append(track) | ||||||
|  |                 continue | ||||||
|  |         return dict(data=[TrackDetails(self, tr) for tr in result[:length]], | ||||||
|  |                     macro=self.macros['recent_changes']) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TrackDetails(object): | ||||||
|  | 
 | ||||||
|  |     timeStampFormat = 'short' | ||||||
|  | 
 | ||||||
|  |     def __init__(self, view, track): | ||||||
|  |         self.view = view | ||||||
|  |         self.track = track | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def object(self): | ||||||
|  |         obj = util.getObjectForUid(self.track.taskId) | ||||||
|  |         node = self.view.nodeView | ||||||
|  |         url = node is not None and node.getUrlForTarget(obj) or '' | ||||||
|  |         return dict(object=obj, title=obj.title, url=url) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def user(self): | ||||||
|  |         obj = util.getObjectForUid(self.track.userName) | ||||||
|  |         if obj is None: | ||||||
|  |             return dict(object=None, title=userName, url='') | ||||||
|  |         node = self.view.nodeView | ||||||
|  |         url = node is not None and node.getUrlForTarget(obj) or '' | ||||||
|  |         return dict(object=obj, title=obj.title, url=url) | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def action(self): | ||||||
|  |         return self.track.data['action'] | ||||||
|  | 
 | ||||||
|  |     @Lazy | ||||||
|  |     def timeStamp(self): | ||||||
|  |         value = datetime.fromtimestamp(self.track.timeStamp) | ||||||
|  |         return format.formatDate(value, 'dateTime', self.timeStampFormat, | ||||||
|  |                                  self.view.languageInfo.language) | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return repr(self.track) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def formatAsMonth(d): | def formatAsMonth(d): | ||||||
|     return d.isoformat()[:7] |     return d.isoformat()[:7] | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								util.py
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								util.py
									
										
									
									
									
								
							|  | @ -85,6 +85,8 @@ def toUnicode(value, encoding='UTF-8'): | ||||||
| def getObjectForUid(uid, intIds=None): | def getObjectForUid(uid, intIds=None): | ||||||
|     if uid == '*': # wild card |     if uid == '*': # wild card | ||||||
|         return '*' |         return '*' | ||||||
|  |     if isinstance(uid, basestring) and not uid.isdigit():   # not a valid uid | ||||||
|  |         return None | ||||||
|     if intIds is None: |     if intIds is None: | ||||||
|         intIds = component.getUtility(IIntIds) |         intIds = component.getUtility(IIntIds) | ||||||
|     return intIds.getObject(int(uid)) |     return intIds.getObject(int(uid)) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 helmutm
						helmutm