"""some hooks and views to handle notification on entity's changes:organization: Logilab:copyright: 2001-2009 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"_=unicodefromitertoolsimportrepeatfromlogilab.common.textutilsimportnormalize_textfromlogilab.common.deprecationimportclass_renamed,deprecatedfromcubicwebimportRegistryExceptionfromcubicweb.selectorsimportimplements,yesfromcubicweb.viewimportComponentfromcubicweb.common.mailimportNotificationView,parse_message_id,SkipEmailfromcubicweb.server.poolimportPreCommitOperation,SingleLastOperationfromcubicweb.server.hookhelperimportSendMailOpfromcubicweb.server.hooksmanagerimportHookparse_message_id=deprecated('parse_message_id is now defined in cubicweb.common.mail')(parse_message_id)classRecipientsFinder(Component):"""this component is responsible to find recipients of a notification by default user's with their email set are notified if any, else the default email addresses specified in the configuration are used """id='recipients_finder'__select__=yes()user_rql=('Any X,E,A WHERE X is CWUser, X in_state S, S name "activated",''X primary_email E, E address A')defrecipients(self):mode=self.config['default-recipients-mode']ifmode=='users':# use unsafe execute else we may don't have the right to see users# to notify...execute=self.req.unsafe_executedests=[(u.get_email(),u.property_value('ui.language'))foruinexecute(self.user_rql,build_descr=True,propagate=True).entities()]elifmode=='default-dest-addrs':lang=self.vreg.property_value('ui.language')dests=zip(self.config['default-dest-addrs'],repeat(lang))else:# mode == 'none'dests=[]returndests# hooks #######################################################################classEntityUpdatedNotificationOp(SingleLastOperation):defprecommit_event(self):session=self.sessionforeidinsession.transaction_data['changes']:view=session.vreg['views'].select('notif_entity_updated',session,rset=session.eid_rset(eid),row=0)RenderAndSendNotificationView(session,view=view)defcommit_event(self):passclassRenderAndSendNotificationView(PreCommitOperation):"""delay rendering of notification view until precommit"""defprecommit_event(self):view=self.viewifview.rsetisnotNoneandnotview.rset:return# entity added and deleted in the same transaction (cache effect)ifview.rsetandview.rset[0][0]inself.session.transaction_data.get('pendingeids',()):return# entity added and deleted in the same transactionself.view.render_and_send(**getattr(self,'viewargs',{}))classStatusChangeHook(Hook):"""notify when a workflowable entity has its state modified"""events=('after_add_entity',)accepts=('TrInfo',)defcall(self,session,entity):ifnotentity.from_state:# not a transitionreturnrset=entity.related('wf_info_for')try:view=session.vreg['views'].select('notif_status_change',session,rset=rset,row=0)exceptRegistryException:returncomment=entity.printable_value('comment',format='text/plain')# XXX don't try to wrap rest until we've a proper transformation (see# #103822)ifcommentandentity.comment_format!='text/rest':comment=normalize_text(comment,80)RenderAndSendNotificationView(session,view=view,viewargs={'comment':comment,'previous_state':entity.previous_state.name,'current_state':entity.new_state.name})classRelationChangeHook(Hook):events=('before_add_relation','after_add_relation','before_delete_relation','after_delete_relation')accepts=('Any',)defcall(self,session,fromeid,rtype,toeid):"""if a notification view is defined for the event, send notification email defined by the view """rset=session.eid_rset(fromeid)vid='notif_%s_%s'%(self.event,rtype)try:view=session.vreg['views'].select(vid,session,rset=rset,row=0)exceptRegistryException:returnRenderAndSendNotificationView(session,view=view)classEntityChangeHook(Hook):events=('after_add_entity','after_update_entity')accepts=('Any',)defcall(self,session,entity):"""if a notification view is defined for the event, send notification email defined by the view """rset=entity.as_rset()vid='notif_%s'%self.eventtry:view=session.vreg['views'].select(vid,session,rset=rset,row=0)exceptRegistryException:returnRenderAndSendNotificationView(session,view=view)classEntityUpdateHook(Hook):events=('before_update_entity',)accepts=()skip_attrs=set()defcall(self,session,entity):ifentity.eidinsession.transaction_data.get('neweids',()):return# entity is being createdifsession.is_super_session:return# ignore changes triggered by hooks# then compute changeschanges=session.transaction_data.setdefault('changes',{})thisentitychanges=changes.setdefault(entity.eid,set())attrs=[kforkinentity.edited_attributesifnotkinself.skip_attrs]ifnotattrs:returnrqlsel,rqlrestr=[],['X eid %(x)s']fori,attrinenumerate(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':entity.eid},'x')fori,attrinenumerate(attrs):oldvalue=rset[0][i]newvalue=entity[attr]ifoldvalue!=newvalue:thisentitychanges.add((attr,oldvalue,newvalue))ifthisentitychanges:EntityUpdatedNotificationOp(session)# abstract or deactivated notification views and mixin ########################classNotificationView(NotificationView):"""overriden to delay actual sending of mails to a commit operation by default """defsend_on_commit(self,recipients,msg):SendMailOp(self.req,recipients=recipients,msg=msg)send=send_on_commitclassStatusChangeMixIn(object):id='notif_status_change'msgid_timestamp=Truemessage=_('status changed')content=_("""%(user)s changed status from <%(previous_state)s> to <%(current_state)s> for entity'%(title)s'%(comment)surl: %(url)s""")################################################################################ Actual notification views. ## ## disable them at the recipients_finder level if you don't want them ################################################################################# XXX should be based on dc_title/dc_description, no?classContentAddedView(NotificationView):"""abstract class for notification on entity/relation all you have to do by default is : * set id and __select__ attributes to match desired events and entity types * set a content attribute to define the content of the email (unless you override call) """__abstract__=Trueid='notif_after_add_entity'msgid_timestamp=Falsemessage=_('new')content="""%(title)s%(content)surl: %(url)s"""defcontext(self,**kwargs):entity=self.entity(self.rowor0,self.color0)content=entity.printable_value(self.content_attr,format='text/plain')ifcontent:contentformat=getattr(entity,self.content_attr+'_format','text/rest')# XXX don't try to wrap rest until we've a proper transformation (see# #103822)ifcontentformat!='text/rest':content=normalize_text(content,80)returnsuper(ContentAddedView,self).context(content=content,**kwargs)defsubject(self):entity=self.entity(self.rowor0,self.color0)returnu'%s #%s (%s)'%(self.req.__('New %s'%entity.e_schema),entity.eid,self.user_data['login'])NormalizedTextView=class_renamed('NormalizedTextView',ContentAddedView)defformat_value(value):ifisinstance(value,unicode):returnu'"%s"'%valuereturnvalueclassEntityUpdatedNotificationView(NotificationView):"""abstract class for notification on entity/relation all you have to do by default is : * set id and __select__ attributes to match desired events and entity types * set a content attribute to define the content of the email (unless you override call) """__abstract__=Trueid='notif_entity_updated'msgid_timestamp=Falsemessage=_('updated')no_detailed_change_attrs=()content="""Properties have been updated by %(user)s:%(changes)surl: %(url)s"""defcontext(self,**kwargs):context=super(EntityUpdatedNotificationView,self).context(**kwargs)changes=self.req.transaction_data['changes'][self.rset[0][0]]_=self.req._formatted_changes=[]forattr,oldvalue,newvalueinsorted(changes):# check current user has permission to see the attributerschema=self.vreg.schema[attr]ifrschema.final:ifnotrschema.has_perm(self.req,'read',eid=self.rset[0][0]):continue# XXX suppose it's a subject relation...elifnotrschema.has_perm(self.req,'read',fromeid=self.rset[0][0]):continueifattrinself.no_detailed_change_attrs:msg=_('%s updated')%_(attr)elifoldvaluenotin(None,''):msg=_('%(attr)s updated from %(oldvalue)s to %(newvalue)s')%{'attr':_(attr),'oldvalue':format_value(oldvalue),'newvalue':format_value(newvalue)}else:msg=_('%(attr)s set to %(newvalue)s')%{'attr':_(attr),'newvalue':format_value(newvalue)}formatted_changes.append('* '+msg)ifnotformatted_changes:# current user isn't allowed to see changes, skip this notificationraiseSkipEmail()context['changes']='\n'.join(formatted_changes)returncontextdefsubject(self):entity=self.entity(self.rowor0,self.color0)returnu'%s #%s (%s)'%(self.req.__('Updated %s'%entity.e_schema),entity.eid,self.user_data['login'])