diff --git a/collective/auditlog/action.py b/collective/auditlog/action.py index e292b4d..9e44951 100644 --- a/collective/auditlog/action.py +++ b/collective/auditlog/action.py @@ -1,41 +1,64 @@ -import json +# coding=utf-8 +from Acquisition import aq_base from Acquisition import aq_parent +from collective.auditlog.utils import addLogEntry +from collective.auditlog.utils import getObjectInfo +from collective.auditlog.async import queueJob +from collective.auditlog.utils import getUID +from datetime import datetime +from OFS.interfaces import IObjectClonedEvent from OFS.SimpleItem import SimpleItem -from zope.component import adapts -from zope.interface import Interface, implements -from zope.formlib import form +from plone.app.contentrules.browser.formhelper import AddForm +from plone.app.contentrules.browser.formhelper import EditForm +from plone.app.iterate.interfaces import IBeforeCheckoutEvent +from plone.app.iterate.interfaces import ICancelCheckoutEvent +from plone.app.iterate.interfaces import ICheckinEvent +from plone.app.iterate.interfaces import IWorkingCopy +from plone.app.iterate.relation import WorkingCopyRelation +from plone.contentrules.rule.interfaces import IExecutable +from plone.contentrules.rule.interfaces import IRuleElementData +from plone.contentrules.rule.rule import RuleExecutable +from plone.memoize.instance import memoize +from plone.registry.interfaces import IRegistry +from Products.Archetypes.interfaces import IBaseObject +from Products.Archetypes.interfaces import IObjectEditedEvent +from Products.Archetypes.interfaces import IObjectInitializedEvent +from Products.CMFCore.interfaces import IActionSucceededEvent +from Products.CMFCore.utils import getToolByName +from Products.PluggableAuthService.interfaces.events import IUserLoggedInEvent +from Products.PluggableAuthService.interfaces.events import IUserLoggedOutEvent +from zope.component import adapter +from zope.component import getUtility +from zope.globalrequest import getRequest +from zope.interface import implementer +from zope.interface import Interface +from zope.lifecycleevent.interfaces import IObjectAddedEvent +from zope.lifecycleevent.interfaces import IObjectCreatedEvent +from zope.lifecycleevent.interfaces import IObjectModifiedEvent +from zope.lifecycleevent.interfaces import IObjectMovedEvent +from zope.lifecycleevent.interfaces import IObjectRemovedEvent + +import inspect +import json +import logging +import warnings + -from plone.app.contentrules.browser.formhelper import AddForm, EditForm -from plone.contentrules.rule.interfaces import IRuleElementData, IExecutable try: from Products.PloneFormGen.interfaces import IPloneFormGenField except ImportError: class IPloneFormGenField(Interface): pass -from Products.Archetypes.interfaces import ( - IObjectInitializedEvent, IObjectEditedEvent, IBaseObject) -from zope.lifecycleevent.interfaces import ( - IObjectCreatedEvent, IObjectModifiedEvent, - IObjectMovedEvent, IObjectRemovedEvent, IObjectAddedEvent) -from plone.app.iterate.relation import WorkingCopyRelation -from OFS.interfaces import IObjectClonedEvent +try: + # Plone4 only (formlib) + from zope.formlib import form + HAS_FORMLIB = True +except ImportError: + form = None + HAS_FORMLIB = False # Plone 5 will use z3c.form -import inspect -from plone.app.iterate.interfaces import ( - ICheckinEvent, IBeforeCheckoutEvent, ICancelCheckoutEvent, - IWorkingCopy) -from Products.CMFCore.interfaces import IActionSucceededEvent -from Products.PluggableAuthService.interfaces.events import ( - IUserLoggedInEvent, IUserLoggedOutEvent) -from zope.globalrequest import getRequest -from zope.component import getUtility -from plone.registry.interfaces import IRegistry -from collective.auditlog.utils import getObjectInfo -from collective.auditlog.utils import addLogEntry - -import logging logger = logging.getLogger('collective.auditlog') @@ -43,38 +66,77 @@ class IAuditAction(Interface): pass +@implementer(IAuditAction, IRuleElementData) class AuditAction(SimpleItem): - implements(IAuditAction, IRuleElementData) element = 'plone.actions.Audit' summary = u"Audit" +@implementer(IExecutable) +@adapter(Interface, IAuditAction, Interface) class AuditActionExecutor(object): - implements(IExecutable) - adapts(Interface, IAuditAction, Interface) def __init__(self, context, element, event): self.context = context self.element = element self.event = event - def canExecute(self, rule, req): + @property + @memoize + def request(self): + ''' Try to get a request + ''' + return getRequest() + + @property + @memoize + def rule(self): + ''' Check up the stack the rule that invoked this action + ''' + # start higher in the stack + frame = inspect.currentframe().f_back.f_back + while frame: + rule = frame.f_locals.get('self', None) + if isinstance(rule, RuleExecutable): + return rule + frame = frame.f_back + return None + + def canExecute(self, rule=None, req=None): + if rule is not None or req is not None: + msg = ( + 'In the next releases the rule and req parameters ' + 'will not be supported anymore. ' + 'In case you want to customize this product ' + 'use the request and rule properties' + ) + warnings.warn(msg, DeprecationWarning) + + if req is None: + req = self.request + + if req.environ.get('disable.auditlog', False): + return False + event = self.event obj = event.object - event_iface = [i for i in event.__implemented__.interfaces()][0] + event_iface = next(event.__implemented__.interfaces()) # for archetypes we need to make sure we're getting the right moved # event here so we do not duplicate - if (not IObjectEditedEvent.providedBy(event) and - rule is not None and event_iface != rule.rule.event): - return False + if not IObjectEditedEvent.providedBy(event): + if rule is None: + rule = self.rule + if rule and event_iface != rule.rule.event: + return False # if archetypes, initialization also does move events - if (IObjectMovedEvent.providedBy(event) and - IBaseObject.providedBy(obj) and - obj.checkCreationFlag()): - return False - if req.environ.get('disable.auditlog', False): + if ( + IObjectMovedEvent.providedBy(event) and + IBaseObject.providedBy(obj) and + obj.checkCreationFlag() + ): return False + return True def get_history_comment(self): @@ -90,26 +152,25 @@ def get_history_comment(self): return transition.get('comments', '') return '' - def __call__(self): - req = getRequest() - if req.environ.get('disable.auditlog', False): - return True + @property + @memoize + def trackWorkingCopies(self): + registry = getUtility(IRegistry) + return registry['collective.auditlog.interfaces.IAuditLogSettings.trackworkingcopies'] # noqa + + def _getObjectInfo(self, obj): + return getObjectInfo(obj, self.request) + @memoize + def getLogEntry(self): + ''' Get's a log entry for your action + ''' event = self.event obj = event.object + data = {'info': ''} + # order of those checks is important since some interfaces # base off the others - rule = inspect.stack()[1][0].f_locals.get('self', None) - registry = getUtility(IRegistry) - trackWorkingCopies = registry['collective.auditlog.interfaces.IAuditLogSettings.trackworkingcopies'] # noqa - - if not self.canExecute(rule, req): - return True # cut out early, we can't do this event - - data = { - 'info': '' - } - if IPloneFormGenField.providedBy(obj): # if ploneformgen field, use parent object for modified data data['field'] = obj.getId() @@ -121,14 +182,16 @@ def __call__(self): # need to keep track of removed events so it doesn't get called # more than once for each object action = 'removed' - elif (IObjectInitializedEvent.providedBy(event) or - IObjectCreatedEvent.providedBy(event) or - IObjectAddedEvent.providedBy(event)): + elif ( + IObjectInitializedEvent.providedBy(event) or + IObjectCreatedEvent.providedBy(event) or + IObjectAddedEvent.providedBy(event) + ): action = 'added' elif IObjectMovedEvent.providedBy(event): # moves can also be renames. Check the parent object if event.oldParent == event.newParent: - if rule is None or 'Rename' in rule.rule.title: + if rule is None or 'Rename' in self.rule.rule.title: info = {'previous_id': event.oldName} data['info'] = json.dumps(info) action = 'rename' @@ -136,9 +199,9 @@ def __call__(self): # cut out here, double action for this event return True else: - if rule is None or 'Moved' in rule.rule.title: + if rule is None or 'Moved' in self.rule.rule.title: parent_path = '/'.join(event.oldParent.getPhysicalPath()) - previous_location = parent_path + '/' + event.oldName + previous_location = u'{0}/{1}'.format(parent_path, event.oldName) info = {'previous_location': previous_location} data['info'] = json.dumps(info) action = 'moved' @@ -158,15 +221,15 @@ def __call__(self): info = {'message': event.message} data['info'] = json.dumps(info) action = 'checked in' - req.environ['disable.auditlog'] = True + self.request.environ['disable.auditlog'] = True data['working_copy'] = '/'.join(obj.getPhysicalPath()) obj = event.baseline elif IBeforeCheckoutEvent.providedBy(event): action = 'checked out' - req.environ['disable.auditlog'] = True + self.request.environ['disable.auditlog'] = True elif ICancelCheckoutEvent.providedBy(event): action = 'cancel check out' - req.environ['disable.auditlog'] = True + self.request.environ['disable.auditlog'] = True data['working_copy'] = '/'.join(obj.getPhysicalPath()) obj = event.baseline elif IUserLoggedInEvent.providedBy(event): @@ -182,43 +245,60 @@ def __call__(self): if IWorkingCopy.providedBy(obj): # if working copy, iterate, check if Track Working Copies is # enabled - if trackWorkingCopies: - # if enabled in control panel, use original object and move - # working copy path to working_copy - data['working_copy'] = '/'.join(obj.getPhysicalPath()) - relationships = obj.getReferences( - WorkingCopyRelation.relationship) - # check relationships, if none, something is wrong, not logging - # action - if len(relationships) > 0: - obj = relationships[0] - else: - return True - else: + if not self.trackWorkingCopies: # if not enabled, we only care about checked messages if 'check' not in action: return True + # if enabled in control panel, use original object and move + # working copy path to working_copy + data['working_copy'] = '/'.join(obj.getPhysicalPath()) + relationships = obj.getReferences( + WorkingCopyRelation.relationship) + # check relationships, if none, something is wrong, not logging + # action + if len(relationships) <= 0: + return True + obj = relationships[0] - data.update(getObjectInfo(obj)) + data.update(self._getObjectInfo(obj)) data['action'] = action + return data - addLogEntry(obj, data) + def _addLogEntry(self, logentry): + addLogEntry(self.event.object, logentry) + + def __call__(self): + try: # Remove in 1.4 + can_execute = self.canExecute() + except TypeError: + # This grants the compatibility with previous versions of the code + # in which the two parameters were required + can_execute = self.canExecute(self.rule, self.request) + + if can_execute: + self._addLogEntry(self.getLogEntry()) return True class AuditAddForm(AddForm): - form_fields = form.FormFields(IAuditAction) # needed for Plone4 (formlib) + + # Plone4 only + form_fields = form.FormFields(IAuditAction) if HAS_FORMLIB else [] schema = IAuditAction # needed for Plone5 (z3c.form) label = u"Add Audit Action" form_name = u"Configure element" def create(self, data): a = AuditAction() - form.applyChanges(a, self.form_fields, data) + HAS_FORMLIB and form.applyChanges(a, self.form_fields, data) return a class AuditEditForm(EditForm): - form_fields = form.FormFields(IAuditAction) + + # Plone4 only + form_fields = form.FormFields(IAuditAction) if HAS_FORMLIB else [] + schema = IAuditAction # needed for Plone5 (z3c.form) + label = u"Edit Audit Action" form_name = u"Configure element" diff --git a/collective/auditlog/utils.py b/collective/auditlog/utils.py index 2f303f8..9b5f302 100644 --- a/collective/auditlog/utils.py +++ b/collective/auditlog/utils.py @@ -1,15 +1,17 @@ -from Products.CMFCore.utils import getToolByName +# coding=utf-8 +from collective.auditlog import td from collective.auditlog.async import queueJob from collective.auditlog.catalog import catalogEntry from collective.auditlog.interfaces import BeforeStoreAuditlogEntryEvent from datetime import datetime from plone.registry.interfaces import IRegistry from plone.uuid.interfaces import IUUID +from Products.CMFCore.utils import getToolByName +from Zope2 import app from zope.component import getUtility from zope.component.hooks import getSite as getSiteHook from zope.event import notify from zope.globalrequest import getRequest -from Zope2 import app def getUID(context): @@ -35,9 +37,6 @@ def getHostname(request): """ stolen from the developer manual """ - if request is None: - return None - if "HTTP_X_FORWARDED_HOST" in request.environ: # Virtual host host = request.environ["HTTP_X_FORWARDED_HOST"] @@ -73,30 +72,25 @@ def getSite(): return site -def getUser(request=None): - if request is None: - request = getRequest() - site = getSite() +def getUser(request): + username = 'unknown' try: - portal_membership = getToolByName(site, 'portal_membership') + portal_membership = getToolByName(getSite(), 'portal_membership') user = portal_membership.getAuthenticatedMember() username = user.getUserName() except AttributeError: - user = request and request.other.get('AUTHENTICATED_USER') or None - if user is not None: - username = user.getUserName() - else: - username = 'unknown' + try: + username = request.other.get('AUTHENTICATED_USER').getUserName() + except AttributeError: + pass return username -def getObjectInfo(obj, request=None): +def getObjectInfo(obj, request): """ Get basic information about an object for logging. This only includes information available on the object itself. Some fields are missing because they depend on the event or rule that was triggered. """ - if request is None: - request = getRequest() obj_id = obj.id if callable(obj_id): obj_id = obj_id() @@ -108,18 +102,23 @@ def getObjectInfo(obj, request=None): site_name=getHostname(request), uid=getUID(obj), type=getattr(obj, 'portal_type', ''), - title=getattr(obj, 'Title', False) and obj.Title() or obj_id, - path=(getattr(obj, 'getPhysicalPath', False) and - '/'.join(obj.getPhysicalPath()) or '/') + title=obj.Title() if getattr(obj, 'Title', False) else obj_id, + path='/'.join(obj.getPhysicalPath()) if getattr(obj, 'getPhysicalPath', False) else '/' ) return data def addLogEntry(obj, data): - registry = getUtility(IRegistry) - storage = registry['collective.auditlog.interfaces.IAuditLogSettings.storage'] # noqa + if not data: + return + tdata = td.get() + if not tdata.registered: + tdata.register() + notify(BeforeStoreAuditlogEntryEvent(obj, data)) queueJob(getSite(), **data) + registry = getUtility(IRegistry) + storage = registry['collective.auditlog.interfaces.IAuditLogSettings.storage'] # noqa if storage != 'sql': catalogEntry(obj, data) diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst index af5fc18..8475c22 100644 --- a/docs/HISTORY.rst +++ b/docs/HISTORY.rst @@ -4,6 +4,16 @@ Changelog 1.3.4 (unreleased) ------------------ +- Fix soft dependency on formlib (#22) + [ale-rt] + +- Speed up rule retrieval + [ale-rt] + +- Added some memoized properties and methods to the `AuditActionExecutor` class + for easier customization + [ale-rt] + - collective.celery integration [enfold] diff --git a/setup.py b/setup.py index ce8671e..8c6a1b1 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,6 @@ 'setuptools', 'sqlalchemy', 'five.globalrequest', - 'five.formlib', # plone 5 ], extras_require={ 'async': [