# HG changeset patch # User Sylvain Thénault # Date 1274381275 -7200 # Node ID 9ab2b4c74bafeea2069eab86339c4fccf7ba9af0 # Parent a64f48dd5fe49fda648f155fe4c6868bae5d0e98 [entity] introduce a new 'adapters' registry This changeset introduces the notion in adapters (as in Zope Component Architecture) in a cubicweb way, eg using a specific registry of appobjects. This allows nicer code structure, by avoid clutering entity classes and moving code usually specific to a place of the ui (or something else) together with the code that use the interface. We don't use actual interface anymore, they are implied by adapters (which may be abstract), whose reg id is an interface name. Appobjects that used to 'implements(IFace)' should now be rewritten by: * coding an IFaceAdapter(EntityAdapter) defining (implementing if desired) the interface, usually with __regid__ = 'IFace' * use "adaptable('IFace')" as selector instead Also, the implements_adapter_compat decorator eases backward compatibility with adapter's methods that may still be found on entities implementing the interface. Notice that unlike ZCA, we don't support automatic adapters chain (yagni?). All interfaces defined in cubicweb have been turned into adapters, also some new ones have been introduced to cleanup Entity / AnyEntity classes namespace. At the end, the pluggable mixins mecanism should disappear in favor of adapters as well. diff -r a64f48dd5fe4 -r 9ab2b4c74baf cwvreg.py --- a/cwvreg.py Thu May 20 20:47:13 2010 +0200 +++ b/cwvreg.py Thu May 20 20:47:55 2010 +0200 @@ -82,7 +82,6 @@ .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_all .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_and_replace .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register -.. automethod:: cubicweb.cwvreg.CubicWebVRegistry.register_if_interface_found .. automethod:: cubicweb.cwvreg.CubicWebVRegistry.unregister Examples: @@ -192,6 +191,8 @@ __docformat__ = "restructuredtext en" _ = unicode +from warnings import warn + from logilab.common.decorators import cached, clear_cache from logilab.common.deprecation import deprecated from logilab.common.modutils import cleanup_sys_modules @@ -211,23 +212,23 @@ def use_interfaces(obj): """return interfaces used by the given object by searching for implements - selectors, with a bw compat fallback to accepts_interfaces attribute + selectors """ from cubicweb.selectors import implements - try: - # XXX deprecated - return sorted(obj.accepts_interfaces) - except AttributeError: - try: - impl = obj.__select__.search_selector(implements) - if impl: - return sorted(impl.expected_ifaces) - except AttributeError: - pass # old-style appobject classes with no accepts_interfaces - except: - print 'bad selector %s on %s' % (obj.__select__, obj) - raise - return () + impl = obj.__select__.search_selector(implements) + if impl: + return sorted(impl.expected_ifaces) + return () + +def require_appobject(obj): + """return interfaces used by the given object by searching for implements + selectors + """ + from cubicweb.selectors import appobject_selectable + impl = obj.__select__.search_selector(appobject_selectable) + if impl: + return (impl.registry, impl.regids) + return None class CWRegistry(Registry): @@ -477,6 +478,7 @@ def reset(self): super(CubicWebVRegistry, self).reset() self._needs_iface = {} + self._needs_appobject = {} # two special registries, propertydefs which care all the property # definitions, and propertyvals which contains values for those # properties @@ -536,6 +538,7 @@ for obj in objects: obj.schema = schema + @deprecated('[3.9] use .register instead') def register_if_interface_found(self, obj, ifaces, **kwargs): """register `obj` but remove it if no entity class implements one of the given `ifaces` interfaces at the end of the registration process. @@ -561,7 +564,15 @@ # XXX bw compat ifaces = use_interfaces(obj) if ifaces: + if not obj.__name__.endswith('Adapter') and \ + any(iface for iface in ifaces if not isinstance(iface, basestring)): + warn('[3.9] %s: interfaces in implements selector are ' + 'deprecated in favor of adapters / appobject_selectable ' + 'selector' % obj.__name__, DeprecationWarning) self._needs_iface[obj] = ifaces + depends_on = require_appobject(obj) + if depends_on is not None: + self._needs_appobject[obj] = depends_on def register_objects(self, path, force_reload=False): """overriden to remove objects requiring a missing interface""" @@ -578,13 +589,18 @@ # we may want to keep interface dependent objects (e.g.for i18n # catalog generation) if self.config.cleanup_interface_sobjects: - # remove appobjects that don't support any available interface + # XXX deprecated with cw 3.9: remove appobjects that don't support + # any available interface implemented_interfaces = set() if 'Any' in self.get('etypes', ()): for etype in self.schema.entities(): if etype.final: continue cls = self['etypes'].etype_class(etype) + if cls.__implements__: + warn('[3.9] %s: using __implements__/interfaces are ' + 'deprecated in favor of adapters' % cls.__name__, + DeprecationWarning) for iface in cls.__implements__: implemented_interfaces.update(iface.__mro__) implemented_interfaces.update(cls.__mro__) @@ -598,9 +614,17 @@ self.debug('kicking appobject %s (no implemented ' 'interface among %s)', obj, ifaces) self.unregister(obj) - # clear needs_iface so we don't try to remove some not-anymore-in - # objects on automatic reloading - self._needs_iface.clear() + # since 3.9: remove appobjects which depending on other, unexistant + # appobjects + for obj, (regname, regids) in self._needs_appobject.items(): + registry = self[regname] + for regid in regids: + if registry.get(regid): + break + else: + self.debug('kicking %s (no %s object in registry %s)', + obj, ' or '.join(regids), registry) + self.unregister(obj) super(CubicWebVRegistry, self).initialization_completed() for rtag in RTAGS: # don't check rtags if we don't want to cleanup_interface_sobjects diff -r a64f48dd5fe4 -r 9ab2b4c74baf devtools/devctl.py --- a/devtools/devctl.py Thu May 20 20:47:13 2010 +0200 +++ b/devtools/devctl.py Thu May 20 20:47:55 2010 +0200 @@ -15,10 +15,10 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""additional cubicweb-ctl commands and command handlers for cubicweb and cubicweb's -cubes development +"""additional cubicweb-ctl commands and command handlers for cubicweb and +cubicweb's cubes development +""" -""" __docformat__ = "restructuredtext en" # *ctl module should limit the number of import to be imported as quickly as diff -r a64f48dd5fe4 -r 9ab2b4c74baf doc/book/en/devrepo/vreg.rst --- a/doc/book/en/devrepo/vreg.rst Thu May 20 20:47:13 2010 +0200 +++ b/doc/book/en/devrepo/vreg.rst Thu May 20 20:47:55 2010 +0200 @@ -37,6 +37,7 @@ .. autoclass:: cubicweb.appobject.yes .. autoclass:: cubicweb.selectors.match_kwargs .. autoclass:: cubicweb.selectors.appobject_selectable +.. autoclass:: cubicweb.selectors.adaptable Result set selectors @@ -75,6 +76,7 @@ .. autoclass:: cubicweb.selectors.partial_has_related_entities .. autoclass:: cubicweb.selectors.has_permission .. autoclass:: cubicweb.selectors.has_add_permission +.. autoclass:: cubicweb.selectors.has_mimetype Logged user selectors diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/__init__.py --- a/entities/__init__.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/__init__.py Thu May 20 20:47:55 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 application's entities class implementation: `AnyEntity` +"""base application's entities class implementation: `AnyEntity`""" -""" __docformat__ = "restructuredtext en" from warnings import warn @@ -28,33 +27,13 @@ from cubicweb import Unauthorized, typed_eid from cubicweb.entity import Entity -from cubicweb.interfaces import IBreadCrumbs, IFeed - class AnyEntity(Entity): """an entity instance has e_schema automagically set on the class and instances have access to their issuing cursor """ __regid__ = 'Any' - __implements__ = (IBreadCrumbs, IFeed) - - fetch_attrs = ('modification_date',) - @classmethod - def fetch_order(cls, attr, var): - """class method used to control sort order when multiple entities of - this type are fetched - """ - return cls.fetch_unrelated_order(attr, var) - - @classmethod - def fetch_unrelated_order(cls, attr, var): - """class method used to control sort order when multiple entities of - this type are fetched to use in edition (eg propose them to create a - new relation on an edited entity). - """ - if attr == 'modification_date': - return '%s DESC' % var - return None + __implements__ = () # meta data api ########################################################### @@ -120,32 +99,6 @@ except (Unauthorized, IndexError): return None - def breadcrumbs(self, view=None, recurs=False): - path = [self] - if hasattr(self, 'parent'): - parent = self.parent() - if parent is not None: - try: - path = parent.breadcrumbs(view, True) + [self] - except TypeError: - warn("breadcrumbs method's now takes two arguments " - "(view=None, recurs=False), please update", - DeprecationWarning) - path = parent.breadcrumbs(view) + [self] - if not recurs: - if view is None: - if 'vtitle' in self._cw.form: - # embeding for instance - path.append( self._cw.form['vtitle'] ) - elif view.__regid__ != 'primary' and hasattr(view, 'title'): - path.append( self._cw._(view.title) ) - return path - - ## IFeed interface ######################################################## - - def rss_feed_url(self): - return self.absolute_url(vid='rss') - # abstractions making the whole things (well, some at least) working ###### def sortvalue(self, rtype=None): @@ -189,35 +142,8 @@ self.__linkto[(rtype, role)] = linkedto return linkedto - # edit controller callbacks ############################################### - - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if hasattr(self, 'parent') and self.parent(): - return self.parent().rest_path(), {} - return str(self.e_schema).lower(), {} - - def pre_web_edit(self): - """callback called by the web editcontroller when an entity will be - created/modified, to let a chance to do some entity specific stuff. - - Do nothing by default. - """ - pass - # server side helpers ##################################################### - def notification_references(self, view): - """used to control References field of email send on notification - for this entity. `view` is the notification view. - - Should return a list of eids which can be used to generate message ids - of previously sent email - """ - return () - # XXX: store a reference to the AnyEntity class since it is hijacked in goa # configuration and we need the actual reference to avoid infinite loops # in mro diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/adapters.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/entities/adapters.py Thu May 20 20:47:55 2010 +0200 @@ -0,0 +1,166 @@ +# copyright 2010 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 . +"""some basic entity adapter implementations, for interfaces used in the +framework itself. +""" + +__docformat__ = "restructuredtext en" + +from cubicweb.view import EntityAdapter, implements_adapter_compat +from cubicweb.selectors import implements, relation_possible +from cubicweb.interfaces import IDownloadable + + +class IEmailableAdapter(EntityAdapter): + __regid__ = 'IEmailable' + __select__ = relation_possible('primary_email') | relation_possible('use_email') + + def get_email(self): + if getattr(self.entity, 'primary_email', None): + return self.entity.primary_email[0].address + if getattr(self.entity, 'use_email', None): + return self.entity.use_email[0].address + return None + + def allowed_massmail_keys(self): + """returns a set of allowed email substitution keys + + The default is to return the entity's attribute list but you might + override this method to allow extra keys. For instance, a Person + class might want to return a `companyname` key. + """ + return set(rschema.type + for rschema, attrtype in self.entity.e_schema.attribute_definitions() + if attrtype.type not in ('Password', 'Bytes')) + + def as_email_context(self): + """returns the dictionary as used by the sendmail controller to + build email bodies. + + NOTE: the dictionary keys should match the list returned by the + `allowed_massmail_keys` method. + """ + return dict( (attr, getattr(self.entity, attr)) + for attr in self.allowed_massmail_keys() ) + + +class INotifiableAdapter(EntityAdapter): + __regid__ = 'INotifiable' + __select__ = implements('Any') + + @implements_adapter_compat('INotifiableAdapter') + def notification_references(self, view): + """used to control References field of email send on notification + for this entity. `view` is the notification view. + + Should return a list of eids which can be used to generate message + identifiers of previously sent email(s) + """ + itree = self.entity.cw_adapt_to('ITree') + if itree is not None: + return itree.path()[:-1] + return () + + +class IFTIndexableAdapter(EntityAdapter): + __regid__ = 'IFTIndexable' + __select__ = implements('Any') + + def fti_containers(self, _done=None): + if _done is None: + _done = set() + entity = self.entity + _done.add(entity.eid) + containers = tuple(entity.e_schema.fulltext_containers()) + if containers: + for rschema, target in containers: + if target == 'object': + targets = getattr(entity, rschema.type) + else: + targets = getattr(entity, 'reverse_%s' % rschema) + for entity in targets: + if entity.eid in _done: + continue + for container in entity.cw_adapt_to('IFTIndexable').fti_containers(_done): + yield container + yielded = True + else: + yield entity + + def get_words(self): + """used by the full text indexer to get words to index + + this method should only be used on the repository side since it depends + on the logilab.database package + + :rtype: list + :return: the list of indexable word of this entity + """ + from logilab.database.fti import tokenize + # take care to cases where we're modyfying the schema + entity = self.entity + pending = self._cw.transaction_data.setdefault('pendingrdefs', set()) + words = [] + for rschema in entity.e_schema.indexable_attributes(): + if (entity.e_schema, rschema) in pending: + continue + try: + value = entity.printable_value(rschema, format='text/plain') + except TransformError: + continue + except: + self.exception("can't add value of %s to text index for entity %s", + rschema, entity.eid) + continue + if value: + words += tokenize(value) + for rschema, role in entity.e_schema.fulltext_relations(): + if role == 'subject': + for entity in getattr(entity, rschema.type): + words += entity.cw_adapt_to('IFTIndexable').get_words() + else: # if role == 'object': + for entity in getattr(entity, 'reverse_%s' % rschema.type): + words += entity.cw_adapt_to('IFTIndexable').get_words() + return words + + +class IDownloadableAdapter(EntityAdapter): + """interface for downloadable entities""" + __regid__ = 'IDownloadable' + __select__ = implements(IDownloadable) # XXX for bw compat, else should be abstract + + @implements_adapter_compat('IDownloadable') + def download_url(self): # XXX not really part of this interface + """return an url to download entity's content""" + raise NotImplementedError + @implements_adapter_compat('IDownloadable') + def download_content_type(self): + """return MIME type of the downloadable content""" + raise NotImplementedError + @implements_adapter_compat('IDownloadable') + def download_encoding(self): + """return encoding of the downloadable content""" + raise NotImplementedError + @implements_adapter_compat('IDownloadable') + def download_file_name(self): + """return file name of the downloadable content""" + raise NotImplementedError + @implements_adapter_compat('IDownloadable') + def download_data(self): + """return actual data of the downloadable content""" + raise NotImplementedError diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/authobjs.py --- a/entities/authobjs.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/authobjs.py Thu May 20 20:47:55 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 . -"""entity classes user and group entities +"""entity classes user and group entities""" -""" __docformat__ = "restructuredtext en" from logilab.common.decorators import cached diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/lib.py --- a/entities/lib.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/lib.py Thu May 20 20:47:55 2010 +0200 @@ -48,13 +48,13 @@ @property def email_of(self): - return self.reverse_use_email and self.reverse_use_email[0] + return self.reverse_use_email and self.reverse_use_email[0] or None @property def prefered(self): return self.prefered_form and self.prefered_form[0] or self - @deprecated('use .prefered') + @deprecated('[3.6] use .prefered') def canonical_form(self): return self.prefered_form and self.prefered_form[0] or self @@ -89,14 +89,6 @@ return self.display_address() return super(EmailAddress, self).printable_value(attr, value, attrtype, format) - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.email_of: - return self.email_of.rest_path(), {} - return super(EmailAddress, self).after_deletion_path() - class Bookmark(AnyEntity): """customized class for Bookmark entities""" @@ -133,12 +125,6 @@ except UnknownProperty: return u'' - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - return 'view', {} - class CWCache(AnyEntity): """Cache""" diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/schemaobjs.py --- a/entities/schemaobjs.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/schemaobjs.py Thu May 20 20:47:55 2010 +0200 @@ -115,14 +115,6 @@ scard, self.relation_type[0].name, ocard, self.to_entity[0].name) - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.relation_type: - return self.relation_type[0].rest_path(), {} - return super(CWRelation, self).after_deletion_path() - @property def rtype(self): return self.relation_type[0] @@ -139,6 +131,7 @@ rschema = self._cw.vreg.schema.rschema(self.rtype.name) return rschema.rdefs[(self.stype.name, self.otype.name)] + class CWAttribute(CWRelation): __regid__ = 'CWAttribute' @@ -160,14 +153,6 @@ def dc_title(self): return '%s(%s)' % (self.cstrtype[0].name, self.value or u'') - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.reverse_constrained_by: - return self.reverse_constrained_by[0].rest_path(), {} - return super(CWConstraint, self).after_deletion_path() - @property def type(self): return self.cstrtype[0].name @@ -201,14 +186,6 @@ def check_expression(self, *args, **kwargs): return self._rqlexpr().check(*args, **kwargs) - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.expression_of: - return self.expression_of.rest_path(), {} - return super(RQLExpression, self).after_deletion_path() - class CWPermission(AnyEntity): __regid__ = 'CWPermission' @@ -218,12 +195,3 @@ if self.label: return '%s (%s)' % (self._cw._(self.name), self.label) return self._cw._(self.name) - - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - permissionof = getattr(self, 'reverse_require_permission', ()) - if len(permissionof) == 1: - return permissionof[0].rest_path(), {} - return super(CWPermission, self).after_deletion_path() diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/test/unittest_base.py --- a/entities/test/unittest_base.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/test/unittest_base.py Thu May 20 20:47:55 2010 +0200 @@ -106,7 +106,7 @@ def test_allowed_massmail_keys(self): e = self.execute('CWUser U WHERE U login "member"').get_entity(0, 0) # Bytes/Password attributes should be omited - self.assertEquals(e.allowed_massmail_keys(), + self.assertEquals(e.cw_adapt_to('IEmailable').allowed_massmail_keys(), set(('surname', 'firstname', 'login', 'last_login_time', 'creation_date', 'modification_date', 'cwuri', 'eid')) ) diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/test/unittest_wfobjs.py --- a/entities/test/unittest_wfobjs.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/test/unittest_wfobjs.py Thu May 20 20:47:55 2010 +0200 @@ -100,35 +100,38 @@ def test_workflow_base(self): e = self.create_user('toto') - self.assertEquals(e.state, 'activated') - e.change_state('deactivated', u'deactivate 1') + iworkflowable = e.cw_adapt_to('IWorkflowable') + self.assertEquals(iworkflowable.state, 'activated') + iworkflowable.change_state('deactivated', u'deactivate 1') self.commit() - e.change_state('activated', u'activate 1') + iworkflowable.change_state('activated', u'activate 1') self.commit() - e.change_state('deactivated', u'deactivate 2') + iworkflowable.change_state('deactivated', u'deactivate 2') self.commit() e.clear_related_cache('wf_info_for', 'object') self.assertEquals([tr.comment for tr in e.reverse_wf_info_for], ['deactivate 1', 'activate 1', 'deactivate 2']) - self.assertEquals(e.latest_trinfo().comment, 'deactivate 2') + self.assertEquals(iworkflowable.latest_trinfo().comment, 'deactivate 2') def test_possible_transitions(self): user = self.execute('CWUser X').get_entity(0, 0) - trs = list(user.possible_transitions()) + iworkflowable = user.cw_adapt_to('IWorkflowable') + trs = list(iworkflowable.possible_transitions()) self.assertEquals(len(trs), 1) self.assertEquals(trs[0].name, u'deactivate') self.assertEquals(trs[0].destination(None).name, u'deactivated') # test a std user get no possible transition cnx = self.login('member') # fetch the entity using the new session - trs = list(cnx.user().possible_transitions()) + trs = list(cnx.user().cw_adapt_to('IWorkflowable').possible_transitions()) self.assertEquals(len(trs), 0) def _test_manager_deactivate(self, user): + iworkflowable = user.cw_adapt_to('IWorkflowable') user.clear_related_cache('in_state', 'subject') self.assertEquals(len(user.in_state), 1) - self.assertEquals(user.state, 'deactivated') - trinfo = user.latest_trinfo() + self.assertEquals(iworkflowable.state, 'deactivated') + trinfo = iworkflowable.latest_trinfo() self.assertEquals(trinfo.previous_state.name, 'activated') self.assertEquals(trinfo.new_state.name, 'deactivated') self.assertEquals(trinfo.comment, 'deactivate user') @@ -137,7 +140,8 @@ def test_change_state(self): user = self.user() - user.change_state('deactivated', comment=u'deactivate user') + iworkflowable = user.cw_adapt_to('IWorkflowable') + iworkflowable.change_state('deactivated', comment=u'deactivate user') trinfo = self._test_manager_deactivate(user) self.assertEquals(trinfo.transition, None) @@ -154,33 +158,36 @@ def test_fire_transition(self): user = self.user() - user.fire_transition('deactivate', comment=u'deactivate user') + iworkflowable = user.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate', comment=u'deactivate user') user.clear_all_caches() - self.assertEquals(user.state, 'deactivated') + self.assertEquals(iworkflowable.state, 'deactivated') self._test_manager_deactivate(user) trinfo = self._test_manager_deactivate(user) self.assertEquals(trinfo.transition.name, 'deactivate') def test_goback_transition(self): - wf = self.session.user.current_workflow + wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow asleep = wf.add_state('asleep') - wf.add_transition('rest', (wf.state_by_name('activated'), wf.state_by_name('deactivated')), - asleep) + wf.add_transition('rest', (wf.state_by_name('activated'), + wf.state_by_name('deactivated')), + asleep) wf.add_transition('wake up', asleep) user = self.create_user('stduser') - user.fire_transition('rest') + iworkflowable = user.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('rest') self.commit() - user.fire_transition('wake up') + iworkflowable.fire_transition('wake up') self.commit() - self.assertEquals(user.state, 'activated') - user.fire_transition('deactivate') + self.assertEquals(iworkflowable.state, 'activated') + iworkflowable.fire_transition('deactivate') self.commit() - user.fire_transition('rest') + iworkflowable.fire_transition('rest') self.commit() - user.fire_transition('wake up') + iworkflowable.fire_transition('wake up') self.commit() user.clear_all_caches() - self.assertEquals(user.state, 'deactivated') + self.assertEquals(iworkflowable.state, 'deactivated') # XXX test managers can change state without matching transition @@ -189,18 +196,18 @@ self.create_user('tutu') cnx = self.login('tutu') req = self.request() - member = req.entity_from_eid(self.member.eid) + iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable') ex = self.assertRaises(ValidationError, - member.fire_transition, 'deactivate') + iworkflowable.fire_transition, 'deactivate') self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"}) cnx.close() cnx = self.login('member') req = self.request() - member = req.entity_from_eid(self.member.eid) - member.fire_transition('deactivate') + iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') cnx.commit() ex = self.assertRaises(ValidationError, - member.fire_transition, 'activate') + iworkflowable.fire_transition, 'activate') self.assertEquals(ex.errors, {'by_transition-subject': "transition may not be fired"}) def test_fire_transition_owned_by(self): @@ -250,43 +257,44 @@ [(swfstate2, state2), (swfstate3, state3)]) self.assertEquals(swftr1.destination(None).eid, swfstate1.eid) # workflows built, begin test - self.group = self.request().create_entity('CWGroup', name=u'grp1') + group = self.request().create_entity('CWGroup', name=u'grp1') self.commit() - self.assertEquals(self.group.current_state.eid, state1.eid) - self.assertEquals(self.group.current_workflow.eid, mwf.eid) - self.assertEquals(self.group.main_workflow.eid, mwf.eid) - self.assertEquals(self.group.subworkflow_input_transition(), None) - self.group.fire_transition('swftr1', u'go') + iworkflowable = group.cw_adapt_to('IWorkflowable') + self.assertEquals(iworkflowable.current_state.eid, state1.eid) + self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.subworkflow_input_transition(), None) + iworkflowable.fire_transition('swftr1', u'go') self.commit() - self.group.clear_all_caches() - self.assertEquals(self.group.current_state.eid, swfstate1.eid) - self.assertEquals(self.group.current_workflow.eid, swf.eid) - self.assertEquals(self.group.main_workflow.eid, mwf.eid) - self.assertEquals(self.group.subworkflow_input_transition().eid, swftr1.eid) - self.group.fire_transition('tr1', u'go') + group.clear_all_caches() + self.assertEquals(iworkflowable.current_state.eid, swfstate1.eid) + self.assertEquals(iworkflowable.current_workflow.eid, swf.eid) + self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.subworkflow_input_transition().eid, swftr1.eid) + iworkflowable.fire_transition('tr1', u'go') self.commit() - self.group.clear_all_caches() - self.assertEquals(self.group.current_state.eid, state2.eid) - self.assertEquals(self.group.current_workflow.eid, mwf.eid) - self.assertEquals(self.group.main_workflow.eid, mwf.eid) - self.assertEquals(self.group.subworkflow_input_transition(), None) + group.clear_all_caches() + self.assertEquals(iworkflowable.current_state.eid, state2.eid) + self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.subworkflow_input_transition(), None) # force back to swfstate1 is impossible since we can't any more find # subworkflow input transition ex = self.assertRaises(ValidationError, - self.group.change_state, swfstate1, u'gadget') + iworkflowable.change_state, swfstate1, u'gadget') self.assertEquals(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"}) self.rollback() # force back to state1 - self.group.change_state('state1', u'gadget') - self.group.fire_transition('swftr1', u'au') - self.group.clear_all_caches() - self.group.fire_transition('tr2', u'chapeau') + iworkflowable.change_state('state1', u'gadget') + iworkflowable.fire_transition('swftr1', u'au') + group.clear_all_caches() + iworkflowable.fire_transition('tr2', u'chapeau') self.commit() - self.group.clear_all_caches() - self.assertEquals(self.group.current_state.eid, state3.eid) - self.assertEquals(self.group.current_workflow.eid, mwf.eid) - self.assertEquals(self.group.main_workflow.eid, mwf.eid) - self.assertListEquals(parse_hist(self.group.workflow_history), + group.clear_all_caches() + self.assertEquals(iworkflowable.current_state.eid, state3.eid) + self.assertEquals(iworkflowable.current_workflow.eid, mwf.eid) + self.assertEquals(iworkflowable.main_workflow.eid, mwf.eid) + self.assertListEquals(parse_hist(iworkflowable.workflow_history), [('state1', 'swfstate1', 'swftr1', 'go'), ('swfstate1', 'swfstate2', 'tr1', 'go'), ('swfstate2', 'state2', 'swftr1', 'exiting from subworkflow subworkflow'), @@ -337,8 +345,9 @@ self.commit() group = self.request().create_entity('CWGroup', name=u'grp1') self.commit() + iworkflowable = group.cw_adapt_to('IWorkflowable') for trans in ('identify', 'release', 'close'): - group.fire_transition(trans) + iworkflowable.fire_transition(trans) self.commit() @@ -362,6 +371,7 @@ self.commit() group = self.request().create_entity('CWGroup', name=u'grp1') self.commit() + iworkflowable = group.cw_adapt_to('IWorkflowable') for trans, nextstate in (('identify', 'xsigning'), ('xabort', 'created'), ('identify', 'xsigning'), @@ -369,10 +379,10 @@ ('release', 'xsigning'), ('xabort', 'identified') ): - group.fire_transition(trans) + iworkflowable.fire_transition(trans) self.commit() group.clear_all_caches() - self.assertEquals(group.state, nextstate) + self.assertEquals(iworkflowable.state, nextstate) class CustomWorkflowTC(CubicWebTC): @@ -389,35 +399,38 @@ self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', {'wf': wf.eid, 'x': self.member.eid}) self.member.clear_all_caches() - self.assertEquals(self.member.state, 'activated')# no change before commit + iworkflowable = self.member.cw_adapt_to('IWorkflowable') + self.assertEquals(iworkflowable.state, 'activated')# no change before commit self.commit() self.member.clear_all_caches() - self.assertEquals(self.member.current_workflow.eid, wf.eid) - self.assertEquals(self.member.state, 'asleep') - self.assertEquals(self.member.workflow_history, ()) + self.assertEquals(iworkflowable.current_workflow.eid, wf.eid) + self.assertEquals(iworkflowable.state, 'asleep') + self.assertEquals(iworkflowable.workflow_history, ()) def test_custom_wf_replace_state_keep_history(self): """member in inital state with some history, state is redirected and state change is recorded to history """ - self.member.fire_transition('deactivate') - self.member.fire_transition('activate') + iworkflowable = self.member.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') + iworkflowable.fire_transition('activate') wf = add_wf(self, 'CWUser') wf.add_state('asleep', initial=True) self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', {'wf': wf.eid, 'x': self.member.eid}) self.commit() self.member.clear_all_caches() - self.assertEquals(self.member.current_workflow.eid, wf.eid) - self.assertEquals(self.member.state, 'asleep') - self.assertEquals(parse_hist(self.member.workflow_history), + self.assertEquals(iworkflowable.current_workflow.eid, wf.eid) + self.assertEquals(iworkflowable.state, 'asleep') + self.assertEquals(parse_hist(iworkflowable.workflow_history), [('activated', 'deactivated', 'deactivate', None), ('deactivated', 'activated', 'activate', None), ('activated', 'asleep', None, 'workflow changed to "CWUser"')]) def test_custom_wf_no_initial_state(self): """try to set a custom workflow which has no initial state""" - self.member.fire_transition('deactivate') + iworkflowable = self.member.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') wf = add_wf(self, 'CWUser') wf.add_state('asleep') self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', @@ -438,7 +451,8 @@ """member in some state shared by the new workflow, nothing has to be done """ - self.member.fire_transition('deactivate') + iworkflowable = self.member.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') wf = add_wf(self, 'CWUser') wf.add_state('asleep', initial=True) self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', @@ -447,12 +461,12 @@ self.execute('DELETE X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', {'wf': wf.eid, 'x': self.member.eid}) self.member.clear_all_caches() - self.assertEquals(self.member.state, 'asleep')# no change before commit + self.assertEquals(iworkflowable.state, 'asleep')# no change before commit self.commit() self.member.clear_all_caches() - self.assertEquals(self.member.current_workflow.name, "default user workflow") - self.assertEquals(self.member.state, 'activated') - self.assertEquals(parse_hist(self.member.workflow_history), + self.assertEquals(iworkflowable.current_workflow.name, "default user workflow") + self.assertEquals(iworkflowable.state, 'activated') + self.assertEquals(parse_hist(iworkflowable.workflow_history), [('activated', 'deactivated', 'deactivate', None), ('deactivated', 'asleep', None, 'workflow changed to "CWUser"'), ('asleep', 'activated', None, 'workflow changed to "default user workflow"'),]) @@ -473,28 +487,29 @@ def test_auto_transition_fired(self): wf = self.setup_custom_wf() user = self.create_user('member') + iworkflowable = user.cw_adapt_to('IWorkflowable') self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', {'wf': wf.eid, 'x': user.eid}) self.commit() user.clear_all_caches() - self.assertEquals(user.state, 'asleep') - self.assertEquals([t.name for t in user.possible_transitions()], + self.assertEquals(iworkflowable.state, 'asleep') + self.assertEquals([t.name for t in iworkflowable.possible_transitions()], ['rest']) - user.fire_transition('rest') + iworkflowable.fire_transition('rest') self.commit() user.clear_all_caches() - self.assertEquals(user.state, 'asleep') - self.assertEquals([t.name for t in user.possible_transitions()], + self.assertEquals(iworkflowable.state, 'asleep') + self.assertEquals([t.name for t in iworkflowable.possible_transitions()], ['rest']) - self.assertEquals(parse_hist(user.workflow_history), + self.assertEquals(parse_hist(iworkflowable.workflow_history), [('asleep', 'asleep', 'rest', None)]) user.set_attributes(surname=u'toto') # fulfill condition self.commit() - user.fire_transition('rest') + iworkflowable.fire_transition('rest') self.commit() user.clear_all_caches() - self.assertEquals(user.state, 'dead') - self.assertEquals(parse_hist(user.workflow_history), + self.assertEquals(iworkflowable.state, 'dead') + self.assertEquals(parse_hist(iworkflowable.workflow_history), [('asleep', 'asleep', 'rest', None), ('asleep', 'asleep', 'rest', None), ('asleep', 'dead', 'sick', None),]) @@ -505,7 +520,8 @@ self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s', {'wf': wf.eid, 'x': user.eid}) self.commit() - self.assertEquals(user.state, 'dead') + iworkflowable = user.cw_adapt_to('IWorkflowable') + self.assertEquals(iworkflowable.state, 'dead') def test_auto_transition_initial_state_fired(self): wf = self.execute('Any WF WHERE ET default_workflow WF, ' @@ -517,14 +533,15 @@ self.commit() user = self.create_user('member', surname=u'toto') self.commit() - self.assertEquals(user.state, 'dead') + iworkflowable = user.cw_adapt_to('IWorkflowable') + self.assertEquals(iworkflowable.state, 'dead') class WorkflowHooksTC(CubicWebTC): def setUp(self): CubicWebTC.setUp(self) - self.wf = self.session.user.current_workflow + self.wf = self.session.user.cw_adapt_to('IWorkflowable').current_workflow self.session.set_pool() self.s_activated = self.wf.state_by_name('activated').eid self.s_deactivated = self.wf.state_by_name('deactivated').eid @@ -572,8 +589,9 @@ def test_transition_checking1(self): cnx = self.login('stduser') user = cnx.user(self.session) + iworkflowable = user.cw_adapt_to('IWorkflowable') ex = self.assertRaises(ValidationError, - user.fire_transition, 'activate') + iworkflowable.fire_transition, 'activate') self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']), u"transition isn't allowed from") cnx.close() @@ -581,8 +599,9 @@ def test_transition_checking2(self): cnx = self.login('stduser') user = cnx.user(self.session) + iworkflowable = user.cw_adapt_to('IWorkflowable') ex = self.assertRaises(ValidationError, - user.fire_transition, 'dummy') + iworkflowable.fire_transition, 'dummy') self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']), u"transition isn't allowed from") cnx.close() @@ -591,15 +610,16 @@ cnx = self.login('stduser') session = self.session user = cnx.user(session) - user.fire_transition('deactivate') + iworkflowable = user.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') cnx.commit() session.set_pool() ex = self.assertRaises(ValidationError, - user.fire_transition, 'deactivate') + iworkflowable.fire_transition, 'deactivate') self.assertEquals(self._cleanup_msg(ex.errors['by_transition-subject']), u"transition isn't allowed from") # get back now - user.fire_transition('activate') + iworkflowable.fire_transition('activate') cnx.commit() cnx.close() diff -r a64f48dd5fe4 -r 9ab2b4c74baf entities/wfobjs.py --- a/entities/wfobjs.py Thu May 20 20:47:13 2010 +0200 +++ b/entities/wfobjs.py Thu May 20 20:47:55 2010 +0200 @@ -15,9 +15,13 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""workflow definition and history related entities +"""workflow handling: +* entity types defining workflow (Workflow, State, Transition...) +* workflow history (TrInfo) +* adapter for workflowable entities (IWorkflowableAdapter) """ + __docformat__ = "restructuredtext en" from warnings import warn @@ -27,7 +31,8 @@ from logilab.common.compat import any from cubicweb.entities import AnyEntity, fetch_config -from cubicweb.interfaces import IWorkflowable +from cubicweb.view import EntityAdapter +from cubicweb.selectors import relation_possible from cubicweb.mixins import MI_REL_TRIGGERS class WorkflowException(Exception): pass @@ -47,15 +52,6 @@ return any(et for et in self.reverse_default_workflow if et.name == etype) - # XXX define parent() instead? what if workflow of multiple types? - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.workflow_of: - return self.workflow_of[0].rest_path(), {'vid': 'workflow'} - return super(Workflow, self).after_deletion_path() - def iter_workflows(self, _done=None): """return an iterator on actual workflows, eg this workflow and its subworkflows @@ -226,14 +222,6 @@ return False return True - def after_deletion_path(self): - """return (path, parameters) which should be used as redirect - information when this entity is being deleted - """ - if self.transition_of: - return self.transition_of[0].rest_path(), {} - return super(BaseTransition, self).after_deletion_path() - def set_permissions(self, requiredgroups=(), conditions=(), reset=True): """set or add (if `reset` is False) groups and conditions for this transition @@ -277,7 +265,7 @@ try: return self.destination_state[0] except IndexError: - return entity.latest_trinfo().previous_state + return entity.cw_adapt_to('IWorkflowable').latest_trinfo().previous_state def potential_destinations(self): try: @@ -288,9 +276,6 @@ for previousstate in tr.reverse_allowed_transition: yield previousstate - def parent(self): - return self.workflow - class WorkflowTransition(BaseTransition): """customized class for WorkflowTransition entities""" @@ -331,7 +316,7 @@ return None if tostateeid is None: # go back to state from which we've entered the subworkflow - return entity.subworkflow_input_trinfo().previous_state + return entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo().previous_state return self._cw.entity_from_eid(tostateeid) @cached @@ -358,9 +343,6 @@ def destination(self): return self.destination_state and self.destination_state[0] or None - def parent(self): - return self.reverse_subworkflow_exit[0] - class State(AnyEntity): """customized class for State entities""" @@ -371,10 +353,7 @@ @property def workflow(self): # take care, may be missing in multi-sources configuration - return self.state_of and self.state_of[0] - - def parent(self): - return self.workflow + return self.state_of and self.state_of[0] or None class TrInfo(AnyEntity): @@ -399,22 +378,99 @@ def transition(self): return self.by_transition and self.by_transition[0] or None - def parent(self): - return self.for_entity - class WorkflowableMixIn(object): """base mixin providing workflow helper methods for workflowable entities. This mixin will be automatically set on class supporting the 'in_state' relation (which implies supporting 'wf_info_for' as well) """ - __implements__ = (IWorkflowable,) + + @property + @deprecated('[3.5] use printable_state') + def displayable_state(self): + return self._cw._(self.state) + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').main_workflow") + def main_workflow(self): + return self.cw_adapt_to('IWorkflowable').main_workflow + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_workflow") + def current_workflow(self): + return self.cw_adapt_to('IWorkflowable').current_workflow + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').current_state") + def current_state(self): + return self.cw_adapt_to('IWorkflowable').current_state + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').state") + def state(self): + return self.cw_adapt_to('IWorkflowable').state + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').printable_state") + def printable_state(self): + return self.cw_adapt_to('IWorkflowable').printable_state + @property + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').workflow_history") + def workflow_history(self): + return self.cw_adapt_to('IWorkflowable').workflow_history + + @deprecated('[3.5] get transition from current workflow and use its may_be_fired method') + def can_pass_transition(self, trname): + """return the Transition instance if the current user can fire the + transition with the given name, else None + """ + tr = self.current_workflow and self.current_workflow.transition_by_name(trname) + if tr and tr.may_be_fired(self.eid): + return tr + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').cwetype_workflow()") + def cwetype_workflow(self): + return self.cw_adapt_to('IWorkflowable').main_workflow() + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').latest_trinfo()") + def latest_trinfo(self): + return self.cw_adapt_to('IWorkflowable').latest_trinfo() + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').possible_transitions()") + def possible_transitions(self, type='normal'): + return self.cw_adapt_to('IWorkflowable').possible_transitions(type) + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').fire_transition()") + def fire_transition(self, tr, comment=None, commentformat=None): + return self.cw_adapt_to('IWorkflowable').fire_transition(tr, comment, commentformat) + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').change_state()") + def change_state(self, statename, comment=None, commentformat=None, tr=None): + return self.cw_adapt_to('IWorkflowable').change_state(statename, comment, commentformat, tr) + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo()") + def subworkflow_input_trinfo(self): + return self.cw_adapt_to('IWorkflowable').subworkflow_input_trinfo() + @deprecated("[3.9] use entity.cw_adapt_to('IWorkflowable').subworkflow_input_transition()") + def subworkflow_input_transition(self): + return self.cw_adapt_to('IWorkflowable').subworkflow_input_transition() + + +MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn + + + +class IWorkflowableAdapter(WorkflowableMixIn, EntityAdapter): + """base adapter providing workflow helper methods for workflowable entities. + """ + __regid__ = 'IWorkflowable' + __select__ = relation_possible('in_state') + + @cached + def cwetype_workflow(self): + """return the default workflow for entities of this type""" + # XXX CWEType method + wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, ' + 'ET name %(et)s', {'et': self.entity.__regid__}) + if wfrset: + return wfrset.get_entity(0, 0) + self.warning("can't find any workflow for %s", self.entity.__regid__) + return None @property def main_workflow(self): """return current workflow applied to this entity""" - if self.custom_workflow: - return self.custom_workflow[0] + if self.entity.custom_workflow: + return self.entity.custom_workflow[0] return self.cwetype_workflow() @property @@ -425,14 +481,14 @@ @property def current_state(self): """return current state entity""" - return self.in_state and self.in_state[0] or None + return self.entity.in_state and self.entity.in_state[0] or None @property def state(self): """return current state name""" try: - return self.in_state[0].name - except IndexError: + return self.current_state.name + except AttributeError: self.warning('entity %s has no state', self) return None @@ -449,26 +505,15 @@ """return the workflow history for this entity (eg ordered list of TrInfo entities) """ - return self.reverse_wf_info_for + return self.entity.reverse_wf_info_for def latest_trinfo(self): """return the latest transition information for this entity""" try: - return self.reverse_wf_info_for[-1] + return self.workflow_history[-1] except IndexError: return None - @cached - def cwetype_workflow(self): - """return the default workflow for entities of this type""" - # XXX CWEType method - wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, ' - 'ET name %(et)s', {'et': self.__regid__}) - if wfrset: - return wfrset.get_entity(0, 0) - self.warning("can't find any workflow for %s", self.__regid__) - return None - def possible_transitions(self, type='normal'): """generates transition that MAY be fired for the given entity, expected to be in this state @@ -483,16 +528,44 @@ {'x': self.current_state.eid, 'type': type, 'wfeid': self.current_workflow.eid}) for tr in rset.entities(): - if tr.may_be_fired(self.eid): + if tr.may_be_fired(self.entity.eid): yield tr + def subworkflow_input_trinfo(self): + """return the TrInfo which has be recorded when this entity went into + the current sub-workflow + """ + if self.main_workflow.eid == self.current_workflow.eid: + return # doesn't make sense + subwfentries = [] + for trinfo in self.workflow_history: + if (trinfo.transition and + trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid): + # entering or leaving a subworkflow + if (subwfentries and + subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and + subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid): + # leave + del subwfentries[-1] + else: + # enter + subwfentries.append(trinfo) + if not subwfentries: + return None + return subwfentries[-1] + + def subworkflow_input_transition(self): + """return the transition which has went through the current sub-workflow + """ + return getattr(self.subworkflow_input_trinfo(), 'transition', None) + def _add_trinfo(self, comment, commentformat, treid=None, tseid=None): kwargs = {} if comment is not None: kwargs['comment'] = comment if commentformat is not None: kwargs['comment_format'] = commentformat - kwargs['wf_info_for'] = self + kwargs['wf_info_for'] = self.entity if treid is not None: kwargs['by_transition'] = self._cw.entity_from_eid(treid) if tseid is not None: @@ -532,51 +605,3 @@ stateeid = state.eid # XXX try to find matching transition? return self._add_trinfo(comment, commentformat, tr and tr.eid, stateeid) - - def subworkflow_input_trinfo(self): - """return the TrInfo which has be recorded when this entity went into - the current sub-workflow - """ - if self.main_workflow.eid == self.current_workflow.eid: - return # doesn't make sense - subwfentries = [] - for trinfo in self.workflow_history: - if (trinfo.transition and - trinfo.previous_state.workflow.eid != trinfo.new_state.workflow.eid): - # entering or leaving a subworkflow - if (subwfentries and - subwfentries[-1].new_state.workflow.eid == trinfo.previous_state.workflow.eid and - subwfentries[-1].previous_state.workflow.eid == trinfo.new_state.workflow.eid): - # leave - del subwfentries[-1] - else: - # enter - subwfentries.append(trinfo) - if not subwfentries: - return None - return subwfentries[-1] - - def subworkflow_input_transition(self): - """return the transition which has went through the current sub-workflow - """ - return getattr(self.subworkflow_input_trinfo(), 'transition', None) - - def clear_all_caches(self): - super(WorkflowableMixIn, self).clear_all_caches() - clear_cache(self, 'cwetype_workflow') - - @deprecated('[3.5] get transition from current workflow and use its may_be_fired method') - def can_pass_transition(self, trname): - """return the Transition instance if the current user can fire the - transition with the given name, else None - """ - tr = self.current_workflow and self.current_workflow.transition_by_name(trname) - if tr and tr.may_be_fired(self.eid): - return tr - - @property - @deprecated('[3.5] use printable_state') - def displayable_state(self): - return self._cw._(self.state) - -MI_REL_TRIGGERS[('in_state', 'subject')] = WorkflowableMixIn diff -r a64f48dd5fe4 -r 9ab2b4c74baf entity.py --- a/entity.py Thu May 20 20:47:13 2010 +0200 +++ b/entity.py Thu May 20 20:47:55 2010 +0200 @@ -107,10 +107,10 @@ if not interface.implements(cls, iface): interface.extend(cls, iface) if role == 'subject': - setattr(cls, rschema.type, SubjectRelation(rschema)) + attr = rschema.type else: attr = 'reverse_%s' % rschema.type - setattr(cls, attr, ObjectRelation(rschema)) + setattr(cls, attr, Relation(rschema, role)) if mixins: # see etype class instantation in cwvreg.ETypeRegistry.etype_class method: # due to class dumping, cls is the generated top level class with actual @@ -125,6 +125,24 @@ cls.__bases__ = tuple(mixins) cls.info('plugged %s mixins on %s', mixins, cls) + fetch_attrs = ('modification_date',) + @classmethod + def fetch_order(cls, attr, var): + """class method used to control sort order when multiple entities of + this type are fetched + """ + return cls.fetch_unrelated_order(attr, var) + + @classmethod + def fetch_unrelated_order(cls, attr, var): + """class method used to control sort order when multiple entities of + this type are fetched to use in edition (eg propose them to create a + new relation on an edited entity). + """ + if attr == 'modification_date': + return '%s DESC' % var + return None + @classmethod def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X', settype=True, ordermethod='fetch_order'): @@ -378,6 +396,23 @@ 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. + + return None if it can not be adapted. + """ + try: + cache = self._cw_adapters_cache + except AttributeError: + self._cw_adapters_cache = cache = {} + try: + return cache[interface] + except KeyError: + adapter = self._cw.vreg['adapters'].select_or_none( + interface, self._cw, entity=self) + cache[interface] = adapter + return adapter + def rql_set_value(self, attr, value): """call by rql execution plan when some attribute is modified @@ -949,6 +984,10 @@ del self.__unique except AttributeError: pass + try: + del self._cw_adapters_cache + except AttributeError: + pass # raw edition utilities ################################################### @@ -1038,61 +1077,6 @@ self.e_schema.check(self, creation=creation, _=_, relations=relations) - def fti_containers(self, _done=None): - if _done is None: - _done = set() - _done.add(self.eid) - containers = tuple(self.e_schema.fulltext_containers()) - if containers: - for rschema, target in containers: - if target == 'object': - targets = getattr(self, rschema.type) - else: - targets = getattr(self, 'reverse_%s' % rschema) - for entity in targets: - if entity.eid in _done: - continue - for container in entity.fti_containers(_done): - yield container - yielded = True - else: - yield self - - def get_words(self): - """used by the full text indexer to get words to index - - this method should only be used on the repository side since it depends - on the logilab.database package - - :rtype: list - :return: the list of indexable word of this entity - """ - from logilab.database.fti import tokenize - # take care to cases where we're modyfying the schema - pending = self._cw.transaction_data.setdefault('pendingrdefs', set()) - words = [] - for rschema in self.e_schema.indexable_attributes(): - if (self.e_schema, rschema) in pending: - continue - try: - value = self.printable_value(rschema, format='text/plain') - except TransformError: - continue - except: - self.exception("can't add value of %s to text index for entity %s", - rschema, self.eid) - continue - if value: - words += tokenize(value) - for rschema, role in self.e_schema.fulltext_relations(): - if role == 'subject': - for entity in getattr(self, rschema.type): - words += entity.get_words() - else: # if role == 'object': - for entity in getattr(self, 'reverse_%s' % rschema.type): - words += entity.get_words() - return words - # attribute and relation descriptors ########################################## @@ -1111,13 +1095,13 @@ def __set__(self, eobj, value): eobj[self._attrname] = value + class Relation(object): """descriptor that controls schema relation access""" - _role = None # for pylint - def __init__(self, rschema): - self._rschema = rschema + def __init__(self, rschema, role): self._rtype = rschema.type + self._role = role def __get__(self, eobj, eclass): if eobj is None: @@ -1129,14 +1113,6 @@ raise NotImplementedError -class SubjectRelation(Relation): - """descriptor that controls schema relation access""" - _role = 'subject' - -class ObjectRelation(Relation): - """descriptor that controls schema relation access""" - _role = 'object' - from logging import getLogger from cubicweb import set_log_methods set_log_methods(Entity, getLogger('cubicweb.entity')) diff -r a64f48dd5fe4 -r 9ab2b4c74baf goa/appobjects/components.py --- a/goa/appobjects/components.py Thu May 20 20:47:13 2010 +0200 +++ b/goa/appobjects/components.py Thu May 20 20:47:55 2010 +0200 @@ -98,7 +98,7 @@ def sendmail(self, recipient, subject, body): sender = '%s <%s>' % ( self.req.user.dc_title() or self.config['sender-name'], - self.req.user.get_email() or self.config['sender-addr']) + self.req.user.cw_adapt_to('IEmailable').get_email() or self.config['sender-addr']) mail.send_mail(sender=sender, to=recipient, subject=subject, body=body) diff -r a64f48dd5fe4 -r 9ab2b4c74baf hooks/syncschema.py --- a/hooks/syncschema.py Thu May 20 20:47:13 2010 +0200 +++ b/hooks/syncschema.py Thu May 20 20:47:55 2010 +0200 @@ -1175,7 +1175,7 @@ still_fti = list(schema[etype].indexable_attributes()) for entity in rset.entities(): source.fti_unindex_entity(session, entity.eid) - for container in entity.fti_containers(): + for container in entity.cw_adapt_to('IFTIndexable').fti_containers(): if still_fti or container is not entity: source.fti_unindex_entity(session, entity.eid) source.fti_index_entity(session, container) diff -r a64f48dd5fe4 -r 9ab2b4c74baf hooks/workflow.py --- a/hooks/workflow.py Thu May 20 20:47:13 2010 +0200 +++ b/hooks/workflow.py Thu May 20 20:47:55 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: workflow related hooks +"""Core hooks: workflow related hooks""" -""" __docformat__ = "restructuredtext en" from datetime import datetime @@ -25,8 +24,7 @@ from yams.schema import role_name from cubicweb import RepositoryError, ValidationError -from cubicweb.interfaces import IWorkflowable -from cubicweb.selectors import implements +from cubicweb.selectors import implements, adaptable from cubicweb.server import hook @@ -51,11 +49,12 @@ def precommit_event(self): session = self.session entity = self.entity + iworkflowable = entity.cw_adapt_to('IWorkflowable') # if there is an initial state and the entity's state is not set, # use the initial state as a default state if not (session.deleted_in_transaction(entity.eid) or entity.in_state) \ - and entity.current_workflow: - state = entity.current_workflow.initial + and iworkflowable.current_workflow: + state = iworkflowable.current_workflow.initial if state: session.add_relation(entity.eid, 'in_state', state.eid) _FireAutotransitionOp(session, entity=entity) @@ -65,10 +64,11 @@ def precommit_event(self): entity = self.entity - autotrs = list(entity.possible_transitions('auto')) + iworkflowable = entity.cw_adapt_to('IWorkflowable') + autotrs = list(iworkflowable.possible_transitions('auto')) if autotrs: assert len(autotrs) == 1 - entity.fire_transition(autotrs[0]) + iworkflowable.fire_transition(autotrs[0]) class _WorkflowChangedOp(hook.Operation): @@ -82,29 +82,30 @@ if self.eid in pendingeids: return entity = session.entity_from_eid(self.eid) + iworkflowable = entity.cw_adapt_to('IWorkflowable') # check custom workflow has not been rechanged to another one in the same # transaction - mainwf = entity.main_workflow + mainwf = iworkflowable.main_workflow if mainwf.eid == self.wfeid: deststate = mainwf.initial if not deststate: qname = role_name('custom_workflow', 'subject') msg = session._('workflow has no initial state') raise ValidationError(entity.eid, {qname: msg}) - if mainwf.state_by_eid(entity.current_state.eid): + if mainwf.state_by_eid(iworkflowable.current_state.eid): # nothing to do return # if there are no history, simply go to new workflow's initial state - if not entity.workflow_history: - if entity.current_state.eid != deststate.eid: + if not iworkflowable.workflow_history: + if iworkflowable.current_state.eid != deststate.eid: _change_state(session, entity.eid, - entity.current_state.eid, deststate.eid) + iworkflowable.current_state.eid, deststate.eid) _FireAutotransitionOp(session, entity=entity) return msg = session._('workflow changed to "%s"') msg %= session._(mainwf.name) session.transaction_data[(entity.eid, 'customwf')] = self.wfeid - entity.change_state(deststate, msg, u'text/plain') + iworkflowable.change_state(deststate, msg, u'text/plain') class _CheckTrExitPoint(hook.Operation): @@ -125,9 +126,10 @@ def precommit_event(self): session = self.session forentity = self.forentity + iworkflowable = forentity.cw_adapt_to('IWorkflowable') trinfo = self.trinfo # we're in a subworkflow, check if we've reached an exit point - wftr = forentity.subworkflow_input_transition() + wftr = iworkflowable.subworkflow_input_transition() if wftr is None: # inconsistency detected qname = role_name('to_state', 'subject') @@ -137,9 +139,9 @@ if tostate is not None: # reached an exit point msg = session._('exiting from subworkflow %s') - msg %= session._(forentity.current_workflow.name) + msg %= session._(iworkflowable.current_workflow.name) session.transaction_data[(forentity.eid, 'subwfentrytr')] = True - forentity.change_state(tostate, msg, u'text/plain', tr=wftr) + iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr) # hooks ######################################################################## @@ -151,7 +153,7 @@ class SetInitialStateHook(WorkflowHook): __regid__ = 'wfsetinitial' - __select__ = WorkflowHook.__select__ & implements(IWorkflowable) + __select__ = WorkflowHook.__select__ & adaptable('IWorkflowable') events = ('after_add_entity',) def __call__(self): @@ -189,18 +191,19 @@ msg = session._('mandatory relation') raise ValidationError(entity.eid, {qname: msg}) forentity = session.entity_from_eid(foreid) + iworkflowable = forentity.cw_adapt_to('IWorkflowable') # then check it has a workflow set, unless we're in the process of changing # entity's workflow if session.transaction_data.get((forentity.eid, 'customwf')): wfeid = session.transaction_data[(forentity.eid, 'customwf')] wf = session.entity_from_eid(wfeid) else: - wf = forentity.current_workflow + wf = iworkflowable.current_workflow if wf is None: msg = session._('related entity has no workflow set') raise ValidationError(entity.eid, {None: msg}) # then check it has a state set - fromstate = forentity.current_state + fromstate = iworkflowable.current_state if fromstate is None: msg = session._('related entity has no state') raise ValidationError(entity.eid, {None: msg}) @@ -278,8 +281,9 @@ _change_state(self._cw, trinfo['wf_info_for'], trinfo['from_state'], trinfo['to_state']) forentity = self._cw.entity_from_eid(trinfo['wf_info_for']) - assert forentity.current_state.eid == trinfo['to_state'] - if forentity.main_workflow.eid != forentity.current_workflow.eid: + iworkflowable = forentity.cw_adapt_to('IWorkflowable') + assert iworkflowable.current_state.eid == trinfo['to_state'] + if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid: _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo) @@ -297,7 +301,8 @@ # state changed through TrInfo insertion, so we already know it's ok return entity = session.entity_from_eid(self.eidfrom) - mainwf = entity.main_workflow + iworkflowable = entity.cw_adapt_to('IWorkflowable') + mainwf = iworkflowable.main_workflow if mainwf is None: msg = session._('entity has no workflow set') raise ValidationError(entity.eid, {None: msg}) @@ -309,7 +314,7 @@ msg = session._("state doesn't belong to entity's workflow. You may " "want to set a custom workflow for this entity first.") raise ValidationError(self.eidfrom, {qname: msg}) - if entity.current_workflow and wf.eid != entity.current_workflow.eid: + if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid: qname = role_name('in_state', 'subject') msg = session._("state doesn't belong to entity's current workflow") raise ValidationError(self.eidfrom, {qname: msg}) @@ -359,7 +364,7 @@ def __call__(self): entity = self._cw.entity_from_eid(self.eidfrom) - typewf = entity.cwetype_workflow() + typewf = entity.cw_adapt_to('IWorkflowable').cwetype_workflow() if typewf is not None: _WorkflowChangedOp(self._cw, eid=self.eidfrom, wfeid=typewf.eid) diff -r a64f48dd5fe4 -r 9ab2b4c74baf interfaces.py --- a/interfaces.py Thu May 20 20:47:13 2010 +0200 +++ b/interfaces.py Thu May 20 20:47:55 2010 +0200 @@ -15,68 +15,24 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -""" -Standard interfaces. +"""Standard interfaces. Deprecated in favor of adapters. .. note:: - The `implements` selector matches not only entity classes but also - their interfaces. Writing __select__ = implements('IGeocodable') is - a perfectly fine thing to do. + The `implements` selector used to match not only entity classes but also their + interfaces. This will disappear in a future version. You should define an + adapter for that interface and use `adaptable('MyIFace')` selector on appobjects + that require that interface. + """ __docformat__ = "restructuredtext en" from logilab.common.interface import Interface -class IEmailable(Interface): - """interface for emailable entities""" - def get_email(self): - """return email address""" - - @classmethod - def allowed_massmail_keys(cls): - """returns a set of allowed email substitution keys - - The default is to return the entity's attribute list but an - entity class might override this method to allow extra keys. - For instance, the Person class might want to return a `companyname` - key. - """ - - def as_email_context(self): - """returns the dictionary as used by the sendmail controller to - build email bodies. - - NOTE: the dictionary keys should match the list returned by the - `allowed_massmail_keys` method. - """ - - -class IWorkflowable(Interface): - """interface for entities dealing with a specific workflow""" - # XXX to be completed, see cw.entities.wfobjs.WorkflowableMixIn - - @property - def state(self): - """return current state name""" - - def change_state(self, stateeid, trcomment=None, trcommentformat=None): - """change the entity's state to the state of the given name in entity's - workflow - """ - - def latest_trinfo(self): - """return the latest transition information for this entity - """ - - +# XXX deprecates in favor of IProgressAdapter class IProgress(Interface): - """something that has a cost, a state and a progression - - Take a look at cubicweb.mixins.ProgressMixIn for some - default implementations - """ + """something that has a cost, a state and a progression""" @property def cost(self): @@ -112,7 +68,7 @@ def progress(self): """returns the % progress of the task item""" - +# XXX deprecates in favor of IMileStoneAdapter class IMileStone(IProgress): """represents an ITask's item""" @@ -135,7 +91,132 @@ def contractors(self): """returns the list of persons supposed to work on this task""" +# XXX deprecates in favor of IEmbedableAdapter +class IEmbedable(Interface): + """interface for embedable entities""" + def embeded_url(self): + """embed action interface""" + +# XXX deprecates in favor of ICalendarAdapter +class ICalendarViews(Interface): + """calendar views interface""" + def matching_dates(self, begin, end): + """ + :param begin: day considered as begin of the range (`DateTime`) + :param end: day considered as end of the range (`DateTime`) + + :return: + a list of dates (`DateTime`) in the range [`begin`, `end`] on which + this entity apply + """ + +# XXX deprecates in favor of ICalendarableAdapter +class ICalendarable(Interface): + """interface for items that do have a begin date 'start' and an end date 'stop' + """ + + @property + def start(self): + """return start date""" + + @property + def stop(self): + """return stop state""" + +# XXX deprecates in favor of ICalendarableAdapter +class ITimetableViews(Interface): + """timetable views interface""" + def timetable_date(self): + """XXX explain + + :return: date (`DateTime`) + """ + +# XXX deprecates in favor of IGeocodableAdapter +class IGeocodable(Interface): + """interface required by geocoding views such as gmap-view""" + + @property + def latitude(self): + """returns the latitude of the entity""" + + @property + def longitude(self): + """returns the longitude of the entity""" + + def marker_icon(self): + """returns the icon that should be used as the marker""" + +# XXX deprecates in favor of ISIOCItemAdapter +class ISiocItem(Interface): + """interface for entities which may be represented as an ISIOC item""" + + def isioc_content(self): + """return item's content""" + + def isioc_container(self): + """return container entity""" + + def isioc_type(self): + """return container type (post, BlogPost, MailMessage)""" + + def isioc_replies(self): + """return replies items""" + + def isioc_topics(self): + """return topics items""" + +# XXX deprecates in favor of ISIOCContainerAdapter +class ISiocContainer(Interface): + """interface for entities which may be represented as an ISIOC container""" + + def isioc_type(self): + """return container type (forum, Weblog, MailingList)""" + + def isioc_items(self): + """return contained items""" + +# XXX deprecates in favor of IEmailableAdapter +class IFeed(Interface): + """interface for entities with rss flux""" + + def rss_feed_url(self): + """""" + +# XXX deprecates in favor of IDownloadableAdapter +class IDownloadable(Interface): + """interface for downloadable entities""" + + def download_url(self): # XXX not really part of this interface + """return an url to download entity's content""" + def download_content_type(self): + """return MIME type of the downloadable content""" + def download_encoding(self): + """return encoding of the downloadable content""" + def download_file_name(self): + """return file name of the downloadable content""" + def download_data(self): + """return actual data of the downloadable content""" + +# XXX deprecates in favor of IPrevNextAdapter +class IPrevNext(Interface): + """interface for entities which can be linked to a previous and/or next + entity + """ + + def next_entity(self): + """return the 'next' entity""" + def previous_entity(self): + """return the 'previous' entity""" + +# XXX deprecates in favor of IBreadCrumbsAdapter +class IBreadCrumbs(Interface): + + def breadcrumbs(self, view, recurs=False): + pass + +# XXX deprecates in favor of ITreeAdapter class ITree(Interface): def parent(self): @@ -159,141 +240,3 @@ def root(self): """returns the root object""" - -## web specific interfaces #################################################### - - -class IPrevNext(Interface): - """interface for entities which can be linked to a previous and/or next - entity - """ - - def next_entity(self): - """return the 'next' entity""" - def previous_entity(self): - """return the 'previous' entity""" - - -class IBreadCrumbs(Interface): - """interface for entities which can be "located" on some path""" - - # XXX fix recurs ! - def breadcrumbs(self, view, recurs=False): - """return a list containing some: - - * tuple (url, label) - * entity - * simple label string - - defining path from a root to the current view - - the main view is given as argument so breadcrumbs may vary according - to displayed view (may be None). When recursing on a parent entity, - the `recurs` argument should be set to True. - """ - - -class IDownloadable(Interface): - """interface for downloadable entities""" - - def download_url(self): # XXX not really part of this interface - """return an url to download entity's content""" - def download_content_type(self): - """return MIME type of the downloadable content""" - def download_encoding(self): - """return encoding of the downloadable content""" - def download_file_name(self): - """return file name of the downloadable content""" - def download_data(self): - """return actual data of the downloadable content""" - - -class IEmbedable(Interface): - """interface for embedable entities""" - - def embeded_url(self): - """embed action interface""" - -class ICalendarable(Interface): - """interface for items that do have a begin date 'start' and an end date 'stop' - """ - - @property - def start(self): - """return start date""" - - @property - def stop(self): - """return stop state""" - -class ICalendarViews(Interface): - """calendar views interface""" - def matching_dates(self, begin, end): - """ - :param begin: day considered as begin of the range (`DateTime`) - :param end: day considered as end of the range (`DateTime`) - - :return: - a list of dates (`DateTime`) in the range [`begin`, `end`] on which - this entity apply - """ - -class ITimetableViews(Interface): - """timetable views interface""" - def timetable_date(self): - """XXX explain - - :return: date (`DateTime`) - """ - -class IGeocodable(Interface): - """interface required by geocoding views such as gmap-view""" - - @property - def latitude(self): - """returns the latitude of the entity""" - - @property - def longitude(self): - """returns the longitude of the entity""" - - def marker_icon(self): - """returns the icon that should be used as the marker - (returns None for default) - """ - -class IFeed(Interface): - """interface for entities with rss flux""" - - def rss_feed_url(self): - """return an url which layout sub-entities item - """ - -class ISiocItem(Interface): - """interface for entities (which are item - in sioc specification) with sioc views""" - - def isioc_content(self): - """return content entity""" - - def isioc_container(self): - """return container entity""" - - def isioc_type(self): - """return container type (post, BlogPost, MailMessage)""" - - def isioc_replies(self): - """return replies items""" - - def isioc_topics(self): - """return topics items""" - -class ISiocContainer(Interface): - """interface for entities (which are container - in sioc specification) with sioc views""" - - def isioc_type(self): - """return container type (forum, Weblog, MailingList)""" - - def isioc_items(self): - """return contained items""" diff -r a64f48dd5fe4 -r 9ab2b4c74baf mail.py --- a/mail.py Thu May 20 20:47:13 2010 +0200 +++ b/mail.py Thu May 20 20:47:55 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 . -"""Common utilies to format / semd emails. +"""Common utilies to format / semd emails.""" -""" __docformat__ = "restructuredtext en" from base64 import b64encode, b64decode @@ -182,7 +181,7 @@ # previous email if not self.msgid_timestamp: refs = [self.construct_message_id(eid) - for eid in entity.notification_references(self)] + for eid in entity.cw_adapt_to('INotifiable').notification_references(self)] else: refs = () msgid = self.construct_message_id(entity.eid) @@ -196,7 +195,7 @@ if isinstance(something, Entity): # hi-jack self._cw to get a session for the returned user self._cw = self._cw.hijack_user(something) - emailaddr = something.get_email() + emailaddr = something.cw_adapt_to('IEmailable').get_email() else: emailaddr, lang = something self._cw.set_language(lang) @@ -244,7 +243,8 @@ # email generation helpers ################################################# def construct_message_id(self, eid): - return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp) + return construct_message_id(self._cw.vreg.config.appid, eid, + self.msgid_timestamp) def format_field(self, attr, value): return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value} diff -r a64f48dd5fe4 -r 9ab2b4c74baf mixins.py --- a/mixins.py Thu May 20 20:47:13 2010 +0200 +++ b/mixins.py Thu May 20 20:47:55 2010 +0200 @@ -21,9 +21,10 @@ from itertools import chain from logilab.common.decorators import cached +from logilab.common.deprecation import deprecated, class_deprecated from cubicweb.selectors import implements -from cubicweb.interfaces import IEmailable, ITree +from cubicweb.interfaces import ITree class TreeMixIn(object): @@ -33,6 +34,9 @@ tree_attribute, parent_target and children_target class attribute to benefit from this default implementation """ + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.9] TreeMixIn is deprecated, use/override ITreeAdapter instead' + tree_attribute = None # XXX misnamed parent_target = 'subject' @@ -117,16 +121,6 @@ return chain([self], _uptoroot(self)) return _uptoroot(self) - def notification_references(self, view): - """used to control References field of email send on notification - for this entity. `view` is the notification view. - - Should return a list of eids which can be used to generate message ids - of previously sent email - """ - return self.path()[:-1] - - ## ITree interface ######################################################## def parent(self): """return the parent entity if any, else None (e.g. if we are on the @@ -171,8 +165,7 @@ NOTE: The default implementation is based on the primary_email / use_email scheme """ - __implements__ = (IEmailable,) - + @deprecated("[3.9] use entity.cw_adapt_to('IEmailable').get_email()") def get_email(self): if getattr(self, 'primary_email', None): return self.primary_email[0].address @@ -180,28 +173,6 @@ return self.use_email[0].address return None - @classmethod - def allowed_massmail_keys(cls): - """returns a set of allowed email substitution keys - - The default is to return the entity's attribute list but an - entity class might override this method to allow extra keys. - For instance, the Person class might want to return a `companyname` - key. - """ - return set(rschema.type - for rschema, attrtype in cls.e_schema.attribute_definitions() - if attrtype.type not in ('Password', 'Bytes')) - - def as_email_context(self): - """returns the dictionary as used by the sendmail controller to - build email bodies. - - NOTE: the dictionary keys should match the list returned by the - `allowed_massmail_keys` method. - """ - return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() ) - """pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity classes which have the relation described by the dict's key. @@ -216,26 +187,14 @@ -def _done_init(done, view, row, col): - """handle an infinite recursion safety belt""" - if done is None: - done = set() - entity = view.cw_rset.get_entity(row, col) - if entity.eid in done: - msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { - 'rel': entity.tree_attribute, - 'eid': entity.eid - } - return None, msg - done.add(entity.eid) - return done, entity - - class TreeViewMixIn(object): """a recursive tree view""" + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.9] TreeViewMixIn is deprecated, use/override BaseTreeView instead' + __regid__ = 'tree' + __select__ = implements(ITree) item_vid = 'treeitem' - __select__ = implements(ITree) def call(self, done=None, **kwargs): if done is None: @@ -262,6 +221,8 @@ class TreePathMixIn(object): """a recursive path view""" + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.9] TreePathMixIn is deprecated, use/override TreePathView instead' __regid__ = 'path' item_vid = 'oneline' separator = u' > ' @@ -286,6 +247,8 @@ class ProgressMixIn(object): """provide a default implementations for IProgress interface methods""" + __metaclass__ = class_deprecated + __deprecation_warning__ = '[3.9] ProgressMixIn is deprecated, use/override IProgressAdapter instead' @property def cost(self): diff -r a64f48dd5fe4 -r 9ab2b4c74baf req.py --- a/req.py Thu May 20 20:47:13 2010 +0200 +++ b/req.py Thu May 20 20:47:55 2010 +0200 @@ -279,7 +279,7 @@ user = self.user userinfo['login'] = user.login userinfo['name'] = user.name() - userinfo['email'] = user.get_email() + userinfo['email'] = user.cw_adapt_to('IEmailable').get_email() return userinfo def is_internal_session(self): diff -r a64f48dd5fe4 -r 9ab2b4c74baf selectors.py --- a/selectors.py Thu May 20 20:47:13 2010 +0200 +++ b/selectors.py Thu May 20 20:47:55 2010 +0200 @@ -301,6 +301,7 @@ if iface is basecls: return index + 3 return 0 + # XXX iface in implements deprecated in 3.9 if implements_iface(cls_or_inst, iface): # implenting an interface takes precedence other special Any interface return 2 @@ -527,18 +528,33 @@ * `registry`, a registry name - * `regid`, an object identifier in this registry + * `regids`, object identifiers in this registry, one of them should be + selectable. """ - def __init__(self, registry, regid): + def __init__(self, registry, *regids): self.registry = registry - self.regid = regid + self.regids = regids def __call__(self, cls, req, **kwargs): - try: - req.vreg[self.registry].select(self.regid, req, **kwargs) - return 1 - except NoSelectableObject: - return 0 + for regid in self.regids: + try: + req.vreg[self.registry].select(regid, req, **kwargs) + return 1 + except NoSelectableObject: + return 0 + + +class adaptable(appobject_selectable): + """Return 1 if another appobject is selectable using the same input context. + + Initializer arguments: + + * `regids`, adapter identifiers (e.g. interface names) to which the context + (usually entities) should be adaptable. One of them should be selectable + when multiple identifiers are given. + """ + def __init__(self, *regids): + super(adaptable, self).__init__('adapters', *regids) # rset selectors ############################################################## @@ -731,7 +747,12 @@ .. note:: when interface is an entity class, the score will reflect class proximity so the most specific object will be selected. + + .. note:: with cubicweb >= 3.9, you should use adapters instead of + interface, so no interface should be given to this selector. Use + :class:`adaptable` instead. """ + def score_class(self, eclass, req): return self.score_interfaces(req, eclass, eclass) @@ -758,6 +779,26 @@ self.score_entity = intscore +class has_mimetype(EntitySelector): + """Return 1 if the entity adapt to IDownloadable and has the given MIME type. + + You can give 'image/' to match any image for instance, or 'image/png' to match + only PNG images. + """ + def __init__(self, mimetype, once_is_enough=False): + super(has_mimetype, self).__init__(once_is_enough) + self.mimetype = mimetype + + def score_entity(self, entity): + idownloadable = entity.cw_adapt_to('IDownloadable') + if idownloadable is None: + return 0 + mt = idownloadable.download_content_type() + if not (mt and mt.startswith(self.mimetype)): + return 0 + return 1 + + class relation_possible(EntitySelector): """Return 1 for entity that supports the relation, provided that the request's user may do some `action` on it (see below). @@ -1283,17 +1324,18 @@ class is_in_state(score_entity): """return 1 if entity is in one of the states given as argument list - you should use this instead of your own score_entity x: x.state == 'bla' - selector to avoid some gotchas: + you should use this instead of your own :class:`score_entity` selector to + avoid some gotchas: * possible views gives a fake entity with no state - * you must use the latest tr info, not entity.state for repository side + * you must use the latest tr info, not entity.in_state for repository side checking of the current state """ def __init__(self, *states): def score(entity, states=set(states)): + trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo() try: - return entity.latest_trinfo().new_state.name in states + return trinfo.new_state.name in states except AttributeError: return None super(is_in_state, self).__init__(score) diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/migractions.py --- a/server/migractions.py Thu May 20 20:47:13 2010 +0200 +++ b/server/migractions.py Thu May 20 20:47:55 2010 +0200 @@ -1157,10 +1157,10 @@ if commit: self.commit() - @deprecated('[3.5] use entity.fire_transition("transition") or entity.change_state("state")', - stacklevel=3) + @deprecated('[3.5] use iworkflowable.fire_transition("transition") or ' + 'iworkflowable.change_state("state")', stacklevel=3) def cmd_set_state(self, eid, statename, commit=False): - self._cw.entity_from_eid(eid).change_state(statename) + self._cw.entity_from_eid(eid).cw_adapt_to('IWorkflowable').change_state(statename) if commit: self.commit() diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/repository.py --- a/server/repository.py Thu May 20 20:47:13 2010 +0200 +++ b/server/repository.py Thu May 20 20:47:55 2010 +0200 @@ -407,7 +407,7 @@ raise AuthenticationError('authentication failed with all sources') cwuser = self._build_user(session, eid) if self.config.consider_user_state and \ - not cwuser.state in cwuser.AUTHENTICABLE_STATES: + not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES: raise AuthenticationError('user is not in authenticable state') return cwuser diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/sources/native.py --- a/server/sources/native.py Thu May 20 20:47:13 2010 +0200 +++ b/server/sources/native.py Thu May 20 20:47:55 2010 +0200 @@ -1165,7 +1165,8 @@ try: # use cursor_index_object, not cursor_reindex_object since # unindexing done in the FTIndexEntityOp - self.dbhelper.cursor_index_object(entity.eid, entity, + self.dbhelper.cursor_index_object(entity.eid, + entity.cw_adapt_to('IFTIndexable'), session.pool['system']) except Exception: # let KeyboardInterrupt / SystemExit propagate self.exception('error while reindexing %s', entity) @@ -1190,7 +1191,8 @@ # processed return done.add(eid) - for container in session.entity_from_eid(eid).fti_containers(): + iftindexable = session.entity_from_eid(eid).cw_adapt_to('IFTIndexable') + for container in iftindexable.fti_containers(): source.fti_unindex_entity(session, container.eid) source.fti_index_entity(session, container) diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_ldapuser.py --- a/server/test/unittest_ldapuser.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_ldapuser.py Thu May 20 20:47:55 2010 +0200 @@ -178,12 +178,13 @@ cnx = self.login(SYT, password='dummypassword') cu = cnx.cursor() adim = cu.execute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0) - adim.fire_transition('deactivate') + iworkflowable = adim.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') try: cnx.commit() adim.clear_all_caches() self.assertEquals(adim.in_state[0].name, 'deactivated') - trinfo = adim.latest_trinfo() + trinfo = iworkflowable.latest_trinfo() self.assertEquals(trinfo.owned_by[0].login, SYT) # select from_state to skip the user's creation TrInfo rset = self.sexecute('Any U ORDERBY D DESC WHERE WF wf_info_for X,' @@ -195,7 +196,7 @@ # restore db state self.restore_connection() adim = self.sexecute('CWUser X WHERE X login %(login)s', {'login': ADIM}).get_entity(0, 0) - adim.fire_transition('activate') + adim.cw_adapt_to('IWorkflowable').fire_transition('activate') self.sexecute('DELETE X in_group G WHERE X login %(syt)s, G name "managers"', {'syt': SYT}) def test_same_column_names(self): diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_msplanner.py --- a/server/test/unittest_msplanner.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_msplanner.py Thu May 20 20:47:55 2010 +0200 @@ -1722,8 +1722,9 @@ ]) def test_nonregr2(self): - self.session.user.fire_transition('deactivate') - treid = self.session.user.latest_trinfo().eid + iworkflowable = self.session.user.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') + treid = iworkflowable.latest_trinfo().eid self._test('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', [('FetchStep', [('Any X,D WHERE X modification_date D, X is Note', [{'X': 'Note', 'D': 'Datetime'}])], diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_multisources.py Thu May 20 20:47:55 2010 +0200 @@ -307,8 +307,9 @@ {'x': affaire.eid, 'u': ueid}) def test_nonregr2(self): - self.session.user.fire_transition('deactivate') - treid = self.session.user.latest_trinfo().eid + iworkflowable = self.session.user.cw_adapt_to('IWorkflowable') + iworkflowable.fire_transition('deactivate') + treid = iworkflowable.latest_trinfo().eid rset = self.sexecute('Any X ORDERBY D DESC WHERE E eid %(x)s, E wf_info_for X, X modification_date D', {'x': treid}) self.assertEquals(len(rset), 1) diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_repository.py --- a/server/test/unittest_repository.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_repository.py Thu May 20 20:47:55 2010 +0200 @@ -205,7 +205,7 @@ session = repo._get_session(cnxid) session.set_pool() user = session.user - user.fire_transition('deactivate') + user.cw_adapt_to('IWorkflowable').fire_transition('deactivate') rset = repo.execute(cnxid, 'TrInfo T WHERE T wf_info_for X, X eid %(x)s', {'x': user.eid}) self.assertEquals(len(rset), 1) repo.rollback(cnxid) diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_security.py --- a/server/test/unittest_security.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_security.py Thu May 20 20:47:55 2010 +0200 @@ -384,7 +384,7 @@ # Note.para attribute editable by managers or if the note is in "todo" state note = self.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) self.commit() - note.fire_transition('markasdone') + note.cw_adapt_to('IWorkflowable').fire_transition('markasdone') self.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid}) self.commit() cnx = self.login('iaminusersgrouponly') @@ -393,13 +393,13 @@ self.assertRaises(Unauthorized, cnx.commit) note2 = cu.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0) cnx.commit() - note2.fire_transition('markasdone') + note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone') cnx.commit() self.assertEquals(len(cu.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s', {'x': note2.eid})), 0) cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}) self.assertRaises(Unauthorized, cnx.commit) - note2.fire_transition('redoit') + note2.cw_adapt_to('IWorkflowable').fire_transition('redoit') cnx.commit() cu.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid}) cnx.commit() @@ -435,7 +435,7 @@ cnx.commit() self.restore_connection() affaire = self.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) - affaire.fire_transition('abort') + affaire.cw_adapt_to('IWorkflowable').fire_transition('abort') self.commit() self.assertEquals(len(self.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')), 1) @@ -537,14 +537,15 @@ cu = cnx.cursor() self.schema['Affaire'].set_action_permissions('read', ('users',)) aff = cu.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0) - aff.fire_transition('abort') + aff.cw_adapt_to('IWorkflowable').fire_transition('abort') cnx.commit() # though changing a user state (even logged user) is reserved to managers user = cnx.user(self.session) # XXX wether it should raise Unauthorized or ValidationError is not clear # the best would probably ValidationError if the transition doesn't exist # from the current state but Unauthorized if it exists but user can't pass it - self.assertRaises(ValidationError, user.fire_transition, 'deactivate') + self.assertRaises(ValidationError, + user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate') finally: # restore orig perms for action, perms in affaire_perms.iteritems(): @@ -552,15 +553,16 @@ def test_trinfo_security(self): aff = self.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0) + iworkflowable = aff.cw_adapt_to('IWorkflowable') self.commit() - aff.fire_transition('abort') + iworkflowable.fire_transition('abort') self.commit() # can change tr info comment self.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"', {'c': u'bouh!'}) self.commit() aff.clear_related_cache('wf_info_for', 'object') - trinfo = aff.latest_trinfo() + trinfo = iworkflowable.latest_trinfo() self.assertEquals(trinfo.comment, 'bouh!') # but not from_state/to_state aff.clear_related_cache('wf_info_for', role='object') diff -r a64f48dd5fe4 -r 9ab2b4c74baf server/test/unittest_undo.py --- a/server/test/unittest_undo.py Thu May 20 20:47:13 2010 +0200 +++ b/server/test/unittest_undo.py Thu May 20 20:47:55 2010 +0200 @@ -160,8 +160,8 @@ self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': toto.eid})) self.failUnless(self.execute('Any X WHERE X eid %(x)s', {'x': e.eid})) self.failUnless(self.execute('Any X WHERE X has_text "toto@logilab"')) - self.assertEquals(toto.state, 'activated') - self.assertEquals(toto.get_email(), 'toto@logilab.org') + self.assertEquals(toto.cw_adapt_to('IWorkflowable').state, 'activated') + self.assertEquals(toto.cw_adapt_to('IEmailable').get_email(), 'toto@logilab.org') self.assertEquals([(p.pkey, p.value) for p in toto.reverse_for_user], [('ui.default-text-format', 'text/rest')]) self.assertEquals([g.name for g in toto.in_group], diff -r a64f48dd5fe4 -r 9ab2b4c74baf sobjects/notification.py --- a/sobjects/notification.py Thu May 20 20:47:13 2010 +0200 +++ b/sobjects/notification.py Thu May 20 20:47:55 2010 +0200 @@ -46,7 +46,8 @@ mode = self._cw.vreg.config['default-recipients-mode'] if mode == 'users': execute = self._cw.execute - dests = [(u.get_email(), u.property_value('ui.language')) + dests = [(u.cw_adapt_to('IEmailable').get_email(), + u.property_value('ui.language')) for u in execute(self.user_rql, build_descr=True).entities()] elif mode == 'default-dest-addrs': lang = self._cw.vreg.property_value('ui.language') diff -r a64f48dd5fe4 -r 9ab2b4c74baf sobjects/test/unittest_notification.py --- a/sobjects/test/unittest_notification.py Thu May 20 20:47:13 2010 +0200 +++ b/sobjects/test/unittest_notification.py Thu May 20 20:47:55 2010 +0200 @@ -85,7 +85,7 @@ def test_status_change_view(self): req = self.request() u = self.create_user('toto', req=req) - u.fire_transition('deactivate', comment=u'yeah') + u.cw_adapt_to('IWorkflowable').fire_transition('deactivate', comment=u'yeah') self.failIf(MAILBOX) self.commit() self.assertEquals(len(MAILBOX), 1) diff -r a64f48dd5fe4 -r 9ab2b4c74baf sobjects/test/unittest_supervising.py --- a/sobjects/test/unittest_supervising.py Thu May 20 20:47:13 2010 +0200 +++ b/sobjects/test/unittest_supervising.py Thu May 20 20:47:55 2010 +0200 @@ -84,7 +84,7 @@ self.assertEquals(op.to_send[0][1], ['test@logilab.fr']) self.commit() # some other changes ####### - user.fire_transition('deactivate') + user.cw_adapt_to('IWorkflowable').fire_transition('deactivate') sentops = [op for op in session.pending_operations if isinstance(op, SupervisionMailOp)] self.assertEquals(len(sentops), 1) diff -r a64f48dd5fe4 -r 9ab2b4c74baf sobjects/textparsers.py --- a/sobjects/textparsers.py Thu May 20 20:47:13 2010 +0200 +++ b/sobjects/textparsers.py Thu May 20 20:47:55 2010 +0200 @@ -74,10 +74,14 @@ if not hasattr(entity, 'in_state'): self.error('bad change state instruction for eid %s', eid) continue - tr = entity.current_workflow and entity.current_workflow.transition_by_name(trname) + iworkflowable = entity.cw_adapt_to('IWorkflowable') + if iworkflowable.current_workflow: + tr = iworkflowable.current_workflow.transition_by_name(trname) + else: + tr = None if tr and tr.may_be_fired(entity.eid): try: - trinfo = entity.fire_transition(tr) + trinfo = iworkflowable.fire_transition(tr) caller.fire_event('state-changed', {'trinfo': trinfo, 'entity': entity}) except: diff -r a64f48dd5fe4 -r 9ab2b4c74baf test/unittest_entity.py --- a/test/unittest_entity.py Thu May 20 20:47:13 2010 +0200 +++ b/test/unittest_entity.py Thu May 20 20:47:55 2010 +0200 @@ -97,14 +97,14 @@ user = self.execute('INSERT CWUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"', {'pwd': 'toto'}).get_entity(0, 0) self.commit() - user.fire_transition('deactivate') + user.cw_adapt_to('IWorkflowable').fire_transition('deactivate') self.commit() eid2 = self.execute('INSERT CWUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0] e = self.execute('Any X WHERE X eid %(x)s', {'x': eid2}).get_entity(0, 0) e.copy_relations(user.eid) self.commit() e.clear_related_cache('in_state', 'subject') - self.assertEquals(e.state, 'activated') + self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated') def test_related_cache_both(self): user = self.execute('Any X WHERE X eid %(x)s', {'x':self.user().eid}).get_entity(0, 0) @@ -435,7 +435,7 @@ e['data_format'] = 'text/html' e['data_encoding'] = 'ascii' e._cw.transaction_data = {} # XXX req should be a session - self.assertEquals(set(e.get_words()), + self.assertEquals(set(e.cw_adapt_to('IFTIndexable').get_words()), set(['an', 'html', 'file', 'du', 'html', 'some', 'data'])) diff -r a64f48dd5fe4 -r 9ab2b4c74baf view.py --- a/view.py Thu May 20 20:47:13 2010 +0200 +++ b/view.py Thu May 20 20:47:55 2010 +0200 @@ -520,3 +520,34 @@ # XXX a generic '%s%s' % (self.__regid__, self.__registry__.capitalize()) would probably be nicer def div_id(self): return '%sComponent' % self.__regid__ + + +class Adapter(AppObject): + """base class for adapters""" + __registry__ = 'adapters' + + +class EntityAdapter(Adapter): + """base class for entity adapters (eg adapt an entity to an interface)""" + def __init__(self, _cw, **kwargs): + try: + self.entity = kwargs.pop('entity') + except KeyError: + self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0, + kwargs.get('col') or 0) + Adapter.__init__(self, _cw, **kwargs) + + +def implements_adapter_compat(iface): + def _pre39_compat(func): + def decorated(self, *args, **kwargs): + entity = self.entity + if hasattr(entity, func.__name__): + warn('[3.9] %s method is deprecated, define it on a custom ' + '%s for %s instead' % (func.__name__, iface, + entity.__class__), + DeprecationWarning) + return getattr(entity, func.__name__)(*args, **kwargs) + return func(self, *args, **kwargs) + return decorated + return _pre39_compat diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/__init__.py --- a/web/__init__.py Thu May 20 20:47:13 2010 +0200 +++ b/web/__init__.py Thu May 20 20:47:55 2010 +0200 @@ -17,9 +17,8 @@ # with CubicWeb. If not, see . """CubicWeb web client core. You'll need a apache-modpython or twisted publisher to get a full CubicWeb web application - +""" -""" __docformat__ = "restructuredtext en" _ = unicode diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/action.py --- a/web/action.py Thu May 20 20:47:13 2010 +0200 +++ b/web/action.py Thu May 20 20:47:55 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 . -"""abstract action classes for CubicWeb web client +"""abstract action classes for CubicWeb web client""" -""" __docformat__ = "restructuredtext en" _ = unicode diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/controller.py --- a/web/controller.py Thu May 20 20:47:13 2010 +0200 +++ b/web/controller.py Thu May 20 20:47:55 2010 +0200 @@ -106,6 +106,16 @@ view.set_http_cache_headers() self._cw.validate_cache() + def sendmail(self, recipient, subject, body): + senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email() + msg = format_mail({'email' : senderemail, + 'name' : self._cw.user.dc_title(),}, + [recipient], body, subject) + if not self._cw.vreg.config.sendmails([(msg, [recipient])]): + msg = self._cw._('could not connect to the SMTP server') + url = self._cw.build_url(__message=msg) + raise Redirect(url) + def reset(self): """reset form parameters and redirect to a view determinated by given parameters diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/test/unittest_views_basecontrollers.py --- a/web/test/unittest_views_basecontrollers.py Thu May 20 20:47:13 2010 +0200 +++ b/web/test/unittest_views_basecontrollers.py Thu May 20 20:47:55 2010 +0200 @@ -128,7 +128,7 @@ self.assertEquals(e.firstname, u'Th\xe9nault') self.assertEquals(e.surname, u'Sylvain') self.assertEquals([g.eid for g in e.in_group], groupeids) - self.assertEquals(e.state, 'activated') + self.assertEquals(e.cw_adapt_to('IWorkflowable').state, 'activated') def test_create_multiple_linked(self): diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/basecontrollers.py --- a/web/views/basecontrollers.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/basecontrollers.py Thu May 20 20:47:55 2010 +0200 @@ -18,22 +18,17 @@ # with CubicWeb. If not, see . """Set of base controllers, which are directly plugged into the application object to handle publication. - +""" -""" __docformat__ = "restructuredtext en" -from smtplib import SMTP - -from logilab.common.decorators import cached from logilab.common.date import strptime from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, AuthenticationError, typed_eid) -from cubicweb.utils import CubicWebJsonEncoder from cubicweb.selectors import authenticated_user, match_form_params -from cubicweb.mail import format_mail -from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, json_dumps, json +from cubicweb.web import (Redirect, RemoteCallFailed, DirectResponse, + json, json_dumps) from cubicweb.web.controller import Controller from cubicweb.web.views import vid_from_rset, formrenderers @@ -250,7 +245,7 @@ errback = str(self._cw.form.get('__onfailure', 'null')) cbargs = str(self._cw.form.get('__cbargs', 'null')) self._cw.set_content_type('text/html') - jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder) + jsargs = json_dumps((status, args, entity)) return """""" % (domid, callback, errback, jsargs, cbargs) @@ -568,42 +563,8 @@ # XXX move to massmailing -class SendMailController(Controller): - __regid__ = 'sendmail' - __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject') - def recipients(self): - """returns an iterator on email's recipients as entities""" - eids = self._cw.form['recipient'] - # eids may be a string if only one recipient was specified - if isinstance(eids, basestring): - rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids}) - else: - rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids))) - return rset.entities() - - def sendmail(self, recipient, subject, body): - msg = format_mail({'email' : self._cw.user.get_email(), - 'name' : self._cw.user.dc_title(),}, - [recipient], body, subject) - if not self._cw.vreg.config.sendmails([(msg, [recipient])]): - msg = self._cw._('could not connect to the SMTP server') - url = self._cw.build_url(__message=msg) - raise Redirect(url) - - def publish(self, rset=None): - # XXX this allows users with access to an cubicweb instance to use it as - # a mail relay - body = self._cw.form['mailbody'] - subject = self._cw.form['subject'] - for recipient in self.recipients(): - text = body % recipient.as_email_context() - self.sendmail(recipient.get_email(), subject, text) - url = self._cw.build_url(__message=self._cw._('emails successfully sent')) - raise Redirect(url) - - -class MailBugReportController(SendMailController): +class MailBugReportController(Controller): __regid__ = 'reportbug' __select__ = match_form_params('description') @@ -614,7 +575,7 @@ raise Redirect(url) -class UndoController(SendMailController): +class UndoController(Controller): __regid__ = 'undo' __select__ = authenticated_user() & match_form_params('txuuid') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/calendar.py --- a/web/views/calendar.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/calendar.py Thu May 20 20:47:55 2010 +0200 @@ -15,20 +15,36 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""html calendar views +"""html calendar views""" -""" __docformat__ = "restructuredtext en" _ = unicode from datetime import datetime, date, timedelta from logilab.mtconverter import xml_escape -from logilab.common.date import strptime, date_range, todate, todatetime +from logilab.common.date import ONEDAY, strptime, date_range, todate, todatetime from cubicweb.interfaces import ICalendarable -from cubicweb.selectors import implements -from cubicweb.view import EntityView +from cubicweb.selectors import implements, adaptable +from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat + + +class ICalendarableAdapter(EntityAdapter): + __regid__ = 'ICalendarable' + __select__ = implements(ICalendarable) # XXX for bw compat, should be abstract + + @property + @implements_adapter_compat('ICalendarable') + def start(self): + """return start date""" + raise NotImplementedError + + @property + @implements_adapter_compat('ICalendarable') + def stop(self): + """return stop state""" + raise NotImplementedError # useful constants & functions ################################################ @@ -52,7 +68,7 @@ Does apply to ICalendarable compatible entities """ - __select__ = implements(ICalendarable) + __select__ = adaptable('ICalendarable') paginable = False content_type = 'text/calendar' title = _('iCalendar') @@ -66,10 +82,11 @@ event = ical.add('vevent') event.add('summary').value = task.dc_title() event.add('description').value = task.dc_description() - if task.start: - event.add('dtstart').value = task.start - if task.stop: - event.add('dtend').value = task.stop + icalendarable = task.cw_adapt_to('ICalendarable') + if icalendarable.start: + event.add('dtstart').value = icalendarable.start + if icalendarable.stop: + event.add('dtend').value = icalendarable.stop buff = ical.serialize() if not isinstance(buff, unicode): @@ -85,7 +102,7 @@ Does apply to ICalendarable compatible entities """ __regid__ = 'hcal' - __select__ = implements(ICalendarable) + __select__ = adaptable('ICalendarable') paginable = False title = _('hCalendar') #templatable = False @@ -98,10 +115,15 @@ self.w(u'

