# HG changeset patch # User Pierre-Yves David # Date 1366889688 -7200 # Node ID 4b195bd82e8bdf4d3cdaf48bbfedeb35ceeea911 # Parent 6a02be3044860aa6744fe8607111cdc7896a7b1e [notification] move notification view in ``sobject.notification`` It has no user outside this module. This enforce serversideness of notification and allow future cleanup. No backward compat is set up to prevent circular import. The class has no other user anyway. (closes 2845144) diff -r 6a02be304486 -r 4b195bd82e8b mail.py --- a/mail.py Thu Apr 25 14:10:55 2013 +0200 +++ b/mail.py Thu Apr 25 13:34:48 2013 +0200 @@ -27,9 +27,6 @@ from email.header import Header from socket import gethostname -from cubicweb.view import EntityView -from cubicweb.entity import Entity - def header(ustring): return Header(ustring.encode('UTF-8'), 'UTF-8') @@ -142,140 +139,3 @@ image = MIMEImage(data) image.add_header('Content-ID', '<%s>' % htmlId) self.attach(image) - - -class NotificationView(EntityView): - """abstract view implementing the "email" API (eg to simplify sending - notification) - """ - # XXX refactor this class to work with len(rset) > 1 - - msgid_timestamp = True - - # to be defined on concrete sub-classes - content = None # body of the mail - message = None # action verb of the subject - - # this is usually the method to call - def render_and_send(self, **kwargs): - """generate and send an email message for this view""" - delayed = kwargs.pop('delay_to_commit', None) - for recipients, msg in self.render_emails(**kwargs): - if delayed is None: - self.send(recipients, msg) - elif delayed: - self.send_on_commit(recipients, msg) - else: - self.send_now(recipients, msg) - - def cell_call(self, row, col=0, **kwargs): - self.w(self._cw._(self.content) % self.context(**kwargs)) - - def render_emails(self, **kwargs): - """generate and send emails for this view (one per recipient)""" - self._kwargs = kwargs - recipients = self.recipients() - if not recipients: - self.info('skipping %s notification, no recipients', self.__regid__) - return - if self.cw_rset is not None: - entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) - # if the view is using timestamp in message ids, no way to reference - # previous email - if not self.msgid_timestamp: - refs = [self.construct_message_id(eid) - for eid in entity.cw_adapt_to('INotifiable').notification_references(self)] - else: - refs = () - msgid = self.construct_message_id(entity.eid) - else: - refs = () - msgid = None - req = self._cw - self.user_data = req.user_data() - origlang = req.lang - for something in recipients: - if isinstance(something, Entity): - # hi-jack self._cw to get a session for the returned user - self._cw = self._cw.hijack_user(something) - emailaddr = something.cw_adapt_to('IEmailable').get_email() - else: - emailaddr, lang = something - self._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 explicitly - self.w = None - # XXX call render before subject to set .row/.col attributes on the - # view - try: - content = self.render(row=0, col=0, **kwargs) - subject = self.subject() - except SkipEmail: - continue - except Exception as ex: - # 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 error - self.exception(str(ex)) - continue - msg = format_mail(self.user_data, [emailaddr], content, subject, - config=self._cw.vreg.config, msgid=msgid, references=refs) - yield [emailaddr], msg - # restore language - req.set_language(origlang) - - # recipients / email sending ############################################### - - def recipients(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_row or 0, col=self.cw_col or 0) - return finder.recipients() - - def send_now(self, recipients, msg): - self._cw.vreg.config.sendmails([(msg, recipients)]) - - def send_on_commit(self, recipients, msg): - raise NotImplementedError - - send = send_now - - # email generation helpers ################################################# - - def construct_message_id(self, eid): - return construct_message_id(self._cw.vreg.config.appid, eid, - self.msgid_timestamp) - - def format_field(self, attr, value): - return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value} - - def format_section(self, attr, value): - return '%(attr)s\n%(ul)s\n%(value)s\n' % { - 'attr': attr, 'ul': '-'*len(attr), 'value': value} - - def subject(self): - entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) - subject = self._cw._(self.message) - etype = entity.dc_type() - eid = entity.eid - login = self.user_data['login'] - return self._cw._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() - - def context(self, **kwargs): - entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) - for key, val in kwargs.iteritems(): - if val and isinstance(val, unicode) and val.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(),}) - return kwargs - - -class SkipEmail(Exception): - """raise this if you decide to skip an email during its generation""" diff -r 6a02be304486 -r 4b195bd82e8b sobjects/notification.py --- a/sobjects/notification.py Thu Apr 25 14:10:55 2013 +0200 +++ b/sobjects/notification.py Thu Apr 25 13:34:48 2013 +0200 @@ -26,9 +26,10 @@ from logilab.common.deprecation import class_renamed, class_moved, deprecated from logilab.common.registry import yes -from cubicweb.view import Component -from cubicweb.mail import NotificationView as BaseNotificationView, SkipEmail +from cubicweb.entity import Entity +from cubicweb.view import Component, EntityView from cubicweb.server.hook import SendMailOp +from cubicweb.mail import construct_message_id, format_mail class RecipientsFinder(Component): @@ -59,6 +60,146 @@ # abstract or deactivated notification views and mixin ######################## + +class SkipEmail(Exception): + """raise this if you decide to skip an email during its generation""" + + +class BaseNotificationView(EntityView): + """abstract view implementing the "email" API (eg to simplify sending + notification) + """ + __abstract__ = True + + # XXX refactor this class to work with len(rset) > 1 + + msgid_timestamp = True + + # to be defined on concrete sub-classes + content = None # body of the mail + message = None # action verb of the subject + + # this is usually the method to call + def render_and_send(self, **kwargs): + """generate and send an email message for this view""" + delayed = kwargs.pop('delay_to_commit', None) + for recipients, msg in self.render_emails(**kwargs): + if delayed is None: + self.send(recipients, msg) + elif delayed: + self.send_on_commit(recipients, msg) + else: + self.send_now(recipients, msg) + + def cell_call(self, row, col=0, **kwargs): + self.w(self._cw._(self.content) % self.context(**kwargs)) + + def render_emails(self, **kwargs): + """generate and send emails for this view (one per recipient)""" + self._kwargs = kwargs + recipients = self.recipients() + if not recipients: + self.info('skipping %s notification, no recipients', self.__regid__) + return + if self.cw_rset is not None: + entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + # if the view is using timestamp in message ids, no way to reference + # previous email + if not self.msgid_timestamp: + refs = [self.construct_message_id(eid) + for eid in entity.cw_adapt_to('INotifiable').notification_references(self)] + else: + refs = () + msgid = self.construct_message_id(entity.eid) + else: + refs = () + msgid = None + req = self._cw + self.user_data = req.user_data() + origlang = req.lang + for something in recipients: + if isinstance(something, Entity): + # hi-jack self._cw to get a session for the returned user + self._cw = self._cw.hijack_user(something) + emailaddr = something.cw_adapt_to('IEmailable').get_email() + else: + emailaddr, lang = something + self._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 explicitly + self.w = None + # XXX call render before subject to set .row/.col attributes on the + # view + try: + content = self.render(row=0, col=0, **kwargs) + subject = self.subject() + except SkipEmail: + continue + except Exception as ex: + # 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 error + self.exception(str(ex)) + continue + msg = format_mail(self.user_data, [emailaddr], content, subject, + config=self._cw.vreg.config, msgid=msgid, references=refs) + yield [emailaddr], msg + # restore language + req.set_language(origlang) + + # recipients / email sending ############################################### + + def recipients(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_row or 0, col=self.cw_col or 0) + return finder.recipients() + + def send_now(self, recipients, msg): + self._cw.vreg.config.sendmails([(msg, recipients)]) + + def send_on_commit(self, recipients, msg): + raise NotImplementedError + + send = send_now + + # email generation helpers ################################################# + + def construct_message_id(self, eid): + return construct_message_id(self._cw.vreg.config.appid, eid, + self.msgid_timestamp) + + def format_field(self, attr, value): + return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value} + + def format_section(self, attr, value): + return '%(attr)s\n%(ul)s\n%(value)s\n' % { + 'attr': attr, 'ul': '-'*len(attr), 'value': value} + + def subject(self): + entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + subject = self._cw._(self.message) + etype = entity.dc_type() + eid = entity.eid + login = self.user_data['login'] + return self._cw._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() + + def context(self, **kwargs): + entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + for key, val in kwargs.iteritems(): + if val and isinstance(val, unicode) and val.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(),}) + return kwargs + + class NotificationView(BaseNotificationView): """overriden to delay actual sending of mails to a commit operation by default diff -r 6a02be304486 -r 4b195bd82e8b sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Thu Apr 25 14:10:55 2013 +0200 +++ b/sobjects/test/unittest_notification.py Thu Apr 25 13:34:48 2013 +0200 @@ -85,7 +85,8 @@ def test_status_change_view(self): req = self.request() u = self.create_user(req, 'toto') - u.cw_adapt_to('IWorkflowable').fire_transition('deactivate', comment=u'yeah') + iwfable = u.cw_adapt_to('IWorkflowable') + iwfable.fire_transition('deactivate', comment=u'yeah') self.assertFalse(MAILBOX) self.commit() self.assertEqual(len(MAILBOX), 1) @@ -99,7 +100,8 @@ url: http://testing.fr/cubicweb/cwuser/toto ''') - self.assertEqual(email.subject, 'status changed CWUser #%s (admin)' % u.eid) + self.assertEqual(email.subject, + 'status changed CWUser #%s (admin)' % u.eid) if __name__ == '__main__': unittest_main()