# HG changeset patch # User Sylvain Thénault # Date 1270042749 -7200 # Node ID a63d7886fcf528ea2836761e0f11537036c39e88 # Parent d6fd82a5a4e8d2b7ba7e9e2f2e8a87e66cce0581# Parent 3c58429f8b5c8411165072d87a0124f4173735cc merge diff -r d6fd82a5a4e8 -r a63d7886fcf5 .hgtags --- a/.hgtags Tue Mar 30 14:32:03 2010 +0200 +++ b/.hgtags Wed Mar 31 15:39:09 2010 +0200 @@ -115,3 +115,5 @@ f9fce56d6a0c2bc6c4b497b66039a8bbbbdc8074 cubicweb-debian-version-3.6.3-1 d010f749c21d55cd85c5feb442b9cf816282953c cubicweb-version-3.7.2 8fda29a6c2191ba3cc59242c17b28b34127c75fa cubicweb-debian-version-3.7.2-1 +768beb8e15f15e079f8ee6cfc35125e12b19e140 cubicweb-version-3.7.3 +44c7bf90df71dd562e5a7be5ced3019da603d24f cubicweb-debian-version-3.7.3-1 diff -r d6fd82a5a4e8 -r a63d7886fcf5 MANIFEST.in --- a/MANIFEST.in Tue Mar 30 14:32:03 2010 +0200 +++ b/MANIFEST.in Wed Mar 31 15:39:09 2010 +0200 @@ -15,7 +15,7 @@ recursive-include etwist *.xml *.html recursive-include i18n *.pot *.po -recursive-include schemas *.py *.sql.* +recursive-include schemas *.py *.sql recursive-include entities/test/data * recursive-include sobjects/test/data * diff -r d6fd82a5a4e8 -r a63d7886fcf5 __pkginfo__.py --- a/__pkginfo__.py Tue Mar 30 14:32:03 2010 +0200 +++ b/__pkginfo__.py Wed Mar 31 15:39:09 2010 +0200 @@ -7,7 +7,7 @@ modname = distname = "cubicweb" -numversion = (3, 7, 2) +numversion = (3, 7, 3) version = '.'.join(str(num) for num in numversion) description = "a repository of entities / relations for knowledge management" @@ -95,8 +95,9 @@ [join(_data_dir, fname) for fname in listdir(_data_dir) if not isdir(join(_data_dir, fname))]], [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'timeline'), - [join(_data_dir, 'timeline', fname) - for fname in listdir(join(_data_dir, 'timeline'))]], + [join(data_dir, 'timeline', fname) for fname in listdir(join(data_dir, 'timeline'))]], + [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'images'), + [join(data_dir, 'images', fname) for fname in listdir(join(data_dir, 'images'))]], [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc'), [join(_wdoc_dir, fname) for fname in listdir(_wdoc_dir) if not isdir(join(_wdoc_dir, fname))]], diff -r d6fd82a5a4e8 -r a63d7886fcf5 appobject.py --- a/appobject.py Tue Mar 30 14:32:03 2010 +0200 +++ b/appobject.py Wed Mar 31 15:39:09 2010 +0200 @@ -14,6 +14,7 @@ from warnings import warn from logilab.common.deprecation import deprecated +from logilab.common.decorators import classproperty from logilab.common.logging_ext import set_log_methods @@ -245,6 +246,12 @@ __regid__ = None __select__ = yes() + @classproperty + def __registries__(cls): + if cls.__registry__ is None: + return () + return (cls.__registry__,) + @classmethod def __registered__(cls, registry): """called by the registry when the appobject has been registered. diff -r d6fd82a5a4e8 -r a63d7886fcf5 cwconfig.py --- a/cwconfig.py Tue Mar 30 14:32:03 2010 +0200 +++ b/cwconfig.py Wed Mar 31 15:39:09 2010 +0200 @@ -337,7 +337,7 @@ def available_cubes(cls): cubes = set() for directory in cls.cubes_search_path(): - if not os.path.exists(directory): + if not exists(directory): cls.error('unexistant directory in cubes search path: %s' % directory) continue diff -r d6fd82a5a4e8 -r a63d7886fcf5 cwctl.py --- a/cwctl.py Tue Mar 30 14:32:03 2010 +0200 +++ b/cwctl.py Wed Mar 31 15:39:09 2010 +0200 @@ -725,7 +725,7 @@ # handle i18n upgrade: # * install new languages # * recompile catalogs - # in the first componant given + # XXX search available language in the first cube given from cubicweb import i18n templdir = cwcfg.cube_dir(config.cubes()[0]) langs = [lang for lang, _ in i18n.available_catalogs(join(templdir, 'i18n'))] @@ -740,7 +740,12 @@ print print '-> instance migrated.' if not (CWDEV or self.config.nostartstop): - StartInstanceCommand().start_instance(appid) + # restart instance through fork to get a proper environment, avoid + # uicfg pb (and probably gettext catalogs, to check...) + forkcmd = '%s start %s' % (sys.argv[0], appid) + status = system(forkcmd) + if status: + print '%s exited with status %s' % (forkcmd, status) print diff -r d6fd82a5a4e8 -r a63d7886fcf5 dataimport.py --- a/dataimport.py Tue Mar 30 14:32:03 2010 +0200 +++ b/dataimport.py Wed Mar 31 15:39:09 2010 +0200 @@ -498,7 +498,7 @@ err = func(buckets) if err: self.errors[title] = (help, err) - self.store.commit() + txuuid = self.store.commit() self._print_stats() if self.errors: if self.askerror == 2 or (self.askerror and confirm('Display errors ?')): @@ -506,7 +506,8 @@ for errkey, error in self.errors.items(): self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1]))) self.tell(pformat(sorted(error[1]))) - + if txuuid is not None: + print 'transaction id:', txuuid def _print_stats(self): nberrors = sum(len(err[1]) for err in self.errors.values()) self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors' diff -r d6fd82a5a4e8 -r a63d7886fcf5 dbapi.py --- a/dbapi.py Tue Mar 30 14:32:03 2010 +0200 +++ b/dbapi.py Wed Mar 31 15:39:09 2010 +0200 @@ -367,6 +367,16 @@ return '' % self.sessionid return '' % self.sessionid + def __enter__(self): + return self.cursor() + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is None: + self.commit() + else: + self.rollback() + return False #propagate the exception + def request(self): return DBAPIRequest(self.vreg, self) diff -r d6fd82a5a4e8 -r a63d7886fcf5 debian/changelog --- a/debian/changelog Tue Mar 30 14:32:03 2010 +0200 +++ b/debian/changelog Wed Mar 31 15:39:09 2010 +0200 @@ -1,3 +1,9 @@ +cubicweb (3.7.3-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault Wed, 31 Mar 2010 14:55:21 +0200 + cubicweb (3.7.2-1) unstable; urgency=low * new upstream release diff -r d6fd82a5a4e8 -r a63d7886fcf5 devtools/testlib.py --- a/devtools/testlib.py Tue Mar 30 14:32:03 2010 +0200 +++ b/devtools/testlib.py Wed Mar 31 15:39:09 2010 +0200 @@ -14,6 +14,7 @@ import re from urllib import unquote from math import log +from contextlib import contextmanager import simplejson @@ -357,6 +358,15 @@ def entity(self, rql, args=None, eidkey=None, req=None): return self.execute(rql, args, eidkey, req=req).get_entity(0, 0) + @contextmanager + def temporary_appobjects(self, *appobjects): + self.vreg._loadedmods.setdefault(self.__module__, {}) + for obj in appobjects: + self.vreg.register(obj) + yield + for obj in appobjects: + self.vreg.unregister(obj) + # vregistry inspection utilities ########################################### def pviews(self, req, rset): diff -r d6fd82a5a4e8 -r a63d7886fcf5 entity.py --- a/entity.py Tue Mar 30 14:32:03 2010 +0200 +++ b/entity.py Wed Mar 31 15:39:09 2010 +0200 @@ -298,6 +298,12 @@ 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 rql_set_value(self, attr, value): """call by rql execution plan when some attribute is modified @@ -874,17 +880,21 @@ # raw edition utilities ################################################### def set_attributes(self, **kwargs): + _check_cw_unsafe(kwargs) assert kwargs - _check_cw_unsafe(kwargs) + assert self._is_saved relations = [] for key in kwargs: relations.append('X %s %%(%s)s' % (key, key)) - # update current local object - self.update(kwargs) # and now update the database kwargs['x'] = self.eid self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations), kwargs, 'x') + 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) def set_relations(self, **kwargs): """add relations to the given object. To set a relation where this entity diff -r d6fd82a5a4e8 -r a63d7886fcf5 etwist/service.py --- a/etwist/service.py Tue Mar 30 14:32:03 2010 +0200 +++ b/etwist/service.py Wed Mar 31 15:39:09 2010 +0200 @@ -17,7 +17,6 @@ os.environ['CW_INSTANCES_DIR'] = r'C:\etc\cubicweb.d' os.environ['USERNAME'] = 'cubicweb' - class CWService(object, win32serviceutil.ServiceFramework): _svc_name_ = None _svc_display_name_ = None @@ -25,7 +24,6 @@ def __init__(self, *args, **kwargs): win32serviceutil.ServiceFramework.__init__(self, *args, **kwargs) - self._stop_event = win32event.CreateEvent(None, 0, 0, None) cwcfg.load_cwctl_plugins() set_log_methods(CubicWebRootResource, logger) server.parsePOSTData = parsePOSTData @@ -33,8 +31,8 @@ def SvcStop(self): self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) logger.info('stopping %s service' % self.instance) - win32event.SetEvent(self._stop_event) - self.ReportServiceStatus(win32service.SERVICE_STOPPED) + reactor.stop() + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) def SvcDoRun(self): self.ReportServiceStatus(win32service.SERVICE_START_PENDING) @@ -57,5 +55,4 @@ except Exception, e: logger.error('service %s stopped (cause: %s)' % (self.instance, e)) logger.exception('what happened ...') - self.SvcStop() - + self.ReportServiceStatus(win32service.SERVICE_STOPPED) diff -r d6fd82a5a4e8 -r a63d7886fcf5 hooks/__init__.py --- a/hooks/__init__.py Tue Mar 30 14:32:03 2010 +0200 +++ b/hooks/__init__.py Wed Mar 31 15:39:09 2010 +0200 @@ -24,7 +24,7 @@ session = repo.internal_session() try: session.system_sql( - 'DELETE FROM transaction WHERE tx_time < %(time)s', + 'DELETE FROM transactions WHERE tx_time < %(time)s', {'time': mindate}) # cleanup deleted entities session.system_sql( diff -r d6fd82a5a4e8 -r a63d7886fcf5 i18n/en.po --- a/i18n/en.po Tue Mar 30 14:32:03 2010 +0200 +++ b/i18n/en.po Wed Mar 31 15:39:09 2010 +0200 @@ -332,6 +332,18 @@ "anymore." msgstr "" +#, python-format +msgid "" +"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't " +"exist anymore" +msgstr "" + +#, python-format +msgid "" +"Can't undo creation of entity %(eid)s of type %(etype)s, type no more " +"supported" +msgstr "" + msgid "Date" msgstr "Date" @@ -3358,6 +3370,9 @@ msgid "some errors occured:" msgstr "" +msgid "some later transaction(s) touch entity, undo them first" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "" diff -r d6fd82a5a4e8 -r a63d7886fcf5 i18n/es.po --- a/i18n/es.po Tue Mar 30 14:32:03 2010 +0200 +++ b/i18n/es.po Wed Mar 31 15:39:09 2010 +0200 @@ -340,6 +340,18 @@ "anymore." msgstr "" +#, python-format +msgid "" +"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't " +"exist anymore" +msgstr "" + +#, python-format +msgid "" +"Can't undo creation of entity %(eid)s of type %(etype)s, type no more " +"supported" +msgstr "" + msgid "Date" msgstr "Fecha" @@ -3439,6 +3451,9 @@ msgid "some errors occured:" msgstr "" +msgid "some later transaction(s) touch entity, undo them first" +msgstr "" + msgid "sorry, the server is unable to handle this query" msgstr "lo sentimos, el servidor no puede manejar esta consulta" diff -r d6fd82a5a4e8 -r a63d7886fcf5 i18n/fr.po --- a/i18n/fr.po Tue Mar 30 14:32:03 2010 +0200 +++ b/i18n/fr.po Wed Mar 31 15:39:09 2010 +0200 @@ -347,6 +347,22 @@ "Ne peut restaurer la relation %(rtype)s, l'entité %(role)s %(eid)s n'existe " "plus." +#, python-format +msgid "" +"Can't undo addition of relation %(rtype)s from %(subj)s to %(obj)s, doesn't " +"exist anymore" +msgstr "" +"Ne peut annuler l'ajout de relation %(rtype)s de %(subj)s vers %(obj)s, " +"cette relation n'existe plus" + +#, python-format +msgid "" +"Can't undo creation of entity %(eid)s of type %(etype)s, type no more " +"supported" +msgstr "" +"Ne peut annuler la création de l'entité %(eid)s de type %(etype)s, ce type " +"n'existe plus" + msgid "Date" msgstr "Date" @@ -3471,6 +3487,10 @@ msgid "some errors occured:" msgstr "des erreurs sont survenues" +msgid "some later transaction(s) touch entity, undo them first" +msgstr "" +"des transactions plus récentes modifient cette entité, annulez les d'abord" + msgid "sorry, the server is unable to handle this query" msgstr "désolé, le serveur ne peut traiter cette requête" diff -r d6fd82a5a4e8 -r a63d7886fcf5 selectors.py --- a/selectors.py Tue Mar 30 14:32:03 2010 +0200 +++ b/selectors.py Wed Mar 31 15:39:09 2010 +0200 @@ -58,26 +58,29 @@ # helpers for debugging selectors SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors') -TRACED_OIDS = () +TRACED_OIDS = None + +def _trace_selector(cls, ret): + # /!\ lltrace decorates pure function or __call__ method, this + # means argument order may be different + if isinstance(cls, Selector): + selname = str(cls) + vobj = args[0] + else: + selname = selector.__name__ + vobj = cls + if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS: + #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls) + print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__) def lltrace(selector): # don't wrap selectors if not in development mode if CubicWebConfiguration.mode == 'system': # XXX config.debug return selector def traced(cls, *args, **kwargs): - # /!\ lltrace decorates pure function or __call__ method, this - # means argument order may be different - if isinstance(cls, Selector): - selname = str(cls) - vobj = args[0] - else: - selname = selector.__name__ - vobj = cls - oid = class_regid(vobj) ret = selector(cls, *args, **kwargs) - if TRACED_OIDS == 'all' or oid in TRACED_OIDS: - #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls) - print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__) + if TRACED_OIDS is not None: + _trace_selector(cls, ret) return ret traced.__name__ = selector.__name__ traced.__doc__ = selector.__doc__ @@ -108,7 +111,7 @@ def __exit__(self, exctype, exc, traceback): global TRACED_OIDS - TRACED_OIDS = () + TRACED_OIDS = None return traceback is None diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/__init__.py --- a/server/__init__.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/__init__.py Wed Mar 31 15:39:09 2010 +0200 @@ -210,8 +210,11 @@ paths = [p for p in config.cubes_path() + [config.apphome] if exists(join(p, 'migration'))] # deactivate every hooks but those responsible to set metadata - # so, NO INTEGRITY CHECKS are done, to have quicker db creation - with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata'): + # so, NO INTEGRITY CHECKS are done, to have quicker db creation. + # Active integrity is kept else we may pb such as two default + # workflows for one entity type. + with hooks_control(session, session.HOOKS_DENY_ALL, 'metadata', + 'activeintegrity'): # execute cubicweb's pre script mhandler.exec_event_script('pre%s' % event) # execute cubes pre script if any diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/hook.py --- a/server/hook.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/hook.py Wed Mar 31 15:39:09 2010 +0200 @@ -48,6 +48,7 @@ from logilab.common.deprecation import deprecated from logilab.common.logging_ext import set_log_methods +from cubicweb import RegistryNotFound from cubicweb.cwvreg import CWRegistry, VRegistry from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector, implements) @@ -66,15 +67,14 @@ class HooksRegistry(CWRegistry): + def initialization_completed(self): + for appobjects in self.values(): + for cls in appobjects: + if not cls.enabled: + warn('[3.6] %s: enabled is deprecated' % cls) + self.unregister(cls) def register(self, obj, **kwargs): - try: - iter(obj.events) - except AttributeError: - raise - except: - raise Exception('bad .events attribute %s on %s.%s' % ( - obj.events, obj.__module__, obj.__name__)) for event in obj.events: if event not in ALL_HOOKS: raise Exception('bad event %s on %s.%s' % ( @@ -96,7 +96,19 @@ for hook in hooks: hook() -VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry +class HooksManager(object): + def __init__(self, vreg): + self.vreg = vreg + + def call_hooks(self, event, session=None, **kwargs): + try: + self.vreg['%s_hooks' % event].call_hooks(event, session, **kwargs) + except RegistryNotFound: + pass # no hooks for this event + + +for event in ALL_HOOKS: + VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry _MARKER = object() def entity_oldnewvalue(entity, attr): @@ -116,21 +128,6 @@ @objectify_selector @lltrace -def _bw_is_enabled(cls, req, **kwargs): - if cls.enabled: - return 1 - warn('[3.6] %s: enabled is deprecated' % cls) - return 0 - -@objectify_selector -@lltrace -def match_event(cls, req, **kwargs): - if kwargs.get('event') in cls.events: - return 1 - return 0 - -@objectify_selector -@lltrace def enabled_category(cls, req, **kwargs): if req is None: return True # XXX how to deactivate server startup / shutdown event @@ -190,11 +187,11 @@ return 1 return 0 + # base class for hook ########################################################## class Hook(AppObject): - __registry__ = 'hooks' - __select__ = match_event() & enabled_category() & _bw_is_enabled() + __select__ = enabled_category() # set this in derivated classes events = None category = None @@ -203,6 +200,16 @@ enabled = True @classproperty + def __registries__(cls): + try: + return ['%s_hooks' % ev for ev in cls.events] + except AttributeError: + raise + except TypeError: + raise Exception('bad .events attribute %s on %s.%s' % ( + cls.events, cls.__module__, cls.__name__)) + + @classproperty def __regid__(cls): warn('[3.6] %s.%s: please specify an id for your hook' % (cls.__module__, cls.__name__), DeprecationWarning) diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/repository.py --- a/server/repository.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/repository.py Wed Mar 31 15:39:09 2010 +0200 @@ -24,7 +24,6 @@ from os.path import join from datetime import datetime from time import time, localtime, strftime -#from pickle import dumps from logilab.common.decorators import cached from logilab.common.compat import any @@ -38,7 +37,7 @@ UnknownEid, AuthenticationError, ExecutionError, ETypeNotSupportedBySources, MultiSourcesError, BadConnectionId, Unauthorized, ValidationError, - typed_eid, onevent) + RepositoryError, typed_eid, onevent) from cubicweb import cwvreg, schema, server from cubicweb.server import utils, hook, pool, querier, sources from cubicweb.server.session import Session, InternalSession, InternalManager, \ @@ -59,7 +58,7 @@ # XXX we should imo rely on the orm to first fetch existing entity if any # then delete it. if session.is_internal_session \ - or not session.is_hook_category_activated('integrity'): + or not session.is_hook_category_activated('activeintegrity'): return card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality') # one may be tented to check for neweids but this may cause more than one @@ -153,10 +152,14 @@ self._available_pools.put_nowait(pool.ConnectionsPool(self.sources)) if config.quick_start: # quick start, usually only to get a minimal repository to get cubes - # information (eg dump/restore/ + # information (eg dump/restore/...) config._cubes = () - self.set_schema(config.load_schema(), resetvreg=False) + # only load hooks and entity classes in the registry + config.cube_appobject_path = set(('hooks', 'entities')) + config.cubicweb_appobject_path = set(('hooks', 'entities')) + self.set_schema(config.load_schema()) config['connections-pool-size'] = 1 + # will be reinitialized later from cubes found in the database config._cubes = None elif config.creating: # repository creation @@ -202,8 +205,7 @@ self._shutting_down = False if config.quick_start: config.init_cubes(self.get_cubes()) - else: - self.hm = self.vreg['hooks'] + self.hm = hook.HooksManager(self.vreg) # internals ############################################################### @@ -1064,62 +1066,68 @@ if server.DEBUG & server.DBG_REPO: print 'UPDATE entity', entity.__regid__, entity.eid, \ dict(entity), edited_attributes - entity.edited_attributes = edited_attributes - if session.is_hook_category_activated('integrity'): - entity.check() + hm = self.hm eschema = entity.e_schema session.set_entity_cache(entity) - only_inline_rels, need_fti_update = True, False - relations = [] - for attr in edited_attributes: - if attr == 'eid': - continue - rschema = eschema.subjrels[attr] - if rschema.final: - if getattr(eschema.rdef(attr), 'fulltextindexed', False): - need_fti_update = True - only_inline_rels = False - else: - # inlined relation - 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]: - previous_value = None + orig_edited_attributes = getattr(entity, 'edited_attributes', None) + entity.edited_attributes = edited_attributes + try: + if session.is_hook_category_activated('integrity'): + entity.check() + only_inline_rels, need_fti_update = True, False + relations = [] + for attr in list(edited_attributes): + if attr == 'eid': + continue + rschema = eschema.subjrels[attr] + if rschema.final: + if getattr(eschema.rdef(attr), 'fulltextindexed', False): + need_fti_update = True + only_inline_rels = False + else: + # inlined relation + 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]: + previous_value = None + else: + hm.call_hooks('before_delete_relation', session, + eidfrom=entity.eid, rtype=attr, + eidto=previous_value) + relations.append((attr, entity[attr], previous_value)) + source = self.source_from_eid(entity.eid, session) + if source.should_call_hooks: + # call hooks for inlined relations + for attr, value, _ in relations: + hm.call_hooks('before_add_relation', session, + eidfrom=entity.eid, rtype=attr, eidto=value) + if not only_inline_rels: + hm.call_hooks('before_update_entity', session, entity=entity) + source.update_entity(session, entity) + self.system_source.update_info(session, entity, need_fti_update) + if source.should_call_hooks: + if not only_inline_rels: + hm.call_hooks('after_update_entity', session, entity=entity) + for attr, value, prevvalue in relations: + # if the relation is already cached, update existant cache + relcache = entity.relation_cached(attr, 'subject') + if prevvalue is not None: + hm.call_hooks('after_delete_relation', session, + eidfrom=entity.eid, rtype=attr, eidto=prevvalue) + if relcache is not None: + session.update_rel_cache_del(entity.eid, attr, prevvalue) + del_existing_rel_if_needed(session, entity.eid, attr, value) + if relcache is not None: + session.update_rel_cache_add(entity.eid, attr, value) else: - self.hm.call_hooks('before_delete_relation', session, - eidfrom=entity.eid, rtype=attr, - eidto=previous_value) - relations.append((attr, entity[attr], previous_value)) - source = self.source_from_eid(entity.eid, session) - if source.should_call_hooks: - # call hooks for inlined relations - for attr, value, _ in relations: - self.hm.call_hooks('before_add_relation', session, - eidfrom=entity.eid, rtype=attr, eidto=value) - if not only_inline_rels: - self.hm.call_hooks('before_update_entity', session, entity=entity) - source.update_entity(session, entity) - self.system_source.update_info(session, entity, need_fti_update) - if source.should_call_hooks: - if not only_inline_rels: - self.hm.call_hooks('after_update_entity', session, entity=entity) - for attr, value, prevvalue in relations: - # if the relation is already cached, update existant cache - relcache = entity.relation_cached(attr, 'subject') - if prevvalue is not None: - self.hm.call_hooks('after_delete_relation', session, - eidfrom=entity.eid, rtype=attr, eidto=prevvalue) - if relcache is not None: - session.update_rel_cache_del(entity.eid, attr, prevvalue) - del_existing_rel_if_needed(session, entity.eid, attr, value) - if relcache is not None: - session.update_rel_cache_add(entity.eid, attr, value) - else: - entity.set_related_cache(attr, 'subject', - session.eid_rset(value)) - self.hm.call_hooks('after_add_relation', session, - eidfrom=entity.eid, rtype=attr, eidto=value) + entity.set_related_cache(attr, 'subject', + session.eid_rset(value)) + 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 def glob_delete_entity(self, session, eid): """delete an entity and all related entities from the repository""" diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/session.py --- a/server/session.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/session.py Wed Mar 31 15:39:09 2010 +0200 @@ -278,8 +278,13 @@ """return a sql cursor on the system database""" if not sql.split(None, 1)[0].upper() == 'SELECT': self.mode = 'write' - return self.pool.source('system').doexec(self, sql, args, - rollback=rollback_on_failure) + source = self.pool.source('system') + try: + return source.doexec(self, sql, args, rollback=rollback_on_failure) + except (source.OperationalError, source.InterfaceError): + source.warning("trying to reconnect") + self.pool.reconnect(self) + return source.doexec(self, sql, args, rollback=rollback_on_failure) def set_language(self, language): """i18n configuration for translation""" diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/sources/native.py --- a/server/sources/native.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/sources/native.py Wed Mar 31 15:39:09 2010 +0200 @@ -410,7 +410,7 @@ cursor = self.doexec(session, sql, args) except (self.OperationalError, self.InterfaceError): # FIXME: better detection of deconnection pb - self.info("request failed '%s' ... retry with a new cursor", sql) + self.warning("trying to reconnect") session.pool.reconnect(self) cursor = self.doexec(session, sql, args) results = self.process_result(cursor, cbs) @@ -1031,8 +1031,8 @@ entity = self.repo.vreg['etypes'].etype_class(etype)(session) except Exception: return [session._( - "Can't undo creation of entity %s of type %s, type " - "no more supported" % (eid, etype))] + "Can't undo creation of entity %(eid)s of type %(etype)s, type " + "no more supported" % {'eid': eid, 'etype': etype})] entity.set_eid(eid) # for proper eid/type cache update hook.set_operation(session, 'pendingeids', eid, @@ -1075,8 +1075,8 @@ cu = self.doexec(session, sql) if cu.fetchone() is None: errors.append(session._( - "Can't undo addition of relation %s from %s to %s, doesn't " - "exist anymore" % (rtype, subj, obj))) + "Can't undo addition of relation %(rtype)s from %(subj)s to" + " %(obj)s, doesn't exist anymore" % locals())) if not errors: self.repo.hm.call_hooks('before_delete_relation', session, eidfrom=subj, rtype=rtype, eidto=obj) diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/test/unittest_hook.py --- a/server/test/unittest_hook.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/test/unittest_hook.py Wed Mar 31 15:39:09 2010 +0200 @@ -81,7 +81,7 @@ raise HookCalled() -class HooksManagerTC(TestCase): +class HooksRegistryTC(TestCase): def setUp(self): """ called before each test from this class """ @@ -115,7 +115,6 @@ is_hook_activated=lambda x, cls: cls.category not in dis) self.assertRaises(HookCalled, self.o.call_hooks, 'before_add_entity', cw) - self.o.call_hooks('before_delete_entity', cw) # nothing to call dis.add('cat1') self.o.call_hooks('before_add_entity', cw) # disabled hooks category, not called dis.remove('cat1') diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/test/unittest_repository.py --- a/server/test/unittest_repository.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/test/unittest_repository.py Wed Mar 31 15:39:09 2010 +0200 @@ -6,6 +6,9 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + +from __future__ import with_statement import os import sys @@ -20,12 +23,14 @@ from cubicweb import (BadConnectionId, RepositoryError, ValidationError, UnknownEid, AuthenticationError) +from cubicweb.selectors import implements from cubicweb.schema import CubicWebSchema, RQLConstraint from cubicweb.dbapi import connect, multiple_connections_unfix from cubicweb.devtools.testlib import CubicWebTC from cubicweb.devtools.repotest import tuplify from cubicweb.server import repository, hook from cubicweb.server.sqlutils import SQL_PREFIX +from cubicweb.server.hook import Hook from cubicweb.server.sources import native # start name server anyway, process will fail if already running @@ -272,8 +277,10 @@ from logilab.common import pyro_ext pyro_ext._DAEMONS.clear() + def _pyro_client(self, done): - cnx = connect(self.repo.config.appid, u'admin', password='gingkow') + cnx = connect(self.repo.config.appid, u'admin', password='gingkow', + initlog=False) # don't reset logging configuration try: # check we can get the schema schema = cnx.get_schema() @@ -283,7 +290,7 @@ cnx.close() done.append(True) finally: - # connect monkey path some method by default, remove them + # connect monkey patch some method by default, remove them multiple_connections_unfix() def test_internal_api(self): @@ -357,6 +364,42 @@ self.assertEquals(rset.rows[0][0], p2.eid) + def test_set_attributes_in_before_update(self): + # local hook + class DummyBeforeHook(Hook): + __regid__ = 'dummy-before-hook' + __select__ = Hook.__select__ & implements('EmailAddress') + events = ('before_update_entity',) + def __call__(self): + # safety belt: avoid potential infinite recursion if the test + # fails (i.e. RuntimeError not raised) + pendings = self._cw.transaction_data.setdefault('pending', set()) + if self.entity.eid not in pendings: + pendings.add(self.entity.eid) + self.entity.set_attributes(alias=u'foo') + with self.temporary_appobjects(DummyBeforeHook): + req = self.request() + addr = req.create_entity('EmailAddress', address=u'a@b.fr') + addr.set_attributes(address=u'a@b.com') + rset = self.execute('Any A,AA WHERE X eid %(x)s, X address A, X alias AA', + {'x': addr.eid}) + self.assertEquals(rset.rows, [[u'a@b.com', u'foo']]) + + def test_set_attributes_in_before_add(self): + # local hook + class DummyBeforeHook(Hook): + __regid__ = 'dummy-before-hook' + __select__ = Hook.__select__ & implements('EmailAddress') + events = ('before_add_entity',) + def __call__(self): + # set_attributes is forbidden within before_add_entity() + self.entity.set_attributes(alias=u'foo') + with self.temporary_appobjects(DummyBeforeHook): + req = self.request() + self.assertRaises(RepositoryError, req.create_entity, + 'EmailAddress', address=u'a@b.fr') + + class DataHelpersTC(CubicWebTC): def test_create_eid(self): @@ -402,6 +445,7 @@ class FTITC(CubicWebTC): def test_reindex_and_modified_since(self): + self.repo.system_source.multisources_etypes.add('Personne') eidp = self.execute('INSERT Personne X: X nom "toto", X prenom "tutu"')[0][0] self.commit() ts = datetime.now() @@ -484,7 +528,6 @@ """ def setUp(self): CubicWebTC.setUp(self) - self.hm = self.repo.hm CALLED[:] = () def _after_relation_hook(self, pool, fromeid, rtype, toeid): @@ -500,20 +543,20 @@ def __call__(self): CALLED.append((self.event, self.eidfrom, self.rtype, self.eidto)) - self.hm.register(EcritParHook) - eidp = self.execute('INSERT Personne X: X nom "toto"')[0][0] - eidn = self.execute('INSERT Note X: X type "T"')[0][0] - self.execute('SET N ecrit_par Y WHERE N type "T", Y nom "toto"') - self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp), - ('after_add_relation', eidn, 'ecrit_par', eidp)]) - CALLED[:] = () - self.execute('DELETE N ecrit_par Y WHERE N type "T", Y nom "toto"') - self.assertEquals(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp), - ('after_delete_relation', eidn, 'ecrit_par', eidp)]) - CALLED[:] = () - eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0] - self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp), - ('after_add_relation', eidn, 'ecrit_par', eidp)]) + with self.temporary_appobjects(EcritParHook): + eidp = self.execute('INSERT Personne X: X nom "toto"')[0][0] + eidn = self.execute('INSERT Note X: X type "T"')[0][0] + self.execute('SET N ecrit_par Y WHERE N type "T", Y nom "toto"') + self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp), + ('after_add_relation', eidn, 'ecrit_par', eidp)]) + CALLED[:] = () + self.execute('DELETE N ecrit_par Y WHERE N type "T", Y nom "toto"') + self.assertEquals(CALLED, [('before_delete_relation', eidn, 'ecrit_par', eidp), + ('after_delete_relation', eidn, 'ecrit_par', eidp)]) + CALLED[:] = () + eidn = self.execute('INSERT Note N: N ecrit_par P WHERE P nom "toto"')[0][0] + self.assertEquals(CALLED, [('before_add_relation', eidn, 'ecrit_par', eidp), + ('after_add_relation', eidn, 'ecrit_par', eidp)]) if __name__ == '__main__': diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/test/unittest_storage.py --- a/server/test/unittest_storage.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/test/unittest_storage.py Wed Mar 31 15:39:09 2010 +0200 @@ -6,6 +6,8 @@ :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses """ +from __future__ import with_statement + from logilab.common.testlib import unittest_main from cubicweb.devtools.testlib import CubicWebTC @@ -93,14 +95,8 @@ self.assertEquals(fspath.getvalue(), '/the/path') def test_source_storage_transparency(self): - self.vreg._loadedmods[__name__] = {} - self.vreg.register(DummyBeforeHook) - self.vreg.register(DummyAfterHook) - try: + with self.temporary_appobjects(DummyBeforeHook, DummyAfterHook): self.create_file() - finally: - self.vreg.unregister(DummyBeforeHook) - self.vreg.unregister(DummyAfterHook) def test_source_mapped_attribute_error_cases(self): ex = self.assertRaises(QueryError, self.execute, diff -r d6fd82a5a4e8 -r a63d7886fcf5 server/test/unittest_undo.py --- a/server/test/unittest_undo.py Tue Mar 30 14:32:03 2010 +0200 +++ b/server/test/unittest_undo.py Wed Mar 31 15:39:09 2010 +0200 @@ -64,7 +64,7 @@ self.assertEquals(a4.eid_to, self.toto.in_group[0].eid) self.assertEquals(a4.order, 4) for i, rtype in ((1, 'owned_by'), (2, 'owned_by'), - (4, 'created_by'), (5, 'in_state')): + (4, 'in_state'), (5, 'created_by')): a = actions[i] self.assertEquals(a.action, 'A') self.assertEquals(a.eid_from, self.toto.eid) diff -r d6fd82a5a4e8 -r a63d7886fcf5 vregistry.py --- a/vregistry.py Tue Mar 30 14:32:03 2010 +0200 +++ b/vregistry.py Wed Mar 31 15:39:09 2010 +0200 @@ -82,6 +82,11 @@ return cls.id return cls.__regid__ +def class_registries(cls, registryname): + if registryname: + return (registryname,) + return cls.__registries__ + class Registry(dict): @@ -205,25 +210,25 @@ if len(args) > 1: warn('[3.5] only the request param can not be named when calling select*', DeprecationWarning, stacklevel=3) - score, winners = 0, [] + score, winners = 0, None for appobject in appobjects: appobjectscore = appobject.__select__(appobject, *args, **kwargs) if appobjectscore > score: score, winners = appobjectscore, [appobject] elif appobjectscore > 0 and appobjectscore == score: winners.append(appobject) - if not winners: + if winners is None: raise NoSelectableObject('args: %s\nkwargs: %s %s' % (args, kwargs.keys(), [repr(v) for v in appobjects])) if len(winners) > 1: + # log in production environement, error while debugging if self.config.debugmode: - self.error('select ambiguity, args: %s\nkwargs: %s %s', - args, kwargs.keys(), [repr(v) for v in winners]) - else: raise Exception('select ambiguity, args: %s\nkwargs: %s %s' % (args, kwargs.keys(), [repr(v) for v in winners])) + self.error('select ambiguity, args: %s\nkwargs: %s %s', + args, kwargs.keys(), [repr(v) for v in winners]) # return the result of calling the appobject return winners[0](*args, **kwargs) @@ -318,31 +323,33 @@ if obj.__module__ != modname or obj in butclasses: continue oid = class_regid(obj) - registryname = obj.__registry__ except AttributeError: continue if oid and not '__abstract__' in obj.__dict__: - self.register(obj, registryname) + self.register(obj, oid=oid) def register(self, obj, registryname=None, oid=None, clear=False): """base method to add an object in the registry""" assert not '__abstract__' in obj.__dict__ - registryname = registryname or obj.__registry__ - registry = self.setdefault(registryname) - registry.register(obj, oid=oid, clear=clear) try: vname = obj.__name__ except AttributeError: + # XXX may occurs? vname = obj.__class__.__name__ - self.debug('registered appobject %s in registry %s with id %s', - vname, registryname, oid or class_regid(obj)) + for registryname in class_registries(obj, registryname): + registry = self.setdefault(registryname) + registry.register(obj, oid=oid, clear=clear) + self.debug('registered appobject %s in registry %s with id %s', + vname, registryname, oid or class_regid(obj)) self._loadedmods[obj.__module__][classid(obj)] = obj def unregister(self, obj, registryname=None): - self[registryname or obj.__registry__].unregister(obj) + for registryname in class_registries(obj, registryname): + self[registryname].unregister(obj) def register_and_replace(self, obj, replaced, registryname=None): - self[registryname or obj.__registry__].register_and_replace(obj, replaced) + for registryname in class_registries(obj, registryname): + self[registryname].register_and_replace(obj, replaced) # initialization methods ################################################### @@ -451,7 +458,7 @@ self._load_ancestors_then_object(modname, parent) if (appobjectcls.__dict__.get('__abstract__') or appobjectcls.__name__[0] == '_' - or not appobjectcls.__registry__ + or not appobjectcls.__registries__ or not class_regid(appobjectcls)): return try: diff -r d6fd82a5a4e8 -r a63d7886fcf5 web/data/cubicweb.edition.js --- a/web/data/cubicweb.edition.js Tue Mar 30 14:32:03 2010 +0200 +++ b/web/data/cubicweb.edition.js Wed Mar 31 15:39:09 2010 +0200 @@ -331,26 +331,30 @@ var firsterrfield = null; for (fieldname in errors) { var errmsg = errors[fieldname]; - var fieldid = fieldname + ':' + eid; - var suffixes = ['', '-subject', '-object']; - var found = false; - // XXX remove suffixes at some point - for (var i=0, length=suffixes.length; i'] diff -r d6fd82a5a4e8 -r a63d7886fcf5 web/views/basecontrollers.py --- a/web/views/basecontrollers.py Tue Mar 30 14:32:03 2010 +0200 +++ b/web/views/basecontrollers.py Wed Mar 31 15:39:09 2010 +0200 @@ -282,8 +282,6 @@ except RemoteCallFailed: raise except Exception, ex: - import traceback - traceback.print_exc() self.exception('an exception occured while calling js_%s(%s): %s', fname, args, ex) raise RemoteCallFailed(repr(ex)) @@ -392,7 +390,18 @@ else: # we receive unicode keys which is not supported by the **syntax extraargs = dict((str(key), value) for key, value in extraargs.items()) - comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, **extraargs) + # XXX while it sounds good, addition of the try/except below cause pb: + # when filtering using facets return an empty rset, the edition box + # isn't anymore selectable, as expected. The pb is that with the + # try/except below, we see a "an error occured" message in the ui, while + # we don't see it without it. Proper fix would probably be to deal with + # this by allowing facet handling code to tell to js_component that such + # error is expected and should'nt be reported. + #try: + comp = self._cw.vreg[registry].select(compid, self._cw, rset=rset, + **extraargs) + #except NoSelectableObject: + # raise RemoteCallFailed('unselectable') extraargs = extraargs or {} stream = comp.set_stream() comp.render(**extraargs) diff -r d6fd82a5a4e8 -r a63d7886fcf5 web/views/bookmark.py --- a/web/views/bookmark.py Tue Mar 30 14:32:03 2010 +0200 +++ b/web/views/bookmark.py Wed Mar 31 15:39:09 2010 +0200 @@ -113,7 +113,7 @@ # we can't edit shared bookmarks we don't own bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, B owned_by U, U eid %(x)s' erset = req.execute(bookmarksrql, {'x': ueid}, 'x', - build_descr=False) + build_descr=False) bookmarksrql %= {'x': ueid} if erset: url = self._cw.build_url(vid='muledit', rql=bookmarksrql) diff -r d6fd82a5a4e8 -r a63d7886fcf5 web/views/facets.py --- a/web/views/facets.py Tue Mar 30 14:32:03 2010 +0200 +++ b/web/views/facets.py Wed Mar 31 15:39:09 2010 +0200 @@ -105,7 +105,10 @@ def display_bookmark_link(self, rset): eschema = self._cw.vreg.schema.eschema('Bookmark') if eschema.has_perm(self._cw, 'add'): - bk_path = 'view?rql=%s' % rset.printable_rql() + bk_path = 'rql=%s' % self._cw.url_quote(rset.printable_rql()) + if self._cw.form.get('vid'): + bk_path += '&vid=%s' % self._cw.url_quote(self._cw.form['vid']) + bk_path = 'view?' + bk_path bk_title = self._cw._('my custom search') linkto = 'bookmarked_by:%s:subject' % self._cw.user.eid bk_add_url = self._cw.build_url('add/Bookmark', path=bk_path, title=bk_title, __linkto=linkto)