# HG changeset patch # User Sylvain Thénault # Date 1284389223 -7200 # Node ID 82d4011f54c1df529eed5f2f9e8519ab390ee66d # Parent a176e68b7d0de2ef70b16466e56ab729f4eca38d# Parent dbb7ad04b963675852dc8ad69926c51965dcd99e backport stable diff -r dbb7ad04b963 -r 82d4011f54c1 cwvreg.py --- a/cwvreg.py Mon Sep 13 16:46:52 2010 +0200 +++ b/cwvreg.py Mon Sep 13 16:47:03 2010 +0200 @@ -421,6 +421,44 @@ VRegistry.REGISTRY_FACTORY['actions'] = ActionsRegistry +class CtxComponentsRegistry(CWRegistry): + def poss_visible_objects(self, *args, **kwargs): + """return an ordered list of possible components""" + context = kwargs.pop('context') + if kwargs.get('rset') is None: + cache = args[0] + else: + cache = kwargs['rset'] + try: + cached = cache.__components_cache + except AttributeError: + ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects( + *args, **kwargs) + cached = cache.__components_cache = {} + for component in ctxcomps: + cached.setdefault(component.cw_propval('context'), []).append(component) + thisctxcomps = cached.get(context, ()) + # XXX set context for bw compat (should now be taken by comp.render()) + for component in thisctxcomps: + component.cw_extra_kwargs['context'] = context + return thisctxcomps + +VRegistry.REGISTRY_FACTORY['ctxcomponents'] = CtxComponentsRegistry + + +class BwCompatCWRegistry(object): + def __init__(self, vreg, oldreg, redirecttoreg): + self.vreg = vreg + self.oldreg = oldreg + self.redirecto = redirecttoreg + + def __getattr__(self, attr): + warn('[3.10] you should now use the %s registry instead of the %s registry' + % (self.redirecto, self.oldreg), DeprecationWarning, stacklevel=2) + return getattr(self.vreg[self.redirecto], attr) + + def clear(self): pass + def initialization_completed(self): pass class CubicWebVRegistry(VRegistry): """Central registry for the cubicweb instance, extending the generic @@ -433,15 +471,23 @@ stored objects. Currently we have the following registries of objects known by the web instance (library may use some others additional registries): - * etypes - * views - * components - * actions - * forms - * formrenderers - * controllers, which are directly plugged into the application - object to handle request publishing XXX to merge with views - * contentnavigation XXX to merge with components? to kill? + * 'etypes', entity type classes + + * 'views', views and templates (e.g. layout views) + + * 'components', non contextual components, like magic search, url evaluators + + * 'ctxcomponents', contextual components like boxes and dynamic section + + * 'actions', contextual actions, eg links to display in predefined places in + the ui + + * 'forms', describing logic of HTML form + + * 'formrenderers', rendering forms to html + + * 'controllers', primary objects to handle request publishing, directly + plugged into the application """ def __init__(self, config, initlog=True): @@ -456,6 +502,8 @@ # don't clear rtags during test, this may cause breakage with # manually imported appobject modules CW_EVENT_MANAGER.bind('before-registry-reload', clear_rtag_objects) + self['boxes'] = BwCompatCWRegistry(self, 'boxes', 'ctxcomponents') + self['contentnavigation'] = BwCompatCWRegistry(self, 'contentnavigation', 'ctxcomponents') def setdefault(self, regid): try: @@ -713,7 +761,7 @@ vocab = pdef['vocabulary'] if vocab is not None: if callable(vocab): - vocab = vocab(key, None) # XXX need a req object + vocab = vocab(None) # XXX need a req object if not value in vocab: raise ValueError(_('unauthorized value')) return value @@ -751,7 +799,7 @@ def possible_actions(self, req, rset=None, **kwargs): return self["actions"].possible_actions(req, rest=rset, **kwargs) - @deprecated('[3.4] use vreg["boxes"].select_object(...)') + @deprecated('[3.4] use vreg["ctxcomponents"].select_object(...)') def select_box(self, oid, *args, **kwargs): return self['boxes'].select_object(oid, *args, **kwargs) diff -r dbb7ad04b963 -r 82d4011f54c1 dataimport.py --- a/dataimport.py Mon Sep 13 16:46:52 2010 +0200 +++ b/dataimport.py Mon Sep 13 16:47:03 2010 +0200 @@ -81,6 +81,7 @@ from logilab.common.deprecation import deprecated from cubicweb.server.utils import eschema_eid +from cubicweb.server.ssplanner import EditedEntity def count_lines(stream_or_filename): if isinstance(stream_or_filename, basestring): @@ -612,8 +613,7 @@ entity = copy(entity) entity.cw_clear_relation_cache() self.metagen.init_entity(entity) - entity.update(kwargs) - entity.edited_attributes = set(entity) + entity.cw_edited.update(kwargs, skipsec=False) session = self.session self.source.add_entity(session, entity) self.source.add_info(session, entity, self.source, None, complete=False) @@ -686,8 +686,9 @@ entity = self.session.vreg['etypes'].etype_class(etype)(self.session) # entity are "surface" copied, avoid shared dict between copies del entity.cw_extra_kwargs + entity.cw_edited = EditedEntity(entity) for attr in self.etype_attrs: - entity[attr] = self.generate(entity, attr) + entity.cw_edited.attribute_edited(attr, self.generate(entity, attr)) rels = {} for rel in self.etype_rels: rels[rel] = self.generate(entity, rel) @@ -696,7 +697,7 @@ def init_entity(self, entity): entity.eid = self.source.create_eid(self.session) for attr in self.entity_attrs: - entity[attr] = self.generate(entity, attr) + entity.cw_edited.attribute_edited(attr, self.generate(entity, attr)) def generate(self, entity, rtype): return getattr(self, 'gen_%s' % rtype)(entity) diff -r dbb7ad04b963 -r 82d4011f54c1 dbapi.py --- a/dbapi.py Mon Sep 13 16:46:52 2010 +0200 +++ b/dbapi.py Mon Sep 13 16:47:03 2010 +0200 @@ -313,19 +313,17 @@ # low level session data management ####################################### - def get_shared_data(self, key, default=None, pop=False): - """return value associated to `key` in shared data""" - return self.cnx.get_shared_data(key, default, pop) - - def set_shared_data(self, key, value, querydata=False): - """set value associated to `key` in shared data + def get_shared_data(self, key, default=None, pop=False, txdata=False): + """see :meth:`Connection.get_shared_data`""" + return self.cnx.get_shared_data(key, default, pop, txdata) - if `querydata` is true, the value will be added to the repository - session's query data which are cleared on commit/rollback of the current - transaction, and won't be available through the connexion, only on the - repository side. - """ - return self.cnx.set_shared_data(key, value, querydata) + def set_shared_data(self, key, value, txdata=False, querydata=None): + """see :meth:`Connection.set_shared_data`""" + if querydata is not None: + txdata = querydata + warn('[3.10] querydata argument has been renamed to txdata', + DeprecationWarning, stacklevel=2) + return self.cnx.set_shared_data(key, value, txdata) # server session compat layer ############################################# @@ -507,10 +505,12 @@ return DBAPIRequest(self.vreg, DBAPISession(self)) def check(self): - """raise `BadConnectionId` if the connection is no more valid""" + """raise `BadConnectionId` if the connection is no more valid, else + return its latest activity timestamp. + """ if self._closed is not None: raise ProgrammingError('Closed connection') - self._repo.check_session(self.sessionid) + return self._repo.check_session(self.sessionid) def set_session_props(self, **props): """raise `BadConnectionId` if the connection is no more valid""" @@ -518,23 +518,29 @@ raise ProgrammingError('Closed connection') self._repo.set_session_props(self.sessionid, props) - def get_shared_data(self, key, default=None, pop=False): - """return value associated to `key` in shared data""" - if self._closed is not None: - raise ProgrammingError('Closed connection') - return self._repo.get_shared_data(self.sessionid, key, default, pop) + def get_shared_data(self, key, default=None, pop=False, txdata=False): + """return value associated to key in the session's data dictionary or + session's transaction's data if `txdata` is true. - def set_shared_data(self, key, value, querydata=False): - """set value associated to `key` in shared data + If pop is True, value will be removed from the dictionnary. - if `querydata` is true, the value will be added to the repository - session's query data which are cleared on commit/rollback of the current - transaction, and won't be available through the connexion, only on the - repository side. + If key isn't defined in the dictionnary, value specified by the + `default` argument will be returned. """ if self._closed is not None: raise ProgrammingError('Closed connection') - return self._repo.set_shared_data(self.sessionid, key, value, querydata) + return self._repo.get_shared_data(self.sessionid, key, default, pop, txdata) + + def set_shared_data(self, key, value, txdata=False): + """set value associated to `key` in shared data + + if `txdata` is true, the value will be added to the repository session's + transaction's data which are cleared on commit/rollback of the current + transaction. + """ + if self._closed is not None: + raise ProgrammingError('Closed connection') + return self._repo.set_shared_data(self.sessionid, key, value, txdata) def get_schema(self): """Return the schema currently used by the repository. @@ -625,7 +631,7 @@ else: from cubicweb.entity import Entity user = Entity(req, rset, row=0) - user['login'] = login # cache login + user.cw_attr_cache['login'] = login # cache login return user def __del__(self): diff -r dbb7ad04b963 -r 82d4011f54c1 devtools/testlib.py --- a/devtools/testlib.py Mon Sep 13 16:46:52 2010 +0200 +++ b/devtools/testlib.py Mon Sep 13 16:47:03 2010 +0200 @@ -499,7 +499,7 @@ def list_boxes_for(self, rset): """returns the list of boxes that can be applied on `rset`""" req = rset.req - for box in self.vreg['boxes'].possible_objects(req, rset=rset): + for box in self.vreg['ctxcomponents'].possible_objects(req, rset=rset): yield box def list_startup_views(self): @@ -951,7 +951,8 @@ for action in self.list_actions_for(rset): yield InnerTest(self._testname(rset, action.__regid__, 'action'), self._test_action, action) for box in self.list_boxes_for(rset): - yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render) + w = [].append + yield InnerTest(self._testname(rset, box.__regid__, 'box'), box.render, w) @staticmethod def _testname(rset, objid, objtype): diff -r dbb7ad04b963 -r 82d4011f54c1 doc/book/en/devrepo/devcore/cwconfig.rst --- a/doc/book/en/devrepo/devcore/cwconfig.rst Mon Sep 13 16:46:52 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ -Configuration -------------- - -.. automodule:: cubicweb.cwconfig - :members: diff -r dbb7ad04b963 -r 82d4011f54c1 doc/book/en/devrepo/devcore/index.rst --- a/doc/book/en/devrepo/devcore/index.rst Mon Sep 13 16:46:52 2010 +0200 +++ b/doc/book/en/devrepo/devcore/index.rst Mon Sep 13 16:47:03 2010 +0200 @@ -6,5 +6,4 @@ dbapi.rst reqbase.rst - cwconfig.rst diff -r dbb7ad04b963 -r 82d4011f54c1 doc/book/en/devrepo/repo/hooks.rst --- a/doc/book/en/devrepo/repo/hooks.rst Mon Sep 13 16:46:52 2010 +0200 +++ b/doc/book/en/devrepo/repo/hooks.rst Mon Sep 13 16:47:03 2010 +0200 @@ -1,158 +1,26 @@ .. -*- coding: utf-8 -*- - .. _hooks: Hooks and Operations ==================== -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`). - -.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html - -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. - -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 definition 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 value ranges and - cardinalities, etc.) - -* implement computed attributes - -Operations are Hook-like objects that may be 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, for instance a validation check which needs that all relations be -already set on an entity. 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. - -Operations also may be used to process various side effects associated -with a transaction such as filesystem udpates, mail notifications, -etc. - -Operations are subclasses of the Operation class in `server/hook.py`, -implementing `precommit_event` and other standard methods (wholly -described in :ref:`operations_api`). - -Events ------- - -Hooks are mostly defined and used to handle `dataflow`_ operations. It -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 - -Below comes a list of the dataflow events related to entities operations: - -* before_add_entity - -* before_update_entity +.. autodocstring:: cubicweb.server.hook -* 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_delete_relation - -* before_add_relation - -* before_delete_relation - -This is an occasion to remind us that relations support the add/delete -operation, but no update. - -Non data events also exist. These are called SYSTEM HOOKS. - -* server_startup - -* server_shutdown - -* server_maintenance - -* server_backup - -* server_restore +Example using dataflow hooks +---------------------------- -* session_open - -* session_close - - -Using dataflow Hooks --------------------- - -Dataflow hooks either automate data operations or maintain the -consistency of the data model. In the later case, we must use a -specific exception named ValidationError - -Validation Errors -~~~~~~~~~~~~~~~~~ - -When a condition is not met in a Hook/Operation, it must raise a -`ValidationError`. Raising anything but a (subclass of) -ValidationError is a programming error. Raising a ValidationError -entails aborting the current transaction. - -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 attribute (or relation) names and values - an end-user facing message (hence properly translated) relating the - problem. - -An entity hook -~~~~~~~~~~~~~~ - -We will use a very simple example to show hooks usage. Let us start -with the following schema. +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) -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. +We would like to add a range constraint over a person's age. Let's write an hook +(supposing yams can not handle this nativly, which is wrong). 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 @@ -166,64 +34,26 @@ __select__ = Hook.__select__ & is_instance('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 -`is_instance` 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 (e.g. 'before_add_entity', -'before_update_entity') at once if needed. + if 'age' in self.entity.cw_edited: + if 0 <= self.entity.age <= 120: + return + msg = self._cw._('age must be between 0 and 120') + raise ValidationError(self.entity.eid, {'age': msg}) -Like all appobjects, hooks have the `self._cw` attribute which -represents the current session. In entity hooks, a `self.entity` -attribute is also present. - - -A relation hook -~~~~~~~~~~~~~~~ - -Let us add another entity type with a relation to person (in -mycube/schema.py). - -.. sourcecode:: python +In our example the base `__select__` is augmented with an `is_instance` selector +matching the desired entity type. - class Company(EntityType): - name = String(required=True) - boss = SubjectRelation('Person', cardinality='1*') +The `events` tuple is used specify that our hook should be called before the +entity is added or updated. -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') +Then in the hook's `__call__` method, we: - 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. +* check if the 'age' attribute is edited +* if so, check the value is in the range +* if not, raise a validation error properly -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. - - -Using Operations ----------------- - -Let's augment our example with a new `subsidiary_of` relation on Company. +Now Let's augment our schema with new `Company` entity type with some relation to +`Person` (in 'mycube/schema.py'). .. sourcecode:: python @@ -232,12 +62,37 @@ boss = SubjectRelation('Person', cardinality='1*') subsidiary_of = SubjectRelation('Company', cardinality='*?') -Base example -~~~~~~~~~~~~ + +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 (still supposing we could not specify that kind of thing in the +schema). + +.. sourcecode:: python + + class CompanyBossLegalAge(Hook): + __regid__ = 'company_boss_legal_age' + __select__ = Hook.__select__ & match_rtype('boss') + events = ('before_add_relation',) -We would like to check that there is no cycle by the `subsidiary_of` -relation. This is best achieved in an Operation since all relations -are likely to be set at commit time. + 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}) + +.. Note:: + + We use the :class:`~cubicweb.server.hook.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. + +Suppose we want to check that there is no cycle by the `subsidiary_of` +relation. This is best achieved in an operation since all relations are likely to +be set at commit time. .. sourcecode:: python @@ -251,6 +106,7 @@ raise ValidationError(eid, {rtype: msg}) parents.add(parent.eid) + class CheckSubsidiaryCycleOp(Operation): def precommit_event(self): @@ -259,30 +115,20 @@ class CheckSubsidiaryCycleHook(Hook): __regid__ = 'check_no_subsidiary_cycle' + __select__ = Hook.__select__ & match_rtype('subsidiary_of') events = ('after_add_relation',) - __select__ = Hook.__select__ & match_rtype('subsidiary_of') def __call__(self): CheckSubsidiaryCycleOp(self._cw, eidto=self.eidto) -The operation is instantiated in the Hook.__call__ method. -An operation always takes a session object as first argument -(accessible as `.session` from the operation instance), and optionally -all keyword arguments needed by the operation. These keyword arguments -will be accessible as attributes from the operation instance. +Like in hooks, :exc:`~cubicweb.ValidationError` can be raised in operations. Other +exceptions are usually programming errors. -Like in Hooks, ValidationError can be raised in Operations. Other -exceptions are programming errors. - -Notice how our hook will instantiate an operation each time the Hook -is called, i.e. each time the `subsidiary_of` relation is set. - -Using set_operation -~~~~~~~~~~~~~~~~~~~ - -There is an alternative method to schedule an Operation from a Hook, -using the `set_operation` function. +In the above example, our hook will instantiate an operation each time the hook +is called, i.e. each time the `subsidiary_of` relation is set. There is an +alternative method to schedule an operation from a hook, using the +:func:`set_operation` function. .. sourcecode:: python @@ -295,142 +141,91 @@ def __call__(self): set_operation(self._cw, 'subsidiary_cycle_detection', self.eidto, - CheckSubsidiaryCycleOp, rtype=self.rtype) + CheckSubsidiaryCycleOp) class CheckSubsidiaryCycleOp(Operation): def precommit_event(self): - for eid in self._cw.transaction_data['subsidiary_cycle_detection']: - check_cycle(self.session, eid, self.rtype) + for eid in self._cw.transaction_data.pop('subsidiary_cycle_detection'): + check_cycle(self.session, eid, 'subsidiary_of') -Here, we call set_operation with a session object, a specially forged -key, a value that is the actual payload of an individual operation (in -our case, the object of the subsidiary_of relation) , the class of the -Operation, and more optional parameters to give to the operation (here -the rtype which do not vary accross operations). -The body of the operation must then iterate over the values that have -been mapped in the transaction_data dictionary to the forged key. - -This mechanism is especially useful on two occasions (not shown in our -example): - -* massive data import (reduced memory consumption within a large - transaction) - -* when several hooks need to instantiate the same operation (e.g. an - entity and a relation hook). +Here, we call :func:`set_operation` so that we will simply accumulate eids of +entities to check at the end in a single CheckSubsidiaryCycleOp operation. Value +are stored in a set associated to the 'subsidiary_cycle_detection' transaction +data key. The set initialization and operation creation are handled nicely by +:func:set_operation. -.. note:: - - A more realistic example can be found in the advanced tutorial - chapter :ref:`adv_tuto_security_propagation`. +A more realistic example can be found in the advanced tutorial chapter +:ref:`adv_tuto_security_propagation`. -.. _operations_api: -Operation: a small API overview -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Hooks writing tips +------------------ -.. autoclass:: cubicweb.server.hook.Operation -.. autoclass:: cubicweb.server.hook.LateOperation -.. autofunction:: cubicweb.server.hook.set_operation +Reminder +~~~~~~~~ -Hooks writing rules -------------------- +Never, ever use the `entity.foo = 42` notation to update an entity. It will not +work.To updating an entity attribute or relation, uses :meth:`set_attributes` and +:meth:`set_relations` methods. -Remainder -~~~~~~~~~ - -Never, ever use the `entity.foo = 42` notation to update an entity. It -will not work. How to choose between a before and an after event ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Before hooks give you access to the old attribute (or relation) -values. By definition the database is not yet updated in a before -hook. - -To access old and new values in an before_update_entity hook, one can -use the `server.hook.entity_oldnewvalue` function which returns a -tuple of the old and new values. This function takes an entity and an -attribute name as parameters. - -In a 'before_add|update_entity' hook the self.entity contains the new -values. One is allowed to further modify them before database -operations, using the dictionary notation. - -.. sourcecode:: python - - self.entity['age'] = 42 +'before_*' hooks give you access to the old attribute (or relation) +values. You can also hi-jack actually edited stuff in the case of entity +modification. Needing one of this will definitly guide your choice. -This is because using self.entity.set_attributes(age=42) will -immediately update the database (which does not make sense in a -pre-database hook), and will trigger any existing -before_add|update_entity hook, thus leading to infinite hook loops or -such awkward situations. - -Beyond these specific cases, updating an entity attribute or relation -must *always* be done using `set_attributes` and `set_relations` -methods. +Else the question is: should I need to do things before or after the actual +modification. If the answer is "it doesn't matter", use an 'after' event. -(Of course, ValidationError will always abort the current transaction, -whetever the event). -Peculiarities of inlined relations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Some relations are defined in the schema as `inlined` (see -:ref:`RelationType` for details). In this case, they are inserted in -the database at the same time as entity attributes. - -Hence in the case of before_add_relation, such relations already exist -in the database. - -Edited attributes +Validation Errors ~~~~~~~~~~~~~~~~~ -On udpates, it is possible to ask the `entity.edited_attributes` -variable whether one attribute has been updated. +When a hook is responsible to maintain the consistency of the data model detect +an error, it must use a specific exception named +:exc:`~cubicweb.ValidationError`. Raising anything but a (subclass of) +:exc:`~cubicweb.ValidationError` is a programming error. Raising a it entails +aborting the current transaction. -.. sourcecode:: python +This 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, - if 'age' not in entity.edited_attribute: - return +* a dict whose keys represent attribute (or relation) names and values + an end-user facing message (hence properly translated) relating the + problem. + + +Checking for object created/deleted in the current transaction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Deleted in transaction -~~~~~~~~~~~~~~~~~~~~~~ +In hooks, you can use the +:meth:`~cubicweb.server.session.Session.added_in_transaction` or +:meth:`~cubicweb.server.session.Session.deleted_in_transaction` of the session +object to check if an eid has been created or deleted during the hook's +transaction. -The session object has a deleted_in_transaction method, which can help -writing deletion Hooks. +This is useful to enable or disable some stuff if some entity is being added or +deleted. .. sourcecode:: python if self._cw.deleted_in_transaction(self.eidto): return -Given this predicate, we can avoid scheduling an operation. -Disabling hooks -~~~~~~~~~~~~~~~ - -It is sometimes convenient to disable some hooks. For instance to -avoid infinite Hook loops. One uses the `hooks_control` context -manager. - -This can be controlled more finely through the `category` Hook class -attribute, which is a string. +Peculiarities of inlined relations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. sourcecode:: python - - with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, ): - # ... do stuff - -.. autoclass:: cubicweb.server.session.hooks_control - -The existing categories are: ``email``, ``syncsession``, -``syncschema``, ``bookmark``, ``security``, ``worfklow``, -``metadata``, ``notification``, ``integrity``, ``activeintegrity``. - -Nothing precludes one to invent new categories and use the -hooks_control context manager to filter them (in or out). +Relations which are defined in the schema as `inlined` (see :ref:`RelationType` +for details) are inserted in the database at the same time as entity attributes. +This may have some side effect, for instance when creating entity and setting an +inlined relation in the same rql query, when 'before_add_relation' for that +relation will be run, the relation will already exist in the database (it's +usually not the case). diff -r dbb7ad04b963 -r 82d4011f54c1 entities/__init__.py --- a/entities/__init__.py Mon Sep 13 16:46:52 2010 +0200 +++ b/entities/__init__.py Mon Sep 13 16:47:03 2010 +0200 @@ -35,6 +35,11 @@ __regid__ = 'Any' __implements__ = () + @classmethod + def cw_create_url(cls, req, **kwargs): + """ return the url of the entity creation form for this entity type""" + return req.build_url('add/%s' % cls.__regid__, **kwargs) + # meta data api ########################################################### def dc_title(self): diff -r dbb7ad04b963 -r 82d4011f54c1 entity.py --- a/entity.py Mon Sep 13 16:46:52 2010 +0200 +++ b/entity.py Mon Sep 13 16:47:03 2010 +0200 @@ -19,7 +19,6 @@ __docformat__ = "restructuredtext en" -from copy import copy from warnings import warn from logilab.common import interface @@ -312,6 +311,9 @@ return '' % ( self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self)) + def __cmp__(self, other): + raise NotImplementedError('comparison not implemented for %s' % self.__class__) + def __json_encode__(self): """custom json dumps hook to dump the entity's eid which is not part of dict structure itself @@ -320,107 +322,6 @@ dumpable['eid'] = self.eid return dumpable - def __nonzero__(self): - return True - - def __hash__(self): - return id(self) - - def __cmp__(self, other): - raise NotImplementedError('comparison not implemented for %s' % self.__class__) - - def __contains__(self, key): - return key in self.cw_attr_cache - - def __iter__(self): - return iter(self.cw_attr_cache) - - def __getitem__(self, key): - if key == 'eid': - warn('[3.7] entity["eid"] is deprecated, use entity.eid instead', - DeprecationWarning, stacklevel=2) - return self.eid - return self.cw_attr_cache[key] - - def __setitem__(self, attr, value): - """override __setitem__ to update self.edited_attributes. - - Typically, a before_[update|add]_hook could do:: - - entity['generated_attr'] = generated_value - - and this way, edited_attributes will be updated accordingly. Also, add - the attribute to skip_security since we don't want to check security - for such attributes set by hooks. - """ - if attr == 'eid': - warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead', - DeprecationWarning, stacklevel=2) - self.eid = value - else: - self.cw_attr_cache[attr] = value - # don't add attribute into skip_security if already in edited - # attributes, else we may accidentaly skip a desired security check - if hasattr(self, 'edited_attributes') and \ - attr not in self.edited_attributes: - self.edited_attributes.add(attr) - self._cw_skip_security_attributes.add(attr) - - def __delitem__(self, attr): - """override __delitem__ to update self.edited_attributes on cleanup of - undesired changes introduced in the entity's dict. For example, see the - code snippet below from the `forge` cube: - - .. sourcecode:: python - - edited = self.entity.edited_attributes - has_load_left = 'load_left' in edited - if 'load' in edited and self.entity.load_left is None: - self.entity.load_left = self.entity['load'] - elif not has_load_left and edited: - # cleanup, this may cause undesired changes - del self.entity['load_left'] - - """ - del self.cw_attr_cache[attr] - if hasattr(self, 'edited_attributes'): - self.edited_attributes.remove(attr) - - def clear(self): - self.cw_attr_cache.clear() - - def get(self, key, default=None): - return self.cw_attr_cache.get(key, default) - - def setdefault(self, attr, default): - """override setdefault to update self.edited_attributes""" - value = self.cw_attr_cache.setdefault(attr, default) - # don't add attribute into skip_security if already in edited - # attributes, else we may accidentaly skip a desired security check - if hasattr(self, 'edited_attributes') and \ - attr not in self.edited_attributes: - self.edited_attributes.add(attr) - self._cw_skip_security_attributes.add(attr) - return value - - def pop(self, attr, default=_marker): - """override pop to update self.edited_attributes on cleanup of - undesired changes introduced in the entity's dict. See `__delitem__` - """ - if default is _marker: - value = self.cw_attr_cache.pop(attr) - else: - value = self.cw_attr_cache.pop(attr, default) - if hasattr(self, 'edited_attributes') and attr in self.edited_attributes: - self.edited_attributes.remove(attr) - return value - - def update(self, values): - """override update to update self.edited_attributes. See `__setitem__` - """ - for attr, value in values.items(): - self[attr] = value # use self.__setitem__ implementation - def cw_adapt_to(self, interface): """return an adapter the entity to the given interface name. @@ -590,12 +491,6 @@ # entity cloning ########################################################## - def cw_copy(self): - thecopy = copy(self) - thecopy.cw_attr_cache = copy(self.cw_attr_cache) - thecopy._cw_related_cache = {} - return thecopy - def copy_relations(self, ceid): # XXX cw_copy_relations """copy relations of the object with the given eid on this object (this method is called on the newly created copy, and @@ -680,7 +575,7 @@ rdef = rschema.rdef(self.e_schema, attrschema) if not self._cw.user.matching_groups(rdef.get_groups('read')) \ or (attrschema.type == 'Password' and skip_pwd): - self[attr] = None + self.cw_attr_cache[attr] = None continue yield attr @@ -739,7 +634,7 @@ rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0] # handle attributes for i in xrange(1, lastattr): - self[str(selected[i-1][0])] = rset[i] + self.cw_attr_cache[str(selected[i-1][0])] = rset[i] # handle relations for i in xrange(lastattr, len(rset)): rtype, role = selected[i-1][0] @@ -759,7 +654,7 @@ :param name: name of the attribute to get """ try: - value = self.cw_attr_cache[name] + return self.cw_attr_cache[name] except KeyError: if not self.cw_is_saved(): return None @@ -767,21 +662,20 @@ try: rset = self._cw.execute(rql, {'x': self.eid}) except Unauthorized: - self[name] = value = None + self.cw_attr_cache[name] = value = None else: assert rset.rowcount <= 1, (self, rql, rset.rowcount) try: - self[name] = value = rset.rows[0][0] + self.cw_attr_cache[name] = value = rset.rows[0][0] except IndexError: # probably a multisource error self.critical("can't get value for attribute %s of entity with eid %s", name, self.eid) if self.e_schema.destination(name) == 'String': - # XXX (syt) imo emtpy string is better - self[name] = value = self._cw._('unaccessible') + self.cw_attr_cache[name] = value = self._cw._('unaccessible') else: - self[name] = value = None - return value + self.cw_attr_cache[name] = value = None + return value def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related """returns a resultset of related entities @@ -985,7 +879,6 @@ you should override this method to clear them as well. """ # clear attributes cache - haseid = 'eid' in self self._cw_completed = False self.cw_attr_cache.clear() # clear relations cache @@ -1012,9 +905,9 @@ kwargs) kwargs.pop('x') # update current local object _after_ the rql query to avoid - # interferences between the query execution itself and the - # edited_attributes / skip_security_attributes machinery - self.update(kwargs) + # interferences between the query execution itself and the cw_edited / + # skip_security machinery + self.cw_attr_cache.update(kwargs) def set_relations(self, **kwargs): # XXX cw_set_relations """add relations to the given object. To set a relation where this entity @@ -1045,58 +938,13 @@ self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema, {'x': self.eid}, **kwargs) - # server side utilities ################################################### - - def _cw_rql_set_value(self, attr, value): - """call by rql execution plan when some attribute is modified - - don't use dict api in such case since we don't want attribute to be - added to skip_security_attributes. - - This method is for internal use, you should not use it. - """ - self.cw_attr_cache[attr] = value + # server side utilities #################################################### def _cw_clear_local_perm_cache(self, action): for rqlexpr in self.e_schema.get_rqlexprs(action): self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None) - @property - def _cw_skip_security_attributes(self): - try: - return self.__cw_skip_security_attributes - except: - self.__cw_skip_security_attributes = set() - return self.__cw_skip_security_attributes - - def _cw_set_defaults(self): - """set default values according to the schema""" - for attr, value in self.e_schema.defaults(): - if not self.cw_attr_cache.has_key(attr): - self[str(attr)] = value - - def _cw_check(self, creation=False): - """check this entity against its schema. Only final relation - are checked here, constraint on actual relations are checked in hooks - """ - # necessary since eid is handled specifically and yams require it to be - # in the dictionary - if self._cw is None: - _ = unicode - else: - _ = self._cw._ - if creation: - # on creations, we want to check all relations, especially - # required attributes - relations = [rschema for rschema in self.e_schema.subject_relations() - if rschema.final and rschema.type != 'eid'] - elif hasattr(self, 'edited_attributes'): - relations = [self._cw.vreg.schema.rschema(rtype) - for rtype in self.edited_attributes] - else: - relations = None - self.e_schema.check(self, creation=creation, _=_, - relations=relations) + # deprecated stuff ######################################################### @deprecated('[3.9] use entity.cw_attr_value(attr)') def get_value(self, name): @@ -1126,6 +974,109 @@ def related_rql(self, rtype, role='subject', targettypes=None): return self.cw_related_rql(rtype, role, targettypes) + @property + @deprecated('[3.10] use entity.cw_edited') + def edited_attributes(self): + return self.cw_edited + + @property + @deprecated('[3.10] use entity.cw_edited.skip_security') + def skip_security_attributes(self): + return self.cw_edited.skip_security + + @property + @deprecated('[3.10] use entity.cw_edited.skip_security') + def _cw_skip_security_attributes(self): + return self.cw_edited.skip_security + + @property + @deprecated('[3.10] use entity.cw_edited.skip_security') + def querier_pending_relations(self): + return self.cw_edited.querier_pending_relations + + @deprecated('[3.10] use key in entity.cw_attr_cache') + def __contains__(self, key): + return key in self.cw_attr_cache + + @deprecated('[3.10] iter on entity.cw_attr_cache') + def __iter__(self): + return iter(self.cw_attr_cache) + + @deprecated('[3.10] use entity.cw_attr_cache[attr]') + def __getitem__(self, key): + if key == 'eid': + warn('[3.7] entity["eid"] is deprecated, use entity.eid instead', + DeprecationWarning, stacklevel=2) + return self.eid + return self.cw_attr_cache[key] + + @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])') + def get(self, key, default=None): + return self.cw_attr_cache.get(key, default) + + @deprecated('[3.10] use entity.cw_attr_cache.clear()') + def clear(self): + self.cw_attr_cache.clear() + # XXX clear cw_edited ? + + @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value') + def __setitem__(self, attr, value): + """override __setitem__ to update self.cw_edited. + + Typically, a before_[update|add]_hook could do:: + + entity['generated_attr'] = generated_value + + and this way, cw_edited will be updated accordingly. Also, add + the attribute to skip_security since we don't want to check security + for such attributes set by hooks. + """ + if attr == 'eid': + warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead', + DeprecationWarning, stacklevel=2) + self.eid = value + else: + try: + self.cw_edited[attr] = value + except AttributeError: + self.cw_attr_cache[attr] = value + + @deprecated('[3.10] use del entity.cw_edited[attr]') + def __delitem__(self, attr): + """override __delitem__ to update self.cw_edited on cleanup of + undesired changes introduced in the entity's dict. For example, see the + code snippet below from the `forge` cube: + + .. sourcecode:: python + + edited = self.entity.cw_edited + has_load_left = 'load_left' in edited + if 'load' in edited and self.entity.load_left is None: + self.entity.load_left = self.entity['load'] + elif not has_load_left and edited: + # cleanup, this may cause undesired changes + del self.entity['load_left'] + """ + del self.cw_edited[attr] + + @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)') + def setdefault(self, attr, default): + """override setdefault to update self.cw_edited""" + return self.cw_edited.setdefault(attr, default) + + @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])') + def pop(self, attr, *args): + """override pop to update self.cw_edited on cleanup of + undesired changes introduced in the entity's dict. See `__delitem__` + """ + return self.cw_edited.pop(attr, *args) + + @deprecated('[3.10] use entity.cw_edited.update(values)') + def update(self, values): + """override update to update self.cw_edited. See `__setitem__` + """ + self.cw_edited.update(values) + # attribute and relation descriptors ########################################## @@ -1141,8 +1092,9 @@ return self return eobj.cw_attr_value(self._attrname) + @deprecated('[3.10] use entity.cw_attr_cache[attr] = value') def __set__(self, eobj, value): - eobj[self._attrname] = value + eobj.cw_attr_cache[self._attrname] = value class Relation(object): diff -r dbb7ad04b963 -r 82d4011f54c1 etwist/twconfig.py --- a/etwist/twconfig.py Mon Sep 13 16:46:52 2010 +0200 +++ b/etwist/twconfig.py Mon Sep 13 16:47:03 2010 +0200 @@ -76,12 +76,6 @@ the repository rather than the user running the command', 'group': 'main', 'level': WebConfiguration.mode == 'system' }), - ('session-time', - {'type' : 'time', - 'default': '30min', - 'help': 'session expiration time, default to 30 minutes', - 'group': 'main', 'level': 1, - }), ('pyro-server', {'type' : 'yn', # pyro is only a recommends by default, so don't activate it here diff -r dbb7ad04b963 -r 82d4011f54c1 goa/appobjects/components.py --- a/goa/appobjects/components.py Mon Sep 13 16:46:52 2010 +0200 +++ b/goa/appobjects/components.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""overrides some base views for cubicweb on google appengine +"""overrides some base views for cubicweb on google appengine""" -""" __docformat__ = "restructuredtext en" from logilab.mtconverter import xml_escape @@ -88,7 +87,8 @@ view = self.vreg.select('views', 'list', req, req.etype_rset(etype)) url = view.url() etypelink = u' %s' % (xml_escape(url), label) - yield (label, etypelink, self.add_entity_link(eschema, req)) + if eschema.has_perm(req, 'add'): + yield (label, etypelink, self.add_entity_link(etype)) ManageView.entity_types = entity_types_no_count diff -r dbb7ad04b963 -r 82d4011f54c1 goa/gaesource.py --- a/goa/gaesource.py Mon Sep 13 16:46:52 2010 +0200 +++ b/goa/gaesource.py Mon Sep 13 16:47:03 2010 +0200 @@ -118,7 +118,7 @@ Put(gaeentity) modified.clear() - def commit_event(self): + def postcommit_event(self): self._put_entities() def precommit_event(self): diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/integrity.py --- a/hooks/integrity.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/integrity.py Mon Sep 13 16:47:03 2010 +0200 @@ -17,8 +17,8 @@ # with CubicWeb. If not, see . """Core hooks: check for data integrity according to the instance'schema validity +""" -""" __docformat__ = "restructuredtext en" from threading import Lock @@ -64,8 +64,6 @@ _UNIQUE_CONSTRAINTS_LOCK.release() class _ReleaseUniqueConstraintsOperation(hook.Operation): - def commit_event(self): - pass def postcommit_event(self): _release_unique_cstr_lock(self.session) def rollback_event(self): @@ -185,9 +183,6 @@ self.critical('can\'t check constraint %s, not supported', constraint) - def commit_event(self): - pass - class CheckConstraintHook(IntegrityHook): """check the relation satisfy its constraints @@ -219,7 +214,7 @@ def __call__(self): eschema = self.entity.e_schema - for attr in self.entity.edited_attributes: + for attr in self.entity.cw_edited: if eschema.subjrels[attr].final: constraints = [c for c in eschema.rdef(attr).constraints if isinstance(c, (RQLUniqueConstraint, RQLConstraint))] @@ -236,9 +231,8 @@ def __call__(self): entity = self.entity eschema = entity.e_schema - for attr in entity.edited_attributes: + for attr, val in entity.cw_edited.iteritems(): if eschema.subjrels[attr].final and eschema.has_unique_values(attr): - val = entity[attr] if val is None: continue rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr) @@ -257,18 +251,17 @@ events = ('before_delete_entity', 'before_update_entity') def __call__(self): - if self.event == 'before_delete_entity' and self.entity.name == 'owners': + entity = self.entity + if self.event == 'before_delete_entity' and entity.name == 'owners': msg = self._cw._('can\'t be deleted') - raise ValidationError(self.entity.eid, {None: msg}) - elif self.event == 'before_update_entity' and \ - 'name' in self.entity.edited_attributes: - newname = self.entity.pop('name') - oldname = self.entity.name + raise ValidationError(entity.eid, {None: msg}) + elif self.event == 'before_update_entity' \ + and 'name' in entity.cw_edited: + oldname, newname = entity.cw_edited.oldnewvalue('name') if oldname == 'owners' and newname != oldname: qname = role_name('name', 'subject') msg = self._cw._('can\'t be changed') - raise ValidationError(self.entity.eid, {qname: msg}) - self.entity['name'] = newname + raise ValidationError(entity.eid, {qname: msg}) class TidyHtmlFields(IntegrityHook): @@ -279,15 +272,16 @@ def __call__(self): entity = self.entity metaattrs = entity.e_schema.meta_attributes() + edited = entity.cw_edited for metaattr, (metadata, attr) in metaattrs.iteritems(): - if metadata == 'format' and attr in entity.edited_attributes: + if metadata == 'format' and attr in edited: try: - value = entity[attr] + value = edited[attr] except KeyError: continue # no text to tidy if isinstance(value, unicode): # filter out None and Binary if getattr(entity, str(metaattr)) == 'text/html': - entity[attr] = soup2xhtml(value, self._cw.encoding) + edited[attr] = soup2xhtml(value, self._cw.encoding) class StripCWUserLoginHook(IntegrityHook): @@ -297,9 +291,9 @@ events = ('before_add_entity', 'before_update_entity',) def __call__(self): - user = self.entity - if 'login' in user.edited_attributes and user.login: - user.login = user.login.strip() + login = self.entity.cw_edited.get('login') + if login: + self.entity.cw_edited['login'] = login.strip() # 'active' integrity hooks: you usually don't want to deactivate them, they are diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/metadata.py --- a/hooks/metadata.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/metadata.py Mon Sep 13 16:47:03 2010 +0200 @@ -41,11 +41,12 @@ def __call__(self): timestamp = datetime.now() - self.entity.setdefault('creation_date', timestamp) - self.entity.setdefault('modification_date', timestamp) + edited = self.entity.cw_edited + edited.setdefault('creation_date', timestamp) + edited.setdefault('modification_date', timestamp) if not self._cw.get_shared_data('do-not-insert-cwuri'): cwuri = u'%seid/%s' % (self._cw.base_url(), self.entity.eid) - self.entity.setdefault('cwuri', cwuri) + edited.setdefault('cwuri', cwuri) class UpdateMetaAttrsHook(MetaDataHook): @@ -60,7 +61,7 @@ # XXX to be really clean, we should turn off modification_date update # explicitly on each command where we do not want that behaviour. if not self._cw.vreg.config.repairing: - self.entity.setdefault('modification_date', datetime.now()) + self.entity.cw_edited.setdefault('modification_date', datetime.now()) class _SetCreatorOp(hook.Operation): diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/notification.py --- a/hooks/notification.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/notification.py Mon Sep 13 16:47:03 2010 +0200 @@ -125,7 +125,7 @@ if session.added_in_transaction(self.entity.eid): return # entity is being created # then compute changes - attrs = [k for k in self.entity.edited_attributes + attrs = [k for k in self.entity.cw_edited if not k in self.skip_attrs] if not attrs: return @@ -168,8 +168,9 @@ if self._cw.added_in_transaction(self.entity.eid): return False if self.entity.e_schema == 'CWUser': - if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date', - 'last_login_time'))): + if not (frozenset(self.entity.cw_edited) + - frozenset(('eid', 'modification_date', + 'last_login_time'))): # don't record last_login_time update which are done # automatically at login time return False diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/security.py --- a/hooks/security.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/security.py Mon Sep 13 16:47:03 2010 +0200 @@ -31,12 +31,9 @@ eschema = entity.e_schema # ._cw_skip_security_attributes is there to bypass security for attributes # set by hooks by modifying the entity's dictionnary - dontcheck = entity._cw_skip_security_attributes if editedattrs is None: - try: - editedattrs = entity.edited_attributes - except AttributeError: - editedattrs = entity # XXX unexpected + editedattrs = entity.cw_edited + dontcheck = editedattrs.skip_security for attr in editedattrs: if attr in dontcheck: continue @@ -46,10 +43,6 @@ if creation and not rdef.permissions.get('update'): continue rdef.check_perm(session, 'update', eid=eid) - # don't update dontcheck until everything went fine: see usage in - # after_update_entity, where if we got an Unauthorized at hook time, we will - # retry and commit time - dontcheck |= frozenset(editedattrs) class _CheckEntityPermissionOp(hook.LateOperation): @@ -57,15 +50,12 @@ #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action session = self.session for values in session.transaction_data.pop('check_entity_perm_op'): - entity = session.entity_from_eid(values[0]) - action = values[1] + eid, action, edited = values + entity = session.entity_from_eid(eid) entity.cw_check_perm(action) - check_entity_attributes(session, entity, values[2:], + check_entity_attributes(session, entity, edited, creation=self.creation) - def commit_event(self): - pass - class _CheckRelationPermissionOp(hook.LateOperation): def precommit_event(self): @@ -76,9 +66,6 @@ session.describe(eidto)[0]) rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto) - def commit_event(self): - pass - @objectify_selector @lltrace @@ -99,7 +86,7 @@ def __call__(self): hook.set_operation(self._cw, 'check_entity_perm_op', - (self.entity.eid, 'add') + tuple(self.entity.edited_attributes), + (self.entity.eid, 'add', self.entity.cw_edited), _CheckEntityPermissionOp, creation=True) @@ -115,10 +102,10 @@ except Unauthorized: self.entity._cw_clear_local_perm_cache('update') # save back editedattrs in case the entity is reedited later in the - # same transaction, which will lead to edited_attributes being + # same transaction, which will lead to cw_edited being # overwritten hook.set_operation(self._cw, 'check_entity_perm_op', - (self.entity.eid, 'update') + tuple(self.entity.edited_attributes), + (self.entity.eid, 'update', self.entity.cw_edited), _CheckEntityPermissionOp, creation=False) diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/syncschema.py --- a/hooks/syncschema.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/syncschema.py Mon Sep 13 16:47:03 2010 +0200 @@ -128,14 +128,12 @@ def check_valid_changes(session, entity, ro_attrs=('name', 'final')): errors = {} # don't use getattr(entity, attr), we would get the modified value if any - for attr in entity.edited_attributes: + for attr in entity.cw_edited: if attr in ro_attrs: - newval = entity.pop(attr) - origval = getattr(entity, attr) + origval, newval = entity.cw_edited.oldnewvalue(attr) if newval != origval: errors[attr] = session._("can't change the %s attribute") % \ display_name(session, attr) - entity[attr] = newval if errors: raise ValidationError(entity.eid, errors) @@ -907,7 +905,7 @@ def __call__(self): entity = self.entity - if entity.get('final'): + if entity.cw_edited.get('final'): return CWETypeAddOp(self._cw, entity=entity) @@ -921,8 +919,8 @@ entity = self.entity check_valid_changes(self._cw, entity, ro_attrs=('final',)) # don't use getattr(entity, attr), we would get the modified value if any - if 'name' in entity.edited_attributes: - oldname, newname = hook.entity_oldnewvalue(entity, 'name') + if 'name' in entity.cw_edited: + oldname, newname = entity.cw_edited.oldnewvalue('name') if newname.lower() != oldname.lower(): CWETypeRenameOp(self._cw, oldname=oldname, newname=newname) @@ -965,8 +963,8 @@ entity = self.entity rtypedef = ybo.RelationType(name=entity.name, description=entity.description, - inlined=entity.get('inlined', False), - symmetric=entity.get('symmetric', False), + inlined=entity.cw_edited.get('inlined', False), + symmetric=entity.cw_edited.get('symmetric', False), eid=entity.eid) MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef) @@ -981,10 +979,10 @@ check_valid_changes(self._cw, entity) newvalues = {} for prop in ('symmetric', 'inlined', 'fulltext_container'): - if prop in entity.edited_attributes: - old, new = hook.entity_oldnewvalue(entity, prop) + if prop in entity.cw_edited: + old, new = entity.cw_edited.oldnewvalue(prop) if old != new: - newvalues[prop] = entity[prop] + newvalues[prop] = new if newvalues: rschema = self._cw.vreg.schema.rschema(entity.name) CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity, @@ -1069,8 +1067,8 @@ attr = 'ordernum' else: attr = prop - if attr in entity.edited_attributes: - old, new = hook.entity_oldnewvalue(entity, attr) + if attr in entity.cw_edited: + old, new = entity.cw_edited.oldnewvalue(attr) if old != new: newvalues[prop] = new if newvalues: diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/syncsession.py --- a/hooks/syncsession.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/syncsession.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Core hooks: synchronize living session on persistent data changes +"""Core hooks: synchronize living session on persistent data changes""" -""" __docformat__ = "restructuredtext en" from yams.schema import role_name @@ -56,26 +55,25 @@ class _DeleteGroupOp(_GroupOperation): """synchronize user when a in_group relation has been deleted""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" groups = self.cnxuser.groups try: groups.remove(self.group) except KeyError: self.error('user %s not in group %s', self.cnxuser, self.group) - return class _AddGroupOp(_GroupOperation): """synchronize user when a in_group relation has been added""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" groups = self.cnxuser.groups if self.group in groups: self.warning('user %s already in group %s', self.cnxuser, self.group) - return - groups.add(self.group) + else: + groups.add(self.group) class SyncInGroupHook(SyncSessionHook): @@ -98,7 +96,7 @@ self.cnxid = cnxid hook.Operation.__init__(self, session) - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" try: self.session.repo.close(self.cnxid) @@ -123,7 +121,7 @@ class _DelCWPropertyOp(hook.Operation): """a user's custom properties has been deleted""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" try: del self.cwpropdict[self.key] @@ -134,7 +132,7 @@ class _ChangeCWPropertyOp(hook.Operation): """a user's custom properties has been added/changed""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" self.cwpropdict[self.key] = self.value @@ -142,7 +140,7 @@ class _AddCWPropertyOp(hook.Operation): """a user's custom properties has been added/changed""" - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been commited""" cwprop = self.cwprop if not cwprop.for_user: @@ -180,8 +178,8 @@ def __call__(self): entity = self.entity - if not ('pkey' in entity.edited_attributes or - 'value' in entity.edited_attributes): + if not ('pkey' in entity.cw_edited or + 'value' in entity.cw_edited): return key, value = entity.pkey, entity.value session = self._cw diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/test/unittest_hooks.py --- a/hooks/test/unittest_hooks.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/test/unittest_hooks.py Mon Sep 13 16:47:03 2010 +0200 @@ -143,7 +143,7 @@ entity.set_attributes(name=u'wf2') self.assertEquals(entity.description, u'yo') entity.set_attributes(description=u'R&D

