merge stable
authorAlexandre Fayolle <alexandre.fayolle@logilab.fr>
Sun, 11 Apr 2010 11:59:45 +0000
branchstable
changeset 5211 8fac9cd3c510
parent 5210 1c635a91e403 (current diff)
parent 5203 0b26a941410f (diff)
child 5212 a545eb9add6f
child 5214 3285b6e3b930
merge
--- a/cwconfig.py	Fri Apr 09 15:01:25 2010 +0000
+++ b/cwconfig.py	Sun Apr 11 11:59:45 2010 +0000
@@ -1019,7 +1019,9 @@
         return i18n.compile_i18n_catalogs(sourcedirs, i18ndir, langs)
 
     def sendmails(self, msgs):
-        """msgs: list of 2-uple (message object, recipients)"""
+        """msgs: list of 2-uple (message object, recipients). Return False
+        if connection to the smtp server failed, else True.
+        """
         server, port = self['smtp-host'], self['smtp-port']
         SMTP_LOCK.acquire()
         try:
@@ -1028,7 +1030,7 @@
             except Exception, ex:
                 self.exception("can't connect to smtp server %s:%s (%s)",
                                server, port, ex)
-                return
+                return False
             heloaddr = '%s <%s>' % (self['sender-name'], self['sender-addr'])
             for msg, recipients in msgs:
                 try:
@@ -1039,6 +1041,7 @@
             smtp.close()
         finally:
             SMTP_LOCK.release()
+        return True
 
 set_log_methods(CubicWebConfiguration, logging.getLogger('cubicweb.configuration'))
 
--- a/doc/book/en/development/devrepo/hooks.rst	Fri Apr 09 15:01:25 2010 +0000
+++ b/doc/book/en/development/devrepo/hooks.rst	Sun Apr 11 11:59:45 2010 +0000
@@ -5,28 +5,29 @@
 Hooks and Operations
 ====================
 
-Principles
-----------
+Generalities
+------------
 
 Paraphrasing the `emacs`_ documentation, let us say that hooks are an
 important mechanism for customizing an application. A hook is
 basically a list of functions to be called on some well-defined
-occasion (This is called `running the hook`).
+occasion (this is called `running the hook`).
 
 .. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
 
-In CubicWeb, hooks are classes subclassing the Hook class in
-`server/hook.py`, implementing their own `call` method, and defined
-over pre-defined `events`.
+In CubicWeb, hooks are subclasses of the Hook class in
+`server/hook.py`, implementing their own `call` method, and selected
+over a set of pre-defined `events` (and possibly more conditions,
+hooks being selectable AppObjects like views and components).
 
 There are two families of events: data events and server events. In a
 typical application, most of the Hooks are defined over data
