move base NotificationView to cw.common.mail, we may want to use it to send notification from the web ui stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 17 Aug 2009 16:41:12 +0200
branchstable
changeset 2879 ae26a80c0635
parent 2878 03244a6d0283
child 2880 bfc8e1831290
child 2897 2658f432284c
move base NotificationView to cw.common.mail, we may want to use it to send notification from the web ui
common/mail.py
sobjects/notification.py
--- a/common/mail.py	Mon Aug 17 13:18:45 2009 +0200
+++ b/common/mail.py	Mon Aug 17 16:41:12 2009 +0200
@@ -7,11 +7,20 @@
 """
 __docformat__ = "restructuredtext en"
 
+from base64 import b64encode, b64decode
+from itertools import repeat
+from time import time
 from email.MIMEMultipart import MIMEMultipart
 from email.MIMEText import MIMEText
 from email.MIMEImage import MIMEImage
 from email.Header import Header
+try:
+    from socket import gethostname
+except ImportError:
+    def gethostname(): # gae
+        return 'XXX'
 
+from cubicweb.view import EntityView
 
 def header(ustring):
     return Header(ustring.encode('UTF-8'), 'UTF-8')
@@ -25,6 +34,34 @@
     return addr
 
 
+def construct_message_id(appid, eid, withtimestamp=True):
+    if withtimestamp:
+        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
+    else:
+        addrpart = 'eid=%s' % eid
+    # we don't want any equal sign nor trailing newlines
+    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
+    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
+
+
+def parse_message_id(msgid, appid):
+    if msgid[0] == '<':
+        msgid = msgid[1:]
+    if msgid[-1] == '>':
+        msgid = msgid[:-1]
+    try:
+        values, qualif = msgid.split('@')
+        padding = len(values) % 4
+        values = b64decode(str(values + '='*padding), '.-')
+        values = dict(v.split('=') for v in values.split('&'))
+        fromappid, host = qualif.split('.', 1)
+    except:
+        return None
+    if appid != fromappid or host != gethostname():
+        return None
+    return values
+
+
 def format_mail(uinfo, to_addrs, content, subject="",
                 cc_addrs=(), msgid=None, references=(), config=None):
     """Sends an Email to 'e_addr' with content 'content', and subject 'subject'
@@ -94,3 +131,115 @@
         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
