--- a/common/entity.py Tue Feb 17 21:58:44 2009 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1105 +0,0 @@
-"""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.selectors import yes
-from cubicweb.common.appobject import AppRsetObject
-from cubicweb.common.registerers import id_registerer
-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):
- 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 '<Entity %s %s %s at %s>' % (
- 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 <name>, 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'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/entity.py Tue Feb 17 22:00:53 2009 +0100
@@ -0,0 +1,1105 @@
+"""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.selectors import yes
+from cubicweb.common.appobject import AppRsetObject
+from cubicweb.common.registerers import id_registerer
+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):
+ 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 '<Entity %s %s %s at %s>' % (
+ 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 <name>, 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'))