backport stable branch
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 17 Aug 2009 18:25:57 +0200
changeset 2880 bfc8e1831290
parent 2869 0cb160fd3cdf (current diff)
parent 2879 ae26a80c0635 (diff)
child 2881 d1a5b77e42bc
backport stable branch
entity.py
hooks/workflow.py
server/hookhelper.py
server/repository.py
server/session.py
sobjects/notification.py
web/views/basecontrollers.py
--- a/common/mail.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/common/mail.py	Mon Aug 17 18:25:57 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/entity.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/entity.py	Mon Aug 17 18:25:57 2009 +0200
@@ -152,11 +152,16 @@
                 desttype = rschema.objects(eschema.type)[0]
                 card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
                 if card not in '?1':
+                    self.warning('bad relation %s specified in fetch attrs for %s',
+                                 attr, self.__class__)
                     selection.pop()
                     restrictions.pop()
                     continue
-                if card == '?':
-                    restrictions[-1] += '?' # left outer join if not mandatory
+                # XXX we need outer join in case the relation is not mandatory
+                # (card == '?')  *or if the entity is being added*, since in
+                # that case the relation may still be missing. As we miss this
+                # later information here, systematically add it.
+                restrictions[-1] += '?'
                 # XXX user.req.vreg iiiirk
                 destcls = user.req.vreg['etypes'].etype_class(desttype)
                 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
@@ -709,7 +714,7 @@
 
     # raw edition utilities ###################################################
 
-    def set_attributes(self, **kwargs):
+    def set_attributes(self, _cw_unsafe=False, **kwargs):
         assert kwargs
         relations = []
         for key in kwargs:
@@ -718,8 +723,12 @@
         self.update(kwargs)
         # and now update the database
         kwargs['x'] = self.eid
-        self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
-                         kwargs, 'x')
+        if _cw_unsafe:
+            self.req.unsafe_execute(
+                'SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x')
+        else:
+            self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
+                             kwargs, 'x')
 
     def delete(self):
         assert self.has_eid(), self.eid
@@ -820,7 +829,8 @@
 
     def __set__(self, eobj, value):
         eobj[self._attrname] = value
-
+        if hasattr(eobj, 'edited_attributes'):
+            eobj.edited_attributes.add(self._attrname)
 
 class Relation(object):
     """descriptor that controls schema relation access"""
--- a/hooks/workflow.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/hooks/workflow.py	Mon Aug 17 18:25:57 2009 +0200
@@ -15,14 +15,16 @@
 from cubicweb.server import hook
 
 
+
 def previous_state(session, eid):
     """return the state of the entity with the given eid,
     usually since it's changing in the current transaction. Due to internal
     relation hooks, the relation may has been deleted at this point, so
     we have handle that
     """
-    if session.added_in_transaction(eid):
-        return
+    # don't check eid has been added in the current transaction, we don't want
+    # to miss previous state of entity whose state change in the same
+    # transaction as it's being created
     pending = session.transaction_data.get('pendingrelations', ())
     for eidfrom, rtype, eidto in reversed(pending):
         if rtype == 'in_state' and eidfrom == eid:
@@ -139,7 +141,8 @@
             return
         entity = self._cw.entity_from_eid(self.eidfrom)
         try:
-            entity.set_attributes(modification_date=datetime.now())
+            entity.set_attributes(modification_date=datetime.now(),
+                                  _cw_unsafe=True)
         except RepositoryError, ex:
             # usually occurs if entity is coming from a read-only source
             # (eg ldap user)
--- a/server/repository.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/server/repository.py	Mon Aug 17 18:25:57 2009 +0200
@@ -1011,7 +1011,7 @@
         session.set_entity_cache(entity)
         only_inline_rels, need_fti_update = True, False
         relations = []
-        for attr in entity.keys():
+        for attr in edited_attributes:
             if attr == 'eid':
                 continue
             rschema = eschema.subject_relation(attr)
@@ -1021,8 +1021,8 @@
                 only_inline_rels = False
             else:
                 # inlined relation
-                previous_value = entity.related(attr)
-                if previous_value:
+                previous_value = entity.related(attr) or None
+                if previous_value is not None:
                     previous_value = previous_value[0][0] # got a result set
                     if previous_value == entity[attr]:
                         previous_value = None
@@ -1051,7 +1051,7 @@
             for attr, value, prevvalue in relations:
                 # if the relation is already cached, update existant cache
                 relcache = entity.relation_cached(attr, 'subject')
-                if prevvalue:
+                if prevvalue is not None:
                     self.hm.call_hooks('after_delete_relation', session,
                                        eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
                     if relcache is not None:
--- a/server/session.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/server/session.py	Mon Aug 17 18:25:57 2009 +0200
@@ -344,7 +344,7 @@
         try:
             csession = self._threaddata.childsession
         except AttributeError:
-            if self.is_super_session:
+            if isinstance(self, (ChildSession, InternalSession)):
                 csession = self
             else:
                 csession = ChildSession(self)
--- a/server/ssplanner.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/server/ssplanner.py	Mon Aug 17 18:25:57 2009 +0200
@@ -482,7 +482,7 @@
         repo = session.repo
         edefs = {}
         # insert relations
-        attributes = [relation.r_type for relation in self.attribute_relations]
+        attributes = set([relation.r_type for relation in self.attribute_relations])
         for row in self.execute_child():
             for relation in self.attribute_relations:
                 lhs, rhs = relation.get_variable_parts()
--- a/sobjects/notification.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/sobjects/notification.py	Mon Aug 17 18:25:57 2009 +0200
@@ -8,21 +8,15 @@
 __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, deprecated
 
 from cubicweb.selectors import yes
-from cubicweb.view import EntityView, Component
+from cubicweb.view import Component
 from cubicweb.common.mail import format_mail
-
+from cubicweb.common.mail import NotificationView
 from cubicweb.server.hookhelper import SendMailOp
 
 
@@ -55,127 +49,13 @@
 
 # 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.rset.get_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.rset.get_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.req.vreg.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.rset.get_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.req.vreg.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):
@@ -201,6 +81,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
@@ -226,7 +113,11 @@
         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
                                   entity.eid, self.user_login())
 
-from logilab.common.deprecation import class_renamed, class_moved
+
+from logilab.common.deprecation import class_renamed, class_moved, deprecated
+from cubicweb.hooks.notification import RenderAndSendNotificationView
+from cubicweb.common.mail import parse_message_id
+
 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
-from cubicweb.hooks.notification import RenderAndSendNotificationView
 RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
+parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')
--- a/web/views/basecontrollers.py	Sun Aug 16 20:42:33 2009 +0200
+++ b/web/views/basecontrollers.py	Mon Aug 17 18:25:57 2009 +0200
@@ -310,7 +310,7 @@
                 vtitle = self.req.form.get('vtitle')
                 if vtitle:
                     stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
-            view.pagination(req, self.rset, view.w, not view.need_navigation)
+            view.paginate()
             if divid == 'pageContent':
                 stream.write(u'<div id="contentmain">')
         view.render(**kwargs)