[web] Slight tweak on jQueryUI's colors on links (related to #4564046)
Follow up to commit 16f550b48d57 which was somewhat brutal. This change
only overrides jQueryUI's CSS rules inside tab panes and leaves actual
tab buttons alone.
# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""Generalities------------Paraphrasing the `emacs`_ documentation, let us say that hooks are an importantmechanism for customizing an application. A hook is basically a list offunctions to be called on some well-defined occasion (this is called `runningthe hook`)... _`emacs`: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.htmlHooks~~~~~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 moreconditions, hooks being selectable appobjects like views and components). Theyshould implement a :meth:`~cubicweb.server.hook.Hook.__call__` method that willbe called when the hook is triggered.There are two families of events: data events (before / after any individualupdate of an entity / or a relation in the repository) and server events (suchas server startup or shutdown). In a typical application, most of the hooks aredefined 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 rolled back.The purpose of data event hooks is usually to complement the data model asdefined in the schema, which is static by nature and only provide a restrictedbuiltin 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 attributesIt is functionally equivalent to a `database trigger`_, except that databasetriggers definition languages are not standardized, hence not portable (forinstance, PL/SQL works with Oracle and PostgreSQL but not SqlServer nor Sqlite)... _`database trigger`: http://en.wikipedia.org/wiki/Database_trigger.. hint:: It is a good practice to write unit tests for each hook. See an example in :ref:`hook_test`Operations~~~~~~~~~~Operations are subclasses of the :class:`~cubicweb.server.hook.Operation` classthat may be created by hooks and scheduled to happen on `precommit`,`postcommit` or `rollback` event (i.e. respectivly before/after a commit orbefore a rollback of a transaction).Hooks are being fired immediately on data operations, and it is sometimenecessary to delay the actual work down to a time where we can expect allinformation to be there, or when all other hooks have run (though take casesince operations may themselves trigger hooks). Also while the order ofexecution of hooks is data dependant (and thus hard to predict), it is possibleto force an order on operations.So, for such case where you may miss some information that may be set later inthe transaction, you should instantiate an operation in the hook.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.Events------Hooks are mostly defined and used to handle `dataflow`_ operations. Itmeans as data gets in (entities added, updated, relations set orunset), specific events are issued and the Hooks matching these eventsare called.You can get the event that triggered a hook by accessing its `event`attribute... _`dataflow`: http://en.wikipedia.org/wiki/DataflowEntity modification related events~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~When called for one of these events, hook will have an `entity` attributecontaining the entity instance.- `before_add_entity`, `before_update_entity`: On those events, you can access the modified attributes of the entity using the `entity.cw_edited` dictionary. The values can be modified and the old values can be retrieved. If you modify the `entity.cw_edited` dictionary in the hook, that is before the database operations take place, you will avoid the need to process a whole new rql query and the underlying backend query (eg usually sql) will contain the modified data. For example: .. sourcecode:: python self.entity.cw_edited['age'] = 42 will modify the age before it is written to the backend storage. Similarly, removing an attribute from `cw_edited` will cancel its modification: .. sourcecode:: python del self.entity.cw_edited['age'] On a `before_update_entity` event, you can access the old and new values: .. sourcecode:: python old, new = entity.cw_edited.oldnewvalue('age')- `after_add_entity`, `after_update_entity` On those events, you can get the list of attributes that were modified using the `entity.cw_edited` dictionary, but you can not modify it or get the old value of an attribute.- `before_delete_entity`, `after_delete_entity` On those events, the entity has no `cw_edited` dictionary... note:: `self.entity.cw_set(age=42)` will set the `age` attribute to 42. But to do so, it will generate a rql query that will have to be processed, hence may trigger some hooks, etc. This could lead to infinitely looping hooks.Relation modification related events~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~When called for one of these events, hook will have `eidfrom`, `rtype`, `eidto`attributes containing respectively the eid of the subject entity, the relationtype and the eid of the object entity.* `before_add_relation`, `before_delete_relation` On those events, you can still get the original relation by issuing a rql query.* `after_add_relation`, `after_delete_relation`Specific selectors are shipped for these kinds of events, see in particular:class:`~cubicweb.server.hook.match_rtype`.Also note that relations can be added or deleted, but not updated.Non data events~~~~~~~~~~~~~~~Hooks called on server start/maintenance/stop event (e.g.`server_startup`, `server_maintenance`, `before_server_shutdown`,`server_shutdown`) have a `repo` attribute, but *their `_cw` attributeis None*. The `server_startup` is called on regular startup, while`server_maintenance` is called on cubicweb-ctl upgrade or shellcommands. `server_shutdown` is called anyway but connections to thenative source is impossible; `before_server_shutdown` handles that.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 nospecial attribute.API---Hooks control~~~~~~~~~~~~~It is sometimes convenient to explicitly enable or disable some hooks. Forinstance if you want to disable some integrity checking hook. This can becontrolled more finely through the `category` class attribute, which is a stringgiving a category name. One can then uses the:meth:`~cubicweb.server.session.Connection.deny_all_hooks_but` and:meth:`~cubicweb.server.session.Connection.allow_all_hooks_but` context managers toexplicitly enable or disable some categories.The existing categories are:* ``security``, security checking hooks* ``worfklow``, workflow handling hooks* ``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 hooksNothing precludes one to invent new categories and use existing mechanisms tofilter them in or out.Hooks specific predicates~~~~~~~~~~~~~~~~~~~~~~~~~.. autoclass:: cubicweb.server.hook.match_rtype.. autoclass:: cubicweb.server.hook.match_rtype_setsHooks and operations classes~~~~~~~~~~~~~~~~~~~~~~~~~~~~.. autoclass:: cubicweb.server.hook.Hook.. autoclass:: cubicweb.server.hook.Operation.. autoclass:: cubicweb.server.hook.LateOperation.. autoclass:: cubicweb.server.hook.DataOperationMixIn"""__docformat__="restructuredtext en"fromwarningsimportwarnfromloggingimportgetLoggerfromitertoolsimportchainfromlogilab.common.decoratorsimportclassproperty,cachedfromlogilab.common.deprecationimportdeprecated,class_renamedfromlogilab.common.logging_extimportset_log_methodsfromlogilab.common.registryimport(NotPredicate,OrPredicate,objectify_predicate)fromcubicwebimportRegistryNotFound,serverfromcubicweb.cwvregimportCWRegistry,CWRegistryStorefromcubicweb.predicatesimportExpectedValuePredicate,is_instancefromcubicweb.appobjectimportAppObjectENTITIES_HOOKS=set(('before_add_entity','after_add_entity','before_update_entity','after_update_entity','before_delete_entity','after_delete_entity'))RELATIONS_HOOKS=set(('before_add_relation','after_add_relation','before_delete_relation','after_delete_relation'))SYSTEM_HOOKS=set(('server_backup','server_restore','server_startup','server_maintenance','server_shutdown','before_server_shutdown','session_open','session_close'))ALL_HOOKS=ENTITIES_HOOKS|RELATIONS_HOOKS|SYSTEM_HOOKSdef_iter_kwargs(entities,eids_from_to,kwargs):ifnotentitiesandnoteids_from_to:yieldkwargselifentities:forentityinentities:kwargs['entity']=entityyieldkwargselse:forsubject,objectineids_from_to:kwargs.update({'eidfrom':subject,'eidto':object})yieldkwargsclassHooksRegistry(CWRegistry):defregister(self,obj,**kwargs):obj.check_events()super(HooksRegistry,self).register(obj,**kwargs)defcall_hooks(self,event,cnx=None,**kwargs):"""call `event` hooks for an entity or a list of entities (passed respectively as the `entity` or ``entities`` keyword argument). """kwargs['event']=eventifcnxisNone:# True for events such as server_startforhookinsorted(self.possible_objects(cnx,**kwargs),key=lambdax:x.order):hook()else:if'entities'inkwargs:assert'entity'notinkwargs, \'can\'t pass "entities" and "entity" arguments simultaneously'assert'eids_from_to'notinkwargs, \'can\'t pass "entities" and "eids_from_to" arguments simultaneously'entities=kwargs.pop('entities')eids_from_to=[]elif'eids_from_to'inkwargs:entities=[]eids_from_to=kwargs.pop('eids_from_to')else:entities=[]eids_from_to=[]pruned=self.get_pruned_hooks(cnx,event,entities,eids_from_to,kwargs)# by default, hooks are executed with security turned offwithcnx.security_enabled(read=False):for_kwargsin_iter_kwargs(entities,eids_from_to,kwargs):hooks=sorted(self.filtered_possible_objects(pruned,cnx,**_kwargs),key=lambdax:x.order)debug=server.DEBUG&server.DBG_HOOKSwithcnx.security_enabled(write=False):forhookinhooks:ifdebug:printevent,_kwargs,hookhook()defget_pruned_hooks(self,cnx,event,entities,eids_from_to,kwargs):"""return a set of hooks that should not be considered by filtered_possible objects the idea is to make a first pass over all the hooks in the registry and to mark put some of them in a pruned list. The pruned hooks are the one which: * are disabled at the connection level * have a selector containing a :class:`match_rtype` or an :class:`is_instance` predicate which does not match the rtype / etype of the relations / entities for which we are calling the hooks. This works because the repository calls the hooks grouped by rtype or by etype when using the entities or eids_to_from keyword arguments Only hooks with a simple predicate or an AndPredicate of simple predicates are considered for disabling. """if'entity'inkwargs:entities=[kwargs['entity']]iflen(entities):look_for_selector=is_instanceetype=entities[0].__regid__elif'rtype'inkwargs:look_for_selector=match_rtypeetype=Noneelse:# nothing to prune, how did we get there ???returnset()cache_key=(event,kwargs.get('rtype'),etype)pruned=cnx.pruned_hooks_cache.get(cache_key)ifprunedisnotNone:returnprunedpruned=set()cnx.pruned_hooks_cache[cache_key]=prunediflook_for_selectorisnotNone:forid,hooksinself.iteritems():forhookinhooks:enabled_cat,main_filter=hook.filterable_selectors()ifenabled_catisnotNone:ifnotenabled_cat(hook,cnx):pruned.add(hook)continueifmain_filterisnotNone:ifisinstance(main_filter,match_rtype)and \(main_filter.frometypesisnotNoneor \main_filter.toetypesisnotNone):continuefirst_kwargs=_iter_kwargs(entities,eids_from_to,kwargs).next()ifnotmain_filter(hook,cnx,**first_kwargs):pruned.add(hook)returnpruneddeffiltered_possible_objects(self,pruned,*args,**kwargs):forappobjectsinself.itervalues():ifpruned:filtered_objects=[objforobjinappobjectsifobjnotinpruned]ifnotfiltered_objects:continueelse:filtered_objects=appobjectsobj=self._select_best(filtered_objects,*args,**kwargs)ifobjisNone:continueyieldobjclassHooksManager(object):def__init__(self,vreg):self.vreg=vregdefcall_hooks(self,event,cnx=None,**kwargs):try:registry=self.vreg['%s_hooks'%event]exceptRegistryNotFound:return# no hooks for this eventregistry.call_hooks(event,cnx,**kwargs)foreventinALL_HOOKS:CWRegistryStore.REGISTRY_FACTORY['%s_hooks'%event]=HooksRegistry@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')defentity_oldnewvalue(entity,attr):returnentity.cw_edited.oldnewvalue(attr)# some hook specific predicates #################################################@objectify_predicatedefenabled_category(cls,req,**kwargs):ifreqisNone:returnTrue# XXX how to deactivate server startup / shutdown eventreturnreq.is_hook_activated(cls)@objectify_predicatedeffrom_dbapi_query(cls,req,**kwargs):ifreq.running_dbapi_query:return1return0classrechain(object):def__init__(self,*iterators):self.iterators=iteratorsdef__iter__(self):returniter(chain(*self.iterators))classmatch_rtype(ExpectedValuePredicate):"""accept if the relation type is found in expected ones. Optional named parameters `frometypes` and `toetypes` can be used to restrict target subject and/or object entity types of the relation. :param \*expected: possible relation types :param frometypes: candidate entity types as subject of relation :param toetypes: candidate entity types as object of relation """def__init__(self,*expected,**more):self.expected=expectedself.frometypes=more.pop('frometypes',None)self.toetypes=more.pop('toetypes',None)assertnotmore,"unexpected kwargs in match_rtype: %s"%moredef__call__(self,cls,req,*args,**kwargs):ifkwargs.get('rtype')notinself.expected:return0ifself.frometypesisnotNoneand \req.entity_metas(kwargs['eidfrom'])['type']notinself.frometypes:return0ifself.toetypesisnotNoneand \req.entity_metas(kwargs['eidto'])['type']notinself.toetypes:return0return1classmatch_rtype_sets(ExpectedValuePredicate):"""accept if the relation type is in one of the sets given as initializer argument. The goal of this predicate is that it keeps reference to original sets, so modification to thoses sets are considered by the predicate. For instance .. sourcecode:: python MYSET = set() class Hook1(Hook): __regid__ = 'hook1' __select__ = Hook.__select__ & match_rtype_sets(MYSET) ... class Hook2(Hook): __regid__ = 'hook2' __select__ = Hook.__select__ & match_rtype_sets(MYSET) Client code can now change `MYSET`, this will changes the selection criteria of :class:`Hook1` and :class:`Hook1`. """def__init__(self,*expected):self.expected=expecteddef__call__(self,cls,req,*args,**kwargs):forrel_setinself.expected:ifkwargs.get('rtype')inrel_set:return1return0# base class for hook ##########################################################classHook(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 connection. 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__ & is_instance('Person') else your hooks will be called madly, whatever the event. """__select__=enabled_category()# set this in derivated classesevents=Nonecategory=Noneorder=0# stop pylint from complaining about missing attributes in Hooks classeseidfrom=eidto=entity=rtype=repo=None@classmethod@cacheddeffilterable_selectors(cls):search=cls.__select__.search_selectorifsearch((NotPredicate,OrPredicate)):returnNone,Noneenabled_cat=search(enabled_category)main_filter=search((is_instance,match_rtype))returnenabled_cat,main_filter@classmethoddefcheck_events(cls):try:foreventincls.events:ifeventnotinALL_HOOKS:raiseException('bad event %s on %s.%s'%(event,cls.__module__,cls.__name__))exceptAttributeError:raiseexceptTypeError:raiseException('bad .events attribute %s on %s.%s'%(cls.events,cls.__module__,cls.__name__))@classmethoddef__registered__(cls,reg):cls.check_events()@classpropertydef__registries__(cls):ifcls.eventsisNone:return[]return['%s_hooks'%evforevincls.events]known_args=set(('entity','rtype','eidfrom','eidto','repo','timestamp'))def__init__(self,req,event,**kwargs):forarginself.known_args:ifarginkwargs:setattr(self,arg,kwargs.pop(arg))super(Hook,self).__init__(req,**kwargs)self.event=eventset_log_methods(Hook,getLogger('cubicweb.hook'))# abtract hooks for relation propagation ######################################## See example usage in hooks of the nosylist cubeclassPropagateRelationHook(Hook):"""propagate some `main_rtype` relation on entities linked as object of `subject_relations` or as subject of `object_relations` (the watched relations). This hook ensure that when one of the watched relation is added, the `main_rtype` relation is added to the target entity of the relation. Notice there are no default behaviour defined when a watched relation is deleted, you'll have to handle this by yourself. You usually want to use the :class:`match_rtype_sets` predicate on concrete classes. """events=('after_add_relation',)# to set in concrete classmain_rtype=Nonesubject_relations=Noneobject_relations=Nonedef__call__(self):assertself.main_rtypeforeidin(self.eidfrom,self.eidto):etype=self._cw.entity_metas(eid)['type']ifself.main_rtypenotinself._cw.vreg.schema.eschema(etype).subjrels:returnifself.rtypeinself.subject_relations:meid,seid=self.eidfrom,self.eidtoelse:assertself.rtypeinself.object_relationsmeid,seid=self.eidto,self.eidfromself._cw.execute('SET E %s P WHERE X %s P, X eid %%(x)s, E eid %%(e)s, NOT E %s P'%(self.main_rtype,self.main_rtype,self.main_rtype),{'x':meid,'e':seid})classPropagateRelationAddHook(Hook):"""Propagate to entities at the end of watched relations when a `main_rtype` relation is added. `subject_relations` and `object_relations` attributes should be specified on subclasses and are usually shared references with attributes of the same name on :class:`PropagateRelationHook`. Because of those shared references, you can use `skip_subject_relations` and `skip_object_relations` attributes when you don't want to propagate to entities linked through some particular relations. """events=('after_add_relation',)# to set in concrete class (mandatory)subject_relations=Noneobject_relations=None# to set in concrete class (optionaly)skip_subject_relations=()skip_object_relations=()def__call__(self):eschema=self._cw.vreg.schema.eschema(self._cw.entity_metas(self.eidfrom)['type'])execute=self._cw.executeforrelinself.subject_relations:ifrelineschema.subjrelsandnotrelinself.skip_subject_relations:execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''X %s R, NOT R %s P'%(self.rtype,rel,self.rtype),{'x':self.eidfrom,'p':self.eidto})forrelinself.object_relations:ifrelineschema.objrelsandnotrelinself.skip_object_relations:execute('SET R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''R %s X, NOT R %s P'%(self.rtype,rel,self.rtype),{'x':self.eidfrom,'p':self.eidto})classPropagateRelationDelHook(PropagateRelationAddHook):"""Propagate to entities at the end of watched relations when a `main_rtype` relation is deleted. This is the opposite of the :class:`PropagateRelationAddHook`, see its documentation for how to use this class. """events=('after_delete_relation',)def__call__(self):eschema=self._cw.vreg.schema.eschema(self._cw.entity_metas(self.eidfrom)['type'])execute=self._cw.executeforrelinself.subject_relations:ifrelineschema.subjrelsandnotrelinself.skip_subject_relations:execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''X %s R'%(self.rtype,rel),{'x':self.eidfrom,'p':self.eidto})forrelinself.object_relations:ifrelineschema.objrelsandnotrelinself.skip_object_relations:execute('DELETE R %s P WHERE X eid %%(x)s, P eid %%(p)s, ''R %s X'%(self.rtype,rel),{'x':self.eidfrom,'p':self.eidto})# abstract classes for operation ###############################################classOperation(object):"""Base class for operations. Operation may be instantiated in the hooks' `__call__` method. It always takes a connection object as first argument (accessible as `.cnx` 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 set events related to commit / rollback transations. Possible events are: * `precommit`: 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 * `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`: the transaction has been either rolled back either: * intentionaly * a 'precommit' event failed, in which case all operations are rolled back once 'revertprecommit'' has been called * `postcommit`: 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 connection, which you will need to commit. For an operation to support an event, one has to implement the `<event name>_event` method with no arguments. The 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,cnx,**kwargs):self.cnx=cnxself.__dict__.update(kwargs)self.register(cnx)# execution informationself.processed=None# 'precommit', 'commit'self.failed=False@property@deprecated('[3.19] Operation.session is deprecated, use Operation.cnx instead')defsession(self):returnself.cnxdefregister(self,cnx):cnx.add_operation(self,self.insert_index())definsert_index(self):"""return the index of the latest instance which is not a LateOperation instance """# faster by inspecting operation in reverse order for heavy transactionsi=Nonefori,opinenumerate(reversed(self.cnx.pending_operations)):ifisinstance(op,(LateOperation,SingleLastOperation)):continuereturn-iorNoneifiisNone:returnNonereturn-(i+1)defhandle_event(self,event):"""delegate event handling to the opertaion"""ifevent=='postcommit_event'andhasattr(self,'commit_event'):warn('[3.10] %s: commit_event method has been replaced by postcommit_event'%self.__class__,DeprecationWarning)self.commit_event()# pylint: disable=E1101getattr(self,event)()defprecommit_event(self):"""the observed connections set is preparing a commit"""defrevertprecommit_event(self):"""an error went when pre-commiting this operation or a later one should revert pre-commit's changes but take care, they may have not been all considered if it's this operation which failed """defrollback_event(self):"""the observed connections set has been rolled back do nothing by default """defpostcommit_event(self):"""the observed connections set has committed"""# these are overridden by set_log_methods below# only defining here to prevent pylint from complaininginfo=warning=error=critical=exception=debug=lambdamsg,*a,**kw:Noneset_log_methods(Operation,getLogger('cubicweb.session'))def_container_add(container,value):{set:set.add,list:list.append}[container.__class__](container,value)classDataOperationMixIn(object):"""Mix-in class to ease applying a single operation on a set of data, avoiding to create as many as operation as they are individual modification. The body of the operation must then iterate over the values that have been stored in a single operation instance. You should try to use this instead of creating on operation for each `value`, since handling operations becomes costly on massive data import. Usage looks like: .. sourcecode:: python class MyEntityHook(Hook): __regid__ = 'my.entity.hook' __select__ = Hook.__select__ & is_instance('MyEntity') events = ('after_add_entity',) def __call__(self): MyOperation.get_instance(self._cw).add_data(self.entity) class MyOperation(DataOperationMixIn, Operation): def precommit_event(self): for bucket in self.get_data(): process(bucket) You can modify the `containercls` class attribute, which defines the container class that should be instantiated to hold payloads. An instance is created on instantiation, and then the :meth:`add_data` method will add the given data to the existing container. Default to a `set`. Give `list` if you want to keep arrival ordering. You can also use another kind of container by redefining :meth:`_build_container` and :meth:`add_data` More optional parameters can be given to the `get_instance` operation, that will be given to the operation constructor (for obvious reasons those parameters should not vary accross different calls to this method for a given operation). .. Note:: For sanity reason `get_data` will reset the operation, so that once the operation has started its treatment, if some hook want to push additional data to this same operation, a new instance will be created (else that data has a great chance to be never treated). This implies: * you should **always** call `get_data` when starting treatment * you should **never** call `get_data` for another reason. """containercls=set@classpropertydefdata_key(cls):return('cw.dataops',cls.__name__)@classmethoddefget_instance(cls,cnx,**kwargs):# no need to lock: transaction_data already comes from thread's local storagetry:returncnx.transaction_data[cls.data_key]exceptKeyError:op=cnx.transaction_data[cls.data_key]=cls(cnx,**kwargs)returnopdef__init__(self,*args,**kwargs):super(DataOperationMixIn,self).__init__(*args,**kwargs)self._container=self._build_container()self._processed=Falsedef__contains__(self,value):returnvalueinself._containerdef_build_container(self):returnself.containercls()defunion(self,data):"""only when container is a set"""assertnotself._processed,"""Trying to add data to a closed operation.Iterating over operation data closed it and should be reserved to precommit /postcommit method of the operation."""self._container|=datadefadd_data(self,data):assertnotself._processed,"""Trying to add data to a closed operation.Iterating over operation data closed it and should be reserved to precommit /postcommit method of the operation."""_container_add(self._container,data)defremove_data(self,data):assertnotself._processed,"""Trying to add data to a closed operation.Iterating over operation data closed it and should be reserved to precommit /postcommit method of the operation."""self._container.remove(data)defget_data(self):assertnotself._processed,"""Trying to get data from a closed operation.Iterating over operation data closed it and should be reserved to precommit /postcommit method of the operation."""self._processed=Trueop=self.cnx.transaction_data.pop(self.data_key)assertopisself,"Bad handling of operation data, found %s instead of %s for key %s"%(op,self,self.data_key)returnself._container@deprecated('[3.10] use opcls.get_instance(cnx, **opkwargs).add_data(value)')defset_operation(cnx,datakey,value,opcls,containercls=set,**opkwargs):"""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: * `cnx`, the current connection * `datakey`, a specially forged key that will be used as key in cnx.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) * `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 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 cnx.transaction_data[`datakey`] (expected to be a set):# if found, simply append `value`_container_add(cnx.transaction_data[datakey],value)exceptKeyError:# else, initialize it to containercls([`value`]) and instantiate the given# `opcls` operation class with additional keyword argumentsopcls(cnx,**opkwargs)cnx.transaction_data[datakey]=containercls()_container_add(cnx.transaction_data[datakey],value)classLateOperation(Operation):"""special operation which should be called after all possible (ie non late) operations """definsert_index(self):"""return the index of the lastest instance which is not a SingleLastOperation instance """# faster by inspecting operation in reverse order for heavy transactionsi=Nonefori,opinenumerate(reversed(self.cnx.pending_operations)):ifisinstance(op,SingleLastOperation):continuereturn-iorNoneifiisNone:returnNonereturn-(i+1)classSingleLastOperation(Operation):"""special operation which should be called once and after all other operations """defregister(self,cnx):"""override register to handle cases where this operation has already been added """operations=cnx.pending_operationsindex=self.equivalent_index(operations)ifindexisnotNone:equivalent=operations.pop(index)else:equivalent=Nonecnx.add_operation(self,self.insert_index())returnequivalentdefequivalent_index(self,operations):"""return the index of the equivalent operation if any"""fori,opinenumerate(reversed(operations)):ifop.__class__isself.__class__:return-(i+1)returnNonedefinsert_index(self):returnNoneclassSendMailOp(SingleLastOperation):def__init__(self,cnx,msg=None,recipients=None,**kwargs):# may not specify msg yet, as# `cubicweb.sobjects.supervision.SupervisionMailOp`ifmsgisnotNone:assertrecipientsself.to_send=[(msg,recipients)]else:assertrecipientsisNoneself.to_send=[]super(SendMailOp,self).__init__(cnx,**kwargs)defregister(self,cnx):previous=super(SendMailOp,self).register(cnx)ifprevious:self.to_send=previous.to_send+self.to_senddefpostcommit_event(self):self.cnx.repo.threaded_task(self.sendmails)defsendmails(self):self.cnx.vreg.config.sendmails(self.to_send)classRQLPrecommitOperation(Operation):# to be defined in concrete classesrqls=Nonedefprecommit_event(self):execute=self.cnx.executeforrqlinself.rqls:execute(*rql)classCleanupNewEidsCacheOp(DataOperationMixIn,SingleLastOperation):"""on rollback of a insert query we have to remove from repository's type/source cache eids of entities added in that transaction. NOTE: querier's rqlst/solutions cache may have been polluted too with queries such as Any X WHERE X eid 32 if 32 has been rolled back however generated queries are unpredictable and analysing all the cache probably too expensive. Notice that there is no pb when using args to specify eids instead of giving them into the rql string. """data_key='neweids'defrollback_event(self):"""the observed connections set has been rolled back, remove inserted eid from repository type/source cache """try:self.cnx.repo.clear_caches(self.get_data())exceptKeyError:passclassCleanupDeletedEidsCacheOp(DataOperationMixIn,SingleLastOperation):"""on commit of delete query, we have to remove from repository's type/source cache eids of entities deleted in that transaction. """data_key='pendingeids'defpostcommit_event(self):"""the observed connections set has been rolled back, remove inserted eid from repository type/source cache """try:eids=self.get_data()self.cnx.repo.clear_caches(eids)self.cnx.repo.app_instances_bus.publish(['delete']+list(str(eid)foreidineids))exceptKeyError:pass