backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 09 Apr 2010 15:10:26 +0200
changeset 5201 2b4267157f85
parent 5196 d14bfd477c44 (current diff)
parent 5200 2b454c6ab7ef (diff)
child 5216 4f4369e63f5e
backport stable
cwconfig.py
entity.py
req.py
server/hook.py
server/repository.py
server/session.py
web/views/basecontrollers.py
--- a/cwconfig.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/cwconfig.py	Fri Apr 09 15:10:26 2010 +0200
@@ -1058,7 +1058,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:
@@ -1067,7 +1069,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:
@@ -1078,6 +1080,7 @@
             smtp.close()
         finally:
             SMTP_LOCK.release()
+        return True
 
 set_log_methods(CubicWebConfiguration, logging.getLogger('cubicweb.configuration'))
 
--- a/doc/book/en/.static/sphinx-default.css	Thu Apr 08 14:11:49 2010 +0200
+++ b/doc/book/en/.static/sphinx-default.css	Fri Apr 09 15:10:26 2010 +0200
@@ -3,7 +3,7 @@
  */
 
 html, body {
-    background: white;	
+    background: white;
 }
 
 body {
@@ -115,7 +115,7 @@
 }
 
 div.sphinxsidebar h3 {
-    font-family: 'Verdanda', sans-serif;
+    font-family: Verdana, sans-serif;
     color: black;
     font-size: 1.2em;
     font-weight: normal;
@@ -126,7 +126,7 @@
 }
 
 div.sphinxsidebar h4 {
-    font-family: 'Verdana', sans-serif;
+    font-family: Verdana, sans-serif;
     color: black;
     font-size: 1.1em;
     font-weight: normal;
--- a/doc/book/en/annexes/rql/intro.rst	Thu Apr 08 14:11:49 2010 +0200
+++ b/doc/book/en/annexes/rql/intro.rst	Fri Apr 09 15:10:26 2010 +0200
@@ -5,10 +5,10 @@
 Goals of RQL
 ~~~~~~~~~~~~
 
-The goal is to have a language emphasizing the way of browsing relations. As
-such, attributes will be regarded as cases of special relations (in terms of
-implementation, the user should see no difference between an attribute and a
-relation).
+The goal is to have a language making relations browsing easy. As
+such, attributes will be regarded as cases of special relations (in
+terms of usage, the user should see no syntactic difference between an
+attribute and a relation).
 
 Comparison with existing languages
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -32,10 +32,10 @@
 
 We should look in more detail, but here are already some ideas for the moment
 ... Versa_ is the language most similar to what we wanted to do, but the model
-underlying data being RDF, there is some number of things such as namespaces or
+underlying data being RDF, there are some things such as namespaces or
 handling of the RDF types which does not interest us. On the functionality
 level, Versa_ is very comprehensive including through many functions of
-conversion and basic types manipulation, which may need to be guided at one time
+conversion and basic types manipulation, which we may want to look at one time
 or another.  Finally, the syntax is a little esoteric.
 
 
--- a/doc/book/en/development/devcore/dbapi.rst	Thu Apr 08 14:11:49 2010 +0200
+++ b/doc/book/en/development/devcore/dbapi.rst	Fri Apr 09 15:10:26 2010 +0200
@@ -5,9 +5,10 @@
 
 The Python API developped to interface with RQL is inspired from the standard db-api,
 with a Connection object having the methods cursor, rollback and commit essentially.
-The most important method is the `execute` method of a cursor :
+The most important method is the `execute` method of a cursor.
 
-`execute(rqlstring, args=None, cachekey=None, build_descr=True)`
+.. sourcecode:: python
+  execute(rqlstring, args=None, cachekey=None, build_descr=True)
 
 :rqlstring: the RQL query to execute (unicode)
 :args: if the query contains substitutions, a dictionary containing the values to use
@@ -18,10 +19,11 @@
    through this argument
 
 
-The `Connection` object owns the methods `commit` and `rollback`. You *should
-never need to use them* during the development of the web interface based on
-the *CubicWeb* framework as it determines the end of the transaction depending
-on the query execution success.
+The `Connection` object owns the methods `commit` and `rollback`. You
+*should never need to use them* during the development of the web
+interface based on the *CubicWeb* framework as it determines the end
+of the transaction depending on the query execution success. They are
+however useful in other contexts such as tests.
 
 .. note::
   While executing update queries (SET, INSERT, DELETE), if a query generates
@@ -30,6 +32,7 @@
 
 Executing RQL queries from a view or a hook
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
 When you're within code of the web interface, the db-api like connexion is
 handled by the request object. You should not have to access it directly, but
 use the `execute` method directly available on the request, eg:
@@ -50,33 +53,30 @@
 
    self._cw.execute('Any T WHERE T in_conf C, C eid %s' % entity.eid)
 
-But it can also be written in a syntax that will benefit from the use
+But it must be written in a syntax that will benefit from the use
 of a cache on the RQL server side:
 
 .. sourcecode:: python
 
-   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid}, 'x')
+   self._cw.execute('Any T WHERE T in_conf C, C eid %(x)s', {'x': entity.eid})
 
