sobjects/notification.py
branchstable
changeset 2879 ae26a80c0635
parent 2650 18aec79ec3a3
child 2880 bfc8e1831290
child 3004 09ab5e93a02c
equal deleted inserted replaced
2878:03244a6d0283 2879:ae26a80c0635
     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
    21 from logilab.common.deprecation import class_renamed
    14 from logilab.common.deprecation import class_renamed, deprecated
    22 
    15 
    23 from cubicweb import RegistryException
    16 from cubicweb import RegistryException
    24 from cubicweb.selectors import implements, yes
    17 from cubicweb.selectors import implements, yes
    25 from cubicweb.view import EntityView, Component
    18 from cubicweb.view import Component
    26 from cubicweb.common.mail import format_mail
    19 from cubicweb.common.mail import NotificationView, parse_message_id
    27 
       
    28 from cubicweb.server.pool import PreCommitOperation
    20 from cubicweb.server.pool import PreCommitOperation
    29 from cubicweb.server.hookhelper import SendMailOp
    21 from cubicweb.server.hookhelper import SendMailOp
    30 from cubicweb.server.hooksmanager import Hook
    22 from cubicweb.server.hooksmanager import Hook
       
    23 
       
    24 parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')
    31 
    25 
    32 
    26 
    33 class RecipientsFinder(Component):
    27 class RecipientsFinder(Component):
    34     """this component is responsible to find recipients of a notification
    28     """this component is responsible to find recipients of a notification
    35 
    29 
    63     """delay rendering of notification view until precommit"""
    57     """delay rendering of notification view until precommit"""
    64     def precommit_event(self):
    58     def precommit_event(self):
    65         if self.view.rset and self.view.rset[0][0] in self.session.transaction_data.get('pendingeids', ()):
    59         if self.view.rset and self.view.rset[0][0] in self.session.transaction_data.get('pendingeids', ()):
    66             return # entity added and deleted in the same transaction
    60             return # entity added and deleted in the same transaction
    67         self.view.render_and_send(**getattr(self, 'viewargs', {}))
    61         self.view.render_and_send(**getattr(self, 'viewargs', {}))
       
    62 
    68 
    63 
    69 class StatusChangeHook(Hook):
    64 class StatusChangeHook(Hook):
    70     """notify when a workflowable entity has its state modified"""
    65     """notify when a workflowable entity has its state modified"""
    71     events = ('after_add_entity',)
    66     events = ('after_add_entity',)
    72     accepts = ('TrInfo',)
    67     accepts = ('TrInfo',)
   123         RenderAndSendNotificationView(session, view=view)
   118         RenderAndSendNotificationView(session, view=view)
   124 
   119 
   125 
   120 
   126 # abstract or deactivated notification views and mixin ########################
   121 # abstract or deactivated notification views and mixin ########################
   127 
   122 
   128 class NotificationView(EntityView):
   123 class NotificationView(NotificationView):
   129     """abstract view implementing the email API
   124     """overriden to delay actual sending of mails to a commit operation by
       
   125     default
       
   126     """
   130 
   127 
   131     all you have to do by default is :
   128     def send_on_commit(self, recipients, msg):
   132     * set id and accepts attributes to match desired events and entity types
       
   133     * set a content attribute to define the content of the email (unless you
       
   134       override call)
       
   135     """
       
   136     # XXX refactor this class to work with len(rset) > 1
       
   137 
       
   138     msgid_timestamp = True
       
   139 
       
   140     def recipients(self):
       
   141         finder = self.vreg['components'].select('recipients_finder', self.req,
       
   142                                   rset=self.rset)
       
   143         return finder.recipients()
       
   144 
       
   145     def subject(self):
       
   146         entity = self.entity(self.row or 0, self.col or 0)
       
   147         subject = self.req._(self.message)
       
   148         etype = entity.dc_type()
       
   149         eid = entity.eid
       
   150         login = self.user_login()
       
   151         return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
       
   152 
       
   153     def user_login(self):
       
   154         # req is actually a session (we are on the server side), and we have to
       
   155         # prevent nested internal session
       
   156         return self.req.actual_session().user.login
       
   157 
       
   158     def context(self, **kwargs):
       
   159         entity = self.entity(self.row or 0, self.col or 0)
       
   160         for key, val in kwargs.iteritems():
       
   161             if val and isinstance(val, unicode) and val.strip():
       
   162                kwargs[key] = self.req._(val)
       
   163         kwargs.update({'user': self.user_login(),
       
   164                        'eid': entity.eid,
       
   165                        'etype': entity.dc_type(),
       
   166                        'url': entity.absolute_url(),
       
   167                        'title': entity.dc_long_title(),})
       
   168         return kwargs
       
   169 
       
   170     def cell_call(self, row, col=0, **kwargs):
       
   171         self.w(self.req._(self.content) % self.context(**kwargs))
       
   172 
       
   173     def construct_message_id(self, eid):
       
   174         return construct_message_id(self.config.appid, eid, self.msgid_timestamp)
       
   175 
       
   176     def render_and_send(self, **kwargs):
       
   177         """generate and send an email message for this view"""
       
   178         self._kwargs = kwargs
       
   179         recipients = self.recipients()
       
   180         if not recipients:
       
   181             self.info('skipping %s notification, no recipients', self.id)
       
   182             return
       
   183         if not isinstance(recipients[0], tuple):
       
   184             from warnings import warn
       
   185             warn('recipients should now return a list of 2-uple (email, language)',
       
   186                  DeprecationWarning, stacklevel=1)
       
   187             lang = self.vreg.property_value('ui.language')
       
   188             recipients = zip(recipients, repeat(lang))
       
   189         if self.rset is not None:
       
   190             entity = self.entity(self.row or 0, self.col or 0)
       
   191             # if the view is using timestamp in message ids, no way to reference
       
   192             # previous email
       
   193             if not self.msgid_timestamp:
       
   194                 refs = [self.construct_message_id(eid)
       
   195                         for eid in entity.notification_references(self)]
       
   196             else:
       
   197                 refs = ()
       
   198             msgid = self.construct_message_id(entity.eid)
       
   199         else:
       
   200             refs = ()
       
   201             msgid = None
       
   202         userdata = self.req.user_data()
       
   203         origlang = self.req.lang
       
   204         for emailaddr, lang in recipients:
       
   205             self.req.set_language(lang)
       
   206             # since the same view (eg self) may be called multiple time and we
       
   207             # need a fresh stream at each iteration, reset it explicitly
       
   208             self.w = None
       
   209             # XXX call render before subject to set .row/.col attributes on the
       
   210             #     view
       
   211             content = self.render(row=0, col=0, **kwargs)
       
   212             subject = self.subject()
       
   213             msg = format_mail(userdata, [emailaddr], content, subject,
       
   214                               config=self.config, msgid=msgid, references=refs)
       
   215             self.send([emailaddr], msg)
       
   216         # restore language
       
   217         self.req.set_language(origlang)
       
   218 
       
   219     def send(self, recipients, msg):
       
   220         SendMailOp(self.req, recipients=recipients, msg=msg)
   129         SendMailOp(self.req, recipients=recipients, msg=msg)
   221 
   130     send = send_on_commit
   222 
       
   223 def construct_message_id(appid, eid, withtimestamp=True):
       
   224     if withtimestamp:
       
   225         addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
       
   226     else:
       
   227         addrpart = 'eid=%s' % eid
       
   228     # we don't want any equal sign nor trailing newlines
       
   229     leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
       
   230     return '<%s@%s.%s>' % (leftpart, appid, gethostname())
       
   231 
       
   232 
       
   233 def parse_message_id(msgid, appid):
       
   234     if msgid[0] == '<':
       
   235         msgid = msgid[1:]
       
   236     if msgid[-1] == '>':
       
   237         msgid = msgid[:-1]
       
   238     try:
       
   239         values, qualif = msgid.split('@')
       
   240         padding = len(values) % 4
       
   241         values = b64decode(str(values + '='*padding), '.-')
       
   242         values = dict(v.split('=') for v in values.split('&'))
       
   243         fromappid, host = qualif.split('.', 1)
       
   244     except:
       
   245         return None
       
   246     if appid != fromappid or host != gethostname():
       
   247         return None
       
   248     return values
       
   249 
       
   250 
   131 
   251 class StatusChangeMixIn(object):
   132 class StatusChangeMixIn(object):
   252     id = 'notif_status_change'
   133     id = 'notif_status_change'
   253     msgid_timestamp = True
   134     msgid_timestamp = True
   254     message = _('status changed')
   135     message = _('status changed')
   269 ###############################################################################
   150 ###############################################################################
   270 
   151 
   271 # XXX should be based on dc_title/dc_description, no?
   152 # XXX should be based on dc_title/dc_description, no?
   272 
   153 
   273 class ContentAddedView(NotificationView):
   154 class ContentAddedView(NotificationView):
       
   155     """abstract class for notification on entity/relation
       
   156 
       
   157     all you have to do by default is :
       
   158     * set id and __select__ attributes to match desired events and entity types
       
   159     * set a content attribute to define the content of the email (unless you
       
   160       override call)
       
   161     """
   274     __abstract__ = True
   162     __abstract__ = True
   275     id = 'notif_after_add_entity'
   163     id = 'notif_after_add_entity'
   276     msgid_timestamp = False
   164     msgid_timestamp = False
   277     message = _('new')
   165     message = _('new')
   278     content = """
   166     content = """