-events. There can be a lot of them.
+events.
 
 The purpose of data hooks is to complement the data model as defined
 in the schema.py, which is static by nature, with dynamic or value
 driven behaviours. It is functionally equivalent to a `database
-trigger`_, except that database triggers definitions languages are not
+trigger`_, except that database triggers definition languages are not
 standardized, hence not portable (for instance, PL/SQL works with
 Oracle and PostgreSQL but not SqlServer nor Sqlite).
 
@@ -35,7 +36,8 @@
 Data hooks can serve the following purposes:
 
 * enforcing constraints that the static schema cannot express
-  (spanning several entities/relations, exotic cardinalities, etc.)
+  (spanning several entities/relations, specific value ranges, exotic
+  cardinalities, etc.)
 
 * implement computed attributes (an example could be the maintenance
   of a relation representing the transitive closure of another relation)
@@ -48,12 +50,17 @@
 the order of execution of Hooks is data dependant (and thus hard to
 predict), it is possible to force an order on Operations.
 
+Operations are subclasses of the Operation class in `server/hook.py`,
+implementing `precommit_event` and other standard methods (wholly
+described later in this chapter).
+
 Events
 ------
 
 Hooks are mostly defined and used to handle `dataflow`_ operations. It
-means as data gets in (mostly), specific events are issued and the
-Hooks matching these events are called.
+means as data gets in (entities added, updated, relations set or
+unset), specific events are issued and the Hooks matching these events
+are called.
 
 .. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
 
@@ -102,3 +109,104 @@
 * session_close
 
 
+Using Hooks
+-----------
+
+We will use a very simple example to show hooks usage. Let us start
+with the following schema.
+
+.. sourcecode:: python
+
+   class Person(EntityType):
+       age = Int(required=True)
+
+An entity hook
+~~~~~~~~~~~~~~
+
+We would like to add a range constraint over a person's age. Let's
+write an hook. It shall be placed into mycube/hooks.py. If this file
+were to grow too much, we can easily have a mycube/hooks/... package
+containing hooks in various modules.
+
+.. sourcecode:: python
+
+   from cubicweb import ValidationError
+   from cubicweb.selectors import implements
+   from cubicweb.server.hook import Hook
+
+   class PersonAgeRange(Hook):
+        __regid__ = 'person_age_range'
+        events = ('before_add_entity', 'before_update_entity')
+        __select__ = Hook.__select__ & implements('Person')
+
+        def __call__(self):
+            if 0 >= self.entity.age <= 120:
+               return
+            msg = self._cw._('age must be between 0 and 120')
+            raise ValidationError(self.entity.eid, {'age': msg})
+
+Hooks being AppObjects like views, they have a __regid__ and a
+__select__ class attribute. The base __select__ is augmented with an
+`implements` selector matching the desired entity type. The `events`
+tuple is used by the Hook.__select__ base selector to dispatch the
+hook on the right events. In an entity hook, it is possible to
+dispatch on any entity event at once if needed.
+
+Like all appobjects, hooks have the self._cw attribute which
+represents the current session. In entity hooks, a self.entity
+attribute is also present.
+
+When a condition is not met in a Hook, it must raise a
+ValidationError. Raising anything but a (subclass of) ValidationError
+is a programming error.
+
+The ValidationError exception is used to convey enough information up
+to the user interface. Hence its constructor is different from the
+default Exception constructor.It accepts, positionally:
+
+* an entity eid,
+
+* a dict whose keys represent attributes and values a message relating
+  the problem; such a message will be presented to the end-users;
+  hence it must be properly translated.
+
+A relation hook
+~~~~~~~~~~~~~~~
+
+Let us add another entity type with a relation to person (in
+mycube/schema.py).
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+
+We would like to constrain the company's bosses to have a minimum
+(legal) age. Let's write an hook for this, which will be fired when
+the `boss` relation is established.
+
+.. sourcecode:: python
+
+   class CompanyBossLegalAge(Hook):
+        __regid__ = 'company_boss_legal_age'
+        events = ('before_add_relation',)
+        __select__ = Hook.__select__ & match_rtype('boss')
+
+        def __call__(self):
+            boss = self._cw.entity_from_eid(self.eidto)
+            if boss.age < 18:
+                msg = self._cw._('the minimum age for a boss is 18')
+                raise ValidationError(self.eidfrom, {'boss': msg})
+
+We use the `match_rtype` selector to select the proper relation type.
+
+The essential difference with respect to an entity hook is that there
+is no self.entity, but `self.eidfrom` and `self.eidto` hook attributes
+which represent the subject and object eid of the relation.
+
+
+# XXX talk about
+
+dict access to entities in before_[add|update]
+set_operation
--- a/entity.py	Fri Apr 09 15:01:25 2010 +0000
+++ b/entity.py	Sun Apr 11 11:59:45 2010 +0000
@@ -202,6 +202,59 @@
             needcheck = False
         return mainattr, needcheck
 
+    @classmethod
+    def cw_instantiate(cls, execute, **kwargs):
+        """add a new entity of this given type
+
+        Example (in a shell session):
+
+        >>> companycls = vreg['etypes'].etype_class(('Company')
+        >>> personcls = vreg['etypes'].etype_class(('Person')
+        >>> c = companycls.cw_instantiate(req.execute, name=u'Logilab')
+        >>> personcls.cw_instantiate(req.execute, firstname=u'John', lastname=u'Doe',
+        ...                          works_for=c)
+
+        """
+        rql = 'INSERT %s X' % cls.__regid__
+        relations = []
+        restrictions = set()
+        pending_relations = []
+        for attr, value in kwargs.items():
+            if isinstance(value, (tuple, list, set, frozenset)):
+                if len(value) == 1:
+                    value = iter(value).next()
+                else:
+                    del kwargs[attr]
+                    pending_relations.append( (attr, value) )
+                    continue
+            if hasattr(value, 'eid'): # non final relation
+                rvar = attr.upper()
+                # XXX safer detection of object relation
+                if attr.startswith('reverse_'):
+                    relations.append('%s %s X' % (rvar, attr[len('reverse_'):]))
+                else:
+                    relations.append('X %s %s' % (attr, rvar))
+                restriction = '%s eid %%(%s)s' % (rvar, attr)
+                if not restriction in restrictions:
+                    restrictions.add(restriction)
+                kwargs[attr] = value.eid
+            else: # attribute
+                relations.append('X %s %%(%s)s' % (attr, attr))
+        if relations:
+            rql = '%s: %s' % (rql, ', '.join(relations))
+        if restrictions:
+            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
+        created = execute(rql, kwargs).get_entity(0, 0)
+        for attr, values in pending_relations:
+            if attr.startswith('reverse_'):
+                restr = 'Y %s X' % attr[len('reverse_'):]
+            else:
+                restr = 'X %s Y' % attr
+            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
+                restr, ','.join(str(r.eid) for r in values)),
+                    {'x': created.eid}, build_descr=False)
+        return created
+
     def __init__(self, req, rset=None, row=None, col=0):
         AppObject.__init__(self, req, rset=rset, row=row, col=col)
         dict.__init__(self)
--- a/req.py	Fri Apr 09 15:01:25 2010 +0000
+++ b/req.py	Sun Apr 11 11:59:45 2010 +0000
@@ -119,9 +119,6 @@
     def set_entity_cache(self, entity):
         pass
 
-    # XXX move to CWEntityManager or even better as factory method (unclear
-    # where yet...)
-
     def create_entity(self, etype, **kwargs):
         """add a new entity of the given type
 
@@ -133,48 +130,8 @@
 
         """
         _check_cw_unsafe(kwargs)
-        execute = self.execute
-        rql = 'INSERT %s X' % etype
-        relations = []
-        restrictions = set()
-        cachekey = []
-        pending_relations = []
-        for attr, value in kwargs.items():
-            if isinstance(value, (tuple, list, set, frozenset)):
-                if len(value) == 1:
-                    value = iter(value).next()
-                else:
-                    del kwargs[attr]
-                    pending_relations.append( (attr, value) )
-                    continue
-            if hasattr(value, 'eid'): # non final relation
-                rvar = attr.upper()
-                # XXX safer detection of object relation
-                if attr.startswith('reverse_'):
-                    relations.append('%s %s X' % (rvar, attr[len('reverse_'):]))
-                else:
-                    relations.append('X %s %s' % (attr, rvar))
-                restriction = '%s eid %%(%s)s' % (rvar, attr)
-                if not restriction in restrictions:
-                    restrictions.add(restriction)
-                cachekey.append(attr)
-                kwargs[attr] = value.eid
-            else: # attribute
-                relations.append('X %s %%(%s)s' % (attr, attr))
-        if relations:
-            rql = '%s: %s' % (rql, ', '.join(relations))
-        if restrictions:
-            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
-        created = execute(rql, kwargs, cachekey).get_entity(0, 0)
-        for attr, values in pending_relations:
-            if attr.startswith('reverse_'):
-                restr = 'Y %s X' % attr[len('reverse_'):]
-            else:
-                restr = 'X %s Y' % attr
-            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
-                restr, ','.join(str(r.eid) for r in values)),
-                    {'x': created.eid}, 'x', build_descr=False)
-        return created
+        cls = self.vreg['etypes'].etype_class(etype)
+        return cls.cw_instantiate(self.execute, **kwargs)
 
     def ensure_ro_rql(self, rql):
         """raise an exception if the given rql is not a select query"""
--- a/server/test/unittest_session.py	Fri Apr 09 15:01:25 2010 +0000
+++ b/server/test/unittest_session.py	Sun Apr 11 11:59:45 2010 +0000
@@ -37,6 +37,7 @@
     def test_dbapi_query(self):
         session = self.repo.internal_session()
         self.assertFalse(session.running_dbapi_query)
+        session.close()
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/views/basecontrollers.py	Fri Apr 09 15:01:25 2010 +0000
+++ b/web/views/basecontrollers.py	Sun Apr 11 11:59:45 2010 +0000
@@ -91,11 +91,11 @@
         #   anonymous connection is allowed and the page will be displayed or
         #   we'll be redirected to the login form
         msg = self._cw._('you have been logged out')
-        if self._cw.https:
-            # XXX hack to generate an url on the http version of the site
-            self._cw._base_url =  self._cw.vreg.config['base-url']
-            self._cw.https = False
-        return self._cw.build_url('view', vid='index', __message=msg)
+        # force base_url so on dual http/https configuration, we generate an url
+        # on the http version of the site
+        return self._cw.build_url('view', vid='index', __message=msg,
+                                  base_url=self._cw.vreg.config['base-url'])
+
 
 class ViewController(Controller):
     """standard entry point :
@@ -595,25 +595,14 @@
         for entity in rset.entities():
             yield entity
 
-    @property
-    @cached
-    def smtp(self):
-        mailhost, port = self._cw.config['smtp-host'], self._cw.config['smtp-port']
-        try:
-            return SMTP(mailhost, port)
-        except Exception, ex:
-            self.exception("can't connect to smtp server %s:%s (%s)",
-                             mailhost, port, ex)
-            url = self._cw.build_url(__message=self._cw._('could not connect to the SMTP server'))
-            raise Redirect(url)
-
     def sendmail(self, recipient, subject, body):
-        helo_addr = '%s <%s>' % (self._cw.config['sender-name'],
-                                 self._cw.config['sender-addr'])
         msg = format_mail({'email' : self._cw.user.get_email(),
                            'name' : self._cw.user.dc_title(),},
                           [recipient], body, subject)
-        self.smtp.sendmail(helo_addr, [recipient], msg.as_string())
+        if not self._cw.vreg.config.sendmails([(msg, [recipient])]):
+            msg = self._cw._('could not connect to the SMTP server')
+            url = self._cw.build_url(__message=msg)
+            raise Redirect(url)
 
     def publish(self, rset=None):
         # XXX this allows users with access to an cubicweb instance to use it as