+
+    def user_login(self):
+        try:
+            # if req is actually a session (we are on the server side), and we
+            # have to prevent nested internal session
+            return self.req.actual_session().user.login
+        except AttributeError:
+            return self.req.user.login
+
+    def recipients(self):
+        finder = self.vreg['components'].select('recipients_finder', self.req,
+                                                rset=self.rset,
+                                                row=self.row or 0,
+                                                col=self.col or 0)
+        return finder.recipients()
+
+    def subject(self):
+        entity = self.entity(self.row or 0, self.col or 0)
+        subject = self.req._(self.message)
+        etype = entity.dc_type()
+        eid = entity.eid
+        login = self.user_login()
+        return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
+
+    def context(self, **kwargs):
+        entity = self.entity(self.row or 0, self.col or 0)
+        for key, val in kwargs.iteritems():
+            if val and isinstance(val, unicode) and val.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(),})
+        return kwargs
+
+    def cell_call(self, row, col=0, **kwargs):
+        self.w(self.req._(self.content) % self.context(**kwargs))
+
+    def construct_message_id(self, eid):
+        return construct_message_id(self.config.appid, eid, self.msgid_timestamp)
+
+    def render_emails(self, **kwargs):
+        """generate and send an email message for this view"""
+        self._kwargs = kwargs
+        recipients = self.recipients()
+        if not recipients:
+            self.info('skipping %s notification, no recipients', self.id)
+            return
+        if not isinstance(recipients[0], tuple):
+            from warnings import warn
+            warn('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))
+        if self.rset is not None:
+            entity = self.entity(self.row or 0, self.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.notification_references(self)]
+            else:
+                refs = ()
+            msgid = self.construct_message_id(entity.eid)
+        else:
+            refs = ()
+            msgid = None
+        userdata = self.req.user_data()
+        origlang = self.req.lang
+        for emailaddr, lang in recipients:
+            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 explicitly
+            self.w = None
+            # XXX call render before subject to set .row/.col attributes on the
+            #     view
+            content = self.render(row=0, col=0, **kwargs)
+            subject = self.subject()
+            msg = format_mail(userdata, [emailaddr], content, subject,
+                              config=self.config, msgid=msgid, references=refs)
+            yield [emailaddr], msg
+        # restore language
+        self.req.set_language(origlang)
+
+    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 send_now(self, recipients, msg):
+        self.config.sendmails([(msg, recipients)])
+
+    def send_on_commit(self, recipients, msg):
+        raise NotImplementedError
+
+    send = send_now
--- a/sobjects/notification.py	Mon Aug 17 13:18:45 2009 +0200
+++ b/sobjects/notification.py	Mon Aug 17 16:41:12 2009 +0200
@@ -8,27 +8,21 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from base64 import b64encode, b64decode
 from itertools import repeat
-from time import time
-try:
-    from socket import gethostname
-except ImportError:
-    def gethostname(): # gae
-        return 'XXX'
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed
+from logilab.common.deprecation import class_renamed, deprecated
 
 from cubicweb import RegistryException
 from cubicweb.selectors import implements, yes
-from cubicweb.view import EntityView, Component
-from cubicweb.common.mail import format_mail
-
+from cubicweb.view import Component
+from cubicweb.common.mail import NotificationView, parse_message_id
 from cubicweb.server.pool import PreCommitOperation
 from cubicweb.server.hookhelper import SendMailOp
 from cubicweb.server.hooksmanager import Hook
 
+parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')
+
 
 class RecipientsFinder(Component):
     """this component is responsible to find recipients of a notification
@@ -66,6 +60,7 @@
             return # entity added and deleted in the same transaction
         self.view.render_and_send(**getattr(self, 'viewargs', {}))
 
+
 class StatusChangeHook(Hook):
     """notify when a workflowable entity has its state modified"""
     events = ('after_add_entity',)
@@ -125,128 +120,14 @@
 
 # abstract or deactivated notification views and mixin ########################
 
-class NotificationView(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)
+class NotificationView(NotificationView):
+    """overriden to delay actual sending of mails to a commit operation by
+    default
     """
-    # XXX refactor this class to work with len(rset) > 1
-
-    msgid_timestamp = True
-
-    def recipients(self):
-        finder = self.vreg['components'].select('recipients_finder', self.req,
-                                  rset=self.rset)
-        return finder.recipients()
-
-    def subject(self):
-        entity = self.entity(self.row or 0, self.col or 0)
-        subject = self.req._(self.message)
-        etype = entity.dc_type()
-        eid = entity.eid
-        login = self.user_login()
-        return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
-
-    def user_login(self):
-        # req is actually a session (we are on the server side), and we have to
-        # prevent nested internal session
-        return self.req.actual_session().user.login
-
-    def context(self, **kwargs):
-        entity = self.entity(self.row or 0, self.col or 0)
-        for key, val in kwargs.iteritems():
-            if val and isinstance(val, unicode) and val.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(),})
-        return kwargs
-
-    def cell_call(self, row, col=0, **kwargs):
-        self.w(self.req._(self.content) % self.context(**kwargs))
-
-    def construct_message_id(self, eid):
-        return construct_message_id(self.config.appid, eid, self.msgid_timestamp)
 
-    def render_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        self._kwargs = kwargs
-        recipients = self.recipients()
-        if not recipients:
-            self.info('skipping %s notification, no recipients', self.id)
-            return
-        if not isinstance(recipients[0], tuple):
-            from warnings import warn
-            warn('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))
-        if self.rset is not None:
-            entity = self.entity(self.row or 0, self.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.notification_references(self)]
-            else:
-                refs = ()
-            msgid = self.construct_message_id(entity.eid)
-        else:
-            refs = ()
-            msgid = None
-        userdata = self.req.user_data()
-        origlang = self.req.lang
-        for emailaddr, lang in recipients:
-            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 explicitly
-            self.w = None
-            # XXX call render before subject to set .row/.col attributes on the
-            #     view
-            content = self.render(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 language
-        self.req.set_language(origlang)
-
-    def send(self, recipients, msg):
+    def send_on_commit(self, recipients, msg):
         SendMailOp(self.req, recipients=recipients, msg=msg)
-
-
-def construct_message_id(appid, eid, withtimestamp=True):
-    if withtimestamp:
-        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
-    else:
-        addrpart = 'eid=%s' % eid
-    # we don't want any equal sign nor trailing newlines
-    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
-    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
-
-
-def parse_message_id(msgid, appid):
-    if msgid[0] == '<':
-        msgid = msgid[1:]
-    if msgid[-1] == '>':
-        msgid = msgid[:-1]
-    try:
-        values, qualif = msgid.split('@')
-        padding = len(values) % 4
-        values = b64decode(str(values + '='*padding), '.-')
-        values = dict(v.split('=') for v in values.split('&'))
-        fromappid, host = qualif.split('.', 1)
-    except:
-        return None
-    if appid != fromappid or host != gethostname():
-        return None
-    return values
-
+    send = send_on_commit
 
 class StatusChangeMixIn(object):
     id = 'notif_status_change'
@@ -271,6 +152,13 @@
 # XXX should be based on dc_title/dc_description, no?
 
 class ContentAddedView(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
     id = 'notif_after_add_entity'
     msgid_timestamp = False