yo') - entity.pop('description') + entity.cw_attr_cache.pop('description') self.assertEquals(entity.description, u'R&D

yo

') diff -r dbb7ad04b963 -r 82d4011f54c1 hooks/workflow.py --- a/hooks/workflow.py Mon Sep 13 16:46:52 2010 +0200 +++ b/hooks/workflow.py Mon Sep 13 16:47:03 2010 +0200 @@ -135,7 +135,7 @@ qname = role_name('to_state', 'subject') msg = session._("state doesn't belong to entity's current workflow") raise ValidationError(self.trinfo.eid, {'to_state': msg}) - tostate = wftr.get_exit_point(forentity, trinfo['to_state']) + tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state']) if tostate is not None: # reached an exit point msg = session._('exiting from subworkflow %s') @@ -185,7 +185,7 @@ entity = self.entity # first retreive entity to which the state change apply try: - foreid = entity['wf_info_for'] + foreid = entity.cw_attr_cache['wf_info_for'] except KeyError: qname = role_name('wf_info_for', 'subject') msg = session._('mandatory relation') @@ -213,7 +213,7 @@ or not session.write_security) # no investigate the requested state change... try: - treid = entity['by_transition'] + treid = entity.cw_attr_cache['by_transition'] except KeyError: # no transition set, check user is a manager and destination state # is specified (and valid) @@ -221,7 +221,7 @@ qname = role_name('by_transition', 'subject') msg = session._('mandatory relation') raise ValidationError(entity.eid, {qname: msg}) - deststateeid = entity.get('to_state') + deststateeid = entity.cw_attr_cache.get('to_state') if not deststateeid: qname = role_name('by_transition', 'subject') msg = session._('mandatory relation') @@ -247,8 +247,8 @@ if not tr.may_be_fired(foreid): msg = session._("transition may not be fired") raise ValidationError(entity.eid, {qname: msg}) - if entity.get('to_state'): - deststateeid = entity['to_state'] + deststateeid = entity.cw_attr_cache.get('to_state') + if deststateeid is not None: if not cowpowers and deststateeid != tr.destination(forentity).eid: qname = role_name('by_transition', 'subject') msg = session._("transition isn't allowed") @@ -262,8 +262,8 @@ else: deststateeid = tr.destination(forentity).eid # everything is ok, add missing information on the trinfo entity - entity['from_state'] = fromstate.eid - entity['to_state'] = deststateeid + entity.cw_edited['from_state'] = fromstate.eid + entity.cw_edited['to_state'] = deststateeid nocheck = session.transaction_data.setdefault('skip-security', set()) nocheck.add((entity.eid, 'from_state', fromstate.eid)) nocheck.add((entity.eid, 'to_state', deststateeid)) @@ -278,11 +278,12 @@ def __call__(self): trinfo = self.entity - _change_state(self._cw, trinfo['wf_info_for'], - trinfo['from_state'], trinfo['to_state']) - forentity = self._cw.entity_from_eid(trinfo['wf_info_for']) + rcache = trinfo.cw_attr_cache + _change_state(self._cw, rcache['wf_info_for'], rcache['from_state'], + rcache['to_state']) + forentity = self._cw.entity_from_eid(rcache['wf_info_for']) iworkflowable = forentity.cw_adapt_to('IWorkflowable') - assert iworkflowable.current_state.eid == trinfo['to_state'] + assert iworkflowable.current_state.eid == rcache['to_state'] if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid: _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo) diff -r dbb7ad04b963 -r 82d4011f54c1 i18n/en.po --- a/i18n/en.po Mon Sep 13 16:46:52 2010 +0200 +++ b/i18n/en.po Mon Sep 13 16:47:03 2010 +0200 @@ -1124,52 +1124,52 @@ msgid "boxes" msgstr "" -msgid "boxes_bookmarks_box" +msgid "ctxcomponents_bookmarks_box" msgstr "bookmarks box" -msgid "boxes_bookmarks_box_description" +msgid "ctxcomponents_bookmarks_box_description" msgstr "box listing the user's bookmarks" -msgid "boxes_download_box" +msgid "ctxcomponents_download_box" msgstr "download box" -msgid "boxes_download_box_description" -msgstr "" - -msgid "boxes_edit_box" +msgid "ctxcomponents_download_box_description" +msgstr "" + +msgid "ctxcomponents_edit_box" msgstr "actions box" -msgid "boxes_edit_box_description" +msgid "ctxcomponents_edit_box_description" msgstr "box listing the applicable actions on the displayed data" -msgid "boxes_filter_box" +msgid "ctxcomponents_filter_box" msgstr "filter" -msgid "boxes_filter_box_description" +msgid "ctxcomponents_filter_box_description" msgstr "box providing filter within current search results functionality" -msgid "boxes_possible_views_box" +msgid "ctxcomponents_possible_views_box" msgstr "possible views box" -msgid "boxes_possible_views_box_description" +msgid "ctxcomponents_possible_views_box_description" msgstr "box listing the possible views for the displayed data" -msgid "boxes_rss" +msgid "ctxcomponents_rss" msgstr "rss box" -msgid "boxes_rss_description" +msgid "ctxcomponents_rss_description" msgstr "RSS icon to get displayed data as a RSS thread" -msgid "boxes_search_box" +msgid "ctxcomponents_search_box" msgstr "search box" -msgid "boxes_search_box_description" +msgid "ctxcomponents_search_box_description" msgstr "search box" -msgid "boxes_startup_views_box" +msgid "ctxcomponents_startup_views_box" msgstr "startup views box" -msgid "boxes_startup_views_box_description" +msgid "ctxcomponents_startup_views_box_description" msgstr "box listing the possible start pages" msgid "bug report sent" @@ -1445,41 +1445,41 @@ msgid "content type" msgstr "" -msgid "contentnavigation" +msgid "ctxcomponents" msgstr "contextual components" -msgid "contentnavigation_breadcrumbs" +msgid "ctxcomponents_breadcrumbs" msgstr "breadcrumb" -msgid "contentnavigation_breadcrumbs_description" +msgid "ctxcomponents_breadcrumbs_description" msgstr "breadcrumbs bar that display a path locating the page in the site" -msgid "contentnavigation_metadata" +msgid "ctxcomponents_metadata" msgstr "entity's metadata" -msgid "contentnavigation_metadata_description" -msgstr "" - -msgid "contentnavigation_prevnext" +msgid "ctxcomponents_metadata_description" +msgstr "" + +msgid "ctxcomponents_prevnext" msgstr "previous / next entity" -msgid "contentnavigation_prevnext_description" +msgid "ctxcomponents_prevnext_description" msgstr "" "display link to go from one entity to another on entities implementing the " "\"previous/next\" interface." -msgid "contentnavigation_seealso" +msgid "ctxcomponents_seealso" msgstr "see also" -msgid "contentnavigation_seealso_description" +msgid "ctxcomponents_seealso_description" msgstr "" "section containing entities related by the \"see also\" relation on entities " "supporting it." -msgid "contentnavigation_wfhistory" +msgid "ctxcomponents_wfhistory" msgstr "workflow history" -msgid "contentnavigation_wfhistory_description" +msgid "ctxcomponents_wfhistory_description" msgstr "show the workflow's history." msgid "context" diff -r dbb7ad04b963 -r 82d4011f54c1 i18n/es.po --- a/i18n/es.po Mon Sep 13 16:46:52 2010 +0200 +++ b/i18n/es.po Mon Sep 13 16:47:03 2010 +0200 @@ -1170,54 +1170,54 @@ msgid "boxes" msgstr "Cajas" -msgid "boxes_bookmarks_box" +msgid "ctxcomponents_bookmarks_box" msgstr "Caja de Favoritos" -msgid "boxes_bookmarks_box_description" +msgid "ctxcomponents_bookmarks_box_description" msgstr "Muestra y permite administrar los favoritos del usuario" -msgid "boxes_download_box" +msgid "ctxcomponents_download_box" msgstr "Configuración de caja de descargas" -msgid "boxes_download_box_description" +msgid "ctxcomponents_download_box_description" msgstr "Caja que contiene los elementos descargados" -msgid "boxes_edit_box" +msgid "ctxcomponents_edit_box" msgstr "Caja de Acciones" -msgid "boxes_edit_box_description" +msgid "ctxcomponents_edit_box_description" msgstr "Muestra las acciones posibles a ejecutar para los datos seleccionados" -msgid "boxes_filter_box" +msgid "ctxcomponents_filter_box" msgstr "Filtros" -msgid "boxes_filter_box_description" +msgid "ctxcomponents_filter_box_description" msgstr "Muestra los filtros aplicables a una búsqueda realizada" -msgid "boxes_possible_views_box" +msgid "ctxcomponents_possible_views_box" msgstr "Caja de Vistas Posibles" -msgid "boxes_possible_views_box_description" +msgid "ctxcomponents_possible_views_box_description" msgstr "Muestra las vistas posibles a aplicar a los datos seleccionados" -msgid "boxes_rss" +msgid "ctxcomponents_rss" msgstr "Ãcono RSS" -msgid "boxes_rss_description" +msgid "ctxcomponents_rss_description" msgstr "Muestra el ícono RSS para vistas RSS" -msgid "boxes_search_box" +msgid "ctxcomponents_search_box" msgstr "Caja de búsqueda" -msgid "boxes_search_box_description" +msgid "ctxcomponents_search_box_description" msgstr "" "Permite realizar una búsqueda simple para cualquier tipo de dato en la " "aplicación" -msgid "boxes_startup_views_box" +msgid "ctxcomponents_startup_views_box" msgstr "Caja Vistas de inicio" -msgid "boxes_startup_views_box_description" +msgid "ctxcomponents_startup_views_box_description" msgstr "Muestra las vistas de inicio de la aplicación" msgid "bug report sent" @@ -1499,38 +1499,38 @@ msgid "contentnavigation" msgstr "Componentes contextuales" -msgid "contentnavigation_breadcrumbs" +msgid "ctxcomponents_breadcrumbs" msgstr "Ruta de Navegación" -msgid "contentnavigation_breadcrumbs_description" +msgid "ctxcomponents_breadcrumbs_description" msgstr "Muestra la ruta que permite localizar la página actual en el Sistema" -msgid "contentnavigation_metadata" +msgid "ctxcomponents_metadata" msgstr "Metadatos de la Entidad" -msgid "contentnavigation_metadata_description" +msgid "ctxcomponents_metadata_description" msgstr "" -msgid "contentnavigation_prevnext" +msgid "ctxcomponents_prevnext" msgstr "Elemento anterior / siguiente" -msgid "contentnavigation_prevnext_description" +msgid "ctxcomponents_prevnext_description" msgstr "" "Muestra las ligas que permiten pasar de una entidad a otra en las entidades " "que implementan la interface \"anterior/siguiente\"." -msgid "contentnavigation_seealso" +msgid "ctxcomponents_seealso" msgstr "Vea también" -msgid "contentnavigation_seealso_description" +msgid "ctxcomponents_seealso_description" msgstr "" "sección que muestra las entidades relacionadas por la relación \"vea también" "\" , si la entidad soporta esta relación." -msgid "contentnavigation_wfhistory" +msgid "ctxcomponents_wfhistory" msgstr "Histórico del workflow." -msgid "contentnavigation_wfhistory_description" +msgid "ctxcomponents_wfhistory_description" msgstr "" "Sección que muestra el reporte histórico de las transiciones del workflow. " "Aplica solo en entidades con workflow." diff -r dbb7ad04b963 -r 82d4011f54c1 i18n/fr.po --- a/i18n/fr.po Mon Sep 13 16:46:52 2010 +0200 +++ b/i18n/fr.po Mon Sep 13 16:47:03 2010 +0200 @@ -1169,53 +1169,53 @@ msgid "boxes" msgstr "boîtes" -msgid "boxes_bookmarks_box" +msgid "ctxcomponents_bookmarks_box" msgstr "boîte signets" -msgid "boxes_bookmarks_box_description" +msgid "ctxcomponents_bookmarks_box_description" msgstr "boîte contenant les signets de l'utilisateur" -msgid "boxes_download_box" +msgid "ctxcomponents_download_box" msgstr "boîte de téléchargement" -msgid "boxes_download_box_description" +msgid "ctxcomponents_download_box_description" msgstr "boîte contenant un lien permettant de télécharger la ressource" -msgid "boxes_edit_box" +msgid "ctxcomponents_edit_box" msgstr "boîte d'actions" -msgid "boxes_edit_box_description" +msgid "ctxcomponents_edit_box_description" msgstr "" "boîte affichant les différentes actions possibles sur les données affichées" -msgid "boxes_filter_box" +msgid "ctxcomponents_filter_box" msgstr "filtrer" -msgid "boxes_filter_box_description" +msgid "ctxcomponents_filter_box_description" msgstr "boîte permettant de filtrer parmi les résultats d'une recherche" -msgid "boxes_possible_views_box" +msgid "ctxcomponents_possible_views_box" msgstr "boîte des vues possibles" -msgid "boxes_possible_views_box_description" +msgid "ctxcomponents_possible_views_box_description" msgstr "boîte affichant les vues possibles pour les données courantes" -msgid "boxes_rss" +msgid "ctxcomponents_rss" msgstr "icône RSS" -msgid "boxes_rss_description" +msgid "ctxcomponents_rss_description" msgstr "l'icône RSS permettant de récupérer la vue RSS des données affichées" -msgid "boxes_search_box" +msgid "ctxcomponents_search_box" msgstr "boîte de recherche" -msgid "boxes_search_box_description" +msgid "ctxcomponents_search_box_description" msgstr "boîte avec un champ de recherche simple" -msgid "boxes_startup_views_box" +msgid "ctxcomponents_startup_views_box" msgstr "boîte des vues de départs" -msgid "boxes_startup_views_box_description" +msgid "ctxcomponents_startup_views_box_description" msgstr "boîte affichant les vues de départs de l'application" msgid "bug report sent" @@ -1496,42 +1496,42 @@ msgid "content type" msgstr "type MIME" -msgid "contentnavigation" +msgid "ctxcomponents" msgstr "composants contextuels" -msgid "contentnavigation_breadcrumbs" +msgid "ctxcomponents_breadcrumbs" msgstr "fil d'ariane" -msgid "contentnavigation_breadcrumbs_description" +msgid "ctxcomponents_breadcrumbs_description" msgstr "" "affiche un chemin permettant de localiser la page courante dans le site" -msgid "contentnavigation_metadata" +msgid "ctxcomponents_metadata" msgstr "méta-données de l'entité" -msgid "contentnavigation_metadata_description" +msgid "ctxcomponents_metadata_description" msgstr "" -msgid "contentnavigation_prevnext" +msgid "ctxcomponents_prevnext" msgstr "élément précedent / suivant" -msgid "contentnavigation_prevnext_description" +msgid "ctxcomponents_prevnext_description" msgstr "" "affiche des liens permettant de passer d'une entité à une autre sur les " "entités implémentant l'interface \"précédent/suivant\"." -msgid "contentnavigation_seealso" +msgid "ctxcomponents_seealso" msgstr "voir aussi" -msgid "contentnavigation_seealso_description" +msgid "ctxcomponents_seealso_description" msgstr "" "section affichant les entités liées par la relation \"voir aussi\" si " "l'entité supporte cette relation." -msgid "contentnavigation_wfhistory" +msgid "ctxcomponents_wfhistory" msgstr "historique du workflow." -msgid "contentnavigation_wfhistory_description" +msgid "ctxcomponents_wfhistory_description" msgstr "" "section affichant l'historique du workflow pour les entités ayant un " "workflow." diff -r dbb7ad04b963 -r 82d4011f54c1 misc/migration/3.10.0_Any.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.10.0_Any.py Mon Sep 13 16:47:03 2010 +0200 @@ -0,0 +1,5 @@ +# rename cwprops for boxes/contentnavigation +for x in rql('Any X,XK WHERE X pkey XK, ' + 'X pkey ~= "boxes.%s" OR ' + 'X pkey ~= "contentnavigation.%s"').entities(): + x.set_attributes(pkey=u'ctxcomponents.' + x.pkey.split('.',1)) diff -r dbb7ad04b963 -r 82d4011f54c1 misc/migration/3.10.0_common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/misc/migration/3.10.0_common.py Mon Sep 13 16:47:03 2010 +0200 @@ -0,0 +1,1 @@ +option_group_changed('cleanup-session-time', 'web', 'main') diff -r dbb7ad04b963 -r 82d4011f54c1 req.py --- a/req.py Mon Sep 13 16:46:52 2010 +0200 +++ b/req.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Base class for request/session +"""Base class for request/session""" -""" __docformat__ = "restructuredtext en" from warnings import warn diff -r dbb7ad04b963 -r 82d4011f54c1 rset.py --- a/rset.py Mon Sep 13 16:46:52 2010 +0200 +++ b/rset.py Mon Sep 13 16:47:03 2010 +0200 @@ -484,7 +484,7 @@ if attr == 'eid': entity.eid = rowvalues[outerselidx] else: - entity[attr] = rowvalues[outerselidx] + entity.cw_attr_cache[attr] = rowvalues[outerselidx] continue else: rschema = eschema.objrels[attr] diff -r dbb7ad04b963 -r 82d4011f54c1 selectors.py --- a/selectors.py Mon Sep 13 16:46:52 2010 +0200 +++ b/selectors.py Mon Sep 13 16:47:03 2010 +0200 @@ -60,9 +60,9 @@ .. sourcecode:: python - class RSSIconBox(ExtResourcesBoxTemplate): + class RSSIconBox(box.Box): ''' just display the RSS icon on uniform result set ''' - __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity() + __select__ = box.Box.__select__ & non_final_entity() It takes into account: @@ -1203,6 +1203,7 @@ # Web request selectors ######################################################## +# XXX deprecate @objectify_selector @lltrace def primary_view(cls, req, view=None, **kwargs): @@ -1220,6 +1221,15 @@ return 1 +@objectify_selector +@lltrace +def contextual(cls, req, view=None, **kwargs): + """Return 1 if view's contextual property is true""" + if view is not None and view.contextual: + return 1 + return 0 + + class match_view(ExpectedValueSelector): """Return 1 if a view is specified an as its registry id is in one of the expected view id given to the initializer. @@ -1231,6 +1241,19 @@ return 1 +class match_context(ExpectedValueSelector): + + @lltrace + def __call__(self, cls, req, context=None, **kwargs): + try: + if not context in self.expected: + return 0 + except AttributeError: + return 1 # class doesn't care about search state, accept it + return 1 + + +# XXX deprecate @objectify_selector @lltrace def match_context_prop(cls, req, context=None, **kwargs): @@ -1251,8 +1274,6 @@ return 1 propval = req.property_value('%s.%s.context' % (cls.__registry__, cls.__regid__)) - if not propval: - propval = cls.context if propval and context != propval: return 0 return 1 diff -r dbb7ad04b963 -r 82d4011f54c1 server/hook.py --- a/server/hook.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/hook.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,37 +15,228 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Hooks management +""" +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`). + +.. _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html + +Hooks +~~~~~ + +In |cubicweb|, hooks are subclasses of the :class:`~cubicweb.server.hook.Hook` +class. They are selected over a set of pre-defined `events` (and possibly more +conditions, hooks being selectable appobjects like views and components). They +should implement a :meth:`~cubicweb.server.hook.Hook.__call__` method that will +be called when the hook is triggered. -This module defined the `Hook` class and registry and a set of abstract classes -for operations. +There are two families of events: data events (before / after any individual +update of an entity / or a relation in the repository) and server events (such +as server startup or shutdown). In a typical application, most of the hooks are +defined over data events. + +Also, some :class:`~cubicweb.server.hook.Operation` may be registered by hooks, +which will be fired when the transaction is commited or rollbacked. + +The purpose of data event hooks is usually to complement the data model as +defined in the schema, which is static by nature and only provide a restricted +builtin set of dynamic constraints, with dynamic or value driven behaviours. +For instance they can serve the following purposes: + +* enforcing constraints that the static schema cannot express (spanning several + entities/relations, exotic value ranges and cardinalities, etc.) + +* implement computed attributes + +It is functionally equivalent to a `database 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). + +.. _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger -Hooks are called before / after any individual update of entities / relations -in the repository and on special events such as server startup or shutdown. +Operations +~~~~~~~~~~ + +Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` class +that may be created by hooks and scheduled to happen just before (or after) the +`precommit`, `postcommit` or `rollback` event. Hooks are being fired immediately +on data operations, and it is sometime necessary to delay the actual work down +to a time where all other hooks have run. 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. + +Operations may be used to: + +* implements a validation check which needs that all relations be already set on + an entity + +* process various side effects associated with a transaction such as filesystem + udpates, mail notifications, etc. -Operations may be registered by hooks during a transaction, which will be -fired when the pool is commited or rollbacked. +Events +------ + +Hooks are mostly defined and used to handle `dataflow`_ operations. It +means as data gets in (entities added, updated, relations set or +unset), specific events are issued and the Hooks matching these events +are called. + +You can get the event that triggered a hook by accessing its :attr:event +attribute. + +.. _`dataflow`: http://en.wikipedia.org/wiki/Dataflow -Entity hooks (eg before_add_entity, after_add_entity, before_update_entity, -after_update_entity, before_delete_entity, after_delete_entity) all have an -`entity` attribute +Entity modification related events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When called for one of these events, hook will have an `entity` attribute +containing the entity instance. + +* 'before_add_entity', 'before_update_entity': + + - on those events, you can check what attributes of the entity are modified in + `entity.cw_edited` (by definition the database is not yet updated in a before + event) + + - you are allowed to further modify the entity before database operations, + using the dictionary notation. By doing this, you'll avoid the need for a + whole new rql query processing, the only difference is that the underlying + backend query (eg usually sql) will contains the additional data. For + example: + + .. sourcecode:: python + + self.entity.set_attributes(age=42) + + will set the `age` attribute of the entity to 42. But to do so, it will + generate a rql query that will have to be processed, then trigger some + hooks, and so one (potentially leading to infinite hook loops or such + awkward situations..) You can avoid this by doing the modification that way: + + .. sourcecode:: python + + self.entity.cw_edited['age'] = 42 + + Here the attribute will simply be edited in the same query that the + one that triggered the hook. -Relation (eg before_add_relation, after_add_relation, before_delete_relation, -after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes. + Similarly, removing an attribute from `cw_edited` will cancel its + modification. + + - on 'before_update_entity' event, you can access to old and new values in + this hook, by using `entity.cw_edited.oldnewvalue(attr)` + + +* 'after_add_entity', 'after_update_entity' + + - on those events, you can still check what attributes of the entity are + modified in `entity.cw_edited` but you can't get anymore the old value, nor + modify it. + +* 'before_delete_entity', 'after_delete_entity' + + - on those events, the entity has no `cw_edited` set. + + +Relation modification related events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When called for one of these events, hook will have `eidfrom`, `rtype`, `eidto` +attributes containing respectivly the eid of the subject entity, the relation +type and the eid of the object entity. + +* 'before_add_relation', 'before_delete_relation' + + - on those events, you can still get original relation by issuing a rql query + +* 'after_add_relation', 'after_delete_relation' + +This is an occasion to remind us that relations support the add / delete +operation, but no update. + + +Non data events +~~~~~~~~~~~~~~~ -Server start/maintenance/stop hooks (eg server_startup, server_maintenance, -server_shutdown) have a `repo` attribute, but *their `_cw` attribute is None*. -The `server_startup` is called on regular startup, while `server_maintenance` -is called on cubicweb-ctl upgrade or shell commands. `server_shutdown` is -called anyway. +Hooks called on server start/maintenance/stop event (eg 'server_startup', +'server_maintenance', 'server_shutdown') have a `repo` attribute, but *their +`_cw` attribute is None*. The `server_startup` is called on regular startup, +while `server_maintenance` is called on cubicweb-ctl upgrade or shell +commands. `server_shutdown` is called anyway. + +Hooks called on backup/restore event (eg 'server_backup', 'server_restore') have +a `repo` and a `timestamp` attributes, but *their `_cw` attribute is None*. + +Hooks called on session event (eg 'session_open', 'session_close') have no +special attribute. + + +API +--- + +Hooks control +~~~~~~~~~~~~~ + +It is sometimes convenient to explicitly enable or disable some hooks. For +instance if you want to disable some integrity checking hook. This can be +controlled more finely through the `category` class attribute, which is a string +giving a category name. One can then uses the +:class:`~cubicweb.server.session.hooks_control` context manager to explicitly +enable or disable some categories. + +.. autoclass:: cubicweb.server.session.hooks_control + + +The existing categories are: + +* ``security``, security checking hooks + +* ``worfklow``, workflow handling hooks -Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a -`timestamp` attributes, but *their `_cw` attribute is None*. +* ``metadata``, hooks setting meta-data on newly created entities + +* ``notification``, email notification hooks + +* ``integrity``, data integrity checking hooks + +* ``activeintegrity``, data integrity consistency hooks, that you should *never* + want to disable + +* ``syncsession``, hooks synchronizing existing sessions + +* ``syncschema``, hooks synchronizing instance schema (including the physical database) + +* ``email``, email address handling hooks + +* ``bookmark``, bookmark entities handling hooks + -Session hooks (eg session_open, session_close) have no special attribute. +Nothing precludes one to invent new categories and use the +:class:`~cubicweb.server.session.hooks_control` context manager to filter them +in or out. + + +Hooks specific selector +~~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: cubicweb.server.hook.match_rtype +.. autoclass:: cubicweb.server.hook.match_rtype_sets + + +Hooks and operations classes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: cubicweb.server.hook.Hook +.. autoclass:: cubicweb.server.hook.Operation +.. autoclass:: cubicweb.server.hook.LateOperation +.. autofunction:: cubicweb.server.hook.set_operation + """ from __future__ import with_statement @@ -61,6 +252,7 @@ from logilab.common.logging_ext import set_log_methods from cubicweb import RegistryNotFound +from cubicweb.vregistry import classid from cubicweb.cwvreg import CWRegistry, VRegistry from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, is_instance) @@ -83,7 +275,7 @@ for appobjects in self.values(): for cls in appobjects: if not cls.enabled: - warn('[3.6] %s: enabled is deprecated' % cls) + warn('[3.6] %s: enabled is deprecated' % classid(cls)) self.unregister(cls) def register(self, obj, **kwargs): @@ -119,21 +311,9 @@ for event in ALL_HOOKS: VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry -_MARKER = object() +@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)') def entity_oldnewvalue(entity, attr): - """returns the couple (old attr value, new attr value) - - NOTE: will only work in a before_update_entity hook - """ - # get new value and remove from local dict to force a db query to - # fetch old value - newvalue = entity.pop(attr, _MARKER) - oldvalue = getattr(entity, attr) - if newvalue is not _MARKER: - entity[attr] = newvalue - else: - newvalue = oldvalue - return oldvalue, newvalue + return entity.cw_edited.oldnewvalue(attr) # some hook specific selectors ################################################# @@ -203,6 +383,29 @@ # base class for hook ########################################################## class Hook(AppObject): + """Base class for hook. + + Hooks being appobjects like views, they have a `__regid__` and a `__select__` + class attribute. Like all appobjects, hooks have the `self._cw` attribute which + represents the current session. In entity hooks, a `self.entity` attribute is + also present. + + The `events` tuple is used by the base class selector to dispatch the hook + on the right events. It is possible to dispatch on multiple events at once + if needed (though take care as hook attribute may vary as described above). + + .. Note:: + + Do not forget to extend the base class selectors as in :: + + .. sourcecode:: python + + class MyHook(Hook): + __regid__ = 'whatever' + __select__ = Hook.__select__ & implements('Person') + + else your hooks will be called madly, whatever the event. + """ __select__ = enabled_category() # set this in derivated classes events = None @@ -231,16 +434,16 @@ @classproperty def __regid__(cls): - warn('[3.6] %s.%s: please specify an id for your hook' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: please specify an id for your hook' % classid(cls), + DeprecationWarning) return str(id(cls)) @classmethod def __registered__(cls, reg): super(Hook, cls).__registered__(reg) if getattr(cls, 'accepts', None): - warn('[3.6] %s.%s: accepts is deprecated, define proper __select__' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: accepts is deprecated, define proper __select__' + % classid(cls), DeprecationWarning) rtypes = [] for ertype in cls.accepts: if ertype.islower(): @@ -261,9 +464,8 @@ def __call__(self): if hasattr(self, 'call'): - cls = self.__class__ - warn('[3.6] %s.%s: call is deprecated, implement __call__' - % (cls.__module__, cls.__name__), DeprecationWarning) + warn('[3.6] %s: call is deprecated, implement __call__' + % classid(self.__class__), DeprecationWarning) if self.event.endswith('_relation'): self.call(self._cw, self.eidfrom, self.rtype, self.eidto) elif 'delete' in self.event: @@ -365,40 +567,53 @@ # abstract classes for operation ############################################### class Operation(object): - """an operation is triggered on connections pool events related to + """Base class for operations. + + Operation may be instantiated in the hooks' `__call__` method. It always + takes a session object as first argument (accessible as `.session` from the + operation instance), and optionally all keyword arguments needed by the + operation. These keyword arguments will be accessible as attributes from the + operation instance. + + An operation is triggered on connections pool events related to commit / rollback transations. Possible events are: - precommit: - the pool is preparing to commit. You shouldn't do anything which - has to be reverted if the commit fails at this point, but you can freely - do any heavy computation or raise an exception if the commit can't go. - You can add some new operations during this phase but their precommit - event won't be triggered + * 'precommit': - commit: - the pool is preparing to commit. You should avoid to do to expensive - stuff or something that may cause an exception in this event + the transaction is being prepared for commit. You can freely do any heavy + computation, raise an exception if the commit can't go. or even add some + new operations during this phase. If you do anything which has to be + reverted if the commit fails afterwards (eg altering the file system for + instance), you'll have to support the 'revertprecommit' event to revert + things by yourself - revertcommit: - if an operation failed while commited, this event is triggered for - all operations which had their commit event already to let them - revert things (including the operation which made fail the commit) + * 'revertprecommit': + + if an operation failed while being pre-commited, this event is triggered + for all operations which had their 'precommit' event already fired to let + them revert things (including the operation which made the commit fail) + + * 'rollback': - rollback: the transaction has been either rollbacked either: + * intentionaly - * a precommit event failed, all operations are rollbacked - * a commit event failed, all operations which are not been triggered for - commit are rollbacked + * a 'precommit' event failed, in which case all operations are rollbacked + once 'revertprecommit'' has been called + + * 'postcommit': - postcommit: - The transaction is over. All the ORM entities are - invalid. If you need to work on the database, you need to stard - a new transaction, for instance using a new internal_session, - which you will need to commit (and close!). + the transaction is over. All the ORM entities accessed by the earlier + transaction are invalid. If you need to work on the database, you need to + start a new transaction, for instance using a new internal session, which + you will need to commit (and close!). - order of operations may be important, and is controlled according to - the insert_index's method output + For an operation to support an event, one has to implement the `_event` method with no arguments. + + Notice order of operations may be important, and is controlled according to + the insert_index's method output (whose implementation vary according to the + base hook class used). """ def __init__(self, session, **kwargs): @@ -428,6 +643,10 @@ def handle_event(self, event): """delegate event handling to the opertaion""" + if event == 'postcommit_event' and hasattr(self, 'commit_event'): + warn('[3.10] %s: commit_event method has been replaced by postcommit_event' + % classid(self.__class__), DeprecationWarning) + self.commit_event() getattr(self, event)() def precommit_event(self): @@ -440,16 +659,6 @@ been all considered if it's this operation which failed """ - def commit_event(self): - """the observed connections pool is commiting""" - - def revertcommit_event(self): - """an error went when commiting this operation or a later one - - should revert commit's changes but take care, they may have not - been all considered if it's this operation which failed - """ - def rollback_event(self): """the observed connections pool has been rollbacked @@ -486,20 +695,55 @@ {set: set.add, list: list.append}[container.__class__](container, value) def set_operation(session, datakey, value, opcls, containercls=set, **opkwargs): - """Search for session.transaction_data[`datakey`] (expected to be a set): + """Function to ease applying a single operation on a set of data, avoiding + to create as many as operation as they are individual modification. You + should try to use this instead of creating on operation for each `value`, + since handling operations becomes coslty on massive data import. + + Arguments are: + + * the `session` object - * if found, simply append `value` + * `datakey`, a specially forged key that will be used as key in + session.transaction_data + + * `value` that is the actual payload of an individual operation + + * `opcls`, the class of the operation. An instance is created on the first + call for the given key, and then subsequent calls will simply add the + payload to the container (hence `opkwargs` is only used on that first + call) - * else, initialize it to containercls([`value`]) and instantiate the given - `opcls` operation class with additional keyword arguments. `containercls` - is a set by default. Give `list` if you want to keep arrival ordering. + * `containercls`, the container class that should be instantiated to hold + payloads. An instance is created on the first call for the given key, and + then subsequent calls will add the data to the existing container. Default + to a set. Give `list` if you want to keep arrival ordering. + + * more optional parameters to give to the operation (here the rtype which do not + vary accross operations). + + The body of the operation must then iterate over the values that have been mapped + in the transaction_data dictionary to the forged key, e.g.: + + .. sourcecode:: python - You should use this instead of creating on operation for each `value`, - since handling operations becomes coslty on massive data import. + for value in self._cw.transaction_data.pop(datakey): + ... + + .. Note:: + **poping** the key from `transaction_data` is not an option, else you may + get unexpected data loss in some case of nested hooks. """ + + + try: + # Search for session.transaction_data[`datakey`] (expected to be a set): + # if found, simply append `value` _container_add(session.transaction_data[datakey], value) except KeyError: + # else, initialize it to containercls([`value`]) and instantiate the given + # `opcls` operation class with additional keyword arguments opcls(session, **opkwargs) session.transaction_data[datakey] = containercls() _container_add(session.transaction_data[datakey], value) @@ -524,8 +768,12 @@ return -(i + 1) -class SingleOperation(Operation): - """special operation which should be called once""" + +class SingleLastOperation(Operation): + """special operation which should be called once and after all other + operations + """ + def register(self, session): """override register to handle cases where this operation has already been added @@ -546,11 +794,6 @@ return -(i+1) return None - -class SingleLastOperation(SingleOperation): - """special operation which should be called once and after all other - operations - """ def insert_index(self): return None @@ -572,7 +815,7 @@ if previous: self.to_send = previous.to_send + self.to_send - def commit_event(self): + def postcommit_event(self): self.session.repo.threaded_task(self.sendmails) def sendmails(self): @@ -612,7 +855,7 @@ type/source cache eids of entities deleted in that transaction. """ - def commit_event(self): + def postcommit_event(self): """the observed connections pool has been rollbacked, remove inserted eid from repository type/source cache """ diff -r dbb7ad04b963 -r 82d4011f54c1 server/pool.py --- a/server/pool.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/pool.py Mon Sep 13 16:47:03 2010 +0200 @@ -144,11 +144,9 @@ self._cursors.pop(source.uri, None) -from cubicweb.server.hook import (Operation, LateOperation, SingleOperation, - SingleLastOperation) +from cubicweb.server.hook import Operation, LateOperation, SingleLastOperation from logilab.common.deprecation import class_moved, class_renamed Operation = class_moved(Operation) PreCommitOperation = class_renamed('PreCommitOperation', Operation) LateOperation = class_moved(LateOperation) -SingleOperation = class_moved(SingleOperation) SingleLastOperation = class_moved(SingleLastOperation) diff -r dbb7ad04b963 -r 82d4011f54c1 server/querier.py --- a/server/querier.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/querier.py Mon Sep 13 16:47:03 2010 +0200 @@ -38,7 +38,7 @@ from cubicweb.server.utils import cleanup_solutions from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata -from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction +from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction, EditedEntity from cubicweb.server.session import security_enabled def empty_rset(rql, args, rqlst=None): @@ -450,7 +450,7 @@ # save originaly selected variable, we may modify this # dictionary for substitution (query parameters) self.selected = rqlst.selection - # list of new or updated entities definition (utils.Entity) + # list of rows of entities definition (ssplanner.EditedEntity) self.e_defs = [[]] # list of new relation definition (3-uple (from_eid, r_type, to_eid) self.r_defs = set() @@ -461,7 +461,6 @@ def add_entity_def(self, edef): """add an entity definition to build""" - edef.querier_pending_relations = {} self.e_defs[-1].append(edef) def add_relation_def(self, rdef): @@ -493,8 +492,9 @@ self.e_defs[i][colidx] = edefs[0] samplerow = self.e_defs[i] for edef_ in edefs[1:]: - row = samplerow[:] - row[colidx] = edef_ + row = [ed.clone() for i, ed in enumerate(samplerow) + if i != colidx] + row.insert(colidx, edef_) self.e_defs.append(row) # now, see if this entity def is referenced as subject in some relation # definition @@ -560,15 +560,16 @@ if isinstance(subj, basestring): subj = typed_eid(subj) elif not isinstance(subj, (int, long)): - subj = subj.eid + subj = subj.entity.eid if isinstance(obj, basestring): obj = typed_eid(obj) elif not isinstance(obj, (int, long)): - obj = obj.eid + obj = obj.entity.eid if repo.schema.rschema(rtype).inlined: entity = session.entity_from_eid(subj) - entity[rtype] = obj - repo.glob_update_entity(session, entity, set((rtype,))) + edited = EditedEntity(entity) + edited.edited_attribute(rtype, obj) + repo.glob_update_entity(session, edited) else: repo.glob_add_relation(session, subj, rtype, obj) diff -r dbb7ad04b963 -r 82d4011f54c1 server/repository.py --- a/server/repository.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/repository.py Mon Sep 13 16:47:03 2010 +0200 @@ -55,7 +55,7 @@ from cubicweb.server import utils, hook, pool, querier, sources from cubicweb.server.session import Session, InternalSession, InternalManager, \ security_enabled -_ = unicode +from cubicweb.server.ssplanner import EditedEntity def del_existing_rel_if_needed(session, eidfrom, rtype, eidto): """delete existing relation when adding a new one if card is 1 or ? @@ -270,7 +270,10 @@ # call instance level initialisation hooks self.hm.call_hooks('server_startup', repo=self) # register a task to cleanup expired session - self.looping_task(self.config['session-time']/3., self.clean_sessions) + self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24 + assert self.cleanup_session_time > 0 + cleanup_session_interval = min(60*60, self.cleanup_session_time / 3) + self.looping_task(cleanup_session_interval, self.clean_sessions) assert isinstance(self._looping_tasks, list), 'already started' for i, (interval, func, args) in enumerate(self._looping_tasks): self._looping_tasks[i] = task = utils.LoopTask(interval, func, args) @@ -533,8 +536,7 @@ password = password.encode('UTF8') kwargs['login'] = login kwargs['upassword'] = password - user.update(kwargs) - self.glob_add_entity(session, user) + self.glob_add_entity(session, EditedEntity(user, **kwargs)) session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"', {'x': user.eid}) if email or '@' in login: @@ -625,24 +627,32 @@ session.reset_pool() def check_session(self, sessionid): - """raise `BadConnectionId` if the connection is no more valid""" - self._get_session(sessionid, setpool=False) + """raise `BadConnectionId` if the connection is no more valid, else + return its latest activity timestamp. + """ + return self._get_session(sessionid, setpool=False).timestamp + + def get_shared_data(self, sessionid, key, default=None, pop=False, txdata=False): + """return value associated to key in the session's data dictionary or + session's transaction's data if `txdata` is true. - def get_shared_data(self, sessionid, key, default=None, pop=False): - """return the session's data dictionary""" + If pop is True, value will be removed from the dictionnary. + + If key isn't defined in the dictionnary, value specified by the + `default` argument will be returned. + """ session = self._get_session(sessionid, setpool=False) - return session.get_shared_data(key, default, pop) + return session.get_shared_data(key, default, pop, txdata) - def set_shared_data(self, sessionid, key, value, querydata=False): + def set_shared_data(self, sessionid, key, value, txdata=False): """set value associated to `key` in shared data - if `querydata` is true, the value will be added to the repository - session's query data which are cleared on commit/rollback of the current - transaction, and won't be available through the connexion, only on the - repository side. + if `txdata` is true, the value will be added to the repository session's + transaction's data which are cleared on commit/rollback of the current + transaction. """ session = self._get_session(sessionid, setpool=False) - session.set_shared_data(key, value, querydata) + session.set_shared_data(key, value, txdata) def commit(self, sessionid, txid=None): """commit transaction for the session with the given id""" @@ -774,7 +784,7 @@ """close sessions not used since an amount of time specified in the configuration """ - mintime = time() - self.config['session-time'] + mintime = time() - self.cleanup_session_time self.debug('cleaning session unused since %s', strftime('%T', localtime(mintime))) nbclosed = 0 @@ -929,7 +939,6 @@ self._extid_cache[cachekey] = eid self._type_source_cache[eid] = (etype, source.uri, extid) entity = source.before_entity_insertion(session, extid, etype, eid) - entity.edited_attributes = set(entity.cw_attr_cache) if source.should_call_hooks: self.hm.call_hooks('before_add_entity', session, entity=entity) # XXX call add_info with complete=False ? @@ -1032,15 +1041,16 @@ self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid) return extid - def glob_add_entity(self, session, entity): + def glob_add_entity(self, session, edited): """add an entity to the repository the entity eid should originaly be None and a unique eid is assigned to the entity instance """ - # init edited_attributes before calling before_add_entity hooks + entity = edited.entity entity._cw_is_saved = False # entity has an eid but is not yet saved - entity.edited_attributes = set(entity.cw_attr_cache) # XXX cw_edited_attributes + # init edited_attributes before calling before_add_entity hooks + entity.cw_edited = edited eschema = entity.e_schema source = self.locate_etype_source(entity.__regid__) # allocate an eid to the entity before calling hooks @@ -1052,33 +1062,30 @@ relations = [] if source.should_call_hooks: self.hm.call_hooks('before_add_entity', session, entity=entity) - # XXX use entity.keys here since edited_attributes is not updated for - # inline relations XXX not true, right? (see edited_attributes - # affectation above) - for attr in entity.cw_attr_cache.iterkeys(): + for attr in edited.iterkeys(): rschema = eschema.subjrels[attr] if not rschema.final: # inlined relation - relations.append((attr, entity[attr])) - entity._cw_set_defaults() + relations.append((attr, edited[attr])) + edited.set_defaults() if session.is_hook_category_activated('integrity'): - entity._cw_check(creation=True) + edited.check(creation=True) try: source.add_entity(session, entity) except UniqueTogetherError, exc: etype, rtypes = exc.args problems = {} for col in rtypes: - problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes)) + problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes)) raise ValidationError(entity.eid, problems) self.add_info(session, entity, source, extid, complete=False) - entity._cw_is_saved = True # entity has an eid and is saved + edited.saved = True # prefill entity relation caches for rschema in eschema.subject_relations(): rtype = str(rschema) if rtype in schema.VIRTUAL_RTYPES: continue if rschema.final: - entity.setdefault(rtype, None) + entity.cw_attr_cache.setdefault(rtype, None) else: entity.cw_set_relation_cache(rtype, 'subject', session.empty_rset()) @@ -1102,23 +1109,24 @@ eidfrom=entity.eid, rtype=attr, eidto=value) return entity.eid - def glob_update_entity(self, session, entity, edited_attributes): + def glob_update_entity(self, session, edited): """replace an entity in the repository the type and the eid of an entity must not be changed """ + entity = edited.entity if server.DEBUG & server.DBG_REPO: print 'UPDATE entity', entity.__regid__, entity.eid, \ - entity.cw_attr_cache, edited_attributes + entity.cw_attr_cache, edited hm = self.hm eschema = entity.e_schema session.set_entity_cache(entity) - orig_edited_attributes = getattr(entity, 'edited_attributes', None) - entity.edited_attributes = edited_attributes + orig_edited = getattr(entity, 'cw_edited', None) + entity.cw_edited = edited try: only_inline_rels, need_fti_update = True, False relations = [] source = self.source_from_eid(entity.eid, session) - for attr in list(edited_attributes): + for attr in list(edited): if attr == 'eid': continue rschema = eschema.subjrels[attr] @@ -1131,13 +1139,13 @@ 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]: + if previous_value == entity.cw_attr_cache[attr]: previous_value = None elif source.should_call_hooks: hm.call_hooks('before_delete_relation', session, eidfrom=entity.eid, rtype=attr, eidto=previous_value) - relations.append((attr, entity[attr], previous_value)) + relations.append((attr, edited[attr], previous_value)) if source.should_call_hooks: # call hooks for inlined relations for attr, value, _t in relations: @@ -1146,16 +1154,16 @@ if not only_inline_rels: hm.call_hooks('before_update_entity', session, entity=entity) if session.is_hook_category_activated('integrity'): - entity._cw_check() + edited.check() try: source.update_entity(session, entity) + edited.saved = True except UniqueTogetherError, exc: etype, rtypes = exc.args problems = {} for col in rtypes: - problems[col] = _('violates unique_together constraints (%s)') % (','.join(rtypes)) + problems[col] = session._('violates unique_together constraints (%s)') % (','.join(rtypes)) raise ValidationError(entity.eid, problems) - self.system_source.update_info(session, entity, need_fti_update) if source.should_call_hooks: if not only_inline_rels: @@ -1177,8 +1185,8 @@ hm.call_hooks('after_add_relation', session, eidfrom=entity.eid, rtype=attr, eidto=value) finally: - if orig_edited_attributes is not None: - entity.edited_attributes = orig_edited_attributes + if orig_edited is not None: + entity.cw_edited = orig_edited def glob_delete_entity(self, session, eid): """delete an entity and all related entities from the repository""" diff -r dbb7ad04b963 -r 82d4011f54c1 server/serverconfig.py --- a/server/serverconfig.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/serverconfig.py Mon Sep 13 16:47:03 2010 +0200 @@ -120,10 +120,16 @@ the repository rather than the user running the command', 'group': 'main', 'level': (CubicWebConfiguration.mode == 'installed') and 0 or 1, }), - ('session-time', + ('cleanup-session-time', {'type' : 'time', - 'default': '30min', - 'help': 'session expiration time, default to 30 minutes', + 'default': '24h', + 'help': 'duration of inactivity after which a session ' + 'will be closed, to limit memory consumption (avoid sessions that ' + 'never expire and cause memory leak when http-session-time is 0, or ' + 'because of bad client that never closes their connection). ' + 'So notice that even if http-session-time is 0 and the user don\'t ' + 'close his browser, he will have to reauthenticate after this time ' + 'of inactivity. Default to 24h.', 'group': 'main', 'level': 3, }), ('connections-pool-size', diff -r dbb7ad04b963 -r 82d4011f54c1 server/session.py --- a/server/session.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/session.py Mon Sep 13 16:47:03 2010 +0200 @@ -64,6 +64,14 @@ If mode is session.`HOOKS_ALLOW_ALL`, given hooks categories will be disabled. + + .. sourcecode:: python + + with hooks_control(self.session, self.session.HOOKS_ALLOW_ALL, 'integrity'): + # ... do stuff with all but 'integrity' hooks activated + + with hooks_control(self.session, self.session.HOOKS_DENY_ALL, 'integrity'): + # ... do stuff with none but 'integrity' hooks activated """ def __init__(self, session, mode, *categories): self.session = session @@ -618,16 +626,20 @@ # shared data handling ################################################### - def get_shared_data(self, key, default=None, pop=False): + def get_shared_data(self, key, default=None, pop=False, txdata=False): """return value associated to `key` in session data""" - if pop: - return self.data.pop(key, default) + if txdata: + data = self.transaction_data else: - return self.data.get(key, default) + data = self.data + if pop: + return data.pop(key, default) + else: + return data.get(key, default) - def set_shared_data(self, key, value, querydata=False): + def set_shared_data(self, key, value, txdata=False): """set value associated to `key` in session data""" - if querydata: + if txdata: self.transaction_data[key] = value else: self.data[key] = value @@ -735,51 +747,50 @@ try: # by default, operations are executed with security turned off with security_enabled(self, False, False): - for trstate in ('precommit', 'commit'): - processed = [] - self.commit_state = trstate - try: - while self.pending_operations: - operation = self.pending_operations.pop(0) - operation.processed = trstate - processed.append(operation) - operation.handle_event('%s_event' % trstate) - self.pending_operations[:] = processed - self.debug('%s session %s done', trstate, self.id) - except: - # if error on [pre]commit: - # - # * set .failed = True on the operation causing the failure - # * call revert_event on processed operations - # * call rollback_event on *all* operations - # - # that seems more natural than not calling rollback_event - # for processed operations, and allow generic rollback - # instead of having to implements rollback, revertprecommit - # and revertcommit, that will be enough in mont case. - operation.failed = True - for operation in reversed(processed): - try: - operation.handle_event('revert%s_event' % trstate) - except: - self.critical('error while reverting %sing', trstate, - exc_info=True) - # XXX use slice notation since self.pending_operations is a - # read-only property. - self.pending_operations[:] = processed + self.pending_operations - self.rollback(reset_pool) - raise + processed = [] + self.commit_state = 'precommit' + try: + while self.pending_operations: + operation = self.pending_operations.pop(0) + operation.processed = 'precommit' + processed.append(operation) + operation.handle_event('precommit_event') + self.pending_operations[:] = processed + self.debug('precommit session %s done', self.id) + except: + # if error on [pre]commit: + # + # * set .failed = True on the operation causing the failure + # * call revert_event on processed operations + # * call rollback_event on *all* operations + # + # that seems more natural than not calling rollback_event + # for processed operations, and allow generic rollback + # instead of having to implements rollback, revertprecommit + # and revertcommit, that will be enough in mont case. + operation.failed = True + for operation in reversed(processed): + try: + operation.handle_event('revertprecommit_event') + except: + self.critical('error while reverting precommit', + exc_info=True) + # XXX use slice notation since self.pending_operations is a + # read-only property. + self.pending_operations[:] = processed + self.pending_operations + self.rollback(reset_pool) + raise self.pool.commit() - self.commit_state = trstate = 'postcommit' + self.commit_state = 'postcommit' while self.pending_operations: operation = self.pending_operations.pop(0) - operation.processed = trstate + operation.processed = 'postcommit' try: - operation.handle_event('%s_event' % trstate) + operation.handle_event('postcommit_event') except: - self.critical('error while %sing', trstate, + self.critical('error while postcommit', exc_info=sys.exc_info()) - self.debug('%s session %s done', trstate, self.id) + self.debug('postcommit session %s done', self.id) return self.transaction_uuid(set=False) finally: self._touch() diff -r dbb7ad04b963 -r 82d4011f54c1 server/sources/__init__.py --- a/server/sources/__init__.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/sources/__init__.py Mon Sep 13 16:47:03 2010 +0200 @@ -26,6 +26,7 @@ from cubicweb import set_log_methods, server from cubicweb.schema import VIRTUAL_RTYPES from cubicweb.server.sqlutils import SQL_PREFIX +from cubicweb.server.ssplanner import EditedEntity def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'): @@ -343,6 +344,7 @@ """ entity = self.repo.vreg['etypes'].etype_class(etype)(session) entity.eid = eid + entity.cw_edited = EditedEntity(entity) return entity def after_entity_insertion(self, session, lid, entity): diff -r dbb7ad04b963 -r 82d4011f54c1 server/sources/ldapuser.py --- a/server/sources/ldapuser.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/sources/ldapuser.py Mon Sep 13 16:47:03 2010 +0200 @@ -574,7 +574,7 @@ entity = super(LDAPUserSource, self).before_entity_insertion(session, lid, etype, eid) res = self._search(session, lid, BASE)[0] for attr in entity.e_schema.indexable_attributes(): - entity[attr] = res[self.user_rev_attrs[attr]] + entity.cw_edited[attr] = res[self.user_rev_attrs[attr]] return entity def after_entity_insertion(self, session, dn, entity): diff -r dbb7ad04b963 -r 82d4011f54c1 server/sources/native.py --- a/server/sources/native.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/sources/native.py Mon Sep 13 16:47:03 2010 +0200 @@ -55,6 +55,7 @@ from cubicweb.server.rqlannotation import set_qdata from cubicweb.server.hook import CleanupDeletedEidsCacheOp from cubicweb.server.session import hooks_control, security_enabled +from cubicweb.server.ssplanner import EditedEntity from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results from cubicweb.server.sources.rql2sql import SQLGenerator @@ -547,21 +548,20 @@ etype = entity.__regid__ for attr, storage in self._storages.get(etype, {}).items(): try: - edited = entity.edited_attributes + edited = entity.cw_edited except AttributeError: assert event == 'deleted' getattr(storage, 'entity_deleted')(entity, attr) else: if attr in edited: handler = getattr(storage, 'entity_%s' % event) - real_value = handler(entity, attr) - restore_values[attr] = real_value + restore_values[attr] = handler(entity, attr) try: yield # 2/ execute the source's instructions finally: # 3/ restore original values for attr, value in restore_values.items(): - entity[attr] = value + entity.cw_edited.edited_attribute(attr, value) def add_entity(self, session, entity): """add a new entity to the source""" @@ -1119,6 +1119,7 @@ err("can't restore entity %s of type %s, type no more supported" % (eid, etype)) return errors + entity.cw_edited = edited = EditedEntity(entity) # check for schema changes, entities linked through inlined relation # still exists, rewrap binary values eschema = entity.e_schema @@ -1135,15 +1136,14 @@ assert value is None elif eschema.destination(rtype) in ('Bytes', 'Password'): action.changes[column] = self._binary(value) - entity[rtype] = Binary(value) + edited[rtype] = Binary(value) elif isinstance(value, str): - entity[rtype] = unicode(value, session.encoding, 'replace') + edited[rtype] = unicode(value, session.encoding, 'replace') else: - entity[rtype] = value + edited[rtype] = value entity.eid = eid session.repo.init_entity_caches(session, entity, self) - entity.edited_attributes = set(entity) - entity._cw_check() + edited.check() self.repo.hm.call_hooks('before_add_entity', session, entity=entity) # restore the entity action.changes['cw_eid'] = eid diff -r dbb7ad04b963 -r 82d4011f54c1 server/sources/storages.py --- a/server/sources/storages.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/sources/storages.py Mon Sep 13 16:47:03 2010 +0200 @@ -23,6 +23,8 @@ from cubicweb import Binary, ValidationError from cubicweb.server import hook +from cubicweb.server.ssplanner import EditedEntity + def set_attribute_storage(repo, etype, attr, storage): repo.system_source.set_storage(etype, attr, storage) @@ -30,6 +32,7 @@ def unset_attribute_storage(repo, etype, attr): repo.system_source.unset_storage(etype, attr) + class Storage(object): """abstract storage @@ -114,12 +117,12 @@ def entity_added(self, entity, attr): """an entity using this storage for attr has been added""" if entity._cw.transaction_data.get('fs_importing'): - binary = Binary(file(entity[attr].getvalue(), 'rb').read()) + binary = Binary(file(entity.cw_edited[attr].getvalue(), 'rb').read()) else: - binary = entity.pop(attr) + binary = entity.cw_edited.pop(attr) fpath = self.new_fs_path(entity, attr) # bytes storage used to store file's path - entity[attr] = Binary(fpath) + entity.cw_edited.edited_attribute(attr, Binary(fpath)) file(fpath, 'wb').write(binary.getvalue()) hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp) return binary @@ -132,7 +135,7 @@ # If we are importing from the filesystem, the file already exists. # We do not need to create it but we need to fetch the content of # the file as the actual content of the attribute - fpath = entity[attr].getvalue() + fpath = entity.cw_edited[attr].getvalue() binary = Binary(file(fpath, 'rb').read()) else: # We must store the content of the attributes @@ -144,7 +147,7 @@ # went ok. # # fetch the current attribute value in memory - binary = entity.pop(attr) + binary = entity.cw_edited.pop(attr) # Get filename for it fpath = self.new_fs_path(entity, attr) assert not osp.exists(fpath) @@ -155,7 +158,7 @@ hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp) if oldpath != fpath: # register the new location for the file. - entity[attr] = Binary(fpath) + entity.cw_edited.edited_attribute(attr, Binary(fpath)) # Mark the old file as useless so the file will be removed at # commit. hook.set_operation(entity._cw, 'bfss_deleted', oldpath, @@ -197,7 +200,7 @@ def migrate_entity(self, entity, attribute): """migrate an entity attribute to the storage""" - entity.edited_attributes = set() + entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache) self.entity_added(entity, attribute) session = entity._cw source = session.repo.system_source @@ -205,6 +208,7 @@ sql = source.sqlgen.update('cw_' + entity.__regid__, attrs, ['cw_eid']) source.doexec(session, sql, attrs) + entity.cw_edited = None class AddFileOp(hook.Operation): @@ -216,7 +220,7 @@ self.error('cant remove %s: %s' % (filepath, ex)) class DeleteFileOp(hook.Operation): - def commit_event(self): + def postcommit_event(self): for filepath in self.session.transaction_data.pop('bfss_deleted'): try: unlink(filepath) diff -r dbb7ad04b963 -r 82d4011f54c1 server/sqlutils.py --- a/server/sqlutils.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/sqlutils.py Mon Sep 13 16:47:03 2010 +0200 @@ -260,11 +260,10 @@ """ attrs = {} eschema = entity.e_schema - for attr in entity.edited_attributes: - value = entity[attr] + for attr, value in entity.cw_edited.iteritems(): rschema = eschema.subjrels[attr] if rschema.final: - atype = str(entity.e_schema.destination(attr)) + atype = str(eschema.destination(attr)) if atype == 'Boolean': value = self.dbhelper.boolean_value(value) elif atype == 'Password': diff -r dbb7ad04b963 -r 82d4011f54c1 server/ssplanner.py --- a/server/ssplanner.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/ssplanner.py Mon Sep 13 16:47:03 2010 +0200 @@ -21,6 +21,8 @@ __docformat__ = "restructuredtext en" +from copy import copy + from rql.stmts import Union, Select from rql.nodes import Constant, Relation @@ -55,11 +57,11 @@ if isinstance(rhs, Constant) and not rhs.uid: # add constant values to entity def value = rhs.eval(plan.args) - eschema = edef.e_schema + eschema = edef.entity.e_schema attrtype = eschema.subjrels[rtype].objects(eschema)[0] if attrtype == 'Password' and isinstance(value, unicode): value = value.encode('UTF8') - edef[rtype] = value + edef.edited_attribute(rtype, value) elif to_build.has_key(str(rhs)): # create a relation between two newly created variables plan.add_relation_def((edef, rtype, to_build[rhs.name])) @@ -126,6 +128,132 @@ return select +_MARKER = object() + +class dict_protocol_catcher(object): + def __init__(self, entity): + self.__entity = entity + def __getitem__(self, attr): + return self.__entity.cw_edited[attr] + def __setitem__(self, attr, value): + self.__entity.cw_edited[attr] = value + def __getattr__(self, attr): + return getattr(self.__entity, attr) + + +class EditedEntity(dict): + """encapsulate entities attributes being written by an RQL query""" + def __init__(self, entity, **kwargs): + dict.__init__(self, **kwargs) + self.entity = entity + self.skip_security = set() + self.querier_pending_relations = {} + self.saved = False + + def __hash__(self): + # dict|set keyable + return hash(id(self)) + + def __cmp__(self, other): + # we don't want comparison by value inherited from dict + return cmp(id(self), id(other)) + + def __setitem__(self, attr, value): + assert attr != 'eid' + # don't add attribute into skip_security if already in edited + # attributes, else we may accidentaly skip a desired security check + if attr not in self: + self.skip_security.add(attr) + self.edited_attribute(attr, value) + + def __delitem__(self, attr): + assert not self.saved, 'too late to modify edited attributes' + super(EditedEntity, self).__delitem__(attr) + self.entity.cw_attr_cache.pop(attr, None) + + def pop(self, attr, *args): + # don't update skip_security by design (think to storage api) + assert not self.saved, 'too late to modify edited attributes' + value = super(EditedEntity, self).pop(attr, *args) + self.entity.cw_attr_cache.pop(attr, *args) + return value + + def setdefault(self, attr, default): + assert attr != 'eid' + # don't add attribute into skip_security if already in edited + # attributes, else we may accidentaly skip a desired security check + if attr not in self: + self[attr] = default + return self[attr] + + def update(self, values, skipsec=True): + if skipsec: + setitem = self.__setitem__ + else: + setitem = self.edited_attribute + for attr, value in values.iteritems(): + setitem(attr, value) + + def edited_attribute(self, attr, value): + """attribute being edited by a rql query: should'nt be added to + skip_security + """ + assert not self.saved, 'too late to modify edited attributes' + super(EditedEntity, self).__setitem__(attr, value) + self.entity.cw_attr_cache[attr] = value + + def oldnewvalue(self, attr): + """returns the couple (old attr value, new attr value) + + NOTE: will only work in a before_update_entity hook + """ + assert not self.saved, 'too late to get the old value' + # get new value and remove from local dict to force a db query to + # fetch old value + newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER) + oldvalue = getattr(self.entity, attr) + if newvalue is not _MARKER: + self.entity.cw_attr_cache[attr] = newvalue + else: + newvalue = oldvalue + return oldvalue, newvalue + + def set_defaults(self): + """set default values according to the schema""" + for attr, value in self.entity.e_schema.defaults(): + if not attr in self: + self[str(attr)] = value + + def check(self, creation=False): + """check the entity edition against its schema. Only final relation + are checked here, constraint on actual relations are checked in hooks + """ + entity = self.entity + if creation: + # on creations, we want to check all relations, especially + # required attributes + relations = [rschema for rschema in entity.e_schema.subject_relations() + if rschema.final and rschema.type != 'eid'] + else: + relations = [entity._cw.vreg.schema.rschema(rtype) + for rtype in self] + from yams import ValidationError + try: + entity.e_schema.check(dict_protocol_catcher(entity), + creation=creation, _=entity._cw._, + relations=relations) + except ValidationError, ex: + ex.entity = self.entity + raise + + def clone(self): + thecopy = EditedEntity(copy(self.entity)) + thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache) + thecopy.entity._cw_related_cache = {} + thecopy.update(self, skipsec=False) + return thecopy + + class SSPlanner(object): """SingleSourcePlanner: build execution plan for rql queries @@ -162,7 +290,7 @@ etype_class = session.vreg['etypes'].etype_class for etype, var in rqlst.main_variables: # need to do this since entity class is shared w. web client code ! - to_build[var.name] = etype_class(etype)(session) + to_build[var.name] = EditedEntity(etype_class(etype)(session)) plan.add_entity_def(to_build[var.name]) # add constant values to entity def, mark variables to be selected to_select = _extract_const_attributes(plan, rqlst, to_build) @@ -177,7 +305,7 @@ for edef, rdefs in to_select.items(): # create a select rql st to fetch needed data select = Select() - eschema = edef.e_schema + eschema = edef.entity.e_schema for i, (rtype, term, reverse) in enumerate(rdefs): if getattr(term, 'variable', None) in eidconsts: value = eidconsts[term.variable] @@ -284,10 +412,8 @@ rhsinfo = selectedidx[rhskey][:-1] + (None,) rschema = getrschema(relation.r_type) updatedefs.append( (lhsinfo, rhsinfo, rschema) ) - if rschema.final or rschema.inlined: - attributes.add(relation.r_type) # the update step - step = UpdateStep(plan, updatedefs, attributes) + step = UpdateStep(plan, updatedefs) # when necessary add substep to fetch yet unknown values select = _build_substep_query(select, rqlst) if select is not None: @@ -476,7 +602,7 @@ result = [[]] for row in result: # get a new entity definition for this row - edef = base_edef.cw_copy() + edef = base_edef.clone() # complete this entity def using row values index = 0 for rtype, rorder, value in self.rdefs: @@ -484,7 +610,7 @@ value = row[index] index += 1 if rorder == InsertRelationsStep.FINAL: - edef._cw_rql_set_value(rtype, value) + edef.edited_attribute(rtype, value) elif rorder == InsertRelationsStep.RELATION: self.plan.add_relation_def( (edef, rtype, value) ) edef.querier_pending_relations[(rtype, 'subject')] = value @@ -495,6 +621,7 @@ self.plan.substitute_entity_def(base_edef, edefs) return result + class InsertStep(Step): """step consisting in inserting new entities / relations""" @@ -555,10 +682,9 @@ definitions and from results fetched in previous step """ - def __init__(self, plan, updatedefs, attributes): + def __init__(self, plan, updatedefs): Step.__init__(self, plan) self.updatedefs = updatedefs - self.attributes = attributes def execute(self): """execute this step""" @@ -578,16 +704,17 @@ if rschema.final or rschema.inlined: eid = typed_eid(lhsval) try: - edef = edefs[eid] + edited = edefs[eid] except KeyError: - edefs[eid] = edef = session.entity_from_eid(eid) - edef._cw_rql_set_value(str(rschema), rhsval) + edef = session.entity_from_eid(eid) + edefs[eid] = edited = EditedEntity(edef) + edited.edited_attribute(str(rschema), rhsval) else: repo.glob_add_relation(session, lhsval, str(rschema), rhsval) result[i] = newrow # update entities - for eid, edef in edefs.iteritems(): - repo.glob_update_entity(session, edef, set(self.attributes)) + for eid, edited in edefs.iteritems(): + repo.glob_update_entity(session, edited) return result def _handle_relterm(info, row, newrow): diff -r dbb7ad04b963 -r 82d4011f54c1 server/test/data/schema.py --- a/server/test/data/schema.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/test/data/schema.py Mon Sep 13 16:47:03 2010 +0200 @@ -17,7 +17,8 @@ # with CubicWeb. If not, see . from yams.buildobjs import (EntityType, RelationType, RelationDefinition, - SubjectRelation, RichString, String, Int, Boolean, Datetime) + SubjectRelation, RichString, String, Int, Float, + Boolean, Datetime) from yams.constraints import SizeConstraint from cubicweb.schema import (WorkflowableEntityType, RQLConstraint, ERQLExpression, RRQLExpression) @@ -39,7 +40,7 @@ description=_('more detailed description')) duration = Int() - invoiced = Int() + invoiced = Float() depends_on = SubjectRelation('Affaire') require_permission = SubjectRelation('CWPermission') diff -r dbb7ad04b963 -r 82d4011f54c1 server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/test/unittest_msplanner.py Mon Sep 13 16:47:03 2010 +0200 @@ -1914,7 +1914,7 @@ [('FetchStep', [('Any WP WHERE 999999 multisource_rel WP, WP is Note', [{'WP': 'Note'}])], [self.cards], None, {'WP': u'table0.C0'}, []), ('OneFetchStep', [('Any S,SUM(DUR),SUM(I),(SUM(I) - SUM(DUR)),MIN(DI),MAX(DI) GROUPBY S ORDERBY S WHERE A duration DUR, A invoiced I, A modification_date DI, A in_state S, S name SN, (EXISTS(A concerne WP, WP is Note)) OR (EXISTS(A concerne 999999)), A is Affaire, S is State', - [{'A': 'Affaire', 'DI': 'Datetime', 'DUR': 'Int', 'I': 'Int', 'S': 'State', 'SN': 'String', 'WP': 'Note'}])], + [{'A': 'Affaire', 'DI': 'Datetime', 'DUR': 'Int', 'I': 'Float', 'S': 'State', 'SN': 'String', 'WP': 'Note'}])], None, None, [self.system], {'WP': u'table0.C0'}, [])], {'n': 999999}) diff -r dbb7ad04b963 -r 82d4011f54c1 server/test/unittest_querier.py --- a/server/test/unittest_querier.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/test/unittest_querier.py Mon Sep 13 16:47:03 2010 +0200 @@ -545,12 +545,25 @@ self.assertEquals(rset.rows[0][0], 'ADMIN') self.assertEquals(rset.description, [('String',)]) -## def test_select_simplified(self): -## ueid = self.session.user.eid -## rset = self.execute('Any L WHERE %s login L'%ueid) -## self.assertEquals(rset.rows[0][0], 'admin') -## rset = self.execute('Any L WHERE %(x)s login L', {'x':ueid}) -## self.assertEquals(rset.rows[0][0], 'admin') + def test_select_float_abs(self): + # test positive number + eid = self.execute('INSERT Affaire A: A invoiced %(i)s', {'i': 1.2})[0][0] + rset = self.execute('Any ABS(I) WHERE X eid %(x)s, X invoiced I', {'x': eid}) + self.assertEquals(rset.rows[0][0], 1.2) + # test negative number + eid = self.execute('INSERT Affaire A: A invoiced %(i)s', {'i': -1.2})[0][0] + rset = self.execute('Any ABS(I) WHERE X eid %(x)s, X invoiced I', {'x': eid}) + self.assertEquals(rset.rows[0][0], 1.2) + + def test_select_int_abs(self): + # test positive number + eid = self.execute('INSERT Affaire A: A duration %(d)s', {'d': 12})[0][0] + rset = self.execute('Any ABS(D) WHERE X eid %(x)s, X duration D', {'x': eid}) + self.assertEquals(rset.rows[0][0], 12) + # test negative number + eid = self.execute('INSERT Affaire A: A duration %(d)s', {'d': -12})[0][0] + rset = self.execute('Any ABS(D) WHERE X eid %(x)s, X duration D', {'x': eid}) + self.assertEquals(rset.rows[0][0], 12) def test_select_searchable_text_1(self): rset = self.execute(u"INSERT Personne X: X nom 'bidüle'") diff -r dbb7ad04b963 -r 82d4011f54c1 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Mon Sep 13 16:46:52 2010 +0200 +++ b/server/test/unittest_repository.py Mon Sep 13 16:47:03 2010 +0200 @@ -179,7 +179,7 @@ def test_check_session(self): repo = self.repo cnxid = repo.connect(self.admlogin, password=self.admpassword) - self.assertEquals(repo.check_session(cnxid), None) + self.assertIsInstance(repo.check_session(cnxid), float) repo.close(cnxid) self.assertRaises(BadConnectionId, repo.check_session, cnxid) @@ -448,7 +448,7 @@ 'EmailAddress', address=u'a@b.fr') def test_multiple_edit_set_attributes(self): - """make sure edited_attributes doesn't get cluttered + """make sure cw_edited doesn't get cluttered by previous entities on multiple set """ # local hook @@ -459,9 +459,9 @@ events = ('before_update_entity',) def __call__(self): # invoiced attribute shouldn't be considered "edited" before the hook - self._test.failIf('invoiced' in self.entity.edited_attributes, - 'edited_attributes cluttered by previous update') - self.entity['invoiced'] = 10 + self._test.failIf('invoiced' in self.entity.cw_edited, + 'cw_edited cluttered by previous update') + self.entity.cw_edited['invoiced'] = 10 with self.temporary_appobjects(DummyBeforeHook): req = self.request() req.create_entity('Affaire', ref=u'AFF01') diff -r dbb7ad04b963 -r 82d4011f54c1 sobjects/supervising.py --- a/sobjects/supervising.py Mon Sep 13 16:46:52 2010 +0200 +++ b/sobjects/supervising.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,10 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""some hooks and views to handle supervising of any data changes +"""some hooks and views to handle supervising of any data changes""" - -""" __docformat__ = "restructuredtext en" from cubicweb import UnknownEid @@ -185,6 +183,6 @@ msg = format_mail(uinfo, recipients, content, view.subject(), config=config) self.to_send = [(msg, recipients)] - def commit_event(self): + def postcommit_event(self): self._prepare_email() - SendMailOp.commit_event(self) + SendMailOp.postcommit_event(self) diff -r dbb7ad04b963 -r 82d4011f54c1 tags.py --- a/tags.py Mon Sep 13 16:46:52 2010 +0200 +++ b/tags.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,9 +15,8 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""helper classes to generate simple (X)HTML tags +"""helper classes to generate simple (X)HTML tags""" -""" __docformat__ = "restructuredtext en" from cubicweb.uilib import simple_sgml_tag, sgml_attributes diff -r dbb7ad04b963 -r 82d4011f54c1 test/unittest_entity.py --- a/test/unittest_entity.py Mon Sep 13 16:46:52 2010 +0200 +++ b/test/unittest_entity.py Mon Sep 13 16:47:03 2010 +0200 @@ -16,9 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""unit tests for cubicweb.web.views.entities module - -""" +"""unit tests for cubicweb.web.views.entities module""" from datetime import datetime @@ -322,30 +320,30 @@ content_format=u'text/rest') self.assertEquals(e.printable_value('content'), '

