common/appobject.py
changeset 1808 aa09e20dd8c0
parent 447 0e52d72104a6
parent 1132 96752791c2b6
child 1977 606923dff11b
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
     1 """Base class for dynamically loaded objects manipulated in the web interface
     1 """pre 3.2 bw compat"""
     2 
     2 # pylint: disable-msg=W0614,W0401
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 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
     3 from warnings import warn
    10 
     4 warn('moved to cubicweb.appobject', DeprecationWarning, stacklevel=2)
    11 from mx.DateTime import now, oneSecond
     5 from cubicweb.appobject import *
    12 from simplejson import dumps
       
    13 
       
    14 from logilab.common.deprecation import obsolete
       
    15 
       
    16 from rql.nodes import VariableRef, SubQuery
       
    17 from rql.stmts import Union, Select
       
    18 
       
    19 from cubicweb import Unauthorized
       
    20 from cubicweb.vregistry import VObject
       
    21 from cubicweb.common.utils import UStringIO
       
    22 from cubicweb.common.uilib import html_escape, ustrftime
       
    23 from cubicweb.common.registerers import yes_registerer, priority_registerer
       
    24 from cubicweb.common.selectors import yes
       
    25 
       
    26 _MARKER = object()
       
    27 
       
    28 
       
    29 class Cache(dict):    
       
    30     def __init__(self):
       
    31         super(Cache, self).__init__()
       
    32         self.cache_creation_date = None
       
    33         self.latest_cache_lookup = now()
       
    34     
       
    35 CACHE_REGISTRY = {}
       
    36 
       
    37 class AppRsetObject(VObject):
       
    38     """This is the base class for CubicWeb application objects
       
    39     which are selected according to a request and result set.
       
    40     
       
    41     Classes are kept in the vregistry and instantiation is done at selection
       
    42     time.
       
    43     
       
    44     At registration time, the following attributes are set on the class:
       
    45     :vreg:
       
    46       the application's registry
       
    47     :schema:
       
    48       the application's schema
       
    49     :config:
       
    50       the application's configuration
       
    51 
       
    52     At instantiation time, the following attributes are set on the instance:
       
    53     :req:
       
    54       current request
       
    55     :rset:
       
    56       result set on which the object is applied
       
    57     """
       
    58 
       
    59     @classmethod
       
    60     def registered(cls, vreg):
       
    61         cls.vreg = vreg
       
    62         cls.schema = vreg.schema
       
    63         cls.config = vreg.config
       
    64         cls.register_properties()
       
    65         return cls
       
    66 
       
    67     @classmethod
       
    68     def selected(cls, req, rset, row=None, col=None, **kwargs):
       
    69         """by default web app objects are usually instantiated on
       
    70         selection according to a request, a result set, and optional
       
    71         row and col
       
    72         """
       
    73         instance = cls(req, rset)
       
    74         instance.row = row
       
    75         instance.col = col
       
    76         return instance
       
    77 
       
    78     # Eproperties definition:
       
    79     # key: id of the property (the actual EProperty key is build using
       
    80     #      <registry name>.<obj id>.<property id>
       
    81     # value: tuple (property type, vocabfunc, default value, property description)
       
    82     #        possible types are those used by `logilab.common.configuration`
       
    83     #
       
    84     # notice that when it exists multiple objects with the same id (adaptation,
       
    85     # overriding) only the first encountered definition is considered, so those
       
    86     # objects can't try to have different default values for instance.
       
    87     
       
    88     property_defs = {}
       
    89     
       
    90     @classmethod
       
    91     def register_properties(cls):
       
    92         for propid, pdef in cls.property_defs.items():
       
    93             pdef = pdef.copy() # may be shared
       
    94             pdef['default'] = getattr(cls, propid, pdef['default'])
       
    95             pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide'))
       
    96             cls.vreg.register_property(cls.propkey(propid), **pdef)
       
    97         
       
    98     @classmethod
       
    99     def propkey(cls, propid):
       
   100         return '%s.%s.%s' % (cls.__registry__, cls.id, propid)
       
   101             
       
   102         
       
   103     def __init__(self, req, rset):
       
   104         super(AppRsetObject, self).__init__()
       
   105         self.req = req
       
   106         self.rset = rset
       
   107 
       
   108     @property
       
   109     def cursor(self): # XXX deprecate in favor of req.cursor?
       
   110         msg = '.cursor is deprecated, use req.execute (or req.cursor if necessary)'
       
   111         warn(msg, DeprecationWarning, stacklevel=2)
       
   112         return self.req.cursor
       
   113         
       
   114     def get_cache(self, cachename):
       
   115         """
       
   116         NOTE: cachename should be dotted names as in :
       
   117         - cubicweb.mycache
       
   118         - cubes.blog.mycache 
       
   119         - etc.
       
   120         """
       
   121         if cachename in CACHE_REGISTRY:
       
   122             cache = CACHE_REGISTRY[cachename]
       
   123         else:
       
   124             cache = Cache()
       
   125             CACHE_REGISTRY[cachename] = cache
       
   126         _now = now()
       
   127         if _now > cache.latest_cache_lookup + oneSecond:
       
   128             ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T', 
       
   129                                       {'name':cachename}).get_entity(0,0)
       
   130             cache.latest_cache_lookup = _now
       
   131             if not ecache.valid(cache.cache_creation_date):
       
   132                 cache.empty()
       
   133                 cache.cache_creation_date = _now
       
   134         return cache
       
   135 
       
   136     def propval(self, propid):
       
   137         assert self.req
       
   138         return self.req.property_value(self.propkey(propid))
       
   139 
       
   140     
       
   141     def limited_rql(self):
       
   142         """return a printable rql for the result set associated to the object,
       
   143         with limit/offset correctly set according to maximum page size and
       
   144         currently displayed page when necessary
       
   145         """
       
   146         # try to get page boundaries from the navigation component
       
   147         # XXX we should probably not have a ref to this component here (eg in
       
   148         #     cubicweb.common)
       
   149         nav = self.vreg.select_component('navigation', self.req, self.rset)
       
   150         if nav:
       
   151             start, stop = nav.page_boundaries()
       
   152             rql = self._limit_offset_rql(stop - start, start)
       
   153         # result set may have be limited manually in which case navigation won't
       
   154         # apply
       
   155         elif self.rset.limited:
       
   156             rql = self._limit_offset_rql(*self.rset.limited)
       
   157         # navigation component doesn't apply and rset has not been limited, no
       
   158         # need to limit query
       
   159         else:
       
   160             rql = self.rset.printable_rql()
       
   161         return rql
       
   162     
       
   163     def _limit_offset_rql(self, limit, offset):
       
   164         rqlst = self.rset.syntax_tree()
       
   165         if len(rqlst.children) == 1:
       
   166             select = rqlst.children[0]
       
   167             olimit, ooffset = select.limit, select.offset
       
   168             select.limit, select.offset = limit, offset
       
   169             rql = rqlst.as_string(kwargs=self.rset.args)
       
   170             # restore original limit/offset
       
   171             select.limit, select.offset = olimit, ooffset
       
   172         else:
       
   173             newselect = Select()
       
   174             newselect.limit = limit
       
   175             newselect.offset = offset
       
   176             aliases = [VariableRef(newselect.get_variable(vref.name, i))
       
   177                        for i, vref in enumerate(rqlst.selection)]
       
   178             newselect.set_with([SubQuery(aliases, rqlst)], check=False)
       
   179             newunion = Union()
       
   180             newunion.append(newselect)
       
   181             rql = rqlst.as_string(kwargs=self.rset.args)
       
   182             rqlst.parent = None
       
   183         return rql
       
   184     
       
   185     # url generation methods ##################################################
       
   186     
       
   187     controller = 'view'
       
   188     
       
   189     def build_url(self, method=None, **kwargs):
       
   190         """return an absolute URL using params dictionary key/values as URL
       
   191         parameters. Values are automatically URL quoted, and the
       
   192         publishing method to use may be specified or will be guessed.
       
   193         """
       
   194         # XXX I (adim) think that if method is passed explicitly, we should
       
   195         #     not try to process it and directly call req.build_url()
       
   196         if method is None:
       
   197             method = self.controller
       
   198             if method == 'view' and self.req.from_controller() == 'view' and \
       
   199                    not '_restpath' in kwargs:
       
   200                 method = self.req.relative_path(includeparams=False) or 'view'
       
   201         return self.req.build_url(method, **kwargs)
       
   202 
       
   203     # various resources accessors #############################################
       
   204 
       
   205     def etype_rset(self, etype, size=1):
       
   206         """return a fake result set for a particular entity type"""
       
   207         msg = '.etype_rset is deprecated, use req.etype_rset'
       
   208         warn(msg, DeprecationWarning, stacklevel=2)
       
   209         return self.req.etype_rset(etype, size=1)
       
   210 
       
   211     def eid_rset(self, eid, etype=None):
       
   212         """return a result set for the given eid"""
       
   213         msg = '.eid_rset is deprecated, use req.eid_rset'
       
   214         warn(msg, DeprecationWarning, stacklevel=2)
       
   215         return self.req.eid_rset(eid, etype)
       
   216     
       
   217     def entity(self, row, col=0):
       
   218         """short cut to get an entity instance for a particular row/column
       
   219         (col default to 0)
       
   220         """
       
   221         return self.rset.get_entity(row, col)
       
   222     
       
   223     def complete_entity(self, row, col=0, skip_bytes=True):
       
   224         """short cut to get an completed entity instance for a particular
       
   225         row (all instance's attributes have been fetched)
       
   226         """
       
   227         entity = self.entity(row, col)
       
   228         entity.complete(skip_bytes=skip_bytes)
       
   229         return entity
       
   230 
       
   231     def user_rql_callback(self, args, msg=None):
       
   232         """register a user callback to execute some rql query and return an url
       
   233         to call it ready to be inserted in html
       
   234         """
       
   235         def rqlexec(req, rql, args=None, key=None):
       
   236             req.execute(rql, args, key)
       
   237         return self.user_callback(rqlexec, args, msg)
       
   238         
       
   239     def user_callback(self, cb, args, msg=None, nonify=False):
       
   240         """register the given user callback and return an url to call it ready to be
       
   241         inserted in html
       
   242         """
       
   243         self.req.add_js('cubicweb.ajax.js')
       
   244         if nonify:
       
   245             # XXX < 2.48.3 bw compat
       
   246             warn('nonify argument is deprecated', DeprecationWarning, stacklevel=2)
       
   247             _cb = cb
       
   248             def cb(*args):
       
   249                 _cb(*args)
       
   250         cbname = self.req.register_onetime_callback(cb, *args)
       
   251         msg = dumps(msg or '') 
       
   252         return "javascript:userCallbackThenReloadPage('%s', %s)" % (
       
   253             cbname, msg)
       
   254 
       
   255     # formating methods #######################################################
       
   256 
       
   257     def tal_render(self, template, variables):
       
   258         """render a precompiled page template with variables in the given
       
   259         dictionary as context
       
   260         """
       
   261         from cubicweb.common.tal import CubicWebContext
       
   262         context = CubicWebContext()
       
   263         context.update({'self': self, 'rset': self.rset, '_' : self.req._,
       
   264                         'req': self.req, 'user': self.req.user})
       
   265         context.update(variables)
       
   266         output = UStringIO()
       
   267         template.expand(context, output)
       
   268         return output.getvalue()
       
   269 
       
   270     def format_date(self, date, date_format=None, time=False):
       
   271         """return a string for a mx date time according to application's
       
   272         configuration
       
   273         """
       
   274         if date:
       
   275             if date_format is None:
       
   276                 if time:
       
   277                     date_format = self.req.property_value('ui.datetime-format')
       
   278                 else:
       
   279                     date_format = self.req.property_value('ui.date-format')
       
   280             return ustrftime(date, date_format)
       
   281         return u''
       
   282 
       
   283     def format_time(self, time):
       
   284         """return a string for a mx date time according to application's
       
   285         configuration
       
   286         """
       
   287         if time:
       
   288             return ustrftime(time, self.req.property_value('ui.time-format'))
       
   289         return u''
       
   290 
       
   291     def format_float(self, num):
       
   292         """return a string for floating point number according to application's
       
   293         configuration
       
   294         """
       
   295         if num:
       
   296             return self.req.property_value('ui.float-format') % num
       
   297         return u''
       
   298     
       
   299     # security related methods ################################################
       
   300     
       
   301     def ensure_ro_rql(self, rql):
       
   302         """raise an exception if the given rql is not a select query"""
       
   303         first = rql.split(' ', 1)[0].lower()
       
   304         if first in ('insert', 'set', 'delete'):
       
   305             raise Unauthorized(self.req._('only select queries are authorized'))
       
   306 
       
   307     # .accepts handling utilities #############################################
       
   308     
       
   309     accepts = ('Any',)
       
   310 
       
   311     @classmethod
       
   312     def accept_rset(cls, req, rset, row, col):
       
   313         """apply the following rules:
       
   314         * if row is None, return the sum of values returned by the method
       
   315           for each entity's type in the result set. If any score is 0,
       
   316           return 0.
       
   317         * if row is specified, return the value returned by the method with
       
   318           the entity's type of this row
       
   319         """
       
   320         if row is None:
       
   321             score = 0
       
   322             for etype in rset.column_types(0):
       
   323                 accepted = cls.accept(req.user, etype)
       
   324                 if not accepted:
       
   325                     return 0
       
   326                 score += accepted
       
   327             return score
       
   328         return cls.accept(req.user, rset.description[row][col or 0])
       
   329         
       
   330     @classmethod
       
   331     def accept(cls, user, etype):
       
   332         """score etype, returning better score on exact match"""
       
   333         if 'Any' in cls.accepts:
       
   334             return 1
       
   335         eschema = cls.schema.eschema(etype)
       
   336         matching_types = [e.type for e in eschema.ancestors()]
       
   337         matching_types.append(etype)
       
   338         for index, basetype in enumerate(matching_types):
       
   339             if basetype in cls.accepts:
       
   340                 return 2 + index
       
   341         return 0
       
   342     
       
   343     # .rtype  handling utilities ##############################################
       
   344     
       
   345     @classmethod
       
   346     def relation_possible(cls, etype):
       
   347         """tell if a relation with etype entity is possible according to 
       
   348         mixed class'.etype, .rtype and .target attributes
       
   349 
       
   350         XXX should probably be moved out to a function
       
   351         """
       
   352         schema = cls.schema
       
   353         rtype = cls.rtype
       
   354         eschema = schema.eschema(etype)
       
   355         if hasattr(cls, 'role'):
       
   356             role = cls.role
       
   357         elif cls.target == 'subject':
       
   358             role = 'object'
       
   359         else:
       
   360             role = 'subject'
       
   361         # check if this relation is possible according to the schema
       
   362         try:
       
   363             if role == 'object':
       
   364                 rschema = eschema.object_relation(rtype)
       
   365             else:
       
   366                 rschema = eschema.subject_relation(rtype)
       
   367         except KeyError:
       
   368             return False            
       
   369         if hasattr(cls, 'etype'):
       
   370             letype = cls.etype
       
   371             try:
       
   372                 if role == 'object':
       
   373                     return etype in rschema.objects(letype)
       
   374                 else:
       
   375                     return etype in rschema.subjects(letype)
       
   376             except KeyError, ex:
       
   377                 return False
       
   378         return True
       
   379 
       
   380     
       
   381     # XXX deprecated (since 2.43) ##########################
       
   382     
       
   383     @obsolete('use req.datadir_url')
       
   384     def datadir_url(self):
       
   385         """return url of the application's data directory"""
       
   386         return self.req.datadir_url
       
   387 
       
   388     @obsolete('use req.external_resource()')
       
   389     def external_resource(self, rid, default=_MARKER):
       
   390         return self.req.external_resource(rid, default)
       
   391 
       
   392         
       
   393 class AppObject(AppRsetObject):
       
   394     """base class for application objects which are not selected
       
   395     according to a result set, only by their identifier.
       
   396     
       
   397     Those objects may not have req, rset and cursor set.
       
   398     """
       
   399     
       
   400     @classmethod
       
   401     def selected(cls, *args, **kwargs):
       
   402         """by default web app objects are usually instantiated on
       
   403         selection
       
   404         """
       
   405         return cls(*args, **kwargs)
       
   406 
       
   407     def __init__(self, req=None, rset=None, **kwargs):
       
   408         self.req = req
       
   409         self.rset = rset
       
   410         self.__dict__.update(kwargs)
       
   411 
       
   412 
       
   413 class ReloadableMixIn(object):
       
   414     """simple mixin for reloadable parts of UI"""
       
   415     
       
   416     def user_callback(self, cb, args, msg=None, nonify=False):
       
   417         """register the given user callback and return an url to call it ready to be
       
   418         inserted in html
       
   419         """
       
   420         self.req.add_js('cubicweb.ajax.js')
       
   421         if nonify:
       
   422             _cb = cb
       
   423             def cb(*args):
       
   424                 _cb(*args)
       
   425         cbname = self.req.register_onetime_callback(cb, *args)
       
   426         return self.build_js(cbname, html_escape(msg or ''))
       
   427         
       
   428     def build_update_js_call(self, cbname, msg):
       
   429         rql = html_escape(self.rset.printable_rql())
       
   430         return "javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')" % (
       
   431             cbname, self.id, rql, msg, self.__registry__, self.div_id())
       
   432     
       
   433     def build_reload_js_call(self, cbname, msg):
       
   434         return "javascript:userCallbackThenReloadPage('%s', '%s')" % (cbname, msg)
       
   435 
       
   436     build_js = build_update_js_call # expect updatable component by default
       
   437     
       
   438     def div_id(self):
       
   439         return ''
       
   440 
       
   441 
       
   442 class ComponentMixIn(ReloadableMixIn):
       
   443     """simple mixin for component object"""
       
   444     __registry__ = 'components'
       
   445     __registerer__ = yes_registerer
       
   446     __selectors__ = (yes,)
       
   447     __select__ = classmethod(*__selectors__)
       
   448 
       
   449     def div_class(self):
       
   450         return '%s %s' % (self.propval('htmlclass'), self.id)
       
   451 
       
   452     def div_id(self):
       
   453         return '%sComponent' % self.id
       
   454 
       
   455 
       
   456 class Component(ComponentMixIn, AppObject):
       
   457     """base class for non displayable components
       
   458     """
       
   459 
       
   460 class SingletonComponent(Component):
       
   461     """base class for non displayable unique components
       
   462     """
       
   463     __registerer__ = priority_registerer