# HG changeset patch # User Sylvain Thénault # Date 1270818626 -7200 # Node ID 2b4267157f852644178aed9641f9c02834d38b89 # Parent d14bfd477c4465123738356a2f9107886a3a2adc# Parent 2b454c6ab7ef1857c5b2564da8a998a92150a85e backport stable diff -r d14bfd477c44 -r 2b4267157f85 cwconfig.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')) diff -r d14bfd477c44 -r 2b4267157f85 doc/book/en/.static/sphinx-default.css --- 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; diff -r d14bfd477c44 -r 2b4267157f85 doc/book/en/annexes/rql/intro.rst --- 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. diff -r d14bfd477c44 -r 2b4267157f85 doc/book/en/development/devcore/dbapi.rst --- 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. diff -r d14bfd477c44 -r 2b4267157f85 doc/book/en/development/devrepo/hooks.rst --- 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 + + diff -r d14bfd477c44 -r 2b4267157f85 doc/book/en/intro/concepts.rst --- 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* diff -r d14bfd477c44 -r 2b4267157f85 entity.py --- 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) diff -r d14bfd477c44 -r 2b4267157f85 req.py --- 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""" diff -r d14bfd477c44 -r 2b4267157f85 server/hook.py --- 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 diff -r d14bfd477c44 -r 2b4267157f85 server/repository.py --- 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: diff -r d14bfd477c44 -r 2b4267157f85 server/session.py --- 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, diff -r d14bfd477c44 -r 2b4267157f85 server/test/unittest_session.py --- 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() diff -r d14bfd477c44 -r 2b4267157f85 web/views/basecontrollers.py --- 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