Commit a26e3312 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[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.
parent a64f48dd5fe4
......@@ -82,7 +82,6 @@ named `vreg`):
.. 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 @@ selectors that will inspect there content and return a score accordingly.
__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 clear_rtag_objects():
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 @@ class CubicWebVRegistry(VRegistry):
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 @@ class CubicWebVRegistry(VRegistry):
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 @@ class CubicWebVRegistry(VRegistry):
# 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 @@ class CubicWebVRegistry(VRegistry):
# 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 @@ class CubicWebVRegistry(VRegistry):
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
......
......@@ -15,10 +15,10 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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
......
......@@ -37,6 +37,7 @@ Those selectors are somewhat dumb, which doesn't mean they're not (very) useful.
.. 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 @@ match or not according to entity's (instance or class) properties.
.. 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
......
......@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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 logilab.common.decorators import cached
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 @@ class AnyEntity(Entity):
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 @@ class AnyEntity(Entity):
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
......
# 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 <http://www.gnu.org/licenses/>.
"""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
......@@ -15,9 +15,8 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""entity classes user and group entities
"""entity classes user and group entities"""
"""
__docformat__ = "restructuredtext en"
from logilab.common.decorators import cached
......
......@@ -48,13 +48,13 @@ class EmailAddress(AnyEntity):
@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 @@ class EmailAddress(AnyEntity):
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 @@ class CWProperty(AnyEntity):
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"""
......
......@@ -115,14 +115,6 @@ class CWRelation(AnyEntity):
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 @@ class CWRelation(AnyEntity):
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 @@ class CWConstraint(AnyEntity):
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 @@ class RQLExpression(AnyEntity):
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 @@ class CWPermission(AnyEntity):
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()
......@@ -106,7 +106,7 @@ class CWUserTC(BaseEntityTC):
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'))
)
......
This diff is collapsed.
......@@ -15,9 +15,13 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""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.deprecation import deprecated
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 @@ class Workflow(AnyEntity):
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 @@ class BaseTransition(AnyEntity):
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