diff -r 000000000000 -r b97547f5f1fa common/appobject.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/common/appobject.py Wed Nov 05 15:52:50 2008 +0100 @@ -0,0 +1,461 @@ +"""Base class for dynamically loaded objects manipulated in the web interface + +: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 mx.DateTime import now, oneSecond +from simplejson import dumps + +from logilab.common.deprecation import obsolete +from rql.stmts import Union, Select + +from cubicweb import Unauthorized +from cubicweb.vregistry import VObject +from cubicweb.common.utils import UStringIO +from cubicweb.common.uilib import html_escape, ustrftime +from cubicweb.common.registerers import yes_registerer, priority_registerer +from cubicweb.common.selectors import yes_selector + +_MARKER = object() + + +class Cache(dict): + def __init__(self): + super(Cache, self).__init__() + self.cache_creation_date = None + self.latest_cache_lookup = now() + +CACHE_REGISTRY = {} + +class AppRsetObject(VObject): + """This is the base class for CubicWeb application objects + which are selected according to a request and result set. + + Classes are kept in the vregistry and instantiation is done at selection + time. + + At registration time, the following attributes are set on the class: + :vreg: + the application's registry + :schema: + the application's schema + :config: + the application's configuration + + At instantiation time, the following attributes are set on the instance: + :req: + current request + :rset: + result set on which the object is applied + """ + + @classmethod + def registered(cls, vreg): + cls.vreg = vreg + cls.schema = vreg.schema + cls.config = vreg.config + cls.register_properties() + return cls + + @classmethod + def selected(cls, req, rset, row=None, col=None, **kwargs): + """by default web app objects are usually instantiated on + selection according to a request, a result set, and optional + row and col + """ + instance = cls(req, rset) + instance.row = row + instance.col = col + return instance + + # Eproperties definition: + # key: id of the property (the actual EProperty key is build using + # .. + # value: tuple (property type, vocabfunc, default value, property description) + # possible types are those used by `logilab.common.configuration` + # + # notice that when it exists multiple objects with the same id (adaptation, + # overriding) only the first encountered definition is considered, so those + # objects can't try to have different default values for instance. + + property_defs = {} + + @classmethod + def register_properties(cls): + for propid, pdef in cls.property_defs.items(): + pdef = pdef.copy() # may be shared + pdef['default'] = getattr(cls, propid, pdef['default']) + pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) + cls.vreg.register_property(cls.propkey(propid), **pdef) + + @classmethod + def propkey(cls, propid): + return '%s.%s.%s' % (cls.__registry__, cls.id, propid) + + + def __init__(self, req, rset): + super(AppRsetObject, self).__init__() + self.req = req + self.rset = rset + + @property + def cursor(self): # XXX deprecate in favor of req.cursor? + msg = '.cursor is deprecated, use req.execute (or req.cursor if necessary)' + warn(msg, DeprecationWarning, stacklevel=2) + return self.req.cursor + + def get_cache(self, cachename): + """ + NOTE: cachename should be dotted names as in : + - cubicweb.mycache + - cubes.blog.mycache + - etc. + """ + if cachename in CACHE_REGISTRY: + cache = CACHE_REGISTRY[cachename] + else: + cache = Cache() + CACHE_REGISTRY[cachename] = cache + _now = now() + if _now > cache.latest_cache_lookup + oneSecond: + ecache = self.req.execute('Any C,T WHERE C is ECache, C name %(name)s, C timestamp T', + {'name':cachename}).get_entity(0,0) + cache.latest_cache_lookup = _now + if not ecache.valid(cache.cache_creation_date): + cache.empty() + cache.cache_creation_date = _now + return cache + + def propval(self, propid): + assert self.req + return self.req.property_value(self.propkey(propid)) + + + def limited_rql(self): + """return a printable rql for the result set associated to the object, + with limit/offset correctly set according to maximum page size and + currently displayed page when necessary + """ + # try to get page boundaries from the navigation component + # XXX we should probably not have a ref to this component here (eg in + # cubicweb.common) + nav = self.vreg.select_component('navigation', self.req, self.rset) + if nav: + start, stop = nav.page_boundaries() + rql = self._limit_offset_rql(stop - start, start) + # result set may have be limited manually in which case navigation won't + # apply + elif self.rset.limited: + rql = self._limit_offset_rql(*self.rset.limited) + # navigation component doesn't apply and rset has not been limited, no + # need to limit query + else: + rql = self.rset.printable_rql() + return rql + + def _limit_offset_rql(self, limit, offset): + rqlst = self.rset.syntax_tree() + if len(rqlst.children) == 1: + select = rqlst.children[0] + olimit, ooffset = select.limit, select.offset + select.limit, select.offset = limit, offset + rql = rqlst.as_string(kwargs=self.rset.args) + # restore original limit/offset + select.limit, select.offset = olimit, ooffset + else: + newselect = Select() + newselect.limit = limit + newselect.offset = offset + aliases = [VariableRef(newselect.get_variable(vref.name, i)) + for i, vref in enumerate(rqlst.selection)] + newselect.set_with([SubQuery(aliases, rqlst)], check=False) + newunion = Union() + newunion.append(newselect) + rql = rqlst.as_string(kwargs=self.rset.args) + rqlst.parent = None + return rql + + # url generation methods ################################################## + + controller = 'view' + + def build_url(self, method=None, **kwargs): + """return an absolute URL using params dictionary key/values as URL + parameters. Values are automatically URL quoted, and the + publishing method to use may be specified or will be guessed. + """ + # XXX I (adim) think that if method is passed explicitly, we should + # not try to process it and directly call req.build_url() + if method is None: + method = self.controller + if method == 'view' and self.req.from_controller() == 'view' and \ + not '_restpath' in kwargs: + method = self.req.relative_path(includeparams=False) or 'view' + return self.req.build_url(method, **kwargs) + + # various resources accessors ############################################# + + def etype_rset(self, etype, size=1): + """return a fake result set for a particular entity type""" + msg = '.etype_rset is deprecated, use req.etype_rset' + warn(msg, DeprecationWarning, stacklevel=2) + return self.req.etype_rset(etype, size=1) + + def eid_rset(self, eid, etype=None): + """return a result set for the given eid""" + msg = '.eid_rset is deprecated, use req.eid_rset' + warn(msg, DeprecationWarning, stacklevel=2) + return self.req.eid_rset(eid, etype) + + def entity(self, row, col=0): + """short cut to get an entity instance for a particular row/column + (col default to 0) + """ + return self.rset.get_entity(row, col) + + def complete_entity(self, row, col=0, skip_bytes=True): + """short cut to get an completed entity instance for a particular + row (all instance's attributes have been fetched) + """ + entity = self.entity(row, col) + entity.complete(skip_bytes=skip_bytes) + return entity + + def user_rql_callback(self, args, msg=None): + """register a user callback to execute some rql query and return an url + to call it ready to be inserted in html + """ + def rqlexec(req, rql, args=None, key=None): + req.execute(rql, args, key) + return self.user_callback(rqlexec, args, msg) + + def user_callback(self, cb, args, msg=None, nonify=False): + """register the given user callback and return an url to call it ready to be + inserted in html + """ + self.req.add_js('cubicweb.ajax.js') + if nonify: + # XXX < 2.48.3 bw compat + warn('nonify argument is deprecated', DeprecationWarning, stacklevel=2) + _cb = cb + def cb(*args): + _cb(*args) + cbname = self.req.register_onetime_callback(cb, *args) + msg = dumps(msg or '') + return "javascript:userCallbackThenReloadPage('%s', %s)" % ( + cbname, msg) + + # formating methods ####################################################### + + def tal_render(self, template, variables): + """render a precompiled page template with variables in the given + dictionary as context + """ + from cubicweb.common.tal import CubicWebContext + context = CubicWebContext() + context.update({'self': self, 'rset': self.rset, '_' : self.req._, + 'req': self.req, 'user': self.req.user}) + context.update(variables) + output = UStringIO() + template.expand(context, output) + return output.getvalue() + + def format_date(self, date, date_format=None, time=False): + """return a string for a mx date time according to application's + configuration + """ + if date: + if date_format is None: + if time: + date_format = self.req.property_value('ui.datetime-format') + else: + date_format = self.req.property_value('ui.date-format') + return ustrftime(date, date_format) + return u'' + + def format_time(self, time): + """return a string for a mx date time according to application's + configuration + """ + if time: + return ustrftime(time, self.req.property_value('ui.time-format')) + return u'' + + def format_float(self, num): + """return a string for floating point number according to application's + configuration + """ + if num: + return self.req.property_value('ui.float-format') % num + return u'' + + # security related methods ################################################ + + def ensure_ro_rql(self, rql): + """raise an exception if the given rql is not a select query""" + first = rql.split(' ', 1)[0].lower() + if first in ('insert', 'set', 'delete'): + raise Unauthorized(self.req._('only select queries are authorized')) + + # .accepts handling utilities ############################################# + + accepts = ('Any',) + + @classmethod + def accept_rset(cls, req, rset, row, col): + """apply the following rules: + * if row is None, return the sum of values returned by the method + for each entity's type in the result set. If any score is 0, + return 0. + * if row is specified, return the value returned by the method with + the entity's type of this row + """ + if row is None: + score = 0 + for etype in rset.column_types(0): + accepted = cls.accept(req.user, etype) + if not accepted: + return 0 + score += accepted + return score + return cls.accept(req.user, rset.description[row][col or 0]) + + @classmethod + def accept(cls, user, etype): + """score etype, returning better score on exact match""" + if 'Any' in cls.accepts: + return 1 + eschema = cls.schema.eschema(etype) + matching_types = [e.type for e in eschema.ancestors()] + matching_types.append(etype) + for index, basetype in enumerate(matching_types): + if basetype in cls.accepts: + return 2 + index + return 0 + + # .rtype handling utilities ############################################## + + @classmethod + def relation_possible(cls, etype): + """tell if a relation with etype entity is possible according to + mixed class'.etype, .rtype and .target attributes + + XXX should probably be moved out to a function + """ + schema = cls.schema + rtype = cls.rtype + eschema = schema.eschema(etype) + if hasattr(cls, 'role'): + role = cls.role + elif cls.target == 'subject': + role = 'object' + else: + role = 'subject' + # check if this relation is possible according to the schema + try: + if role == 'object': + rschema = eschema.object_relation(rtype) + else: + rschema = eschema.subject_relation(rtype) + except KeyError: + return False + if hasattr(cls, 'etype'): + letype = cls.etype + try: + if role == 'object': + return etype in rschema.objects(letype) + else: + return etype in rschema.subjects(letype) + except KeyError, ex: + return False + return True + + + # XXX deprecated (since 2.43) ########################## + + @obsolete('use req.datadir_url') + def datadir_url(self): + """return url of the application's data directory""" + return self.req.datadir_url + + @obsolete('use req.external_resource()') + def external_resource(self, rid, default=_MARKER): + return self.req.external_resource(rid, default) + + +class AppObject(AppRsetObject): + """base class for application objects which are not selected + according to a result set, only by their identifier. + + Those objects may not have req, rset and cursor set. + """ + + @classmethod + def selected(cls, *args, **kwargs): + """by default web app objects are usually instantiated on + selection + """ + return cls(*args, **kwargs) + + def __init__(self, req=None, rset=None, **kwargs): + self.req = req + self.rset = rset + self.__dict__.update(kwargs) + + +class ReloadableMixIn(object): + """simple mixin for reloadable parts of UI""" + + def user_callback(self, cb, args, msg=None, nonify=False): + """register the given user callback and return an url to call it ready to be + inserted in html + """ + self.req.add_js('cubicweb.ajax.js') + if nonify: + _cb = cb + def cb(*args): + _cb(*args) + cbname = self.req.register_onetime_callback(cb, *args) + return self.build_js(cbname, html_escape(msg or '')) + + def build_update_js_call(self, cbname, msg): + rql = html_escape(self.rset.printable_rql()) + return "javascript:userCallbackThenUpdateUI('%s', '%s', '%s', '%s', '%s', '%s')" % ( + cbname, self.id, rql, msg, self.__registry__, self.div_id()) + + def build_reload_js_call(self, cbname, msg): + return "javascript:userCallbackThenReloadPage('%s', '%s')" % (cbname, msg) + + build_js = build_update_js_call # expect updatable component by default + + def div_id(self): + return '' + + +class ComponentMixIn(ReloadableMixIn): + """simple mixin for component object""" + __registry__ = 'components' + __registerer__ = yes_registerer + __selectors__ = (yes_selector,) + __select__ = classmethod(*__selectors__) + + def div_class(self): + return '%s %s' % (self.propval('htmlclass'), self.id) + + def div_id(self): + return '%sComponent' % self.id + + +class Component(ComponentMixIn, AppObject): + """base class for non displayable components + """ + +class SingletonComponent(Component): + """base class for non displayable unique components + """ + __registerer__ = priority_registerer