entities/__init__.py
changeset 0 b97547f5f1fa
child 125 979dbe0cade3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/entities/__init__.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,432 @@
+"""base application's entities class implementation: `AnyEntity`
+
+: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"
+
+from warnings import warn
+
+from logilab.common.deprecation import deprecated_function
+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.schema import FormatConstraint
+
+from cubicweb.interfaces import IBreadCrumbs
+
+class AnyEntity(Entity):
+    """an entity instance has e_schema automagically set on the class and
+    instances have access to their issuing cursor
+    """
+    id = 'Any'   
+    __rtags__ = {
+        'is' : ('generated', 'link'),
+        'is_instance_of' : ('generated', 'link'),
+        'identity' : ('generated', 'link'),
+        
+        # use primary and not generated for eid since it has to be an hidden
+        # field in edition
+        ('eid',                '*', 'subject'): 'primary',
+        ('creation_date',      '*', 'subject'): 'generated',
+        ('modification_date',  '*', 'subject'): 'generated',
+        ('has_text',           '*', 'subject'): 'generated',
+        
+        ('require_permission', '*', 'subject') : ('generated', 'link'),
+        ('owned_by',           '*', 'subject') : ('generated', 'link'),
+        ('created_by',         '*', 'subject') : ('generated', 'link'),
+        
+        ('wf_info_for',        '*', 'subject') : ('generated', 'link'),
+        ('wf_info_for',        '*', 'object')  : ('generated', 'link'),
+                 
+        ('description',        '*', 'subject'): 'secondary',
+
+        # XXX should be moved in their respective cubes
+        ('filed_under',        '*', 'subject') : ('generic', 'link'),
+        ('filed_under',        '*', 'object')  : ('generic', 'create'),
+        # generated since there is a componant to handle comments
+        ('comments',           '*', 'subject') : ('generated', 'link'),
+        ('comments',           '*', 'object')  : ('generated', 'link'),
+        }
+
+    __implements__ = (IBreadCrumbs,)
+    
+    @classmethod
+    def selected(cls, etype):
+        """the special Any entity is used as the default factory, so
+        the actual class has to be constructed at selection time once we
+        have an actual entity'type
+        """
+        if cls.id == etype:
+            return cls
+        usercls = dump_class(cls, etype)
+        usercls.id = etype
+        usercls.__initialize__()
+        return usercls
+    
+    fetch_attrs = ('modification_date',)
+    @classmethod
+    def fetch_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched
+        """
+        return cls.fetch_unrelated_order(attr, var)
+    
+    @classmethod
+    def fetch_unrelated_order(cls, attr, var):
+        """class method used to control sort order when multiple entities of
+        this type are fetched to use in edition (eg propose them to create a
+        new relation on an edited entity).
+        """
+        if attr == 'modification_date':
+            return '%s DESC' % var
+        return None
+
+    @classmethod
+    def __initialize__(cls): 
+        super(ANYENTITY, cls).__initialize__() # XXX
+        eschema = cls.e_schema
+        eschema.format_fields = {}
+        # set a default_ATTR method for rich text format fields
+        for attr, formatattr in eschema.rich_text_fields():
+            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 ###########################################################
+
+    def dc_title(self):
+        """return a suitable *unicode* title for this entity"""
+        for rschema, attrschema in self.e_schema.attribute_definitions():
+            if rschema.meta:
+                continue
+            value = self.get_value(rschema.type)
+            if value:
+                # make the value printable (dates, floats, bytes, etc.)
+                return self.printable_value(rschema.type, value, attrschema.type,
+                                            format='text/plain')
+        return u'%s #%s' % (self.dc_type(), self.eid)
+
+    def dc_long_title(self):
+        """return a more detailled title for this entity"""
+        return self.dc_title()
+    
+    def dc_description(self, format='text/plain'):
+        """return a suitable description for this entity"""
+        if hasattr(self, 'description'):
+            return self.printable_value('description', format=format)
+        return u''
+
+    def dc_authors(self):
+        """return a suitable description for the author(s) of the entity"""
+        try:
+            return ', '.join(u.name() for u in self.owned_by)
+        except Unauthorized:
+            return u''
+
+    def dc_creator(self):
+        """return a suitable description for the creator of the entity"""
+        if self.creator:
+            return self.creator.name()
+        return u''
+
+    def dc_date(self, date_format=None):# XXX default to ISO 8601 ?
+        """return latest modification date of this entity"""
+        return self.format_date(self.modification_date, date_format=date_format)
+
+    def dc_type(self, form=''):
+        """return the display name for the type of this entity (translated)"""
+        return self.e_schema.display_name(self.req, form)
+    display_name = deprecated_function(dc_type) # require agueol > 0.8.1, asteretud > 0.10.0 for removal
+
+    def dc_language(self):
+        """return language used by this entity (translated)"""
+        # check if entities has internationalizable attributes
+        # XXX one is enough or check if all String attributes are internationalizable?
+        for rschema, attrschema in self.e_schema.attribute_definitions():
+            if rschema.rproperty(self.e_schema, attrschema,
+                                 'internationalizable'):
+                return self.req._(self.req.user.property_value('ui.language'))
+        return self.req._(self.vreg.property_value('ui.language'))
+        
+    @property
+    def creator(self):
+        """return the EUser entity which has created this entity, or None if
+        unknown or if the curent user doesn't has access to this euser
+        """
+        try:
+            return self.created_by[0]
+        except (Unauthorized, IndexError):
+            return None
+
+    def breadcrumbs(self, view=None, recurs=False):
+        path = [self]
+        if hasattr(self, 'parent'):
+            parent = self.parent()
+            if parent is not None:
+                try:
+                    path = parent.breadcrumbs(view, True) + [self]
+                except TypeError:
+                    warn("breadcrumbs method's now takes two arguments "
+                         "(view=None, recurs=False), please update",
+                         DeprecationWarning)
+                    path = parent.breadcrumbs(view) + [self]
+        if not recurs:
+            if view is None:
+                if 'vtitle' in self.req.form:
+                    # embeding for instance
+                    path.append( self.req.form['vtitle'] )
+            elif view.id != 'primary' and hasattr(view, 'title'):
+                path.append( self.req._(view.title) )
+        return path
+
+    # abstractions making the whole things (well, some at least) working ######
+    
+    @classmethod
+    def get_widget(cls, rschema, x='subject'):
+        """return a widget to view or edit a relation
+
+        notice that when the relation support multiple target types, the widget
+        is necessarily the same for all those types
+        """
+        # let ImportError propage if web par isn't available
+        from cubicweb.web.widgets import widget
+        if isinstance(rschema, basestring):
+            rschema = cls.schema.rschema(rschema)
+        if x == 'subject':
+            tschema = rschema.objects(cls.e_schema)[0]
+            wdg = widget(cls.vreg, cls, rschema, tschema, 'subject')
+        else:
+            tschema = rschema.subjects(cls.e_schema)[0]
+            wdg = widget(cls.vreg, tschema, rschema, cls, 'object')
+        return wdg
+        
+    def sortvalue(self, rtype=None):
+        """return a value which can be used to sort this entity or given
+        entity's attribute
+        """
+        if rtype is None:
+            return self.dc_title().lower()
+        value = self.get_value(rtype)
+        # do not restrict to `unicode` because Bytes will return a `str` value
+        if isinstance(value, basestring):
+            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.
+
+        If you're using explicit 'addrelated' actions for an entity types, you
+        should probably overrides this method to return an empty list else you
+        may get some unexpected actions.
+        """
+        req = self.req
+        eschema = self.e_schema
+        for role, rschemas in (('subject', eschema.subject_relations()),
+                               ('object', eschema.object_relations())):
+            for rschema in rschemas:
+                if rschema.is_final():
+                    continue
+                # check the relation can be added as well
+                if role == 'subject'and not rschema.has_perm(req, 'add', fromeid=self.eid):
+                    continue
+                if role == 'object'and not rschema.has_perm(req, 'add', toeid=self.eid):
+                    continue
+                # check the target types can be added as well
+                for teschema in rschema.targets(eschema, role):
+                    if not self.relation_mode(rschema, teschema, role) == 'create':
+                        continue
+                    if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
+                        yield rschema, teschema, role
+
+    def relation_mode(self, rtype, targettype, role='subject'):
+        """return a string telling if the given relation is usually created
+        to a new entity ('create' mode) or to an existant entity ('link' mode)
+        """
+        return self.rtags.get_mode(rtype, targettype, role)
+
+    # edition helper functions ################################################
+    
+    def relations_by_category(self, categories=None, permission=None):
+        if categories is not None:
+            if not isinstance(categories, (list, tuple, set, frozenset)):
+                categories = (categories,)
+            if not isinstance(categories, (set, frozenset)):
+                categories = frozenset(categories)
+        eschema, rtags  = self.e_schema, self.rtags
+        if self.has_eid():
+            eid = self.eid
+        else:
+            eid = None
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            if rschema in ('identity', 'has_text'):
+                continue
+            # check category first, potentially lower cost than checking
+            # permission which may imply rql queries
+            if categories is not None:
+                targetschemas = [tschema for tschema in targetschemas
+                                 if rtags.get_tags(rschema.type, tschema.type, role).intersection(categories)]
+                if not targetschemas:
+                    continue
+            tags = rtags.get_tags(rschema.type, role=role)
+            if permission is not None:
+                # tag allowing to hijack the permission machinery when
+                # permission is not verifiable until the entity is actually
+                # created...
+                if eid is None and ('%s_on_new' % permission) in tags:
+                    yield (rschema, targetschemas, role)
+                    continue
+                if rschema.is_final():
+                    if not rschema.has_perm(self.req, permission, eid):
+                        continue
+                elif role == 'subject':
+                    if not ((eid is None and rschema.has_local_role(permission)) or
+                            rschema.has_perm(self.req, permission, fromeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
+                        and self.has_eid() and self.related(rschema.type, role)
+                        and not rschema.has_perm(self.req, 'delete', fromeid=eid,
+                                                 toeid=self.related(rschema.type, role)[0][0])):
+                        continue
+                elif role == 'object':
+                    if not ((eid is None and rschema.has_local_role(permission)) or
+                            rschema.has_perm(self.req, permission, toeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
+                        and self.has_eid() and self.related(rschema.type, role)
+                        and not rschema.has_perm(self.req, 'delete', toeid=eid,
+                                                 fromeid=self.related(rschema.type, role)[0][0])):
+                        continue
+            yield (rschema, targetschemas, role)
+
+    def srelations_by_category(self, categories=None, permission=None):
+        result = []
+        for rschema, ttypes, target in self.relations_by_category(categories,
+                                                                  permission):
+            if rschema.is_final():
+                continue
+            result.append( (rschema.display_name(self.req, target), rschema, target) )
+        return sorted(result)
+                
+    def attribute_values(self, attrname):
+        if self.has_eid() or attrname in self:
+            try:
+                values = self[attrname]
+            except KeyError:
+                values = getattr(self, attrname)
+            # actual relation return a list of entities
+            if isinstance(values, list):
+                return [v.eid for v in values]
+            return (values,)
+        # the entity is being created, try to find default value for
+        # this attribute
+        try:
+            values = self.req.form[attrname]
+        except KeyError:
+            try:
+                values = self[attrname] # copying
+            except KeyError:
+                values = getattr(self, 'default_%s' % attrname,
+                                 self.e_schema.default(attrname))
+                if callable(values):
+                    values = values()
+        if values is None:
+            values = ()
+        elif not isinstance(values, (list, tuple)):
+            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).
+        """
+        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 ()
+
+# XXX:  store a reference to the AnyEntity class since it is hijacked in goa
+#       configuration and we need the actual reference to avoid infinite loops
+#       in mro
+ANYENTITY = AnyEntity
+
+def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'):
+    if pclass is ANYENTITY:
+        pclass = AnyEntity # AnyEntity and ANYENTITY may be different classes
+    if pclass is not None:
+        fetchattrs += pclass.fetch_attrs
+    if mainattr is None:
+        mainattr = fetchattrs[0]
+    @classmethod
+    def fetch_order(cls, attr, var):
+        if attr == mainattr:
+            return '%s %s' % (var, order)
+        return None
+    return fetchattrs, fetch_order