entities/__init__.py
changeset 0 b97547f5f1fa
child 125 979dbe0cade3
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """base application's entities class implementation: `AnyEntity`
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 from warnings import warn
       
    10 
       
    11 from logilab.common.deprecation import deprecated_function
       
    12 from logilab.common.decorators import cached
       
    13 
       
    14 from cubicweb import Unauthorized, typed_eid
       
    15 from cubicweb.common.utils import dump_class
       
    16 from cubicweb.common.entity import Entity
       
    17 from cubicweb.schema import FormatConstraint
       
    18 
       
    19 from cubicweb.interfaces import IBreadCrumbs
       
    20 
       
    21 class AnyEntity(Entity):
       
    22     """an entity instance has e_schema automagically set on the class and
       
    23     instances have access to their issuing cursor
       
    24     """
       
    25     id = 'Any'   
       
    26     __rtags__ = {
       
    27         'is' : ('generated', 'link'),
       
    28         'is_instance_of' : ('generated', 'link'),
       
    29         'identity' : ('generated', 'link'),
       
    30         
       
    31         # use primary and not generated for eid since it has to be an hidden
       
    32         # field in edition
       
    33         ('eid',                '*', 'subject'): 'primary',
       
    34         ('creation_date',      '*', 'subject'): 'generated',
       
    35         ('modification_date',  '*', 'subject'): 'generated',
       
    36         ('has_text',           '*', 'subject'): 'generated',
       
    37         
       
    38         ('require_permission', '*', 'subject') : ('generated', 'link'),
       
    39         ('owned_by',           '*', 'subject') : ('generated', 'link'),
       
    40         ('created_by',         '*', 'subject') : ('generated', 'link'),
       
    41         
       
    42         ('wf_info_for',        '*', 'subject') : ('generated', 'link'),
       
    43         ('wf_info_for',        '*', 'object')  : ('generated', 'link'),
       
    44                  
       
    45         ('description',        '*', 'subject'): 'secondary',
       
    46 
       
    47         # XXX should be moved in their respective cubes
       
    48         ('filed_under',        '*', 'subject') : ('generic', 'link'),
       
    49         ('filed_under',        '*', 'object')  : ('generic', 'create'),
       
    50         # generated since there is a componant to handle comments
       
    51         ('comments',           '*', 'subject') : ('generated', 'link'),
       
    52         ('comments',           '*', 'object')  : ('generated', 'link'),
       
    53         }
       
    54 
       
    55     __implements__ = (IBreadCrumbs,)
       
    56     
       
    57     @classmethod
       
    58     def selected(cls, etype):
       
    59         """the special Any entity is used as the default factory, so
       
    60         the actual class has to be constructed at selection time once we
       
    61         have an actual entity'type
       
    62         """
       
    63         if cls.id == etype:
       
    64             return cls
       
    65         usercls = dump_class(cls, etype)
       
    66         usercls.id = etype
       
    67         usercls.__initialize__()
       
    68         return usercls
       
    69     
       
    70     fetch_attrs = ('modification_date',)
       
    71     @classmethod
       
    72     def fetch_order(cls, attr, var):
       
    73         """class method used to control sort order when multiple entities of
       
    74         this type are fetched
       
    75         """
       
    76         return cls.fetch_unrelated_order(attr, var)
       
    77     
       
    78     @classmethod
       
    79     def fetch_unrelated_order(cls, attr, var):
       
    80         """class method used to control sort order when multiple entities of
       
    81         this type are fetched to use in edition (eg propose them to create a
       
    82         new relation on an edited entity).
       
    83         """
       
    84         if attr == 'modification_date':
       
    85             return '%s DESC' % var
       
    86         return None
       
    87 
       
    88     @classmethod
       
    89     def __initialize__(cls): 
       
    90         super(ANYENTITY, cls).__initialize__() # XXX
       
    91         eschema = cls.e_schema
       
    92         eschema.format_fields = {}
       
    93         # set a default_ATTR method for rich text format fields
       
    94         for attr, formatattr in eschema.rich_text_fields():
       
    95             if not hasattr(cls, 'default_%s' % formatattr):
       
    96                 setattr(cls, 'default_%s' % formatattr, cls._default_format)
       
    97             eschema.format_fields[formatattr] = attr
       
    98             
       
    99     def _default_format(self):
       
   100         return self.req.property_value('ui.default-text-format')
       
   101 
       
   102     def use_fckeditor(self, attr):
       
   103         """return True if fckeditor should be used to edit entity's attribute named
       
   104         `attr`, according to user preferences
       
   105         """
       
   106         req = self.req
       
   107         if req.property_value('ui.fckeditor') and self.has_format(attr):
       
   108             if self.has_eid() or '%s_format' % attr in self:
       
   109                 return self.format(attr) == 'text/html'
       
   110             return req.property_value('ui.default-text-format') == 'text/html'
       
   111         return False
       
   112     
       
   113     # meta data api ###########################################################
       
   114 
       
   115     def dc_title(self):
       
   116         """return a suitable *unicode* title for this entity"""
       
   117         for rschema, attrschema in self.e_schema.attribute_definitions():
       
   118             if rschema.meta:
       
   119                 continue
       
   120             value = self.get_value(rschema.type)
       
   121             if value:
       
   122                 # make the value printable (dates, floats, bytes, etc.)
       
   123                 return self.printable_value(rschema.type, value, attrschema.type,
       
   124                                             format='text/plain')
       
   125         return u'%s #%s' % (self.dc_type(), self.eid)
       
   126 
       
   127     def dc_long_title(self):
       
   128         """return a more detailled title for this entity"""
       
   129         return self.dc_title()
       
   130     
       
   131     def dc_description(self, format='text/plain'):
       
   132         """return a suitable description for this entity"""
       
   133         if hasattr(self, 'description'):
       
   134             return self.printable_value('description', format=format)
       
   135         return u''
       
   136 
       
   137     def dc_authors(self):
       
   138         """return a suitable description for the author(s) of the entity"""
       
   139         try:
       
   140             return ', '.join(u.name() for u in self.owned_by)
       
   141         except Unauthorized:
       
   142             return u''
       
   143 
       
   144     def dc_creator(self):
       
   145         """return a suitable description for the creator of the entity"""
       
   146         if self.creator:
       
   147             return self.creator.name()
       
   148         return u''
       
   149 
       
   150     def dc_date(self, date_format=None):# XXX default to ISO 8601 ?
       
   151         """return latest modification date of this entity"""
       
   152         return self.format_date(self.modification_date, date_format=date_format)
       
   153 
       
   154     def dc_type(self, form=''):
       
   155         """return the display name for the type of this entity (translated)"""
       
   156         return self.e_schema.display_name(self.req, form)
       
   157     display_name = deprecated_function(dc_type) # require agueol > 0.8.1, asteretud > 0.10.0 for removal
       
   158 
       
   159     def dc_language(self):
       
   160         """return language used by this entity (translated)"""
       
   161         # check if entities has internationalizable attributes
       
   162         # XXX one is enough or check if all String attributes are internationalizable?
       
   163         for rschema, attrschema in self.e_schema.attribute_definitions():
       
   164             if rschema.rproperty(self.e_schema, attrschema,
       
   165                                  'internationalizable'):
       
   166                 return self.req._(self.req.user.property_value('ui.language'))
       
   167         return self.req._(self.vreg.property_value('ui.language'))
       
   168         
       
   169     @property
       
   170     def creator(self):
       
   171         """return the EUser entity which has created this entity, or None if
       
   172         unknown or if the curent user doesn't has access to this euser
       
   173         """
       
   174         try:
       
   175             return self.created_by[0]
       
   176         except (Unauthorized, IndexError):
       
   177             return None
       
   178 
       
   179     def breadcrumbs(self, view=None, recurs=False):
       
   180         path = [self]
       
   181         if hasattr(self, 'parent'):
       
   182             parent = self.parent()
       
   183             if parent is not None:
       
   184                 try:
       
   185                     path = parent.breadcrumbs(view, True) + [self]
       
   186                 except TypeError:
       
   187                     warn("breadcrumbs method's now takes two arguments "
       
   188                          "(view=None, recurs=False), please update",
       
   189                          DeprecationWarning)
       
   190                     path = parent.breadcrumbs(view) + [self]
       
   191         if not recurs:
       
   192             if view is None:
       
   193                 if 'vtitle' in self.req.form:
       
   194                     # embeding for instance
       
   195                     path.append( self.req.form['vtitle'] )
       
   196             elif view.id != 'primary' and hasattr(view, 'title'):
       
   197                 path.append( self.req._(view.title) )
       
   198         return path
       
   199 
       
   200     # abstractions making the whole things (well, some at least) working ######
       
   201     
       
   202     @classmethod
       
   203     def get_widget(cls, rschema, x='subject'):
       
   204         """return a widget to view or edit a relation
       
   205 
       
   206         notice that when the relation support multiple target types, the widget
       
   207         is necessarily the same for all those types
       
   208         """
       
   209         # let ImportError propage if web par isn't available
       
   210         from cubicweb.web.widgets import widget
       
   211         if isinstance(rschema, basestring):
       
   212             rschema = cls.schema.rschema(rschema)
       
   213         if x == 'subject':
       
   214             tschema = rschema.objects(cls.e_schema)[0]
       
   215             wdg = widget(cls.vreg, cls, rschema, tschema, 'subject')
       
   216         else:
       
   217             tschema = rschema.subjects(cls.e_schema)[0]
       
   218             wdg = widget(cls.vreg, tschema, rschema, cls, 'object')
       
   219         return wdg
       
   220         
       
   221     def sortvalue(self, rtype=None):
       
   222         """return a value which can be used to sort this entity or given
       
   223         entity's attribute
       
   224         """
       
   225         if rtype is None:
       
   226             return self.dc_title().lower()
       
   227         value = self.get_value(rtype)
       
   228         # do not restrict to `unicode` because Bytes will return a `str` value
       
   229         if isinstance(value, basestring):
       
   230             return self.printable_value(rtype, format='text/plain').lower()
       
   231         return value
       
   232 
       
   233     def after_deletion_path(self):
       
   234         """return (path, parameters) which should be used as redirect
       
   235         information when this entity is being deleted
       
   236         """
       
   237         return str(self.e_schema).lower(), {}
       
   238 
       
   239     def add_related_schemas(self):
       
   240         """this is actually used ui method to generate 'addrelated' actions from
       
   241         the schema.
       
   242 
       
   243         If you're using explicit 'addrelated' actions for an entity types, you
       
   244         should probably overrides this method to return an empty list else you
       
   245         may get some unexpected actions.
       
   246         """
       
   247         req = self.req
       
   248         eschema = self.e_schema
       
   249         for role, rschemas in (('subject', eschema.subject_relations()),
       
   250                                ('object', eschema.object_relations())):
       
   251             for rschema in rschemas:
       
   252                 if rschema.is_final():
       
   253                     continue
       
   254                 # check the relation can be added as well
       
   255                 if role == 'subject'and not rschema.has_perm(req, 'add', fromeid=self.eid):
       
   256                     continue
       
   257                 if role == 'object'and not rschema.has_perm(req, 'add', toeid=self.eid):
       
   258                     continue
       
   259                 # check the target types can be added as well
       
   260                 for teschema in rschema.targets(eschema, role):
       
   261                     if not self.relation_mode(rschema, teschema, role) == 'create':
       
   262                         continue
       
   263                     if teschema.has_local_role('add') or teschema.has_perm(req, 'add'):
       
   264                         yield rschema, teschema, role
       
   265 
       
   266     def relation_mode(self, rtype, targettype, role='subject'):
       
   267         """return a string telling if the given relation is usually created
       
   268         to a new entity ('create' mode) or to an existant entity ('link' mode)
       
   269         """
       
   270         return self.rtags.get_mode(rtype, targettype, role)
       
   271 
       
   272     # edition helper functions ################################################
       
   273     
       
   274     def relations_by_category(self, categories=None, permission=None):
       
   275         if categories is not None:
       
   276             if not isinstance(categories, (list, tuple, set, frozenset)):
       
   277                 categories = (categories,)
       
   278             if not isinstance(categories, (set, frozenset)):
       
   279                 categories = frozenset(categories)
       
   280         eschema, rtags  = self.e_schema, self.rtags
       
   281         if self.has_eid():
       
   282             eid = self.eid
       
   283         else:
       
   284             eid = None
       
   285         for rschema, targetschemas, role in eschema.relation_definitions(True):
       
   286             if rschema in ('identity', 'has_text'):
       
   287                 continue
       
   288             # check category first, potentially lower cost than checking
       
   289             # permission which may imply rql queries
       
   290             if categories is not None:
       
   291                 targetschemas = [tschema for tschema in targetschemas
       
   292                                  if rtags.get_tags(rschema.type, tschema.type, role).intersection(categories)]
       
   293                 if not targetschemas:
       
   294                     continue
       
   295             tags = rtags.get_tags(rschema.type, role=role)
       
   296             if permission is not None:
       
   297                 # tag allowing to hijack the permission machinery when
       
   298                 # permission is not verifiable until the entity is actually
       
   299                 # created...
       
   300                 if eid is None and ('%s_on_new' % permission) in tags:
       
   301                     yield (rschema, targetschemas, role)
       
   302                     continue
       
   303                 if rschema.is_final():
       
   304                     if not rschema.has_perm(self.req, permission, eid):
       
   305                         continue
       
   306                 elif role == 'subject':
       
   307                     if not ((eid is None and rschema.has_local_role(permission)) or
       
   308                             rschema.has_perm(self.req, permission, fromeid=eid)):
       
   309                         continue
       
   310                     # on relation with cardinality 1 or ?, we need delete perm as well
       
   311                     # if the relation is already set
       
   312                     if (permission == 'add'
       
   313                         and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
       
   314                         and self.has_eid() and self.related(rschema.type, role)
       
   315                         and not rschema.has_perm(self.req, 'delete', fromeid=eid,
       
   316                                                  toeid=self.related(rschema.type, role)[0][0])):
       
   317                         continue
       
   318                 elif role == 'object':
       
   319                     if not ((eid is None and rschema.has_local_role(permission)) or
       
   320                             rschema.has_perm(self.req, permission, toeid=eid)):
       
   321                         continue
       
   322                     # on relation with cardinality 1 or ?, we need delete perm as well
       
   323                     # if the relation is already set
       
   324                     if (permission == 'add'
       
   325                         and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
       
   326                         and self.has_eid() and self.related(rschema.type, role)
       
   327                         and not rschema.has_perm(self.req, 'delete', toeid=eid,
       
   328                                                  fromeid=self.related(rschema.type, role)[0][0])):
       
   329                         continue
       
   330             yield (rschema, targetschemas, role)
       
   331 
       
   332     def srelations_by_category(self, categories=None, permission=None):
       
   333         result = []
       
   334         for rschema, ttypes, target in self.relations_by_category(categories,
       
   335                                                                   permission):
       
   336             if rschema.is_final():
       
   337                 continue
       
   338             result.append( (rschema.display_name(self.req, target), rschema, target) )
       
   339         return sorted(result)
       
   340                 
       
   341     def attribute_values(self, attrname):
       
   342         if self.has_eid() or attrname in self:
       
   343             try:
       
   344                 values = self[attrname]
       
   345             except KeyError:
       
   346                 values = getattr(self, attrname)
       
   347             # actual relation return a list of entities
       
   348             if isinstance(values, list):
       
   349                 return [v.eid for v in values]
       
   350             return (values,)
       
   351         # the entity is being created, try to find default value for
       
   352         # this attribute
       
   353         try:
       
   354             values = self.req.form[attrname]
       
   355         except KeyError:
       
   356             try:
       
   357                 values = self[attrname] # copying
       
   358             except KeyError:
       
   359                 values = getattr(self, 'default_%s' % attrname,
       
   360                                  self.e_schema.default(attrname))
       
   361                 if callable(values):
       
   362                     values = values()
       
   363         if values is None:
       
   364             values = ()
       
   365         elif not isinstance(values, (list, tuple)):
       
   366             values = (values,)
       
   367         return values
       
   368 
       
   369     def linked_to(self, rtype, target, remove=True):
       
   370         """if entity should be linked to another using __linkto form param for
       
   371         the given relation/target, return eids of related entities
       
   372 
       
   373         This method is consuming matching link-to information from form params
       
   374         if `remove` is True (by default).
       
   375         """
       
   376         try:
       
   377             return self.__linkto[(rtype, target)]
       
   378         except AttributeError:
       
   379             self.__linkto = {}
       
   380         except KeyError:
       
   381             pass
       
   382         linktos = list(self.req.list_form_param('__linkto'))
       
   383         linkedto = []
       
   384         for linkto in linktos[:]:
       
   385             ltrtype, eid, lttarget = linkto.split(':')
       
   386             if rtype == ltrtype and target == lttarget:
       
   387                 # delete __linkto from form param to avoid it being added as
       
   388                 # hidden input
       
   389                 if remove:
       
   390                     linktos.remove(linkto)
       
   391                     self.req.form['__linkto'] = linktos
       
   392                 linkedto.append(typed_eid(eid))
       
   393         self.__linkto[(rtype, target)] = linkedto
       
   394         return linkedto
       
   395 
       
   396     def pre_web_edit(self):
       
   397         """callback called by the web editcontroller when an entity will be
       
   398         created/modified, to let a chance to do some entity specific stuff.
       
   399 
       
   400         Do nothing by default.
       
   401         """
       
   402         pass
       
   403     
       
   404     # server side helpers #####################################################
       
   405     
       
   406     def notification_references(self, view):
       
   407         """used to control References field of email send on notification
       
   408         for this entity. `view` is the notification view.
       
   409         
       
   410         Should return a list of eids which can be used to generate message ids
       
   411         of previously sent email
       
   412         """
       
   413         return ()
       
   414 
       
   415 # XXX:  store a reference to the AnyEntity class since it is hijacked in goa
       
   416 #       configuration and we need the actual reference to avoid infinite loops
       
   417 #       in mro
       
   418 ANYENTITY = AnyEntity
       
   419 
       
   420 def fetch_config(fetchattrs, mainattr=None, pclass=AnyEntity, order='ASC'):
       
   421     if pclass is ANYENTITY:
       
   422         pclass = AnyEntity # AnyEntity and ANYENTITY may be different classes
       
   423     if pclass is not None:
       
   424         fetchattrs += pclass.fetch_attrs
       
   425     if mainattr is None:
       
   426         mainattr = fetchattrs[0]
       
   427     @classmethod
       
   428     def fetch_order(cls, attr, var):
       
   429         if attr == mainattr:
       
   430             return '%s %s' % (var, order)
       
   431         return None
       
   432     return fetchattrs, fetch_order