Added tag cubicweb-debian-version-3_0_3-1 for changeset a736bae56d4a
"""some hooks and views to handle notification on entity's changes:organization: Logilab:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"frombase64importb64encode,b64decodefromitertoolsimportrepeatfromtimeimporttimetry:fromsocketimportgethostnameexceptImportError:defgethostname():return'XXX'fromlogilab.common.textutilsimportnormalize_textfromcubicwebimportRegistryExceptionfromcubicweb.common.viewimportEntityViewfromcubicweb.common.appobjectimportComponentfromcubicweb.common.registerersimportaccepts_registererfromcubicweb.common.selectorsimportacceptfromcubicweb.common.mailimportformat_mailfromcubicweb.server.poolimportPreCommitOperationfromcubicweb.server.hookhelperimportSendMailOpfromcubicweb.server.hooksmanagerimportHook_=unicodeclassRecipientsFinder(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'__registerer__=accepts_registerer__selectors__=(accept,)accepts=('Any',)user_rql=('Any X,E,A WHERE X is EUser, 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 #######################################################################classRenderAndSendNotificationView(PreCommitOperation):"""delay rendering of notification view until precommit"""defprecommit_event(self):ifself.view.rset[0][0]inself.session.query_data('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.select_view('notif_status_change',session,rset,row=0)exceptRegistryException:returncomment=entity.printable_value('comment',format='text/plain')ifcomment:comment=normalize_text(comment,80,rest=entity.comment_format=='text/rest')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.select_view(vid,session,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.select_view(vid,session,rset,row=0)exceptRegistryException:returnRenderAndSendNotificationView(session,view=view)# abstract or deactivated notification views and mixin ########################classNotificationView(EntityView):"""abstract view implementing the email API all you have to do by default is : * set id and accepts attributes to match desired events and entity types * set a content attribute to define the content of the email (unless you override call) """accepts=()id=Nonemsgid_timestamp=Truedefrecipients(self):finder=self.vreg.select_component('recipients_finder',req=self.req,rset=self.rset)returnfinder.recipients()defsubject(self):entity=self.entity(0,0)subject=self.req._(self.message)etype=entity.dc_type()eid=entity.eidlogin=self.user_login()returnself.req._('%(subject)s%(etype)s #%(eid)s (%(login)s)')%locals()defuser_login(self):# req is actually a session (we are on the server side), and we have to# prevent nested internal sessionreturnself.req.actual_session().user.logindefcontext(self,**kwargs):entity=self.entity(0,0)forkey,valinkwargs.iteritems():ifvalandval.strip():kwargs[key]=self.req._(val)kwargs.update({'user':self.user_login(),'eid':entity.eid,'etype':entity.dc_type(),'url':entity.absolute_url(),'title':entity.dc_long_title(),})returnkwargsdefcell_call(self,row,col=0,**kwargs):self.w(self.req._(self.content)%self.context(**kwargs))defconstruct_message_id(self,eid):returnconstruct_message_id(self.config.appid,eid,self.msgid_timestamp)defrender_and_send(self,**kwargs):"""generate and send an email message for this view"""self._kwargs=kwargsrecipients=self.recipients()ifnotrecipients:self.info('skipping %s%s notification which has no recipients',self.id,self.accepts)returnifnotisinstance(recipients[0],tuple):fromwarningsimportwarnwarn('recipients should now return a list of 2-uple (email, language)',DeprecationWarning,stacklevel=1)lang=self.vreg.property_value('ui.language')recipients=zip(recipients,repeat(lang))entity=self.entity(0,0)# if the view is using timestamp in message ids, no way to reference# previous emailifnotself.msgid_timestamp:refs=[self.construct_message_id(eid)foreidinentity.notification_references(self)]else:refs=()msgid=self.construct_message_id(entity.eid)userdata=self.req.user_data()origlang=self.req.langforemailaddr,langinrecipients:self.req.set_language(lang)# since the same view (eg self) may be called multiple time and we# need a fresh stream at each iteration, reset it explicitlyself.w=None# call dispatch before subject to set .row/.col attributes on the view :/content=self.dispatch(row=0,col=0,**kwargs)subject=self.subject()msg=format_mail(userdata,[emailaddr],content,subject,config=self.config,msgid=msgid,references=refs)self.send([emailaddr],msg)# restore languageself.req.set_language(origlang)defsend(self,recipients,msg):SendMailOp(self.req,recipients=recipients,msg=msg)defconstruct_message_id(appid,eid,withtimestamp=True):ifwithtimestamp:addrpart='eid=%s×tamp=%.10f'%(eid,time())else:addrpart='eid=%s'%eid# we don't want any equal sign nor trailing newlinesleftpart=b64encode(addrpart,'.-').rstrip().rstrip('=')return'<%s@%s.%s>'%(leftpart,appid,gethostname())defparse_message_id(msgid,appid):ifmsgid[0]=='<':msgid=msgid[1:]ifmsgid[-1]=='>':msgid=msgid[:-1]try:values,qualif=msgid.split('@')padding=len(values)%4values=b64decode(str(values+'='*padding),'.-')values=dict(v.split('=')forvinvalues.split('&'))fromappid,host=qualif.split('.',1)except:returnNoneifappid!=fromappidorhost!=gethostname():returnNonereturnvaluesclassStatusChangeMixIn(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""")classContentAddedMixIn(object):"""define emailcontent view for entity types for which you want to be notified """id='notif_after_add_entity'msgid_timestamp=Falsemessage=_('new')content="""%(title)s%(content)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?classNormalizedTextView(ContentAddedMixIn,NotificationView):defcontext(self,**kwargs):entity=self.entity(0,0)content=entity.printable_value(self.content_attr,format='text/plain')ifcontent:contentformat=getattr(entity,self.content_attr+'_format','text/rest')content=normalize_text(content,80,rest=contentformat=='text/rest')returnsuper(NormalizedTextView,self).context(content=content,**kwargs)defsubject(self):entity=self.entity(0,0)returnu'%s #%s (%s)'%(self.req.__('New %s'%entity.e_schema),entity.eid,self.user_login())classCardAddedView(NormalizedTextView):"""get notified from new cards"""accepts=('Card',)content_attr='synopsis'