# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""some views to handle notification on data changes"""__docformat__="restructuredtext en"_=unicodefromitertoolsimportrepeatfromlogilab.common.textutilsimportnormalize_textfromlogilab.common.deprecationimportclass_renamed,class_moved,deprecatedfromlogilab.common.registryimportyesfromcubicweb.entityimportEntityfromcubicweb.viewimportComponent,EntityViewfromcubicweb.server.hookimportSendMailOpfromcubicweb.mailimportconstruct_message_id,format_mailfromcubicweb.server.sessionimportSessionclassRecipientsFinder(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 """__regid__='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._cw.vreg.config['default-recipients-mode']ifmode=='users':execute=self._cw.executedests=[(u.cw_adapt_to('IEmailable').get_email(),u.property_value('ui.language'))foruinexecute(self.user_rql,build_descr=True).entities()]elifmode=='default-dest-addrs':lang=self._cw.vreg.property_value('ui.language')dests=zip(self._cw.vreg.config['default-dest-addrs'],repeat(lang))else:# mode == 'none'dests=[]returndests# abstract or deactivated notification views and mixin ########################classSkipEmail(Exception):"""raise this if you decide to skip an email during its generation"""classNotificationView(EntityView):"""abstract view implementing the "email" API (eg to simplify sending notification) """# XXX refactor this class to work with len(rset) > 1msgid_timestamp=True# to be defined on concrete sub-classescontent=None# body of the mailmessage=None# action verb of the subject# this is usually the method to calldefrender_and_send(self,**kwargs):"""generate and send an email message for this view"""delayed=kwargs.pop('delay_to_commit',None)forrecipients,msginself.render_emails(**kwargs):ifdelayedisNone:self.send(recipients,msg)elifdelayed:self.send_on_commit(recipients,msg)else:self.send_now(recipients,msg)defcell_call(self,row,col=0,**kwargs):self.w(self._cw._(self.content)%self.context(**kwargs))defrender_emails(self,**kwargs):"""generate and send emails for this view (one per recipient)"""self._kwargs=kwargsrecipients=self.recipients()ifnotrecipients:self.info('skipping %s notification, no recipients',self.__regid__)returnifself.cw_rsetisnotNone:entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)# if the view is using timestamp in message ids, no way to reference# previous emailifnotself.msgid_timestamp:refs=[self.construct_message_id(eid)foreidinentity.cw_adapt_to('INotifiable').notification_references(self)]else:refs=()msgid=self.construct_message_id(entity.eid)else:refs=()msgid=Nonereq=self._cwself.user_data=req.user_data()origlang=req.langforsomethinginrecipients:ifisinstance(something,Entity):# hi-jack self._cw to get a session for the returned userself._cw=Session(self._cw.repo,something)emailaddr=something.cw_adapt_to('IEmailable').get_email()else:emailaddr,lang=somethingself._cw.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# XXX call render before subject to set .row/.col attributes on the# viewtry:content=self.render(row=0,col=0,**kwargs)subject=self.subject()exceptSkipEmail:continueexceptExceptionasex:# shouldn't make the whole transaction fail because of rendering# error (unauthorized or such) XXX check it doesn't actually# occurs due to rollback on such errorself.exception(str(ex))continuemsg=format_mail(self.user_data,[emailaddr],content,subject,config=self._cw.vreg.config,msgid=msgid,references=refs)yield[emailaddr],msg# restore languagereq.set_language(origlang)# recipients / email sending ###############################################defrecipients(self):"""return a list of either 2-uple (email, language) or user entity to who this email should be sent """finder=self._cw.vreg['components'].select('recipients_finder',self._cw,rset=self.cw_rset,row=self.cw_rowor0,col=self.cw_color0)returnfinder.recipients()defsend_now(self,recipients,msg):self._cw.vreg.config.sendmails([(msg,recipients)])defsend_on_commit(self,recipients,msg):SendMailOp(self._cw,recipients=recipients,msg=msg)send=send_on_commit# email generation helpers #################################################defconstruct_message_id(self,eid):returnconstruct_message_id(self._cw.vreg.config.appid,eid,self.msgid_timestamp)defformat_field(self,attr,value):return':%(attr)s: %(value)s'%{'attr':attr,'value':value}defformat_section(self,attr,value):return'%(attr)s\n%(ul)s\n%(value)s\n'%{'attr':attr,'ul':'-'*len(attr),'value':value}defsubject(self):entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)subject=self._cw._(self.message)etype=entity.dc_type()eid=entity.eidlogin=self.user_data['login']returnself._cw._('%(subject)s%(etype)s #%(eid)s (%(login)s)')%locals()defcontext(self,**kwargs):entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)forkey,valinkwargs.iteritems():ifvalandisinstance(val,unicode)andval.strip():kwargs[key]=self._cw._(val)kwargs.update({'user':self.user_data['login'],'eid':entity.eid,'etype':entity.dc_type(),'url':entity.absolute_url(),'title':entity.dc_long_title(),})returnkwargsclassStatusChangeMixIn(object):__regid__='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__=True__regid__='notif_after_add_entity'msgid_timestamp=Falsemessage=_('new')content="""%(title)s%(content)surl: %(url)s"""# to be defined on concrete sub-classescontent_attr=Nonedefcontext(self,**kwargs):entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_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.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)returnu'%s #%s (%s)'%(self._cw.__('New %s'%entity.e_schema),entity.eid,self.user_data['login'])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__=True__regid__='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._cw.transaction_data['changes'][self.cw_rset[0][0]]_=self._cw._formatted_changes=[]entity=self.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)forattr,oldvalue,newvalueinsorted(changes):# check current user has permission to see the attributerschema=self._cw.vreg.schema[attr]ifrschema.final:rdef=entity.e_schema.rdef(rschema)ifnotrdef.has_perm(self._cw,'read',eid=self.cw_rset[0][0]):continue# XXX suppose it's a subject relation...elifnotrschema.has_perm(self._cw,'read',fromeid=self.cw_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.cw_rset.get_entity(self.cw_rowor0,self.cw_color0)returnu'%s #%s (%s)'%(self._cw.__('Updated %s'%entity.e_schema),entity.eid,self.user_data['login'])