du *ReST*

\n') - e['content'] = 'du html users' - e['content_format'] = 'text/html' + e.cw_attr_cache['content'] = 'du html users' + e.cw_attr_cache['content_format'] = 'text/html' self.assertEquals(e.printable_value('content'), 'du html users') - e['content'] = 'du *texte*' - e['content_format'] = 'text/plain' + e.cw_attr_cache['content'] = 'du *texte*' + e.cw_attr_cache['content_format'] = 'text/plain' self.assertEquals(e.printable_value('content'), '

\ndu *texte*
\n

') - e['title'] = 'zou' - e['content'] = '''\ + e.cw_attr_cache['title'] = 'zou' + e.cw_attr_cache['content'] = '''\ a title ======= du :eid:`1:*ReST*`''' - e['content_format'] = 'text/rest' + e.cw_attr_cache['content_format'] = 'text/rest' self.assertEquals(e.printable_value('content', format='text/plain'), - e['content']) + e.cw_attr_cache['content']) - e['content'] = u'yo (zou éà ;)' - e['content_format'] = 'text/html' + e.cw_attr_cache['content'] = u'yo (zou éà ;)' + e.cw_attr_cache['content_format'] = 'text/html' self.assertEquals(e.printable_value('content', format='text/plain').strip(), u'**yo (zou éà ;)**') if HAS_TAL: - e['content'] = '

