[repo] optimize massive insertion/deletion by using the new set_operation function
Idea is that on massive insertion, cost of handling the list of operation
become non negligeable, so we should minimize the number of operations in
that list.
The set_operation function ease usage of operation associated to data in
session.transaction_data, and we only add the operation when data set isn't
initialized yet, else we simply add data to the set. The operation then
simply process accumulated data.
"""some hooks to handle notification on entity's changes
:organization: Logilab
:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"
from logilab.common.textutils import normalize_text
from cubicweb.selectors import implements
from cubicweb.server import hook
from cubicweb.sobjects.supervising import SupervisionMailOp
class RenderAndSendNotificationView(hook.Operation):
"""delay rendering of notification view until precommit"""
def precommit_event(self):
view = self.view
if view.cw_rset is not None and not view.cw_rset:
return # entity added and deleted in the same transaction (cache effect)
if view.cw_rset and self.session.deleted_in_transaction(view.cw_rset[view.cw_row or 0][view.cw_col or 0]):
return # entity added and deleted in the same transaction
self.view.render_and_send(**getattr(self, 'viewargs', {}))
class NotificationHook(hook.Hook):
__abstract__ = True
category = 'notification'
def select_view(self, vid, rset, row=0, col=0):
return self._cw.vreg['views'].select_or_none(vid, self._cw, rset=rset,
row=row, col=col)
class StatusChangeHook(NotificationHook):
"""notify when a workflowable entity has its state modified"""
__regid__ = 'notifystatuschange'
__select__ = NotificationHook.__select__ & implements('TrInfo')
events = ('after_add_entity',)
def __call__(self):
entity = self.entity
if not entity.from_state: # not a transition
return
rset = entity.related('wf_info_for')
view = self.select_view('notif_status_change', rset=rset, row=0)
if view is None:
return
comment = entity.printable_value('comment', format='text/plain')
# XXX don't try to wrap rest until we've a proper transformation (see
# #103822)
if comment and entity.comment_format != 'text/rest':
comment = normalize_text(comment, 80)
RenderAndSendNotificationView(self._cw, view=view, viewargs={
'comment': comment, 'previous_state': entity.previous_state.name,
'current_state': entity.new_state.name})
class RelationChangeHook(NotificationHook):
__regid__ = 'notifyrelationchange'
events = ('before_add_relation', 'after_add_relation',
'before_delete_relation', 'after_delete_relation')
def __call__(self):
"""if a notification view is defined for the event, send notification
email defined by the view
"""
rset = self._cw.eid_rset(self.eidfrom)
view = self.select_view('notif_%s_%s' % (self.event, self.rtype),
rset=rset, row=0)
if view is None:
return
RenderAndSendNotificationView(self._cw, view=view)
class EntityChangeHook(NotificationHook):
"""if a notification view is defined for the event, send notification
email defined by the view
"""
__regid__ = 'notifyentitychange'
events = ('after_add_entity', 'after_update_entity')
def __call__(self):
rset = self.entity.as_rset()
view = self.select_view('notif_%s' % self.event, rset=rset, row=0)
if view is None:
return
RenderAndSendNotificationView(self._cw, view=view)
class EntityUpdatedNotificationOp(hook.SingleLastOperation):
def precommit_event(self):
session = self.session
for eid in session.transaction_data['changes']:
view = session.vreg['views'].select('notif_entity_updated', session,
rset=session.eid_rset(eid),
row=0)
RenderAndSendNotificationView(session, view=view)
class EntityUpdateHook(NotificationHook):
__regid__ = 'notifentityupdated'
__abstract__ = True # do not register by default
__select__ = NotificationHook.__select__ & hook.from_dbapi_query()
events = ('before_update_entity',)
skip_attrs = set()
def __call__(self):
session = self._cw
if session.added_in_transaction(self.entity.eid):
return # entity is being created
# then compute changes
attrs = [k for k in self.entity.edited_attributes
if not k in self.skip_attrs]
if not attrs:
return
changes = session.transaction_data.setdefault('changes', {})
thisentitychanges = changes.setdefault(self.entity.eid, set())
rqlsel, rqlrestr = [], ['X eid %(x)s']
for i, attr in enumerate(attrs):
var = chr(65+i)
rqlsel.append(var)
rqlrestr.append('X %s %s' % (attr, var))
rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr))
rset = session.execute(rql, {'x': self.entity.eid}, 'x')
for i, attr in enumerate(attrs):
oldvalue = rset[0][i]
newvalue = self.entity[attr]
if oldvalue != newvalue:
thisentitychanges.add((attr, oldvalue, newvalue))
if thisentitychanges:
EntityUpdatedNotificationOp(session)
# supervising ##################################################################
class SomethingChangedHook(NotificationHook):
__regid__ = 'supervising'
__select__ = NotificationHook.__select__ & hook.from_dbapi_query()
events = ('before_add_relation', 'before_delete_relation',
'after_add_entity', 'before_update_entity')
def __call__(self):
dest = self._cw.vreg.config['supervising-addrs']
if not dest: # no supervisors, don't do this for nothing...
return
if self._call():
SupervisionMailOp(self._cw)
def _call(self):
event = self.event.split('_', 1)[1]
if event == 'update_entity':
if self._cw.added_in_transaction(self.entity.eid):
return False
if self.entity.e_schema == 'CWUser':
if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
'last_login_time'))):
# don't record last_login_time update which are done
# automatically at login time
return False
self._cw.transaction_data.setdefault('pendingchanges', []).append(
(event, self))
return True
class EntityDeleteHook(SomethingChangedHook):
__regid__ = 'supervisingentitydel'
events = ('before_delete_entity',)
def _call(self):
try:
title = self.entity.dc_title()
except:
# may raise an error during deletion process, for instance due to
# missing required relation
title = '#%s' % eid
self._cw.transaction_data.setdefault('pendingchanges', []).append(
('delete_entity', (self.entity.eid, str(self.entity.e_schema), title)))
return True