%s

' % xml_escape(task.dc_title())) self.w(u'
%s
' % task.dc_description(format='text/html')) - if task.start: - self.w(u'%s' % (task.start.isoformat(), self._cw.format_date(task.start))) - if task.stop: - self.w(u'%s' % (task.stop.isoformat(), self._cw.format_date(task.stop))) + icalendarable = task.cw_adapt_to('ICalendarable') + if icalendarable.start: + self.w(u'%s' + % (icalendarable.start.isoformat(), + self._cw.format_date(icalendarable.start))) + if icalendarable.stop: + self.w(u'%s' + % (icalendarable.stop.isoformat(), + self._cw.format_date(icalendarable.stop))) self.w(u'') self.w(u'') @@ -113,10 +135,15 @@ task = self.cw_rset.complete_entity(row, 0) task.view('oneline', w=self.w) if dates: - if task.start and task.stop: - self.w('
' % self._cw._('from %(date)s' % {'date': self._cw.format_date(task.start)})) - self.w('
' % self._cw._('to %(date)s' % {'date': self._cw.format_date(task.stop)})) - self.w('
to %s'%self._cw.format_date(task.stop)) + icalendarable = task.cw_adapt_to('ICalendarable') + if icalendarable.start and icalendarable.stop: + self.w('
%s' % self._cw._('from %(date)s') + % {'date': self._cw.format_date(icalendarable.start)}) + self.w('
%s' % self._cw._('to %(date)s') + % {'date': self._cw.format_date(icalendarable.stop)}) + else: + self.w('
%s'%self._cw.format_date(icalendarable.start + or icalendarable.stop)) class CalendarLargeItemView(CalendarItemView): __regid__ = 'calendarlargeitem' @@ -128,22 +155,25 @@ self.color = color self.index = index self.length = 1 + icalendarable = task.cw_adapt_to('ICalendarable') + self.start = icalendarable.start + self.stop = icalendarable.stop def in_working_hours(self): """predicate returning True is the task is in working hours""" - if todatetime(self.task.start).hour > 7 and todatetime(self.task.stop).hour < 20: + if todatetime(self.start).hour > 7 and todatetime(self.stop).hour < 20: return True return False def is_one_day_task(self): - task = self.task - return task.start and task.stop and task.start.isocalendar() == task.stop.isocalendar() + return self.start and self.stop and self.start.isocalendar() == self.stop.isocalendar() class OneMonthCal(EntityView): """At some point, this view will probably replace ampm calendars""" __regid__ = 'onemonthcal' - __select__ = implements(ICalendarable) + __select__ = adaptable('ICalendarable') + paginable = False title = _('one month') @@ -181,13 +211,14 @@ else: user = None the_dates = [] - tstart = task.start + icalendarable = task.cw_adapt_to('ICalendarable') + tstart = icalendarable.start if tstart: - tstart = todate(task.start) + tstart = todate(icalendarable.start) if tstart > lastday: continue the_dates = [tstart] - tstop = task.stop + tstop = icalendarable.stop if tstop: tstop = todate(tstop) if tstop < firstday: @@ -199,7 +230,7 @@ the_dates = [tstart] else: the_dates = date_range(max(tstart, firstday), - min(tstop, lastday)) + min(tstop + ONEDAY, lastday)) if not the_dates: continue @@ -335,7 +366,8 @@ class OneWeekCal(EntityView): """At some point, this view will probably replace ampm calendars""" __regid__ = 'oneweekcal' - __select__ = implements(ICalendarable) + __select__ = adaptable('ICalendarable') + paginable = False title = _('one week') @@ -368,8 +400,9 @@ continue done_tasks.append(task) the_dates = [] - tstart = task.start - tstop = task.stop + icalendarable = task.cw_adapt_to('ICalendarable') + tstart = icalendarable.start + tstop = icalendarable.stop if tstart: tstart = todate(tstart) if tstart > lastday: @@ -382,7 +415,7 @@ the_dates = [tstop] if tstart and tstop: the_dates = date_range(max(tstart, firstday), - min(tstop, lastday)) + min(tstop + ONEDAY, lastday)) if not the_dates: continue @@ -462,7 +495,7 @@ def _build_calendar_cell(self, date, task_descrs): inday_tasks = [t for t in task_descrs if t.is_one_day_task() and t.in_working_hours()] wholeday_tasks = [t for t in task_descrs if not t.is_one_day_task()] - inday_tasks.sort(key=lambda t:t.task.start) + inday_tasks.sort(key=lambda t:t.start) sorted_tasks = [] for i, t in enumerate(wholeday_tasks): t.index = i @@ -470,7 +503,7 @@ while inday_tasks: t = inday_tasks.pop(0) for i, c in enumerate(sorted_tasks): - if not c or c[-1].task.stop <= t.task.start: + if not c or c[-1].stop <= t.start: c.append(t) t.index = i+ncols break @@ -491,15 +524,15 @@ start_min = 0 stop_hour = 20 stop_min = 0 - if task.start: - if date < todate(task.start) < date + ONEDAY: - start_hour = max(8, task.start.hour) - start_min = task.start.minute - if task.stop: - if date < todate(task.stop) < date + ONEDAY: - stop_hour = min(20, task.stop.hour) + if task_desc.start: + if date < todate(task_desc.start) < date + ONEDAY: + start_hour = max(8, task_desc.start.hour) + start_min = task_desc.start.minute + if task_desc.stop: + if date < todate(task_desc.stop) < date + ONEDAY: + stop_hour = min(20, task_desc.stop.hour) if stop_hour < 20: - stop_min = task.stop.minute + stop_min = task_desc.stop.minute height = 100.0*(stop_hour+stop_min/60.0-start_hour-start_min/60.0)/(20-8) top = 100.0*(start_hour+start_min/60.0-8)/(20-8) @@ -518,7 +551,7 @@ self.w(u'
' % xml_escape(url)) task.view('tooltip', w=self.w) self.w(u'
') - if task.start is None: + if task_desc.start is None: self.w(u'
') self.w(u'
') self.w(u'
') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/cwproperties.py --- a/web/views/cwproperties.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/cwproperties.py Thu May 20 20:47:55 2010 +0200 @@ -35,7 +35,7 @@ from cubicweb.web.formfields import FIELDS, StringField from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton, FieldWidget) -from cubicweb.web.views import primary, formrenderers +from cubicweb.web.views import primary, formrenderers, editcontroller uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden') @@ -396,6 +396,15 @@ w(u'
') +class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter): + __select__ = implements('CWProperty') + + def after_deletion_path(self): + """return (path, parameters) which should be used as redirect + information when this entity is being deleted + """ + return 'view', {} + _afs = uicfg.autoform_section _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden') _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/cwuser.py --- a/web/views/cwuser.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/cwuser.py Thu May 20 20:47:55 2010 +0200 @@ -80,7 +80,7 @@ if entity.firstname: self.w(u'%s\n' % xml_escape(entity.firstname)) - emailaddr = entity.get_email() + emailaddr = entity.cw_adapt_to('IEmailable').get_email() if emailaddr: self.w(u'%s\n' % xml_escape(emailaddr)) self.w(u'\n') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/editcontroller.py --- a/web/views/editcontroller.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/editcontroller.py Thu May 20 20:47:55 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 . -"""The edit controller, handling form submitting. +"""The edit controller, automatically handling entity form submitting""" -""" __docformat__ = "restructuredtext en" from warnings import warn @@ -27,9 +26,37 @@ from logilab.common.textutils import splitstrip from cubicweb import Binary, ValidationError, typed_eid -from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, ProcessFormError +from cubicweb.view import EntityAdapter, implements_adapter_compat +from cubicweb.selectors import implements +from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, + ProcessFormError) from cubicweb.web.views import basecontrollers, autoform + +class IEditControlAdapter(EntityAdapter): + __regid__ = 'IEditControl' + __select__ = implements('Any') + + @implements_adapter_compat('IEditControl') + def after_deletion_path(self): + """return (path, parameters) which should be used as redirect + information when this entity is being deleted + """ + parent = self.cw_adapt_to('IBreadCrumbs').parent_entity() + if parent is not None: + return parent.rest_path(), {} + return str(self.e_schema).lower(), {} + + @implements_adapter_compat('IEditControl') + def pre_web_edit(self): + """callback called by the web editcontroller when an entity will be + created/modified, to let a chance to do some entity specific stuff. + + Do nothing by default. + """ + pass + + def valerror_eid(eid): try: return typed_eid(eid) @@ -155,7 +182,7 @@ entity.eid = formparams['eid'] is_main_entity = self._cw.form.get('__maineid') == formparams['eid'] # let a chance to do some entity specific stuff - entity.pre_web_edit() + entity.cw_adapt_to('IEditControl').pre_web_edit() # create a rql query from parameters rqlquery = RqlQuery() # process inlined relations at the same time as attributes @@ -276,7 +303,7 @@ eidtypes = tuple(eidtypes) for eid, etype in eidtypes: entity = self._cw.entity_from_eid(eid, etype) - path, params = entity.after_deletion_path() + path, params = entity.cw_adapt_to('IEditControl').after_deletion_path() redirect_info.add( (path, tuple(params.iteritems())) ) entity.delete() if len(redirect_info) > 1: diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/emailaddress.py --- a/web/views/emailaddress.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/emailaddress.py Thu May 20 20:47:55 2010 +0200 @@ -26,7 +26,7 @@ from cubicweb.selectors import implements from cubicweb import Unauthorized from cubicweb.web import uicfg -from cubicweb.web.views import baseviews, primary +from cubicweb.web.views import baseviews, primary, ibreadcrumbs _pvs = uicfg.primaryview_section _pvs.tag_subject_of(('*', 'use_email', '*'), 'attributes') @@ -138,3 +138,10 @@ def cell_call(self, row, col, **kwargs): self.w(self.cw_rset.get_entity(row, col).display_address()) + + +class EmailAddressIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('EmailAddress') + + def parent_entity(self): + return self.email_of diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/embedding.py --- a/web/views/embedding.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/embedding.py Thu May 20 20:47:55 2010 +0200 @@ -16,10 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . """Objects interacting together to provides the external page embeding -functionality. +functionality.""" - -""" __docformat__ = "restructuredtext en" import re @@ -29,16 +27,27 @@ from logilab.mtconverter import guess_encoding -from cubicweb.selectors import (one_line_rset, score_entity, - match_search_state, implements) +from cubicweb.selectors import (one_line_rset, score_entity, implements, + adaptable, match_search_state) from cubicweb.interfaces import IEmbedable -from cubicweb.view import NOINDEX, NOFOLLOW +from cubicweb.view import NOINDEX, NOFOLLOW, EntityAdapter, implements_adapter_compat from cubicweb.uilib import soup2xhtml from cubicweb.web.controller import Controller from cubicweb.web.action import Action from cubicweb.web.views import basetemplates +class IEmbedableAdapter(EntityAdapter): + """interface for embedable entities""" + __regid__ = 'IEmbedable' + __select__ = implements(IEmbedable) # XXX for bw compat, should be abstract + + @implements_adapter_compat('IEmbedable') + def embeded_url(self): + """embed action interface""" + raise NotImplementedError + + class ExternalTemplate(basetemplates.TheMainTemplate): """template embeding an external web pages into CubicWeb web interface """ @@ -92,7 +101,7 @@ def entity_has_embedable_url(entity): """return 1 if the entity provides an allowed embedable url""" - url = entity.embeded_url() + url = entity.cw_adapt_to('IEmbedable').embeded_url() if not url or not url.strip(): return 0 allowed = entity._cw.vreg.config['embed-allowed'] @@ -107,14 +116,14 @@ """ __regid__ = 'embed' __select__ = (one_line_rset() & match_search_state('normal') - & implements(IEmbedable) + & adaptable('IEmbedable') & score_entity(entity_has_embedable_url)) title = _('embed') def url(self, row=0): entity = self.cw_rset.get_entity(row, 0) - url = urljoin(self._cw.base_url(), entity.embeded_url()) + url = urljoin(self._cw.base_url(), entity.cw_adapt_to('IEmbedable').embeded_url()) if self._cw.form.has_key('rql'): return self._cw.build_url('embed', url=url, rql=self._cw.form['rql']) return self._cw.build_url('embed', url=url) diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/ibreadcrumbs.py --- a/web/views/ibreadcrumbs.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/ibreadcrumbs.py Thu May 20 20:47:55 2010 +0200 @@ -21,20 +21,78 @@ __docformat__ = "restructuredtext en" _ = unicode +from warnings import warn + from logilab.mtconverter import xml_escape -from cubicweb.interfaces import IBreadCrumbs -from cubicweb.selectors import (one_line_rset, implements, one_etype_rset, - multi_lines_rset, any_rset) -from cubicweb.view import EntityView, Component +#from cubicweb.interfaces import IBreadCrumbs +from cubicweb.selectors import (implements, one_line_rset, adaptable, + one_etype_rset, multi_lines_rset, any_rset) +from cubicweb.view import EntityView, Component, EntityAdapter # don't use AnyEntity since this may cause bug with isinstance() due to reloading from cubicweb.entity import Entity from cubicweb import tags, uilib +# ease bw compat +def ibreadcrumb_adapter(entity): + if hasattr(entity, 'breadcrumbs'): + warn('[3.9] breadcrumbs() method is deprecated, define a custom ' + 'IBreadCrumbsAdapter for %s instead' % entity.__class__, + DeprecationWarning) + return entity + return entity.cw_adapt_to('IBreadCrumbs') + + +class IBreadCrumbsAdapter(EntityAdapter): + """adapters for entities which can be"located" on some path to display in + the web ui + """ + __regid__ = 'IBreadCrumbs' + __select__ = implements('Any', accept_none=False) + + def parent_entity(self): + if hasattr(self.entity, 'parent'): + warn('[3.9] parent() method is deprecated, define a ' + 'custom IBreadCrumbsAdapter/ITreeAdapter for %s instead' + % self.entity.__class__, DeprecationWarning) + return self.entity.parent() + itree = self.entity.cw_adapt_to('ITree') + if itree is not None: + return itree.parent() + return None + + def breadcrumbs(self, view=None, recurs=False): + """return a list containing some: + + * tuple (url, label) + * entity + * simple label string + + defining path from a root to the current view + + the main view is given as argument so breadcrumbs may vary according + to displayed view (may be None). When recursing on a parent entity, + the `recurs` argument should be set to True. + """ + path = [self.entity] + parent = self.parent_entity() + if parent is not None: + adapter = ibreadcrumb_adapter(self.entity) + path = adapter.breadcrumbs(view, True) + [self.entity] + if not recurs: + if view is None: + if 'vtitle' in self._cw.form: + # embeding for instance + path.append( self._cw.form['vtitle'] ) + elif view.__regid__ != 'primary' and hasattr(view, 'title'): + path.append( self._cw._(view.title) ) + return path + + class BreadCrumbEntityVComponent(Component): __regid__ = 'breadcrumbs' - __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False) + __select__ = one_line_rset() & adaptable('IBreadCrumbs') cw_property_defs = { _('visible'): dict(type='Boolean', default=True, @@ -47,7 +105,8 @@ def call(self, view=None, first_separator=True): entity = self.cw_rset.get_entity(0, 0) - path = entity.breadcrumbs(view) + adapter = ibreadcrumb_adapter(entity) + path = adapter.breadcrumbs(view) if path: self.open_breadcrumbs() if first_separator: @@ -73,7 +132,7 @@ self.w(u"\n") self.wpath_part(parent, contextentity, i == len(path) - 1) - def wpath_part(self, part, contextentity, last=False): + def wpath_part(self, part, contextentity, last=False): # XXX deprecates last argument? if isinstance(part, Entity): self.w(part.view('breadcrumbs')) elif isinstance(part, tuple): @@ -88,7 +147,7 @@ class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent): __select__ = multi_lines_rset() & one_etype_rset() & \ - implements(IBreadCrumbs, accept_none=False) + adaptable('IBreadCrumbs') def render_breadcrumbs(self, contextentity, path): # XXX hack: only display etype name or first non entity path part diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/idownloadable.py --- a/web/views/idownloadable.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/idownloadable.py Thu May 20 20:47:55 2010 +0200 @@ -15,29 +15,21 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Specific views for entities implementing IDownloadable +"""Specific views for entities adapting to IDownloadable""" -""" __docformat__ = "restructuredtext en" _ = unicode from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape from cubicweb.view import EntityView -from cubicweb.selectors import (one_line_rset, score_entity, - implements, match_context_prop) -from cubicweb.interfaces import IDownloadable +from cubicweb.selectors import (one_line_rset, implements, match_context_prop, + adaptable, has_mimetype) from cubicweb.mttransforms import ENGINE from cubicweb.web.box import EntityBoxTemplate from cubicweb.web.views import primary, baseviews -def is_image(entity): - mt = entity.download_content_type() - if not (mt and mt.startswith('image/')): - return 0 - return 1 - def download_box(w, entity, title=None, label=None, footer=u''): req = entity._cw w(u'') +from cubicweb.interfaces import IPrevNext + +class IPrevNextAdapter(EntityAdapter): + """interface for entities which can be linked to a previous and/or next + entity + """ + __regid__ = 'IPrevNext' + __select__ = implements(IPrevNext) # XXX for bw compat, else should be abstract + + @implements_adapter_compat('IPrevNext') + def next_entity(self): + """return the 'next' entity""" + raise NotImplementedError + + @implements_adapter_compat('IPrevNext') + def previous_entity(self): + """return the 'previous' entity""" + raise NotImplementedError + + class NextPrevNavigationComponent(EntityVComponent): __regid__ = 'prevnext' # register msg not generated since no entity implements IPrevNext in cubicweb # itself title = _('contentnavigation_prevnext') help = _('contentnavigation_prevnext_description') - __select__ = (one_line_rset() & primary_view() - & match_context_prop() & implements(IPrevNext)) + __select__ = (EntityVComponent.__select__ + & adaptable('IPrevNext')) context = 'navbottom' order = 10 def call(self, view=None): entity = self.cw_rset.get_entity(0, 0) - previous = entity.previous_entity() - next = entity.next_entity() + adapter = entity.cw_adapt_to('IDownloadable') + previous = adapter.previous_entity() + next = adapter.next_entity() if previous or next: textsize = self._cw.property_value('navigation.short-line-size') self.w(u'
') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/old_calendar.py --- a/web/views/old_calendar.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/old_calendar.py Thu May 20 20:47:55 2010 +0200 @@ -15,9 +15,7 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""html calendar views - -""" +"""html calendar views""" from datetime import date, time, timedelta @@ -26,8 +24,25 @@ next_month, first_day, last_day, date_range) from cubicweb.interfaces import ICalendarViews -from cubicweb.selectors import implements -from cubicweb.view import EntityView +from cubicweb.selectors import implements, adaptable +from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat + +class ICalendarViewsAdapter(EntityAdapter): + """calendar views interface""" + __regid__ = 'ICalendarViews' + __select__ = implements(ICalendarViews) # XXX for bw compat, should be abstract + + @implements_adapter_compat('ICalendarViews') + def matching_dates(self, begin, end): + """ + :param begin: day considered as begin of the range (`DateTime`) + :param end: day considered as end of the range (`DateTime`) + + :return: + a list of dates (`DateTime`) in the range [`begin`, `end`] on which + this entity apply + """ + raise NotImplementedError # used by i18n tools WEEKDAYS = [_("monday"), _("tuesday"), _("wednesday"), _("thursday"), @@ -39,7 +54,7 @@ class _CalendarView(EntityView): """base calendar view containing helpful methods to build calendar views""" - __select__ = implements(ICalendarViews,) + __select__ = adaptable('ICalendarViews') paginable = False # Navigation building methods / views #################################### @@ -126,7 +141,7 @@ infos = u'
' infos += self._cw.view(itemvid, self.cw_rset, row=row) infos += u'
' - for date_ in entity.matching_dates(begin, end): + for date_ in entity.cw_adapt_to('ICalendarViews').matching_dates(begin, end): day = date(date_.year, date_.month, date_.day) try: dt = time(date_.hour, date_.minute, date_.second) @@ -288,7 +303,7 @@ monthlink = '%s' % (xml_escape(url), umonth) self.w(u'%s %s (%s)' \ % (_('week'), monday.isocalendar()[1], monthlink)) - for day in date_range(monday, sunday): + for day in date_range(monday, sunday+ONEDAY): self.w(u'') self.w(u'%s' % _(WEEKDAYS[day.weekday()])) self.w(u'%s' % (day.strftime('%Y-%m-%d'))) @@ -478,7 +493,7 @@ w(u'%s' % ( WEEK_TITLE % (_('week'), monday.isocalendar()[1], monthlink))) w(u'%s '% _(u'Date')) - for day in date_range(monday, sunday): + for day in date_range(monday, sunday+ONEDAY): events = schedule.get(day) style = day.weekday() % 2 and "even" or "odd" w(u'' % style) diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/schema.py --- a/web/views/schema.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/schema.py Thu May 20 20:47:55 2010 +0200 @@ -35,7 +35,7 @@ from cubicweb import tags, uilib from cubicweb.web import action, facet, uicfg, schemaviewer from cubicweb.web.views import TmpFileViewMixin -from cubicweb.web.views import primary, baseviews, tabs, tableview, iprogress +from cubicweb.web.views import primary, baseviews, tabs, tableview, ibreadcrumbs ALWAYS_SKIP_TYPES = BASE_TYPES | SCHEMA_TYPES SKIP_TYPES = (ALWAYS_SKIP_TYPES | META_RTYPES | SYSTEM_RTYPES | WORKFLOW_TYPES @@ -680,6 +680,37 @@ visitor = OneHopRSchemaVisitor(self._cw, rschema) s2d.schema2dot(outputfile=tmpfile, visitor=visitor) +# breadcrumbs ################################################################## + +class CWRelationIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('CWRelation') + def parent_entity(self): + return self.entity.rtype + +class CWAttributeIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('CWAttribute') + def parent_entity(self): + return self.entity.stype + +class CWConstraintIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('CWConstraint') + def parent_entity(self): + if self.entity.reverse_constrained_by: + return self.entity.reverse_constrained_by[0] + +class RQLExpressionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('RQLExpression') + def parent_entity(self): + return self.entity.expression_of + +class CWPermissionIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('CWPermission') + def parent_entity(self): + # XXX useless with permission propagation + permissionof = getattr(self.entity, 'reverse_require_permission', ()) + if len(permissionof) == 1: + return permissionof[0] + # misc: facets, actions ######################################################## diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/tableview.py --- a/web/views/tableview.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/tableview.py Thu May 20 20:47:55 2010 +0200 @@ -369,9 +369,9 @@ self._cw.add_css(self.css_files) _ = self._cw._ self.columns = columns or self.columns - ecls = self._cw.vreg['etypes'].etype_class(self.cw_rset.description[0][0]) + sample = self.cw_rset.get_entity(0, 0) self.w(u'' % self.table_css) - self.table_header(ecls) + self.table_header(sample) self.w(u'') for row in xrange(self.cw_rset.rowcount): self.cell_call(row=row, col=0) @@ -396,16 +396,15 @@ self.w(line % infos) self.w(u'\n') - def table_header(self, ecls): + def table_header(self, sample): """builds the table's header""" self.w(u'') - _ = self._cw._ for column in self.columns: meth = getattr(self, 'header_for_%s' % column, None) if meth: - colname = meth(ecls) + colname = meth(sample) else: - colname = _(column) + colname = self._cw._(column) self.w(u'' % xml_escape(colname)) self.w(u'\n') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/timeline.py --- a/web/views/timeline.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/timeline.py Thu May 20 20:47:55 2010 +0200 @@ -18,14 +18,13 @@ """basic support for SIMILE's timline widgets cf. http://code.google.com/p/simile-widgets/ +""" -""" __docformat__ = "restructuredtext en" from logilab.mtconverter import xml_escape -from cubicweb.interfaces import ICalendarable -from cubicweb.selectors import implements +from cubicweb.selectors import adaptable from cubicweb.view import EntityView, StartupView from cubicweb.web import json @@ -37,11 +36,12 @@ should be properties of entity classes or subviews) """ __regid__ = 'timeline-json' + __select__ = adaptable('ICalendarable') + binary = True templatable = False content_type = 'application/json' - __select__ = implements(ICalendarable) date_fmt = '%Y/%m/%d' def call(self): @@ -74,8 +74,9 @@ 'link': 'http://www.allposters.com/-sp/Portrait-of-Horace-Brodsky-Posters_i1584413_.htm' } """ - start = entity.start - stop = entity.stop + icalendarable = entity.cw_adapt_to('ICalendarable') + start = icalendarable.start + stop = icalendarable.stop start = start or stop if start is None and stop is None: return None @@ -116,7 +117,7 @@ """builds a cubicweb timeline widget node""" __regid__ = 'timeline' title = _('timeline') - __select__ = implements(ICalendarable) + __select__ = adaptable('ICalendarable') paginable = False def call(self, tlunit=None): self._cw.html_headers.define_var('Timeline_urlPrefix', self._cw.datadir_url) diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/timetable.py --- a/web/views/timetable.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/timetable.py Thu May 20 20:47:55 2010 +0200 @@ -15,16 +15,16 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""html calendar views +"""html timetable views""" -""" +__docformat__ = "restructuredtext en" +_ = unicode from logilab.mtconverter import xml_escape -from logilab.common.date import date_range, todatetime +from logilab.common.date import ONEDAY, date_range, todatetime -from cubicweb.interfaces import ITimetableViews -from cubicweb.selectors import implements -from cubicweb.view import AnyRsetView +from cubicweb.selectors import adaptable +from cubicweb.view import EntityView class _TaskEntry(object): @@ -37,10 +37,10 @@ MIN_COLS = 3 # minimum number of task columns for a single user ALL_USERS = object() -class TimeTableView(AnyRsetView): +class TimeTableView(EntityView): __regid__ = 'timetable' title = _('timetable') - __select__ = implements(ITimetableViews) + __select__ = adaptable('ICalendarable') paginable = False def call(self, title=None): @@ -53,20 +53,22 @@ # XXX: try refactoring with calendar.py:OneMonthCal for row in xrange(self.cw_rset.rowcount): task = self.cw_rset.get_entity(row, 0) + icalendarable = task.cw_adapt_to('ICalendarable') if len(self.cw_rset[row]) > 1: user = self.cw_rset.get_entity(row, 1) else: user = ALL_USERS the_dates = [] - if task.start and task.stop: - if task.start.toordinal() == task.stop.toordinal(): - the_dates.append(task.start) + if icalendarable.start and icalendarable.stop: + if icalendarable.start.toordinal() == icalendarable.stop.toordinal(): + the_dates.append(icalendarable.start) else: - the_dates += date_range( task.start, task.stop ) - elif task.start: - the_dates.append(task.start) - elif task.stop: - the_dates.append(task.stop) + the_dates += date_range(icalendarable.start, + icalendarable.stop + ONEDAY) + elif icalendarable.start: + the_dates.append(icalendarable.start) + elif icalendarable.stop: + the_dates.append(icalendarable.stop) for d in the_dates: d = todatetime(d) d_users = dates.setdefault(d, {}) diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/treeview.py --- a/web/views/treeview.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/treeview.py Thu May 20 20:47:55 2010 +0200 @@ -15,22 +15,252 @@ # # You should have received a copy of the GNU Lesser General Public License along # with CubicWeb. If not, see . -"""Set of tree-building widgets, based on jQuery treeview plugin - +"""Set of tree views / tree-building widgets, some based on jQuery treeview +plugin. """ __docformat__ = "restructuredtext en" +from warnings import warn + from logilab.mtconverter import xml_escape +from logilab.common.decorators import cached + from cubicweb.utils import make_uid +from cubicweb.selectors import implements, adaptable +from cubicweb.view import EntityView, EntityAdapter, implements_adapter_compat +from cubicweb.web import json from cubicweb.interfaces import ITree -from cubicweb.selectors import implements -from cubicweb.view import EntityView -from cubicweb.web import json +from cubicweb.web.views import baseviews def treecookiename(treeid): return str('%s-treestate' % treeid) + +class ITreeAdapter(EntityAdapter): + """This adapter has to be overriden to be configured using the + tree_relation, child_role and parent_role class attributes to + benefit from this default implementation + """ + __regid__ = 'ITree' + __select__ = implements(ITree) # XXX for bw compat, else should be abstract + + tree_relation = None + child_role = 'subject' + parent_role = 'object' + + @implements_adapter_compat('ITree') + def children_rql(self): + """returns RQL to get children + + XXX should be removed from the public interface + """ + return self.entity.related_rql(self.tree_relation, self.parent_role) + + @implements_adapter_compat('ITree') + def different_type_children(self, entities=True): + """return children entities of different type as this entity. + + according to the `entities` parameter, return entity objects or the + equivalent result set + """ + res = self.entity.related(self.tree_relation, self.parent_role, + entities=entities) + eschema = self.entity.e_schema + if entities: + return [e for e in res if e.e_schema != eschema] + return res.filtered_rset(lambda x: x.e_schema != eschema, self.entity.cw_col) + + @implements_adapter_compat('ITree') + def same_type_children(self, entities=True): + """return children entities of the same type as this entity. + + according to the `entities` parameter, return entity objects or the + equivalent result set + """ + res = self.entity.related(self.tree_relation, self.parent_role, + entities=entities) + eschema = self.entity.e_schema + if entities: + return [e for e in res if e.e_schema == eschema] + return res.filtered_rset(lambda x: x.e_schema is eschema, self.entity.cw_col) + + @implements_adapter_compat('ITree') + def is_leaf(self): + """returns true if this node as no child""" + return len(self.children()) == 0 + + @implements_adapter_compat('ITree') + def is_root(self): + """returns true if this node has no parent""" + return self.parent() is None + + @implements_adapter_compat('ITree') + def root(self): + """return the root object""" + return self._cw.entity_from_eid(self.path()[0]) + + @implements_adapter_compat('ITree') + def parent(self): + """return the parent entity if any, else None (e.g. if we are on the + root) + """ + try: + return self.entity.related(self.tree_relation, self.child_role, + entities=True)[0] + except (KeyError, IndexError): + return None + + @implements_adapter_compat('ITree') + def children(self, entities=True, sametype=False): + """return children entities + + according to the `entities` parameter, return entity objects or the + equivalent result set + """ + if sametype: + return self.same_type_children(entities) + else: + return self.entity.related(self.tree_relation, self.parent_role, + entities=entities) + + @implements_adapter_compat('ITree') + def iterparents(self, strict=True): + def _uptoroot(self): + curr = self + while True: + curr = curr.parent() + if curr is None: + break + yield curr + curr = curr.cw_adapt_to('ITree') + if not strict: + return chain([self.entity], _uptoroot(self)) + return _uptoroot(self) + + @implements_adapter_compat('ITree') + def iterchildren(self, _done=None): + """iterates over the item's children""" + if _done is None: + _done = set() + for child in self.children(): + if child.eid in _done: + self.error('loop in %s tree', child.__regid__.lower()) + continue + yield child + _done.add(child.eid) + + @implements_adapter_compat('ITree') + def prefixiter(self, _done=None): + if _done is None: + _done = set() + if self.entity.eid in _done: + return + _done.add(self.entity.eid) + yield self.entity + for child in self.same_type_children(): + for entity in child.cw_adapt_to('ITree').prefixiter(_done): + yield entity + + @cached + @implements_adapter_compat('ITree') + def path(self): + """returns the list of eids from the root object to this object""" + path = [] + adapter = self + entity = adapter.entity + while entity is not None: + if entity.eid in path: + self.error('loop in %s tree', entity.__regid__.lower()) + break + path.append(entity.eid) + try: + # check we are not jumping to another tree + if (adapter.tree_relation != self.tree_relation or + adapter.child_role != self.child_role): + break + entity = adapter.parent() + adapter = entity.cw_adapt_to('ITree') + except AttributeError: + break + path.reverse() + return path + + +def _done_init(done, view, row, col): + """handle an infinite recursion safety belt""" + if done is None: + done = set() + entity = view.cw_rset.get_entity(row, col) + if entity.eid in done: + msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % { + 'rel': entity.tree_attribute, + 'eid': entity.eid + } + return None, msg + done.add(entity.eid) + return done, entity + + +class BaseTreeView(baseviews.ListView): + """base tree view""" + __regid__ = 'tree' + __select__ = adaptable('ITree') + item_vid = 'treeitem' + + def call(self, done=None, **kwargs): + if done is None: + done = set() + super(TreeViewMixIn, self).call(done=done, **kwargs) + + def cell_call(self, row, col=0, vid=None, done=None, **kwargs): + done, entity = _done_init(done, self, row, col) + if done is None: + # entity is actually an error message + self.w(u'
  • %s
  • ' % entity) + return + self.open_item(entity) + entity.view(vid or self.item_vid, w=self.w, **kwargs) + relatedrset = entity.cw_adapt_to('ITree').children(entities=False) + self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs) + self.close_item(entity) + + def open_item(self, entity): + self.w(u'
  • \n' % entity.__regid__.lower()) + def close_item(self, entity): + self.w(u'
  • \n') + + + +class TreePathView(EntityView): + """a recursive path view""" + __regid__ = 'path' + __select__ = adaptable('ITree') + item_vid = 'oneline' + separator = u' > ' + + def call(self, **kwargs): + self.w(u'
    ') + super(TreePathMixIn, self).call(**kwargs) + self.w(u'
    ') + + def cell_call(self, row, col=0, vid=None, done=None, **kwargs): + done, entity = _done_init(done, self, row, col) + if done is None: + # entity is actually an error message + self.w(u'%s' % entity) + return + parent = entity.cw_adapt_to('ITree').parent_entity() + if parent: + parent.view(self.__regid__, w=self.w, done=done) + self.w(self.separator) + entity.view(vid or self.item_vid, w=self.w) + + +# XXX rename regid to ajaxtree/foldabletree or something like that (same for +# treeitemview) class TreeView(EntityView): + """ajax tree view, click to expand folder""" + __regid__ = 'treeview' itemvid = 'treeitemview' subvid = 'oneline' @@ -112,7 +342,7 @@ def cell_call(self, row, col): entity = self.cw_rset.get_entity(row, col) - if ITree.is_implemented_by(entity.__class__) and not entity.is_leaf(): + if entity.cw_adapt_to('ITree') and not entity.is_leaf(): self.w(u'
    %s
    \n' % entity.view('oneline')) else: # XXX define specific CSS classes according to mime types @@ -120,7 +350,7 @@ class DefaultTreeViewItemView(EntityView): - """default treeitem view for entities which don't implement ITree""" + """default treeitem view for entities which don't adapt to ITree""" __regid__ = 'treeitemview' def cell_call(self, row, col, vid='oneline', treeid=None, **morekwargs): @@ -131,12 +361,12 @@ class TreeViewItemView(EntityView): - """specific treeitem view for entities which implement ITree + """specific treeitem view for entities which adapt to ITree (each item should be expandable if it's not a tree leaf) """ __regid__ = 'treeitemview' - __select__ = implements(ITree) + __select__ = adaptable('ITree') default_branch_state_is_open = False def open_state(self, eeid, treeid): @@ -150,15 +380,16 @@ is_last=False, **morekwargs): w = self.w entity = self.cw_rset.get_entity(row, col) + itree = entity.cw_adapt_to('ITree') liclasses = [] is_open = self.open_state(entity.eid, treeid) - is_leaf = not hasattr(entity, 'is_leaf') or entity.is_leaf() + is_leaf = not hasattr(entity, 'is_leaf') or itree.is_leaf() if is_leaf: if is_last: liclasses.append('last') w(u'
  • ' % u' '.join(liclasses)) else: - rql = entity.children_rql() % {'x': entity.eid} + rql = itree.children_rql() % {'x': entity.eid} url = xml_escape(self._cw.build_url('json', rql=rql, vid=parentvid, pageid=self._cw.pageid, treeid=treeid, @@ -197,7 +428,7 @@ # the local node info self.wview(vid, self.cw_rset, row=row, col=col, **morekwargs) if is_open and not is_leaf: # => rql is defined - self.wview(parentvid, entity.children(entities=False), subvid=vid, + self.wview(parentvid, itree.children(entities=False), subvid=vid, treeid=treeid, initial_load=False, **morekwargs) w(u'
  • ') diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/workflow.py --- a/web/views/workflow.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/workflow.py Thu May 20 20:47:55 2010 +0200 @@ -33,13 +33,13 @@ from cubicweb import Unauthorized, view from cubicweb.selectors import (implements, has_related_entities, one_line_rset, relation_possible, match_form_params, - implements, score_entity) -from cubicweb.interfaces import IWorkflowable + implements, score_entity, adaptable) from cubicweb.view import EntityView from cubicweb.schema import display_name from cubicweb.web import uicfg, stdmsgs, action, component, form, action from cubicweb.web import formfields as ff, formwidgets as fwdgs -from cubicweb.web.views import TmpFileViewMixin, forms, primary, autoform +from cubicweb.web.views import TmpFileViewMixin +from cubicweb.web.views import forms, primary, autoform, ibreadcrumbs from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab _pvs = uicfg.primaryview_section @@ -89,8 +89,9 @@ class ChangeStateFormView(form.FormViewMixIn, view.EntityView): __regid__ = 'statuschange' title = _('status change') - __select__ = (one_line_rset() & implements(IWorkflowable) - & match_form_params('treid')) + __select__ = (one_line_rset() + & match_form_params('treid') + & adaptable('IWorkflowable')) def cell_call(self, row, col): entity = self.cw_rset.get_entity(row, col) @@ -99,7 +100,7 @@ self.w(u'

    %s %s

    \n' % (self._cw._(transition.name), entity.view('oneline'))) msg = self._cw._('status will change from %(st1)s to %(st2)s') % { - 'st1': entity.printable_state, + 'st1': entity.cw_adapt_to('IWorkflowable').printable_state, 'st2': self._cw._(transition.destination(entity).name)} self.w(u'

    %s

    \n' % msg) self.w(form.render()) @@ -128,7 +129,7 @@ class WFHistoryView(EntityView): __regid__ = 'wfhistory' __select__ = relation_possible('wf_info_for', role='object') & \ - score_entity(lambda x: x.workflow_history) + score_entity(lambda x: x.cw_adapt_to('IWorkflowable').workflow_history) title = _('Workflow history') @@ -183,22 +184,24 @@ def fill_menu(self, box, menu): entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) - menu.label = u'%s: %s' % (self._cw._('state'), entity.printable_state) + menu.label = u'%s: %s' % (self._cw._('state'), + entity.cw_adapt_to('IWorkflowable').printable_state) menu.append_anyway = True super(WorkflowActions, self).fill_menu(box, menu) def actual_actions(self): entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + iworkflowable = entity.cw_adapt_to('IWorkflowable') hastr = False - for tr in entity.possible_transitions(): + for tr in iworkflowable.possible_transitions(): url = entity.absolute_url(vid='statuschange', treid=tr.eid) yield self.build_action(self._cw._(tr.name), url) hastr = True # don't propose to see wf if user can't pass any transition if hastr: - wfurl = entity.current_workflow.absolute_url() + wfurl = iworkflowable.current_workflow.absolute_url() yield self.build_action(self._cw._('view workflow'), wfurl) - if entity.workflow_history: + if iworkflowable.workflow_history: wfurl = entity.absolute_url(vid='wfhistory') yield self.build_action(self._cw._('view history'), wfurl) @@ -346,6 +349,27 @@ 'allowed_transition') return [] +class WorkflowIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('Workflow') + # XXX what if workflow of multiple types? + def parent_entity(self): + return self.entity.workflow_of and self.entity.workflow_of[0] or None + +class WorkflowItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('BaseTransition', 'State') + def parent_entity(self): + return self.entity.workflow + +class TransitionItemIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('SubWorkflowExitPoint') + def parent_entity(self): + return self.entity.reverse_subworkflow_exit[0] + +class TrInfoIBreadCrumbsAdapter(ibreadcrumbs.IBreadCrumbsAdapter): + __select__ = implements('TrInfo') + def parent_entity(self): + return self.entity.for_entity + # workflow images ############################################################## diff -r a64f48dd5fe4 -r 9ab2b4c74baf web/views/xmlrss.py --- a/web/views/xmlrss.py Thu May 20 20:47:13 2010 +0200 +++ b/web/views/xmlrss.py Thu May 20 20:47:55 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 xml and rss views +"""base xml and rss views""" -""" __docformat__ = "restructuredtext en" _ = unicode @@ -25,8 +24,10 @@ from logilab.mtconverter import xml_escape -from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable -from cubicweb.view import EntityView, AnyRsetView, Component +from cubicweb.selectors import (implements, non_final_entity, one_line_rset, + appobject_selectable, adaptable) +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 @@ -120,6 +121,16 @@ # RSS stuff ################################################################### +class IFeedAdapter(EntityAdapter): + __regid__ = 'IFeed' + __select__ = implements('Any') + + @implements_adapter_compat('IFeed') + def rss_feed_url(self): + """return an url to the rss feed for this entity""" + return self.absolute_url(vid='rss') + + class RSSFeedURL(Component): __regid__ = 'rss_feed_url' __select__ = non_final_entity() @@ -130,10 +141,11 @@ class RSSEntityFeedURL(Component): __regid__ = 'rss_feed_url' - __select__ = non_final_entity() & one_line_rset() + __select__ = one_line_rset() & adaptable('IFeed') def feed_url(self): - return self.cw_rset.get_entity(0, 0).rss_feed_url() + entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) + return entity.cw_adapt_to('IFeed').rss_feed_url() class RSSIconBox(box.BoxTemplate):
    %s