# HG changeset patch
# User sylvain.thenault@logilab.fr
# Date 1236024234 -3600
# Node ID 6a25c58a1c237b1a376453a9105f427073ec9a9b
# Parent 536e421b082bb496dd468d41775ee00ed2018c78# Parent 93447d75c4b93de61d0b079f35da50a5a2150e14
backport stable branch, take care a lot of conflicts occured, this may be the revision you're looking for...
diff -r 93447d75c4b9 -r 6a25c58a1c23 appobject.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/appobject.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,338 @@
+"""Base class for dynamically loaded objects manipulated in the web interface
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+
+from mx.DateTime import now, oneSecond
+from simplejson import dumps
+
+from logilab.common.decorators import classproperty
+from logilab.common.deprecation import obsolete
+
+from rql.nodes import VariableRef, SubQuery
+from rql.stmts import Union, Select
+
+from cubicweb import Unauthorized
+from cubicweb.vregistry import VObject, AndSelector
+from cubicweb.selectors import yes
+from cubicweb.utils import UStringIO, ustrftime
+
+
+class Cache(dict):
+ def __init__(self):
+ super(Cache, self).__init__()
+ self.cache_creation_date = None
+ self.latest_cache_lookup = now()
+
+CACHE_REGISTRY = {}
+
+class AppRsetObject(VObject):
+ """This is the base class for CubicWeb application objects
+ which are selected according to a request and result set.
+
+ Classes are kept in the vregistry and instantiation is done at selection
+ time.
+
+ At registration time, the following attributes are set on the class:
+ :vreg:
+ the application's registry
+ :schema:
+ the application's schema
+ :config:
+ the application's configuration
+
+ At instantiation time, the following attributes are set on the instance:
+ :req:
+ current request
+ :rset:
+ result set on which the object is applied
+ """
+ __select__ = yes()
+
+ @classmethod
+ def registered(cls, vreg):
+ super(AppRsetObject, cls).registered(vreg)
+ cls.vreg = vreg
+ cls.schema = vreg.schema
+ cls.config = vreg.config
+ cls.register_properties()
+ return cls
+
+ @classmethod
+ def selected(cls, *args, **kwargs):
+ """by default web app objects are usually instantiated on
+ selection according to a request, a result set, and optional
+ row and col
+ """
+ assert len(args) <= 2
+# for key in ('req', 'rset'):
+# if key in kwargs:
+# args += (kwargs.pop(key),)
+ instance = cls(*args)
+ instance.row = kwargs.pop('row', None)
+ instance.col = kwargs.pop('col', None)
+ instance.extra_kwargs = kwargs
+ return instance
+
+ # Eproperties definition:
+ # key: id of the property (the actual EProperty key is build using
+ # ..
+ # value: tuple (property type, vocabfunc, default value, property description)
+ # possible types are those used by `logilab.common.configuration`
+ #
+ # notice that when it exists multiple objects with the same id (adaptation,
+ # overriding) only the first encountered definition is considered, so those
+ # objects can't try to have different default values for instance.
+
+ property_defs = {}
+
+ @classmethod
+ def register_properties(cls):
+ for propid, pdef in cls.property_defs.items():
+ pdef = pdef.copy() # may be shared
+ pdef['default'] = getattr(cls, propid, pdef['default'])
+ pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide'))
+ cls.vreg.register_property(cls.propkey(propid), **pdef)
+
+ @classmethod
+ def propkey(cls, propid):
+ return '%s.%s.%s' % (cls.__registry__, cls.id, propid)
+
+ @classproperty
+ @obsolete('use __select__ and & or | operators')
+ def __selectors__(cls):
+ selector = cls.__select__
+ if isinstance(selector, AndSelector):
+ return tuple(selector.selectors)
+ if not isinstance(selector, tuple):
+ selector = (selector,)
+ return selector
+
+ def __init__(self, req=None, rset=None):
+ super(AppRsetObject, self).__init__()
+ self.req = req
+ self.rset = rset
+
+ @property
+ def cursor(self): # XXX deprecate in favor of req.cursor?
+ msg = '.cursor is deprecated, use req.execute (or req.cursor if necessary)'
+ warn(msg, DeprecationWarning, stacklevel=2)
+ return self.req.cursor
+
+ def get_cache(self, cachename):
+ """
+ NOTE: cachename should be dotted names as in :
+ - cubicweb.mycache
+ - cubes.blog.mycache
+ - etc.
+ """
+ if cachename in CACHE_REGISTRY:
+ cache = CACHE_REGISTRY[cachename]
+ else:
+ cache = Cache()
+ CACHE_REGISTRY[cachename] = cache
+ _now = now()
+ if _now > cache.latest_cache_lookup + oneSecond:
+ ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T',
+ {'name':cachename}).get_entity(0,0)
+ cache.latest_cache_lookup = _now
+ if not ecache.valid(cache.cache_creation_date):
+ cache.empty()
+ cache.cache_creation_date = _now
+ return cache
+
+ def propval(self, propid):
+ assert self.req
+ return self.req.property_value(self.propkey(propid))
+
+
+ def limited_rql(self):
+ """return a printable rql for the result set associated to the object,
+ with limit/offset correctly set according to maximum page size and
+ currently displayed page when necessary
+ """
+ # try to get page boundaries from the navigation component
+ # XXX we should probably not have a ref to this component here (eg in
+ # cubicweb.common)
+ nav = self.vreg.select_component('navigation', self.req, self.rset)
+ if nav:
+ start, stop = nav.page_boundaries()
+ rql = self._limit_offset_rql(stop - start, start)
+ # result set may have be limited manually in which case navigation won't
+ # apply
+ elif self.rset.limited:
+ rql = self._limit_offset_rql(*self.rset.limited)
+ # navigation component doesn't apply and rset has not been limited, no
+ # need to limit query
+ else:
+ rql = self.rset.printable_rql()
+ return rql
+
+ def _limit_offset_rql(self, limit, offset):
+ rqlst = self.rset.syntax_tree()
+ if len(rqlst.children) == 1:
+ select = rqlst.children[0]
+ olimit, ooffset = select.limit, select.offset
+ select.limit, select.offset = limit, offset
+ rql = rqlst.as_string(kwargs=self.rset.args)
+ # restore original limit/offset
+ select.limit, select.offset = olimit, ooffset
+ else:
+ newselect = Select()
+ newselect.limit = limit
+ newselect.offset = offset
+ aliases = [VariableRef(newselect.get_variable(vref.name, i))
+ for i, vref in enumerate(rqlst.selection)]
+ newselect.set_with([SubQuery(aliases, rqlst)], check=False)
+ newunion = Union()
+ newunion.append(newselect)
+ rql = rqlst.as_string(kwargs=self.rset.args)
+ rqlst.parent = None
+ return rql
+
+ # url generation methods ##################################################
+
+ controller = 'view'
+
+ def build_url(self, method=None, **kwargs):
+ """return an absolute URL using params dictionary key/values as URL
+ parameters. Values are automatically URL quoted, and the
+ publishing method to use may be specified or will be guessed.
+ """
+ # XXX I (adim) think that if method is passed explicitly, we should
+ # not try to process it and directly call req.build_url()
+ if method is None:
+ method = self.controller
+ if method == 'view' and self.req.from_controller() == 'view' and \
+ not '_restpath' in kwargs:
+ method = self.req.relative_path(includeparams=False) or 'view'
+ return self.req.build_url(method, **kwargs)
+
+ # various resources accessors #############################################
+
+ def etype_rset(self, etype, size=1):
+ """return a fake result set for a particular entity type"""
+ msg = '.etype_rset is deprecated, use req.etype_rset'
+ warn(msg, DeprecationWarning, stacklevel=2)
+ return self.req.etype_rset(etype, size=1)
+
+ def eid_rset(self, eid, etype=None):
+ """return a result set for the given eid"""
+ msg = '.eid_rset is deprecated, use req.eid_rset'
+ warn(msg, DeprecationWarning, stacklevel=2)
+ return self.req.eid_rset(eid, etype)
+
+ def entity(self, row, col=0):
+ """short cut to get an entity instance for a particular row/column
+ (col default to 0)
+ """
+ return self.rset.get_entity(row, col)
+
+ def complete_entity(self, row, col=0, skip_bytes=True):
+ """short cut to get an completed entity instance for a particular
+ row (all instance's attributes have been fetched)
+ """
+ entity = self.entity(row, col)
+ entity.complete(skip_bytes=skip_bytes)
+ return entity
+
+ def user_rql_callback(self, args, msg=None):
+ """register a user callback to execute some rql query and return an url
+ to call it ready to be inserted in html
+ """
+ def rqlexec(req, rql, args=None, key=None):
+ req.execute(rql, args, key)
+ return self.user_callback(rqlexec, args, msg)
+
+ def user_callback(self, cb, args, msg=None, nonify=False):
+ """register the given user callback and return an url to call it ready to be
+ inserted in html
+ """
+ self.req.add_js('cubicweb.ajax.js')
+ if nonify:
+ # XXX < 2.48.3 bw compat
+ warn('nonify argument is deprecated', DeprecationWarning, stacklevel=2)
+ _cb = cb
+ def cb(*args):
+ _cb(*args)
+ cbname = self.req.register_onetime_callback(cb, *args)
+ msg = dumps(msg or '')
+ return "javascript:userCallbackThenReloadPage('%s', %s)" % (
+ cbname, msg)
+
+ # formating methods #######################################################
+
+ def tal_render(self, template, variables):
+ """render a precompiled page template with variables in the given
+ dictionary as context
+ """
+ from cubicweb.common.tal import CubicWebContext
+ context = CubicWebContext()
+ context.update({'self': self, 'rset': self.rset, '_' : self.req._,
+ 'req': self.req, 'user': self.req.user})
+ context.update(variables)
+ output = UStringIO()
+ template.expand(context, output)
+ return output.getvalue()
+
+ def format_date(self, date, date_format=None, time=False):
+ """return a string for a mx date time according to application's
+ configuration
+ """
+ if date:
+ if date_format is None:
+ if time:
+ date_format = self.req.property_value('ui.datetime-format')
+ else:
+ date_format = self.req.property_value('ui.date-format')
+ return ustrftime(date, date_format)
+ return u''
+
+ def format_time(self, time):
+ """return a string for a mx date time according to application's
+ configuration
+ """
+ if time:
+ return ustrftime(time, self.req.property_value('ui.time-format'))
+ return u''
+
+ def format_float(self, num):
+ """return a string for floating point number according to application's
+ configuration
+ """
+ if num:
+ return self.req.property_value('ui.float-format') % num
+ return u''
+
+ # security related methods ################################################
+
+ def ensure_ro_rql(self, rql):
+ """raise an exception if the given rql is not a select query"""
+ first = rql.split(' ', 1)[0].lower()
+ if first in ('insert', 'set', 'delete'):
+ raise Unauthorized(self.req._('only select queries are authorized'))
+
+
+class AppObject(AppRsetObject):
+ """base class for application objects which are not selected
+ according to a result set, only by their identifier.
+
+ Those objects may not have req, rset and cursor set.
+ """
+
+ @classmethod
+ def selected(cls, *args, **kwargs):
+ """by default web app objects are usually instantiated on
+ selection
+ """
+ return cls(*args, **kwargs)
+
+ def __init__(self, req=None, rset=None, **kwargs):
+ self.req = req
+ self.rset = rset
+ self.__dict__.update(kwargs)
diff -r 93447d75c4b9 -r 6a25c58a1c23 common/appobject.py
--- a/common/appobject.py Fri Feb 27 09:59:53 2009 +0100
+++ b/common/appobject.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,463 +1,3 @@
-"""Base class for dynamically loaded objects manipulated in the web interface
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
from warnings import warn
-
-from mx.DateTime import now, oneSecond
-from simplejson import dumps
-
-from logilab.common.deprecation import obsolete
-
-from rql.nodes import VariableRef, SubQuery
-from rql.stmts import Union, Select
-
-from cubicweb import Unauthorized
-from cubicweb.vregistry import VObject
-from cubicweb.common.utils import UStringIO
-from cubicweb.common.uilib import html_escape, ustrftime
-from cubicweb.common.registerers import yes_registerer, priority_registerer
-from cubicweb.common.selectors import yes
-
-_MARKER = object()
-
-
-class Cache(dict):
- def __init__(self):
- super(Cache, self).__init__()
- self.cache_creation_date = None
- self.latest_cache_lookup = now()
-
-CACHE_REGISTRY = {}
-
-class AppRsetObject(VObject):
- """This is the base class for CubicWeb application objects
- which are selected according to a request and result set.
-
- Classes are kept in the vregistry and instantiation is done at selection
- time.
-
- At registration time, the following attributes are set on the class:
- :vreg:
- the application's registry
- :schema:
- the application's schema
- :config:
- the application's configuration
-
- At instantiation time, the following attributes are set on the instance:
- :req:
- current request
- :rset:
- result set on which the object is applied
- """
-
- @classmethod
- def registered(cls, vreg):
- cls.vreg = vreg
- cls.schema = vreg.schema
- cls.config = vreg.config
- cls.register_properties()
- return cls
-
- @classmethod
- def selected(cls, req, rset, row=None, col=None, **kwargs):
- """by default web app objects are usually instantiated on
- selection according to a request, a result set, and optional
- row and col
- """
- instance = cls(req, rset)
- instance.row = row
- instance.col = col
- return instance
-
- # Eproperties definition:
- # key: id of the property (the actual EProperty key is build using
- # ..
- # value: tuple (property type, vocabfunc, default value, property description)
- # possible types are those used by `logilab.common.configuration`
- #
- # notice that when it exists multiple objects with the same id (adaptation,
- # overriding) only the first encountered definition is considered, so those
- # objects can't try to have different default values for instance.
-
- property_defs = {}
-
- @classmethod
- def register_properties(cls):
- for propid, pdef in cls.property_defs.items():
- pdef = pdef.copy() # may be shared
- pdef['default'] = getattr(cls, propid, pdef['default'])
- pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide'))
- cls.vreg.register_property(cls.propkey(propid), **pdef)
-
- @classmethod
- def propkey(cls, propid):
- return '%s.%s.%s' % (cls.__registry__, cls.id, propid)
-
-
- def __init__(self, req, rset):
- super(AppRsetObject, self).__init__()
- self.req = req
- self.rset = rset
-
- @property
- def cursor(self): # XXX deprecate in favor of req.cursor?
- msg = '.cursor is deprecated, use req.execute (or req.cursor if necessary)'
- warn(msg, DeprecationWarning, stacklevel=2)
- return self.req.cursor
-
- def get_cache(self, cachename):
- """
- NOTE: cachename should be dotted names as in :
- - cubicweb.mycache
- - cubes.blog.mycache
- - etc.
- """
- if cachename in CACHE_REGISTRY:
- cache = CACHE_REGISTRY[cachename]
- else:
- cache = Cache()
- CACHE_REGISTRY[cachename] = cache
- _now = now()
- if _now > cache.latest_cache_lookup + oneSecond:
- ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T',
- {'name':cachename}).get_entity(0,0)
- cache.latest_cache_lookup = _now
- if not ecache.valid(cache.cache_creation_date):
- cache.empty()
- cache.cache_creation_date = _now
- return cache
-
- def propval(self, propid):
- assert self.req
- return self.req.property_value(self.propkey(propid))
-
-
- def limited_rql(self):
- """return a printable rql for the result set associated to the object,
- with limit/offset correctly set according to maximum page size and
- currently displayed page when necessary
- """
- # try to get page boundaries from the navigation component
- # XXX we should probably not have a ref to this component here (eg in
- # cubicweb.common)
- nav = self.vreg.select_component('navigation', self.req, self.rset)
- if nav:
- start, stop = nav.page_boundaries()
- rql = self._limit_offset_rql(stop - start, start)
- # result set may have be limited manually in which case navigation won't
- # apply
- elif self.rset.limited:
- rql = self._limit_offset_rql(*self.rset.limited)
- # navigation component doesn't apply and rset has not been limited, no
- # need to limit query
- else:
- rql = self.rset.printable_rql()
- return rql
-
- def _limit_offset_rql(self, limit, offset):
- rqlst = self.rset.syntax_tree()
- if len(rqlst.children) == 1:
- select = rqlst.children[0]
- olimit, ooffset = select.limit, select.offset
- select.limit, select.offset = limit, offset
- rql = rqlst.as_string(kwargs=self.rset.args)
- # restore original limit/offset
- select.limit, select.offset = olimit, ooffset
- else:
- newselect = Select()
- newselect.limit = limit
- newselect.offset = offset
- aliases = [VariableRef(newselect.get_variable(vref.name, i))
- for i, vref in enumerate(rqlst.selection)]
- newselect.set_with([SubQuery(aliases, rqlst)], check=False)
- newunion = Union()
- newunion.append(newselect)
- rql = rqlst.as_string(kwargs=self.rset.args)
- rqlst.parent = None
- return rql
-
- # url generation methods ##################################################
-
- controller = 'view'
-
- def build_url(self, method=None, **kwargs):
- """return an absolute URL using params dictionary key/values as URL
- parameters. Values are automatically URL quoted, and the
- publishing method to use may be specified or will be guessed.
- """
- # XXX I (adim) think that if method is passed explicitly, we should
- # not try to process it and directly call req.build_url()
- if method is None:
- method = self.controller
- if method == 'view' and self.req.from_controller() == 'view' and \
- not '_restpath' in kwargs:
- method = self.req.relative_path(includeparams=False) or 'view'
- return self.req.build_url(method, **kwargs)
-
- # various resources accessors #############################################
-
- def etype_rset(self, etype, size=1):
- """return a fake result set for a particular entity type"""
- msg = '.etype_rset is deprecated, use req.etype_rset'
- warn(msg, DeprecationWarning, stacklevel=2)
- return self.req.etype_rset(etype, size=1)
-
- def eid_rset(self, eid, etype=None):
- """return a result set for the given eid"""
- msg = '.eid_rset is deprecated, use req.eid_rset'
- warn(msg, DeprecationWarning, stacklevel=2)
- return self.req.eid_rset(eid, etype)
-
- def entity(self, row, col=0):
- """short cut to get an entity instance for a particular row/column
- (col default to 0)
- """
- return self.rset.get_entity(row, col)
-
- def complete_entity(self, row, col=0, skip_bytes=True):
- """short cut to get an completed entity instance for a particular
- row (all instance's attributes have been fetched)
- """
- entity = self.entity(row, col)
- entity.complete(skip_bytes=skip_bytes)
- return entity
-
- def user_rql_callback(self, args, msg=None):
- """register a user callback to execute some rql query and return an url
- to call it ready to be inserted in html
- """
- def rqlexec(req, rql, args=None, key=None):
- req.execute(rql, args, key)
- return self.user_callback(rqlexec, args, msg)
-
- def user_callback(self, cb, args, msg=None, nonify=False):
- """register the given user callback and return an url to call it ready to be
- inserted in html
- """
- self.req.add_js('cubicweb.ajax.js')
- if nonify:
- # XXX < 2.48.3 bw compat
- warn('nonify argument is deprecated', DeprecationWarning, stacklevel=2)
- _cb = cb
- def cb(*args):
- _cb(*args)
- cbname = self.req.register_onetime_callback(cb, *args)
- msg = dumps(msg or '')
- return "javascript:userCallbackThenReloadPage('%s', %s)" % (
- cbname, msg)
-
- # formating methods #######################################################
-
- def tal_render(self, template, variables):
- """render a precompiled page template with variables in the given
- dictionary as context
- """
- from cubicweb.common.tal import CubicWebContext
- context = CubicWebContext()
- context.update({'self': self, 'rset': self.rset, '_' : self.req._,
- 'req': self.req, 'user': self.req.user})
- context.update(variables)
- output = UStringIO()
- template.expand(context, output)
- return output.getvalue()
-
- def format_date(self, date, date_format=None, time=False):
- """return a string for a mx date time according to application's
- configuration
- """
- if date:
- if date_format is None:
- if time:
- date_format = self.req.property_value('ui.datetime-format')
- else:
- date_format = self.req.property_value('ui.date-format')
- return ustrftime(date, date_format)
- return u''
-
- def format_time(self, time):
- """return a string for a mx date time according to application's
- configuration
- """
- if time:
- return ustrftime(time, self.req.property_value('ui.time-format'))
- return u''
-
- def format_float(self, num):
- """return a string for floating point number according to application's
- configuration
- """
- if num:
- return self.req.property_value('ui.float-format') % num
- return u''
-
- # security related methods ################################################
-
- def ensure_ro_rql(self, rql):
- """raise an exception if the given rql is not a select query"""
- first = rql.split(' ', 1)[0].lower()
- if first in ('insert', 'set', 'delete'):
- raise Unauthorized(self.req._('only select queries are authorized'))
-
- # .accepts handling utilities #############################################
-
- accepts = ('Any',)
-
- @classmethod
- def accept_rset(cls, req, rset, row, col):
- """apply the following rules:
- * if row is None, return the sum of values returned by the method
- for each entity's type in the result set. If any score is 0,
- return 0.
- * if row is specified, return the value returned by the method with
- the entity's type of this row
- """
- if row is None:
- score = 0
- for etype in rset.column_types(0):
- accepted = cls.accept(req.user, etype)
- if not accepted:
- return 0
- score += accepted
- return score
- return cls.accept(req.user, rset.description[row][col or 0])
-
- @classmethod
- def accept(cls, user, etype):
- """score etype, returning better score on exact match"""
- if 'Any' in cls.accepts:
- return 1
- eschema = cls.schema.eschema(etype)
- matching_types = [e.type for e in eschema.ancestors()]
- matching_types.append(etype)
- for index, basetype in enumerate(matching_types):
- if basetype in cls.accepts:
- return 2 + index
- return 0
-
- # .rtype handling utilities ##############################################
-
- @classmethod
- def relation_possible(cls, etype):
- """tell if a relation with etype entity is possible according to
- mixed class'.etype, .rtype and .target attributes
-
- XXX should probably be moved out to a function
- """
- schema = cls.schema
- rtype = cls.rtype
- eschema = schema.eschema(etype)
- if hasattr(cls, 'role'):
- role = cls.role
- elif cls.target == 'subject':
- role = 'object'
- else:
- role = 'subject'
- # check if this relation is possible according to the schema
- try:
- if role == 'object':
- rschema = eschema.object_relation(rtype)
- else:
- rschema = eschema.subject_relation(rtype)
- except KeyError:
- return False
- if hasattr(cls, 'etype'):
- letype = cls.etype
- try:
- if role == 'object':
- return etype in rschema.objects(letype)
- else:
- return etype in rschema.subjects(letype)
- except KeyError, ex:
- return False
- return True
-
-
- # XXX deprecated (since 2.43) ##########################
-
- @obsolete('use req.datadir_url')
- def datadir_url(self):
- """return url of the application's data directory"""
- return self.req.datadir_url
-
- @obsolete('use req.external_resource()')
- def external_resource(self, rid, default=_MARKER):
- return self.req.external_resource(rid, default)
-
-
-class AppObject(AppRsetObject):
- """base class for application objects which are not selected
- according to a result set, only by their identifier.
-
- Those objects may not have req, rset and cursor set.
- """
-
- @classmethod
- def selected(cls, *args, **kwargs):
- """by default web app objects are usually instantiated on
- selection
- """
- return cls(*args, **kwargs)
-
- def __init__(self, req=None, rset=None, **kwargs):
- self.req = req
- self.rset = rset
- self.__dict__.update(kwargs)
-
-
-class ReloadableMixIn(object):
- """simple mixin for reloadable parts of UI"""
-
- def user_callback(self, cb, args, msg=None, nonify=False):
- """register the given user callback and return an url to call it ready to be
- inserted in html
- """
- self.req.add_js('cubicweb.ajax.js')
- if nonify:
- _cb = cb
- def cb(*args):
- _cb(*args)
- cbname = self.req.register_onetime_callback(cb, *args)
- return self.build_js(cbname, html_escape(msg or ''))
-
- def build_update_js_call(self, cbname, msg):
- rql = html_escape(self.rset.printable_rql())
- return "javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')" % (
- cbname, self.id, rql, msg, self.__registry__, self.div_id())
-
- def build_reload_js_call(self, cbname, msg):
- return "javascript:userCallbackThenReloadPage('%s', '%s')" % (cbname, msg)
-
- build_js = build_update_js_call # expect updatable component by default
-
- def div_id(self):
- return ''
-
-
-class ComponentMixIn(ReloadableMixIn):
- """simple mixin for component object"""
- __registry__ = 'components'
- __registerer__ = yes_registerer
- __selectors__ = (yes,)
- __select__ = classmethod(*__selectors__)
-
- def div_class(self):
- return '%s %s' % (self.propval('htmlclass'), self.id)
-
- def div_id(self):
- return '%sComponent' % self.id
-
-
-class Component(ComponentMixIn, AppObject):
- """base class for non displayable components
- """
-
-class SingletonComponent(Component):
- """base class for non displayable unique components
- """
- __registerer__ = priority_registerer
+warn('moved to cubicweb.appobject', DeprecationWarning, stacklevel=2)
+from cubicweb.appobject import *
diff -r 93447d75c4b9 -r 6a25c58a1c23 common/entity.py
--- a/common/entity.py Fri Feb 27 09:59:53 2009 +0100
+++ b/common/entity.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,1106 +1,4 @@
-"""Base class for entity objects manipulated in clients
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common import interface
-from logilab.common.compat import all
-from logilab.common.decorators import cached
-from logilab.mtconverter import TransformData, TransformError
-from rql.utils import rqlvar_maker
-
-from cubicweb import Unauthorized
-from cubicweb.vregistry import autoselectors
-from cubicweb.rset import ResultSet
-from cubicweb.common.appobject import AppRsetObject
-from cubicweb.common.registerers import id_registerer
-from cubicweb.common.selectors import yes
-from cubicweb.common.uilib import printable_value, html_escape, soup2xhtml
-from cubicweb.common.mixins import MI_REL_TRIGGERS
-from cubicweb.common.mttransforms import ENGINE
-from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype
-
-_marker = object()
-
-def greater_card(rschema, subjtypes, objtypes, index):
- for subjtype in subjtypes:
- for objtype in objtypes:
- card = rschema.rproperty(subjtype, objtype, 'cardinality')[index]
- if card in '+*':
- return card
- return '1'
-
-
-class RelationTags(object):
-
- MODE_TAGS = frozenset(('link', 'create'))
- CATEGORY_TAGS = frozenset(('primary', 'secondary', 'generic', 'generated',
- 'inlineview'))
-
- def __init__(self, eclass, tagdefs):
- # XXX if a rtag is redefined in a subclass,
- # the rtag of the base class overwrite the rtag of the subclass
- self.eclass = eclass
- self._tagdefs = {}
- for relation, tags in tagdefs.iteritems():
- # tags must become a set
- if isinstance(tags, basestring):
- tags = set((tags,))
- elif not isinstance(tags, set):
- tags = set(tags)
- # relation must become a 3-uple (rtype, targettype, role)
- if isinstance(relation, basestring):
- self._tagdefs[(relation, '*', 'subject')] = tags
- self._tagdefs[(relation, '*', 'object')] = tags
- elif len(relation) == 1: # useful ?
- self._tagdefs[(relation[0], '*', 'subject')] = tags
- self._tagdefs[(relation[0], '*', 'object')] = tags
- elif len(relation) == 2:
- rtype, ttype = relation
- ttype = bw_normalize_etype(ttype) # XXX bw compat
- self._tagdefs[rtype, ttype, 'subject'] = tags
- self._tagdefs[rtype, ttype, 'object'] = tags
- elif len(relation) == 3:
- relation = list(relation) # XXX bw compat
- relation[1] = bw_normalize_etype(relation[1])
- self._tagdefs[tuple(relation)] = tags
- else:
- raise ValueError('bad rtag definition (%r)' % (relation,))
-
-
- def __initialize__(self):
- # eclass.[*]schema are only set when registering
- self.schema = self.eclass.schema
- eschema = self.eschema = self.eclass.e_schema
- rtags = self._tagdefs
- # expand wildcards in rtags and add automatic tags
- for rschema, tschemas, role in sorted(eschema.relation_definitions(True)):
- rtype = rschema.type
- star_tags = rtags.pop((rtype, '*', role), set())
- for tschema in tschemas:
- tags = rtags.setdefault((rtype, tschema.type, role), set(star_tags))
- if role == 'subject':
- X, Y = eschema, tschema
- card = rschema.rproperty(X, Y, 'cardinality')[0]
- composed = rschema.rproperty(X, Y, 'composite') == 'object'
- else:
- X, Y = tschema, eschema
- card = rschema.rproperty(X, Y, 'cardinality')[1]
- composed = rschema.rproperty(X, Y, 'composite') == 'subject'
- # set default category tags if needed
- if not tags & self.CATEGORY_TAGS:
- if card in '1+':
- if not rschema.is_final() and composed:
- category = 'generated'
- elif rschema.is_final() and (
- rschema.type.endswith('_format')
- or rschema.type.endswith('_encoding')):
- category = 'generated'
- else:
- category = 'primary'
- elif rschema.is_final():
- if (rschema.type.endswith('_format')
- or rschema.type.endswith('_encoding')):
- category = 'generated'
- else:
- category = 'secondary'
- else:
- category = 'generic'
- tags.add(category)
- if not tags & self.MODE_TAGS:
- if card in '?1':
- # by default, suppose link mode if cardinality doesn't allow
- # more than one relation
- mode = 'link'
- elif rschema.rproperty(X, Y, 'composite') == role:
- # if self is composed of the target type, create mode
- mode = 'create'
- else:
- # link mode by default
- mode = 'link'
- tags.add(mode)
-
- def _default_target(self, rschema, role='subject'):
- eschema = self.eschema
- if role == 'subject':
- return eschema.subject_relation(rschema).objects(eschema)[0]
- else:
- return eschema.object_relation(rschema).subjects(eschema)[0]
-
- # dict compat
- def __getitem__(self, key):
- if isinstance(key, basestring):
- key = (key,)
- return self.get_tags(*key)
-
- __contains__ = __getitem__
-
- def get_tags(self, rtype, targettype=None, role='subject'):
- rschema = self.schema.rschema(rtype)
- if targettype is None:
- tschema = self._default_target(rschema, role)
- else:
- tschema = self.schema.eschema(targettype)
- return self._tagdefs[(rtype, tschema.type, role)]
-
- __call__ = get_tags
-
- def get_mode(self, rtype, targettype=None, role='subject'):
- # XXX: should we make an assertion on rtype not being final ?
- # assert not rschema.is_final()
- tags = self.get_tags(rtype, targettype, role)
- # do not change the intersection order !
- modes = tags & self.MODE_TAGS
- assert len(modes) == 1
- return modes.pop()
-
- def get_category(self, rtype, targettype=None, role='subject'):
- tags = self.get_tags(rtype, targettype, role)
- categories = tags & self.CATEGORY_TAGS
- assert len(categories) == 1
- return categories.pop()
-
- def is_inlined(self, rtype, targettype=None, role='subject'):
- # return set(('primary', 'secondary')) & self.get_tags(rtype, targettype)
- return 'inlineview' in self.get_tags(rtype, targettype, role)
-
-
-class metaentity(autoselectors):
- """this metaclass sets the relation tags on the entity class
- and deals with the `widgets` attribute
- """
- def __new__(mcs, name, bases, classdict):
- # collect baseclass' rtags
- tagdefs = {}
- widgets = {}
- for base in bases:
- tagdefs.update(getattr(base, '__rtags__', {}))
- widgets.update(getattr(base, 'widgets', {}))
- # update with the class' own rtgas
- tagdefs.update(classdict.get('__rtags__', {}))
- widgets.update(classdict.get('widgets', {}))
- # XXX decide whether or not it's a good idea to replace __rtags__
- # good point: transparent support for inheritance levels >= 2
- # bad point: we loose the information of which tags are specific
- # to this entity class
- classdict['__rtags__'] = tagdefs
- classdict['widgets'] = widgets
- eclass = super(metaentity, mcs).__new__(mcs, name, bases, classdict)
- # adds the "rtags" attribute
- eclass.rtags = RelationTags(eclass, tagdefs)
- return eclass
-
-
-class Entity(AppRsetObject, dict):
- """an entity instance has e_schema automagically set on
- the class and instances has access to their issuing cursor.
-
- A property is set for each attribute and relation on each entity's type
- class. Becare that among attributes, 'eid' is *NEITHER* stored in the
- dict containment (which acts as a cache for other attributes dynamically
- fetched)
-
- :type e_schema: `cubicweb.schema.EntitySchema`
- :ivar e_schema: the entity's schema
-
- :type rest_var: str
- :cvar rest_var: indicates which attribute should be used to build REST urls
- If None is specified, the first non-meta attribute will
- be used
-
- :type skip_copy_for: list
- :cvar skip_copy_for: a list of relations that should be skipped when copying
- this kind of entity. Note that some relations such
- as composite relations or relations that have '?1' as object
- cardinality
- """
- __metaclass__ = metaentity
- __registry__ = 'etypes'
- __registerer__ = id_registerer
- __selectors__ = (yes,)
- widgets = {}
- id = None
- e_schema = None
- eid = None
- rest_attr = None
- skip_copy_for = ()
-
- @classmethod
- def registered(cls, registry):
- """build class using descriptor at registration time"""
- assert cls.id is not None
- super(Entity, cls).registered(registry)
- if cls.id != 'Any':
- cls.__initialize__()
- return cls
-
- MODE_TAGS = set(('link', 'create'))
- CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
- @classmethod
- def __initialize__(cls):
- """initialize a specific entity class by adding descriptors to access
- entity type's attributes and relations
- """
- etype = cls.id
- assert etype != 'Any', etype
- cls.e_schema = eschema = cls.schema.eschema(etype)
- for rschema, _ in eschema.attribute_definitions():
- if rschema.type == 'eid':
- continue
- setattr(cls, rschema.type, Attribute(rschema.type))
- mixins = []
- for rschema, _, x in eschema.relation_definitions():
- if (rschema, x) in MI_REL_TRIGGERS:
- mixin = MI_REL_TRIGGERS[(rschema, x)]
- if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ?
- mixins.append(mixin)
- for iface in getattr(mixin, '__implements__', ()):
- if not interface.implements(cls, iface):
- interface.extend(cls, iface)
- if x == 'subject':
- setattr(cls, rschema.type, SubjectRelation(rschema))
- else:
- attr = 'reverse_%s' % rschema.type
- setattr(cls, attr, ObjectRelation(rschema))
- if mixins:
- cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object])
- cls.debug('plugged %s mixins on %s', mixins, etype)
- cls.rtags.__initialize__()
-
- @classmethod
- def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
- settype=True, ordermethod='fetch_order'):
- """return a rql to fetch all entities of the class type"""
- restrictions = restriction or []
- if settype:
- restrictions.append('%s is %s' % (mainvar, cls.id))
- if fetchattrs is None:
- fetchattrs = cls.fetch_attrs
- selection = [mainvar]
- orderby = []
- # start from 26 to avoid possible conflicts with X
- varmaker = rqlvar_maker(index=26)
- cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
- orderby, restrictions, user, ordermethod)
- rql = 'Any %s' % ','.join(selection)
- if orderby:
- rql += ' ORDERBY %s' % ','.join(orderby)
- rql += ' WHERE %s' % ', '.join(restrictions)
- return rql
-
- @classmethod
- def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
- selection, orderby, restrictions, user,
- ordermethod='fetch_order', visited=None):
- eschema = cls.e_schema
- if visited is None:
- visited = set((eschema.type,))
- elif eschema.type in visited:
- # avoid infinite recursion
- return
- else:
- visited.add(eschema.type)
- _fetchattrs = []
- for attr in fetchattrs:
- try:
- rschema = eschema.subject_relation(attr)
- except KeyError:
- cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
- attr, cls.id)
- continue
- if not user.matching_groups(rschema.get_groups('read')):
- continue
- var = varmaker.next()
- selection.append(var)
- restriction = '%s %s %s' % (mainvar, attr, var)
- restrictions.append(restriction)
- if not rschema.is_final():
- # XXX this does not handle several destination types
- desttype = rschema.objects(eschema.type)[0]
- card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
- if card not in '?1':
- selection.pop()
- restrictions.pop()
- continue
- if card == '?':
- restrictions[-1] += '?' # left outer join if not mandatory
- destcls = cls.vreg.etype_class(desttype)
- destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
- selection, orderby, restrictions,
- user, ordermethod, visited=visited)
- orderterm = getattr(cls, ordermethod)(attr, var)
- if orderterm:
- orderby.append(orderterm)
- return selection, orderby, restrictions
-
- def __init__(self, req, rset, row=None, col=0):
- AppRsetObject.__init__(self, req, rset)
- dict.__init__(self)
- self.row, self.col = row, col
- self._related_cache = {}
- if rset is not None:
- self.eid = rset[row][col]
- else:
- self.eid = None
- self._is_saved = True
-
- def __repr__(self):
- return '' % (
- self.e_schema, self.eid, self.keys(), id(self))
-
- def __nonzero__(self):
- return True
-
- def __hash__(self):
- return id(self)
-
- def pre_add_hook(self):
- """hook called by the repository before doing anything to add the entity
- (before_add entity hooks have not been called yet). This give the
- occasion to do weird stuff such as autocast (File -> Image for instance).
-
- This method must return the actual entity to be added.
- """
- return self
-
- def set_eid(self, eid):
- self.eid = self['eid'] = eid
-
- def has_eid(self):
- """return True if the entity has an attributed eid (False
- meaning that the entity has to be created
- """
- try:
- int(self.eid)
- return True
- except (ValueError, TypeError):
- return False
-
- def is_saved(self):
- """during entity creation, there is some time during which the entity
- has an eid attributed though it's not saved (eg during before_add_entity
- hooks). You can use this method to ensure the entity has an eid *and* is
- saved in its source.
- """
- return self.has_eid() and self._is_saved
-
- @cached
- def metainformation(self):
- res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid)))
- res['source'] = self.req.source_defs()[res['source']]
- return res
-
- def clear_local_perm_cache(self, action):
- for rqlexpr in self.e_schema.get_rqlexprs(action):
- self.req.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
-
- def check_perm(self, action):
- self.e_schema.check_perm(self.req, action, self.eid)
-
- def has_perm(self, action):
- return self.e_schema.has_perm(self.req, action, self.eid)
-
- def view(self, vid, __registry='views', **kwargs):
- """shortcut to apply a view on this entity"""
- return self.vreg.render(__registry, vid, self.req, rset=self.rset,
- row=self.row, col=self.col, **kwargs)
-
- def absolute_url(self, method=None, **kwargs):
- """return an absolute url to view this entity"""
- # in linksearch mode, we don't want external urls else selecting
- # the object for use in the relation is tricky
- # XXX search_state is web specific
- if getattr(self.req, 'search_state', ('normal',))[0] == 'normal':
- kwargs['base_url'] = self.metainformation()['source'].get('base-url')
- if method is None or method == 'view':
- kwargs['_restpath'] = self.rest_path()
- else:
- kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
- return self.build_url(method, **kwargs)
-
- def rest_path(self):
- """returns a REST-like (relative) path for this entity"""
- mainattr, needcheck = self._rest_attr_info()
- etype = str(self.e_schema)
- if mainattr == 'eid':
- value = self.eid
- else:
- value = getattr(self, mainattr)
- if value is None:
- return '%s/eid/%s' % (etype.lower(), self.eid)
- if needcheck:
- # make sure url is not ambiguous
- rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (etype, mainattr)
- if value is not None:
- nbresults = self.req.execute(rql, {'value' : value})[0][0]
- # may an assertion that nbresults is not 0 would be a good idea
- if nbresults != 1: # no ambiguity
- return '%s/eid/%s' % (etype.lower(), self.eid)
- return '%s/%s' % (etype.lower(), self.req.url_quote(value))
-
- @classmethod
- def _rest_attr_info(cls):
- mainattr, needcheck = 'eid', True
- if cls.rest_attr:
- mainattr = cls.rest_attr
- needcheck = not cls.e_schema.has_unique_values(mainattr)
- else:
- for rschema in cls.e_schema.subject_relations():
- if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema):
- mainattr = str(rschema)
- needcheck = False
- break
- if mainattr == 'eid':
- needcheck = False
- return mainattr, needcheck
-
- @cached
- def formatted_attrs(self):
- """returns the list of attributes which have some format information
- (i.e. rich text strings)
- """
- attrs = []
- for rschema, attrschema in self.e_schema.attribute_definitions():
- if attrschema.type == 'String' and self.has_format(rschema):
- attrs.append(rschema.type)
- return attrs
-
- def format(self, attr):
- """return the mime type format for an attribute (if specified)"""
- return getattr(self, '%s_format' % attr, None)
-
- def text_encoding(self, attr):
- """return the text encoding for an attribute, default to site encoding
- """
- encoding = getattr(self, '%s_encoding' % attr, None)
- return encoding or self.vreg.property_value('ui.encoding')
-
- def has_format(self, attr):
- """return true if this entity's schema has a format field for the given
- attribute
- """
- return self.e_schema.has_subject_relation('%s_format' % attr)
-
- def has_text_encoding(self, attr):
- """return true if this entity's schema has ab encoding field for the
- given attribute
- """
- return self.e_schema.has_subject_relation('%s_encoding' % attr)
-
- def printable_value(self, attr, value=_marker, attrtype=None,
- format='text/html', displaytime=True):
- """return a displayable value (i.e. unicode string) which may contains
- html tags
- """
- attr = str(attr)
- if value is _marker:
- value = getattr(self, attr)
- if isinstance(value, basestring):
- value = value.strip()
- if value is None or value == '': # don't use "not", 0 is an acceptable value
- return u''
- if attrtype is None:
- attrtype = self.e_schema.destination(attr)
- props = self.e_schema.rproperties(attr)
- if attrtype == 'String':
- # internalinalized *and* formatted string such as schema
- # description...
- if props.get('internationalizable'):
- value = self.req._(value)
- attrformat = self.format(attr)
- if attrformat:
- return self.mtc_transform(value, attrformat, format,
- self.req.encoding)
- elif attrtype == 'Bytes':
- attrformat = self.format(attr)
- if attrformat:
- try:
- encoding = getattr(self, '%s_encoding' % attr)
- except AttributeError:
- encoding = self.req.encoding
- return self.mtc_transform(value.getvalue(), attrformat, format,
- encoding)
- return u''
- value = printable_value(self.req, attrtype, value, props, displaytime)
- if format == 'text/html':
- value = html_escape(value)
- return value
-
- def mtc_transform(self, data, format, target_format, encoding,
- _engine=ENGINE):
- trdata = TransformData(data, format, encoding, appobject=self)
- data = _engine.convert(trdata, target_format).decode()
- if format == 'text/html':
- data = soup2xhtml(data, self.req.encoding)
- return data
-
- # entity cloning ##########################################################
-
- def copy_relations(self, ceid):
- """copy relations of the object with the given eid on this object
-
- By default meta and composite relations are skipped.
- Overrides this if you want another behaviour
- """
- assert self.has_eid()
- execute = self.req.execute
- for rschema in self.e_schema.subject_relations():
- if rschema.meta or rschema.is_final():
- continue
- # skip already defined relations
- if getattr(self, rschema.type):
- continue
- if rschema.type in self.skip_copy_for:
- continue
- if rschema.type == 'in_state':
- # if the workflow is defining an initial state (XXX AND we are
- # not in the managers group? not done to be more consistent)
- # don't try to copy in_state
- if execute('Any S WHERE S state_of ET, ET initial_state S,'
- 'ET name %(etype)s', {'etype': str(self.e_schema)}):
- continue
- # skip composite relation
- if self.e_schema.subjrproperty(rschema, 'composite'):
- continue
- # skip relation with card in ?1 else we either change the copied
- # object (inlined relation) or inserting some inconsistency
- if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1':
- continue
- rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
- rschema.type, rschema.type)
- execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
- self.clear_related_cache(rschema.type, 'subject')
- for rschema in self.e_schema.object_relations():
- if rschema.meta:
- continue
- # skip already defined relations
- if getattr(self, 'reverse_%s' % rschema.type):
- continue
- # skip composite relation
- if self.e_schema.objrproperty(rschema, 'composite'):
- continue
- # skip relation with card in ?1 else we either change the copied
- # object (inlined relation) or inserting some inconsistency
- if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1':
- continue
- rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
- rschema.type, rschema.type)
- execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
- self.clear_related_cache(rschema.type, 'object')
-
- # data fetching methods ###################################################
-
- @cached
- def as_rset(self):
- """returns a resultset containing `self` information"""
- rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
- {'x': self.eid}, [(self.id,)])
- return self.req.decorate_rset(rset)
-
- def to_complete_relations(self):
- """by default complete final relations to when calling .complete()"""
- for rschema in self.e_schema.subject_relations():
- if rschema.is_final():
- continue
- if len(rschema.objects(self.e_schema)) > 1:
- # ambigous relations, the querier doesn't handle
- # outer join correctly in this case
- continue
- if rschema.inlined:
- matching_groups = self.req.user.matching_groups
- if matching_groups(rschema.get_groups('read')) and \
- all(matching_groups(es.get_groups('read'))
- for es in rschema.objects(self.e_schema)):
- yield rschema, 'subject'
-
- def to_complete_attributes(self, skip_bytes=True):
- for rschema, attrschema in self.e_schema.attribute_definitions():
- # skip binary data by default
- if skip_bytes and attrschema.type == 'Bytes':
- continue
- attr = rschema.type
- if attr == 'eid':
- continue
- # password retreival is blocked at the repository server level
- if not self.req.user.matching_groups(rschema.get_groups('read')) \
- or attrschema.type == 'Password':
- self[attr] = None
- continue
- yield attr
-
- def complete(self, attributes=None, skip_bytes=True):
- """complete this entity by adding missing attributes (i.e. query the
- repository to fill the entity)
-
- :type skip_bytes: bool
- :param skip_bytes:
- if true, attribute of type Bytes won't be considered
- """
- assert self.has_eid()
- varmaker = rqlvar_maker()
- V = varmaker.next()
- rql = ['WHERE %s eid %%(x)s' % V]
- selected = []
- for attr in (attributes or self.to_complete_attributes(skip_bytes)):
- # if attribute already in entity, nothing to do
- if self.has_key(attr):
- continue
- # case where attribute must be completed, but is not yet in entity
- var = varmaker.next()
- rql.append('%s %s %s' % (V, attr, var))
- selected.append((attr, var))
- # +1 since this doen't include the main variable
- lastattr = len(selected) + 1
- if attributes is None:
- # fetch additional relations (restricted to 0..1 relations)
- for rschema, role in self.to_complete_relations():
- rtype = rschema.type
- if self.relation_cached(rtype, role):
- continue
- var = varmaker.next()
- if role == 'subject':
- targettype = rschema.objects(self.e_schema)[0]
- card = rschema.rproperty(self.e_schema, targettype,
- 'cardinality')[0]
- if card == '1':
- rql.append('%s %s %s' % (V, rtype, var))
- else: # '?"
- rql.append('%s %s %s?' % (V, rtype, var))
- else:
- targettype = rschema.subjects(self.e_schema)[1]
- card = rschema.rproperty(self.e_schema, targettype,
- 'cardinality')[1]
- if card == '1':
- rql.append('%s %s %s' % (var, rtype, V))
- else: # '?"
- rql.append('%s? %s %s' % (var, rtype, V))
- assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
- role, card)
- selected.append(((rtype, role), var))
- if selected:
- # select V, we need it as the left most selected variable
- # if some outer join are included to fetch inlined relations
- rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
- ','.join(rql))
- execute = getattr(self.req, 'unsafe_execute', self.req.execute)
- rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0]
- # handle attributes
- for i in xrange(1, lastattr):
- self[str(selected[i-1][0])] = rset[i]
- # handle relations
- for i in xrange(lastattr, len(rset)):
- rtype, x = selected[i-1][0]
- value = rset[i]
- if value is None:
- rrset = ResultSet([], rql, {'x': self.eid})
- self.req.decorate_rset(rrset)
- else:
- rrset = self.req.eid_rset(value)
- self.set_related_cache(rtype, x, rrset)
-
- def get_value(self, name):
- """get value for the attribute relation , query the repository
- to get the value if necessary.
-
- :type name: str
- :param name: name of the attribute to get
- """
- try:
- value = self[name]
- except KeyError:
- if not self.is_saved():
- return None
- rql = "Any A WHERE X eid %%(x)s, X %s A" % name
- # XXX should we really use unsafe_execute here??
- execute = getattr(self.req, 'unsafe_execute', self.req.execute)
- try:
- rset = execute(rql, {'x': self.eid}, 'x')
- except Unauthorized:
- self[name] = value = None
- else:
- assert rset.rowcount <= 1, (self, rql, rset.rowcount)
- try:
- self[name] = value = rset.rows[0][0]
- except IndexError:
- # probably a multisource error
- self.critical("can't get value for attribute %s of entity with eid %s",
- name, self.eid)
- if self.e_schema.destination(name) == 'String':
- self[name] = value = self.req._('unaccessible')
- else:
- self[name] = value = None
- return value
-
- def related(self, rtype, role='subject', limit=None, entities=False):
- """returns a resultset of related entities
-
- :param role: is the role played by 'self' in the relation ('subject' or 'object')
- :param limit: resultset's maximum size
- :param entities: if True, the entites are returned; if False, a result set is returned
- """
- try:
- return self.related_cache(rtype, role, entities, limit)
- except KeyError:
- pass
- assert self.has_eid()
- rql = self.related_rql(rtype, role)
- rset = self.req.execute(rql, {'x': self.eid}, 'x')
- self.set_related_cache(rtype, role, rset)
- return self.related(rtype, role, limit, entities)
-
- def related_rql(self, rtype, role='subject'):
- rschema = self.schema[rtype]
- if role == 'subject':
- targettypes = rschema.objects(self.e_schema)
- restriction = 'E eid %%(x)s, E %s X' % rtype
- card = greater_card(rschema, (self.e_schema,), targettypes, 0)
- else:
- targettypes = rschema.subjects(self.e_schema)
- restriction = 'E eid %%(x)s, X %s E' % rtype
- card = greater_card(rschema, targettypes, (self.e_schema,), 1)
- if len(targettypes) > 1:
- fetchattrs_list = []
- for ttype in targettypes:
- etypecls = self.vreg.etype_class(ttype)
- fetchattrs_list.append(set(etypecls.fetch_attrs))
- fetchattrs = reduce(set.intersection, fetchattrs_list)
- rql = etypecls.fetch_rql(self.req.user, [restriction], fetchattrs,
- settype=False)
- else:
- etypecls = self.vreg.etype_class(targettypes[0])
- rql = etypecls.fetch_rql(self.req.user, [restriction], settype=False)
- # optimisation: remove ORDERBY if cardinality is 1 or ? (though
- # greater_card return 1 for those both cases)
- if card == '1':
- if ' ORDERBY ' in rql:
- rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
- rql.split(' WHERE ', 1)[1])
- elif not ' ORDERBY ' in rql:
- args = tuple(rql.split(' WHERE ', 1))
- rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args
- return rql
-
- # generic vocabulary methods ##############################################
-
- def vocabulary(self, rtype, role='subject', limit=None):
- """vocabulary functions must return a list of couples
- (label, eid) that will typically be used to fill the
- edition view's combobox.
-
- If `eid` is None in one of these couples, it should be
- interpreted as a separator in case vocabulary results are grouped
- """
- try:
- vocabfunc = getattr(self, '%s_%s_vocabulary' % (role, rtype))
- except AttributeError:
- vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
- # NOTE: it is the responsibility of `vocabfunc` to sort the result
- # (direclty through RQL or via a python sort). This is also
- # important because `vocabfunc` might return a list with
- # couples (label, None) which act as separators. In these
- # cases, it doesn't make sense to sort results afterwards.
- return vocabfunc(rtype, limit)
-
- def subject_relation_vocabulary(self, rtype, limit=None):
- """defaut vocabulary method for the given relation, looking for
- relation's object entities (i.e. self is the subject)
- """
- if isinstance(rtype, basestring):
- rtype = self.schema.rschema(rtype)
- done = None
- assert not rtype.is_final(), rtype
- if self.has_eid():
- done = set(e.eid for e in getattr(self, str(rtype)))
- result = []
- rsetsize = None
- for objtype in rtype.objects(self.e_schema):
- if limit is not None:
- rsetsize = limit - len(result)
- result += self.relation_vocabulary(rtype, objtype, 'subject',
- rsetsize, done)
- if limit is not None and len(result) >= limit:
- break
- return result
-
- def object_relation_vocabulary(self, rtype, limit=None):
- """defaut vocabulary method for the given relation, looking for
- relation's subject entities (i.e. self is the object)
- """
- if isinstance(rtype, basestring):
- rtype = self.schema.rschema(rtype)
- done = None
- if self.has_eid():
- done = set(e.eid for e in getattr(self, 'reverse_%s' % rtype))
- result = []
- rsetsize = None
- for subjtype in rtype.subjects(self.e_schema):
- if limit is not None:
- rsetsize = limit - len(result)
- result += self.relation_vocabulary(rtype, subjtype, 'object',
- rsetsize, done)
- if limit is not None and len(result) >= limit:
- break
- return result
-
- def relation_vocabulary(self, rtype, targettype, role,
- limit=None, done=None):
- if done is None:
- done = set()
- req = self.req
- rset = self.unrelated(rtype, targettype, role, limit)
- res = []
- for entity in rset.entities():
- if entity.eid in done:
- continue
- done.add(entity.eid)
- res.append((entity.view('combobox'), entity.eid))
- return res
-
- def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
- vocabconstraints=True):
- """build a rql to fetch `targettype` entities unrelated to this entity
- using (rtype, role) relation
- """
- ordermethod = ordermethod or 'fetch_unrelated_order'
- if isinstance(rtype, basestring):
- rtype = self.schema.rschema(rtype)
- if role == 'subject':
- evar, searchedvar = 'S', 'O'
- subjtype, objtype = self.e_schema, targettype
- else:
- searchedvar, evar = 'S', 'O'
- objtype, subjtype = self.e_schema, targettype
- if self.has_eid():
- restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar]
- else:
- restriction = []
- constraints = rtype.rproperty(subjtype, objtype, 'constraints')
- if vocabconstraints:
- # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
- # will be included as well
- restriction += [cstr.restriction for cstr in constraints
- if isinstance(cstr, RQLVocabularyConstraint)]
- else:
- restriction += [cstr.restriction for cstr in constraints
- if isinstance(cstr, RQLConstraint)]
- etypecls = self.vreg.etype_class(targettype)
- rql = etypecls.fetch_rql(self.req.user, restriction,
- mainvar=searchedvar, ordermethod=ordermethod)
- # ensure we have an order defined
- if not ' ORDERBY ' in rql:
- before, after = rql.split(' WHERE ', 1)
- rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
- return rql
-
- def unrelated(self, rtype, targettype, role='subject', limit=None,
- ordermethod=None):
- """return a result set of target type objects that may be related
- by a given relation, with self as subject or object
- """
- rql = self.unrelated_rql(rtype, targettype, role, ordermethod)
- if limit is not None:
- before, after = rql.split(' WHERE ', 1)
- rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
- if self.has_eid():
- return self.req.execute(rql, {'x': self.eid})
- return self.req.execute(rql)
-
- # relations cache handling ################################################
-
- def relation_cached(self, rtype, role):
- """return true if the given relation is already cached on the instance
- """
- return '%s_%s' % (rtype, role) in self._related_cache
-
- def related_cache(self, rtype, role, entities=True, limit=None):
- """return values for the given relation if it's cached on the instance,
- else raise `KeyError`
- """
- res = self._related_cache['%s_%s' % (rtype, role)][entities]
- if limit:
- if entities:
- res = res[:limit]
- else:
- res = res.limit(limit)
- return res
-
- def set_related_cache(self, rtype, role, rset, col=0):
- """set cached values for the given relation"""
- if rset:
- related = list(rset.entities(col))
- rschema = self.schema.rschema(rtype)
- if role == 'subject':
- rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
- 'cardinality')[1]
- target = 'object'
- else:
- rcard = rschema.rproperty(related[0].e_schema, self.e_schema,
- 'cardinality')[0]
- target = 'subject'
- if rcard in '?1':
- for rentity in related:
- rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self])
- else:
- related = []
- self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
-
- def clear_related_cache(self, rtype=None, role=None):
- """clear cached values for the given relation or the entire cache if
- no relation is given
- """
- if rtype is None:
- self._related_cache = {}
- else:
- assert role
- self._related_cache.pop('%s_%s' % (rtype, role), None)
-
- # raw edition utilities ###################################################
-
- def set_attributes(self, **kwargs):
- assert kwargs
- relations = []
- for key in kwargs:
- relations.append('X %s %%(%s)s' % (key, key))
- # update current local object
- self.update(kwargs)
- # and now update the database
- kwargs['x'] = self.eid
- self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
- kwargs, 'x')
-
- def delete(self):
- assert self.has_eid(), self.eid
- self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
- {'x': self.eid})
-
- # server side utilities ###################################################
-
- def set_defaults(self):
- """set default values according to the schema"""
- self._default_set = set()
- for attr, value in self.e_schema.defaults():
- if not self.has_key(attr):
- self[str(attr)] = value
- self._default_set.add(attr)
-
- def check(self, creation=False):
- """check this entity against its schema. Only final relation
- are checked here, constraint on actual relations are checked in hooks
- """
- # necessary since eid is handled specifically and yams require it to be
- # in the dictionary
- if self.req is None:
- _ = unicode
- else:
- _ = self.req._
- self.e_schema.check(self, creation=creation, _=_)
-
- 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:
- yielded = False
- 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
- if not yielded:
- yield self
- 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 indexer package
-
- :rtype: list
- :return: the list of indexable word of this entity
- """
- from indexer.query_objects import tokenize
- words = []
- for rschema in self.e_schema.indexable_attributes():
- try:
- value = self.printable_value(rschema, format='text/plain')
- except TransformError, ex:
- 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 ##########################################
-
-class Attribute(object):
- """descriptor that controls schema attribute access"""
-
- def __init__(self, attrname):
- assert attrname != 'eid'
- self._attrname = attrname
-
- def __get__(self, eobj, eclass):
- if eobj is None:
- return self
- return eobj.get_value(self._attrname)
-
- def __set__(self, eobj, value):
- # XXX bw compat
- # would be better to generate UPDATE queries than the current behaviour
- eobj.warning("deprecated usage, don't use 'entity.attr = val' notation)")
- eobj[self._attrname] = value
-
-
-class Relation(object):
- """descriptor that controls schema relation access"""
- _role = None # for pylint
-
- def __init__(self, rschema):
- self._rschema = rschema
- self._rtype = rschema.type
-
- def __get__(self, eobj, eclass):
- if eobj is None:
- raise AttributeError('%s cannot be only be accessed from instances'
- % self._rtype)
- return eobj.related(self._rtype, self._role, entities=True)
-
- def __set__(self, eobj, value):
- 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'))
+from warnings import warn
+warn('moved to cubicweb.entity', DeprecationWarning, stacklevel=2)
+from cubicweb.entity import *
+from cubicweb.entity import _marker
diff -r 93447d75c4b9 -r 6a25c58a1c23 common/html4zope.py
--- a/common/html4zope.py Fri Feb 27 09:59:53 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,153 +0,0 @@
-# Author: David Goodger
-# Contact: goodger@users.sourceforge.net
-# Revision: $Revision: 1.2 $
-# Date: $Date: 2005-07-04 16:36:50 $
-# Copyright: This module has been placed in the public domain.
-
-"""
-Simple HyperText Markup Language document tree Writer.
-
-The output conforms to the HTML 4.01 Transitional DTD and to the Extensible
-HTML version 1.0 Transitional DTD (*almost* strict). The output contains a
-minimum of formatting information. A cascading style sheet ("default.css" by
-default) is required for proper viewing with a modern graphical browser.
-
-http://cvs.zope.org/Zope/lib/python/docutils/writers/Attic/html4zope.py?rev=1.1.2.2&only_with_tag=ajung-restructuredtext-integration-branch&content-type=text/vnd.viewcvs-markup
-"""
-
-__docformat__ = 'reStructuredText'
-
-from logilab.mtconverter import html_escape
-
-from docutils import nodes
-from docutils.writers.html4css1 import Writer as CSS1Writer
-from docutils.writers.html4css1 import HTMLTranslator as CSS1HTMLTranslator
-import os
-
-default_level = int(os.environ.get('STX_DEFAULT_LEVEL', 3))
-
-class Writer(CSS1Writer):
- """css writer using our html translator"""
- def __init__(self, base_url):
- CSS1Writer.__init__(self)
- self.translator_class = URLBinder(base_url, HTMLTranslator)
-
- def apply_template(self):
- """overriding this is necessary with docutils >= 0.5"""
- return self.visitor.astext()
-
-class URLBinder:
- def __init__(self, url, klass):
- self.base_url = url
- self.translator_class = HTMLTranslator
-
- def __call__(self, document):
- translator = self.translator_class(document)
- translator.base_url = self.base_url
- return translator
-
-class HTMLTranslator(CSS1HTMLTranslator):
- """ReST tree to html translator"""
-
- def astext(self):
- """return the extracted html"""
- return ''.join(self.body)
-
- def visit_title(self, node):
- """Only 6 section levels are supported by HTML."""
- if isinstance(node.parent, nodes.topic):
- self.body.append(
- self.starttag(node, 'p', '', CLASS='topic-title'))
- if node.parent.hasattr('id'):
- self.body.append(
- self.starttag({}, 'a', '', name=node.parent['id']))
- self.context.append('
around each
- # rset item
- add_div_section = True
-
- def call(self, **kwargs):
- """the view is called for an entire result set, by default loop
- other rows of the result set and call the same view on the
- particular row
-
- Views applicable on None result sets have to override this method
- """
- rset = self.rset
- if rset is None:
- raise NotImplementedError, self
- wrap = self.templatable and len(rset) > 1 and self.add_div_section
- for i in xrange(len(rset)):
- if wrap:
- self.w(u'
")
-
- def cell_call(self, row, col, **kwargs):
- """the view is called for a particular result set cell"""
- raise NotImplementedError, self
-
- def linkable(self):
- """return True if the view may be linked in a menu
-
- by default views without title are not meant to be displayed
- """
- if not getattr(self, 'title', None):
- return False
- return True
-
- def is_primary(self):
- return self.id == 'primary'
-
- def url(self):
- """return the url associated with this view. Should not be
- necessary for non linkable views, but a default implementation
- is provided anyway.
- """
- try:
- return self.build_url(vid=self.id, rql=self.req.form['rql'])
- except KeyError:
- return self.build_url(vid=self.id)
-
- def set_request_content_type(self):
- """set the content type returned by this view"""
- self.req.set_content_type(self.content_type)
-
- # view utilities ##########################################################
-
- def view(self, __vid, rset, __fallback_vid=None, **kwargs):
- """shortcut to self.vreg.render method avoiding to pass self.req"""
- try:
- view = self.vreg.select_view(__vid, self.req, rset, **kwargs)
- except NoSelectableObject:
- if __fallback_vid is None:
- raise
- view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs)
- return view.dispatch(**kwargs)
-
- def wview(self, __vid, rset, __fallback_vid=None, **kwargs):
- """shortcut to self.view method automatically passing self.w as argument
- """
- self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
-
- def whead(self, data):
- self.req.html_headers.write(data)
-
- def wdata(self, data):
- """simple helper that escapes `data` and writes into `self.w`"""
- self.w(html_escape(data))
-
- def action(self, actionid, row=0):
- """shortcut to get action object with id `actionid`"""
- return self.vreg.select_action(actionid, self.req, self.rset,
- row=row)
-
- def action_url(self, actionid, label=None, row=0):
- """simple method to be able to display `actionid` as a link anywhere
- """
- action = self.vreg.select_action(actionid, self.req, self.rset,
- row=row)
- if action:
- label = label or self.req._(action.title)
- return u'%s' % (html_escape(action.url()), label)
- return u''
-
- def html_headers(self):
- """return a list of html headers (eg something to be inserted between
- and of the returned page
-
- by default return a meta tag to disable robot indexation of the page
- """
- return [NOINDEX]
-
- def page_title(self):
- """returns a title according to the result set - used for the
- title in the HTML header
- """
- vtitle = self.req.form.get('vtitle')
- if vtitle:
- return self.req._(vtitle)
- # class defined title will only be used if the resulting title doesn't
- # seem clear enough
- vtitle = getattr(self, 'title', None) or u''
- if vtitle:
- vtitle = self.req._(vtitle)
- rset = self.rset
- if rset and rset.rowcount:
- if rset.rowcount == 1:
- try:
- entity = self.complete_entity(0)
- # use long_title to get context information if any
- clabel = entity.dc_long_title()
- except NotAnEntity:
- clabel = display_name(self.req, rset.description[0][0])
- clabel = u'%s (%s)' % (clabel, vtitle)
- else :
- etypes = rset.column_types(0)
- if len(etypes) == 1:
- etype = iter(etypes).next()
- clabel = display_name(self.req, etype, 'plural')
- else :
- clabel = u'#[*] (%s)' % vtitle
- else:
- clabel = vtitle
- return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title'))
-
- def output_url_builder( self, name, url, args ):
- self.w(u'\n')
-
- def create_url(self, etype, **kwargs):
- """ return the url of the entity creation form for a given entity type"""
- return self.req.build_url('add/%s'%etype, **kwargs)
-
-
-# concrete views base classes #################################################
-
-class EntityView(View):
- """base class for views applying on an entity (i.e. uniform result set)
- """
- __registerer__ = accepts_registerer
- __selectors__ = (accept,)
- accepts = ('Any',)
- category = 'entityview'
-
- def field(self, label, value, row=True, show_label=True, w=None, tr=True):
- """ read-only field """
- if w is None:
- w = self.w
- if row:
- w(u'
')
- if show_label:
- if tr:
- label = display_name(self.req, label)
- w(u'%s' % label)
- w(u'
%s
' % value)
- if row:
- w(u'
')
-
-
-class StartupView(View):
- """base class for views which doesn't need a particular result set
- to be displayed (so they can always be displayed !)
- """
- __registerer__ = priority_registerer
- __selectors__ = (match_user_group, none_rset)
- require_groups = ()
- category = 'startupview'
-
- def url(self):
- """return the url associated with this view. We can omit rql here"""
- return self.build_url('view', vid=self.id)
-
- def html_headers(self):
- """return a list of html headers (eg something to be inserted between
- and of the returned page
-
- by default startup views are indexed
- """
- return []
-
-
-class EntityStartupView(EntityView):
- """base class for entity views which may also be applied to None
- result set (usually a default rql is provided by the view class)
- """
- __registerer__ = accepts_registerer
- __selectors__ = (chainfirst(none_rset, accept),)
-
- default_rql = None
-
- def __init__(self, req, rset):
- super(EntityStartupView, self).__init__(req, rset)
- if rset is None:
- # this instance is not in the "entityview" category
- self.category = 'startupview'
-
- def startup_rql(self):
- """return some rql to be executedif the result set is None"""
- return self.default_rql
-
- def call(self, **kwargs):
- """override call to execute rql returned by the .startup_rql
- method if necessary
- """
- if self.rset is None:
- self.rset = self.req.execute(self.startup_rql())
- rset = self.rset
- for i in xrange(len(rset)):
- self.wview(self.id, rset, row=i, **kwargs)
-
- def url(self):
- """return the url associated with this view. We can omit rql if we
- are on a result set on which we do not apply.
- """
- if not self.__select__(self.req, self.rset):
- return self.build_url(vid=self.id)
- return super(EntityStartupView, self).url()
-
-
-class AnyRsetView(View):
- """base class for views applying on any non empty result sets"""
- __registerer__ = priority_registerer
- __selectors__ = (nonempty_rset,)
-
- category = 'anyrsetview'
-
- def columns_labels(self, tr=True):
- if tr:
- translate = display_name
- else:
- translate = lambda req, val: val
- rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support
- labels = []
- for colindex, attr in enumerate(rqlstdescr):
- # compute column header
- if colindex == 0 or attr == 'Any': # find a better label
- label = ','.join(translate(self.req, et)
- for et in self.rset.column_types(colindex))
- else:
- label = translate(self.req, attr)
- labels.append(label)
- return labels
-
-
-class EmptyRsetView(View):
- """base class for views applying on any empty result sets"""
- __registerer__ = priority_registerer
- __selectors__ = (empty_rset,)
-
-
-# concrete template base classes ##############################################
-
-class Template(View):
- """a template is almost like a view, except that by default a template
- is only used globally (i.e. no result set adaptation)
- """
- __registry__ = 'templates'
- __registerer__ = priority_registerer
- __selectors__ = (match_user_group,)
-
- require_groups = ()
-
- def template(self, oid, **kwargs):
- """shortcut to self.registry.render method on the templates registry"""
- w = kwargs.pop('w', self.w)
- self.vreg.render('templates', oid, self.req, w=w, **kwargs)
-
-
-class MainTemplate(Template):
- """main template are primary access point to render a full HTML page.
- There is usually at least a regular main template and a simple fallback
- one to display error if the first one failed
- """
-
- base_doctype = STRICT_DOCTYPE
-
- @property
- def doctype(self):
- if self.req.xhtml_browser():
- return self.base_doctype % CW_XHTML_EXTENSIONS
- return self.base_doctype % ''
-
- def set_stream(self, w=None, templatable=True):
- if templatable and self.w is not None:
- return
-
- if w is None:
- if self.binary:
- self._stream = stream = StringIO()
- elif not templatable:
- # not templatable means we're using a non-html view, we don't
- # want the HTMLStream stuff to interfere during data generation
- self._stream = stream = UStringIO()
- else:
- self._stream = stream = HTMLStream(self.req)
- w = stream.write
- else:
- stream = None
- self.w = w
- return stream
-
- def write_doctype(self, xmldecl=True):
- assert isinstance(self._stream, HTMLStream)
- self._stream.doctype = self.doctype
- if not xmldecl:
- self._stream.xmldecl = u''
-
-# viewable components base classes ############################################
-
-class VComponent(ComponentMixIn, View):
- """base class for displayable components"""
- property_defs = {
- 'visible': dict(type='Boolean', default=True,
- help=_('display the component or not')),}
-
-class SingletonVComponent(VComponent):
- """base class for displayable unique components"""
- __registerer__ = priority_registerer
+from warnings import warn
+warn('moved to cubicweb.view', DeprecationWarning, stacklevel=2)
+from cubicweb.view import *
diff -r 93447d75c4b9 -r 6a25c58a1c23 cwconfig.py
--- a/cwconfig.py Fri Feb 27 09:59:53 2009 +0100
+++ b/cwconfig.py Mon Mar 02 21:03:54 2009 +0100
@@ -59,7 +59,7 @@
# XXX generate this according to the configuration (repository/all-in-one/web)
VREGOPTIONS = []
for registry in ('etypes', 'hooks', 'controllers', 'actions', 'components',
- 'views', 'templates', 'boxes', 'contentnavigation', 'urlrewriting',
+ 'views', 'boxes', 'contentnavigation', 'urlrewriting',
'facets'):
VREGOPTIONS.append(('disable-%s'%registry,
{'type' : 'csv', 'default': (),
diff -r 93447d75c4b9 -r 6a25c58a1c23 cwvreg.py
--- a/cwvreg.py Fri Feb 27 09:59:53 2009 +0100
+++ b/cwvreg.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""extend the generic VRegistry with some cubicweb specific stuff
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -9,6 +9,7 @@
from warnings import warn
from logilab.common.decorators import cached, clear_cache
+from logilab.common.interface import extend
from rql import RQLHelper
@@ -17,11 +18,25 @@
_ = unicode
-class DummyCursorError(Exception): pass
-class RaiseCursor:
- @classmethod
- def execute(cls, rql, args=None, eid_key=None):
- raise DummyCursorError()
+def use_interfaces(obj):
+ 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 vobject classes with no accepts_interfaces
+ return ()
+
+def expand_parent_classes(iface):
+ res = [iface]
+ for parent in iface.__bases__:
+ res += expand_parent_classes(parent)
+ return res
class CubicWebRegistry(VRegistry):
@@ -47,8 +62,10 @@
def reset(self):
self._registries = {}
self._lastmodifs = {}
- # two special registries, propertydefs which care all the property definitions, and
- # propertyvals which contains values for those properties
+ self._needs_iface = {}
+ # two special registries, propertydefs which care all the property
+ # definitions, and propertyvals which contains values for those
+ # properties
self._registries['propertydefs'] = {}
self._registries['propertyvalues'] = self.eprop_values = {}
for key, propdef in self.config.eproperty_definitions():
@@ -72,43 +89,51 @@
for objects in regcontent.values():
for obj in objects:
obj.schema = schema
+
+ def register_if_interface_found(self, obj, ifaces, **kwargs):
+ """register an object but remove it if no entity class implements one of
+ the given interfaces
+ """
+ self.register(obj, **kwargs)
+ if not isinstance(ifaces, (tuple, list)):
+ self._needs_iface[obj] = (ifaces,)
+ else:
+ self._needs_iface[obj] = ifaces
+
+ def register(self, obj, **kwargs):
+ if kwargs.get('registryname', obj.__registry__) == 'etypes':
+ kwargs['clear'] = True
+ super(CubicWebRegistry, self).register(obj, **kwargs)
+ # XXX bw compat
+ ifaces = use_interfaces(obj)
+ if ifaces:
+ self._needs_iface[obj] = ifaces
def register_objects(self, path, force_reload=None):
- """overriden to handle type class cache issue"""
- if super(CubicWebRegistry, self).register_objects(path, force_reload):
+ """overriden to remove objects requiring a missing interface"""
+ if super(CubicWebRegistry, self).register_objects(path, force_reload):
# clear etype cache if you don't want to run into deep weirdness
clear_cache(self, 'etype_class')
+ # we may want to keep interface dependent objects (e.g.for i18n
+ # catalog generation)
+ if not self.config.cleanup_interface_sobjects:
+ return
# remove vobjects that don't support any available interface
interfaces = set()
for classes in self.get('etypes', {}).values():
for cls in classes:
- interfaces.update(cls.__implements__)
- if not self.config.cleanup_interface_sobjects:
- return
- for registry, regcontent in self._registries.items():
- if registry in ('propertydefs', 'propertyvalues', 'etypes'):
- continue
- for oid, objects in regcontent.items():
- for obj in reversed(objects[:]):
- if not obj in objects:
- continue # obj has been kicked by a previous one
- accepted = set(getattr(obj, 'accepts_interfaces', ()))
- if accepted:
- for accepted_iface in accepted:
- for found_iface in interfaces:
- if issubclass(found_iface, accepted_iface):
- # consider priority if necessary
- if hasattr(obj.__registerer__, 'remove_all_equivalents'):
- registerer = obj.__registerer__(self, obj)
- registerer.remove_all_equivalents(objects)
- break
- else:
- self.debug('kicking vobject %s (unsupported interface)', obj)
- objects.remove(obj)
- # if objects is empty, remove oid from registry
- if not objects:
- del regcontent[oid]
-
+ for iface in cls.__implements__:
+ interfaces.update(expand_parent_classes(iface))
+ interfaces.update(expand_parent_classes(cls))
+ for obj, ifaces in self._needs_iface.items():
+ ifaces = frozenset(isinstance(iface, basestring) and self.etype_class(iface) or iface
+ for iface in ifaces)
+ if not ifaces & interfaces:
+ self.debug('kicking vobject %s (unsupported interface)', obj)
+ self.unregister(obj)
+
+
+
def eid_rset(self, cursor, eid, etype=None):
"""return a result set for the given eid without doing actual query
(we have the eid, we can suppose it exists and user has access to the
@@ -129,6 +154,8 @@
default to a dump of the class registered for 'Any'
"""
etype = str(etype)
+ if etype == 'Any':
+ return self.select(self.registry_objects('etypes', 'Any'), 'Any')
eschema = self.schema.eschema(etype)
baseschemas = [eschema] + eschema.ancestors()
# browse ancestors from most specific to most generic and
@@ -136,12 +163,21 @@
for baseschema in baseschemas:
btype = str(baseschema)
try:
- return self.select(self.registry_objects('etypes', btype), etype)
+ cls = self.select(self.registry_objects('etypes', btype), etype)
+ break
except ObjectNotFound:
pass
- # no entity class for any of the ancestors, fallback to the default one
- return self.select(self.registry_objects('etypes', 'Any'), etype)
-
+ else:
+ # no entity class for any of the ancestors, fallback to the default
+ # one
+ cls = self.select(self.registry_objects('etypes', 'Any'), etype)
+ # add class itself to the list of implemented interfaces, as well as the
+ # Any entity class so we can select according to class using the
+ # `implements` selector
+ extend(cls, cls)
+ extend(cls, self.etype_class('Any'))
+ return cls
+
def render(self, registry, oid, req, **context):
"""select an object in a given registry and render it
@@ -157,12 +193,12 @@
selected = self.select(objclss, req, rset, **context)
return selected.dispatch(**context)
- def main_template(self, req, oid='main', **context):
+ def main_template(self, req, oid='main-template', **context):
"""display query by calling the given template (default to main),
and returning the output as a string instead of requiring the [w]rite
method as argument
"""
- res = self.render('templates', oid, req, **context)
+ res = self.render('views', oid, req, **context)
if isinstance(res, unicode):
return res.encode(req.encoding)
assert isinstance(res, str)
diff -r 93447d75c4b9 -r 6a25c58a1c23 debian/control
--- a/debian/control Fri Feb 27 09:59:53 2009 +0100
+++ b/debian/control Mon Mar 02 21:03:54 2009 +0100
@@ -9,7 +9,6 @@
Homepage: http://www.cubicweb.org
XS-Python-Version: >= 2.4, << 2.6
-
Package: cubicweb
Architecture: all
XB-Python-Version: ${python:Versions}
diff -r 93447d75c4b9 -r 6a25c58a1c23 debian/cubicweb-web.postinst
--- a/debian/cubicweb-web.postinst Fri Feb 27 09:59:53 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-#! /bin/sh -e
-
-ln -sf /usr/share/fckeditor/fckeditor.js /usr/share/cubicweb/cubes/shared/data
-
-#DEBHELPER#
-
-exit 0
diff -r 93447d75c4b9 -r 6a25c58a1c23 devtools/htmlparser.py
--- a/devtools/htmlparser.py Fri Feb 27 09:59:53 2009 +0100
+++ b/devtools/htmlparser.py Mon Mar 02 21:03:54 2009 +0100
@@ -6,7 +6,7 @@
from lxml import etree
from lxml.builder import E
-from cubicweb.common.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE, CW_XHTML_EXTENSIONS
+from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE, CW_XHTML_EXTENSIONS
STRICT_DOCTYPE = str(STRICT_DOCTYPE % CW_XHTML_EXTENSIONS).strip()
TRANSITIONAL_DOCTYPE = str(TRANSITIONAL_DOCTYPE % CW_XHTML_EXTENSIONS).strip()
diff -r 93447d75c4b9 -r 6a25c58a1c23 devtools/test/runtests.py
--- a/devtools/test/runtests.py Fri Feb 27 09:59:53 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-from logilab.common.testlib import main
-
-if __name__ == '__main__':
- import sys, os
- main(os.path.dirname(sys.argv[0]) or '.')
diff -r 93447d75c4b9 -r 6a25c58a1c23 devtools/testlib.py
--- a/devtools/testlib.py Fri Feb 27 09:59:53 2009 +0100
+++ b/devtools/testlib.py Mon Mar 02 21:03:54 2009 +0100
@@ -158,7 +158,7 @@
self.commit()
@nocoverage
- def _check_html(self, output, view, template='main'):
+ def _check_html(self, output, view, template='main-template'):
"""raises an exception if the HTML is invalid"""
try:
validatorclass = self.vid_validators[view.id]
@@ -175,7 +175,7 @@
return validator.parse_string(output.strip())
- def view(self, vid, rset, req=None, template='main', **kwargs):
+ def view(self, vid, rset, req=None, template='main-template', **kwargs):
"""This method tests the view `vid` on `rset` using `template`
If no error occured while rendering the view, the HTML is analyzed
@@ -197,24 +197,16 @@
self.set_description("testing %s, mod=%s (%s)" % (vid, view.__module__, rset.printable_rql()))
else:
self.set_description("testing %s, mod=%s (no rset)" % (vid, view.__module__))
- viewfunc = lambda **k: self.vreg.main_template(req, template, **kwargs)
if template is None: # raw view testing, no template
viewfunc = view.dispatch
- elif template == 'main':
- _select_view_and_rset = TheMainTemplate._select_view_and_rset
- # patch TheMainTemplate.process_rql to avoid recomputing resultset
- def __select_view_and_rset(self, view=view, rset=rset):
- self.rset = rset
- return view, rset
- TheMainTemplate._select_view_and_rset = __select_view_and_rset
- try:
- return self._test_view(viewfunc, view, template, **kwargs)
- finally:
- if template == 'main':
- TheMainTemplate._select_view_and_rset = _select_view_and_rset
+ else:
+ templateview = self.vreg.select_view(template, req, rset, view=view, **kwargs)
+ kwargs['view'] = view
+ viewfunc = lambda **k: self.vreg.main_template(req, template, **kwargs)
+ return self._test_view(viewfunc, view, template, kwargs)
- def _test_view(self, viewfunc, view, template='main', **kwargs):
+ def _test_view(self, viewfunc, view, template='main-template', kwargs={}):
"""this method does the actual call to the view
If no error occured while rendering the view, the HTML is analyzed
@@ -332,7 +324,7 @@
backup_rset = rset._prepare_copy(rset.rows, rset.description)
yield InnerTest(self._testname(rset, view.id, 'view'),
self.view, view.id, rset,
- rset.req.reset_headers(), 'main')
+ rset.req.reset_headers(), 'main-template')
# We have to do this because some views modify the
# resultset's syntax tree
rset = backup_rset
diff -r 93447d75c4b9 -r 6a25c58a1c23 entities/__init__.py
--- a/entities/__init__.py Fri Feb 27 09:59:53 2009 +0100
+++ b/entities/__init__.py Mon Mar 02 21:03:54 2009 +0100
@@ -12,8 +12,8 @@
from logilab.common.decorators import cached
from cubicweb import Unauthorized, typed_eid
-from cubicweb.common.utils import dump_class
-from cubicweb.common.entity import Entity
+from cubicweb.entity import Entity
+from cubicweb.utils import dump_class
from cubicweb.schema import FormatConstraint
from cubicweb.interfaces import IBreadCrumbs, IFeed
@@ -96,20 +96,6 @@
if not hasattr(cls, 'default_%s' % formatattr):
setattr(cls, 'default_%s' % formatattr, cls._default_format)
eschema.format_fields[formatattr] = attr
-
- def _default_format(self):
- return self.req.property_value('ui.default-text-format')
-
- def use_fckeditor(self, attr):
- """return True if fckeditor should be used to edit entity's attribute named
- `attr`, according to user preferences
- """
- req = self.req
- if req.property_value('ui.fckeditor') and self.has_format(attr):
- if self.has_eid() or '%s_format' % attr in self:
- return self.format(attr) == 'text/html'
- return req.property_value('ui.default-text-format') == 'text/html'
- return False
# meta data api ###########################################################
@@ -236,12 +222,6 @@
return self.printable_value(rtype, format='text/plain').lower()
return value
- def after_deletion_path(self):
- """return (path, parameters) which should be used as redirect
- information when this entity is being deleted
- """
- return str(self.e_schema).lower(), {}
-
def add_related_schemas(self):
"""this is actually used ui method to generate 'addrelated' actions from
the schema.
@@ -343,6 +323,65 @@
continue
result.append( (rschema.display_name(self.req, target), rschema, target) )
return sorted(result)
+
+ def linked_to(self, rtype, target, remove=True):
+ """if entity should be linked to another using __linkto form param for
+ the given relation/target, return eids of related entities
+
+ This method is consuming matching link-to information from form params
+ if `remove` is True (by default).
+ """
+ try:
+ return self.__linkto[(rtype, target)]
+ except AttributeError:
+ self.__linkto = {}
+ except KeyError:
+ pass
+ linktos = list(self.req.list_form_param('__linkto'))
+ linkedto = []
+ for linkto in linktos[:]:
+ ltrtype, eid, lttarget = linkto.split(':')
+ if rtype == ltrtype and target == lttarget:
+ # delete __linkto from form param to avoid it being added as
+ # hidden input
+ if remove:
+ linktos.remove(linkto)
+ self.req.form['__linkto'] = linktos
+ linkedto.append(typed_eid(eid))
+ self.__linkto[(rtype, target)] = 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
+ """
+ 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 deprecates, may be killed once old widgets system is gone ###########
+
+ def _default_format(self):
+ return self.req.property_value('ui.default-text-format')
def attribute_values(self, attrname):
if self.has_eid() or attrname in self:
@@ -372,51 +411,16 @@
values = (values,)
return values
- def linked_to(self, rtype, target, remove=True):
- """if entity should be linked to another using __linkto form param for
- the given relation/target, return eids of related entities
-
- This method is consuming matching link-to information from form params
- if `remove` is True (by default).
+ def use_fckeditor(self, attr):
+ """return True if fckeditor should be used to edit entity's attribute named
+ `attr`, according to user preferences
"""
- try:
- return self.__linkto[(rtype, target)]
- except AttributeError:
- self.__linkto = {}
- except KeyError:
- pass
- linktos = list(self.req.list_form_param('__linkto'))
- linkedto = []
- for linkto in linktos[:]:
- ltrtype, eid, lttarget = linkto.split(':')
- if rtype == ltrtype and target == lttarget:
- # delete __linkto from form param to avoid it being added as
- # hidden input
- if remove:
- linktos.remove(linkto)
- self.req.form['__linkto'] = linktos
- linkedto.append(typed_eid(eid))
- self.__linkto[(rtype, target)] = linkedto
- return linkedto
-
- 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 ()
+ req = self.req
+ if req.property_value('ui.fckeditor') and self.has_format(attr):
+ if self.has_eid() or '%s_format' % attr in self:
+ return self.format(attr) == 'text/html'
+ return req.property_value('ui.default-text-format') == 'text/html'
+ return False
# 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
diff -r 93447d75c4b9 -r 6a25c58a1c23 entities/lib.py
--- a/entities/lib.py Fri Feb 27 09:59:53 2009 +0100
+++ b/entities/lib.py Mon Mar 02 21:03:54 2009 +0100
@@ -11,7 +11,7 @@
from logilab.common.decorators import cached
-from cubicweb.common.entity import _marker
+from cubicweb.entity import _marker
from cubicweb.entities import AnyEntity, fetch_config
def mangle_email(address):
diff -r 93447d75c4b9 -r 6a25c58a1c23 entity.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entity.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,1054 @@
+"""Base class for entity objects manipulated in clients
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common import interface
+from logilab.common.compat import all
+from logilab.common.decorators import cached
+from logilab.mtconverter import TransformData, TransformError, html_escape
+
+from rql.utils import rqlvar_maker
+
+from cubicweb import Unauthorized
+from cubicweb.rset import ResultSet
+from cubicweb.selectors import yes
+from cubicweb.appobject import AppRsetObject
+from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype
+
+try:
+ from cubicweb.common.uilib import printable_value, soup2xhtml
+ from cubicweb.common.mixins import MI_REL_TRIGGERS
+ from cubicweb.common.mttransforms import ENGINE
+except ImportError:
+ # missing -common
+ MI_REL_TRIGGERS = {}
+
+_marker = object()
+
+def greater_card(rschema, subjtypes, objtypes, index):
+ for subjtype in subjtypes:
+ for objtype in objtypes:
+ card = rschema.rproperty(subjtype, objtype, 'cardinality')[index]
+ if card in '+*':
+ return card
+ return '1'
+
+
+class RelationTags(object):
+
+ MODE_TAGS = frozenset(('link', 'create'))
+ CATEGORY_TAGS = frozenset(('primary', 'secondary', 'generic', 'generated',
+ 'inlineview'))
+
+ def __init__(self, eclass, tagdefs):
+ # XXX if a rtag is redefined in a subclass,
+ # the rtag of the base class overwrite the rtag of the subclass
+ self.eclass = eclass
+ self._tagdefs = {}
+ for relation, tags in tagdefs.iteritems():
+ # tags must become a set
+ if isinstance(tags, basestring):
+ tags = set((tags,))
+ elif not isinstance(tags, set):
+ tags = set(tags)
+ # relation must become a 3-uple (rtype, targettype, role)
+ if isinstance(relation, basestring):
+ self._tagdefs[(relation, '*', 'subject')] = tags
+ self._tagdefs[(relation, '*', 'object')] = tags
+ elif len(relation) == 1: # useful ?
+ self._tagdefs[(relation[0], '*', 'subject')] = tags
+ self._tagdefs[(relation[0], '*', 'object')] = tags
+ elif len(relation) == 2:
+ rtype, ttype = relation
+ ttype = bw_normalize_etype(ttype) # XXX bw compat
+ self._tagdefs[rtype, ttype, 'subject'] = tags
+ self._tagdefs[rtype, ttype, 'object'] = tags
+ elif len(relation) == 3:
+ relation = list(relation) # XXX bw compat
+ relation[1] = bw_normalize_etype(relation[1])
+ self._tagdefs[tuple(relation)] = tags
+ else:
+ raise ValueError('bad rtag definition (%r)' % (relation,))
+
+
+ def __initialize__(self):
+ # eclass.[*]schema are only set when registering
+ self.schema = self.eclass.schema
+ eschema = self.eschema = self.eclass.e_schema
+ rtags = self._tagdefs
+ # expand wildcards in rtags and add automatic tags
+ for rschema, tschemas, role in sorted(eschema.relation_definitions(True)):
+ rtype = rschema.type
+ star_tags = rtags.pop((rtype, '*', role), set())
+ for tschema in tschemas:
+ tags = rtags.setdefault((rtype, tschema.type, role), set(star_tags))
+ if role == 'subject':
+ X, Y = eschema, tschema
+ card = rschema.rproperty(X, Y, 'cardinality')[0]
+ composed = rschema.rproperty(X, Y, 'composite') == 'object'
+ else:
+ X, Y = tschema, eschema
+ card = rschema.rproperty(X, Y, 'cardinality')[1]
+ composed = rschema.rproperty(X, Y, 'composite') == 'subject'
+ # set default category tags if needed
+ if not tags & self.CATEGORY_TAGS:
+ if card in '1+':
+ if not rschema.is_final() and composed:
+ category = 'generated'
+ elif rschema.is_final() and (
+ rschema.type.endswith('_format')
+ or rschema.type.endswith('_encoding')):
+ category = 'generated'
+ else:
+ category = 'primary'
+ elif rschema.is_final():
+ if (rschema.type.endswith('_format')
+ or rschema.type.endswith('_encoding')):
+ category = 'generated'
+ else:
+ category = 'secondary'
+ else:
+ category = 'generic'
+ tags.add(category)
+ if not tags & self.MODE_TAGS:
+ if card in '?1':
+ # by default, suppose link mode if cardinality doesn't allow
+ # more than one relation
+ mode = 'link'
+ elif rschema.rproperty(X, Y, 'composite') == role:
+ # if self is composed of the target type, create mode
+ mode = 'create'
+ else:
+ # link mode by default
+ mode = 'link'
+ tags.add(mode)
+
+ def _default_target(self, rschema, role='subject'):
+ eschema = self.eschema
+ if role == 'subject':
+ return eschema.subject_relation(rschema).objects(eschema)[0]
+ else:
+ return eschema.object_relation(rschema).subjects(eschema)[0]
+
+ # dict compat
+ def __getitem__(self, key):
+ if isinstance(key, basestring):
+ key = (key,)
+ return self.get_tags(*key)
+
+ __contains__ = __getitem__
+
+ def get_tags(self, rtype, targettype=None, role='subject'):
+ rschema = self.schema.rschema(rtype)
+ if targettype is None:
+ tschema = self._default_target(rschema, role)
+ else:
+ tschema = self.schema.eschema(targettype)
+ return self._tagdefs[(rtype, tschema.type, role)]
+
+ __call__ = get_tags
+
+ def get_mode(self, rtype, targettype=None, role='subject'):
+ # XXX: should we make an assertion on rtype not being final ?
+ # assert not rschema.is_final()
+ tags = self.get_tags(rtype, targettype, role)
+ # do not change the intersection order !
+ modes = tags & self.MODE_TAGS
+ assert len(modes) == 1
+ return modes.pop()
+
+ def get_category(self, rtype, targettype=None, role='subject'):
+ tags = self.get_tags(rtype, targettype, role)
+ categories = tags & self.CATEGORY_TAGS
+ assert len(categories) == 1
+ return categories.pop()
+
+ def is_inlined(self, rtype, targettype=None, role='subject'):
+ # return set(('primary', 'secondary')) & self.get_tags(rtype, targettype)
+ return 'inlineview' in self.get_tags(rtype, targettype, role)
+
+
+class metaentity(type):
+ """this metaclass sets the relation tags on the entity class
+ and deals with the `widgets` attribute
+ """
+ def __new__(mcs, name, bases, classdict):
+ # collect baseclass' rtags
+ tagdefs = {}
+ widgets = {}
+ for base in bases:
+ tagdefs.update(getattr(base, '__rtags__', {}))
+ widgets.update(getattr(base, 'widgets', {}))
+ # update with the class' own rtgas
+ tagdefs.update(classdict.get('__rtags__', {}))
+ widgets.update(classdict.get('widgets', {}))
+ # XXX decide whether or not it's a good idea to replace __rtags__
+ # good point: transparent support for inheritance levels >= 2
+ # bad point: we loose the information of which tags are specific
+ # to this entity class
+ classdict['__rtags__'] = tagdefs
+ classdict['widgets'] = widgets
+ eclass = super(metaentity, mcs).__new__(mcs, name, bases, classdict)
+ # adds the "rtags" attribute
+ eclass.rtags = RelationTags(eclass, tagdefs)
+ return eclass
+
+
+class Entity(AppRsetObject, dict):
+ """an entity instance has e_schema automagically set on
+ the class and instances has access to their issuing cursor.
+
+ A property is set for each attribute and relation on each entity's type
+ class. Becare that among attributes, 'eid' is *NEITHER* stored in the
+ dict containment (which acts as a cache for other attributes dynamically
+ fetched)
+
+ :type e_schema: `cubicweb.schema.EntitySchema`
+ :ivar e_schema: the entity's schema
+
+ :type rest_var: str
+ :cvar rest_var: indicates which attribute should be used to build REST urls
+ If None is specified, the first non-meta attribute will
+ be used
+
+ :type skip_copy_for: list
+ :cvar skip_copy_for: a list of relations that should be skipped when copying
+ this kind of entity. Note that some relations such
+ as composite relations or relations that have '?1' as object
+ cardinality
+ """
+ __metaclass__ = metaentity
+ __registry__ = 'etypes'
+ __select__ = yes()
+ widgets = {}
+ id = None
+ e_schema = None
+ eid = None
+ rest_attr = None
+ skip_copy_for = ()
+
+ @classmethod
+ def registered(cls, registry):
+ """build class using descriptor at registration time"""
+ assert cls.id is not None
+ super(Entity, cls).registered(registry)
+ if cls.id != 'Any':
+ cls.__initialize__()
+ return cls
+
+ MODE_TAGS = set(('link', 'create'))
+ CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
+ @classmethod
+ def __initialize__(cls):
+ """initialize a specific entity class by adding descriptors to access
+ entity type's attributes and relations
+ """
+ etype = cls.id
+ assert etype != 'Any', etype
+ cls.e_schema = eschema = cls.schema.eschema(etype)
+ for rschema, _ in eschema.attribute_definitions():
+ if rschema.type == 'eid':
+ continue
+ setattr(cls, rschema.type, Attribute(rschema.type))
+ mixins = []
+ for rschema, _, x in eschema.relation_definitions():
+ if (rschema, x) in MI_REL_TRIGGERS:
+ mixin = MI_REL_TRIGGERS[(rschema, x)]
+ if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ?
+ mixins.append(mixin)
+ for iface in getattr(mixin, '__implements__', ()):
+ if not interface.implements(cls, iface):
+ interface.extend(cls, iface)
+ if x == 'subject':
+ setattr(cls, rschema.type, SubjectRelation(rschema))
+ else:
+ attr = 'reverse_%s' % rschema.type
+ setattr(cls, attr, ObjectRelation(rschema))
+ if mixins:
+ cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object])
+ cls.debug('plugged %s mixins on %s', mixins, etype)
+ cls.rtags.__initialize__()
+
+ @classmethod
+ def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
+ settype=True, ordermethod='fetch_order'):
+ """return a rql to fetch all entities of the class type"""
+ restrictions = restriction or []
+ if settype:
+ restrictions.append('%s is %s' % (mainvar, cls.id))
+ if fetchattrs is None:
+ fetchattrs = cls.fetch_attrs
+ selection = [mainvar]
+ orderby = []
+ # start from 26 to avoid possible conflicts with X
+ varmaker = rqlvar_maker(index=26)
+ cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
+ orderby, restrictions, user, ordermethod)
+ rql = 'Any %s' % ','.join(selection)
+ if orderby:
+ rql += ' ORDERBY %s' % ','.join(orderby)
+ rql += ' WHERE %s' % ', '.join(restrictions)
+ return rql
+
+ @classmethod
+ def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
+ selection, orderby, restrictions, user,
+ ordermethod='fetch_order', visited=None):
+ eschema = cls.e_schema
+ if visited is None:
+ visited = set((eschema.type,))
+ elif eschema.type in visited:
+ # avoid infinite recursion
+ return
+ else:
+ visited.add(eschema.type)
+ _fetchattrs = []
+ for attr in fetchattrs:
+ try:
+ rschema = eschema.subject_relation(attr)
+ except KeyError:
+ cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
+ attr, cls.id)
+ continue
+ if not user.matching_groups(rschema.get_groups('read')):
+ continue
+ var = varmaker.next()
+ selection.append(var)
+ restriction = '%s %s %s' % (mainvar, attr, var)
+ restrictions.append(restriction)
+ if not rschema.is_final():
+ # XXX this does not handle several destination types
+ desttype = rschema.objects(eschema.type)[0]
+ card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
+ if card not in '?1':
+ selection.pop()
+ restrictions.pop()
+ continue
+ if card == '?':
+ restrictions[-1] += '?' # left outer join if not mandatory
+ destcls = cls.vreg.etype_class(desttype)
+ destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
+ selection, orderby, restrictions,
+ user, ordermethod, visited=visited)
+ orderterm = getattr(cls, ordermethod)(attr, var)
+ if orderterm:
+ orderby.append(orderterm)
+ return selection, orderby, restrictions
+
+ def __init__(self, req, rset, row=None, col=0):
+ AppRsetObject.__init__(self, req, rset)
+ dict.__init__(self)
+ self.row, self.col = row, col
+ self._related_cache = {}
+ if rset is not None:
+ self.eid = rset[row][col]
+ else:
+ self.eid = None
+ self._is_saved = True
+
+ def __repr__(self):
+ return '' % (
+ self.e_schema, self.eid, self.keys(), id(self))
+
+ def __nonzero__(self):
+ return True
+
+ def __hash__(self):
+ return id(self)
+
+ def pre_add_hook(self):
+ """hook called by the repository before doing anything to add the entity
+ (before_add entity hooks have not been called yet). This give the
+ occasion to do weird stuff such as autocast (File -> Image for instance).
+
+ This method must return the actual entity to be added.
+ """
+ return self
+
+ def set_eid(self, eid):
+ self.eid = self['eid'] = eid
+
+ def has_eid(self):
+ """return True if the entity has an attributed eid (False
+ meaning that the entity has to be created
+ """
+ try:
+ int(self.eid)
+ return True
+ except (ValueError, TypeError):
+ return False
+
+ def is_saved(self):
+ """during entity creation, there is some time during which the entity
+ has an eid attributed though it's not saved (eg during before_add_entity
+ hooks). You can use this method to ensure the entity has an eid *and* is
+ saved in its source.
+ """
+ return self.has_eid() and self._is_saved
+
+ @cached
+ def metainformation(self):
+ res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid)))
+ res['source'] = self.req.source_defs()[res['source']]
+ return res
+
+ def clear_local_perm_cache(self, action):
+ for rqlexpr in self.e_schema.get_rqlexprs(action):
+ self.req.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
+
+ def check_perm(self, action):
+ self.e_schema.check_perm(self.req, action, self.eid)
+
+ def has_perm(self, action):
+ return self.e_schema.has_perm(self.req, action, self.eid)
+
+ def view(self, vid, __registry='views', **kwargs):
+ """shortcut to apply a view on this entity"""
+ return self.vreg.render(__registry, vid, self.req, rset=self.rset,
+ row=self.row, col=self.col, **kwargs)
+
+ def absolute_url(self, method=None, **kwargs):
+ """return an absolute url to view this entity"""
+ # in linksearch mode, we don't want external urls else selecting
+ # the object for use in the relation is tricky
+ # XXX search_state is web specific
+ if getattr(self.req, 'search_state', ('normal',))[0] == 'normal':
+ kwargs['base_url'] = self.metainformation()['source'].get('base-url')
+ if method is None or method == 'view':
+ kwargs['_restpath'] = self.rest_path()
+ else:
+ kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
+ return self.build_url(method, **kwargs)
+
+ def rest_path(self):
+ """returns a REST-like (relative) path for this entity"""
+ mainattr, needcheck = self._rest_attr_info()
+ etype = str(self.e_schema)
+ if mainattr == 'eid':
+ value = self.eid
+ else:
+ value = getattr(self, mainattr)
+ if value is None:
+ return '%s/eid/%s' % (etype.lower(), self.eid)
+ if needcheck:
+ # make sure url is not ambiguous
+ rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (etype, mainattr)
+ if value is not None:
+ nbresults = self.req.execute(rql, {'value' : value})[0][0]
+ # may an assertion that nbresults is not 0 would be a good idea
+ if nbresults != 1: # no ambiguity
+ return '%s/eid/%s' % (etype.lower(), self.eid)
+ return '%s/%s' % (etype.lower(), self.req.url_quote(value))
+
+ @classmethod
+ def _rest_attr_info(cls):
+ mainattr, needcheck = 'eid', True
+ if cls.rest_attr:
+ mainattr = cls.rest_attr
+ needcheck = not cls.e_schema.has_unique_values(mainattr)
+ else:
+ for rschema in cls.e_schema.subject_relations():
+ if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema):
+ mainattr = str(rschema)
+ needcheck = False
+ break
+ if mainattr == 'eid':
+ needcheck = False
+ return mainattr, needcheck
+
+ @cached
+ def formatted_attrs(self):
+ """returns the list of attributes which have some format information
+ (i.e. rich text strings)
+ """
+ attrs = []
+ for rschema, attrschema in self.e_schema.attribute_definitions():
+ if attrschema.type == 'String' and self.has_format(rschema):
+ attrs.append(rschema.type)
+ return attrs
+
+ def format(self, attr):
+ """return the mime type format for an attribute (if specified)"""
+ return getattr(self, '%s_format' % attr, None)
+
+ def text_encoding(self, attr):
+ """return the text encoding for an attribute, default to site encoding
+ """
+ encoding = getattr(self, '%s_encoding' % attr, None)
+ return encoding or self.vreg.property_value('ui.encoding')
+
+ def has_format(self, attr):
+ """return true if this entity's schema has a format field for the given
+ attribute
+ """
+ return self.e_schema.has_subject_relation('%s_format' % attr)
+
+ def has_text_encoding(self, attr):
+ """return true if this entity's schema has ab encoding field for the
+ given attribute
+ """
+ return self.e_schema.has_subject_relation('%s_encoding' % attr)
+
+ def printable_value(self, attr, value=_marker, attrtype=None,
+ format='text/html', displaytime=True):
+ """return a displayable value (i.e. unicode string) which may contains
+ html tags
+ """
+ attr = str(attr)
+ if value is _marker:
+ value = getattr(self, attr)
+ if isinstance(value, basestring):
+ value = value.strip()
+ if value is None or value == '': # don't use "not", 0 is an acceptable value
+ return u''
+ if attrtype is None:
+ attrtype = self.e_schema.destination(attr)
+ props = self.e_schema.rproperties(attr)
+ if attrtype == 'String':
+ # internalinalized *and* formatted string such as schema
+ # description...
+ if props.get('internationalizable'):
+ value = self.req._(value)
+ attrformat = self.format(attr)
+ if attrformat:
+ return self.mtc_transform(value, attrformat, format,
+ self.req.encoding)
+ elif attrtype == 'Bytes':
+ attrformat = self.format(attr)
+ if attrformat:
+ try:
+ encoding = getattr(self, '%s_encoding' % attr)
+ except AttributeError:
+ encoding = self.req.encoding
+ return self.mtc_transform(value.getvalue(), attrformat, format,
+ encoding)
+ return u''
+ value = printable_value(self.req, attrtype, value, props, displaytime)
+ if format == 'text/html':
+ value = html_escape(value)
+ return value
+
+ def mtc_transform(self, data, format, target_format, encoding,
+ _engine=ENGINE):
+ trdata = TransformData(data, format, encoding, appobject=self)
+ data = _engine.convert(trdata, target_format).decode()
+ if format == 'text/html':
+ data = soup2xhtml(data, self.req.encoding)
+ return data
+
+ # entity cloning ##########################################################
+
+ def copy_relations(self, ceid):
+ """copy relations of the object with the given eid on this object
+
+ By default meta and composite relations are skipped.
+ Overrides this if you want another behaviour
+ """
+ assert self.has_eid()
+ execute = self.req.execute
+ for rschema in self.e_schema.subject_relations():
+ if rschema.meta or rschema.is_final():
+ continue
+ # skip already defined relations
+ if getattr(self, rschema.type):
+ continue
+ if rschema.type in self.skip_copy_for:
+ continue
+ if rschema.type == 'in_state':
+ # if the workflow is defining an initial state (XXX AND we are
+ # not in the managers group? not done to be more consistent)
+ # don't try to copy in_state
+ if execute('Any S WHERE S state_of ET, ET initial_state S,'
+ 'ET name %(etype)s', {'etype': str(self.e_schema)}):
+ continue
+ # skip composite relation
+ if self.e_schema.subjrproperty(rschema, 'composite'):
+ continue
+ # skip relation with card in ?1 else we either change the copied
+ # object (inlined relation) or inserting some inconsistency
+ if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1':
+ continue
+ rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
+ rschema.type, rschema.type)
+ execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
+ self.clear_related_cache(rschema.type, 'subject')
+ for rschema in self.e_schema.object_relations():
+ if rschema.meta:
+ continue
+ # skip already defined relations
+ if getattr(self, 'reverse_%s' % rschema.type):
+ continue
+ # skip composite relation
+ if self.e_schema.objrproperty(rschema, 'composite'):
+ continue
+ # skip relation with card in ?1 else we either change the copied
+ # object (inlined relation) or inserting some inconsistency
+ if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1':
+ continue
+ rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
+ rschema.type, rschema.type)
+ execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
+ self.clear_related_cache(rschema.type, 'object')
+
+ # data fetching methods ###################################################
+
+ @cached
+ def as_rset(self):
+ """returns a resultset containing `self` information"""
+ rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
+ {'x': self.eid}, [(self.id,)])
+ return self.req.decorate_rset(rset)
+
+ def to_complete_relations(self):
+ """by default complete final relations to when calling .complete()"""
+ for rschema in self.e_schema.subject_relations():
+ if rschema.is_final():
+ continue
+ if len(rschema.objects(self.e_schema)) > 1:
+ # ambigous relations, the querier doesn't handle
+ # outer join correctly in this case
+ continue
+ if rschema.inlined:
+ matching_groups = self.req.user.matching_groups
+ if matching_groups(rschema.get_groups('read')) and \
+ all(matching_groups(es.get_groups('read'))
+ for es in rschema.objects(self.e_schema)):
+ yield rschema, 'subject'
+
+ def to_complete_attributes(self, skip_bytes=True):
+ for rschema, attrschema in self.e_schema.attribute_definitions():
+ # skip binary data by default
+ if skip_bytes and attrschema.type == 'Bytes':
+ continue
+ attr = rschema.type
+ if attr == 'eid':
+ continue
+ # password retreival is blocked at the repository server level
+ if not self.req.user.matching_groups(rschema.get_groups('read')) \
+ or attrschema.type == 'Password':
+ self[attr] = None
+ continue
+ yield attr
+
+ def complete(self, attributes=None, skip_bytes=True):
+ """complete this entity by adding missing attributes (i.e. query the
+ repository to fill the entity)
+
+ :type skip_bytes: bool
+ :param skip_bytes:
+ if true, attribute of type Bytes won't be considered
+ """
+ assert self.has_eid()
+ varmaker = rqlvar_maker()
+ V = varmaker.next()
+ rql = ['WHERE %s eid %%(x)s' % V]
+ selected = []
+ for attr in (attributes or self.to_complete_attributes(skip_bytes)):
+ # if attribute already in entity, nothing to do
+ if self.has_key(attr):
+ continue
+ # case where attribute must be completed, but is not yet in entity
+ var = varmaker.next()
+ rql.append('%s %s %s' % (V, attr, var))
+ selected.append((attr, var))
+ # +1 since this doen't include the main variable
+ lastattr = len(selected) + 1
+ if attributes is None:
+ # fetch additional relations (restricted to 0..1 relations)
+ for rschema, role in self.to_complete_relations():
+ rtype = rschema.type
+ if self.relation_cached(rtype, role):
+ continue
+ var = varmaker.next()
+ if role == 'subject':
+ targettype = rschema.objects(self.e_schema)[0]
+ card = rschema.rproperty(self.e_schema, targettype,
+ 'cardinality')[0]
+ if card == '1':
+ rql.append('%s %s %s' % (V, rtype, var))
+ else: # '?"
+ rql.append('%s %s %s?' % (V, rtype, var))
+ else:
+ targettype = rschema.subjects(self.e_schema)[1]
+ card = rschema.rproperty(self.e_schema, targettype,
+ 'cardinality')[1]
+ if card == '1':
+ rql.append('%s %s %s' % (var, rtype, V))
+ else: # '?"
+ rql.append('%s? %s %s' % (var, rtype, V))
+ assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
+ role, card)
+ selected.append(((rtype, role), var))
+ if selected:
+ # select V, we need it as the left most selected variable
+ # if some outer join are included to fetch inlined relations
+ rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
+ ','.join(rql))
+ execute = getattr(self.req, 'unsafe_execute', self.req.execute)
+ rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0]
+ # handle attributes
+ for i in xrange(1, lastattr):
+ self[str(selected[i-1][0])] = rset[i]
+ # handle relations
+ for i in xrange(lastattr, len(rset)):
+ rtype, x = selected[i-1][0]
+ value = rset[i]
+ if value is None:
+ rrset = ResultSet([], rql, {'x': self.eid})
+ self.req.decorate_rset(rrset)
+ else:
+ rrset = self.req.eid_rset(value)
+ self.set_related_cache(rtype, x, rrset)
+
+ def get_value(self, name):
+ """get value for the attribute relation , query the repository
+ to get the value if necessary.
+
+ :type name: str
+ :param name: name of the attribute to get
+ """
+ try:
+ value = self[name]
+ except KeyError:
+ if not self.is_saved():
+ return None
+ rql = "Any A WHERE X eid %%(x)s, X %s A" % name
+ # XXX should we really use unsafe_execute here??
+ execute = getattr(self.req, 'unsafe_execute', self.req.execute)
+ try:
+ rset = execute(rql, {'x': self.eid}, 'x')
+ except Unauthorized:
+ self[name] = value = None
+ else:
+ assert rset.rowcount <= 1, (self, rql, rset.rowcount)
+ try:
+ self[name] = value = rset.rows[0][0]
+ except IndexError:
+ # probably a multisource error
+ self.critical("can't get value for attribute %s of entity with eid %s",
+ name, self.eid)
+ if self.e_schema.destination(name) == 'String':
+ self[name] = value = self.req._('unaccessible')
+ else:
+ self[name] = value = None
+ return value
+
+ def related(self, rtype, role='subject', limit=None, entities=False):
+ """returns a resultset of related entities
+
+ :param role: is the role played by 'self' in the relation ('subject' or 'object')
+ :param limit: resultset's maximum size
+ :param entities: if True, the entites are returned; if False, a result set is returned
+ """
+ try:
+ return self.related_cache(rtype, role, entities, limit)
+ except KeyError:
+ pass
+ assert self.has_eid()
+ rql = self.related_rql(rtype, role)
+ rset = self.req.execute(rql, {'x': self.eid}, 'x')
+ self.set_related_cache(rtype, role, rset)
+ return self.related(rtype, role, limit, entities)
+
+ def related_rql(self, rtype, role='subject'):
+ rschema = self.schema[rtype]
+ if role == 'subject':
+ targettypes = rschema.objects(self.e_schema)
+ restriction = 'E eid %%(x)s, E %s X' % rtype
+ card = greater_card(rschema, (self.e_schema,), targettypes, 0)
+ else:
+ targettypes = rschema.subjects(self.e_schema)
+ restriction = 'E eid %%(x)s, X %s E' % rtype
+ card = greater_card(rschema, targettypes, (self.e_schema,), 1)
+ if len(targettypes) > 1:
+ fetchattrs_list = []
+ for ttype in targettypes:
+ etypecls = self.vreg.etype_class(ttype)
+ fetchattrs_list.append(set(etypecls.fetch_attrs))
+ fetchattrs = reduce(set.intersection, fetchattrs_list)
+ rql = etypecls.fetch_rql(self.req.user, [restriction], fetchattrs,
+ settype=False)
+ else:
+ etypecls = self.vreg.etype_class(targettypes[0])
+ rql = etypecls.fetch_rql(self.req.user, [restriction], settype=False)
+ # optimisation: remove ORDERBY if cardinality is 1 or ? (though
+ # greater_card return 1 for those both cases)
+ if card == '1':
+ if ' ORDERBY ' in rql:
+ rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
+ rql.split(' WHERE ', 1)[1])
+ elif not ' ORDERBY ' in rql:
+ args = tuple(rql.split(' WHERE ', 1))
+ rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args
+ return rql
+
+ # generic vocabulary methods ##############################################
+
+ def vocabulary(self, rtype, role='subject', limit=None):
+ """vocabulary functions must return a list of couples
+ (label, eid) that will typically be used to fill the
+ edition view's combobox.
+
+ If `eid` is None in one of these couples, it should be
+ interpreted as a separator in case vocabulary results are grouped
+ """
+ try:
+ vocabfunc = getattr(self, '%s_%s_vocabulary' % (role, rtype))
+ except AttributeError:
+ vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
+ # NOTE: it is the responsibility of `vocabfunc` to sort the result
+ # (direclty through RQL or via a python sort). This is also
+ # important because `vocabfunc` might return a list with
+ # couples (label, None) which act as separators. In these
+ # cases, it doesn't make sense to sort results afterwards.
+ return vocabfunc(rtype, limit)
+
+ def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
+ vocabconstraints=True):
+ """build a rql to fetch `targettype` entities unrelated to this entity
+ using (rtype, role) relation
+ """
+ ordermethod = ordermethod or 'fetch_unrelated_order'
+ if isinstance(rtype, basestring):
+ rtype = self.schema.rschema(rtype)
+ if role == 'subject':
+ evar, searchedvar = 'S', 'O'
+ subjtype, objtype = self.e_schema, targettype
+ else:
+ searchedvar, evar = 'S', 'O'
+ objtype, subjtype = self.e_schema, targettype
+ if self.has_eid():
+ restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar]
+ else:
+ restriction = []
+ constraints = rtype.rproperty(subjtype, objtype, 'constraints')
+ if vocabconstraints:
+ # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
+ # will be included as well
+ restriction += [cstr.restriction for cstr in constraints
+ if isinstance(cstr, RQLVocabularyConstraint)]
+ else:
+ restriction += [cstr.restriction for cstr in constraints
+ if isinstance(cstr, RQLConstraint)]
+ etypecls = self.vreg.etype_class(targettype)
+ rql = etypecls.fetch_rql(self.req.user, restriction,
+ mainvar=searchedvar, ordermethod=ordermethod)
+ # ensure we have an order defined
+ if not ' ORDERBY ' in rql:
+ before, after = rql.split(' WHERE ', 1)
+ rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
+ return rql
+
+ def unrelated(self, rtype, targettype, role='subject', limit=None,
+ ordermethod=None):
+ """return a result set of target type objects that may be related
+ by a given relation, with self as subject or object
+ """
+ rql = self.unrelated_rql(rtype, targettype, role, ordermethod)
+ if limit is not None:
+ before, after = rql.split(' WHERE ', 1)
+ rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
+ if self.has_eid():
+ return self.req.execute(rql, {'x': self.eid})
+ return self.req.execute(rql)
+
+ # relations cache handling ################################################
+
+ def relation_cached(self, rtype, role):
+ """return true if the given relation is already cached on the instance
+ """
+ return '%s_%s' % (rtype, role) in self._related_cache
+
+ def related_cache(self, rtype, role, entities=True, limit=None):
+ """return values for the given relation if it's cached on the instance,
+ else raise `KeyError`
+ """
+ res = self._related_cache['%s_%s' % (rtype, role)][entities]
+ if limit:
+ if entities:
+ res = res[:limit]
+ else:
+ res = res.limit(limit)
+ return res
+
+ def set_related_cache(self, rtype, role, rset, col=0):
+ """set cached values for the given relation"""
+ if rset:
+ related = list(rset.entities(col))
+ rschema = self.schema.rschema(rtype)
+ if role == 'subject':
+ rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
+ 'cardinality')[1]
+ target = 'object'
+ else:
+ rcard = rschema.rproperty(related[0].e_schema, self.e_schema,
+ 'cardinality')[0]
+ target = 'subject'
+ if rcard in '?1':
+ for rentity in related:
+ rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self])
+ else:
+ related = []
+ self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
+
+ def clear_related_cache(self, rtype=None, role=None):
+ """clear cached values for the given relation or the entire cache if
+ no relation is given
+ """
+ if rtype is None:
+ self._related_cache = {}
+ else:
+ assert role
+ self._related_cache.pop('%s_%s' % (rtype, role), None)
+
+ # raw edition utilities ###################################################
+
+ def set_attributes(self, **kwargs):
+ assert kwargs
+ relations = []
+ for key in kwargs:
+ relations.append('X %s %%(%s)s' % (key, key))
+ # update current local object
+ self.update(kwargs)
+ # and now update the database
+ kwargs['x'] = self.eid
+ self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
+ kwargs, 'x')
+
+ def delete(self):
+ assert self.has_eid(), self.eid
+ self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
+ {'x': self.eid})
+
+ # server side utilities ###################################################
+
+ def set_defaults(self):
+ """set default values according to the schema"""
+ self._default_set = set()
+ for attr, value in self.e_schema.defaults():
+ if not self.has_key(attr):
+ self[str(attr)] = value
+ self._default_set.add(attr)
+
+ def check(self, creation=False):
+ """check this entity against its schema. Only final relation
+ are checked here, constraint on actual relations are checked in hooks
+ """
+ # necessary since eid is handled specifically and yams require it to be
+ # in the dictionary
+ if self.req is None:
+ _ = unicode
+ else:
+ _ = self.req._
+ self.e_schema.check(self, creation=creation, _=_)
+
+ 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:
+ yielded = False
+ 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
+ if not yielded:
+ yield self
+ 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 indexer package
+
+ :rtype: list
+ :return: the list of indexable word of this entity
+ """
+ from indexer.query_objects import tokenize
+ words = []
+ for rschema in self.e_schema.indexable_attributes():
+ try:
+ value = self.printable_value(rschema, format='text/plain')
+ except TransformError, ex:
+ 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 ##########################################
+
+class Attribute(object):
+ """descriptor that controls schema attribute access"""
+
+ def __init__(self, attrname):
+ assert attrname != 'eid'
+ self._attrname = attrname
+
+ def __get__(self, eobj, eclass):
+ if eobj is None:
+ return self
+ return eobj.get_value(self._attrname)
+
+ def __set__(self, eobj, value):
+ # XXX bw compat
+ # would be better to generate UPDATE queries than the current behaviour
+ eobj.warning("deprecated usage, don't use 'entity.attr = val' notation)")
+ eobj[self._attrname] = value
+
+
+class Relation(object):
+ """descriptor that controls schema relation access"""
+ _role = None # for pylint
+
+ def __init__(self, rschema):
+ self._rschema = rschema
+ self._rtype = rschema.type
+
+ def __get__(self, eobj, eclass):
+ if eobj is None:
+ raise AttributeError('%s cannot be only be accessed from instances'
+ % self._rtype)
+ return eobj.related(self._rtype, self._role, entities=True)
+
+ def __set__(self, eobj, value):
+ 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 93447d75c4b9 -r 6a25c58a1c23 ext/__init__.py
diff -r 93447d75c4b9 -r 6a25c58a1c23 ext/html4zope.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/html4zope.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,153 @@
+# Author: David Goodger
+# Contact: goodger@users.sourceforge.net
+# Revision: $Revision: 1.2 $
+# Date: $Date: 2005-07-04 16:36:50 $
+# Copyright: This module has been placed in the public domain.
+
+"""
+Simple HyperText Markup Language document tree Writer.
+
+The output conforms to the HTML 4.01 Transitional DTD and to the Extensible
+HTML version 1.0 Transitional DTD (*almost* strict). The output contains a
+minimum of formatting information. A cascading style sheet ("default.css" by
+default) is required for proper viewing with a modern graphical browser.
+
+http://cvs.zope.org/Zope/lib/python/docutils/writers/Attic/html4zope.py?rev=1.1.2.2&only_with_tag=ajung-restructuredtext-integration-branch&content-type=text/vnd.viewcvs-markup
+"""
+
+__docformat__ = 'reStructuredText'
+
+from logilab.mtconverter import html_escape
+
+from docutils import nodes
+from docutils.writers.html4css1 import Writer as CSS1Writer
+from docutils.writers.html4css1 import HTMLTranslator as CSS1HTMLTranslator
+import os
+
+default_level = int(os.environ.get('STX_DEFAULT_LEVEL', 3))
+
+class Writer(CSS1Writer):
+ """css writer using our html translator"""
+ def __init__(self, base_url):
+ CSS1Writer.__init__(self)
+ self.translator_class = URLBinder(base_url, HTMLTranslator)
+
+ def apply_template(self):
+ """overriding this is necessary with docutils >= 0.5"""
+ return self.visitor.astext()
+
+class URLBinder:
+ def __init__(self, url, klass):
+ self.base_url = url
+ self.translator_class = HTMLTranslator
+
+ def __call__(self, document):
+ translator = self.translator_class(document)
+ translator.base_url = self.base_url
+ return translator
+
+class HTMLTranslator(CSS1HTMLTranslator):
+ """ReST tree to html translator"""
+
+ def astext(self):
+ """return the extracted html"""
+ return ''.join(self.body)
+
+ def visit_title(self, node):
+ """Only 6 section levels are supported by HTML."""
+ if isinstance(node.parent, nodes.topic):
+ self.body.append(
+ self.starttag(node, 'p', '', CLASS='topic-title'))
+ if node.parent.hasattr('id'):
+ self.body.append(
+ self.starttag({}, 'a', '', name=node.parent['id']))
+ self.context.append('\n')
+ else:
+ self.context.append('\n')
+ elif self.section_level == 0:
+ # document title
+ self.head.append('%s\n'
+ % self.encode(node.astext()))
+ self.body.append(self.starttag(node, 'h%d' % default_level, '',
+ CLASS='title'))
+ self.context.append('\n' % default_level)
+ else:
+ self.body.append(
+ self.starttag(node, 'h%s' % (
+ default_level+self.section_level-1), ''))
+ atts = {}
+ if node.hasattr('refid'):
+ atts['class'] = 'toc-backref'
+ atts['href'] = '%s#%s' % (self.base_url, node['refid'])
+ self.body.append(self.starttag({}, 'a', '', **atts))
+ self.context.append('\n' % (
+ default_level+self.section_level-1))
+
+ def visit_subtitle(self, node):
+ """format a subtitle"""
+ if isinstance(node.parent, nodes.sidebar):
+ self.body.append(self.starttag(node, 'p', '',
+ CLASS='sidebar-subtitle'))
+ self.context.append('\n')
+ else:
+ self.body.append(
+ self.starttag(node, 'h%s' % (default_level+1), '',
+ CLASS='subtitle'))
+ self.context.append('\n' % (default_level+1))
+
+ def visit_document(self, node):
+ """syt: i don't want the enclosing
"""
+ def depart_document(self, node):
+ """syt: i don't want the enclosing
"""
+
+ def visit_reference(self, node):
+ """syt: i want absolute urls"""
+ if node.has_key('refuri'):
+ href = node['refuri']
+ if ( self.settings.cloak_email_addresses
+ and href.startswith('mailto:')):
+ href = self.cloak_mailto(href)
+ self.in_mailto = 1
+ else:
+ assert node.has_key('refid'), \
+ 'References must have "refuri" or "refid" attribute.'
+ href = '%s#%s' % (self.base_url, node['refid'])
+ atts = {'href': href, 'class': 'reference'}
+ if not isinstance(node.parent, nodes.TextElement):
+ assert len(node) == 1 and isinstance(node[0], nodes.image)
+ atts['class'] += ' image-reference'
+ self.body.append(self.starttag(node, 'a', '', **atts))
+
+ ## override error messages to avoid XHTML problems ########################
+ def visit_problematic(self, node):
+ pass
+
+ def depart_problematic(self, node):
+ pass
+
+ def visit_system_message(self, node):
+ backref_text = ''
+ if len(node['backrefs']):
+ backrefs = node['backrefs']
+ if len(backrefs) == 1:
+ backref_text = '; backlink'
+ else:
+ i = 1
+ backlinks = []
+ for backref in backrefs:
+ backlinks.append(str(i))
+ i += 1
+ backref_text = ('; backlinks: %s'
+ % ', '.join(backlinks))
+ if node.hasattr('line'):
+ line = ', line %s' % node['line']
+ else:
+ line = ''
+ a_start = a_end = ''
+ error = u'System Message: %s%s/%s%s (%s %s)%s\n' % (
+ a_start, node['type'], node['level'], a_end,
+ self.encode(node['source']), line, backref_text)
+ self.body.append(u'
ReST / HTML errors:%s
' % html_escape(error))
+
+ def depart_system_message(self, node):
+ pass
diff -r 93447d75c4b9 -r 6a25c58a1c23 ext/rest.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/rest.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,223 @@
+"""rest publishing functions
+
+contains some functions and setup of docutils for cubicweb
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from cStringIO import StringIO
+from itertools import chain
+from logging import getLogger
+from os.path import join
+
+from docutils import statemachine, nodes, utils, io
+from docutils.core import publish_string
+from docutils.parsers.rst import Parser, states, directives
+from docutils.parsers.rst.roles import register_canonical_role, set_classes
+
+from logilab.mtconverter import html_escape
+
+from cubicweb.ext.html4zope import Writer
+
+# We provide our own parser as an attempt to get rid of
+# state machine reinstanciation
+
+import re
+# compile states.Body patterns
+for k, v in states.Body.patterns.items():
+ if isinstance(v, str):
+ states.Body.patterns[k] = re.compile(v)
+
+# register ReStructured Text mimetype / extensions
+import mimetypes
+mimetypes.add_type('text/rest', '.rest')
+mimetypes.add_type('text/rest', '.rst')
+
+
+LOGGER = getLogger('cubicweb.rest')
+
+def eid_reference_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ try:
+ try:
+ eid_num, rest = text.split(u':', 1)
+ except:
+ eid_num, rest = text, '#'+text
+ eid_num = int(eid_num)
+ if eid_num < 0:
+ raise ValueError
+ except ValueError:
+ msg = inliner.reporter.error(
+ 'EID number must be a positive number; "%s" is invalid.'
+ % text, line=lineno)
+ prb = inliner.problematic(rawtext, rawtext, msg)
+ return [prb], [msg]
+ # Base URL mainly used by inliner.pep_reference; so this is correct:
+ context = inliner.document.settings.context
+ refedentity = context.req.eid_rset(eid_num).get_entity(0, 0)
+ ref = refedentity.absolute_url()
+ set_classes(options)
+ return [nodes.reference(rawtext, utils.unescape(rest), refuri=ref,
+ **options)], []
+
+register_canonical_role('eid', eid_reference_role)
+
+
+def card_reference_role(role, rawtext, text, lineno, inliner,
+ options={}, content=[]):
+ text = text.strip()
+ try:
+ wikiid, rest = text.split(u':', 1)
+ except:
+ wikiid, rest = text, text
+ context = inliner.document.settings.context
+ cardrset = context.req.execute('Card X WHERE X wikiid %(id)s',
+ {'id': wikiid})
+ if cardrset:
+ ref = cardrset.get_entity(0, 0).absolute_url()
+ else:
+ schema = context.schema
+ if schema.eschema('Card').has_perm(context.req, 'add'):
+ ref = context.req.build_url('view', vid='creation', etype='Card', wikiid=wikiid)
+ else:
+ ref = '#'
+ set_classes(options)
+ return [nodes.reference(rawtext, utils.unescape(rest), refuri=ref,
+ **options)], []
+
+register_canonical_role('card', card_reference_role)
+
+
+def winclude_directive(name, arguments, options, content, lineno,
+ content_offset, block_text, state, state_machine):
+ """Include a reST file as part of the content of this reST file.
+
+ same as standard include directive but using config.locate_doc_resource to
+ get actual file to include.
+
+ Most part of this implementation is copied from `include` directive defined
+ in `docutils.parsers.rst.directives.misc`
+ """
+ context = state.document.settings.context
+ source = state_machine.input_lines.source(
+ lineno - state_machine.input_offset - 1)
+ #source_dir = os.path.dirname(os.path.abspath(source))
+ fid = arguments[0]
+ for lang in chain((context.req.lang, context.vreg.property_value('ui.language')),
+ context.config.available_languages()):
+ rid = '%s_%s.rst' % (fid, lang)
+ resourcedir = context.config.locate_doc_file(rid)
+ if resourcedir:
+ break
+ else:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive path:\nno resource matching %s.'
+ % (name, fid),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ path = join(resourcedir, rid)
+ encoding = options.get('encoding', state.document.settings.input_encoding)
+ try:
+ state.document.settings.record_dependencies.add(path)
+ include_file = io.FileInput(
+ source_path=path, encoding=encoding,
+ error_handler=state.document.settings.input_encoding_error_handler,
+ handle_io_errors=None)
+ except IOError, error:
+ severe = state_machine.reporter.severe(
+ 'Problems with "%s" directive path:\n%s: %s.'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ try:
+ include_text = include_file.read()
+ except UnicodeError, error:
+ severe = state_machine.reporter.severe(
+ 'Problem with "%s" directive:\n%s: %s'
+ % (name, error.__class__.__name__, error),
+ nodes.literal_block(block_text, block_text), line=lineno)
+ return [severe]
+ if options.has_key('literal'):
+ literal_block = nodes.literal_block(include_text, include_text,
+ source=path)
+ literal_block.line = 1
+ return literal_block
+ else:
+ include_lines = statemachine.string2lines(include_text,
+ convert_whitespace=1)
+ state_machine.insert_input(include_lines, path)
+ return []
+
+winclude_directive.arguments = (1, 0, 1)
+winclude_directive.options = {'literal': directives.flag,
+ 'encoding': directives.encoding}
+directives.register_directive('winclude', winclude_directive)
+
+class CubicWebReSTParser(Parser):
+ """The (customized) reStructuredText parser."""
+
+ def __init__(self):
+ self.initial_state = 'Body'
+ self.state_classes = states.state_classes
+ self.inliner = states.Inliner()
+ self.statemachine = states.RSTStateMachine(
+ state_classes=self.state_classes,
+ initial_state=self.initial_state,
+ debug=0)
+
+ def parse(self, inputstring, document):
+ """Parse `inputstring` and populate `document`, a document tree."""
+ self.setup_parse(inputstring, document)
+ inputlines = statemachine.string2lines(inputstring,
+ convert_whitespace=1)
+ self.statemachine.run(inputlines, document, inliner=self.inliner)
+ self.finish_parse()
+
+
+_REST_PARSER = CubicWebReSTParser()
+
+def rest_publish(context, data):
+ """publish a string formatted as ReStructured Text to HTML
+
+ :type context: a cubicweb application object
+
+ :type data: str
+ :param data: some ReST text
+
+ :rtype: unicode
+ :return:
+ the data formatted as HTML or the original data if an error occured
+ """
+ req = context.req
+ if isinstance(data, unicode):
+ encoding = 'unicode'
+ else:
+ encoding = req.encoding
+ settings = {'input_encoding': encoding, 'output_encoding': 'unicode',
+ 'warning_stream': StringIO(), 'context': context,
+ # dunno what's the max, severe is 4, and we never want a crash
+ # (though try/except may be a better option...)
+ 'halt_level': 10,
+ }
+ if context:
+ if hasattr(req, 'url'):
+ base_url = req.url()
+ elif hasattr(context, 'absolute_url'):
+ base_url = context.absolute_url()
+ else:
+ base_url = req.base_url()
+ else:
+ base_url = None
+ try:
+ return publish_string(writer=Writer(base_url=base_url),
+ parser=_REST_PARSER, source=data,
+ settings_overrides=settings)
+ except Exception:
+ LOGGER.exception('error while publishing ReST text')
+ if not isinstance(data, unicode):
+ data = unicode(data, encoding, 'replace')
+ return html_escape(req._('error while publishing ReST text')
+ + '\n\n' + data)
diff -r 93447d75c4b9 -r 6a25c58a1c23 ext/tal.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ext/tal.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,256 @@
+"""provides simpleTAL extensions for CubicWeb
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+
+__docformat__ = "restructuredtext en"
+
+import sys
+import re
+from os.path import exists, isdir, join
+from logging import getLogger
+from StringIO import StringIO
+
+from simpletal import simpleTAL, simpleTALES
+
+from logilab.common.decorators import cached
+
+LOGGER = getLogger('cubicweb.tal')
+
+
+class LoggerAdapter(object):
+ def __init__(self, tal_logger):
+ self.tal_logger = tal_logger
+
+ def debug(self, msg):
+ LOGGER.debug(msg)
+
+ def warn(self, msg):
+ LOGGER.warning(msg)
+
+ def __getattr__(self, attrname):
+ return getattr(self.tal_logger, attrname)
+
+
+class CubicWebContext(simpleTALES.Context):
+ """add facilities to access entity / resultset"""
+
+ def __init__(self, options=None, allowPythonPath=1):
+ simpleTALES.Context.__init__(self, options, allowPythonPath)
+ self.log = LoggerAdapter(self.log)
+
+ def update(self, context):
+ for varname, value in context.items():
+ self.addGlobal(varname, value)
+
+ def addRepeat(self, name, var, initialValue):
+ simpleTALES.Context.addRepeat(self, name, var, initialValue)
+
+# XXX FIXME need to find a clean to define OPCODE values for extensions
+I18N_CONTENT = 18
+I18N_REPLACE = 19
+RQL_EXECUTE = 20
+# simpleTAL uses the OPCODE values to define priority over commands.
+# TAL_ITER should have the same priority than TAL_REPEAT (i.e. 3), but
+# we can't use the same OPCODE for two different commands without changing
+# the simpleTAL implementation. Another solution would be to totally override
+# the REPEAT implementation with the ITER one, but some specific operations
+# (involving len() for instance) are not implemented for ITER, so we prefer
+# to keep both implementations for now, and to fool simpleTAL by using a float
+# number between 3 and 4
+TAL_ITER = 3.1
+
+
+# FIX simpleTAL HTML 4.01 stupidity
+# (simpleTAL never closes tags like INPUT, IMG, HR ...)
+simpleTAL.HTML_FORBIDDEN_ENDTAG.clear()
+
+class CubicWebTemplateCompiler(simpleTAL.HTMLTemplateCompiler):
+ """extends default compiler by adding i18n:content commands"""
+
+ def __init__(self):
+ simpleTAL.HTMLTemplateCompiler.__init__(self)
+ self.commandHandler[I18N_CONTENT] = self.compile_cmd_i18n_content
+ self.commandHandler[I18N_REPLACE] = self.compile_cmd_i18n_replace
+ self.commandHandler[RQL_EXECUTE] = self.compile_cmd_rql
+ self.commandHandler[TAL_ITER] = self.compile_cmd_tal_iter
+
+ def setTALPrefix(self, prefix):
+ simpleTAL.TemplateCompiler.setTALPrefix(self, prefix)
+ self.tal_attribute_map['i18n:content'] = I18N_CONTENT
+ self.tal_attribute_map['i18n:replace'] = I18N_REPLACE
+ self.tal_attribute_map['rql:execute'] = RQL_EXECUTE
+ self.tal_attribute_map['tal:iter'] = TAL_ITER
+
+ def compile_cmd_i18n_content(self, argument):
+ # XXX tal:content structure=, text= should we support this ?
+ structure_flag = 0
+ return (I18N_CONTENT, (argument, False, structure_flag, self.endTagSymbol))
+
+ def compile_cmd_i18n_replace(self, argument):
+ # XXX tal:content structure=, text= should we support this ?
+ structure_flag = 0
+ return (I18N_CONTENT, (argument, True, structure_flag, self.endTagSymbol))
+
+ def compile_cmd_rql(self, argument):
+ return (RQL_EXECUTE, (argument, self.endTagSymbol))
+
+ def compile_cmd_tal_iter(self, argument):
+ original_id, (var_name, expression, end_tag_symbol) = \
+ simpleTAL.HTMLTemplateCompiler.compileCmdRepeat(self, argument)
+ return (TAL_ITER, (var_name, expression, self.endTagSymbol))
+
+ def getTemplate(self):
+ return CubicWebTemplate(self.commandList, self.macroMap, self.symbolLocationTable)
+
+ def compileCmdAttributes (self, argument):
+ """XXX modified to support single attribute
+ definition ending by a ';'
+
+ backport this to simpleTAL
+ """
+ # Compile tal:attributes into attribute command
+ # Argument: [(attributeName, expression)]
+
+ # Break up the list of attribute settings first
+ commandArgs = []
+ # We only want to match semi-colons that are not escaped
+ argumentSplitter = re.compile(r'(?\n' % nbclosed)
self.w(u'%s remaining sessions \n' % remaining)
self.w(u'
')
+
+def registration_callback(vreg):
+ vreg.register(SessionsCleaner)
+ vreg.register(GAEAuthenticationManager, clear=True)
+ vreg.register(GAEPersistentSessionManager, clear=True)
diff -r 93447d75c4b9 -r 6a25c58a1c23 goa/db.py
--- a/goa/db.py Fri Feb 27 09:59:53 2009 +0100
+++ b/goa/db.py Mon Mar 02 21:03:54 2009 +0100
@@ -25,7 +25,7 @@
* XXX ListProperty
:organization: Logilab
-:copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2008-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -37,7 +37,7 @@
from cubicweb import RequestSessionMixIn, Binary, entities
from cubicweb.rset import ResultSet
-from cubicweb.common.entity import metaentity
+from cubicweb.entity import metaentity
from cubicweb.server.utils import crypt_password
from cubicweb.goa import use_mx_for_dates, mx2datetime, MODE
from cubicweb.goa.dbinit import init_relations
diff -r 93447d75c4b9 -r 6a25c58a1c23 goa/goactl.py
--- a/goa/goactl.py Fri Feb 27 09:59:53 2009 +0100
+++ b/goa/goactl.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""cubicweb on appengine plugins for cubicweb-ctl
:organization: Logilab
-:copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2008-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -11,7 +11,7 @@
from cubicweb import BadCommandUsage
from cubicweb import CW_SOFTWARE_ROOT
from cubicweb.toolsutils import (Command, register_commands, copy_skeleton,
- create_dir, create_symlink, create_copy)
+ create_dir, create_symlink, create_copy)
from cubicweb.cwconfig import CubicWebConfiguration
from logilab import common as lgc
@@ -47,28 +47,29 @@
'__init__.py',
'__pkginfo__.py',
'_exceptions.py',
+ 'appobject.py',
'dbapi.py',
'cwvreg.py',
'cwconfig.py',
+ 'entity.py',
'interfaces.py',
'rset.py',
'schema.py',
'schemaviewer.py',
+ 'selectors.py',
+ 'utils.py',
'vregistry.py',
+ 'view.py',
- 'common/appobject.py',
- 'common/entity.py',
- 'common/html4zope.py',
'common/mail.py',
'common/migration.py',
'common/mixins.py',
'common/mttransforms.py',
'common/registerers.py',
- 'common/rest.py',
- 'common/selectors.py',
- 'common/view.py',
'common/uilib.py',
- 'common/utils.py',
+
+ 'ext/html4zope.py',
+ 'ext/rest.py',
'server/hookhelper.py',
'server/hooksmanager.py',
diff -r 93447d75c4b9 -r 6a25c58a1c23 i18n/en.po
--- a/i18n/en.po Fri Feb 27 09:59:53 2009 +0100
+++ b/i18n/en.po Mon Mar 02 21:03:54 2009 +0100
@@ -1841,6 +1841,9 @@
msgid "i18n_login_popup"
msgstr "login"
+msgid "i18n_register_user"
+msgstr "register"
+
msgid "i18nprevnext_next"
msgstr "next"
@@ -2046,6 +2049,9 @@
msgid "login"
msgstr ""
+msgid "login or email"
+msgstr ""
+
msgid "login_action"
msgstr "log in"
diff -r 93447d75c4b9 -r 6a25c58a1c23 i18n/es.po
--- a/i18n/es.po Fri Feb 27 09:59:53 2009 +0100
+++ b/i18n/es.po Mon Mar 02 21:03:54 2009 +0100
@@ -1924,6 +1924,9 @@
msgid "i18n_login_popup"
msgstr "identificarse"
+msgid "i18n_register_user"
+msgstr "registrarse"
+
msgid "i18nprevnext_next"
msgstr "siguiente"
diff -r 93447d75c4b9 -r 6a25c58a1c23 i18n/fr.po
--- a/i18n/fr.po Fri Feb 27 09:59:53 2009 +0100
+++ b/i18n/fr.po Mon Mar 02 21:03:54 2009 +0100
@@ -1924,6 +1924,9 @@
msgid "i18n_login_popup"
msgstr "s'authentifier"
+msgid "i18n_register_user"
+msgstr "s'enregister"
+
msgid "i18nprevnext_next"
msgstr "suivant"
@@ -2140,6 +2143,9 @@
msgid "login"
msgstr "identifiant"
+msgid "login or email"
+msgstr "identifiant ou email"
+
msgid "login_action"
msgstr "identifiez vous"
diff -r 93447d75c4b9 -r 6a25c58a1c23 interfaces.py
--- a/interfaces.py Fri Feb 27 09:59:53 2009 +0100
+++ b/interfaces.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""Specific views for entities implementing IDownloadable
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
@@ -193,8 +193,8 @@
"""embed action interface"""
class ICalendarable(Interface):
- """interface for itms that do have a begin date 'start' and an end
-date 'stop'"""
+ """interface for items that do have a begin date 'start' and an end date 'stop'
+ """
class ICalendarViews(Interface):
"""calendar views interface"""
diff -r 93447d75c4b9 -r 6a25c58a1c23 misc/migration/3.2.0_Any.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/misc/migration/3.2.0_Any.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,3 @@
+rql('SET X value "main-template" WHERE X is EProperty, '
+ 'X pkey "ui.main-template", X value "main"')
+checkpoint()
diff -r 93447d75c4b9 -r 6a25c58a1c23 schema.py
--- a/schema.py Fri Feb 27 09:59:53 2009 +0100
+++ b/schema.py Mon Mar 02 21:03:54 2009 +0100
@@ -10,7 +10,7 @@
import re
from logging import getLogger
-from logilab.common.decorators import cached, clear_cache
+from logilab.common.decorators import cached, clear_cache, monkeypatch
from logilab.common.compat import any
from yams import BadSchemaDefinition, buildobjs as ybo
@@ -68,6 +68,41 @@
return (etype,)
ybo.RelationDefinition._actual_types = _actual_types
+
+## cubicweb provides a RichString class for convenience
+class RichString(ybo.String):
+ """Convenience RichString attribute type
+ The follwing declaration::
+
+ class Card(EntityType):
+ content = RichString(fulltextindexed=True, default_format='text/rest')
+
+ is equivalent to::
+
+ class Card(EntityType):
+ content_format = String(meta=True, internationalizable=True,
+ default='text/rest', constraints=[format_constraint])
+ content = String(fulltextindexed=True)
+ """
+ def __init__(self, default_format='text/plain', format_constraints=None, **kwargs):
+ self.default_format = default_format
+ self.format_constraints = format_constraints or [format_constraint]
+ super(RichString, self).__init__(**kwargs)
+
+PyFileReader.context['RichString'] = RichString
+
+## need to monkeypatch yams' _add_relation function to handle RichString
+yams_add_relation = ybo._add_relation
+@monkeypatch(ybo)
+def _add_relation(relations, rdef, name=None, insertidx=None):
+ if isinstance(rdef, RichString):
+ default_format = rdef.default_format
+ format_attrdef = ybo.String(meta=True, internationalizable=True,
+ default=rdef.default_format, maxsize=50,
+ constraints=rdef.format_constraints)
+ yams_add_relation(relations, format_attrdef, name+'_format', insertidx)
+ yams_add_relation(relations, rdef, name, insertidx)
+
def display_name(req, key, form=''):
"""return a internationalized string for the key (schema entity or relation
name) in a given form
@@ -805,7 +840,34 @@
PyFileReader.context['RRQLExpression'] = RRQLExpression
-
+# workflow extensions #########################################################
+
+class workflowable_definition(ybo.metadefinition):
+ """extends default EntityType's metaclass to add workflow relations
+ (i.e. in_state and wf_info_for).
+ This is the default metaclass for WorkflowableEntityType
+ """
+ def __new__(mcs, name, bases, classdict):
+ abstract = classdict.pop('abstract', False)
+ defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict)
+ if not abstract:
+ existing_rels = set(rdef.name for rdef in defclass.__relations__)
+ if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels:
+ in_state = ybo.SubjectRelation('State', cardinality='1*',
+ # XXX automatize this
+ constraints=[RQLConstraint('S is ET, O state_of ET')],
+ description=_('account state'))
+ yams_add_relation(defclass.__relations__, in_state, 'in_state')
+ wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object')
+ yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for')
+ return defclass
+
+class WorkflowableEntityType(ybo.EntityType):
+ __metaclass__ = workflowable_definition
+ abstract = True
+
+PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
+
# schema loading ##############################################################
class CubicWebRelationFileReader(RelationFileReader):
@@ -877,6 +939,7 @@
def _load_definition_files(self, cubes):
for filepath in (self.include_schema_files('bootstrap')
+ self.include_schema_files('base')
+ + self.include_schema_files('workflow')
+ self.include_schema_files('Bookmark')
+ self.include_schema_files('Card')):
self.info('loading %s', filepath)
@@ -892,8 +955,8 @@
PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
class FormatConstraint(StaticVocabularyConstraint):
- need_perm_formats = (_('text/cubicweb-page-template'),
- )
+ need_perm_formats = [_('text/cubicweb-page-template')]
+
regular_formats = (_('text/rest'),
_('text/html'),
_('text/plain'),
@@ -913,7 +976,7 @@
def vocabulary(self, entity=None):
if entity and entity.req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
- return self.regular_formats + self.need_perm_formats
+ return self.regular_formats + tuple(self.need_perm_formats)
return self.regular_formats
def __str__(self):
diff -r 93447d75c4b9 -r 6a25c58a1c23 schemas/Card.py
--- a/schemas/Card.py Fri Feb 27 09:59:53 2009 +0100
+++ b/schemas/Card.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,4 +1,4 @@
-from cubicweb.schema import format_constraint
+# from cubicweb.schema import format_constraint
class Card(EntityType):
"""a card is a textual content used as documentation, reference, procedure reminder"""
@@ -12,7 +12,5 @@
title = String(required=True, fulltextindexed=True, maxsize=256)
synopsis = String(fulltextindexed=True, maxsize=512,
description=_("an abstract for this card"))
- content_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/rest', constraints=[format_constraint])
- content = String(fulltextindexed=True)
+ content = RichString(fulltextindexed=True, default_format='text/rest')
wikiid = String(maxsize=64, indexed=True)
diff -r 93447d75c4b9 -r 6a25c58a1c23 schemas/base.py
--- a/schemas/base.py Fri Feb 27 09:59:53 2009 +0100
+++ b/schemas/base.py Mon Mar 02 21:03:54 2009 +0100
@@ -9,8 +9,9 @@
from cubicweb.schema import format_constraint
-class EUser(RestrictedEntityType):
+class EUser(WorkflowableEntityType):
"""define a CubicWeb user"""
+ meta = True # XXX backported from old times, shouldn't be there anymore
permissions = {
'read': ('managers', 'users', ERQLExpression('X identity U')),
'add': ('managers',),
@@ -33,11 +34,6 @@
in_group = SubjectRelation('EGroup', cardinality='+*',
constraints=[RQLConstraint('NOT O name "owners"')],
description=_('groups grant permissions to the user'))
- in_state = SubjectRelation('State', cardinality='1*',
- # XXX automatize this
- constraints=[RQLConstraint('S is ET, O state_of ET')],
- description=_('account state'))
- wf_info_for = ObjectRelation('TrInfo', cardinality='1*', composite='object')
class EmailAddress(MetaEntityType):
@@ -130,112 +126,7 @@
cardinality = '11'
subject = '**'
object = 'Datetime'
-
-
-class State(MetaEntityType):
- """used to associate simple states to an entity type and/or to define
- workflows
- """
- name = String(required=True, indexed=True, internationalizable=True,
- maxsize=256)
- description_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/rest', constraints=[format_constraint])
- description = String(fulltextindexed=True,
- description=_('semantic description of this state'))
- state_of = SubjectRelation('EEType', cardinality='+*',
- description=_('entity types which may use this state'),
- constraints=[RQLConstraint('O final FALSE')])
- allowed_transition = SubjectRelation('Transition', cardinality='**',
- constraints=[RQLConstraint('S state_of ET, O transition_of ET')],
- description=_('allowed transitions from this state'))
-
- initial_state = ObjectRelation('EEType', cardinality='?*',
- # S initial_state O, O state_of S
- constraints=[RQLConstraint('O state_of S')],
- description=_('initial state for entities of this type'))
-
-
-class Transition(MetaEntityType):
- """use to define a transition from one or multiple states to a destination
- states in workflow's definitions.
- """
- name = String(required=True, indexed=True, internationalizable=True,
- maxsize=256)
- description_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/rest', constraints=[format_constraint])
- description = String(fulltextindexed=True,
- description=_('semantic description of this transition'))
- condition = SubjectRelation('RQLExpression', cardinality='*?', composite='subject',
- description=_('a RQL expression which should return some results, '
- 'else the transition won\'t be available. '
- 'This query may use X and U variables '
- 'that will respectivly represents '
- 'the current entity and the current user'))
-
- require_group = SubjectRelation('EGroup', cardinality='**',
- description=_('group in which a user should be to be '
- 'allowed to pass this transition'))
- transition_of = SubjectRelation('EEType', cardinality='+*',
- description=_('entity types which may use this transition'),
- constraints=[RQLConstraint('O final FALSE')])
- destination_state = SubjectRelation('State', cardinality='?*',
- constraints=[RQLConstraint('S transition_of ET, O state_of ET')],
- description=_('destination state for this transition'))
-
-
-class TrInfo(MetaEntityType):
- from_state = SubjectRelation('State', cardinality='?*')
- to_state = SubjectRelation('State', cardinality='1*')
- comment_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/rest', constraints=[format_constraint])
- comment = String(fulltextindexed=True)
- # get actor and date time using owned_by and creation_date
-
-
-class from_state(MetaRelationType):
- inlined = True
-class to_state(MetaRelationType):
- inlined = True
-class wf_info_for(MetaRelationType):
- """link a transition information to its object"""
- permissions = {
- 'read': ('managers', 'users', 'guests',),# RRQLExpression('U has_read_permission O')),
- 'add': (), # handled automatically, no one should add one explicitly
- 'delete': ('managers',), # RRQLExpression('U has_delete_permission O')
- }
- inlined = True
- composite = 'object'
- fulltext_container = composite
-
-class state_of(MetaRelationType):
- """link a state to one or more entity type"""
-class transition_of(MetaRelationType):
- """link a transition to one or more entity type"""
-
-class initial_state(MetaRelationType):
- """indicate which state should be used by default when an entity using
- states is created
- """
- inlined = True
-
-class destination_state(MetaRelationType):
- """destination state of a transition"""
- inlined = True
-
-class allowed_transition(MetaRelationType):
- """allowed transition from this state"""
-
-class in_state(UserRelationType):
- """indicate the current state of an entity"""
- meta = True
- # not inlined intentionnaly since when using ldap sources, user'state
- # has to be stored outside the EUser table
-
- # add/delete perms given to managers/users, after what most of the job
- # is done by workflow enforcment
-
-
class EProperty(EntityType):
"""used for cubicweb configuration. Once a property has been created you
can't change the key.
diff -r 93447d75c4b9 -r 6a25c58a1c23 schemas/bootstrap.py
--- a/schemas/bootstrap.py Fri Feb 27 09:59:53 2009 +0100
+++ b/schemas/bootstrap.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""core CubicWeb schema necessary for bootstrapping the actual application's schema
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
@@ -14,10 +14,8 @@
"""define an entity type, used to build the application schema"""
name = String(required=True, indexed=True, internationalizable=True,
unique=True, maxsize=64)
- description_format = String(meta=True, internationalizable=True, maxsize=50,
- default='text/plain', constraints=[format_constraint])
- description = String(internationalizable=True,
- description=_('semantic description of this entity type'))
+ description = RichString(internationalizable=True,
+ description=_('semantic description of this entity type'))
meta = Boolean(description=_('is it an application entity type or not ?'))
# necessary to filter using RQL
final = Boolean(description=_('automatic'))
diff -r 93447d75c4b9 -r 6a25c58a1c23 schemas/workflow.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/schemas/workflow.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,108 @@
+"""workflow related schemas
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+
+class State(MetaEntityType):
+ """used to associate simple states to an entity type and/or to define
+ workflows
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ maxsize=256)
+ description = RichString(fulltextindexed=True, default_format='text/rest',
+ description=_('semantic description of this state'))
+
+ state_of = SubjectRelation('EEType', cardinality='+*',
+ description=_('entity types which may use this state'),
+ constraints=[RQLConstraint('O final FALSE')])
+ allowed_transition = SubjectRelation('Transition', cardinality='**',
+ constraints=[RQLConstraint('S state_of ET, O transition_of ET')],
+ description=_('allowed transitions from this state'))
+
+ initial_state = ObjectRelation('EEType', cardinality='?*',
+ # S initial_state O, O state_of S
+ constraints=[RQLConstraint('O state_of S')],
+ description=_('initial state for entities of this type'))
+
+
+class Transition(MetaEntityType):
+ """use to define a transition from one or multiple states to a destination
+ states in workflow's definitions.
+ """
+ name = String(required=True, indexed=True, internationalizable=True,
+ maxsize=256)
+ description_format = String(meta=True, internationalizable=True, maxsize=50,
+ default='text/rest', constraints=[format_constraint])
+ description = String(fulltextindexed=True,
+ description=_('semantic description of this transition'))
+ condition = SubjectRelation('RQLExpression', cardinality='*?', composite='subject',
+ description=_('a RQL expression which should return some results, '
+ 'else the transition won\'t be available. '
+ 'This query may use X and U variables '
+ 'that will respectivly represents '
+ 'the current entity and the current user'))
+
+ require_group = SubjectRelation('EGroup', cardinality='**',
+ description=_('group in which a user should be to be '
+ 'allowed to pass this transition'))
+ transition_of = SubjectRelation('EEType', cardinality='+*',
+ description=_('entity types which may use this transition'),
+ constraints=[RQLConstraint('O final FALSE')])
+ destination_state = SubjectRelation('State', cardinality='?*',
+ constraints=[RQLConstraint('S transition_of ET, O state_of ET')],
+ description=_('destination state for this transition'))
+
+
+class TrInfo(MetaEntityType):
+ from_state = SubjectRelation('State', cardinality='?*')
+ to_state = SubjectRelation('State', cardinality='1*')
+ comment_format = String(meta=True, internationalizable=True, maxsize=50,
+ default='text/rest', constraints=[format_constraint])
+ comment = String(fulltextindexed=True)
+ # get actor and date time using owned_by and creation_date
+
+
+class from_state(MetaRelationType):
+ inlined = True
+class to_state(MetaRelationType):
+ inlined = True
+class wf_info_for(MetaRelationType):
+ """link a transition information to its object"""
+ permissions = {
+ 'read': ('managers', 'users', 'guests',),# RRQLExpression('U has_read_permission O')),
+ 'add': (), # handled automatically, no one should add one explicitly
+ 'delete': ('managers',), # RRQLExpression('U has_delete_permission O')
+ }
+ inlined = True
+ composite = 'object'
+ fulltext_container = composite
+
+class state_of(MetaRelationType):
+ """link a state to one or more entity type"""
+class transition_of(MetaRelationType):
+ """link a transition to one or more entity type"""
+
+class initial_state(MetaRelationType):
+ """indicate which state should be used by default when an entity using
+ states is created
+ """
+ inlined = True
+
+class destination_state(MetaRelationType):
+ """destination state of a transition"""
+ inlined = True
+
+class allowed_transition(MetaRelationType):
+ """allowed transition from this state"""
+
+class in_state(UserRelationType):
+ """indicate the current state of an entity"""
+ meta = True
+ # not inlined intentionnaly since when using ldap sources, user'state
+ # has to be stored outside the EUser table
+
+ # add/delete perms given to managers/users, after what most of the job
+ # is done by workflow enforcment
+
diff -r 93447d75c4b9 -r 6a25c58a1c23 selectors.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/selectors.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,1103 @@
+"""This file contains some basic selectors required by application objects.
+
+A selector is responsible to score how well an object may be used with a
+given context by returning a score.
+
+In CubicWeb Usually the context consists for a request object, a result set
+or None, a specific row/col in the result set, etc...
+
+
+If you have trouble with selectors, especially if the objet (typically
+a view or a component) you want to use is not selected and you want to
+know which one(s) of its selectors fail (e.g. returns 0), you can use
+`traced_selection` or even direclty `TRACED_OIDS`.
+
+`TRACED_OIDS` is a tuple of traced object ids. The special value
+'all' may be used to log selectors for all objects.
+
+For instance, say that the following code yields a `NoSelectableObject`
+exception::
+
+ self.view('calendar', myrset)
+
+You can log the selectors involved for *calendar* by replacing the line
+above by::
+
+ # in Python2.5
+ from cubicweb.selectors import traced_selection
+ with traced_selection():
+ self.view('calendar', myrset)
+
+ # in Python2.4
+ from cubicweb import selectors
+ selectors.TRACED_OIDS = ('calendar',)
+ self.view('calendar', myrset)
+ selectors.TRACED_OIDS = ()
+
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+
+__docformat__ = "restructuredtext en"
+
+import logging
+from warnings import warn
+
+from logilab.common.compat import all
+from logilab.common.deprecation import deprecated_function
+from logilab.common.interface import implements as implements_iface
+
+from yams import BASE_TYPES
+
+from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, role
+from cubicweb.vregistry import (NoSelectableObject, Selector,
+ chainall, chainfirst, objectify_selector)
+from cubicweb.cwconfig import CubicWebConfiguration
+from cubicweb.schema import split_expression
+
+# helpers for debugging selectors
+SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
+TRACED_OIDS = ()
+
+def lltrace(selector):
+ # don't wrap selectors if not in development mode
+ if CubicWebConfiguration.mode == 'installed':
+ return selector
+ def traced(cls, *args, **kwargs):
+ # /!\ lltrace decorates pure function or __call__ method, this
+ # means argument order may be different
+ if isinstance(cls, Selector):
+ selname = str(cls)
+ vobj = args[0]
+ else:
+ selname = selector.__name__
+ vobj = cls
+ oid = vobj.id
+ ret = selector(cls, *args, **kwargs)
+ if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
+ #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
+ print 'selector %s returned %s for %s' % (selname, ret, vobj)
+ return ret
+ traced.__name__ = selector.__name__
+ return traced
+
+class traced_selection(object):
+ """selector debugging helper.
+
+ Typical usage is :
+
+ >>> with traced_selection():
+ ... # some code in which you want to debug selectors
+ ... # for all objects
+
+ or
+
+ >>> with traced_selection( ('oid1', 'oid2') ):
+ ... # some code in which you want to debug selectors
+ ... # for objects with id 'oid1' and 'oid2'
+
+ """
+ def __init__(self, traced='all'):
+ self.traced = traced
+
+ def __enter__(self):
+ global TRACED_OIDS
+ TRACED_OIDS = self.traced
+
+ def __exit__(self, exctype, exc, traceback):
+ global TRACED_OIDS
+ TRACED_OIDS = ()
+ return traceback is None
+
+
+# abstract selectors ##########################################################
+class PartialSelectorMixIn(object):
+ """convenience mix-in for selectors that will look into the containing
+ class to find missing information.
+
+ cf. `cubicweb.web.action.LinkToEntityAction` for instance
+ """
+ def __call__(self, cls, *args, **kwargs):
+ self.complete(cls)
+ return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
+
+class EClassSelector(Selector):
+ """abstract class for selectors working on the entity classes of the result
+ set. Its __call__ method has the following behaviour:
+
+ * if row is specified, return the score returned by the score_class method
+ called with the entity class found in the specified cell
+ * else return the sum of score returned by the score_class method for each
+ entity type found in the specified column, unless:
+ - `once_is_enough` is True, in which case the first non-zero score is
+ returned
+ - `once_is_enough` is False, in which case if score_class return 0, 0 is
+ returned
+ """
+ def __init__(self, once_is_enough=False):
+ self.once_is_enough = once_is_enough
+
+ @lltrace
+ def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+ if not rset:
+ return 0
+ score = 0
+ if row is None:
+ for etype in rset.column_types(col):
+ if etype is None: # outer join
+ continue
+ escore = self.score(cls, req, etype)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ else:
+ etype = rset.description[row][col]
+ if etype is not None:
+ score = self.score(cls, req, etype)
+ return score
+
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return self.score_class(cls.vreg.etype_class(etype), req)
+
+ def score_class(self, eclass, req):
+ raise NotImplementedError()
+
+
+class EntitySelector(EClassSelector):
+ """abstract class for selectors working on the entity instances of the
+ result set. Its __call__ method has the following behaviour:
+
+ * if row is specified, return the score returned by the score_entity method
+ called with the entity instance found in the specified cell
+ * else return the sum of score returned by the score_entity method for each
+ entity found in the specified column, unless:
+ - `once_is_enough` is True, in which case the first non-zero score is
+ returned
+ - `once_is_enough` is False, in which case if score_class return 0, 0 is
+ returned
+
+ note: None values (resulting from some outer join in the query) are not
+ considered.
+ """
+
+ @lltrace
+ def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+ if not rset:
+ return 0
+ score = 0
+ if row is None:
+ for row, rowvalue in enumerate(rset.rows):
+ if rowvalue[col] is None: # outer join
+ continue
+ escore = self.score(req, rset, row, col)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ else:
+ etype = rset.description[row][col]
+ if etype is not None: # outer join
+ score = self.score(req, rset, row, col)
+ return score
+
+ def score(self, req, rset, row, col):
+ try:
+ return self.score_entity(rset.get_entity(row, col))
+ except NotAnEntity:
+ return 0
+
+ def score_entity(self, entity):
+ raise NotImplementedError()
+
+
+# very basic selectors ########################################################
+@objectify_selector
+def yes(cls, *args, **kwargs):
+ """accept everything"""
+ return 1
+
+@objectify_selector
+@lltrace
+def none_rset(cls, req, rset, *args, **kwargs):
+ """accept no result set (e.g. given rset is None)"""
+ if rset is None:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def any_rset(cls, req, rset, *args, **kwargs):
+ """accept result set, whatever the number of result it contains"""
+ if rset is not None:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def nonempty_rset(cls, req, rset, *args, **kwargs):
+ """accept any non empty result set"""
+ if rset is not None and rset.rowcount:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def empty_rset(cls, req, rset, *args, **kwargs):
+ """accept empty result set"""
+ if rset is not None and rset.rowcount == 0:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def one_line_rset(cls, req, rset, row=None, *args, **kwargs):
+ """if row is specified, accept result set with a single line of result,
+ else accepts anyway
+ """
+ if rset is not None and (row is not None or rset.rowcount == 1):
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def two_lines_rset(cls, req, rset, *args, **kwargs):
+ """accept result set with *at least* two lines of result"""
+ if rset is not None and rset.rowcount > 1:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def two_cols_rset(cls, req, rset, *args, **kwargs):
+ """accept result set with at least one line and two columns of result"""
+ if rset is not None and rset.rowcount and len(rset.rows[0]) > 1:
+ return 1
+ return 0
+
+@objectify_selector
+@lltrace
+def paginated_rset(cls, req, rset, *args, **kwargs):
+ """accept result set with more lines than the page size.
+
+ Page size is searched in (respecting order):
+ * a page_size argument
+ * a page_size form parameters
+ * the navigation.page-size property
+ """
+ page_size = kwargs.get('page_size')
+ if page_size is None:
+ page_size = req.form.get('page_size')
+ if page_size is None:
+ page_size = req.property_value('navigation.page-size')
+ else:
+ page_size = int(page_size)
+ if rset is None or rset.rowcount <= page_size:
+ return 0
+ return 1
+
+@objectify_selector
+@lltrace
+def sorted_rset(cls, req, rset, row=None, col=0, **kwargs):
+ """accept sorted result set"""
+ rqlst = rset.syntax_tree()
+ if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
+ return 0
+ return 2
+
+@objectify_selector
+@lltrace
+def one_etype_rset(cls, req, rset, row=None, col=0, *args, **kwargs):
+ """accept result set where entities in the specified column (or 0) are all
+ of the same type
+ """
+ if rset is None:
+ return 0
+ if len(rset.column_types(col)) != 1:
+ return 0
+ return 1
+
+@objectify_selector
+@lltrace
+def two_etypes_rset(cls, req, rset, row=None, col=0, **kwargs):
+ """accept result set where entities in the specified column (or 0) are not
+ of the same type
+ """
+ if rset:
+ etypes = rset.column_types(col)
+ if len(etypes) > 1:
+ return 1
+ return 0
+
+class non_final_entity(EClassSelector):
+ """accept if entity type found in the result set is non final.
+
+ See `EClassSelector` documentation for behaviour when row is not specified.
+ """
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return 1
+
+@objectify_selector
+@lltrace
+def authenticated_user(cls, req, *args, **kwargs):
+ """accept if user is anonymous"""
+ if req.cnx.anonymous_connection:
+ return 0
+ return 1
+
+def anonymous_user():
+ return ~ authenticated_user()
+
+@objectify_selector
+@lltrace
+def primary_view(cls, req, rset, row=None, col=0, view=None, **kwargs):
+ """accept if view given as named argument is a primary view, or if no view
+ is given
+ """
+ if view is not None and not view.is_primary():
+ return 0
+ return 1
+
+@objectify_selector
+@lltrace
+def match_context_prop(cls, req, rset, row=None, col=0, context=None,
+ **kwargs):
+ """accept if:
+ * no context given
+ * context (`basestring`) is matching the context property value for the
+ given cls
+ """
+ propval = req.property_value('%s.%s.context' % (cls.__registry__, cls.id))
+ if not propval:
+ propval = cls.context
+ if context is not None and propval and context != propval:
+ return 0
+ return 1
+
+
+class match_search_state(Selector):
+ """accept if the current request search state is in one of the expected
+ states given to the initializer
+
+ :param expected: either 'normal' or 'linksearch' (eg searching for an
+ object to create a relation with another)
+ """
+ def __init__(self, *expected):
+ assert expected, self
+ self.expected = frozenset(expected)
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(sorted(str(s) for s in self.expected)))
+
+ @lltrace
+ def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+ try:
+ if not req.search_state[0] in self.expected:
+ return 0
+ except AttributeError:
+ return 1 # class doesn't care about search state, accept it
+ return 1
+
+
+class match_form_params(match_search_state):
+ """accept if parameters specified as initializer arguments are specified
+ in request's form parameters
+
+ :param *expected: parameters (eg `basestring`) which are expected to be
+ found in request's form parameters
+ """
+
+ @lltrace
+ def __call__(self, cls, req, *args, **kwargs):
+ score = 0
+ for param in self.expected:
+ val = req.form.get(param)
+ if not val:
+ return 0
+ score += 1
+ return len(self.expected)
+
+
+class match_kwargs(match_search_state):
+ """accept if parameters specified as initializer arguments are specified
+ in named arguments given to the selector
+
+ :param *expected: parameters (eg `basestring`) which are expected to be
+ found in named arguments (kwargs)
+ """
+
+ @lltrace
+ def __call__(self, cls, req, *args, **kwargs):
+ for arg in self.expected:
+ if not arg in kwargs:
+ return 0
+ return len(self.expected)
+
+
+class match_user_groups(match_search_state):
+ """accept if logged users is in at least one of the given groups. Returned
+ score is the number of groups in which the user is.
+
+ If the special 'owners' group is given:
+ * if row is specified check the entity at the given row/col is owned by the
+ logged user
+ * if row is not specified check all entities in col are owned by the logged
+ user
+
+ :param *required_groups: name of groups (`basestring`) in which the logged
+ user should be
+ """
+
+ @lltrace
+ def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ user = req.user
+ if user is None:
+ return int('guests' in self.expected)
+ score = user.matching_groups(self.expected)
+ if not score and 'owners' in self.expected and rset:
+ if row is not None:
+ if not user.owns(rset[row][col]):
+ return 0
+ score = 1
+ else:
+ score = all(user.owns(r[col]) for r in rset)
+ return score
+
+
+class appobject_selectable(Selector):
+ """accept with another appobject is selectable using selector's input
+ context.
+
+ :param registry: a registry name (`basestring`)
+ :param oid: an object identifier (`basestring`)
+ """
+ def __init__(self, registry, oid):
+ self.registry = registry
+ self.oid = oid
+
+ def __call__(self, cls, req, rset, *args, **kwargs):
+ try:
+ cls.vreg.select_object(self.registry, self.oid, req, rset, *args, **kwargs)
+ return 1
+ except NoSelectableObject:
+ return 0
+
+
+# not so basic selectors ######################################################
+
+class implements(EClassSelector):
+ """accept if entity class found in the result set implements at least one
+ of the interfaces given as argument. Returned score is the number of
+ implemented interfaces.
+
+ See `EClassSelector` documentation for behaviour when row is not specified.
+
+ :param *expected_ifaces: expected interfaces. An interface may be a class
+ or an entity type (e.g. `basestring`) in which case
+ the associated class will be searched in the
+ registry (at selection time)
+
+ note: when interface is an entity class, the score will reflect class
+ proximity so the most specific object'll be selected
+ """
+ def __init__(self, *expected_ifaces):
+ super(implements, self).__init__()
+ self.expected_ifaces = expected_ifaces
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected_ifaces))
+
+ def score_class(self, eclass, req):
+ score = 0
+ for iface in self.expected_ifaces:
+ if isinstance(iface, basestring):
+ # entity type
+ iface = eclass.vreg.etype_class(iface)
+ if implements_iface(eclass, iface):
+ if getattr(iface, '__registry__', None) == 'etypes':
+ # adjust score if the interface is an entity class
+ if iface is eclass:
+ score += len(eclass.e_schema.ancestors()) + 4
+ else:
+ parents = [e.type for e in eclass.e_schema.ancestors()]
+ for index, etype in enumerate(reversed(parents)):
+ basecls = eclass.vreg.etype_class(etype)
+ if iface is basecls:
+ score += index + 3
+ break
+ else: # Any
+ score += 1
+ else:
+ # implenting an interface takes precedence other special Any
+ # interface
+ score += 2
+ return score
+
+
+class specified_etype_implements(implements):
+ """accept if entity class specified using an 'etype' parameters in name
+ argument or request form implements at least one of the interfaces given as
+ argument. Returned score is the number of implemented interfaces.
+
+ :param *expected_ifaces: expected interfaces. An interface may be a class
+ or an entity type (e.g. `basestring`) in which case
+ the associated class will be searched in the
+ registry (at selection time)
+
+ note: when interface is an entity class, the score will reflect class
+ proximity so the most specific object'll be selected
+ """
+
+ @lltrace
+ def __call__(self, cls, req, *args, **kwargs):
+ try:
+ etype = req.form['etype']
+ except KeyError:
+ try:
+ etype = kwargs['etype']
+ except KeyError:
+ return 0
+ return self.score_class(cls.vreg.etype_class(etype), req)
+
+
+class relation_possible(EClassSelector):
+ """accept if entity class found in the result set support the relation.
+
+ See `EClassSelector` documentation for behaviour when row is not specified.
+
+ :param rtype: a relation type (`basestring`)
+ :param role: the role of the result set entity in the relation. 'subject' or
+ 'object', default to 'subject'.
+ :param target_type: if specified, check the relation's end may be of this
+ target type (`basestring`)
+ :param action: a relation schema action (one of 'read', 'add', 'delete')
+ which must be granted to the logged user, else a 0 score will
+ be returned
+ """
+ def __init__(self, rtype, role='subject', target_etype=None,
+ action='read', once_is_enough=False):
+ super(relation_possible, self).__init__(once_is_enough)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+ self.action = action
+
+ @lltrace
+ def __call__(self, cls, req, *args, **kwargs):
+ rschema = cls.schema.rschema(self.rtype)
+ if not (rschema.has_perm(req, self.action)
+ or rschema.has_local_role(self.action)):
+ return 0
+ score = super(relation_possible, self).__call__(cls, req, *args, **kwargs)
+ return score
+
+ def score_class(self, eclass, req):
+ eschema = eclass.e_schema
+ try:
+ if self.role == 'object':
+ rschema = eschema.object_relation(self.rtype)
+ else:
+ rschema = eschema.subject_relation(self.rtype)
+ except KeyError:
+ return 0
+ if self.target_etype is not None:
+ try:
+ if self.role == 'subject':
+ return int(self.target_etype in rschema.objects(eschema))
+ else:
+ return int(self.target_etype in rschema.subjects(eschema))
+ except KeyError, ex:
+ return 0
+ return 1
+
+
+class partial_relation_possible(PartialSelectorMixIn, relation_possible):
+ """partial version of the relation_possible selector
+
+ The selector will look for class attributes to find its missing
+ information. The list of attributes required on the class
+ for this selector are:
+
+ - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+
+ - `role`: this attribute will be passed to the `cubicweb.role` function
+ to determine the role of class in the relation
+
+ - `etype` (optional): the entity type on the other side of the relation
+
+ :param action: a relation schema action (one of 'read', 'add', 'delete')
+ which must be granted to the logged user, else a 0 score will
+ be returned
+ """
+ def __init__(self, action='read', once_is_enough=False):
+ super(partial_relation_possible, self).__init__(None, None, None,
+ action, once_is_enough)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'etype', None)
+
+
+class has_editable_relation(EntitySelector):
+ """accept if some relations for an entity found in the result set is
+ editable by the logged user.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+ """
+
+ def score_entity(self, entity):
+ # if user has no update right but it can modify some relation,
+ # display action anyway
+ for dummy in entity.srelations_by_category(('generic', 'metadata'),
+ 'add'):
+ return 1
+ for rschema, targetschemas, role in entity.relations_by_category(
+ ('primary', 'secondary'), 'add'):
+ if not rschema.is_final():
+ return 1
+ return 0
+
+
+class may_add_relation(EntitySelector):
+ """accept if the relation can be added to an entity found in the result set
+ by the logged user.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+
+ :param rtype: a relation type (`basestring`)
+ :param role: the role of the result set entity in the relation. 'subject' or
+ 'object', default to 'subject'.
+ """
+
+ def __init__(self, rtype, role='subject', once_is_enough=False):
+ super(may_add_relation, self).__init__(once_is_enough)
+ self.rtype = rtype
+ self.role = role
+
+ def score_entity(self, entity):
+ rschema = entity.schema.rschema(self.rtype)
+ if self.role == 'subject':
+ if not rschema.has_perm(entity.req, 'add', fromeid=entity.eid):
+ return 0
+ elif not rschema.has_perm(entity.req, 'add', toeid=entity.eid):
+ return 0
+ return 1
+
+
+class partial_may_add_relation(PartialSelectorMixIn, may_add_relation):
+ """partial version of the may_add_relation selector
+
+ The selector will look for class attributes to find its missing
+ information. The list of attributes required on the class
+ for this selector are:
+
+ - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+
+ - `role`: this attribute will be passed to the `cubicweb.role` function
+ to determine the role of class in the relation.
+
+ :param action: a relation schema action (one of 'read', 'add', 'delete')
+ which must be granted to the logged user, else a 0 score will
+ be returned
+ """
+ def __init__(self, once_is_enough=False):
+ super(partial_may_add_relation, self).__init__(None, None, once_is_enough)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+
+
+class has_related_entities(EntitySelector):
+ """accept if entity found in the result set has some linked entities using
+ the specified relation (optionaly filtered according to the specified target
+ type). Checks first if the relation is possible.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+
+ :param rtype: a relation type (`basestring`)
+ :param role: the role of the result set entity in the relation. 'subject' or
+ 'object', default to 'subject'.
+ :param target_type: if specified, check the relation's end may be of this
+ target type (`basestring`)
+ """
+ def __init__(self, rtype, role='subject', target_etype=None,
+ once_is_enough=False):
+ super(has_related_entities, self).__init__(once_is_enough)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+
+ def score_entity(self, entity):
+ relpossel = relation_possible(self.rtype, self.role, self.target_etype)
+ if not relpossel.score_class(entity.__class__, entity.req):
+ return 0
+ rset = entity.related(self.rtype, self.role)
+ if self.target_etype:
+ return any(x for x, in rset.description if x == self.target_etype)
+ return rset and 1 or 0
+
+
+class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
+ """partial version of the has_related_entities selector
+
+ The selector will look for class attributes to find its missing
+ information. The list of attributes required on the class
+ for this selector are:
+
+ - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+
+ - `role`: this attribute will be passed to the `cubicweb.role` function
+ to determine the role of class in the relation.
+
+ - `etype` (optional): the entity type on the other side of the relation
+
+ :param action: a relation schema action (one of 'read', 'add', 'delete')
+ which must be granted to the logged user, else a 0 score will
+ be returned
+ """
+ def __init__(self, once_is_enough=False):
+ super(partial_has_related_entities, self).__init__(None, None,
+ None, once_is_enough)
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'etype', None)
+
+
+class has_permission(EntitySelector):
+ """accept if user has the permission to do the requested action on a result
+ set entity.
+
+ * if row is specified, return 1 if user has the permission on the entity
+ instance found in the specified cell
+ * else return a positive score if user has the permission for every entity
+ in the found in the specified column
+
+ note: None values (resulting from some outer join in the query) are not
+ considered.
+
+ :param action: an entity schema action (eg 'read'/'add'/'delete'/'update')
+ """
+ def __init__(self, action, once_is_enough=False):
+ super(has_permission, self).__init__(once_is_enough)
+ self.action = action
+
+ @lltrace
+ def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+ if rset is None:
+ return 0
+ user = req.user
+ action = self.action
+ if row is None:
+ score = 0
+ need_local_check = []
+ geteschema = cls.schema.eschema
+ for etype in rset.column_types(0):
+ if etype in BASE_TYPES:
+ return 0
+ eschema = geteschema(etype)
+ if not user.matching_groups(eschema.get_groups(action)):
+ if eschema.has_local_role(action):
+ # have to ckeck local roles
+ need_local_check.append(eschema)
+ continue
+ else:
+ # even a local role won't be enough
+ return 0
+ score += 1
+ if need_local_check:
+ # check local role for entities of necessary types
+ for i, row in enumerate(rset):
+ if not rset.description[i][0] in need_local_check:
+ continue
+ if not self.score(req, rset, i, col):
+ return 0
+ score += 1
+ return score
+ return self.score(req, rset, row, col)
+
+ def score_entity(self, entity):
+ if entity.has_perm(self.action):
+ return 1
+ return 0
+
+
+class has_add_permission(EClassSelector):
+ """accept if logged user has the add permission on entity class found in the
+ result set, and class is not a strict subobject.
+
+ See `EClassSelector` documentation for behaviour when row is not specified.
+ """
+ def score(self, cls, req, etype):
+ eschema = cls.schema.eschema(etype)
+ if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
+ and eschema.has_perm(req, 'add'):
+ return 1
+ return 0
+
+
+class rql_condition(EntitySelector):
+ """accept if an arbitrary rql return some results for an eid found in the
+ result set. Returned score is the number of items returned by the rql
+ condition.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+
+ :param expression: basestring containing an rql expression, which should use
+ X variable to represent the context entity and may use U
+ to represent the logged user
+
+ return the sum of the number of items returned by the rql condition as score
+ or 0 at the first entity scoring to zero.
+ """
+ def __init__(self, expression, once_is_enough=False):
+ super(rql_condition, self).__init__(once_is_enough)
+ if 'U' in frozenset(split_expression(expression)):
+ rql = 'Any X WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
+ else:
+ rql = 'Any X WHERE X eid %%(x)s, %s' % expression
+ self.rql = rql
+
+ def score(self, req, rset, row, col):
+ try:
+ return len(req.execute(self.rql, {'x': rset[row][col],
+ 'u': req.user.eid}, 'x'))
+ except Unauthorized:
+ return 0
+
+
+class but_etype(EntitySelector):
+ """accept if the given entity types are not found in the result set.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+
+ :param *etypes: entity types (`basestring`) which should be refused
+ """
+ def __init__(self, *etypes):
+ super(but_etype, self).__init__()
+ self.but_etypes = etypes
+
+ def score(self, req, rset, row, col):
+ if rset.description[row][col] in self.but_etypes:
+ return 0
+ return 1
+
+
+class score_entity(EntitySelector):
+ """accept if some arbitrary function return a positive score for an entity
+ found in the result set.
+
+ See `EntitySelector` documentation for behaviour when row is not specified.
+
+ :param scorefunc: callable expected to take an entity as argument and to
+ return a score >= 0
+ """
+ def __init__(self, scorefunc, once_is_enough=False):
+ super(EntitySelector, self).__init__(once_is_enough)
+ self.score_entity = scorefunc
+
+
+# XXX DEPRECATED ##############################################################
+
+yes_selector = deprecated_function(yes)
+norset_selector = deprecated_function(none_rset)
+rset_selector = deprecated_function(any_rset)
+anyrset_selector = deprecated_function(nonempty_rset)
+emptyrset_selector = deprecated_function(empty_rset)
+onelinerset_selector = deprecated_function(one_line_rset)
+twolinerset_selector = deprecated_function(two_lines_rset)
+twocolrset_selector = deprecated_function(two_cols_rset)
+largerset_selector = deprecated_function(paginated_rset)
+sortedrset_selector = deprecated_function(sorted_rset)
+oneetyperset_selector = deprecated_function(one_etype_rset)
+multitype_selector = deprecated_function(two_etypes_rset)
+anonymous_selector = deprecated_function(anonymous_user)
+not_anonymous_selector = deprecated_function(authenticated_user)
+primaryview_selector = deprecated_function(primary_view)
+contextprop_selector = deprecated_function(match_context_prop)
+
+def nfentity_selector(cls, req, rset, row=None, col=0, **kwargs):
+ return non_final_entity()(cls, req, rset, row, col)
+nfentity_selector = deprecated_function(nfentity_selector)
+
+def implement_interface(cls, req, rset, row=None, col=0, **kwargs):
+ return implements(*cls.accepts_interfaces)(cls, req, rset, row, col)
+_interface_selector = deprecated_function(implement_interface)
+interface_selector = deprecated_function(implement_interface)
+implement_interface = deprecated_function(implement_interface, 'use implements')
+
+def accept_etype(cls, req, *args, **kwargs):
+ """check etype presence in request form *and* accepts conformance"""
+ return specified_etype_implements(*cls.accepts)(cls, req, *args)
+etype_form_selector = deprecated_function(accept_etype)
+accept_etype = deprecated_function(accept_etype, 'use specified_etype_implements')
+
+def searchstate_selector(cls, req, rset, row=None, col=0, **kwargs):
+ return match_search_state(cls.search_states)(cls, req, rset, row, col)
+searchstate_selector = deprecated_function(searchstate_selector)
+
+def match_user_group(cls, req, rset=None, row=None, col=0, **kwargs):
+ return match_user_groups(*cls.require_groups)(cls, req, rset, row, col, **kwargs)
+in_group_selector = deprecated_function(match_user_group)
+match_user_group = deprecated_function(match_user_group)
+
+def has_relation(cls, req, rset, row=None, col=0, **kwargs):
+ return relation_possible(cls.rtype, role(cls), cls.etype,
+ getattr(cls, 'require_permission', 'read'))(cls, req, rset, row, col, **kwargs)
+has_relation = deprecated_function(has_relation)
+
+def one_has_relation(cls, req, rset, row=None, col=0, **kwargs):
+ return relation_possible(cls.rtype, role(cls), cls.etype,
+ getattr(cls, 'require_permission', 'read',
+ once_is_enough=True))(cls, req, rset, row, col, **kwargs)
+one_has_relation = deprecated_function(one_has_relation, 'use relation_possible selector')
+
+def accept_rset(cls, req, rset, row=None, col=0, **kwargs):
+ """simply delegate to cls.accept_rset method"""
+ return implements(*cls.accepts)(cls, req, rset, row=row, col=col)
+accept_rset_selector = deprecated_function(accept_rset)
+accept_rset = deprecated_function(accept_rset, 'use implements selector')
+
+accept = chainall(non_final_entity(), accept_rset, name='accept')
+accept_selector = deprecated_function(accept)
+accept = deprecated_function(accept, 'use implements selector')
+
+accept_one = deprecated_function(chainall(one_line_rset, accept,
+ name='accept_one'))
+accept_one_selector = deprecated_function(accept_one)
+
+
+def _rql_condition(cls, req, rset, row=None, col=0, **kwargs):
+ if cls.condition:
+ return rql_condition(cls.condition)(cls, req, rset, row, col)
+ return 1
+_rqlcondition_selector = deprecated_function(_rql_condition)
+
+rqlcondition_selector = deprecated_function(chainall(non_final_entity(), one_line_rset, _rql_condition,
+ name='rql_condition'))
+
+def but_etype_selector(cls, req, rset, row=None, col=0, **kwargs):
+ return but_etype(cls.etype)(cls, req, rset, row, col)
+but_etype_selector = deprecated_function(but_etype_selector)
+
+@lltrace
+def etype_rtype_selector(cls, req, rset, row=None, col=0, **kwargs):
+ schema = cls.schema
+ perm = getattr(cls, 'require_permission', 'read')
+ if hasattr(cls, 'etype'):
+ eschema = schema.eschema(cls.etype)
+ if not (eschema.has_perm(req, perm) or eschema.has_local_role(perm)):
+ return 0
+ if hasattr(cls, 'rtype'):
+ rschema = schema.rschema(cls.rtype)
+ if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
+ return 0
+ return 1
+etype_rtype_selector = deprecated_function(etype_rtype_selector)
+
+#req_form_params_selector = deprecated_function(match_form_params) # form_params
+#kwargs_selector = deprecated_function(match_kwargs) # expected_kwargs
+
+# compound selectors ##########################################################
+
+searchstate_accept = chainall(nonempty_rset(), accept,
+ name='searchstate_accept')
+searchstate_accept_selector = deprecated_function(searchstate_accept)
+
+searchstate_accept_one = chainall(one_line_rset, accept, _rql_condition,
+ name='searchstate_accept_one')
+searchstate_accept_one_selector = deprecated_function(searchstate_accept_one)
+
+searchstate_accept = deprecated_function(searchstate_accept)
+searchstate_accept_one = deprecated_function(searchstate_accept_one)
+
+
+def unbind_method(selector):
+ def new_selector(registered):
+ # get the unbound method
+ if hasattr(registered, 'im_func'):
+ registered = registered.im_func
+ # don't rebind since it will be done automatically during
+ # the assignment, inside the destination class body
+ return selector(registered)
+ new_selector.__name__ = selector.__name__
+ return new_selector
+
+
+def deprecate(registered, msg):
+ # get the unbound method
+ if hasattr(registered, 'im_func'):
+ registered = registered.im_func
+ def _deprecate(cls, vreg):
+ warn(msg, DeprecationWarning)
+ return registered(cls, vreg)
+ return _deprecate
+
+@unbind_method
+def require_group_compat(registered):
+ def plug_selector(cls, vreg):
+ cls = registered(cls, vreg)
+ if getattr(cls, 'require_groups', None):
+ warn('use "match_user_groups(group1, group2)" instead of using require_groups',
+ DeprecationWarning)
+ cls.__select__ &= match_user_groups(cls.require_groups)
+ return cls
+ return plug_selector
+
+@unbind_method
+def accepts_compat(registered):
+ def plug_selector(cls, vreg):
+ cls = registered(cls, vreg)
+ if getattr(cls, 'accepts', None):
+ warn('use "implements("EntityType", IFace)" instead of using accepts',
+ DeprecationWarning)
+ cls.__select__ &= implements(*cls.accepts)
+ return cls
+ return plug_selector
+
+@unbind_method
+def accepts_etype_compat(registered):
+ def plug_selector(cls, vreg):
+ cls = registered(cls, vreg)
+ if getattr(cls, 'accepts', None):
+ warn('use "specified_etype_implements("EntityType", IFace)" instead of using accepts',
+ DeprecationWarning)
+ cls.__select__ &= specified_etype_implements(*cls.accepts)
+ return cls
+ return plug_selector
+
+@unbind_method
+def condition_compat(registered):
+ def plug_selector(cls, vreg):
+ cls = registered(cls, vreg)
+ if getattr(cls, 'condition', None):
+ warn('use "use rql_condition(expression)" instead of using condition',
+ DeprecationWarning)
+ cls.__select__ &= rql_condition(cls.condition)
+ return cls
+ return plug_selector
+
+@unbind_method
+def has_relation_compat(registered):
+ def plug_selector(cls, vreg):
+ cls = registered(cls, vreg)
+ if getattr(cls, 'etype', None):
+ warn('use relation_possible selector instead of using etype_rtype',
+ DeprecationWarning)
+ cls.__select__ &= relation_possible(cls.rtype, role(cls),
+ getattr(cls, 'etype', None),
+ action=getattr(cls, 'require_permission', 'read'))
+ return cls
+ return plug_selector
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/checkintegrity.py
--- a/server/checkintegrity.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/checkintegrity.py Mon Mar 02 21:03:54 2009 +0100
@@ -84,7 +84,7 @@
', '.join(sorted(str(e) for e in etypes))
pb = ProgressBar(len(etypes) + 1)
# first monkey patch Entity.check to disable validation
- from cubicweb.common.entity import Entity
+ from cubicweb.entity import Entity
_check = Entity.check
Entity.check = lambda self, creation=False: True
# clear fti table first
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/hooksmanager.py
--- a/server/hooksmanager.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/hooksmanager.py Mon Mar 02 21:03:54 2009 +0100
@@ -23,7 +23,7 @@
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -180,13 +180,12 @@
# self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
# self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-from cubicweb.vregistry import autoselectors
-from cubicweb.common.appobject import AppObject
-from cubicweb.common.registerers import accepts_registerer, yes_registerer
-from cubicweb.common.selectors import yes
+from cubicweb.selectors import yes
+from cubicweb.appobject import AppObject
-class autoid(autoselectors):
+class autoid(type):
"""metaclass to create an unique 'id' attribute on the class using it"""
+ # XXX is this metaclass really necessary ?
def __new__(mcs, name, bases, classdict):
cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
cls.id = str(id(cls))
@@ -195,8 +194,7 @@
class Hook(AppObject):
__metaclass__ = autoid
__registry__ = 'hooks'
- __registerer__ = accepts_registerer
- __selectors__ = (yes,)
+ __select__ = yes()
# set this in derivated classes
events = None
accepts = None
@@ -245,7 +243,6 @@
raise NotImplementedError
class SystemHook(Hook):
- __registerer__ = yes_registerer
accepts = ('',)
from logging import getLogger
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/msplanner.py
--- a/server/msplanner.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/msplanner.py Mon Mar 02 21:03:54 2009 +0100
@@ -64,7 +64,7 @@
from rql.nodes import VariableRef, Comparison, Relation, Constant, Exists, Variable
from cubicweb import server
-from cubicweb.common.utils import make_uid
+from cubicweb.utils import make_uid
from cubicweb.server.utils import cleanup_solutions
from cubicweb.server.ssplanner import SSPlanner, OneFetchStep, add_types_restriction
from cubicweb.server.mssteps import *
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/repository.py
--- a/server/repository.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/repository.py Mon Mar 02 21:03:54 2009 +0100
@@ -491,6 +491,9 @@
try:
if session.execute('EUser X WHERE X login %(login)s', {'login': login}):
return False
+ if session.execute('EUser X WHERE X use_email C, C address %(login)s',
+ {'login': login}):
+ return False
# we have to create the user
user = self.vreg.etype_class('EUser')(session, None)
if isinstance(password, unicode):
@@ -502,6 +505,11 @@
self.glob_add_entity(session, user)
session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"',
{'x': user.eid})
+ # FIXME this does not work yet
+ if '@' in login:
+ session.execute('INSERT EmailAddress X: X address "%(login)s", '
+ 'U primary_email X, U use_email X WHERE U login "%(login)s"',
+ {'login':login})
session.commit()
finally:
session.close()
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/serverconfig.py
--- a/server/serverconfig.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/serverconfig.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""server.serverconfig definition
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/session.py
--- a/server/session.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/session.py Mon Mar 02 21:03:54 2009 +0100
@@ -18,7 +18,7 @@
from cubicweb import RequestSessionMixIn, Binary
from cubicweb.dbapi import ConnectionProperties
-from cubicweb.common.utils import make_uid
+from cubicweb.utils import make_uid
from cubicweb.server.rqlrewrite import RQLRewriter
_ETYPE_PYOBJ_MAP = { bool: 'Boolean',
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/sources/extlite.py
--- a/server/sources/extlite.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/sources/extlite.py Mon Mar 02 21:03:54 2009 +0100
@@ -203,7 +203,7 @@
"""add a new entity to the source"""
raise NotImplementedError()
- def local_update_entity(self, session, entity):
+ def local_update_entity(self, session, entity, attrs=None):
"""update an entity in the source
This is not provided as update_entity implementation since usually
@@ -211,7 +211,8 @@
and the source implementor may use this method if necessary
"""
cu = session.pool[self.uri]
- attrs = self.sqladapter.preprocess_entity(entity)
+ if attrs is None:
+ attrs = self.sqladapter.preprocess_entity(entity)
sql = self.sqladapter.sqlgen.update(str(entity.e_schema), attrs, ['eid'])
cu.execute(sql, attrs)
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/test/data/schema/Affaire.py
--- a/server/test/data/schema/Affaire.py Fri Feb 27 09:59:53 2009 +0100
+++ b/server/test/data/schema/Affaire.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,6 +1,6 @@
from cubicweb.schema import format_constraint
-class Affaire(EntityType):
+class Affaire(WorkflowableEntityType):
permissions = {
'read': ('managers',
ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
@@ -13,9 +13,6 @@
constraints=[SizeConstraint(16)])
sujet = String(fulltextindexed=True,
constraints=[SizeConstraint(256)])
- in_state = SubjectRelation('State', cardinality='1*',
- constraints=[RQLConstraint('O state_of ET, ET name "Affaire"')],
- description=_('account state'))
descr_format = String(meta=True, internationalizable=True,
default='text/rest', constraints=[format_constraint])
descr = String(fulltextindexed=True,
@@ -23,8 +20,7 @@
duration = Int()
invoiced = Int()
-
- wf_info_for = ObjectRelation('TrInfo', cardinality='1*', composite='object')
+
depends_on = SubjectRelation('Affaire')
require_permission = SubjectRelation('EPermission')
diff -r 93447d75c4b9 -r 6a25c58a1c23 server/test/runtests.py
--- a/server/test/runtests.py Fri Feb 27 09:59:53 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-from logilab.common.testlib import main
-
-if __name__ == '__main__':
- import sys, os
- main(os.path.dirname(sys.argv[0]) or '.')
diff -r 93447d75c4b9 -r 6a25c58a1c23 sobjects/notification.py
--- a/sobjects/notification.py Fri Feb 27 09:59:53 2009 +0100
+++ b/sobjects/notification.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""some hooks and views to handle notification on entity's changes
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -16,12 +16,11 @@
return 'XXX'
from logilab.common.textutils import normalize_text
+from logilab.common.deprecation import class_renamed
from cubicweb import RegistryException
-from cubicweb.common.view import EntityView
-from cubicweb.common.appobject import Component
-from cubicweb.common.registerers import accepts_registerer
-from cubicweb.common.selectors import accept
+from cubicweb.selectors import implements, yes
+from cubicweb.view import EntityView, Component
from cubicweb.common.mail import format_mail
from cubicweb.server.pool import PreCommitOperation
@@ -37,9 +36,7 @@
email addresses specified in the configuration are used
"""
id = 'recipients_finder'
- __registerer__ = accepts_registerer
- __selectors__ = (accept,)
- accepts = ('Any',)
+ __select__ = yes()
user_rql = ('Any X,E,A WHERE X is EUser, X in_state S, S name "activated",'
'X primary_email E, E address A')
@@ -135,13 +132,10 @@
* set a content attribute to define the content of the email (unless you
override call)
"""
- accepts = ()
- id = None
msgid_timestamp = True
def recipients(self):
- finder = self.vreg.select_component('recipients_finder',
- req=self.req, rset=self.rset)
+ finder = self.vreg.select_component('recipients_finder', self.req, self.rset)
return finder.recipients()
def subject(self):
@@ -180,8 +174,7 @@
self._kwargs = kwargs
recipients = self.recipients()
if not recipients:
- self.info('skipping %s%s notification which has no recipients',
- self.id, self.accepts)
+ self.info('skipping %s notification, no recipients', self.id)
return
if not isinstance(recipients[0], tuple):
from warnings import warn
@@ -260,9 +253,16 @@
""")
-class ContentAddedMixIn(object):
- """define emailcontent view for entity types for which you want to be notified
- """
+###############################################################################
+# Actual notification views. #
+# #
+# disable them at the recipients_finder level if you don't want them #
+###############################################################################
+
+# XXX should be based on dc_title/dc_description, no?
+
+class ContentAddedView(NotificationView):
+ __abstract__ = True
id = 'notif_after_add_entity'
msgid_timestamp = False
message = _('new')
@@ -273,33 +273,25 @@
url: %(url)s
"""
-
-###############################################################################
-# Actual notification views. #
-# #
-# disable them at the recipients_finder level if you don't want them #
-###############################################################################
-
-# XXX should be based on dc_title/dc_description, no?
-
-class NormalizedTextView(ContentAddedMixIn, NotificationView):
+
def context(self, **kwargs):
entity = self.entity(0, 0)
content = entity.printable_value(self.content_attr, format='text/plain')
if content:
contentformat = getattr(entity, self.content_attr + '_format', 'text/rest')
content = normalize_text(content, 80, rest=contentformat=='text/rest')
- return super(NormalizedTextView, self).context(content=content, **kwargs)
+ return super(ContentAddedView, self).context(content=content, **kwargs)
def subject(self):
entity = self.entity(0, 0)
return u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
entity.eid, self.user_login())
+NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
-class CardAddedView(NormalizedTextView):
+class CardAddedView(ContentAddedView):
"""get notified from new cards"""
- accepts = ('Card',)
+ __select__ = implements('Card')
content_attr = 'synopsis'
diff -r 93447d75c4b9 -r 6a25c58a1c23 sobjects/supervising.py
--- a/sobjects/supervising.py Fri Feb 27 09:59:53 2009 +0100
+++ b/sobjects/supervising.py Mon Mar 02 21:03:54 2009 +0100
@@ -2,13 +2,14 @@
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
from cubicweb import UnknownEid
-from cubicweb.common.view import ComponentMixIn, StartupView
+from cubicweb.selectors import none_rset
+from cubicweb.view import Component
from cubicweb.common.mail import format_mail
from cubicweb.server.hooksmanager import Hook
from cubicweb.server.hookhelper import SendMailOp
@@ -137,9 +138,10 @@
yield change
-class SupervisionEmailView(ComponentMixIn, StartupView):
+class SupervisionEmailView(Component):
"""view implementing the email API for data changes supervision notification
"""
+ __select__ = none_rset()
id = 'supervision_notif'
def recipients(self):
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/data/bootstrap_cubes
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/bootstrap_cubes Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,1 @@
+file, tag
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/data/bootstrap_packages
--- a/test/data/bootstrap_packages Fri Feb 27 09:59:53 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/data/entities.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/entities.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,15 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+class Personne(AnyEntity):
+ """customized class forne Person entities"""
+ id = 'Personne'
+ fetch_attrs, fetch_order = fetch_config(['nom', 'prenom'])
+ rest_attr = 'nom'
+
+
+class Societe(AnyEntity):
+ id = 'Societe'
+ fetch_attrs = ('nom',)
+
+class Note(AnyEntity):
+ id = 'Note'
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/data/schema.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/schema.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,27 @@
+class Personne(EntityType):
+ nom = String(required=True)
+ prenom = String()
+ type = String()
+ travaille = SubjectRelation('Societe')
+ evaluee = SubjectRelation(('Note', 'Personne'))
+ connait = SubjectRelation('Personne', symetric=True)
+
+class Societe(EntityType):
+ nom = String()
+ evaluee = SubjectRelation('Note')
+
+class Note(EntityType):
+ type = String()
+ ecrit_par = SubjectRelation('Personne')
+
+class SubNote(Note):
+ __specializes_schema__ = True
+ description = String()
+
+class tags(RelationDefinition):
+ subject = 'Tag'
+ object = ('Personne', 'Note')
+
+class evaluee(RelationDefinition):
+ subject = 'EUser'
+ object = 'Note'
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/unittest_cwconfig.py
--- a/test/unittest_cwconfig.py Fri Feb 27 09:59:53 2009 +0100
+++ b/test/unittest_cwconfig.py Mon Mar 02 21:03:54 2009 +0100
@@ -68,7 +68,8 @@
self.assertEquals([unabsolutize(p) for p in self.config.vregistry_path()],
['entities', 'web/views', 'sobjects',
'file/entities.py', 'file/views', 'file/hooks.py',
- 'email/entities.py', 'email/views', 'email/hooks.py'])
+ 'email/entities.py', 'email/views', 'email/hooks.py',
+ 'test/data/entities.py'])
if __name__ == '__main__':
unittest_main()
diff -r 93447d75c4b9 -r 6a25c58a1c23 test/unittest_entity.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_entity.py Mon Mar 02 21:03:54 2009 +0100
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+"""unit tests for cubicweb.web.views.entities module"""
+
+from cubicweb.devtools.apptest import EnvBasedTC
+
+from mx.DateTime import DateTimeType, now
+
+from cubicweb import Binary
+from cubicweb.common.mttransforms import HAS_TAL
+
+class EntityTC(EnvBasedTC):
+
+## def setup_database(self):
+## self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
+## self.add_entity('Task', title=u'fait ca !', description=u'et plus vite', start=now())
+## self.add_entity('Tag', name=u'x')
+## self.add_entity('Link', title=u'perdu', url=u'http://www.perdu.com',
+## embed=False)
+
+ def test_boolean_value(self):
+ e = self.etype_instance('EUser')
+ self.failUnless(e)
+
+ def test_yams_inheritance(self):
+ from entities import Note
+ e = self.etype_instance('SubNote')
+ self.assertIsInstance(e, Note)
+ e2 = self.etype_instance('SubNote')
+ self.assertIs(e.__class__, e2.__class__)
+
+ def test_has_eid(self):
+ e = self.etype_instance('EUser')
+ self.assertEquals(e.eid, None)
+ self.assertEquals(e.has_eid(), False)
+ e.eid = 'X'
+ self.assertEquals(e.has_eid(), False)
+ e.eid = 0
+ self.assertEquals(e.has_eid(), True)
+ e.eid = 2
+ self.assertEquals(e.has_eid(), True)
+
+ def test_copy(self):
+ self.add_entity('Tag', name=u'x')
+ p = self.add_entity('Personne', nom=u'toto')
+ oe = self.add_entity('Note', type=u'x')
+ self.execute('SET T ecrit_par U WHERE T eid %(t)s, U eid %(u)s',
+ {'t': oe.eid, 'u': p.eid}, ('t','u'))
+ self.execute('SET TAG tags X WHERE X eid %(x)s', {'x': oe.eid}, 'x')
+ e = self.add_entity('Note', type=u'z')
+ e.copy_relations(oe.eid)
+ self.assertEquals(len(e.ecrit_par), 1)
+ self.assertEquals(e.ecrit_par[0].eid, p.eid)
+ self.assertEquals(len(e.reverse_tags), 0)
+
+ def test_copy_with_nonmeta_composite_inlined(self):
+ p = self.add_entity('Personne', nom=u'toto')
+ oe = self.add_entity('Note', type=u'x')
+ self.schema['ecrit_par'].set_rproperty('Note', 'Personne', 'composite', 'subject')
+ self.execute('SET T ecrit_par U WHERE T eid %(t)s, U eid %(u)s',
+ {'t': oe.eid, 'u': p.eid}, ('t','u'))
+ e = self.add_entity('Note', type=u'z')
+ e.copy_relations(oe.eid)
+ self.failIf(e.ecrit_par)
+ self.failUnless(oe.ecrit_par)
+
+ def test_copy_with_composite(self):
+ user = self.user()
+ adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
+ e = self.entity('Any X WHERE X eid %(x)s', {'x':user.eid}, 'x')
+ self.assertEquals(e.use_email[0].address, "toto@logilab.org")
+ self.assertEquals(e.use_email[0].eid, adeleid)
+ usereid = self.execute('INSERT EUser X: X login "toto", X upassword "toto", X in_group G, X in_state S '
+ 'WHERE G name "users", S name "activated"')[0][0]
+ e = self.entity('Any X WHERE X eid %(x)s', {'x':usereid}, 'x')
+ e.copy_relations(user.eid)
+ self.failIf(e.use_email)
+ self.failIf(e.primary_email)
+
+ def test_copy_with_non_initial_state(self):
+ user = self.user()
+ eid = self.execute('INSERT EUser X: X login "toto", X upassword %(pwd)s, X in_group G WHERE G name "users"',
+ {'pwd': 'toto'})[0][0]
+ self.commit()
+ self.execute('SET X in_state S WHERE X eid %(x)s, S name "deactivated"', {'x': eid}, 'x')
+ self.commit()
+ eid2 = self.execute('INSERT EUser X: X login "tutu", X upassword %(pwd)s', {'pwd': 'toto'})[0][0]
+ e = self.entity('Any X WHERE X eid %(x)s', {'x': eid2}, 'x')
+ e.copy_relations(eid)
+ self.commit()
+ e.clear_related_cache('in_state', 'subject')
+ self.assertEquals(e.state, 'activated')
+
+ def test_related_cache_both(self):
+ user = self.entity('Any X WHERE X eid %(x)s', {'x':self.user().eid}, 'x')
+ adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
+ self.commit()
+ self.assertEquals(user._related_cache.keys(), [])
+ email = user.primary_email[0]
+ self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
+ self.assertEquals(email._related_cache.keys(), ['primary_email_object'])
+ groups = user.in_group
+ self.assertEquals(sorted(user._related_cache), ['in_group_subject', 'primary_email_subject'])
+ for group in groups:
+ self.failIf('in_group_subject' in group._related_cache, group._related_cache.keys())
+
+ def test_related_limit(self):
+ p = self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
+ for tag in u'abcd':
+ self.add_entity('Tag', name=tag)
+ self.execute('SET X tags Y WHERE X is Tag, Y is Personne')
+ self.assertEquals(len(p.related('tags', 'object', limit=2)), 2)
+ self.assertEquals(len(p.related('tags', 'object')), 4)
+
+
+ def test_fetch_rql(self):
+ user = self.user()
+ Personne = self.vreg.etype_class('Personne')
+ Societe = self.vreg.etype_class('Societe')
+ Note = self.vreg.etype_class('Note')
+ peschema = Personne.e_schema
+ seschema = Societe.e_schema
+ peschema.subject_relation('travaille').set_rproperty(peschema, seschema, 'cardinality', '1*')
+ peschema.subject_relation('connait').set_rproperty(peschema, peschema, 'cardinality', '11')
+ peschema.subject_relation('evaluee').set_rproperty(peschema, Note.e_schema, 'cardinality', '1*')
+ seschema.subject_relation('evaluee').set_rproperty(seschema, Note.e_schema, 'cardinality', '1*')
+ # testing basic fetch_attrs attribute
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB,AC ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X modification_date AC')
+ pfetch_attrs = Personne.fetch_attrs
+ sfetch_attrs = Societe.fetch_attrs
+ try:
+ # testing unknown attributes
+ Personne.fetch_attrs = ('bloug', 'beep')
+ self.assertEquals(Personne.fetch_rql(user), 'Any X WHERE X is Personne')
+ # testing one non final relation
+ Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC, AC nom AD')
+ # testing two non final relations
+ Personne.fetch_attrs = ('nom', 'prenom', 'travaille', 'evaluee')
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, '
+ 'X prenom AB, X travaille AC, AC nom AD, X evaluee AE, AE modification_date AF')
+ # testing one non final relation with recursion
+ Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
+ Societe.fetch_attrs = ('nom', 'evaluee')
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB,AC,AD,AE,AF ORDERBY AA ASC,AF DESC WHERE X is Personne, X nom AA, X prenom AB, '
+ 'X travaille AC, AC nom AD, AC evaluee AE, AE modification_date AF'
+ )
+ # testing symetric relation
+ Personne.fetch_attrs = ('nom', 'connait')
+ self.assertEquals(Personne.fetch_rql(user), 'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X connait AB')
+ # testing optional relation
+ peschema.subject_relation('travaille').set_rproperty(peschema, seschema, 'cardinality', '?*')
+ Personne.fetch_attrs = ('nom', 'prenom', 'travaille')
+ Societe.fetch_attrs = ('nom',)
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB,AC,AD ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB, X travaille AC?, AC nom AD')
+ # testing relation with cardinality > 1
+ peschema.subject_relation('travaille').set_rproperty(peschema, seschema, 'cardinality', '**')
+ self.assertEquals(Personne.fetch_rql(user),
+ 'Any X,AA,AB ORDERBY AA ASC WHERE X is Personne, X nom AA, X prenom AB')
+ # XXX test unauthorized attribute
+ finally:
+ Personne.fetch_attrs = pfetch_attrs
+ Societe.fetch_attrs = sfetch_attrs
+
+ def test_related_rql(self):
+ from cubicweb.entities import fetch_config
+ Personne = self.vreg.etype_class('Personne')
+ Note = self.vreg.etype_class('Note')
+ Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', 'type'))
+ Note.fetch_attrs, Note.fetch_order = fetch_config(('type',))
+ aff = self.add_entity('Personne', nom=u'pouet')
+ self.assertEquals(aff.related_rql('evaluee'),
+ 'Any X,AA,AB ORDERBY AA ASC WHERE E eid %(x)s, E evaluee X, '
+ 'X type AA, X modification_date AB')
+ Personne.fetch_attrs, Personne.fetch_order = fetch_config(('nom', ))
+ # XXX
+ self.assertEquals(aff.related_rql('evaluee'),
+ 'Any X,AA ORDERBY Z DESC WHERE X modification_date Z, E eid %(x)s, E evaluee X, X modification_date AA')
+
+ def test_entity_unrelated(self):
+ p = self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
+ e = self.add_entity('Tag', name=u'x')
+ rschema = e.e_schema.subject_relation('tags')
+ related = [r.eid for r in e.tags]
+ self.failUnlessEqual(related, [])
+ unrelated = [reid for rview, reid in e.vocabulary(rschema, 'subject')]
+ self.failUnless(p.eid in unrelated)
+ self.execute('SET X tags Y WHERE X is Tag, Y is Personne')
+ e = self.entity('Any X WHERE X is Tag')
+ unrelated = [reid for rview, reid in e.vocabulary(rschema, 'subject')]
+ self.failIf(p.eid in unrelated)
+
+ def test_entity_unrelated_limit(self):
+ e = self.add_entity('Tag', name=u'x')
+ self.add_entity('Personne', nom=u'di mascio', prenom=u'adrien')
+ self.add_entity('Personne', nom=u'di mascio', prenom=u'gwen')
+ rschema = e.e_schema.subject_relation('tags')
+ self.assertEquals(len(e.vocabulary(rschema, 'subject', limit=1)),
+ 1)
+
+ def test_new_entity_unrelated(self):
+ e = self.etype_instance('EUser')
+ rschema = e.e_schema.subject_relation('in_group')
+ unrelated = [reid for rview, reid in e.vocabulary(rschema, 'subject')]
+ # should be default groups but owners, i.e. managers, users, guests
+ self.assertEquals(len(unrelated), 3)
+
+
+ def test_rtags_expansion(self):
+ from cubicweb.entities import AnyEntity
+ class Personne(AnyEntity):
+ id = 'Personne'
+ __rtags__ = {
+ ('travaille', 'Societe', 'subject') : set(('primary',)),
+ ('evaluee', '*', 'subject') : set(('secondary',)),
+ 'ecrit_par' : set(('inlineview',)),
+ }
+ self.vreg.register_vobject_class(Personne)
+ rtags = Personne.rtags
+ self.assertEquals(rtags.get_tags('evaluee', 'Note', 'subject'), set(('secondary', 'link')))
+ self.assertEquals(rtags.is_inlined('evaluee', 'Note', 'subject'), False)
+ self.assertEquals(rtags.get_tags('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
+ self.assertEquals(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
+ self.assertEquals(rtags.get_tags('ecrit_par', 'Note', 'object'), set(('inlineview', 'link')))
+ self.assertEquals(rtags.is_inlined('ecrit_par', 'Note', 'object'), True)
+ class Personne2(Personne):
+ id = 'Personne'
+ __rtags__ = {
+ ('evaluee', 'Note', 'subject') : set(('inlineview',)),
+ }
+ self.vreg.register_vobject_class(Personne2)
+ rtags = Personne2.rtags
+ self.assertEquals(rtags.get_tags('evaluee', 'Note', 'subject'), set(('inlineview', 'link')))
+ self.assertEquals(rtags.is_inlined('evaluee', 'Note', 'subject'), True)
+ self.assertEquals(rtags.get_tags('evaluee', 'Personne', 'subject'), set(('secondary', 'link')))
+ self.assertEquals(rtags.is_inlined('evaluee', 'Personne', 'subject'), False)
+
+ def test_relations_by_category(self):
+ e = self.etype_instance('EUser')
+ def rbc(iterable):
+ return [(rschema.type, x) for rschema, tschemas, x in iterable]
+ self.assertEquals(rbc(e.relations_by_category('primary')),
+ [('login', 'subject'), ('upassword', 'subject'),
+ ('in_group', 'subject'), ('in_state', 'subject'),
+ ('eid', 'subject'),])
+ # firstname and surname are put in secondary category in views.entities.EUserEntity
+ self.assertListEquals(rbc(e.relations_by_category('secondary')),
+ [('firstname', 'subject'), ('surname', 'subject')])
+ self.assertListEquals(rbc(e.relations_by_category('generic')),
+ [('primary_email', 'subject'),
+ ('evaluee', 'subject'),
+ ('for_user', 'object')])
+ # owned_by is defined both as subject and object relations on EUser
+ self.assertListEquals(rbc(e.relations_by_category('generated')),
+ [('last_login_time', 'subject'),
+ ('created_by', 'subject'),
+ ('creation_date', 'subject'),
+ ('is', 'subject'),
+ ('is_instance_of', 'subject'),
+ ('modification_date', 'subject'),
+ ('owned_by', 'subject'),
+ ('created_by', 'object'),
+ ('wf_info_for', 'object'),
+ ('owned_by', 'object'),
+ ('bookmarked_by', 'object')])
+ e = self.etype_instance('Personne')
+ self.assertListEquals(rbc(e.relations_by_category('primary')),
+ [('nom', 'subject'), ('eid', 'subject')])
+ self.assertListEquals(rbc(e.relations_by_category('secondary')),
+ [('prenom', 'subject'),
+ ('type', 'subject'),])
+ self.assertListEquals(rbc(e.relations_by_category('generic')),
+ [('travaille', 'subject'),
+ ('evaluee', 'subject'),
+ ('connait', 'subject'),
+ ('ecrit_par', 'object'),
+ ('evaluee', 'object'),
+ ('tags', 'object')])
+ self.assertListEquals(rbc(e.relations_by_category('generated')),
+ [('created_by', 'subject'),
+ ('creation_date', 'subject'),
+ ('is', 'subject'),
+ ('is_instance_of', 'subject'),
+ ('modification_date', 'subject'),
+ ('owned_by', 'subject')])
+
+
+ def test_printable_value_string(self):
+ e = self.add_entity('Card', title=u'rest test', content=u'du :eid:`1:*ReST*`',
+ content_format=u'text/rest')
+ self.assertEquals(e.printable_value('content'),
+ '
around each
+ # rset item
+ add_div_section = True
+
+ def call(self, **kwargs):
+ """the view is called for an entire result set, by default loop
+ other rows of the result set and call the same view on the
+ particular row
+
+ Views applicable on None result sets have to override this method
+ """
+ rset = self.rset
+ if rset is None:
+ raise NotImplementedError, self
+ wrap = self.templatable and len(rset) > 1 and self.add_div_section
+ for i in xrange(len(rset)):
+ if wrap:
+ self.w(u'
")
+
+ def cell_call(self, row, col, **kwargs):
+ """the view is called for a particular result set cell"""
+ raise NotImplementedError, self
+
+ def linkable(self):
+ """return True if the view may be linked in a menu
+
+ by default views without title are not meant to be displayed
+ """
+ if not getattr(self, 'title', None):
+ return False
+ return True
+
+ def is_primary(self):
+ return self.id == 'primary'
+
+ def url(self):
+ """return the url associated with this view. Should not be
+ necessary for non linkable views, but a default implementation
+ is provided anyway.
+ """
+ try:
+ return self.build_url(vid=self.id, rql=self.req.form['rql'])
+ except KeyError:
+ return self.build_url(vid=self.id)
+
+ def set_request_content_type(self):
+ """set the content type returned by this view"""
+ self.req.set_content_type(self.content_type)
+
+ # view utilities ##########################################################
+
+ def view(self, __vid, rset=None, __fallback_vid=None, **kwargs):
+ """shortcut to self.vreg.render method avoiding to pass self.req"""
+ try:
+ view = self.vreg.select_view(__vid, self.req, rset, **kwargs)
+ except NoSelectableObject:
+ if __fallback_vid is None:
+ raise
+ view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs)
+ return view.dispatch(**kwargs)
+
+ def wview(self, __vid, rset, __fallback_vid=None, **kwargs):
+ """shortcut to self.view method automatically passing self.w as argument
+ """
+ self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
+
+ # XXX Template bw compat
+ template = obsolete('.template is deprecated, use .view')(wview)
+
+ def whead(self, data):
+ self.req.html_headers.write(data)
+
+ def wdata(self, data):
+ """simple helper that escapes `data` and writes into `self.w`"""
+ self.w(html_escape(data))
+
+ def action(self, actionid, row=0):
+ """shortcut to get action object with id `actionid`"""
+ return self.vreg.select_action(actionid, self.req, self.rset,
+ row=row)
+
+ def action_url(self, actionid, label=None, row=0):
+ """simple method to be able to display `actionid` as a link anywhere
+ """
+ action = self.vreg.select_action(actionid, self.req, self.rset,
+ row=row)
+ if action:
+ label = label or self.req._(action.title)
+ return u'%s' % (html_escape(action.url()), label)
+ return u''
+
+ def html_headers(self):
+ """return a list of html headers (eg something to be inserted between
+ and of the returned page
+
+ by default return a meta tag to disable robot indexation of the page
+ """
+ return [NOINDEX]
+
+ def page_title(self):
+ """returns a title according to the result set - used for the
+ title in the HTML header
+ """
+ vtitle = self.req.form.get('vtitle')
+ if vtitle:
+ return self.req._(vtitle)
+ # class defined title will only be used if the resulting title doesn't
+ # seem clear enough
+ vtitle = getattr(self, 'title', None) or u''
+ if vtitle:
+ vtitle = self.req._(vtitle)
+ rset = self.rset
+ if rset and rset.rowcount:
+ if rset.rowcount == 1:
+ try:
+ entity = self.complete_entity(0)
+ # use long_title to get context information if any
+ clabel = entity.dc_long_title()
+ except NotAnEntity:
+ clabel = display_name(self.req, rset.description[0][0])
+ clabel = u'%s (%s)' % (clabel, vtitle)
+ else :
+ etypes = rset.column_types(0)
+ if len(etypes) == 1:
+ etype = iter(etypes).next()
+ clabel = display_name(self.req, etype, 'plural')
+ else :
+ clabel = u'#[*] (%s)' % vtitle
+ else:
+ clabel = vtitle
+ return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title'))
+
+ def output_url_builder( self, name, url, args ):
+ self.w(u'\n')
+
+ def create_url(self, etype, **kwargs):
+ """ return the url of the entity creation form for a given entity type"""
+ return self.req.build_url('add/%s'%etype, **kwargs)
+<<<<<<< /home/syt/src/fcubicweb/cubicweb/view.py
+
+=======
+
+
+# concrete views base classes #################################################
+
+class EntityView(View):
+ """base class for views applying on an entity (i.e. uniform result set)
+ """
+ __registerer__ = accepts_registerer
+ __selectors__ = (accept,)
+ accepts = ('Any',)
+ category = 'entityview'
+
+>>>>>>> /tmp/view.py~other.mliJlS
+ def field(self, label, value, row=True, show_label=True, w=None, tr=True):
+ """ read-only field """
+ if w is None:
+ w = self.w
+ if row:
+ w(u'
')
+ if show_label:
+ if tr:
+ label = display_name(self.req, label)
+ w(u'%s' % label)
+ w(u'
%s
' % value)
+ if row:
+ w(u'
')
+
+
+# concrete views base classes #################################################
+
+class EntityView(View):
+ """base class for views applying on an entity (i.e. uniform result set)"""
+ __registerer__ = accepts_registerer
+ __select__ = non_final_entity()
+ registered = accepts_compat(View.registered)
+
+ category = 'entityview'
+
+
+class StartupView(View):
+ """base class for views which doesn't need a particular result set to be
+ displayed (so they can always be displayed !)
+ """
+ __registerer__ = priority_registerer
+ __select__ = none_rset()
+ registered = require_group_compat(View.registered)
+
+ category = 'startupview'
+
+ def url(self):
+ """return the url associated with this view. We can omit rql here"""
+ return self.build_url('view', vid=self.id)
+
+ def html_headers(self):
+ """return a list of html headers (eg something to be inserted between
+ and of the returned page
+
+ by default startup views are indexed
+ """
+ return []
+
+
+class EntityStartupView(EntityView):
+ """base class for entity views which may also be applied to None
+ result set (usually a default rql is provided by the view class)
+ """
+ __select__ = none_rset() | non_final_entity()
+
+ default_rql = None
+
+ def __init__(self, req, rset):
+ super(EntityStartupView, self).__init__(req, rset)
+ if rset is None:
+ # this instance is not in the "entityview" category
+ self.category = 'startupview'
+
+ def startup_rql(self):
+ """return some rql to be executed if the result set is None"""
+ return self.default_rql
+
+ def call(self, **kwargs):
+ """override call to execute rql returned by the .startup_rql method if
+ necessary
+ """
+ if self.rset is None:
+ self.rset = self.req.execute(self.startup_rql())
+ rset = self.rset
+ for i in xrange(len(rset)):
+ self.wview(self.id, rset, row=i, **kwargs)
+
+ def url(self):
+ """return the url associated with this view. We can omit rql if we are
+ on a result set on which we do not apply.
+ """
+ if self.rset is None:
+ return self.build_url(vid=self.id)
+ return super(EntityStartupView, self).url()
+
+
+class AnyRsetView(View):
+ """base class for views applying on any non empty result sets"""
+ __select__ = nonempty_rset()
+
+ category = 'anyrsetview'
+
+ def columns_labels(self, tr=True):
+ if tr:
+ translate = display_name
+ else:
+ translate = lambda req, val: val
+ rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support
+ labels = []
+ for colindex, attr in enumerate(rqlstdescr):
+ # compute column header
+ if colindex == 0 or attr == 'Any': # find a better label
+ label = ','.join(translate(self.req, et)
+ for et in self.rset.column_types(colindex))
+ else:
+ label = translate(self.req, attr)
+ labels.append(label)
+ return labels
+
+
+# concrete template base classes ##############################################
+
+class MainTemplate(View):
+ """main template are primary access point to render a full HTML page.
+ There is usually at least a regular main template and a simple fallback
+ one to display error if the first one failed
+ """
+ base_doctype = STRICT_DOCTYPE
+ registered = require_group_compat(View.registered)
+
+ @property
+ def doctype(self):
+ if self.req.xhtml_browser():
+ return self.base_doctype % CW_XHTML_EXTENSIONS
+ return self.base_doctype % ''
+
+ def set_stream(self, w=None, templatable=True):
+ if templatable and self.w is not None:
+ return
+
+ if w is None:
+ if self.binary:
+ self._stream = stream = StringIO()
+ elif not templatable:
+ # not templatable means we're using a non-html view, we don't
+ # want the HTMLStream stuff to interfere during data generation
+ self._stream = stream = UStringIO()
+ else:
+ self._stream = stream = HTMLStream(self.req)
+ w = stream.write
+ else:
+ stream = None
+ self.w = w
+ return stream
+
+ def write_doctype(self, xmldecl=True):
+ assert isinstance(self._stream, HTMLStream)
+ self._stream.doctype = self.doctype
+ if not xmldecl:
+ self._stream.xmldecl = u''
+
+ def linkable(self):
+ return False
+
+# concrete component base classes #############################################
+
+class ReloadableMixIn(object):
+ """simple mixin for reloadable parts of UI"""
+
+ def user_callback(self, cb, args, msg=None, nonify=False):
+ """register the given user callback and return an url to call it ready to be
+ inserted in html
+ """
+ self.req.add_js('cubicweb.ajax.js')
+ if nonify:
+ _cb = cb
+ def cb(*args):
+ _cb(*args)
+ cbname = self.req.register_onetime_callback(cb, *args)
+ return self.build_js(cbname, html_escape(msg or ''))
+
+ def build_update_js_call(self, cbname, msg):
+ rql = html_escape(self.rset.printable_rql())
+ return "javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')" % (
+ cbname, self.id, rql, msg, self.__registry__, self.div_id())
+
+ def build_reload_js_call(self, cbname, msg):
+ return "javascript:userCallbackThenReloadPage('%s', '%s')" % (cbname, msg)
+
+ build_js = build_update_js_call # expect updatable component by default
+
+ def div_id(self):
+ return ''
+
+
+class Component(ReloadableMixIn, View):
+ """base class for components"""
+ __registry__ = 'components'
+ __registerer__ = yes_registerer
+ __select__ = yes()
+ property_defs = {
+ _('visible'): dict(type='Boolean', default=True,
+ help=_('display the component or not')),
+ }
+
+ def div_class(self):
+ return '%s %s' % (self.propval('htmlclass'), self.id)
+
+ def div_id(self):
+ return '%sComponent' % self.id
diff -r 93447d75c4b9 -r 6a25c58a1c23 vregistry.py
--- a/vregistry.py Fri Feb 27 09:59:53 2009 +0100
+++ b/vregistry.py Mon Mar 02 21:03:54 2009 +0100
@@ -29,24 +29,14 @@
from os import listdir, stat
from os.path import dirname, join, realpath, split, isdir
from logging import getLogger
+import types
from cubicweb import CW_SOFTWARE_ROOT, set_log_methods
from cubicweb import RegistryNotFound, ObjectNotFound, NoSelectableObject
-class vobject_helper(object):
- """object instantiated at registration time to help a wrapped
- VObject subclass
- """
- def __init__(self, registry, vobject):
- self.registry = registry
- self.vobject = vobject
- self.config = registry.config
- self.schema = registry.schema
-
-
-class registerer(vobject_helper):
+class registerer(object):
"""do whatever is needed at registration time for the wrapped
class, according to current application schema and already
registered objects of the same kind (i.e. same registry name and
@@ -59,7 +49,10 @@
"""
def __init__(self, registry, vobject):
- super(registerer, self).__init__(registry, vobject)
+ self.registry = registry
+ self.vobject = vobject
+ self.config = registry.config
+ self.schema = registry.schema
self.kicked = set()
def do_it_yourself(self, registered):
@@ -73,49 +66,11 @@
def skip(self):
self.debug('no schema compat, skipping %s', self.vobject)
-
-def selector(cls, *args, **kwargs):
- """selector is called to help choosing the correct object for a
- particular request and result set by returning a score.
-
- it must implement a .score_method taking a request, a result set and
- optionaly row and col arguments which return an int telling how well
- the wrapped class apply to the given request and result set. 0 score
- means that it doesn't apply.
-
- rset may be None. If not, row and col arguments may be optionally
- given if the registry is scoring a given row or a given cell of
- the result set (both row and col are int if provided).
- """
- raise NotImplementedError(cls)
-
-
-class autoselectors(type):
- """implements __selectors__ / __select__ compatibility layer so that:
-
- __select__ = chainall(classmethod(A, B, C))
+class yes_registerer(registerer):
+ """register without any other action"""
+ def do_it_yourself(self, registered):
+ return self.vobject
- can be replaced by something like:
-
- __selectors__ = (A, B, C)
- """
- def __new__(mcs, name, bases, classdict):
- if '__select__' in classdict and '__selectors__' in classdict:
- raise TypeError("__select__ and __selectors__ "
- "can't be used together")
- if '__select__' not in classdict and '__selectors__' in classdict:
- selectors = classdict['__selectors__']
- if len(selectors) > 1:
- classdict['__select__'] = classmethod(chainall(*selectors))
- else:
- classdict['__select__'] = classmethod(selectors[0])
- return super(autoselectors, mcs).__new__(mcs, name, bases, classdict)
-
- def __setattr__(self, attr, value):
- if attr == '__selectors__':
- self.__select__ = classmethod(chainall(*value))
- super(autoselectors, self).__setattr__(attr, value)
-
class VObject(object):
"""visual object, use to be handled somehow by the visual components
@@ -129,22 +84,16 @@
:id:
object's identifier in the registry (string like 'main',
'primary', 'folder_box')
- :__registerer__:
- registration helper class
:__select__:
- selection helper function
- :__selectors__:
- tuple of selectors to be chained
- (__select__ and __selectors__ are mutually exclusive)
+ class'selector
Moreover, the `__abstract__` attribute may be set to True to indicate
that a vobject is abstract and should not be registered
"""
- __metaclass__ = autoselectors
# necessary attributes to interact with the registry
id = None
__registry__ = None
- __registerer__ = None
+ __registerer__ = yes_registerer
__select__ = None
@classmethod
@@ -155,6 +104,7 @@
may be the right hook to create an instance for example). By
default the vobject is returned without any transformation.
"""
+ cls.build___select__()
return cls
@classmethod
@@ -173,6 +123,29 @@
"""returns a unique identifier for the vobject"""
return '%s.%s' % (cls.__module__, cls.__name__)
+ # XXX bw compat code
+ @classmethod
+ def build___select__(cls):
+ for klass in cls.mro():
+ if klass.__name__ == 'AppRsetObject':
+ continue # the bw compat __selector__ is there
+ klassdict = klass.__dict__
+ if ('__select__' in klassdict and '__selectors__' in klassdict
+ and '__selgenerated__' not in klassdict):
+ raise TypeError("__select__ and __selectors__ can't be used together on class %s" % cls)
+ if '__selectors__' in klassdict and '__selgenerated__' not in klassdict:
+ cls.__selgenerated__ = True
+ # case where __selectors__ is defined locally (but __select__
+ # is in a parent class)
+ selectors = klassdict['__selectors__']
+ if len(selectors) == 1:
+ # micro optimization: don't bother with AndSelector if there's
+ # only one selector
+ select = _instantiate_selector(selectors[0])
+ else:
+ select = AndSelector(*selectors)
+ cls.__select__ = select
+
class VRegistry(object):
"""class responsible to register, propose and select the various
@@ -204,107 +177,6 @@
def __contains__(self, key):
return key in self._registries
-
- def register_vobject_class(self, cls, _kicked=set()):
- """handle vobject class registration
-
- vobject class with __abstract__ == True in their local dictionnary or
- with a name starting starting by an underscore are not registered.
- Also a vobject class needs to have __registry__ and id attributes set
- to a non empty string to be registered.
-
- Registration is actually handled by vobject's registerer.
- """
- if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_'
- or not cls.__registry__ or not cls.id):
- return
- # while reloading a module :
- # if cls was previously kicked, it means that there is a more specific
- # vobject defined elsewhere re-registering cls would kick it out
- if cls.classid() in _kicked:
- self.debug('not re-registering %s because it was previously kicked',
- cls.classid())
- else:
- regname = cls.__registry__
- if cls.id in self.config['disable-%s' % regname]:
- return
- registry = self._registries.setdefault(regname, {})
- vobjects = registry.setdefault(cls.id, [])
- registerer = cls.__registerer__(self, cls)
- cls = registerer.do_it_yourself(vobjects)
- #_kicked |= registerer.kicked
- if cls:
- vobject = cls.registered(self)
- try:
- vname = vobject.__name__
- except AttributeError:
- vname = vobject.__class__.__name__
- self.debug('registered vobject %s in registry %s with id %s',
- vname, cls.__registry__, cls.id)
- vobjects.append(vobject)
-
- def unregister_module_vobjects(self, modname):
- """removes registered objects coming from a given module
-
- returns a dictionnary classid/class of all classes that will need
- to be updated after reload (i.e. vobjects referencing classes defined
- in the module)
- """
- unregistered = {}
- # browse each registered object
- for registry, objdict in self.items():
- for oid, objects in objdict.items():
- for obj in objects[:]:
- objname = obj.classid()
- # if the vobject is defined in this module, remove it
- if objname.startswith(modname):
- unregistered[objname] = obj
- objects.remove(obj)
- self.debug('unregistering %s in %s registry',
- objname, registry)
- # if not, check if the vobject can be found in baseclasses
- # (because we also want subclasses to be updated)
- else:
- if not isinstance(obj, type):
- obj = obj.__class__
- for baseclass in obj.__bases__:
- if hasattr(baseclass, 'classid'):
- baseclassid = baseclass.classid()
- if baseclassid.startswith(modname):
- unregistered[baseclassid] = baseclass
- # update oid entry
- if objects:
- objdict[oid] = objects
- else:
- del objdict[oid]
- return unregistered
-
-
- def update_registered_subclasses(self, oldnew_mapping):
- """updates subclasses of re-registered vobjects
-
- if baseviews.PrimaryView is changed, baseviews.py will be reloaded
- automatically and the new version of PrimaryView will be registered.
- But all existing subclasses must also be notified of this change, and
- that's what this method does
-
- :param oldnew_mapping: a dict mapping old version of a class to
- the new version
- """
- # browse each registered object
- for objdict in self.values():
- for objects in objdict.values():
- for obj in objects:
- if not isinstance(obj, type):
- obj = obj.__class__
- # build new baseclasses tuple
- newbases = tuple(oldnew_mapping.get(baseclass, baseclass)
- for baseclass in obj.__bases__)
- # update obj's baseclasses tuple (__bases__) if needed
- if newbases != obj.__bases__:
- self.debug('updating %s.%s base classes',
- obj.__module__, obj.__name__)
- obj.__bases__ = newbases
def registry(self, name):
"""return the registry (dictionary of class objects) associated to
@@ -330,7 +202,92 @@
for objs in registry.values():
result += objs
return result
-
+
+ def object_by_id(self, registry, cid, *args, **kwargs):
+ """return the most specific component according to the resultset"""
+ objects = self[registry][cid]
+ assert len(objects) == 1, objects
+ return objects[0].selected(*args, **kwargs)
+
+ # methods for explicit (un)registration ###################################
+
+# def clear(self, key):
+# regname, oid = key.split('.')
+# self[regname].pop(oid, None)
+ def register_all(self, objects, modname, butclasses=()):
+ for obj in objects:
+ try:
+ if obj.__module__ != modname or obj in butclasses:
+ continue
+ oid = obj.id
+ except AttributeError:
+ continue
+ if oid:
+ self.register(obj)
+
+ def register(self, obj, registryname=None, oid=None, clear=False):
+ """base method to add an object in the registry"""
+ assert not '__abstract__' in obj.__dict__
+ registryname = registryname or obj.__registry__
+ oid = oid or obj.id
+ assert oid
+ registry = self._registries.setdefault(registryname, {})
+ if clear:
+ vobjects = registry[oid] = []
+ else:
+ vobjects = registry.setdefault(oid, [])
+ # registered() is technically a classmethod but is not declared
+ # as such because we need to compose registered in some cases
+ vobject = obj.registered.im_func(obj, self)
+ assert not vobject in vobjects
+ assert callable(vobject.__select__), vobject
+ vobjects.append(vobject)
+ try:
+ vname = vobject.__name__
+ except AttributeError:
+ vname = vobject.__class__.__name__
+ self.debug('registered vobject %s in registry %s with id %s',
+ vname, registryname, oid)
+ # automatic reloading management
+ self._registered['%s.%s' % (obj.__module__, oid)] = obj
+
+ def unregister(self, obj, registryname=None):
+ registryname = registryname or obj.__registry__
+ registry = self.registry(registryname)
+ removed_id = obj.classid()
+ for registered in registry[obj.id]:
+ # use classid() to compare classes because vreg will probably
+ # have its own version of the class, loaded through execfile
+ if registered.classid() == removed_id:
+ # XXX automatic reloading management
+ try:
+ registry[obj.id].remove(registered)
+ except ValueError:
+ self.warning('can\'t remove %s, no id %s in the %s registry',
+ removed_id, obj.id, registryname)
+ except ValueError:
+ self.warning('can\'t remove %s, not in the %s registry with id %s',
+ removed_id, registryname, obj.id)
+# else:
+# # if objects is empty, remove oid from registry
+# if not registry[obj.id]:
+# del regcontent[oid]
+ break
+
+ def register_and_replace(self, obj, replaced, registryname=None):
+ if hasattr(replaced, 'classid'):
+ replaced = replaced.classid()
+ registryname = registryname or obj.__registry__
+ registry = self.registry(registryname)
+ registered_objs = registry[obj.id]
+ for index, registered in enumerate(registered_objs):
+ if registered.classid() == replaced:
+ del registry[obj.id][index]
+ break
+ self.register(obj, registryname=registryname)
+
+ # dynamic selection methods ###############################################
+
def select(self, vobjects, *args, **kwargs):
"""return an instance of the most specific object according
to parameters
@@ -339,7 +296,7 @@
"""
score, winners = 0, []
for vobject in vobjects:
- vobjectscore = vobject.__select__(*args, **kwargs)
+ vobjectscore = vobject.__select__(vobject, *args, **kwargs)
if vobjectscore > score:
score, winners = vobjectscore, [vobject]
elif vobjectscore > 0 and vobjectscore == score:
@@ -372,15 +329,8 @@
def select_object(self, registry, cid, *args, **kwargs):
"""return the most specific component according to the resultset"""
return self.select(self.registry_objects(registry, cid), *args, **kwargs)
-
- def object_by_id(self, registry, cid, *args, **kwargs):
- """return the most specific component according to the resultset"""
- objects = self[registry][cid]
- assert len(objects) == 1, objects
- return objects[0].selected(*args, **kwargs)
# intialization methods ###################################################
-
def register_objects(self, path, force_reload=None):
if force_reload is None:
@@ -471,15 +421,18 @@
return True
def load_module(self, module):
- registered = {}
- self.info('loading %s', module)
- for objname, obj in vars(module).items():
- if objname.startswith('_'):
- continue
- self.load_ancestors_then_object(module.__name__, registered, obj)
- return registered
+ self._registered = {}
+ if hasattr(module, 'registration_callback'):
+ module.registration_callback(self)
+ else:
+ self.info('loading %s', module)
+ for objname, obj in vars(module).items():
+ if objname.startswith('_'):
+ continue
+ self.load_ancestors_then_object(module.__name__, obj)
+ return self._registered
- def load_ancestors_then_object(self, modname, registered, obj):
+ def load_ancestors_then_object(self, modname, obj):
# skip imported classes
if getattr(obj, '__module__', None) != modname:
return
@@ -490,11 +443,11 @@
except TypeError:
return
objname = '%s.%s' % (modname, obj.__name__)
- if objname in registered:
+ if objname in self._registered:
return
- registered[objname] = obj
+ self._registered[objname] = obj
for parent in obj.__bases__:
- self.load_ancestors_then_object(modname, registered, parent)
+ self.load_ancestors_then_object(modname, parent)
self.load_object(obj)
def load_object(self, obj):
@@ -505,41 +458,277 @@
raise
self.exception('vobject %s registration failed: %s', obj, ex)
+ # old automatic registration XXX deprecated ###############################
+
+ def register_vobject_class(self, cls):
+ """handle vobject class registration
+
+ vobject class with __abstract__ == True in their local dictionnary or
+ with a name starting starting by an underscore are not registered.
+ Also a vobject class needs to have __registry__ and id attributes set
+ to a non empty string to be registered.
+
+ Registration is actually handled by vobject's registerer.
+ """
+ if (cls.__dict__.get('__abstract__') or cls.__name__[0] == '_'
+ or not cls.__registry__ or not cls.id):
+ return
+ regname = cls.__registry__
+ if cls.id in self.config['disable-%s' % regname]:
+ return
+ registry = self._registries.setdefault(regname, {})
+ vobjects = registry.setdefault(cls.id, [])
+ registerer = cls.__registerer__(self, cls)
+ cls = registerer.do_it_yourself(vobjects)
+ if cls:
+ self.register(cls)
+
+ def unregister_module_vobjects(self, modname):
+ """removes registered objects coming from a given module
+
+ returns a dictionnary classid/class of all classes that will need
+ to be updated after reload (i.e. vobjects referencing classes defined
+ in the module)
+ """
+ unregistered = {}
+ # browse each registered object
+ for registry, objdict in self.items():
+ for oid, objects in objdict.items():
+ for obj in objects[:]:
+ objname = obj.classid()
+ # if the vobject is defined in this module, remove it
+ if objname.startswith(modname):
+ unregistered[objname] = obj
+ objects.remove(obj)
+ self.debug('unregistering %s in %s registry',
+ objname, registry)
+ # if not, check if the vobject can be found in baseclasses
+ # (because we also want subclasses to be updated)
+ else:
+ if not isinstance(obj, type):
+ obj = obj.__class__
+ for baseclass in obj.__bases__:
+ if hasattr(baseclass, 'classid'):
+ baseclassid = baseclass.classid()
+ if baseclassid.startswith(modname):
+ unregistered[baseclassid] = baseclass
+ # update oid entry
+ if objects:
+ objdict[oid] = objects
+ else:
+ del objdict[oid]
+ return unregistered
+
+ def update_registered_subclasses(self, oldnew_mapping):
+ """updates subclasses of re-registered vobjects
+
+ if baseviews.PrimaryView is changed, baseviews.py will be reloaded
+ automatically and the new version of PrimaryView will be registered.
+ But all existing subclasses must also be notified of this change, and
+ that's what this method does
+
+ :param oldnew_mapping: a dict mapping old version of a class to
+ the new version
+ """
+ # browse each registered object
+ for objdict in self.values():
+ for objects in objdict.values():
+ for obj in objects:
+ if not isinstance(obj, type):
+ obj = obj.__class__
+ # build new baseclasses tuple
+ newbases = tuple(oldnew_mapping.get(baseclass, baseclass)
+ for baseclass in obj.__bases__)
+ # update obj's baseclasses tuple (__bases__) if needed
+ if newbases != obj.__bases__:
+ self.debug('updating %s.%s base classes',
+ obj.__module__, obj.__name__)
+ obj.__bases__ = newbases
+
# init logging
set_log_methods(VObject, getLogger('cubicweb'))
set_log_methods(VRegistry, getLogger('cubicweb.registry'))
set_log_methods(registerer, getLogger('cubicweb.registration'))
-# advanced selector building functions ########################################
+# selector base classes and operations ########################################
+
+class Selector(object):
+ """base class for selector classes providing implementation
+ for operators ``&`` and ``|``
+
+ This class is only here to give access to binary operators, the
+ selector logic itself should be implemented in the __call__ method
+
+
+ a selector is called to help choosing the correct object for a
+ particular context by returning a score (`int`) telling how well
+ the class given as first argument apply to the given context.
+
+ 0 score means that the class doesn't apply.
+ """
+
+ @property
+ def func_name(self):
+ # backward compatibility
+ return self.__class__.__name__
+
+ def search_selector(self, selector):
+ """search for the given selector or selector instance in the selectors
+ tree. Return it of None if not found
+ """
+ if self is selector:
+ return self
+ if isinstance(selector, type) and isinstance(self, selector):
+ return self
+ return None
+
+ def __str__(self):
+ return self.__class__.__name__
+
+ def __and__(self, other):
+ return AndSelector(self, other)
+ def __rand__(self, other):
+ return AndSelector(other, self)
+
+ def __or__(self, other):
+ return OrSelector(self, other)
+ def __ror__(self, other):
+ return OrSelector(other, self)
+
+ def __invert__(self):
+ return NotSelector(self)
+
+ # XXX (function | function) or (function & function) not managed yet
+
+ def __call__(self, cls, *args, **kwargs):
+ return NotImplementedError("selector %s must implement its logic "
+ "in its __call__ method" % self.__class__)
+
+class MultiSelector(Selector):
+ """base class for compound selector classes"""
+
+ def __init__(self, *selectors):
+ self.selectors = self.merge_selectors(selectors)
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.selectors))
-def chainall(*selectors):
- """return a selector chaining given selectors. If one of
- the selectors fail, selection will fail, else the returned score
- will be the sum of each selector'score
+ @classmethod
+ def merge_selectors(cls, selectors):
+ """deal with selector instanciation when necessary and merge
+ multi-selectors if possible:
+
+ AndSelector(AndSelector(sel1, sel2), AndSelector(sel3, sel4))
+ ==> AndSelector(sel1, sel2, sel3, sel4)
+ """
+ merged_selectors = []
+ for selector in selectors:
+ try:
+ selector = _instantiate_selector(selector)
+ except:
+ pass
+ #assert isinstance(selector, Selector), selector
+ if isinstance(selector, cls):
+ merged_selectors += selector.selectors
+ else:
+ merged_selectors.append(selector)
+ return merged_selectors
+
+ def search_selector(self, selector):
+ """search for the given selector or selector instance in the selectors
+ tree. Return it of None if not found
+ """
+ for childselector in self.selectors:
+ if childselector is selector:
+ return childselector
+ found = childselector.search_selector(selector)
+ if found is not None:
+ return found
+ return None
+
+
+def objectify_selector(selector_func):
+ """convenience decorator for simple selectors where a class definition
+ would be overkill::
+
+ @objectify_selector
+ def yes(cls, *args, **kwargs):
+ return 1
+
"""
- assert selectors
- def selector(cls, *args, **kwargs):
+ return type(selector_func.__name__, (Selector,),
+ {'__call__': lambda self, *args, **kwargs: selector_func(*args, **kwargs)})
+
+def _instantiate_selector(selector):
+ """ensures `selector` is a `Selector` instance
+
+ NOTE: This should only be used locally in build___select__()
+ XXX: then, why not do it ??
+ """
+ if isinstance(selector, types.FunctionType):
+ return objectify_selector(selector)()
+ if isinstance(selector, type) and issubclass(selector, Selector):
+ return selector()
+ return selector
+
+
+class AndSelector(MultiSelector):
+ """and-chained selectors (formerly known as chainall)"""
+ def __call__(self, cls, *args, **kwargs):
score = 0
- for selector in selectors:
+ for selector in self.selectors:
partscore = selector(cls, *args, **kwargs)
if not partscore:
return 0
score += partscore
return score
+
+
+class OrSelector(MultiSelector):
+ """or-chained selectors (formerly known as chainfirst)"""
+ def __call__(self, cls, *args, **kwargs):
+ for selector in self.selectors:
+ partscore = selector(cls, *args, **kwargs)
+ if partscore:
+ return partscore
+ return 0
+
+class NotSelector(Selector):
+ """negation selector"""
+ def __init__(self, selector):
+ self.selector = selector
+
+ def __call__(self, cls, *args, **kwargs):
+ score = self.selector(cls, *args, **kwargs)
+ return int(not score)
+
+ def __str__(self):
+ return 'NOT(%s)' % super(NotSelector, self).__str__()
+
+
+# XXX bw compat functions #####################################################
+
+def chainall(*selectors, **kwargs):
+ """return a selector chaining given selectors. If one of
+ the selectors fail, selection will fail, else the returned score
+ will be the sum of each selector'score
+ """
+ assert selectors
+ # XXX do we need to create the AndSelector here, a tuple might be enough
+ selector = AndSelector(*selectors)
+ if 'name' in kwargs:
+ selector.__name__ = kwargs['name']
return selector
-def chainfirst(*selectors):
+def chainfirst(*selectors, **kwargs):
"""return a selector chaining given selectors. If all
the selectors fail, selection will fail, else the returned score
will be the first non-zero selector score
"""
assert selectors
- def selector(cls, *args, **kwargs):
- for selector in selectors:
- partscore = selector(cls, *args, **kwargs)
- if partscore:
- return partscore
- return 0
+ selector = OrSelector(*selectors)
+ if 'name' in kwargs:
+ selector.__name__ = kwargs['name']
return selector
-
diff -r 93447d75c4b9 -r 6a25c58a1c23 web/action.py
--- a/web/action.py Fri Feb 27 09:59:53 2009 +0100
+++ b/web/action.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,17 +1,18 @@
"""abstract action classes for CubicWeb web client
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
-from cubicweb.common.appobject import AppRsetObject
-from cubicweb.common.registerers import action_registerer
-from cubicweb.common.selectors import add_etype_selector, \
- match_search_state, searchstate_accept_one, \
- searchstate_accept_one_but_etype
-
+from cubicweb import target
+from cubicweb.selectors import (partial_relation_possible, match_search_state,
+ one_line_rset, partial_may_add_relation, yes,
+ accepts_compat, condition_compat, deprecate)
+from cubicweb.appobject import AppRsetObject
+from cubicweb.common.registerers import accepts_registerer
+
_ = unicode
@@ -20,10 +21,9 @@
request search state.
"""
__registry__ = 'actions'
- __registerer__ = action_registerer
- __selectors__ = (match_search_state,)
- # by default actions don't appear in link search mode
- search_states = ('normal',)
+ __registerer__ = accepts_registerer
+ __select__ = yes()
+
property_defs = {
'visible': dict(type='Boolean', default=True,
help=_('display the action or not')),
@@ -37,53 +37,6 @@
site_wide = True # don't want user to configuration actions eproperties
category = 'moreactions'
- @classmethod
- def accept_rset(cls, req, rset, row, col):
- user = req.user
- action = cls.schema_action
- if row is None:
- score = 0
- need_local_check = []
- geteschema = cls.schema.eschema
- for etype in rset.column_types(0):
- accepted = cls.accept(user, etype)
- if not accepted:
- return 0
- if action:
- eschema = geteschema(etype)
- if not user.matching_groups(eschema.get_groups(action)):
- if eschema.has_local_role(action):
- # have to ckeck local roles
- need_local_check.append(eschema)
- continue
- else:
- # even a local role won't be enough
- return 0
- score += accepted
- if need_local_check:
- # check local role for entities of necessary types
- for i, row in enumerate(rset):
- if not rset.description[i][0] in need_local_check:
- continue
- if not cls.has_permission(rset.get_entity(i, 0), action):
- return 0
- score += 1
- return score
- col = col or 0
- etype = rset.description[row][col]
- score = cls.accept(user, etype)
- if score and action:
- if not cls.has_permission(rset.get_entity(row, col), action):
- return 0
- return score
-
- @classmethod
- def has_permission(cls, entity, action):
- """defined in a separated method to ease overriding (see ModifyAction
- for instance)
- """
- return entity.has_perm(action)
-
def url(self):
"""return the url associated with this action"""
raise NotImplementedError
@@ -94,6 +47,7 @@
if self.category:
return 'box' + self.category.capitalize()
+
class UnregisteredAction(Action):
"""non registered action used to build boxes. Unless you set them
explicitly, .vreg and .schema attributes at least are None.
@@ -111,111 +65,32 @@
return self._path
-class AddEntityAction(Action):
- """link to the entity creation form. Concrete class must set .etype and
- may override .vid
- """
- __selectors__ = (add_etype_selector, match_search_state)
- vid = 'creation'
- etype = None
-
- def url(self):
- return self.build_url(vid=self.vid, etype=self.etype)
-
-
-class EntityAction(Action):
- """an action for an entity. By default entity actions are only
- displayable on single entity result if accept match.
- """
- __selectors__ = (searchstate_accept_one,)
- schema_action = None
- condition = None
-
- @classmethod
- def accept(cls, user, etype):
- score = super(EntityAction, cls).accept(user, etype)
- if not score:
- return 0
- # check if this type of entity has the necessary relation
- if hasattr(cls, 'rtype') and not cls.relation_possible(etype):
- return 0
- return score
-
-
-class LinkToEntityAction(EntityAction):
+class LinkToEntityAction(Action):
"""base class for actions consisting to create a new object
with an initial relation set to an entity.
Additionaly to EntityAction behaviour, this class is parametrized
using .etype, .rtype and .target attributes to check if the
action apply and if the logged user has access to it
"""
- etype = None
- rtype = None
- target = None
+ __select__ = (match_search_state('normal') & one_line_rset()
+ & partial_relation_possible(action='add')
+ & partial_may_add_relation())
+ registered = accepts_compat(Action.registered)
+
category = 'addrelated'
-
- @classmethod
- def accept_rset(cls, req, rset, row, col):
- entity = rset.get_entity(row or 0, col or 0)
- # check if this type of entity has the necessary relation
- if hasattr(cls, 'rtype') and not cls.relation_possible(entity.e_schema):
- return 0
- score = cls.accept(req.user, entity.e_schema)
- if not score:
- return 0
- if not cls.check_perms(req, entity):
- return 0
- return score
-
- @classmethod
- def check_perms(cls, req, entity):
- if not cls.check_rtype_perm(req, entity):
- return False
- # XXX document this:
- # if user can create the relation, suppose it can create the entity
- # this is because we usually can't check "add" permission before the
- # entity has actually been created, and schema security should be
- # defined considering this
- #if not cls.check_etype_perm(req, entity):
- # return False
- return True
-
- @classmethod
- def check_etype_perm(cls, req, entity):
- eschema = cls.schema.eschema(cls.etype)
- if not eschema.has_perm(req, 'add'):
- #print req.user.login, 'has no add perm on etype', cls.etype
- return False
- #print 'etype perm ok', cls
- return True
-
- @classmethod
- def check_rtype_perm(cls, req, entity):
- rschema = cls.schema.rschema(cls.rtype)
- # cls.target is telling us if we want to add the subject or object of
- # the relation
- if cls.target == 'subject':
- if not rschema.has_perm(req, 'add', toeid=entity.eid):
- #print req.user.login, 'has no add perm on subject rel', cls.rtype, 'with', entity
- return False
- elif not rschema.has_perm(req, 'add', fromeid=entity.eid):
- #print req.user.login, 'has no add perm on object rel', cls.rtype, 'with', entity
- return False
- #print 'rtype perm ok', cls
- return True
-
+
def url(self):
current_entity = self.rset.get_entity(self.row or 0, self.col or 0)
- linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, self.target)
+ linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self))
return self.build_url(vid='creation', etype=self.etype,
__linkto=linkto,
__redirectpath=current_entity.rest_path(), # should not be url quoted!
__redirectvid=self.req.form.get('__redirectvid', ''))
-
-class LinkToEntityAction2(LinkToEntityAction):
- """LinkToEntity action where the action is not usable on the same
- entity's type as the one refered by the .etype attribute
+class EntityAction(Action):
+ """DEPRECATED / BACKWARD COMPAT
"""
- __selectors__ = (searchstate_accept_one_but_etype,)
+ registered = deprecate(condition_compat(accepts_compat(Action.registered)),
+ msg='EntityAction is deprecated, use Action with '
+ 'appropriate selectors')
diff -r 93447d75c4b9 -r 6a25c58a1c23 web/application.py
--- a/web/application.py Fri Feb 27 09:59:53 2009 +0100
+++ b/web/application.py Mon Mar 02 21:03:54 2009 +0100
@@ -18,13 +18,13 @@
from cubicweb.cwvreg import CubicWebRegistry
from cubicweb.web import (LOGGER, StatusResponse, DirectResponse, Redirect, NotFound,
RemoteCallFailed, ExplicitLogin, InvalidSession)
-from cubicweb.web.component import SingletonComponent
+from cubicweb.web.component import Component
# make session manager available through a global variable so the debug view can
# print information about web session
SESSION_MANAGER = None
-class AbstractSessionManager(SingletonComponent):
+class AbstractSessionManager(Component):
"""manage session data associated to a session identifier"""
id = 'sessionmanager'
@@ -87,7 +87,7 @@
raise NotImplementedError()
-class AbstractAuthenticationManager(SingletonComponent):
+class AbstractAuthenticationManager(Component):
"""authenticate user associated to a request and check session validity"""
id = 'authmanager'
@@ -384,9 +384,11 @@
if tb:
req.data['excinfo'] = excinfo
req.form['vid'] = 'error'
- content = self.vreg.main_template(req, 'main')
+ errview = self.vreg.select_view('error', req, None)
+ template = self.main_template_id(req)
+ content = self.vreg.main_template(req, template, view=errview)
except:
- content = self.vreg.main_template(req, 'error')
+ content = self.vreg.main_template(req, 'error-template')
raise StatusResponse(500, content)
def need_login_content(self, req):
@@ -396,10 +398,17 @@
return self.vreg.main_template(req, 'loggedout')
def notfound_content(self, req):
- template = req.property_value('ui.main-template') or 'main'
req.form['vid'] = '404'
- return self.vreg.main_template(req, template)
+ view = self.vreg.select_view('404', req, None)
+ template = self.main_template_id(req)
+ return self.vreg.main_template(req, template, view=view)
+ def main_template_id(self, req):
+ template = req.property_value('ui.main-template')
+ if template not in self.vreg.registry('views') :
+ template = 'main-template'
+ return template
+
set_log_methods(CubicWebPublisher, LOGGER)
set_log_methods(CookieSessionHandler, LOGGER)
diff -r 93447d75c4b9 -r 6a25c58a1c23 web/box.py
--- a/web/box.py Fri Feb 27 09:59:53 2009 +0100
+++ b/web/box.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,7 +1,7 @@
"""abstract box classes for CubicWeb web client
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
@@ -10,15 +10,11 @@
from logilab.mtconverter import html_escape
from cubicweb import Unauthorized, role as get_role
-from cubicweb.common.registerers import (
- accepts_registerer, extresources_registerer,
- etype_rtype_priority_registerer)
-from cubicweb.common.selectors import (
- etype_rtype_selector, one_line_rset, accept, has_relation,
- primary_view, match_context_prop, has_related_entities,
- _rql_condition)
-from cubicweb.common.view import Template
-from cubicweb.common.appobject import ReloadableMixIn
+from cubicweb.selectors import (one_line_rset, primary_view,
+ match_context_prop, partial_has_related_entities,
+ accepts_compat, has_relation_compat,
+ condition_compat, require_group_compat)
+from cubicweb.view import View, ReloadableMixIn
from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
RawBoxItem, BoxSeparator)
@@ -27,7 +23,7 @@
_ = unicode
-class BoxTemplate(Template):
+class BoxTemplate(View):
"""base template for boxes, usually a (contextual) list of possible
actions. Various classes attributes may be used to control the box
@@ -42,7 +38,8 @@
box.render(self.w)
"""
__registry__ = 'boxes'
- __selectors__ = Template.__selectors__ + (match_context_prop,)
+ __select__ = match_context_prop()
+ registered = require_group_compat(View.registered)
categories_in_order = ()
property_defs = {
@@ -105,8 +102,7 @@
according to application schema and display according to connected
user's rights) and rql attributes
"""
- __registerer__ = etype_rtype_priority_registerer
- __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,)
+#XXX __selectors__ = BoxTemplate.__selectors__ + (etype_rtype_selector,)
rql = None
@@ -139,23 +135,11 @@
return (self.rql, {'x': self.req.user.eid}, 'x')
-class ExtResourcesBoxTemplate(BoxTemplate):
- """base class for boxes displaying external resources such as the RSS logo.
- It should list necessary resources with the .need_resources attribute.
- """
- __registerer__ = extresources_registerer
- need_resources = ()
-
-
class EntityBoxTemplate(BoxTemplate):
"""base class for boxes related to a single entity"""
- __registerer__ = accepts_registerer
- __selectors__ = (one_line_rset, primary_view,
- match_context_prop, etype_rtype_selector,
- has_relation, accept, _rql_condition)
- accepts = ('Any',)
+ __select__ = BoxTemplate.__select__ & one_line_rset() & primary_view()
+ registered = accepts_compat(has_relation_compat(condition_compat(BoxTemplate.registered)))
context = 'incontext'
- condition = None
def call(self, row=0, col=0, **kwargs):
"""classes inheriting from EntityBoxTemplate should define cell_call"""
@@ -163,8 +147,8 @@
class RelatedEntityBoxTemplate(EntityBoxTemplate):
- __selectors__ = EntityBoxTemplate.__selectors__ + (has_related_entities,)
-
+ __select__ = EntityBoxTemplate.__select__ & partial_has_related_entities()
+
def cell_call(self, row, col, **kwargs):
entity = self.entity(row, col)
limit = self.req.property_value('navigation.related-limit') + 1
diff -r 93447d75c4b9 -r 6a25c58a1c23 web/component.py
--- a/web/component.py Fri Feb 27 09:59:53 2009 +0100
+++ b/web/component.py Mon Mar 02 21:03:54 2009 +0100
@@ -1,25 +1,26 @@
"""abstract component class and base components definition for CubicWeb web client
:organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
-from cubicweb.common.appobject import Component, SingletonComponent
-from cubicweb.common.utils import merge_dicts
-from cubicweb.common.view import VComponent, SingletonVComponent
-from cubicweb.common.registerers import action_registerer
-from cubicweb.common.selectors import (paginated_rset, one_line_rset,
- rql_condition, accept, primary_view,
- match_context_prop, has_relation,
- etype_rtype_selector)
-from cubicweb.common.uilib import html_escape
+from logilab.common.deprecation import class_renamed
+from logilab.mtconverter import html_escape
+
+from cubicweb import role
+from cubicweb.utils import merge_dicts
+from cubicweb.view import View, Component
+from cubicweb.selectors import (
+ paginated_rset, one_line_rset, primary_view, match_context_prop,
+ partial_has_related_entities, partial_relation_possible,
+ condition_compat, accepts_compat, has_relation_compat)
+from cubicweb.common.registerers import accepts_registerer
_ = unicode
-
-class EntityVComponent(VComponent):
+class EntityVComponent(Component):
"""abstract base class for additinal components displayed in content
headers and footer according to:
@@ -31,11 +32,9 @@
"""
__registry__ = 'contentnavigation'
- __registerer__ = action_registerer
- __selectors__ = (one_line_rset, primary_view,
- match_context_prop, etype_rtype_selector,
- has_relation, accept,
- rql_condition)
+ __registerer__ = accepts_registerer
+ __select__ = one_line_rset() & primary_view() & match_context_prop()
+ registered = accepts_compat(has_relation_compat(condition_compat(View.registered)))
property_defs = {
_('visible'): dict(type='Boolean', default=True,
@@ -51,21 +50,20 @@
help=_('html class of the component')),
}
- accepts = ('Any',)
context = 'navcontentbottom' # 'footer' | 'header' | 'incontext'
- condition = None
- def call(self, view):
+ def call(self, view=None):
return self.cell_call(0, 0, view)
- def cell_call(self, row, col, view):
+ def cell_call(self, row, col, view=None):
raise NotImplementedError()
-class NavigationComponent(VComponent):
+class NavigationComponent(Component):
"""abstract base class for navigation components"""
- __selectors__ = (paginated_rset,)
id = 'navigation'
+ __select__ = paginated_rset()
+
page_size_property = 'navigation.page-size'
start_param = '__start'
stop_param = '__stop'
@@ -73,19 +71,6 @@
selected_page_link_templ = u'%s'
previous_page_link_templ = next_page_link_templ = page_link_templ
no_previous_page_link = no_next_page_link = u''
-
- @classmethod
- def selected(cls, req, rset, row=None, col=None, page_size=None, **kwargs):
- """by default web app objects are usually instantiated on
- selection according to a request, a result set, and optional
- row and col
- """
- instance = super(NavigationComponent, cls).selected(req, rset, row, col, **kwargs)
- if page_size is not None:
- instance.page_size = page_size
- elif 'page_size' in req.form:
- instance.page_size = int(req.form['page_size'])
- return instance
def __init__(self, req, rset):
super(NavigationComponent, self).__init__(req, rset)
@@ -96,8 +81,14 @@
try:
return self._page_size
except AttributeError:
- self._page_size = self.req.property_value(self.page_size_property)
- return self._page_size
+ page_size = self.extra_kwargs.get('page_size')
+ if page_size is None:
+ if 'page_size' in self.req.form:
+ page_size = int(self.req.form['page_size'])
+ else:
+ page_size = self.req.property_value(self.page_size_property)
+ self._page_size = page_size
+ return page_size
def set_page_size(self, page_size):
self._page_size = page_size
@@ -151,25 +142,19 @@
class RelatedObjectsVComponent(EntityVComponent):
"""a section to display some related entities"""
- __selectors__ = (one_line_rset, primary_view,
- etype_rtype_selector, has_relation,
- match_context_prop, accept)
+ __select__ = EntityVComponent.__select__ & partial_has_related_entities()
+
vid = 'list'
-
+
def rql(self):
- """override this method if you want to use a custom rql query.
- """
+ """override this method if you want to use a custom rql query"""
return None
def cell_call(self, row, col, view=None):
rql = self.rql()
if rql is None:
entity = self.rset.get_entity(row, col)
- if self.target == 'object':
- role = 'subject'
- else:
- role = 'object'
- rset = entity.related(self.rtype, role)
+ rset = entity.related(self.rtype, role(self))
else:
eid = self.rset[row][col]
rset = self.req.execute(self.rql(), {'x': eid}, 'x')
@@ -178,3 +163,10 @@
self.w(u'
')
+ entity = self.rset.get_entity(i, 0)
+ subform = EntityFieldsForm(req, set_error_url=False,
+ entity=entity)
+ form.form_add_subform(subform)
+ # don't use outofcontext view or any other that may contain inline edition form
+ w(u'