sobjects/notification.py
changeset 2880 bfc8e1831290
parent 2841 107ba1c45227
parent 2879 ae26a80c0635
child 3023 7864fee8b4ec
equal deleted inserted replaced
2869:0cb160fd3cdf 2880:bfc8e1831290
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
     7 """
     7 """
     8 __docformat__ = "restructuredtext en"
     8 __docformat__ = "restructuredtext en"
     9 _ = unicode
     9 _ = unicode
    10 
    10 
    11 from base64 import b64encode, b64decode
       
    12 from itertools import repeat
    11 from itertools import repeat
    13 from time import time
       
    14 try:
       
    15     from socket import gethostname
       
    16 except ImportError:
       
    17     def gethostname(): # gae
       
    18         return 'XXX'
       
    19 
    12 
    20 from logilab.common.textutils import normalize_text
    13 from logilab.common.textutils import normalize_text
       
    14 from logilab.common.deprecation import class_renamed, deprecated
    21 
    15 
    22 from cubicweb.selectors import yes
    16 from cubicweb.selectors import yes
    23 from cubicweb.view import EntityView, Component
    17 from cubicweb.view import Component
    24 from cubicweb.common.mail import format_mail
    18 from cubicweb.common.mail import format_mail
    25 
    19 from cubicweb.common.mail import NotificationView
    26 from cubicweb.server.hookhelper import SendMailOp
    20 from cubicweb.server.hookhelper import SendMailOp
    27 
    21 
    28 
    22 
    29 class RecipientsFinder(Component):
    23 class RecipientsFinder(Component):
    30     """this component is responsible to find recipients of a notification
    24     """this component is responsible to find recipients of a notification
    53         return dests
    47         return dests
    54 
    48 
    55 
    49 
    56 # abstract or deactivated notification views and mixin ########################
    50 # abstract or deactivated notification views and mixin ########################
    57 
    51 
    58 class NotificationView(EntityView):
    52 class NotificationView(NotificationView):
    59     """abstract view implementing the email API
    53     """overriden to delay actual sending of mails to a commit operation by
    60 
    54     default
    61     all you have to do by default is :
       
    62     * set id and accepts attributes to match desired events and entity types
       
    63     * set a content attribute to define the content of the email (unless you
       
    64       override call)
       
    65     """
    55     """
    66     # XXX refactor this class to work with len(rset) > 1
    56     def send_on_commit(self, recipients, msg):
    67 
       
    68     msgid_timestamp = True
       
    69 
       
    70     def recipients(self):
       
    71         finder = self.vreg['components'].select('recipients_finder', self.req,
       
    72                                   rset=self.rset)
       
    73         return finder.recipients()
       
    74 
       
    75     def subject(self):
       
    76         entity = self.rset.get_entity(self.row or 0, self.col or 0)
       
    77         subject = self.req._(self.message)
       
    78         etype = entity.dc_type()
       
    79         eid = entity.eid
       
    80         login = self.user_login()
       
    81         return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
       
    82 
       
    83     def user_login(self):
       
    84         # req is actually a session (we are on the server side), and we have to
       
    85         # prevent nested internal session
       
    86         return self.req.actual_session().user.login
       
    87 
       
    88     def context(self, **kwargs):
       
    89         entity = self.rset.get_entity(self.row or 0, self.col or 0)
       
    90         for key, val in kwargs.iteritems():
       
    91             if val and isinstance(val, unicode) and val.strip():
       
    92                kwargs[key] = self.req._(val)
       
    93         kwargs.update({'user': self.user_login(),
       
    94                        'eid': entity.eid,
       
    95                        'etype': entity.dc_type(),
       
    96                        'url': entity.absolute_url(),
       
    97                        'title': entity.dc_long_title(),})
       
    98         return kwargs
       
    99 
       
   100     def cell_call(self, row, col=0, **kwargs):
       
   101         self.w(self.req._(self.content) % self.context(**kwargs))
       
   102 
       
   103     def construct_message_id(self, eid):
       
   104         return construct_message_id(self.req.vreg.config.appid, eid, self.msgid_timestamp)
       
   105 
       
   106     def render_and_send(self, **kwargs):
       
   107         """generate and send an email message for this view"""
       
   108         self._kwargs = kwargs
       
   109         recipients = self.recipients()
       
   110         if not recipients:
       
   111             self.info('skipping %s notification, no recipients', self.id)
       
   112             return
       
   113         if not isinstance(recipients[0], tuple):
       
   114             from warnings import warn
       
   115             warn('recipients should now return a list of 2-uple (email, language)',
       
   116                  DeprecationWarning, stacklevel=1)
       
   117             lang = self.vreg.property_value('ui.language')
       
   118             recipients = zip(recipients, repeat(lang))
       
   119         if self.rset is not None:
       
   120             entity = self.rset.get_entity(self.row or 0, self.col or 0)
       
   121             # if the view is using timestamp in message ids, no way to reference
       
   122             # previous email
       
   123             if not self.msgid_timestamp:
       
   124                 refs = [self.construct_message_id(eid)
       
   125                         for eid in entity.notification_references(self)]
       
   126             else:
       
   127                 refs = ()
       
   128             msgid = self.construct_message_id(entity.eid)
       
   129         else:
       
   130             refs = ()
       
   131             msgid = None
       
   132         userdata = self.req.user_data()
       
   133         origlang = self.req.lang
       
   134         for emailaddr, lang in recipients:
       
   135             self.req.set_language(lang)
       
   136             # since the same view (eg self) may be called multiple time and we
       
   137             # need a fresh stream at each iteration, reset it explicitly
       
   138             self.w = None
       
   139             # XXX call render before subject to set .row/.col attributes on the
       
   140             #     view
       
   141             content = self.render(row=0, col=0, **kwargs)
       
   142             subject = self.subject()
       
   143             msg = format_mail(userdata, [emailaddr], content, subject,
       
   144                               config=self.req.vreg.config, msgid=msgid, references=refs)
       
   145             self.send([emailaddr], msg)
       
   146         # restore language
       
   147         self.req.set_language(origlang)
       
   148 
       
   149     def send(self, recipients, msg):
       
   150         SendMailOp(self.req, recipients=recipients, msg=msg)
    57         SendMailOp(self.req, recipients=recipients, msg=msg)
   151 
    58     send = send_on_commit
   152 
       
   153 def construct_message_id(appid, eid, withtimestamp=True):
       
   154     if withtimestamp:
       
   155         addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
       
   156     else:
       
   157         addrpart = 'eid=%s' % eid
       
   158     # we don't want any equal sign nor trailing newlines
       
   159     leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
       
   160     return '<%s@%s.%s>' % (leftpart, appid, gethostname())
       
   161 
       
   162 
       
   163 def parse_message_id(msgid, appid):
       
   164     if msgid[0] == '<':
       
   165         msgid = msgid[1:]
       
   166     if msgid[-1] == '>':
       
   167         msgid = msgid[:-1]
       
   168     try:
       
   169         values, qualif = msgid.split('@')
       
   170         padding = len(values) % 4
       
   171         values = b64decode(str(values + '='*padding), '.-')
       
   172         values = dict(v.split('=') for v in values.split('&'))
       
   173         fromappid, host = qualif.split('.', 1)
       
   174     except:
       
   175         return None
       
   176     if appid != fromappid or host != gethostname():
       
   177         return None
       
   178     return values
       
   179 
    59 
   180 
    60 
   181 class StatusChangeMixIn(object):
    61 class StatusChangeMixIn(object):
   182     id = 'notif_status_change'
    62     id = 'notif_status_change'
   183     msgid_timestamp = True
    63     msgid_timestamp = True
   199 ###############################################################################
    79 ###############################################################################
   200 
    80 
   201 # XXX should be based on dc_title/dc_description, no?
    81 # XXX should be based on dc_title/dc_description, no?
   202 
    82 
   203 class ContentAddedView(NotificationView):
    83 class ContentAddedView(NotificationView):
       
    84     """abstract class for notification on entity/relation
       
    85 
       
    86     all you have to do by default is :
       
    87     * set id and __select__ attributes to match desired events and entity types
       
    88     * set a content attribute to define the content of the email (unless you
       
    89       override call)
       
    90     """
   204     __abstract__ = True
    91     __abstract__ = True
   205     id = 'notif_after_add_entity'
    92     id = 'notif_after_add_entity'
   206     msgid_timestamp = False
    93     msgid_timestamp = False
   207     message = _('new')
    94     message = _('new')
   208     content = """
    95     content = """
   224     def subject(self):
   111     def subject(self):
   225         entity = self.rset.get_entity(self.row or 0, self.col or 0)
   112         entity = self.rset.get_entity(self.row or 0, self.col or 0)
   226         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
   113         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
   227                                   entity.eid, self.user_login())
   114                                   entity.eid, self.user_login())
   228 
   115 
   229 from logilab.common.deprecation import class_renamed, class_moved
   116 
       
   117 from logilab.common.deprecation import class_renamed, class_moved, deprecated
       
   118 from cubicweb.hooks.notification import RenderAndSendNotificationView
       
   119 from cubicweb.common.mail import parse_message_id
       
   120 
   230 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
   121 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
   231 from cubicweb.hooks.notification import RenderAndSendNotificationView
       
   232 RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
   122 RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
       
   123 parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')