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