titre

' - e['content_format'] = 'text/cubicweb-page-template' + e.cw_attr_cache['content'] = '

titre

' + e.cw_attr_cache['content_format'] = 'text/cubicweb-page-template' self.assertEquals(e.printable_value('content'), '

zou

') @@ -387,30 +385,30 @@ tidy = lambda x: x.replace('\n', '') self.assertEquals(tidy(e.printable_value('content')), '
R&D
') - e['content'] = u'yo !! R&D
pas fermé' + e.cw_attr_cache['content'] = u'yo !! R&D
pas fermé' self.assertEquals(tidy(e.printable_value('content')), u'yo !! R&D
pas fermé
') - e['content'] = u'R&D' + e.cw_attr_cache['content'] = u'R&D' self.assertEquals(tidy(e.printable_value('content')), u'R&D') - e['content'] = u'R&D;' + e.cw_attr_cache['content'] = u'R&D;' self.assertEquals(tidy(e.printable_value('content')), u'R&D;') - e['content'] = u'yo !! R&D
pas fermé' + e.cw_attr_cache['content'] = u'yo !! R&D
pas fermé' self.assertEquals(tidy(e.printable_value('content')), u'yo !! R&D
pas fermé
') - e['content'] = u'été
été' + e.cw_attr_cache['content'] = u'été
été' self.assertEquals(tidy(e.printable_value('content')), u'été
été
') - e['content'] = u'C'est un exemple sérieux' + e.cw_attr_cache['content'] = u'C'est un exemple sérieux' self.assertEquals(tidy(e.printable_value('content')), u"C'est un exemple sérieux") # make sure valid xhtml is left untouched - e['content'] = u'
R&D
' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'
été
' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'été' - self.assertEquals(e.printable_value('content'), e['content']) - e['content'] = u'hop\r\nhop\nhip\rmomo' + e.cw_attr_cache['content'] = u'
R&D
' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'
été
' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'été' + self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content']) + e.cw_attr_cache['content'] = u'hop\r\nhop\nhip\rmomo' self.assertEquals(e.printable_value('content'), u'hop\nhop\nhip\nmomo') def test_printable_value_bad_html_ms(self): @@ -419,7 +417,7 @@ e = req.create_entity('Card', title=u'bad html', content=u'
R&D
', content_format=u'text/html') tidy = lambda x: x.replace('\n', '') - e['content'] = u'
ms orifice produces weird html
' + e.cw_attr_cache['content'] = u'
ms orifice produces weird html
' self.assertEquals(tidy(e.printable_value('content')), u'
ms orifice produces weird html
') import tidy as tidymod # apt-get install python-tidy @@ -435,12 +433,12 @@ def test_fulltextindex(self): e = self.vreg['etypes'].etype_class('File')(self.request()) - e['description'] = 'du html' - e['description_format'] = 'text/html' - e['data'] = Binary('some data') - e['data_name'] = 'an html file' - e['data_format'] = 'text/html' - e['data_encoding'] = 'ascii' + e.cw_attr_cache['description'] = 'du html' + e.cw_attr_cache['description_format'] = 'text/html' + e.cw_attr_cache['data'] = Binary('some data') + e.cw_attr_cache['data_name'] = 'an html file' + e.cw_attr_cache['data_format'] = 'text/html' + e.cw_attr_cache['data_encoding'] = 'ascii' e._cw.transaction_data = {} # XXX req should be a session self.assertEquals(e.cw_adapt_to('IFTIndexable').get_words(), {'C': [u'du', u'html', 'an', 'html', 'file', u'some', u'data']}) @@ -461,7 +459,7 @@ 'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0] trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0) trinfo.complete() - self.failUnless(isinstance(trinfo['creation_date'], datetime)) + self.failUnless(isinstance(trinfo.cw_attr_cache['creation_date'], datetime)) self.failUnless(trinfo.cw_relation_cached('from_state', 'subject')) self.failUnless(trinfo.cw_relation_cached('to_state', 'subject')) self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject')) diff -r dbb7ad04b963 -r 82d4011f54c1 test/unittest_rset.py --- a/test/unittest_rset.py Mon Sep 13 16:46:52 2010 +0200 +++ b/test/unittest_rset.py Mon Sep 13 16:47:03 2010 +0200 @@ -157,13 +157,13 @@ rs.req = self.request() rs.vreg = self.vreg - rs2 = rs.sorted_rset(lambda e:e['login']) + rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login']) self.assertEquals(len(rs2), 3) self.assertEquals([login for _, login in rs2], ['adim', 'nico', 'syt']) # make sure rs is unchanged self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico']) - rs2 = rs.sorted_rset(lambda e:e['login'], reverse=True) + rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'], reverse=True) self.assertEquals(len(rs2), 3) self.assertEquals([login for _, login in rs2], ['syt', 'nico', 'adim']) # make sure rs is unchanged @@ -187,7 +187,7 @@ rs.req = self.request() rs.vreg = self.vreg - rsets = rs.split_rset(lambda e:e['login']) + rsets = rs.split_rset(lambda e:e.cw_attr_cache['login']) self.assertEquals(len(rsets), 3) self.assertEquals([login for _, login,_ in rsets[0]], ['adim', 'adim']) self.assertEquals([login for _, login,_ in rsets[1]], ['syt']) @@ -195,7 +195,7 @@ # make sure rs is unchanged self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico']) - rsets = rs.split_rset(lambda e:e['login'], return_dict=True) + rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'], return_dict=True) self.assertEquals(len(rsets), 3) self.assertEquals([login for _, login,_ in rsets['nico']], ['nico', 'nico']) self.assertEquals([login for _, login,_ in rsets['adim']], ['adim', 'adim']) @@ -230,12 +230,12 @@ self.request().create_entity('CWUser', login=u'adim', upassword='adim', surname=u'di mascio', firstname=u'adrien') e = self.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0) - self.assertEquals(e['surname'], 'di mascio') - self.assertRaises(KeyError, e.__getitem__, 'firstname') - self.assertRaises(KeyError, e.__getitem__, 'creation_date') + self.assertEquals(e.cw_attr_cache['surname'], 'di mascio') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'creation_date') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e.complete() - self.assertEquals(e['firstname'], 'adrien') + self.assertEquals(e.cw_attr_cache['firstname'], 'adrien') self.assertEquals(pprelcachedict(e._cw_related_cache), []) def test_get_entity_advanced(self): @@ -246,20 +246,20 @@ e = rset.get_entity(0, 0) self.assertEquals(e.cw_row, 0) self.assertEquals(e.cw_col, 0) - self.assertEquals(e['title'], 'zou') - self.assertRaises(KeyError, e.__getitem__, 'path') + self.assertEquals(e.cw_attr_cache['title'], 'zou') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'path') self.assertEquals(e.view('text'), 'zou') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e = rset.get_entity(0, 1) self.assertEquals(e.cw_row, 0) self.assertEquals(e.cw_col, 1) - self.assertEquals(e['login'], 'anon') - self.assertRaises(KeyError, e.__getitem__, 'firstname') + self.assertEquals(e.cw_attr_cache['login'], 'anon') + self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname') self.assertEquals(pprelcachedict(e._cw_related_cache), []) e.complete() - self.assertEquals(e['firstname'], None) + self.assertEquals(e.cw_attr_cache['firstname'], None) self.assertEquals(e.view('text'), 'anon') self.assertEquals(pprelcachedict(e._cw_related_cache), []) @@ -282,17 +282,17 @@ rset = self.execute('Any X,U,S,XT,UL,SN WHERE X created_by U, U in_state S, ' 'X title XT, S name SN, U login UL, X eid %s' % e.eid) e = rset.get_entity(0, 0) - self.assertEquals(e['title'], 'zou') + self.assertEquals(e.cw_attr_cache['title'], 'zou') self.assertEquals(pprelcachedict(e._cw_related_cache), [('created_by_subject', [5])]) # first level of recursion u = e.created_by[0] - self.assertEquals(u['login'], 'admin') - self.assertRaises(KeyError, u.__getitem__, 'firstname') + self.assertEquals(u.cw_attr_cache['login'], 'admin') + self.assertRaises(KeyError, u.cw_attr_cache.__getitem__, 'firstname') # second level of recursion s = u.in_state[0] - self.assertEquals(s['name'], 'activated') - self.assertRaises(KeyError, s.__getitem__, 'description') + self.assertEquals(s.cw_attr_cache['name'], 'activated') + self.assertRaises(KeyError, s.cw_attr_cache.__getitem__, 'description') def test_get_entity_cache_with_left_outer_join(self): @@ -322,7 +322,7 @@ etype, n = expected[entity.cw_row] self.assertEquals(entity.__regid__, etype) attr = etype == 'Bookmark' and 'title' or 'name' - self.assertEquals(entity[attr], n) + self.assertEquals(entity.cw_attr_cache[attr], n) def test_related_entity_optional(self): e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path') diff -r dbb7ad04b963 -r 82d4011f54c1 test/unittest_uilib.py --- a/test/unittest_uilib.py Mon Sep 13 16:46:52 2010 +0200 +++ b/test/unittest_uilib.py Mon Sep 13 16:47:03 2010 +0200 @@ -16,14 +16,11 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""unittests for cubicweb.uilib - -""" +"""unittests for cubicweb.uilib""" __docformat__ = "restructuredtext en" from logilab.common.testlib import TestCase, unittest_main -from logilab.common.tree import Node from cubicweb import uilib diff -r dbb7ad04b963 -r 82d4011f54c1 test/unittest_utils.py --- a/test/unittest_utils.py Mon Sep 13 16:46:52 2010 +0200 +++ b/test/unittest_utils.py Mon Sep 13 16:47:03 2010 +0200 @@ -140,14 +140,14 @@ def test_encoding_bare_entity(self): e = Entity(None) - e['pouet'] = 'hop' + e.cw_attr_cache['pouet'] = 'hop' e.eid = 2 self.assertEquals(json.loads(self.encode(e)), {'pouet': 'hop', 'eid': 2}) def test_encoding_entity_in_list(self): e = Entity(None) - e['pouet'] = 'hop' + e.cw_attr_cache['pouet'] = 'hop' e.eid = 2 self.assertEquals(json.loads(self.encode([e])), [{'pouet': 'hop', 'eid': 2}]) diff -r dbb7ad04b963 -r 82d4011f54c1 utils.py --- a/utils.py Mon Sep 13 16:46:52 2010 +0200 +++ b/utils.py Mon Sep 13 16:47:03 2010 +0200 @@ -24,6 +24,7 @@ import decimal import datetime import random +from inspect import getargspec from itertools import repeat from uuid import uuid4 from warnings import warn @@ -64,6 +65,40 @@ '__doc__': cls.__doc__, '__module__': cls.__module__}) +def support_args(callable, *argnames): + """return true if the callable support given argument names""" + argspec = getargspec(callable) + if argspec[2]: + return True + for argname in argnames: + if argname not in argspec[0]: + return False + return True + + +class wrap_on_write(object): + def __init__(self, w, tag, closetag=None): + self.written = False + self.tag = unicode(tag) + self.closetag = closetag + self.w = w + + def __enter__(self): + return self + + def __call__(self, data): + if self.written is False: + self.w(self.tag) + self.written = True + self.w(data) + + def __exit__(self, exctype, value, traceback): + if self.written is True: + if self.closetag: + self.w(unicode(self.closetag)) + else: + self.w(self.tag.replace('<', '\n\n') - + @deprecated('[3.10] use vreg["etypes"].etype_class(etype).cw_create_url(req)') def create_url(self, etype, **kwargs): """ return the url of the entity creation form for a given entity type""" - return self._cw.build_url('add/%s' % etype, **kwargs) + return self._cw.vreg["etypes"].etype_class(etype).cw_create_url( + self._cw, **kwargs) def field(self, label, value, row=True, show_label=True, w=None, tr=True, table=False): @@ -514,8 +504,13 @@ build_js = build_update_js_call # expect updatable component by default + @property + def domid(self): + return domid(self.__regid__) + + @deprecated('[3.10] use .domid property') def div_id(self): - return '' + return self.domid class Component(ReloadableMixIn, View): @@ -523,14 +518,20 @@ __registry__ = 'components' __select__ = yes() - # XXX huummm, much probably useless + # XXX huummm, much probably useless (should be...) htmlclass = 'mainRelated' + @property + def cssclass(self): + return '%s %s' % (self.htmlclass, domid(self.__regid__)) + + # XXX should rely on ReloadableMixIn.domid + @property + def domid(self): + return '%sComponent' % domid(self.__regid__) + + @deprecated('[3.10] use .cssclass property') def div_class(self): - return '%s %s' % (self.htmlclass, self.__regid__) - - # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer - def div_id(self): - return '%sComponent' % self.__regid__ + return self.cssclass class Adapter(AppObject): diff -r dbb7ad04b963 -r 82d4011f54c1 vregistry.py --- a/vregistry.py Mon Sep 13 16:46:52 2010 +0200 +++ b/vregistry.py Mon Sep 13 16:47:03 2010 +0200 @@ -173,7 +173,7 @@ assert len(objects) == 1, objects return objects[0](*args, **kwargs) - def select(self, oid, *args, **kwargs): + def select(self, __oid, *args, **kwargs): """return the most specific object among those with the given oid according to the given context. @@ -181,14 +181,14 @@ raise :exc:`NoSelectableObject` if not object apply """ - return self._select_best(self[oid], *args, **kwargs) + return self._select_best(self[__oid], *args, **kwargs) - def select_or_none(self, oid, *args, **kwargs): + def select_or_none(self, __oid, *args, **kwargs): """return the most specific object among those with the given oid according to the given context, or None if no object applies. """ try: - return self.select(oid, *args, **kwargs) + return self.select(__oid, *args, **kwargs) except (NoSelectableObject, ObjectNotFound): return None select_object = deprecated('[3.6] use select_or_none instead of select_object' @@ -467,7 +467,7 @@ self.load_module(module) def load_module(self, module): - self.info('loading %s', module) + self.info('loading %s from %s', module.__name__, module.__file__) if hasattr(module, 'registration_callback'): module.registration_callback(self) else: diff -r dbb7ad04b963 -r 82d4011f54c1 web/action.py --- a/web/action.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/action.py Mon Sep 13 16:47:03 2010 +0200 @@ -45,35 +45,31 @@ for action in self.actual_actions(): menu.append(box.box_action(action)) + def html_class(self): + if self._cw.selected(self.url()): + return 'selected' + + def build_action(self, title, url, **kwargs): + return UnregisteredAction(self._cw, title, url, **kwargs) + def url(self): """return the url associated with this action""" raise NotImplementedError - def html_class(self): - if self._cw.selected(self.url()): - return 'selected' - if self.category: - return 'box' + self.category.capitalize() - - def build_action(self, title, path, **kwargs): - return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs) - class UnregisteredAction(Action): - """non registered action used to build boxes. Unless you set them - explicitly, .vreg and .schema attributes at least are None. - """ + """non registered action, used to build boxes""" category = None id = None - def __init__(self, req, rset, title, path, **kwargs): - Action.__init__(self, req, rset=rset) + def __init__(self, req, title, url, **kwargs): + Action.__init__(self, req) self.title = req._(title) - self._path = path + self._url = url self.__dict__.update(kwargs) def url(self): - return self._path + return self._url class LinkToEntityAction(Action): diff -r dbb7ad04b963 -r 82d4011f54c1 web/application.py --- a/web/application.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/application.py Mon Sep 13 16:47:03 2010 +0200 @@ -31,7 +31,7 @@ from cubicweb import set_log_methods, cwvreg from cubicweb import ( ValidationError, Unauthorized, AuthenticationError, NoSelectableObject, - RepositoryError, CW_EVENT_MANAGER) + RepositoryError, BadConnectionId, CW_EVENT_MANAGER) from cubicweb.dbapi import DBAPISession from cubicweb.web import LOGGER, component from cubicweb.web import ( @@ -48,48 +48,43 @@ def __init__(self, vreg): self.session_time = vreg.config['http-session-time'] or None - if self.session_time is not None: - assert self.session_time > 0 - self.cleanup_session_time = self.session_time - else: - self.cleanup_session_time = vreg.config['cleanup-session-time'] or 1440 * 60 - assert self.cleanup_session_time > 0 - self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60 - assert self.cleanup_anon_session_time > 0 self.authmanager = vreg['components'].select('authmanager', vreg=vreg) + interval = (self.session_time or 0) / 2. if vreg.config.anonymous_user() is not None: - self.clean_sessions_interval = max( - 5 * 60, min(self.cleanup_session_time / 2., - self.cleanup_anon_session_time / 2.)) - else: - self.clean_sessions_interval = max( - 5 * 60, - self.cleanup_session_time / 2.) + self.cleanup_anon_session_time = vreg.config['cleanup-anonymous-session-time'] or 5 * 60 + assert self.cleanup_anon_session_time > 0 + if self.session_time is not None: + self.cleanup_anon_session_time = min(self.session_time, + self.cleanup_anon_session_time) + interval = self.cleanup_anon_session_time / 2. + # we don't want to check session more than once every 5 minutes + self.clean_sessions_interval = max(5 * 60, interval) def clean_sessions(self): """cleanup sessions which has not been unused since a given amount of time. Return the number of sessions which have been closed. """ self.debug('cleaning http sessions') + session_time = self.session_time closed, total = 0, 0 for session in self.current_sessions(): - no_use_time = (time() - session.last_usage_time) total += 1 - if session.anonymous_session: - if no_use_time >= self.cleanup_anon_session_time: + try: + last_usage_time = session.cnx.check() + except BadConnectionId: + self.close_session(session) + closed += 1 + else: + no_use_time = (time() - last_usage_time) + if session.anonymous_session: + if no_use_time >= self.cleanup_anon_session_time: + self.close_session(session) + closed += 1 + elif session_time is not None and no_use_time >= session_time: self.close_session(session) closed += 1 - elif no_use_time >= self.cleanup_session_time: - self.close_session(session) - closed += 1 return closed, total - closed - def has_expired(self, session): - """return True if the web session associated to the session is expired - """ - return not (self.session_time is None or - time() < session.last_usage_time + self.session_time) - def current_sessions(self): """return currently open sessions""" raise NotImplementedError() @@ -213,8 +208,6 @@ except AuthenticationError: req.remove_cookie(cookie, self.SESSION_VAR) raise - # remember last usage time for web session tracking - session.last_usage_time = time() def get_session(self, req, sessionid): return self.session_manager.get_session(req, sessionid) @@ -224,8 +217,6 @@ cookie = req.get_cookie() cookie[self.SESSION_VAR] = session.sessionid req.set_cookie(cookie, self.SESSION_VAR, maxage=None) - # remember last usage time for web session tracking - session.last_usage_time = time() if not session.anonymous_session: self._postlogin(req) return session diff -r dbb7ad04b963 -r 82d4011f54c1 web/box.py --- a/web/box.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/box.py Mon Sep 13 16:47:03 2010 +0200 @@ -21,27 +21,44 @@ _ = unicode from logilab.mtconverter import xml_escape +from logilab.common.deprecation import class_deprecated, class_renamed from cubicweb import Unauthorized, role as get_role, target as get_target from cubicweb.schema import display_name -from cubicweb.selectors import (no_cnx, one_line_rset, primary_view, - match_context_prop, partial_relation_possible, - partial_has_related_entities) -from cubicweb.view import View, ReloadableMixIn -from cubicweb.uilib import domid, js +from cubicweb.selectors import no_cnx, one_line_rset +from cubicweb.view import View from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget, RawBoxItem, BoxSeparator) from cubicweb.web.action import UnregisteredAction +def sort_by_category(actions, categories_in_order=None): + """return a list of (category, actions_sorted_by_title)""" + result = [] + actions_by_cat = {} + for action in actions: + actions_by_cat.setdefault(action.category, []).append( + (action.title, action) ) + for key, values in actions_by_cat.items(): + actions_by_cat[key] = [act for title, act in sorted(values)] + if categories_in_order: + for cat in categories_in_order: + if cat in actions_by_cat: + result.append( (cat, actions_by_cat[cat]) ) + for item in sorted(actions_by_cat.items()): + result.append(item) + return result + + +# old box system, deprecated ################################################### + class BoxTemplate(View): """base template for boxes, usually a (contextual) list of possible - actions. Various classes attributes may be used to control the box rendering. - You may override on of the formatting callbacks is this is not necessary + You may override one of the formatting callbacks if this is not necessary for your custom box. Classes inheriting from this class usually only have to override call @@ -49,8 +66,11 @@ box.render(self.w) """ - __registry__ = 'boxes' - __select__ = ~no_cnx() & match_context_prop() + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)' + + __registry__ = 'ctxcomponents' + __select__ = ~no_cnx() categories_in_order = () cw_property_defs = { @@ -64,34 +84,21 @@ help=_('context where this box should be displayed')), } context = 'left' - htmlitemclass = 'boxItem' def sort_actions(self, actions): """return a list of (category, actions_sorted_by_title)""" - result = [] - actions_by_cat = {} - for action in actions: - actions_by_cat.setdefault(action.category, []).append( - (action.title, action) ) - for key, values in actions_by_cat.items(): - actions_by_cat[key] = [act for title, act in sorted(values)] - for cat in self.categories_in_order: - if cat in actions_by_cat: - result.append( (cat, actions_by_cat[cat]) ) - for item in sorted(actions_by_cat.items()): - result.append(item) - return result + return sort_by_category(actions, self.categories_in_order) - def mk_action(self, title, path, escape=True, **kwargs): + def mk_action(self, title, url, escape=True, **kwargs): """factory function to create dummy actions compatible with the .format_actions method """ if escape: title = xml_escape(title) - return self.box_action(self._action(title, path, **kwargs)) + return self.box_action(self._action(title, url, **kwargs)) - def _action(self, title, path, **kwargs): - return UnregisteredAction(self._cw, self.cw_rset, title, path, **kwargs) + def _action(self, title, url, **kwargs): + return UnregisteredAction(self._cw, title, url, **kwargs) # formating callbacks @@ -101,18 +108,14 @@ return u'' def box_action(self, action): - cls = getattr(action, 'html_class', lambda: None)() or self.htmlitemclass + klass = getattr(action, 'html_class', lambda: None)() return BoxLink(action.url(), self._cw._(action.title), - cls, self.boxitem_link_tooltip(action)) + klass, self.boxitem_link_tooltip(action)) class RQLBoxTemplate(BoxTemplate): """abstract box for boxes displaying the content of a rql query not related to the current result set. - - It rely on etype, rtype (both optional, usable to control registration - according to application schema and display according to connected - user's rights) and rql attributes """ rql = None @@ -148,29 +151,17 @@ class EntityBoxTemplate(BoxTemplate): """base class for boxes related to a single entity""" - __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view() + __select__ = BoxTemplate.__select__ & one_line_rset() context = 'incontext' def call(self, row=0, col=0, **kwargs): """classes inheriting from EntityBoxTemplate should define cell_call""" self.cell_call(row, col, **kwargs) - -class RelatedEntityBoxTemplate(EntityBoxTemplate): - __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities() - - def cell_call(self, row, col, **kwargs): - entity = self.cw_rset.get_entity(row, col) - limit = self._cw.property_value('navigation.related-limit') + 1 - role = get_role(self) - self.w(u'') +from cubicweb.web.component import AjaxEditRelationCtxComponent, EditRelationMixIn -class EditRelationBoxTemplate(ReloadableMixIn, EntityBoxTemplate): +class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate): """base class for boxes which let add or remove entities linked by a given relation @@ -181,7 +172,8 @@ def cell_call(self, row, col, view=None, **kwargs): self._cw.add_js('cubicweb.ajax.js') entity = self.cw_rset.get_entity(row, col) - title = display_name(self._cw, self.rtype, get_role(self), context=entity.__regid__) + title = display_name(self._cw, self.rtype, get_role(self), + context=entity.__regid__) box = SideBoxWidget(title, self.__regid__) related = self.related_boxitems(entity) unrelated = self.unrelated_boxitems(entity) @@ -191,144 +183,13 @@ box.extend(unrelated) box.render(self.w) - def div_id(self): - return self.__regid__ - def box_item(self, entity, etarget, rql, label): - """builds HTML link to edit relation between `entity` and `etarget` - """ - role, target = get_role(self), get_target(self) - args = {role[0] : entity.eid, target[0] : etarget.eid} - url = self._cw.user_rql_callback((rql, args)) - # for each target, provide a link to edit the relation - label = u'[%s] %s' % (xml_escape(url), label, - etarget.view('incontext')) + label = super(EditRelationBoxTemplate, self).box_item( + entity, etarget, rql, label) return RawBoxItem(label, liclass=u'invisible') - def related_boxitems(self, entity): - rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype - related = [] - for etarget in self.related_entities(entity): - related.append(self.box_item(entity, etarget, rql, u'-')) - return related - - def unrelated_boxitems(self, entity): - rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype - unrelated = [] - for etarget in self.unrelated_entities(entity): - unrelated.append(self.box_item(entity, etarget, rql, u'+')) - return unrelated - - def related_entities(self, entity): - return entity.related(self.rtype, get_role(self), entities=True) - - def unrelated_entities(self, entity): - """returns the list of unrelated entities, using the entity's - appropriate vocabulary function - """ - skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self), - entities=True)) - skip.add(None) - skip.add(INTERNAL_FIELD_VALUE) - filteretype = getattr(self, 'etype', None) - entities = [] - form = self._cw.vreg['forms'].select('edition', self._cw, - rset=self.cw_rset, - row=self.cw_row or 0) - field = form.field_by_name(self.rtype, get_role(self), entity.e_schema) - for _, eid in field.vocabulary(form): - if eid not in skip: - entity = self._cw.entity_from_eid(eid) - if filteretype is None or entity.__regid__ == filteretype: - entities.append(entity) - return entities - -class AjaxEditRelationBoxTemplate(EntityBoxTemplate): - __select__ = EntityBoxTemplate.__select__ & partial_relation_possible() - - # view used to display related entties - item_vid = 'incontext' - # values separator when multiple values are allowed - separator = ',' - # msgid of the message to display when some new relation has been added/removed - added_msg = None - removed_msg = None - - # class attributes below *must* be set in concret classes (additionaly to - # rtype / role [/ target_etype]. They should correspond to js_* methods on - # the json controller - - # function(eid) - # -> expected to return a list of values to display as input selector - # vocabulary - fname_vocabulary = None - - # function(eid, value) - # -> handle the selector's input (eg create necessary entities and/or - # relations). If the relation is multiple, you'll get a list of value, else - # a single string value. - fname_validate = None - - # function(eid, linked entity eid) - # -> remove the relation - fname_remove = None +AjaxEditRelationBoxTemplate = class_renamed( + 'AjaxEditRelationBoxTemplate', AjaxEditRelationCtxComponent, + '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationCtxComponent') - def cell_call(self, row, col, **kwargs): - req = self._cw - entity = self.cw_rset.get_entity(row, col) - related = entity.related(self.rtype, self.role) - rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype) - if self.role == 'subject': - mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid) - maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid) - else: - mayadd = rdef.has_perm(req, 'add', toeid=entity.eid) - maydel = rdef.has_perm(req, 'delete', toeid=entity.eid) - if not (related or mayadd): - return - if mayadd or maydel: - req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) - _ = req._ - w = self.w - divid = domid(self.__regid__) + unicode(entity.eid) - w(u'\n') diff -r dbb7ad04b963 -r 82d4011f54c1 web/component.py --- a/web/component.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/component.py Mon Sep 13 16:47:03 2010 +0200 @@ -22,57 +22,20 @@ __docformat__ = "restructuredtext en" _ = unicode -from logilab.common.deprecation import class_renamed +from logilab.common.deprecation import class_deprecated, class_renamed from logilab.mtconverter import xml_escape -from cubicweb import role -from cubicweb.utils import json_dumps -from cubicweb.view import Component -from cubicweb.selectors import ( - paginated_rset, one_line_rset, primary_view, match_context_prop, - partial_has_related_entities) +from cubicweb import Unauthorized, role, tags +from cubicweb.uilib import js, domid +from cubicweb.view import ReloadableMixIn, Component +from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset, + non_final_entity, partial_relation_possible, + partial_has_related_entities) +from cubicweb.appobject import AppObject +from cubicweb.web import htmlwidgets, stdmsgs -class EntityVComponent(Component): - """abstract base class for additinal components displayed in content - headers and footer according to: - - * the displayed entity's type - * a context (currently 'header' or 'footer') - - it should be configured using .accepts, .etype, .rtype, .target and - .context class attributes - """ - - __registry__ = 'contentnavigation' - __select__ = one_line_rset() & primary_view() & match_context_prop() - - cw_property_defs = { - _('visible'): dict(type='Boolean', default=True, - help=_('display the component or not')), - _('order'): dict(type='Int', default=99, - help=_('display order of the component')), - _('context'): dict(type='String', default='navtop', - vocabulary=(_('navtop'), _('navbottom'), - _('navcontenttop'), _('navcontentbottom'), - _('ctxtoolbar')), - help=_('context where this component should be displayed')), - } - - context = 'navcontentbottom' - - def call(self, view=None): - if self.cw_rset is None: - self.entity_call(self.cw_extra_kwargs.pop('entity')) - else: - self.cell_call(0, 0, view=view) - - def cell_call(self, row, col, view=None): - self.entity_call(self.cw_rset.get_entity(row, col), view=view) - - def entity_call(self, entity, view=None): - raise NotImplementedError() - +# abstract base class for navigation components ################################ class NavigationComponent(Component): """abstract base class for navigation components""" @@ -145,10 +108,9 @@ elif path == 'json': rql = params.pop('rql', self.cw_rset.printable_rql()) # latest 'true' used for 'swap' mode - url = 'javascript: replacePageChunk(%s, %s, %s, %s, true)' % ( - json_dumps(params.get('divid', 'pageContent')), - json_dumps(rql), json_dumps(params.pop('vid', None)), - json_dumps(params)) + url = 'javascript: %s' % (js.replacePageChunk( + params.get('divid', 'pageContent'), rql, + params.pop('vid', None), params)) else: url = self._cw.build_url(path, **params) return url @@ -177,6 +139,405 @@ return self.next_page_link_templ % (url, title, content) +# new contextual components system ############################################# + +def override_ctx(cls, **kwargs): + cwpdefs = cls.cw_property_defs.copy() + cwpdefs['context'] = cwpdefs['context'].copy() + cwpdefs['context'].update(kwargs) + return cwpdefs + + +class EmptyComponent(Exception): + """some selectable component has actually no content and should not be + rendered + """ + +class Layout(Component): + __regid__ = 'layout' + __abstract__ = True + + def init_rendering(self): + """init view for rendering. Return true if we should go on, false + if we should stop now. + """ + view = self.cw_extra_kwargs['view'] + try: + view.init_rendering() + except Unauthorized, ex: + self.warning("can't render %s: %s", view, ex) + return False + except EmptyComponent: + return False + return True + + +class CtxComponent(AppObject): + """base class for contextual compontents. The following contexts are + predefined: + + * boxes: 'left', 'incontext', 'right' + * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' + * other: 'ctxtoolbar' + + The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' + context are handled by the default primary view, others by the default main + template. + + All subclasses may not support all those contexts (for instance if it can't + be displayed as box, or as a toolbar icon). You may restrict allowed context + as followed: + + .. sourcecode:: python + + class MyComponent(CtxComponent): + cw_property_defs = override_ctx(CtxComponent, + vocabulary=[list of contexts]) + context = 'my default context' + + You can configure default component's context by simply giving appropriate + value to the `context` class attribute, as seen above. + """ + __registry__ = 'ctxcomponents' + __select__ = ~no_cnx() + + categories_in_order = () + cw_property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the box or not')), + _('order'): dict(type='Int', default=99, + help=_('display order of the box')), + _('context'): dict(type='String', default='left', + vocabulary=(_('left'), _('incontext'), _('right'), + _('navtop'), _('navbottom'), + _('navcontenttop'), _('navcontentbottom'), + _('ctxtoolbar')), + help=_('context where this component should be displayed')), + } + context = 'left' + contextual = False + title = None + + # XXX support kwargs for compat with old boxes which gets the view as + # argument + def render(self, w, **kwargs): + getlayout = self._cw.vreg['components'].select + try: + # XXX ensure context is given when the component is reloaded through + # ajax + context = self.cw_extra_kwargs['context'] + except KeyError: + context = self.cw_propval('context') + layout = getlayout('layout', self._cw, rset=self.cw_rset, + row=self.cw_row, col=self.cw_col, + view=self, context=context) + layout.render(w) + + def init_rendering(self): + """init rendering callback: that's the good time to check your component + has some content to display. If not, you can still raise + :exc:`EmptyComponent` to inform it should be skipped. + + Also, :exc:`Unauthorized` will be catched, logged, then the component + will be skipped. + """ + self.items = [] + + @property + def domid(self): + """return the HTML DOM identifier for this component""" + return domid(self.__regid__) + + @property + def cssclass(self): + """return the CSS class name for this component""" + return domid(self.__regid__) + + def render_title(self, w): + """return the title for this component""" + if self.title: + w(self._cw._(self.title)) + + def render_body(self, w): + """return the body (content) for this component""" + raise NotImplementedError() + + def render_items(self, w, items=None, klass=u'boxListing'): + if items is None: + items = self.items + assert items + w(u'
    ' % klass) + for item in items: + if hasattr(item, 'render'): + item.render(w) # XXX display
  • by itself + else: + w(u'
  • ') + w(item) + w(u'
  • ') + w(u'
') + + def append(self, item): + self.items.append(item) + + def box_action(self, action): # XXX action_link + return self.build_link(self._cw._(action.title), action.url()) + + def build_link(self, title, url, **kwargs): + if self._cw.selected(url): + try: + kwargs['klass'] += ' selected' + except KeyError: + kwargs['klass'] = 'selected' + return tags.a(title, href=url, **kwargs) + + +class EntityCtxComponent(CtxComponent): + """base class for boxes related to a single entity""" + __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset() + context = 'incontext' + contextual = True + + def __init__(self, *args, **kwargs): + super(EntityCtxComponent, self).__init__(*args, **kwargs) + try: + entity = kwargs['entity'] + except KeyError: + entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + self.entity = entity + + @property + def domid(self): + return domid(self.__regid__) + unicode(self.entity.eid) + + +# high level abstract classes ################################################## + +class RQLCtxComponent(CtxComponent): + """abstract box for boxes displaying the content of a rql query not + related to the current result set. + """ + rql = None + + def to_display_rql(self): + assert self.rql is not None, self.__regid__ + return (self.rql,) + + def init_rendering(self): + rset = self._cw.execute(*self.to_display_rql()) + if not rset: + raise EmptyComponent() + if len(rset[0]) == 2: + self.items = [] + for i, (eid, label) in enumerate(rset): + entity = rset.get_entity(i, 0) + self.items.append(self.build_link(label, entity.absolute_url())) + else: + self.items = [self.build_link(e.dc_title(), e.absolute_url()) + for e in rset.entities()] + + def render_body(self, w): + self.render_items(w) + + +class EditRelationMixIn(ReloadableMixIn): + def box_item(self, entity, etarget, rql, label): + """builds HTML link to edit relation between `entity` and `etarget`""" + role, target = role(self), get_target(self) + args = {role[0] : entity.eid, target[0] : etarget.eid} + url = self._cw.user_rql_callback((rql, args)) + # for each target, provide a link to edit the relation + return u'[%s] %s' % (xml_escape(url), label, + etarget.view('incontext')) + + def related_boxitems(self, entity): + rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype + return [self.box_item(entity, etarget, rql, u'-') + for etarget in self.related_entities(entity)] + + def related_entities(self, entity): + return entity.related(self.rtype, role(self), entities=True) + + def unrelated_boxitems(self, entity): + rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype + return [self.box_item(entity, etarget, rql, u'+') + for etarget in self.unrelated_entities(entity)] + + def unrelated_entities(self, entity): + """returns the list of unrelated entities, using the entity's + appropriate vocabulary function + """ + skip = set(unicode(e.eid) for e in entity.related(self.rtype, role(self), + entities=True)) + skip.add(None) + skip.add(INTERNAL_FIELD_VALUE) + filteretype = getattr(self, 'etype', None) + entities = [] + form = self._cw.vreg['forms'].select('edition', self._cw, + rset=self.cw_rset, + row=self.cw_row or 0) + field = form.field_by_name(self.rtype, role(self), entity.e_schema) + for _, eid in field.vocabulary(form): + if eid not in skip: + entity = self._cw.entity_from_eid(eid) + if filteretype is None or entity.__regid__ == filteretype: + entities.append(entity) + return entities + + +class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent): + """base class for boxes which let add or remove entities linked by a given + relation + + subclasses should define at least id, rtype and target class attributes. + """ + def render_title(self, w): + return display_name(self._cw, self.rtype, role(self), + context=self.entity.__regid__) + + def render_body(self, w): + self._cw.add_js('cubicweb.ajax.js') + related = self.related_boxitems(self.entity) + unrelated = self.unrelated_boxitems(self.entity) + self.items.extend(related) + if related and unrelated: + self.items.append(htmlwidgets.BoxSeparator()) + self.items.extend(unrelated) + self.render_items(w) + + +class AjaxEditRelationCtxComponent(EntityCtxComponent): + __select__ = EntityCtxComponent.__select__ & ( + partial_relation_possible(action='add') | partial_has_related_entities()) + + # view used to display related entties + item_vid = 'incontext' + # values separator when multiple values are allowed + separator = ',' + # msgid of the message to display when some new relation has been added/removed + added_msg = None + removed_msg = None + + # class attributes below *must* be set in concret classes (additionaly to + # rtype / role [/ target_etype]. They should correspond to js_* methods on + # the json controller + + # function(eid) + # -> expected to return a list of values to display as input selector + # vocabulary + fname_vocabulary = None + + # function(eid, value) + # -> handle the selector's input (eg create necessary entities and/or + # relations). If the relation is multiple, you'll get a list of value, else + # a single string value. + fname_validate = None + + # function(eid, linked entity eid) + # -> remove the relation + fname_remove = None + + def __init__(self, *args, **kwargs): + super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs) + self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype) + + def render_title(self, w): + w(self.rdef.rtype.display_name(self._cw, self.role, + context=self.entity.__regid__)) + + def render_body(self, w): + req = self._cw + entity = self.entity + related = entity.related(self.rtype, self.role) + if self.role == 'subject': + mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) + maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid) + else: + mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) + maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid) + if mayadd or maydel: + req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) + _ = req._ + if related: + w(u'') + for rentity in related.entities(): + # for each related entity, provide a link to remove the relation + subview = rentity.view(self.item_vid) + if maydel: + jscall = unicode(js.ajaxBoxRemoveLinkedEntity( + self.__regid__, entity.eid, rentity.eid, + self.fname_remove, + self.removed_msg and _(self.removed_msg))) + w(u'' + '' % (xml_escape(jscall), + subview)) + else: + w(u'' % (subview)) + w(u'
[-] %s
%s
') + else: + w(_('no related entity')) + if mayadd: + req.add_js('jquery.autocomplete.js') + req.add_css('jquery.autocomplete.css') + multiple = self.rdef.role_cardinality(self.role) in '*+' + w(u'
') + jscall = unicode(js.ajaxBoxShowSelector( + self.__regid__, entity.eid, self.fname_vocabulary, + self.fname_validate, self.added_msg and _(self.added_msg), + _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), + multiple and self.separator)) + w('%s' % ( + xml_escape(jscall), + multiple and _('add_relation') or _('update_relation'))) + w(u'') + w(u'
' % self.domid) + w(u'
') + + +# old contextual components, deprecated ######################################## + +class EntityVComponent(Component): + """abstract base class for additinal components displayed in content + headers and footer according to: + + * the displayed entity's type + * a context (currently 'header' or 'footer') + + it should be configured using .accepts, .etype, .rtype, .target and + .context class attributes + """ + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)' + + __registry__ = 'ctxcomponents' + __select__ = one_line_rset() + + cw_property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the component or not')), + _('order'): dict(type='Int', default=99, + help=_('display order of the component')), + _('context'): dict(type='String', default='navtop', + vocabulary=(_('navtop'), _('navbottom'), + _('navcontenttop'), _('navcontentbottom'), + _('ctxtoolbar')), + help=_('context where this component should be displayed')), + } + + context = 'navcontentbottom' + + def call(self, view=None): + if self.cw_rset is None: + self.entity_call(self.cw_extra_kwargs.pop('entity')) + else: + self.cell_call(0, 0, view=view) + + def cell_call(self, row, col, view=None): + self.entity_call(self.cw_rset.get_entity(row, col), view=view) + + def entity_call(self, entity, view=None): + raise NotImplementedError() + + class RelatedObjectsVComponent(EntityVComponent): """a section to display some related entities""" __select__ = EntityVComponent.__select__ & partial_has_related_entities() @@ -197,14 +558,15 @@ rset = self._cw.execute(self.rql(), {'x': eid}) if not rset.rowcount: return - self.w(u'
' % self.div_class()) + self.w(u'
' % self.cssclass) self.w(u'

%s

\n' % self._cw._(self.title).capitalize()) self.wview(self.vid, rset) self.w(u'
') + VComponent = class_renamed('VComponent', Component, - 'VComponent is deprecated, use Component') + '[3.2] VComponent is deprecated, use Component') SingletonVComponent = class_renamed('SingletonVComponent', Component, - 'SingletonVComponent is deprecated, use ' + '[3.2] SingletonVComponent is deprecated, use ' 'Component and explicit registration control') diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/actionBoxHeader.png Binary file web/data/actionBoxHeader.png has changed diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/boxHeader.png Binary file web/data/boxHeader.png has changed diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/contextFreeBoxHeader.png Binary file web/data/contextFreeBoxHeader.png has changed diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/contextualBoxHeader.png Binary file web/data/contextualBoxHeader.png has changed diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.calendar.css Mon Sep 13 16:47:03 2010 +0200 @@ -230,7 +230,7 @@ .calendar th.month { font-weight:bold; padding-bottom:0.2em; - background: %(actionBoxTitleBgColor)s; + background: %(incontextBoxBodyBgColor)s; } .calendar th.month a{ diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.css --- a/web/data/cubicweb.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.css Mon Sep 13 16:47:03 2010 +0200 @@ -31,19 +31,22 @@ /* h3 { font-size:1.30769em; } */ /* scale traditional */ -h1 { font-size: %(h1FontSize)s; } +h1, +.vtitle { font-size: %(h1FontSize)s; } h2 { font-size: %(h2FontSize)s; } h3 { font-size: %(h3FontSize)s; } /* paddings */ -h1 { +h1, +.vtitle { border-bottom: %(h1BorderBottomStyle)s; padding: %(h1Padding)s; margin: %(h1Margin)s; color: %(h1Color)s; } -div.tabbedprimary + h1, h1.plain { +div.tabbedprimary + h1, +h1.plain { border-bottom: none; } @@ -100,7 +103,7 @@ } ol ol, -ul ul{ +ul ul { margin-left: 8px; margin-bottom : 0px; } @@ -113,7 +116,7 @@ margin-left: 1.5em; } -img{ +img { border: none; } @@ -139,7 +142,7 @@ border: 1px inset %(headerBgColor)s; } -hr{ +hr { border: none; border-bottom: 1px solid %(defaultColor)s; height: 1px; @@ -234,16 +237,11 @@ /* Popup on login box and userActionBox */ div.popupWrapper { position: relative; - z-index: 100; } div.popup { position: absolute; background: #fff; - /* background-color: #f0eff0; */ - /* background-image: url(popup.png); */ - /* background-repeat: repeat-x; */ - /* background-positon: top left; */ border: 1px solid %(listingBorderColor)s; border-top: none; text-align: left; @@ -261,12 +259,13 @@ margin: %(defaultLayoutMargin)s; } -table#mainLayout #navColumnLeft { +table#mainLayout td#navColumnLeft { width: 16em; padding-right: %(defaultLayoutMargin)s; + } -table#mainLayout #navColumnRight { +table#mainLayout td#navColumnRight { width: 16em; padding-left: %(defaultLayoutMargin)s; } @@ -301,28 +300,15 @@ color: %(defaultColor)s; } -/* rql bar */ - -div#rqlinput { - margin-bottom: %(defaultLayoutMargin)s; -} - -input#rql{ - padding: 0.25em 0.3em; - width: 99%; -} - -/* boxes */ +/* XXX old boxes, deprecated */ div.boxFrame { width: 100%; } div.boxTitle { - overflow: hidden; - font-weight: bold; color: #fff; - background: %(boxTitleBg)s; + background: %(contextualBoxTitleBgColor)s; } div.boxTitle span, @@ -331,14 +317,7 @@ white-space: nowrap; } -div.searchBoxFrame div.boxTitle, -div.greyBoxFrame div.boxTitle { - background: %(actionBoxTitleBg)s; -} - -div.sideBoxTitle span, -div.searchBoxFrame div.boxTitle span, -div.greyBoxFrame div.boxTitle span { +div.sideBoxTitle span { color: %(defaultColor)s; } @@ -352,34 +331,13 @@ border-top: none; } -a.boxMenu { - display: block; - padding: 1px 9px 1px 3px; - background: transparent %(bulletDownImg)s; -} - -a.boxMenu:hover { - background: %(sideBoxBodyBgColor)s %(bulletDownImg)s; - cursor: pointer; -} - -a.popupMenu { - background: transparent url("puce_down_black.png") 2% 6px no-repeat; - padding-left: 2em; -} - -div.searchBoxFrame div.boxContent { - padding: 4px 4px 3px; - background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; -} - div.shadow{ height: 14px; background: url("shadow.gif") no-repeat top right; } div.sideBoxTitle { - background: %(actionBoxTitleBg)s; + background: %(incontextBoxBodyBg)s; display: block; font-weight: bold; } @@ -400,11 +358,11 @@ div.sideBoxBody { padding: 0.2em 5px; - background: %(sideBoxBodyBg)s; + background: %(incontextBoxBodyBg)s; } div.sideBoxBody a { - color: %(sideBoxBodyColor)s; + color: %(incontextBoxBodyColor)s; } div.sideBoxBody a:hover { @@ -415,6 +373,174 @@ padding-right: 1em; } +/* boxes */ + +div.boxTitle { + overflow: hidden; + font-weight: bold; +} + +div.boxTitle span { + padding: 0px 0.5em; + white-space: nowrap; +} + +div.boxBody { + padding: 5px; + border-top: none; + background-color: %(leftrightBoxBodyBgColor)s; +} + +div.boxBody a { + color: %(leftrightBoxBodyColor)s; +} + +div.boxBody a:hover { + text-decoration: none; + cursor: pointer; + background-color: %(leftrightBoxBodyHoverBgColor)s; +} + +/* boxes contextual customization */ + +.contextFreeBox div.boxTitle { + background: %(contextFreeBoxTitleBg)s; + color: %(contextFreeBoxTitleColor)s; +} + +.contextualBox div.boxTitle { + background: %(contextualBoxTitleBg)s; + color: %(contextualBoxTitleColor)s; +} + +.primaryRight div.boxTitle { + background: %(incontextBoxTitleBg)s; + color: %(incontextBoxTitleColor)s; +} + +.primaryRight div.boxBody { + padding: 0.2em 5px; + background: %(incontextBoxBodyBgColor)s; +} + +.primaryRight div.boxBody a { + color: %(incontextBoxBodyColor)s; +} + +.primaryRight div.boxBody a:hover { + background-color: %(incontextBoxBodyHoverBgColor)s; +} + +.primaryRight div.boxFooter { + margin-bottom: 1em; +} + +#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ + height: 14px; + background: url("shadow.gif") no-repeat top right; +} + +/* boxes lists and menus */ + +ul.boxListing { + margin: 0; + padding: 0; +} + +ul.boxListing ul { + padding: 1px 3px; +} + +ul.boxListing a { + color: %(defaultColor)s; + display: block; + padding: 1px 9px 1px 3px; +} + +ul.boxListing li { + margin: 0px; + padding: 0px; + background-image: none; +} + +ul.boxListing ul li { + margin: 0px; + padding-left: 8px; +} + +ul.boxListing ul li a { + padding-left: 10px; + background-image: url("bullet_orange.png"); + background-repeat: no-repeat; + background-position: 0 6px; +} + +ul.boxListing .selected { + color: %(aColor)s; + font-weight: bold; +} + +ul.boxListing a.boxMenu:hover { + border-top: medium none; + background: %(leftrightBoxBodyHoverBgColor)s; +} + +a.boxMenu, +ul.boxListing a.boxMenu{ + display: block; + padding: 1px 3px; + background: transparent %(bulletDownImg)s; +} + +ul.boxListing a.boxMenu:hover { + border-top: medium none; + background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s; +} + +a.boxMenu:hover { + cursor: pointer; +} + +a.popupMenu { + background: transparent url("puce_down_black.png") 2% 6px no-repeat; + padding-left: 2em; +} + +/* custom boxes */ + +.search_box div.boxBody { + padding: 4px 4px 3px; + background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; +} + +.bookmarks_box ul.boxListing div a{ + background: #fff; + display: inline; + padding: 0; +} +.bookmarks_box ul.boxListing div a:hover{ + border-bottom: 1px solid #000; +} + +.download_box div.boxTitle { + background : #8fbc8f !important; +} + +.download_box div.boxBody { + background : #eefed9; +} + +/* search box and rql bar */ + +div#rqlinput { + margin-bottom: %(defaultLayoutMargin)s; +} + +input#rql{ + padding: 0.25em 0.3em; + width: 99%; +} + input.rqlsubmit{ display: block; width: 20px; @@ -424,7 +550,7 @@ } input#norql{ - width:13em; + width:155px; margin-right: 2px; } @@ -435,7 +561,7 @@ } div#userActionsBox { - width: 14em; + width: 15em; text-align: right; } @@ -445,20 +571,6 @@ padding-right: 2em; } -/* download box XXX move to its own file? */ -div.downloadBoxTitle{ - background : #8fbc8f; - font-weight: bold; -} - -div.downloadBox{ - font-weight: bold; -} - -div.downloadBox div.sideBoxBody{ - background : #eefed9; -} - /**************/ /* navigation */ /**************/ @@ -566,7 +678,7 @@ div#appMsg { margin-bottom: %(defaultLayoutMargin)s; - border: 1px solid %(actionBoxTitleBgColor)s; + border: 1px solid %(incontextBoxTitleBgColor)s; } .message { @@ -579,7 +691,7 @@ padding-left: 25px; background: %(msgBgColor)s url("critical.png") 2px center no-repeat; color: %(errorMsgColor)s; - border: 1px solid %(actionBoxTitleBgColor)s; + border: 1px solid %(incontextBoxTitleBgColor)s; } /* search-associate message */ @@ -734,7 +846,7 @@ input.button{ margin: 1em 1em 0px 0px; border: 1px solid %(buttonBorderColor)s; - border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s; + border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s; } /* FileItemInnerView jquery.treeview.css */ @@ -754,74 +866,20 @@ ul.startup li, ul.section li { - margin-left:0px -} - -ul.boxListing { - margin: 0px; - padding: 0px 3px; -} - -ul.boxListing li, -ul.boxListing ul li { - margin: 0px; - padding: 0px; - background-image: none; -} - -ul.boxListing ul { - padding: 1px 3px; -} - -ul.boxListing a { - color: %(defaultColor)s; - display:block; - padding: 1px 9px 1px 3px; -} - -ul.boxListing .selected { - color: %(aColor)s; - font-weight: bold; -} - -ul.boxListing a.boxMenu:hover { - border-top: medium none; - background: %(sideBoxBodyBgColor)s %(bulletDownImg)s; -} - -ul.boxListing a.boxBookmark { - padding-left: 3px; - background-image: none; - background:#fff; + margin-left: 0px } ul.simple li, -ul.boxListing ul li , .popupWrapper ul li { background: transparent url("bullet_orange.png") no-repeat 0% 6px; } -ul.boxListing a.boxBookmark:hover, -ul.boxListing a:hover, -ul.boxListing ul li a:hover { - text-decoration: none; - background: %(sideBoxBodyBg)s; -} - -ul.boxListing ul li a:hover{ - background-color: transparent; -} - -ul.boxListing ul li a { - padding: 1px 3px 0px 10px; -} - ul.simple li { padding-left: 8px; } .popupWrapper ul { - padding:0.2em 0.3em; + padding: 0.2em 0.3em; margin-bottom: 0px; } @@ -854,20 +912,12 @@ .validateButton { margin: 1em 1em 0px 0px; border: 1px solid %(buttonBorderColor)s; - border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s; + border-color: %(buttonBorderColor)s %(incontextBoxTitleBgColor)s %(incontextBoxTitleBgColor)s %(buttonBorderColor)s; background: %(buttonBgColor)s url("button.png") bottom left repeat-x; } /********************************/ -/* placement of alt. view icons */ -/********************************/ - -.otherView { - float: right; -} - -/********************************/ -/* rest releted classes */ +/* rest related classes */ /********************************/ img.align-right { diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.form.css --- a/web/data/cubicweb.form.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.form.css Mon Sep 13 16:47:03 2010 +0200 @@ -229,6 +229,6 @@ margin: 1em 1em 0px 0px; border-width: 1px; border-style: solid; - border-color: %(buttonBorderColor)s %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s %(buttonBorderColor)s; + border-color: %(buttonBorderColor)s %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s %(buttonBorderColor)s; background: %(buttonBgColor)s %(buttonBgImg)s; } diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.login.css --- a/web/data/cubicweb.login.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.login.css Mon Sep 13 16:47:03 2010 +0200 @@ -30,7 +30,7 @@ margin-left: -14em; width: 28em; background: #fff; - border: 2px solid %(actionBoxTitleBgColor)s; + border: 2px solid %(incontextBoxBodyBgColor)s; padding-bottom: 0.5em; text-align: center; } @@ -80,7 +80,7 @@ .loginButton { border: 1px solid #edecd2; - border-color: #edecd2 %(actionBoxTitleBgColor)s %(actionBoxTitleBgColor)s #edecd2; + border-color: #edecd2 %(incontextBoxBodyBgColor)s %(incontextBoxBodyBgColor)s #edecd2; margin: 2px 0px 0px; background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; } diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.old.css --- a/web/data/cubicweb.old.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.old.css Mon Sep 13 16:47:03 2010 +0200 @@ -22,7 +22,8 @@ font-family: Verdana, sans-serif; } -h1 { +h1, +.vtitle { font-size: 188%; margin: 0.2em 0px 0.3em; border-bottom: 1px solid #000; @@ -243,10 +244,6 @@ } /* Popup on login box and userActionBox */ -div.popupWrapper{ - position:relative; - z-index:100; -} div.popup { position: absolute; @@ -303,18 +300,23 @@ div#rqlinput { border: 1px solid #cfceb7; margin-bottom: 8px; - padding: 3px; + padding: 1px; background: #cfceb7; + width: 100%; +} + +input#rql { + width: 99%; } -input#rql{ - width: 95%; +input.rqlsubmit{ + display: block; + width: 20px; + height: 20px; + background: %(buttonBgColor)s url("go.png") 50% 50% no-repeat; + vertical-align: bottom; } - -/* boxes */ -div.navboxes { - margin-top: 8px; -} +/* old boxes, deprecated */ div.boxFrame { width: 100%; @@ -324,25 +326,17 @@ padding-top: 0px; padding-bottom: 0.2em; font: bold 100% Georgia; - overflow: hidden; color: #fff; background: #ff9900 url("search.png") left bottom repeat-x; } -div.searchBoxFrame div.boxTitle, -div.greyBoxFrame div.boxTitle { - background: #cfceb7; -} - div.boxTitle span, div.sideBoxTitle span { padding: 0px 5px; white-space: nowrap; } -div.sideBoxTitle span, -div.searchBoxFrame div.boxTitle span, -div.greyBoxFrame div.boxTitle span { +div.sideBoxTitle span { color: #222211; } @@ -356,85 +350,6 @@ border-top: none; } -ul.boxListing { - margin: 0px; - padding: 0px 3px; -} - -ul.boxListing li, -ul.boxListing ul li { - display: inline; - margin: 0px; - padding: 0px; - background-image: none; -} - -ul.boxListing ul { - margin: 0px 0px 0px 7px; - padding: 1px 3px; -} - -ul.boxListing a { - color: #000; - display: block; - padding: 1px 9px 1px 3px; -} - -ul.boxListing .selected { - color: #FF4500; - font-weight: bold; -} - -ul.boxListing a.boxBookmark:hover, -ul.boxListing a:hover, -ul.boxListing ul li a:hover { - text-decoration: none; - background: #eeedd9; - color: #111100; -} - -ul.boxListing a.boxMenu:hover { - background: #eeedd9 url(puce_down.png) no-repeat scroll 98% 6px; - cursor:pointer; - border-top:medium none; - } -a.boxMenu { - background: transparent url("puce_down.png") 98% 6px no-repeat; - display: block; - padding: 1px 9px 1px 3px; -} - - -a.popupMenu { - background: transparent url("puce_down_black.png") 2% 6px no-repeat; - padding-left: 2em; -} - -ul.boxListing ul li a:hover { - background: #eeedd9 url("bullet_orange.png") 0% 6px no-repeat; -} - -a.boxMenu:hover { - background: #eeedd9 url("puce_down.png") 98% 6px no-repeat; - cursor: pointer; -} - -ul.boxListing a.boxBookmark { - padding-left: 3px; - background-image:none; - background:#fff; -} - -ul.boxListing ul li a { - background: #fff url("bullet_orange.png") 0% 6px no-repeat; - padding: 1px 3px 0px 10px; -} - -div.searchBoxFrame div.boxContent { - padding: 4px 4px 3px; - background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; -} - div.shadow{ height: 14px; background: url("shadow.gif") no-repeat top right; @@ -474,16 +389,164 @@ padding-right: 1em; } -input.rqlsubmit{ - background: #fffff8 url("go.png") 50% 50% no-repeat; - width: 20px; - height: 20px; - margin: 0px; +/* boxes */ + +div.navboxes { + padding-top: 0.5em; +} + +div.boxTitle { + overflow: hidden; + font-weight: bold; +} + +div.boxTitle span { + padding: 0px 0.5em; + white-space: nowrap; +} + +div.boxBody { + padding: 3px 3px; + border-top: none; + background-color: %(leftrightBoxBodyBgColor)s; +} + +div.boxBody a { + color: %(leftrightBoxBodyColor)s; +} + +div.boxBody a:hover { + text-decoration: none; + cursor: pointer; + background-color: %(leftrightBoxBodyHoverBgColor)s; +} + +/* boxes contextual customization */ + +.contextFreeBox div.boxTitle { + background: %(contextFreeBoxTitleBg)s; + color: %(contextFreeBoxTitleColor)s; +} + +.contextualBox div.boxTitle { + background: %(contextualBoxTitleBg)s; + color: %(contextualBoxTitleColor)s; +} + +.primaryRight div.boxTitle { + background: %(incontextBoxTitleBg)s; + color: %(incontextBoxTitleColor)s; +} + +.primaryRight div.boxBody { + padding: 0.2em 5px; + background: %(incontextBoxBodyBgColor)s; +} + +.primaryRight div.boxBody a { + color: %(incontextBoxBodyColor)s; +} + +.primaryRight div.boxBody a:hover { + background-color: %(incontextBoxBodyHoverBgColor)s; +} + +.primaryRight div.boxFooter { + margin-bottom: 1em; +} + +#navColumnLeft div.boxFooter, #navColumnRight div.boxFooter{ + height: 14px; + background: url("shadow.gif") no-repeat top right; +} + +/* boxes lists and menus */ + +ul.boxListing { + margin: 0; + padding: 0; } -input#norql{ - width:13em; - margin-right: 2px; +ul.boxListing ul { + padding: 1px 3px; +} + +ul.boxListing a { + color: %(defaultColor)s; + display: block; + padding: 1px 3px; +} + +ul.boxListing li { + margin: 0px; + padding: 0px; + background-image: none; +} + +ul.boxListing ul li { + margin: 0px; + padding-left: 1em; +} + +ul.boxListing ul li a { + padding-left: 10px; + background-image: url("bullet_orange.png"); + background-repeat: no-repeat; + background-position: 0 6px; +} + +ul.boxListing .selected { + color: %(aColor)s; + font-weight: bold; +} + +ul.boxListing a.boxMenu:hover { + border-top: medium none; + background: %(leftrightBoxBodyHoverBgColor)s; +} + +a.boxMenu, +ul.boxListing a.boxMenu{ + display: block; + padding: 1px 3px; + background: transparent %(bulletDownImg)s; +} + +ul.boxListing a.boxMenu:hover { + border-top: medium none; + background: %(leftrightBoxBodyHoverBgColor)s %(bulletDownImg)s; +} + +a.boxMenu:hover { + cursor: pointer; +} + +a.popupMenu { + background: transparent url("puce_down_black.png") 2% 6px no-repeat; + padding-left: 2em; +} + + +/* custom boxes */ + +.search_box div.boxBody { + padding: 4px 4px 3px; + background: #f0eff0 url("gradient-grey-up.png") left top repeat-x; +} + +.bookmarks_box ul.boxListing div a { + background: #fff; + display: inline; + padding: 0; +} + +.download_box div.boxTitle { + background : #8fbc8f !important; +} + +.download_box div.boxBody { + background : #eefed9; + vertical-align: center; } /* user actions menu */ @@ -503,20 +566,6 @@ padding-right: 2em; } -/* download box XXX move to its own file? */ -div.downloadBoxTitle{ - background : #8FBC8F; - font-weight: bold; -} - -div.downloadBox{ - font-weight: bold; -} - -div.downloadBox div.sideBoxBody{ - background : #EEFED9; -} - /**************/ /* navigation */ /**************/ @@ -852,11 +901,3 @@ border-color:#edecd2 #cfceb7 #cfceb7 #edecd2; background: #fffff8 url("button.png") bottom left repeat-x; } - -/********************************/ -/* placement of alt. view icons */ -/********************************/ - -.otherView { - float: right; -} diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/cubicweb.tableview.css --- a/web/data/cubicweb.tableview.css Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/cubicweb.tableview.css Mon Sep 13 16:47:03 2010 +0200 @@ -6,7 +6,7 @@ font-weight: bold; background: #ebe8d9 url("button.png") repeat-x; padding: 0.3em; - border-bottom: 1px solid %(actionBoxTitleBgColor)s; + border-bottom: 1px solid %(incontextBoxBodyBgColor)s; text-align: left; } diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/incontextBoxHeader.png Binary file web/data/incontextBoxHeader.png has changed diff -r dbb7ad04b963 -r 82d4011f54c1 web/data/uiprops.py --- a/web/data/uiprops.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/data/uiprops.py Mon Sep 13 16:47:03 2010 +0200 @@ -103,18 +103,36 @@ pageContentPadding = '1em' pageMinHeight = '800px' -# boxes -boxTitleBg = lazystr('%(headerBgColor)s url("boxHeader.png") repeat-x 50%% 50%%') -boxBodyBgColor = '#efefde' +# boxes ######################################################################## + +# title of contextFree / contextual boxes +contextFreeBoxTitleBgColor = '#CFCEB7' +contextFreeBoxTitleBg = lazystr('%(contextFreeBoxTitleBgColor)s url("contextFreeBoxHeader.png") repeat-x 50%% 50%%') +contextFreeBoxTitleColor = '#000' + +contextualBoxTitleBgColor = '#FF9900' +contextualBoxTitleBg = lazystr('%(contextualBoxTitleBgColor)s url("contextualBoxHeader.png") repeat-x 50%% 50%%') +contextualBoxTitleColor = '#FFF' -# action, search, sideBoxes -actionBoxTitleBgColor = '#cfceb7' -actionBoxTitleBg = lazystr('%(actionBoxTitleBgColor)s url("actionBoxHeader.png") repeat-x 50%% 50%%') -sideBoxBodyBgColor = '#f8f8ee' -sideBoxBodyBg = lazystr('%(sideBoxBodyBgColor)s') -sideBoxBodyColor = '#555544' +# title of 'incontext' boxes (eg displayed insinde the primary view) +incontextBoxTitleBgColor = lazystr('%(contextFreeBoxTitleBgColor)s') +incontextBoxTitleBg = lazystr('%(incontextBoxTitleBgColor)s url("incontextBoxHeader.png") repeat-x 50%% 50%%') +incontextBoxTitleColor = '#000' -# table listing & co +# body of boxes in the left or right column (whatever contextFree / contextual) +leftrightBoxBodyBgColor = '#FFF' +leftrightBoxBodyBg = lazystr('%(leftrightBoxBodyBgColor)s') +leftrightBoxBodyColor = '#black' +leftrightBoxBodyHoverBgColor = '#EEEDD9' + +# body of 'incontext' boxes (eg displayed insinde the primary view) +incontextBoxBodyBgColor = '#f8f8ee' +incontextBoxBodyBg = lazystr('%(incontextBoxBodyBgColor)s') +incontextBoxBodyColor = '#555544' +incontextBoxBodyHoverBgColor = lazystr('%(incontextBoxBodyBgColor)s') + + +# table listing & co ########################################################### listingBorderColor = '#ccc' listingHeaderBgColor = '#efefef' listingHihligthedBgColor = '#fbfbfb' diff -r dbb7ad04b963 -r 82d4011f54c1 web/formfields.py --- a/web/formfields.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/formfields.py Mon Sep 13 16:47:03 2010 +0200 @@ -73,6 +73,7 @@ FormatConstraint) from cubicweb import Binary, tags, uilib +from cubicweb.utils import support_args from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \ formwidgets as fw, uicfg @@ -332,7 +333,7 @@ if self.eidparam and self.role is not None: entity = form.edited_entity if form._cw.vreg.schema.rschema(self.name).final: - if entity.has_eid() or self.name in entity: + if entity.has_eid() or self.name in entity.cw_attr_cache: value = getattr(entity, self.name) if value is not None or not self.fallback_on_none_attribute: return value @@ -345,7 +346,12 @@ def initial_typed_value(self, form, load_bytes): if self.value is not _MARKER: if callable(self.value): - return self.value(form) + if support_args(self.value, 'form', 'field'): + return self.value(form, self) + else: + warn("[3.10] field's value callback must now take form and field as argument", + DeprecationWarning) + return self.value(form) return self.value formattr = '%s_%s_default' % (self.role, self.name) if hasattr(form, formattr): @@ -427,7 +433,7 @@ if self.eidparam and self.role == 'subject': entity = form.edited_entity if entity.e_schema.has_metadata(self.name, 'format') and ( - entity.has_eid() or '%s_format' % self.name in entity): + entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache): return form.edited_entity.cw_attr_metadata(self.name, 'format') return form._cw.property_value('ui.default-text-format') diff -r dbb7ad04b963 -r 82d4011f54c1 web/htmlwidgets.py --- a/web/htmlwidgets.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/htmlwidgets.py Mon Sep 13 16:47:03 2010 +0200 @@ -19,11 +19,10 @@ those are in cubicweb since we need to know available widgets at schema serialization time - """ +import random from math import floor -import random from logilab.mtconverter import xml_escape @@ -182,7 +181,10 @@ toggle_action(ident), self.link_class, self.label)) self._begin_menu(ident) for item in self.items: - item.render(self.w) + if hasattr(item, 'render'): + item.render(self.w) + else: + self.w(u'
  • %s
  • ' % item) self._end_menu() if self.isitem: self.w(u'') diff -r dbb7ad04b963 -r 82d4011f54c1 web/test/unittest_session.py --- a/web/test/unittest_session.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/test/unittest_session.py Mon Sep 13 16:47:03 2010 +0200 @@ -7,10 +7,11 @@ :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ from cubicweb.devtools.testlib import CubicWebTC +from cubicweb.web import InvalidSession class SessionTC(CubicWebTC): - def test_auto_reconnection(self): + def test_session_expiration(self): sm = self.app.session_handler.session_manager # make is if the web session has been opened by the session manager sm._sessions[self.cnx.sessionid] = self.websession @@ -23,11 +24,8 @@ # fake an incoming http query with sessionid in session cookie # don't use self.request() which try to call req.set_session req = self.requestcls(self.vreg) - websession = sm.get_session(req, sessionid) - self.assertEquals(len(sm._sessions), 1) - self.assertIs(websession, self.websession) - self.assertEquals(websession.sessionid, sessionid) - self.assertNotEquals(websession.sessionid, websession.cnx.sessionid) + self.assertRaises(InvalidSession, sm.get_session, req, sessionid) + self.assertEquals(len(sm._sessions), 0) finally: # avoid error in tearDown by telling this connection is closed... self.cnx._closed = True diff -r dbb7ad04b963 -r 82d4011f54c1 web/test/unittest_views_navigation.py --- a/web/test/unittest_views_navigation.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/test/unittest_views_navigation.py Mon Sep 13 16:47:03 2010 +0200 @@ -122,26 +122,26 @@ # view = mock_object(is_primary=lambda x: True) # rset = self.execute('CWUser X LIMIT 1') # req = self.request() - # objs = self.vreg['contentnavigation'].poss_visible_objects( + # objs = self.vreg['ctxcomponents'].poss_visible_objects( # req, rset=rset, view=view, context='navtop') # # breadcrumbs should be in headers by default # clsids = set(obj.id for obj in objs) # self.failUnless('breadcrumbs' in clsids) - # objs = self.vreg['contentnavigation'].poss_visible_objects( + # objs = self.vreg['ctxcomponents'].poss_visible_objects( # req, rset=rset, view=view, context='navbottom') # # breadcrumbs should _NOT_ be in footers by default # clsids = set(obj.id for obj in objs) # self.failIf('breadcrumbs' in clsids) - # self.execute('INSERT CWProperty P: P pkey "contentnavigation.breadcrumbs.context", ' + # self.execute('INSERT CWProperty P: P pkey "ctxcomponents.breadcrumbs.context", ' # 'P value "navbottom"') # # breadcrumbs should now be in footers # req.cnx.commit() - # objs = self.vreg['contentnavigation'].poss_visible_objects( + # objs = self.vreg['ctxcomponents'].poss_visible_objects( # req, rset=rset, view=view, context='navbottom') # clsids = [obj.id for obj in objs] # self.failUnless('breadcrumbs' in clsids) - # objs = self.vreg['contentnavigation'].poss_visible_objects( + # objs = self.vreg['ctxcomponents'].poss_visible_objects( # req, rset=rset, view=view, context='navtop') # clsids = [obj.id for obj in objs] diff -r dbb7ad04b963 -r 82d4011f54c1 web/test/unittest_viewselector.py --- a/web/test/unittest_viewselector.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/test/unittest_viewselector.py Mon Sep 13 16:47:03 2010 +0200 @@ -468,18 +468,18 @@ def test_properties(self): self.assertEquals(sorted(k for k in self.vreg['propertydefs'].keys() - if k.startswith('boxes.edit_box')), - ['boxes.edit_box.context', - 'boxes.edit_box.order', - 'boxes.edit_box.visible']) + if k.startswith('ctxcomponents.edit_box')), + ['ctxcomponents.edit_box.context', + 'ctxcomponents.edit_box.order', + 'ctxcomponents.edit_box.visible']) self.assertEquals([k for k in self.vreg['propertyvalues'].keys() if not k.startswith('system.version')], []) - self.assertEquals(self.vreg.property_value('boxes.edit_box.visible'), True) - self.assertEquals(self.vreg.property_value('boxes.edit_box.order'), 2) - self.assertEquals(self.vreg.property_value('boxes.possible_views_box.visible'), False) - self.assertEquals(self.vreg.property_value('boxes.possible_views_box.order'), 10) - self.assertRaises(UnknownProperty, self.vreg.property_value, 'boxes.actions_box') + self.assertEquals(self.vreg.property_value('ctxcomponents.edit_box.visible'), True) + self.assertEquals(self.vreg.property_value('ctxcomponents.edit_box.order'), 2) + self.assertEquals(self.vreg.property_value('ctxcomponents.possible_views_box.visible'), False) + self.assertEquals(self.vreg.property_value('ctxcomponents.possible_views_box.order'), 10) + self.assertRaises(UnknownProperty, self.vreg.property_value, 'ctxcomponents.actions_box') diff -r dbb7ad04b963 -r 82d4011f54c1 web/uicfg.py --- a/web/uicfg.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/uicfg.py Mon Sep 13 16:47:03 2010 +0200 @@ -107,11 +107,8 @@ def init_primaryview_display_ctrl(rtag, sschema, rschema, oschema, role): if role == 'subject': oschema = '*' - label = rschema.type else: sschema = '*' - label = '%s_%s' % (rschema, role) - rtag.setdefault((sschema, rschema, oschema, role), 'label', label) rtag.counter += 1 rtag.setdefault((sschema, rschema, oschema, role), 'order', rtag.counter) diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/ajaxedit.py --- a/web/views/ajaxedit.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/ajaxedit.py Mon Sep 13 16:47:03 2010 +0200 @@ -15,21 +15,19 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Set of views allowing edition of entities/relations using ajax +"""Set of views allowing edition of entities/relations using ajax""" -""" __docformat__ = "restructuredtext en" from cubicweb import role +from cubicweb.view import View from cubicweb.selectors import match_form_params, match_kwargs -from cubicweb.web.box import EditRelationBoxTemplate +from cubicweb.web.component import EditRelationMixIn -class AddRelationView(EditRelationBoxTemplate): - """base class for view which let add entities linked - by a given relation +class AddRelationView(EditRelationMixIn, View): + """base class for view which let add entities linked by a given relation - subclasses should define at least id, rtype and target - class attributes. + subclasses should define at least id, rtype and target class attributes. """ __registry__ = 'views' __regid__ = 'xaddrelation' @@ -38,7 +36,7 @@ cw_property_defs = {} # don't want to inherit this from Box expected_kwargs = form_params = ('rtype', 'target') - build_js = EditRelationBoxTemplate.build_reload_js_call + build_js = EditRelationMixIn.build_reload_js_call def cell_call(self, row, col, rtype=None, target=None, etype=None): self.rtype = rtype or self._cw.form['rtype'] @@ -53,13 +51,13 @@ etypes = rschema.subjects(entity.e_schema) if len(etypes) == 1: self.etype = etypes[0] - self.w(u'
    ' % self.__regid__) + self.w(u'
    ' % self.domid) self.w(u'

    %s

    ' % self._cw._('relation %(relname)s of %(ent)s') % {'relname': rschema.display_name(self._cw, role(self)), 'ent': entity.view('incontext')}) self.w(u'
      ') for boxitem in self.unrelated_boxitems(entity): - boxitem.render(self.w) + self.w('' % botitem) self.w(u'
    ') def unrelated_entities(self, entity): @@ -74,11 +72,4 @@ ordermethod='fetch_order') self.pagination(self._cw, rset, w=self.w) return rset.entities() - # in other cases, use vocabulary functions - entities = [] - # XXX to update for 3.2 - for _, eid in entity.vocabulary(self.rtype, role(self)): - if eid is not None: - rset = self._cw.eid_rset(eid) - entities.append(rset.get_entity(0, 0)) - return entities + super(AddRelationView, self).unrelated_entities(self) diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/authentication.py --- a/web/views/authentication.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/authentication.py Mon Sep 13 16:47:03 2010 +0200 @@ -74,7 +74,7 @@ self.repo = vreg.config.repository(vreg) self.log_queries = vreg.config['query-log-file'] self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg), - key=lambda x: x.order) + key=lambda x: x.order) # 2-uple login / password, login is None when no anonymous access # configured self.anoninfo = vreg.config.anonymous_user() @@ -98,25 +98,11 @@ if login and session.login != login: raise InvalidSession('login mismatch') try: - lock = session.reconnection_lock - except AttributeError: - lock = session.reconnection_lock = Lock() - # need to be locked two avoid duplicated reconnections on concurrent - # requests - with lock: - cnx = session.cnx - try: - # calling cnx.user() check connection validity, raise - # BadConnectionId on failure - user = cnx.user(req) - except BadConnectionId: - # check if a connection should be automatically restablished - if (login is None or login == session.login): - cnx = self._authenticate(session.login, session.authinfo) - user = cnx.user(req) - session.cnx = cnx - else: - raise InvalidSession('bad connection id') + # calling cnx.user() check connection validity, raise + # BadConnectionId on failure + user = session.cnx.user(req) + except BadConnectionId: + raise InvalidSession('bad connection id') return user def authenticate(self, req): diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/basecomponents.py --- a/web/views/basecomponents.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/basecomponents.py Mon Sep 13 16:47:03 2010 +0200 @@ -20,6 +20,7 @@ * the rql input form * the logged user link """ +from __future__ import with_statement __docformat__ = "restructuredtext en" _ = unicode @@ -27,9 +28,11 @@ from logilab.mtconverter import xml_escape from rql import parse -from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params, +from cubicweb.selectors import (yes, multi_etypes_rset, + match_form_params, match_context, anonymous_user, authenticated_user) from cubicweb.schema import display_name +from cubicweb.utils import wrap_on_write from cubicweb.uilib import toggle_action from cubicweb.web import component from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator, @@ -148,8 +151,7 @@ self.w(u'
    \n' % (toggle_action('appMsg'), (msgs and ' ' or 'hidden'))) for msg in msgs: - self.w(u'
    %s
    ' % ( - self.div_id(), msg)) + self.w(u'
    %s
    ' % (self.domid, msg)) self.w(u'
    ') @@ -167,18 +169,6 @@ self._cw.base_url(), xml_escape(title))) -class SeeAlsoVComponent(component.RelatedObjectsVComponent): - """display any entity's see also""" - __regid__ = 'seealso' - context = 'navcontentbottom' - rtype = 'see_also' - role = 'subject' - order = 40 - # register msg not generated since no entity use see_also in cubicweb itself - title = _('contentnavigation_seealso') - help = _('contentnavigation_seealso_description') - - class EtypeRestrictionComponent(component.Component): """displays the list of entity types contained in the resultset to be able to filter accordingly. @@ -230,17 +220,46 @@ self.w(u' | '.join(html)) self.w(u'
    ') +# contextual components ######################################################## -class MetaDataComponent(component.EntityVComponent): +# class SeeAlsoVComponent(component.RelatedObjectsVComponent): +# """display any entity's see also""" +# __regid__ = 'seealso' +# context = 'navcontentbottom' +# rtype = 'see_also' +# role = 'subject' +# order = 40 +# # register msg not generated since no entity use see_also in cubicweb itself +# title = _('ctxcomponents_seealso') +# help = _('ctxcomponents_seealso_description') + + +class MetaDataComponent(component.EntityCtxComponent): __regid__ = 'metadata' context = 'navbottom' order = 1 - def cell_call(self, row, col, view=None): - self.wview('metadata', self.cw_rset, row=row, col=col) + def render_body(self, w): + self.entity.view('metadata', w=w) -def registration_callback(vreg): - vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,)) - if 'see_also' in vreg.schema: - vreg.register(SeeAlsoVComponent) +class SectionLayout(component.Layout): + __select__ = match_context('navtop', 'navbottom', + 'navcontenttop', 'navcontentbottom') + cssclass = 'section' + + def render(self, w): + if self.init_rendering(): + view = self.cw_extra_kwargs['view'] + w(u'
    ' % (self.cssclass, view.cssclass, + view.domid)) + with wrap_on_write(w, '

    ') as wow: + view.render_title(wow) + view.render_body(w) + w(u'

    \n') + + +# def registration_callback(vreg): +# vreg.register_all(globals().values(), __name__, (SeeAlsoVComponent,)) +# if 'see_also' in vreg.schema: +# vreg.register(SeeAlsoVComponent) diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/basecontrollers.py --- a/web/views/basecontrollers.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/basecontrollers.py Mon Sep 13 16:47:03 2010 +0200 @@ -26,7 +26,7 @@ from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, AuthenticationError, typed_eid) -from cubicweb.utils import json, json_dumps +from cubicweb.utils import UStringIO, json, json_dumps from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params from cubicweb.mail import format_mail from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse @@ -346,14 +346,19 @@ def _call_view(self, view, paginate=False, **kwargs): divid = self._cw.form.get('divid', 'pageContent') # we need to call pagination before with the stream set - stream = view.set_stream() + try: + stream = view.set_stream() + except AttributeError: + stream = UStringIO() + kwargs['w'] = stream.write + assert not paginate if paginate: if divid == 'pageContent': # mimick main template behaviour stream.write(u'
    ') vtitle = self._cw.form.get('vtitle') if vtitle: - stream.write(u'

    %s

    \n' % vtitle) + stream.write(u'
    %s
    \n' % vtitle) view.paginate() if divid == 'pageContent': stream.write(u'
    ') diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/basetemplates.py --- a/web/views/basetemplates.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/basetemplates.py Mon Sep 13 16:47:03 2010 +0200 @@ -127,7 +127,7 @@ w(u'
    \n') vtitle = self._cw.form.get('vtitle') if vtitle: - w(u'

    %s

    \n' % xml_escape(vtitle)) + w(u'
    %s
    \n' % xml_escape(vtitle)) # display entity type restriction component etypefilter = self._cw.vreg['components'].select_or_none( 'etypenavigation', self._cw, rset=self.cw_rset) @@ -188,9 +188,10 @@ self.w(u'') def nav_column(self, view, context): - boxes = list(self._cw.vreg['boxes'].poss_visible_objects( + boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects( self._cw, rset=self.cw_rset, view=view, context=context)) if boxes: + getlayout = self._cw.vreg['components'].select self.w(u'' % footer) w(u'
    \n') -class DownloadBox(box.EntityBoxTemplate): +class DownloadBox(component.EntityCtxComponent): __regid__ = 'download_box' # no download box for images - # XXX primary_view selector ? - __select__ = (one_line_rset() & match_context_prop() - & adaptable('IDownloadable') & ~has_mimetype('image/')) + __select__ = (component.EntityCtxComponent.__select__ & + adaptable('IDownloadable') & ~has_mimetype('image/')) + order = 10 + title = _('download') - def cell_call(self, row, col, title=None, label=None, **kwargs): - entity = self.cw_rset.get_entity(row, col) - download_box(self.w, entity, title, label) + def render_body(self, w): + w(u'%s %s' + % (xml_escape(self.entity.cw_adapt_to('IDownloadable').download_url()), + self._cw.uiprops['DOWNLOAD_ICON'], + self._cw._('download icon'), xml_escape(self.entity.dc_title()))) class DownloadView(EntityView): diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/navigation.py --- a/web/views/navigation.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/navigation.py Mon Sep 13 16:47:03 2010 +0200 @@ -29,7 +29,7 @@ adaptable, implements) from cubicweb.uilib import cut from cubicweb.view import EntityAdapter, implements_adapter_compat -from cubicweb.web.component import EntityVComponent, NavigationComponent +from cubicweb.web.component import EmptyComponent, EntityCtxComponent, NavigationComponent class PageNavigation(NavigationComponent): @@ -201,59 +201,55 @@ raise NotImplementedError -class NextPrevNavigationComponent(EntityVComponent): +class NextPrevNavigationComponent(EntityCtxComponent): __regid__ = 'prevnext' # register msg not generated since no entity implements IPrevNext in cubicweb # itself - title = _('contentnavigation_prevnext') - help = _('contentnavigation_prevnext_description') - __select__ = EntityVComponent.__select__ & adaptable('IPrevNext') + help = _('ctxcomponents_prevnext_description') + __select__ = EntityCtxComponent.__select__ & adaptable('IPrevNext') context = 'navbottom' order = 10 - def call(self, view=None): - self.cell_call(0, 0, view=view) + def init_rendering(self): + adapter = self.entity.cw_adapt_to('IPrevNext') + self.previous = adapter.previous_entity() + self.next = adapter.next_entity() + if not (self.previous or self.next): + raise EmptyComponent() - def cell_call(self, row, col, view=None): - entity = self.cw_rset.get_entity(row, col) - adapter = entity.cw_adapt_to('IPrevNext') - previous = adapter.previous_entity() - next = adapter.next_entity() - if previous or next: - textsize = self._cw.property_value('navigation.short-line-size') - self.w(u'
    ') - if previous: - self.previous_div(previous, textsize) - if next: - self.next_div(next, textsize) - self.w(u'
    ') - self.w(u'
    ') + def render_body(self, w): + w(u'
    ') + self.prevnext(w) + w(u'
    ') + w(u'
    ') + + def prevnext(self, w): + if self.previous: + self.prevnext_entity(w, self.previous, 'prev') + if self.next: + self.prevnext_entity(w, self.next, 'next') - def previous_div(self, previous, textsize): - self.w(u'
    ') - self.w(self.previous_link(previous, textsize)) - self.w(u'
    ') - self._cw.html_headers.add_raw('' - % xml_escape(previous.absolute_url())) - - def previous_link(self, previous, textsize): - return u'<< %s' % ( - xml_escape(previous.absolute_url()), - self._cw._('i18nprevnext_previous'), - xml_escape(cut(previous.dc_title(), textsize))) + def prevnext_entity(self, w, entity, type): + textsize = self._cw.property_value('navigation.short-line-size') + if type == 'prev': + title = self._cw._('i18nprevnext_previous') + icon = u'<< ' + cssclass = u'previousEntity left' + else: + title = self._cw._('i18nprevnext_next') + icon = u'>> ' + cssclass = u'nextEntity right' + self.prevnext_div(w, type, cssclass, entity.absolute_url(), + title, icon + xml_escape(cut(entity.dc_title(), textsize))) - def next_div(self, next, textsize): - self.w(u'
    ') - self.w(self.next_link(next, textsize)) - self.w(u'
    ') - self._cw.html_headers.add_raw('' - % xml_escape(next.absolute_url())) - - def next_link(self, next, textsize): - return u'%s >>' % ( - xml_escape(next.absolute_url()), - self._cw._('i18nprevnext_next'), - xml_escape(cut(next.dc_title(), textsize))) + def prevnext_div(self, w, type, cssclass, url, title, content): + w(u'
    ' % cssclass) + w(u'%s' % (xml_escape(url), + xml_escape(title), + content)) + w(u'
    ') + self._cw.html_headers.add_raw('' % ( + type, xml_escape(url))) def do_paginate(view, rset=None, w=None, show_all_option=True, page_size=None): diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/primary.py --- a/web/views/primary.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/primary.py Mon Sep 13 16:47:03 2010 +0200 @@ -25,6 +25,7 @@ from logilab.mtconverter import xml_escape from cubicweb import Unauthorized +from cubicweb.utils import support_args from cubicweb.selectors import match_kwargs from cubicweb.view import EntityView from cubicweb.schema import VIRTUAL_RTYPES, display_name @@ -89,7 +90,7 @@ def content_navigation_components(self, context): self.w(u'
    ' % context) - for comp in self._cw.vreg['contentnavigation'].poss_visible_objects( + for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects( self._cw, rset=self.cw_rset, row=self.cw_row, view=self, context=context): try: comp.render(w=self.w, row=self.cw_row, view=self) @@ -143,10 +144,15 @@ if display_attributes: self.w(u'') for rschema, role, dispctrl, value in display_attributes: - try: - self._render_attribute(dispctrl, rschema, value, - role=role, table=True) - except TypeError: + if support_args(self._render_attribute, 'label'): + label = self._rel_label(entity, rschema, role, dispctrl) + self._render_attribute(label, value, table=True) + elif support_args(self._render_attribute, 'dispctrl'): + warn('[3.10] _render_attribute prototype has changed, please' + ' update %s' % self.__class___, DeprecationWarning) + self._render_attribute(dispctrl, rschema, value, role=role, + table=True) + else: warn('[3.6] _render_attribute prototype has changed, please' ' update %s' % self.__class___, DeprecationWarning) self._render_attribute(rschema, value, role=role, table=True) @@ -166,9 +172,14 @@ continue rset = self._relation_rset(entity, rschema, role, dispctrl) if rset: - try: + if support_args(self._render_relation, 'label'): + label = self._rel_label(entity, rschema, role, dispctrl) + self._render_relation(label, dispctrl, rset, 'autolimited') + elif not support_args(self._render_relation, 'showlabel'): + warn('[3.10] _render_relation prototype has changed, ' + 'please update %s' % self.__class__, DeprecationWarning) self._render_relation(dispctrl, rset, 'autolimited') - except TypeError: + else: warn('[3.6] _render_relation prototype has changed, ' 'please update %s' % self.__class__, DeprecationWarning) self._render_relation(rset, dispctrl, 'autolimited', @@ -183,42 +194,45 @@ try: label, rset, vid, dispctrl = box except ValueError: - warn('[3.5] box views should now be defined as a 4-uple (label, rset, vid, dispctrl), ' - 'please update %s' % self.__class__.__name__, - DeprecationWarning) label, rset, vid = box dispctrl = {} + warn('[3.10] box views should now be a RsetBox instance, ' + 'please update %s' % self.__class__.__name__, + DeprecationWarning) self.w(u'') else: - try: - box.render(w=self.w, row=self.cw_row) - except NotImplementedError: - # much probably a context insensitive box, which only implements - # .call() and not cell_call() + try: + box.render(w=self.w, row=self.cw_row) + except NotImplementedError: + # much probably a context insensitive box, which only + # implements .call() and not cell_call() + # XXX shouldn't occurs with the new box system box.render(w=self.w) def _prepare_side_boxes(self, entity): sideboxes = [] + boxesreg = self._cw.vreg['ctxcomponents'] for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'): rset = self._relation_rset(entity, rschema, role, dispctrl) if not rset: continue - label = display_name(self._cw, rschema.type, role) - vid = dispctrl.get('vid', 'sidebox') - sideboxes.append( (label, rset, vid, dispctrl) ) - sideboxes += self._cw.vreg['boxes'].poss_visible_objects( - self._cw, rset=self.cw_rset, row=self.cw_row, view=self, - context='incontext') + label = self._rel_label(entity, rschema, role, dispctrl) + vid = dispctrl.get('vid', 'autolimited') + box = boxesreg.select('rsetbox', self._cw, rset=rset, + vid=vid, title=label, dispctrl=dispctrl, + context='incontext') + sideboxes.append(box) + sideboxes += boxesreg.poss_visible_objects( + self._cw, rset=self.cw_rset, row=self.cw_row, view=self, + context='incontext') # XXX since we've two sorted list, it may be worth using bisect def get_order(x): - if isinstance(x, tuple): - # x is a view box (label, rset, vid, dispctrl) - # default to 1000 so view boxes occurs after component boxes - return x[-1].get('order', 1000) - # x is a component box - return x.cw_propval('order') + if 'order' in x.cw_property_defs: + return x.cw_propval('order') + # default to 9999 so view boxes occurs after component boxes + return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999) return sorted(sideboxes, key=get_order) def _section_def(self, entity, where): @@ -251,25 +265,16 @@ rset = dispctrl['filter'](rset) return rset - def _render_relation(self, dispctrl, rset, defaultvid): + def _render_relation(self, label, dispctrl, rset, defaultvid): self.w(u'
    ') - if dispctrl.get('showlabel', self.show_rel_label): - self.w(u'

    %s

    ' % self._cw._(dispctrl['label'])) + if label: + self.w(u'

    %s

    ' % label) self.wview(dispctrl.get('vid', defaultvid), rset, initargs={'dispctrl': dispctrl}) self.w(u'
    ') - def _render_attribute(self, dispctrl, rschema, value, - role='subject', table=False): - if rschema.final: - showlabel = dispctrl.get('showlabel', self.show_attr_label) - else: - showlabel = dispctrl.get('showlabel', self.show_rel_label) - if dispctrl.get('label'): - label = self._cw._(dispctrl.get('label')) - else: - label = display_name(self._cw, rschema.type, role) - self.field(label, value, show_label=showlabel, tr=False, table=table) + def _render_attribute(self, label, value, table=False): + self.field(label, value, tr=False, table=table) def _rel_label(self, entity, rschema, role, dispctrl): if rschema.final: @@ -339,6 +344,6 @@ _pvs = uicfg.primaryview_section for rtype in ('eid', 'creation_date', 'modification_date', 'cwuri', 'is', 'is_instance_of', 'identity', 'owned_by', 'created_by', - 'require_permission', 'see_also'): + 'require_permission'): _pvs.tag_subject_of(('*', rtype, '*'), 'hidden') _pvs.tag_object_of(('*', rtype, '*'), 'hidden') diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/sessions.py --- a/web/views/sessions.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/sessions.py Mon Sep 13 16:47:03 2010 +0200 @@ -17,8 +17,8 @@ # with CubicWeb. If not, see . """web session component: by dfault the session is actually the db connection object :/ +""" -""" __docformat__ = "restructuredtext en" from cubicweb.web import InvalidSession @@ -51,9 +51,6 @@ if not sessionid in self._sessions: raise InvalidSession() session = self._sessions[sessionid] - if self.has_expired(session): - self.close_session(session) - raise InvalidSession() try: user = self.authmanager.validate_session(req, session) except InvalidSession: diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/startup.py --- a/web/views/startup.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/startup.py Mon Sep 13 16:47:03 2010 +0200 @@ -159,15 +159,14 @@ url = self._cw.build_url(etype) etypelink = u' %s (%d)' % ( xml_escape(url), label, nb) - yield (label, etypelink, self.add_entity_link(eschema, req)) + if eschema.has_perm(req, 'add'): + yield (label, etypelink, self.add_entity_link(etype)) - def add_entity_link(self, eschema, req): - """creates a [+] link for adding an entity if user has permission to do so""" - if not eschema.has_perm(req, 'add'): - return u'' + def add_entity_link(self, etype): + """creates a [+] link for adding an entity""" + url = self._cw.vreg["etypes"].etype_class(etype).cw_create_url(self._cw) return u'[+]' % ( - xml_escape(self.create_url(eschema.type)), - self._cw.__('add a %s' % eschema)) + xml_escape(url), self._cw.__('add a %s' % etype)) class IndexView(ManageView): diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/treeview.py --- a/web/views/treeview.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/treeview.py Mon Sep 13 16:47:03 2010 +0200 @@ -110,7 +110,7 @@ __regid__ = 'treeview' itemvid = 'treeitemview' subvid = 'oneline' - css_classes = 'treeview widget' + cssclass = 'treeview widget' title = _('tree view') def _init_params(self, subvid, treeid, initial_load, initial_thru_ajax, morekwargs): @@ -144,7 +144,7 @@ if toplevel: self._init_headers(treeid, toplevel_thru_ajax) ulid = ' id="tree-%s"' % treeid - self.w(u'' % (ulid, self.css_classes)) + self.w(u'' % (ulid, self.cssclass)) # XXX force sorting on x.sortvalue() (which return dc_title by default) # we need proper ITree & co specification to avoid this. # (pb when type ambiguity at the other side of the tree relation, @@ -171,7 +171,7 @@ """specific version of the treeview to display file trees """ __regid__ = 'filetree' - css_classes = 'treeview widget filetree' + cssclass = 'treeview widget filetree' title = _('file tree view') def call(self, subvid=None, treeid=None, initial_load=True, **kwargs): diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/workflow.py --- a/web/views/workflow.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/workflow.py Mon Sep 13 16:47:03 2010 +0200 @@ -25,6 +25,7 @@ _ = unicode import os +from warnings import warn from logilab.mtconverter import xml_escape from logilab.common.graph import escape @@ -160,15 +161,21 @@ displaycols=displaycols, headers=headers) -class WFHistoryVComponent(component.EntityVComponent): +class WFHistoryVComponent(component.CtxComponent): """display the workflow history for entities supporting it""" __regid__ = 'wfhistory' __select__ = WFHistoryView.__select__ & component.EntityVComponent.__select__ context = 'navcontentbottom' title = _('Workflow history') - def cell_call(self, row, col, view=None): - self.wview('wfhistory', self.cw_rset, row=row, col=col, view=view) + def render_body(self, w): + if hasattr(self, 'cell_call'): + warn('[3.10] %s should now implement render_body instead of cell_call', + DeprecationWarning, self.__class__) + self.w = w + self.cell_call(self.entity.cw_row, self.entity.cw_col) + else: + self.entity.view('wfhistory', w=w) # workflow actions ############################################################# diff -r dbb7ad04b963 -r 82d4011f54c1 web/views/xmlrss.py --- a/web/views/xmlrss.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/views/xmlrss.py Mon Sep 13 16:47:03 2010 +0200 @@ -29,7 +29,7 @@ from cubicweb.view import EntityView, EntityAdapter, AnyRsetView, Component from cubicweb.view import implements_adapter_compat from cubicweb.uilib import simple_sgml_tag -from cubicweb.web import httpcache, box +from cubicweb.web import httpcache, component # base xml views ############################################################## @@ -68,7 +68,7 @@ value = entity.eid else: try: - value = entity[attr] + value = entity.cw_attr_cache[attr] except KeyError: # Bytes continue @@ -148,25 +148,25 @@ return entity.cw_adapt_to('IFeed').rss_feed_url() -class RSSIconBox(box.BoxTemplate): +class RSSIconBox(component.CtxComponent): """just display the RSS icon on uniform result set""" __regid__ = 'rss' - __select__ = (box.BoxTemplate.__select__ + __select__ = (component.CtxComponent.__select__ & appobject_selectable('components', 'rss_feed_url')) visible = False order = 999 - def call(self, **kwargs): + def render(self, w, **kwargs): try: rss = self._cw.uiprops['RSS_LOGO'] except KeyError: self.error('missing RSS_LOGO external resource') return urlgetter = self._cw.vreg['components'].select('rss_feed_url', self._cw, - rset=self.cw_rset) + rset=self.cw_rset) url = urlgetter.feed_url() - self.w(u'rss\n' % (xml_escape(url), rss)) + w(u'rss\n' % (xml_escape(url), rss)) class RSSView(XMLView): diff -r dbb7ad04b963 -r 82d4011f54c1 web/webconfig.py --- a/web/webconfig.py Mon Sep 13 16:46:52 2010 +0200 +++ b/web/webconfig.py Mon Sep 13 16:47:03 2010 +0200 @@ -135,17 +135,6 @@ "Should be 0 or greater than repository\'s session-time.", 'group': 'web', 'level': 2, }), - ('cleanup-session-time', - {'type' : 'time', - 'default': '24h', - 'help': 'duration of inactivity after which a connection ' - 'will be closed, to limit memory consumption (avoid sessions that ' - 'never expire and cause memory leak when http-session-time is 0). ' - 'So even if http-session-time is 0 and the user don\'t close his ' - 'browser, he will have to reauthenticate after this time of ' - 'inactivity. Default to 24h.', - 'group': 'web', 'level': 3, - }), ('cleanup-anonymous-session-time', {'type' : 'time', 'default': '5min',