# HG changeset patch # User Sylvain Thénault # Date 1250520072 -7200 # Node ID ae26a80c0635daabb827d66c076b886fef1376b5 # Parent 03244a6d0283cd6c97284d8dc7c845b62462c2dc move base NotificationView to cw.common.mail, we may want to use it to send notification from the web ui diff -r 03244a6d0283 -r ae26a80c0635 common/mail.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×tamp=%.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 diff -r 03244a6d0283 -r ae26a80c0635 sobjects/notification.py --- 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×tamp=%.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