-Beside proper usage of the `args` argument, notice the latest argument: this is what's called
-the cache key. The cache key should be either a string or a tuple containing the names of keys
-in args which are referencing eids. *YOU MUST SET THIS PROPERLY* if you don't want weird result
-on queries which have ambigous solutions deambiguified by specifing an eid. So the good habit is:
-*always put in the cache key all eid keys*.
+The syntax tree is built once for the "generic" RQL and can be re-used
+with a number of different eids. There rql IN operator is an exception
+to this rule.
 
-The syntax tree is build once for the "generic" RQL and can be re-used
-with a number of different eid.
+.. sourcecode:: python
+
+   self._cw.execute('Any T WHERE T in_conf C, C name IN (%s)' % ','.join(['foo', 'bar']))
 
-Alternativelly, some of the common data related to an entity can be obtained from
-the top-level `entity.related()` method (which is used under the hood by the orm
-when you use attribute access notation on an entity to get a relation. The above
-would then be translated to:
+Alternativelly, some of the common data related to an entity can be
+obtained from the `entity.related()` method (which is used under the
+hood by the orm when you use attribute access notation on an entity to
+get a relation. The initial request would then be translated to:
 
 .. sourcecode:: python
 
    entity.related('in_conf', 'object')
 
-The `related()` method, as more generally others orm methods, makes extensive use
-of the cache mechanisms so you don't have to worry about them. Additionnaly this
-use will get you commonly used attributes that you will be able to use in your
-view generation without having to ask the data backend.
-
+Additionnaly this benefits from the fetch_attrs policy eventually
+defined on the class element, which says which attributes must be also
+loaded when the entity is loaded through the orm.
--- a/doc/book/en/development/devrepo/hooks.rst	Thu Apr 08 14:11:49 2010 +0200
+++ b/doc/book/en/development/devrepo/hooks.rst	Fri Apr 09 15:10:26 2010 +0200
@@ -2,31 +2,103 @@
 
 .. _hooks:
 
-Hooks
-=====
+Hooks and Operations
+====================
+
+Principles
+----------
 
-XXX FILLME
+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`).
+
+.. _`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`.
+
+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.
 
-*Hooks* are executed before or after updating an entity or a relation in the
-repository.
+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
+standardized, hence not portable (for instance, PL/SQL works with
+Oracle and PostgreSQL but not SqlServer nor Sqlite).
+
+.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger
+
+Data hooks can serve the following purposes:
+
+* enforcing constraints that the static schema cannot express
+  (spanning several entities/relations, exotic cardinalities, etc.)
 
-Their prototypes are as follows:
+* implement computed attributes (an example could be the maintenance
+  of a relation representing the transitive closure of another relation)
+
+Operations are Hook-like objects that are created by Hooks and
+scheduled to happen just before (or after) the `commit` event. Hooks
+being fired immediately on data operations, it is sometime necessary
+to delay the actual work down to a time where all other Hooks have run
+and the application state converges towards consistency. Also while
+the order of execution of Hooks is data dependant (and thus hard to
+predict), it is possible to force an order on Operations.
+
+Events
+------
 
-    * after_add_entity     (session, entity)
-    * after_update_entity  (session, entity)
-    * after_delete_entity  (session, eid)
-    * before_add_entity    (session, entity)
-    * before_update_entity (session, entity)
-    * before_delete_entity (session, eid)
+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.
+
+.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow
+
+Below comes a list of the dataflow events related to entities operations:
+
+* before_add_entity
+
+* before_update_entity
+
+* before_delete_entity
+
+* after_add_entity
+
+* after_update_entity
+
+* after_delete_entity
+
+These define ENTTIES HOOKS. RELATIONS HOOKS are defined
+over the following events:
+
+* after_add_relation
 
-    * after_add_relation     (session, fromeid, rtype, toeid)
-    * after_delete_relation  (session, fromeid, rtype, toeid)
-    * before_add_relation    (session, fromeid, rtype, toeid)
-    * before_delete_relation (session, fromeid, rtype, toeid)
+* after_delete_relation
+
+* before_add_relation
+
+* before_delete_relation
+
+This is an occasion to remind us that relations support the add/delete
+operation, but no delete.
+
+Non data events also exist. These are called SYSTEM HOOKS.
+
+* server_startup
 
-    * server_startup
-    * server_shutdown
+* server_shutdown
+
+* server_maintenance
+
+* server_backup
 
-    * session_open
-    * session_close
+* server_restore
+
+* session_open
 
+* session_close
+
+
--- a/doc/book/en/intro/concepts.rst	Thu Apr 08 14:11:49 2010 +0200
+++ b/doc/book/en/intro/concepts.rst	Fri Apr 09 15:10:26 2010 +0200
@@ -339,5 +339,5 @@
 .. Note:
    RQL queries executed in hooks and operations are *unsafe* by default, e.g. the
    read and write security is deactivated unless explicitly asked.
-  
+
 .. |cubicweb| replace:: *CubicWeb*
--- a/entity.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/entity.py	Fri Apr 09 15:10:26 2010 +0200
@@ -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	Thu Apr 08 14:11:49 2010 +0200
+++ b/req.py	Fri Apr 09 15:10:26 2010 +0200
@@ -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,46 +130,8 @@
 
         """
         _check_cw_unsafe(kwargs)
-        execute = self.execute
-        rql = 'INSERT %s X' % etype
-        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
+        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/hook.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/server/hook.py	Fri Apr 09 15:10:26 2010 +0200
@@ -30,7 +30,6 @@
 
 Session hooks (eg session_open, session_close) have no special attribute.
 
-
 :organization: Logilab
 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
--- a/server/repository.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/server/repository.py	Fri Apr 09 15:10:26 2010 +0200
@@ -1119,7 +1119,7 @@
                                       eidfrom=entity.eid, rtype=attr, eidto=value)
                     if not only_inline_rels:
                         hm.call_hooks('before_update_entity', session, entity=entity)
-                source.update_entity(session, entity)
+            source.update_entity(session, entity)
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
--- a/server/session.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/server/session.py	Fri Apr 09 15:10:26 2010 +0200
@@ -953,6 +953,7 @@
 class InternalSession(Session):
     """special session created internaly by the repository"""
     is_internal_session = True
+    running_dbapi_query = False
 
     def __init__(self, repo, cnxprops=None):
         super(InternalSession, self).__init__(InternalManager(), repo, cnxprops,
--- a/server/test/unittest_session.py	Thu Apr 08 14:11:49 2010 +0200
+++ b/server/test/unittest_session.py	Fri Apr 09 15:10:26 2010 +0200
@@ -7,6 +7,7 @@
 """
 from logilab.common.testlib import TestCase, unittest_main, mock_object
 
+from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.session import _make_description
 
 class Variable:
@@ -32,5 +33,11 @@
         self.assertEquals(_make_description((Function('max', 'A'), Variable('B')), {}, solution),
                           ['Int','CWUser'])
 
+class InternalSessionTC(CubicWebTC):
+    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	Thu Apr 08 14:11:49 2010 +0200
+++ b/web/views/basecontrollers.py	Fri Apr 09 15:10:26 2010 +0200
@@ -92,11 +92,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 :
@@ -596,25 +596,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