# HG changeset patch # User sylvain.thenault@logilab.fr # Date 1234904225 -3600 # Node ID a2471775aef62f16b396f58b8cdd534870053ba1 # Parent cc149f4def1e76eeb8b110931f42b3d91559015e move view and appobject from cw.common to cw diff -r cc149f4def1e -r a2471775aef6 appobject.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/appobject.py Tue Feb 17 21:57:05 2009 +0100 @@ -0,0 +1,333 @@ +"""Base class for dynamically loaded objects manipulated in the web interface + +:organization: Logilab +:copyright: 2001-2009 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.decorators import classproperty +from logilab.common.deprecation import obsolete + +from rql.nodes import VariableRef, SubQuery +from rql.stmts import Union, Select + +from cubicweb import Unauthorized +from cubicweb.vregistry import VObject +from cubicweb.selectors import yes +from cubicweb.common.utils import UStringIO +from cubicweb.common.uilib import html_escape, ustrftime +from cubicweb.common.registerers import yes_registerer, priority_registerer + + + +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, *args, **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(*args) + instance.row = kwargs.pop('row', None) + instance.col = kwargs.pop('col', None) + instance.selection_kwargs = kwargs + 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) + + @classproperty + @obsolete('use __select__ and & or | operators') + def __selectors__(cls): + if isinstance(self.__select__, AndSelector): + return self.__select__.selectors + return self.__select__ + + @classmethod + def __init__(self, req=None, rset=None): + 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')) + + +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) diff -r cc149f4def1e -r a2471775aef6 common/appobject.py --- a/common/appobject.py Tue Feb 17 21:50:41 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,333 +0,0 @@ -"""Base class for dynamically loaded objects manipulated in the web interface - -:organization: Logilab -:copyright: 2001-2009 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.decorators import classproperty -from logilab.common.deprecation import obsolete - -from rql.nodes import VariableRef, SubQuery -from rql.stmts import Union, Select - -from cubicweb import Unauthorized -from cubicweb.vregistry import VObject -from cubicweb.selectors import yes -from cubicweb.common.utils import UStringIO -from cubicweb.common.uilib import html_escape, ustrftime -from cubicweb.common.registerers import yes_registerer, priority_registerer - - - -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, *args, **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(*args) - instance.row = kwargs.pop('row', None) - instance.col = kwargs.pop('col', None) - instance.selection_kwargs = kwargs - 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) - - @classproperty - @obsolete('use __select__ and & or | operators') - def __selectors__(cls): - if isinstance(self.__select__, AndSelector): - return self.__select__.selectors - return self.__select__ - - @classmethod - def __init__(self, req=None, rset=None): - 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')) - - -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) diff -r cc149f4def1e -r a2471775aef6 common/view.py --- a/common/view.py Tue Feb 17 21:50:41 2009 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,514 +0,0 @@ -"""abstract views and templates classes for CubicWeb web client - - -:organization: Logilab -:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -""" -__docformat__ = "restructuredtext en" - -from cStringIO import StringIO - -from logilab.mtconverter import html_escape - -from cubicweb import NotAnEntity, NoSelectableObject -from cubicweb.selectors import (yes, match_user_groups, implements, - nonempty_rset, none_rset) -from cubicweb.selectors import require_group_compat, accepts_compat -from cubicweb.common.registerers import accepts_registerer, priority_registerer, yes_registerer -from cubicweb.common.appobject import AppRsetObject -from cubicweb.common.utils import UStringIO, HTMLStream - -_ = unicode - - -# robots control -NOINDEX = u'' -NOFOLLOW = u'' - -CW_XHTML_EXTENSIONS = '''[ - - - ] ''' - -TRANSITIONAL_DOCTYPE = u'\n' - -STRICT_DOCTYPE = u'\n' - -# base view object ############################################################ - -class View(AppRsetObject): - """abstract view class, used as base for every renderable object such - as views, templates, some components...web - - A view is instantiated to render a [part of a] result set. View - subclasses may be parametred using the following class attributes: - - * `templatable` indicates if the view may be embeded in a main - template or if it has to be rendered standalone (i.e. XML for - instance) - * if the view is not templatable, it should set the `content_type` class - attribute to the correct MIME type (text/xhtml by default) - * the `category` attribute may be used in the interface to regroup related - objects together - - At instantiation time, the standard `req`, `rset`, and `cursor` - attributes are added and the `w` attribute will be set at rendering - time to a write function to use. - """ - __registerer__ = priority_registerer - __registry__ = 'views' - - templatable = True - need_navigation = True - # content_type = 'application/xhtml+xml' # text/xhtml' - binary = False - add_to_breadcrumbs = True - category = 'view' - - def __init__(self, req=None, rset=None): - super(View, self).__init__(req, rset) - self.w = None - - @property - def content_type(self): - if self.req.xhtml_browser(): - return 'application/xhtml+xml' - return 'text/html' - - def set_stream(self, w=None): - if self.w is not None: - return - if w is None: - if self.binary: - self._stream = stream = StringIO() - else: - self._stream = stream = UStringIO() - w = stream.write - else: - stream = None - self.w = w - return stream - - # main view interface ##################################################### - - def dispatch(self, w=None, **context): - """called to render a view object for a result set. - - This method is a dispatched to an actual method selected - according to optional row and col parameters, which are locating - a particular row or cell in the result set: - - * if row [and col] are specified, `cell_call` is called - * if none of them is supplied, the view is considered to apply on - the whole result set (which may be None in this case), `call` is - called - """ - row, col = context.get('row'), context.get('col') - if row is not None: - context.setdefault('col', 0) - view_func = self.cell_call - else: - view_func = self.call - stream = self.set_stream(w) - # stream = self.set_stream(context) - view_func(**context) - # return stream content if we have created it - if stream is not None: - return self._stream.getvalue() - - # should default .call() method add a
around each - # rset item - add_div_section = True - - def call(self, **kwargs): - """the view is called for an entire result set, by default loop - other rows of the result set and call the same view on the - particular row - - Views applicable on None result sets have to override this method - """ - rset = self.rset - if rset is None: - raise NotImplementedError, self - wrap = self.templatable and len(rset) > 1 and self.add_div_section - for i in xrange(len(rset)): - if wrap: - self.w(u'
') - self.wview(self.id, rset, row=i, **kwargs) - if wrap: - self.w(u"
") - - def cell_call(self, row, col, **kwargs): - """the view is called for a particular result set cell""" - raise NotImplementedError, self - - def linkable(self): - """return True if the view may be linked in a menu - - by default views without title are not meant to be displayed - """ - if not getattr(self, 'title', None): - return False - return True - - def is_primary(self): - return self.id == 'primary' - - def url(self): - """return the url associated with this view. Should not be - necessary for non linkable views, but a default implementation - is provided anyway. - """ - try: - return self.build_url(vid=self.id, rql=self.req.form['rql']) - except KeyError: - return self.build_url(vid=self.id) - - def set_request_content_type(self): - """set the content type returned by this view""" - self.req.set_content_type(self.content_type) - - # view utilities ########################################################## - - def view(self, __vid, rset, __fallback_vid=None, **kwargs): - """shortcut to self.vreg.render method avoiding to pass self.req""" - try: - view = self.vreg.select_view(__vid, self.req, rset, **kwargs) - except NoSelectableObject: - if __fallback_vid is None: - raise - view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) - return view.dispatch(**kwargs) - - def wview(self, __vid, rset, __fallback_vid=None, **kwargs): - """shortcut to self.view method automatically passing self.w as argument - """ - self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) - - def whead(self, data): - self.req.html_headers.write(data) - - def wdata(self, data): - """simple helper that escapes `data` and writes into `self.w`""" - self.w(html_escape(data)) - - def action(self, actionid, row=0): - """shortcut to get action object with id `actionid`""" - return self.vreg.select_action(actionid, self.req, self.rset, - row=row) - - def action_url(self, actionid, label=None, row=0): - """simple method to be able to display `actionid` as a link anywhere - """ - action = self.vreg.select_action(actionid, self.req, self.rset, - row=row) - if action: - label = label or self.req._(action.title) - return u'%s' % (html_escape(action.url()), label) - return u'' - - def html_headers(self): - """return a list of html headers (eg something to be inserted between - and of the returned page - - by default return a meta tag to disable robot indexation of the page - """ - return [NOINDEX] - - def page_title(self): - """returns a title according to the result set - used for the - title in the HTML header - """ - vtitle = self.req.form.get('vtitle') - if vtitle: - return self.req._(vtitle) - # class defined title will only be used if the resulting title doesn't - # seem clear enough - vtitle = getattr(self, 'title', None) or u'' - if vtitle: - vtitle = self.req._(vtitle) - rset = self.rset - if rset and rset.rowcount: - if rset.rowcount == 1: - try: - entity = self.complete_entity(0) - # use long_title to get context information if any - clabel = entity.dc_long_title() - except NotAnEntity: - clabel = display_name(self.req, rset.description[0][0]) - clabel = u'%s (%s)' % (clabel, vtitle) - else : - etypes = rset.column_types(0) - if len(etypes) == 1: - etype = iter(etypes).next() - clabel = display_name(self.req, etype, 'plural') - else : - clabel = u'#[*] (%s)' % vtitle - else: - clabel = vtitle - return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title')) - - def output_url_builder( self, name, url, args ): - self.w(u'\n') - - def create_url(self, etype, **kwargs): - """ return the url of the entity creation form for a given entity type""" - return self.req.build_url('add/%s'%etype, **kwargs) - - def field(self, label, value, row=True, show_label=True, w=None, tr=True): - """ read-only field """ - if w is None: - w = self.w - if row: - w(u'
') - if show_label: - if tr: - label = display_name(self.req, label) - w(u'%s' % label) - w(u'
%s
' % value) - if row: - w(u'
') - - -# concrete views base classes ################################################# - -class EntityView(View): - """base class for views applying on an entity (i.e. uniform result set) - """ - # XXX deprecate - __registerer__ = accepts_registerer - __selectors__ = (implements('Any'),) - registered = accepts_compat(View.registered.im_func) - - category = 'entityview' - - -class StartupView(View): - """base class for views which doesn't need a particular result set - to be displayed (so they can always be displayed !) - """ - __registerer__ = priority_registerer - __selectors__ = (none_rset,) - registered = require_group_compat(View.registered.im_func) - - category = 'startupview' - - def url(self): - """return the url associated with this view. We can omit rql here""" - return self.build_url('view', vid=self.id) - - def html_headers(self): - """return a list of html headers (eg something to be inserted between - and of the returned page - - by default startup views are indexed - """ - return [] - - -class EntityStartupView(EntityView): - """base class for entity views which may also be applied to None - result set (usually a default rql is provided by the view class) - """ - __selectors__ = ((none_rset | implements('Any')),) - - default_rql = None - - def __init__(self, req, rset): - super(EntityStartupView, self).__init__(req, rset) - if rset is None: - # this instance is not in the "entityview" category - self.category = 'startupview' - - def startup_rql(self): - """return some rql to be executedif the result set is None""" - return self.default_rql - - def call(self, **kwargs): - """override call to execute rql returned by the .startup_rql - method if necessary - """ - if self.rset is None: - self.rset = self.req.execute(self.startup_rql()) - rset = self.rset - for i in xrange(len(rset)): - self.wview(self.id, rset, row=i, **kwargs) - - def url(self): - """return the url associated with this view. We can omit rql if we - are on a result set on which we do not apply. - """ - if not self.__select__(self.req, self.rset): - return self.build_url(vid=self.id) - return super(EntityStartupView, self).url() - - -class AnyRsetView(View): - """base class for views applying on any non empty result sets""" - __selectors__ = (nonempty_rset,) - - category = 'anyrsetview' - - def columns_labels(self, tr=True): - if tr: - translate = display_name - else: - translate = lambda req, val: val - rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support - labels = [] - for colindex, attr in enumerate(rqlstdescr): - # compute column header - if colindex == 0 or attr == 'Any': # find a better label - label = ','.join(translate(self.req, et) - for et in self.rset.column_types(colindex)) - else: - label = translate(self.req, attr) - labels.append(label) - return labels - - -# concrete template base classes ############################################## - -class Template(View): - """a template is almost like a view, except that by default a template - is only used globally (i.e. no result set adaptation) - """ - __registry__ = 'templates' - __selectors__ = (yes,) - - registered = require_group_compat(View.registered.im_func) - - def template(self, oid, **kwargs): - """shortcut to self.registry.render method on the templates registry""" - w = kwargs.pop('w', self.w) - self.vreg.render('templates', oid, self.req, w=w, **kwargs) - - -class MainTemplate(Template): - """main template are primary access point to render a full HTML page. - There is usually at least a regular main template and a simple fallback - one to display error if the first one failed - """ - base_doctype = STRICT_DOCTYPE - - @property - def doctype(self): - if self.req.xhtml_browser(): - return self.base_doctype % CW_XHTML_EXTENSIONS - return self.base_doctype % '' - - def set_stream(self, w=None, templatable=True): - if templatable and self.w is not None: - return - - if w is None: - if self.binary: - self._stream = stream = StringIO() - elif not templatable: - # not templatable means we're using a non-html view, we don't - # want the HTMLStream stuff to interfere during data generation - self._stream = stream = UStringIO() - else: - self._stream = stream = HTMLStream(self.req) - w = stream.write - else: - stream = None - self.w = w - return stream - - def write_doctype(self, xmldecl=True): - assert isinstance(self._stream, HTMLStream) - self._stream.doctype = self.doctype - if not xmldecl: - self._stream.xmldecl = u'' - -# concrete component base classes ############################################# - -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 Component(ReloadableMixIn, View): - """base class for components""" - __registry__ = 'components' - __registerer__ = yes_registerer - __selectors__ = (yes,) - property_defs = { - _('visible'): dict(type='Boolean', default=True, - help=_('display the box or not')), - } - - def div_class(self): - return '%s %s' % (self.propval('htmlclass'), self.id) - - def div_id(self): - return '%sComponent' % self.id diff -r cc149f4def1e -r a2471775aef6 view.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view.py Tue Feb 17 21:57:05 2009 +0100 @@ -0,0 +1,514 @@ +"""abstract views and templates classes for CubicWeb web client + + +:organization: Logilab +:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" +__docformat__ = "restructuredtext en" + +from cStringIO import StringIO + +from logilab.mtconverter import html_escape + +from cubicweb import NotAnEntity, NoSelectableObject +from cubicweb.selectors import (yes, match_user_groups, implements, + nonempty_rset, none_rset) +from cubicweb.selectors import require_group_compat, accepts_compat +from cubicweb.common.registerers import accepts_registerer, priority_registerer, yes_registerer +from cubicweb.common.appobject import AppRsetObject +from cubicweb.common.utils import UStringIO, HTMLStream + +_ = unicode + + +# robots control +NOINDEX = u'' +NOFOLLOW = u'' + +CW_XHTML_EXTENSIONS = '''[ + + + ] ''' + +TRANSITIONAL_DOCTYPE = u'\n' + +STRICT_DOCTYPE = u'\n' + +# base view object ############################################################ + +class View(AppRsetObject): + """abstract view class, used as base for every renderable object such + as views, templates, some components...web + + A view is instantiated to render a [part of a] result set. View + subclasses may be parametred using the following class attributes: + + * `templatable` indicates if the view may be embeded in a main + template or if it has to be rendered standalone (i.e. XML for + instance) + * if the view is not templatable, it should set the `content_type` class + attribute to the correct MIME type (text/xhtml by default) + * the `category` attribute may be used in the interface to regroup related + objects together + + At instantiation time, the standard `req`, `rset`, and `cursor` + attributes are added and the `w` attribute will be set at rendering + time to a write function to use. + """ + __registerer__ = priority_registerer + __registry__ = 'views' + + templatable = True + need_navigation = True + # content_type = 'application/xhtml+xml' # text/xhtml' + binary = False + add_to_breadcrumbs = True + category = 'view' + + def __init__(self, req=None, rset=None): + super(View, self).__init__(req, rset) + self.w = None + + @property + def content_type(self): + if self.req.xhtml_browser(): + return 'application/xhtml+xml' + return 'text/html' + + def set_stream(self, w=None): + if self.w is not None: + return + if w is None: + if self.binary: + self._stream = stream = StringIO() + else: + self._stream = stream = UStringIO() + w = stream.write + else: + stream = None + self.w = w + return stream + + # main view interface ##################################################### + + def dispatch(self, w=None, **context): + """called to render a view object for a result set. + + This method is a dispatched to an actual method selected + according to optional row and col parameters, which are locating + a particular row or cell in the result set: + + * if row [and col] are specified, `cell_call` is called + * if none of them is supplied, the view is considered to apply on + the whole result set (which may be None in this case), `call` is + called + """ + row, col = context.get('row'), context.get('col') + if row is not None: + context.setdefault('col', 0) + view_func = self.cell_call + else: + view_func = self.call + stream = self.set_stream(w) + # stream = self.set_stream(context) + view_func(**context) + # return stream content if we have created it + if stream is not None: + return self._stream.getvalue() + + # should default .call() method add a
around each + # rset item + add_div_section = True + + def call(self, **kwargs): + """the view is called for an entire result set, by default loop + other rows of the result set and call the same view on the + particular row + + Views applicable on None result sets have to override this method + """ + rset = self.rset + if rset is None: + raise NotImplementedError, self + wrap = self.templatable and len(rset) > 1 and self.add_div_section + for i in xrange(len(rset)): + if wrap: + self.w(u'
') + self.wview(self.id, rset, row=i, **kwargs) + if wrap: + self.w(u"
") + + def cell_call(self, row, col, **kwargs): + """the view is called for a particular result set cell""" + raise NotImplementedError, self + + def linkable(self): + """return True if the view may be linked in a menu + + by default views without title are not meant to be displayed + """ + if not getattr(self, 'title', None): + return False + return True + + def is_primary(self): + return self.id == 'primary' + + def url(self): + """return the url associated with this view. Should not be + necessary for non linkable views, but a default implementation + is provided anyway. + """ + try: + return self.build_url(vid=self.id, rql=self.req.form['rql']) + except KeyError: + return self.build_url(vid=self.id) + + def set_request_content_type(self): + """set the content type returned by this view""" + self.req.set_content_type(self.content_type) + + # view utilities ########################################################## + + def view(self, __vid, rset, __fallback_vid=None, **kwargs): + """shortcut to self.vreg.render method avoiding to pass self.req""" + try: + view = self.vreg.select_view(__vid, self.req, rset, **kwargs) + except NoSelectableObject: + if __fallback_vid is None: + raise + view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) + return view.dispatch(**kwargs) + + def wview(self, __vid, rset, __fallback_vid=None, **kwargs): + """shortcut to self.view method automatically passing self.w as argument + """ + self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) + + def whead(self, data): + self.req.html_headers.write(data) + + def wdata(self, data): + """simple helper that escapes `data` and writes into `self.w`""" + self.w(html_escape(data)) + + def action(self, actionid, row=0): + """shortcut to get action object with id `actionid`""" + return self.vreg.select_action(actionid, self.req, self.rset, + row=row) + + def action_url(self, actionid, label=None, row=0): + """simple method to be able to display `actionid` as a link anywhere + """ + action = self.vreg.select_action(actionid, self.req, self.rset, + row=row) + if action: + label = label or self.req._(action.title) + return u'%s' % (html_escape(action.url()), label) + return u'' + + def html_headers(self): + """return a list of html headers (eg something to be inserted between + and of the returned page + + by default return a meta tag to disable robot indexation of the page + """ + return [NOINDEX] + + def page_title(self): + """returns a title according to the result set - used for the + title in the HTML header + """ + vtitle = self.req.form.get('vtitle') + if vtitle: + return self.req._(vtitle) + # class defined title will only be used if the resulting title doesn't + # seem clear enough + vtitle = getattr(self, 'title', None) or u'' + if vtitle: + vtitle = self.req._(vtitle) + rset = self.rset + if rset and rset.rowcount: + if rset.rowcount == 1: + try: + entity = self.complete_entity(0) + # use long_title to get context information if any + clabel = entity.dc_long_title() + except NotAnEntity: + clabel = display_name(self.req, rset.description[0][0]) + clabel = u'%s (%s)' % (clabel, vtitle) + else : + etypes = rset.column_types(0) + if len(etypes) == 1: + etype = iter(etypes).next() + clabel = display_name(self.req, etype, 'plural') + else : + clabel = u'#[*] (%s)' % vtitle + else: + clabel = vtitle + return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title')) + + def output_url_builder( self, name, url, args ): + self.w(u'\n') + + def create_url(self, etype, **kwargs): + """ return the url of the entity creation form for a given entity type""" + return self.req.build_url('add/%s'%etype, **kwargs) + + def field(self, label, value, row=True, show_label=True, w=None, tr=True): + """ read-only field """ + if w is None: + w = self.w + if row: + w(u'
') + if show_label: + if tr: + label = display_name(self.req, label) + w(u'%s' % label) + w(u'
%s
' % value) + if row: + w(u'
') + + +# concrete views base classes ################################################# + +class EntityView(View): + """base class for views applying on an entity (i.e. uniform result set) + """ + # XXX deprecate + __registerer__ = accepts_registerer + __selectors__ = (implements('Any'),) + registered = accepts_compat(View.registered.im_func) + + category = 'entityview' + + +class StartupView(View): + """base class for views which doesn't need a particular result set + to be displayed (so they can always be displayed !) + """ + __registerer__ = priority_registerer + __selectors__ = (none_rset,) + registered = require_group_compat(View.registered.im_func) + + category = 'startupview' + + def url(self): + """return the url associated with this view. We can omit rql here""" + return self.build_url('view', vid=self.id) + + def html_headers(self): + """return a list of html headers (eg something to be inserted between + and of the returned page + + by default startup views are indexed + """ + return [] + + +class EntityStartupView(EntityView): + """base class for entity views which may also be applied to None + result set (usually a default rql is provided by the view class) + """ + __selectors__ = ((none_rset | implements('Any')),) + + default_rql = None + + def __init__(self, req, rset): + super(EntityStartupView, self).__init__(req, rset) + if rset is None: + # this instance is not in the "entityview" category + self.category = 'startupview' + + def startup_rql(self): + """return some rql to be executedif the result set is None""" + return self.default_rql + + def call(self, **kwargs): + """override call to execute rql returned by the .startup_rql + method if necessary + """ + if self.rset is None: + self.rset = self.req.execute(self.startup_rql()) + rset = self.rset + for i in xrange(len(rset)): + self.wview(self.id, rset, row=i, **kwargs) + + def url(self): + """return the url associated with this view. We can omit rql if we + are on a result set on which we do not apply. + """ + if not self.__select__(self.req, self.rset): + return self.build_url(vid=self.id) + return super(EntityStartupView, self).url() + + +class AnyRsetView(View): + """base class for views applying on any non empty result sets""" + __selectors__ = (nonempty_rset,) + + category = 'anyrsetview' + + def columns_labels(self, tr=True): + if tr: + translate = display_name + else: + translate = lambda req, val: val + rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support + labels = [] + for colindex, attr in enumerate(rqlstdescr): + # compute column header + if colindex == 0 or attr == 'Any': # find a better label + label = ','.join(translate(self.req, et) + for et in self.rset.column_types(colindex)) + else: + label = translate(self.req, attr) + labels.append(label) + return labels + + +# concrete template base classes ############################################## + +class Template(View): + """a template is almost like a view, except that by default a template + is only used globally (i.e. no result set adaptation) + """ + __registry__ = 'templates' + __selectors__ = (yes,) + + registered = require_group_compat(View.registered.im_func) + + def template(self, oid, **kwargs): + """shortcut to self.registry.render method on the templates registry""" + w = kwargs.pop('w', self.w) + self.vreg.render('templates', oid, self.req, w=w, **kwargs) + + +class MainTemplate(Template): + """main template are primary access point to render a full HTML page. + There is usually at least a regular main template and a simple fallback + one to display error if the first one failed + """ + base_doctype = STRICT_DOCTYPE + + @property + def doctype(self): + if self.req.xhtml_browser(): + return self.base_doctype % CW_XHTML_EXTENSIONS + return self.base_doctype % '' + + def set_stream(self, w=None, templatable=True): + if templatable and self.w is not None: + return + + if w is None: + if self.binary: + self._stream = stream = StringIO() + elif not templatable: + # not templatable means we're using a non-html view, we don't + # want the HTMLStream stuff to interfere during data generation + self._stream = stream = UStringIO() + else: + self._stream = stream = HTMLStream(self.req) + w = stream.write + else: + stream = None + self.w = w + return stream + + def write_doctype(self, xmldecl=True): + assert isinstance(self._stream, HTMLStream) + self._stream.doctype = self.doctype + if not xmldecl: + self._stream.xmldecl = u'' + +# concrete component base classes ############################################# + +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 Component(ReloadableMixIn, View): + """base class for components""" + __registry__ = 'components' + __registerer__ = yes_registerer + __selectors__ = (yes,) + property_defs = { + _('visible'): dict(type='Boolean', default=True, + help=_('display the box or not')), + } + + def div_class(self): + return '%s %s' % (self.propval('htmlclass'), self.id) + + def div_id(self): + return '%sComponent' % self.id