--- 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