diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/view.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/view.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,534 @@ +# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of CubicWeb. +# +# CubicWeb is free software: you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# CubicWeb is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with CubicWeb. If not, see . +"""abstract views and templates classes for CubicWeb web client""" + +__docformat__ = "restructuredtext en" +from cubicweb import _ + +from io import BytesIO +from warnings import warn +from functools import partial + +from six.moves import range + +from logilab.common.deprecation import deprecated +from logilab.common.registry import yes +from logilab.mtconverter import xml_escape + +from rql import nodes + +from cubicweb import NotAnEntity +from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset +from cubicweb.appobject import AppObject +from cubicweb.utils import UStringIO, HTMLStream +from cubicweb.uilib import domid, js +from cubicweb.schema import display_name + +# robots control +NOINDEX = u'' +NOFOLLOW = u'' + +TRANSITIONAL_DOCTYPE_NOEXT = u'\n' +TRANSITIONAL_DOCTYPE = TRANSITIONAL_DOCTYPE_NOEXT # bw compat + +STRICT_DOCTYPE_NOEXT = u'\n' +STRICT_DOCTYPE = STRICT_DOCTYPE_NOEXT # bw compat + +# base view object ############################################################ + +class View(AppObject): + """This class is an abstraction of a view class, used as a base class for + every renderable object such as views, templates and other user interface + components. + + A `View` is instantiated to render a result set or part of a result + set. `View` subclasses may be parametrized using the following class + attributes: + + :py:attr:`templatable` indicates if the view may be embedded in a main + template or if it has to be rendered standalone (i.e. pure XML views must + not be embedded in the main template of HTML pages) + :py:attr:`content_type` if the view is not templatable, it should set the + `content_type` class attribute to the correct MIME type (text/xhtml being + the default) + :py:attr:`category` this attribute may be used in the interface to regroup + related objects (view kinds) together + + :py:attr:`paginable` + + :py:attr:`binary` + + + A view writes to its output stream thanks to its attribute `w` (the + append method of an `UStreamIO`, except for binary views). + + At instantiation time, the standard `_cw`, and `cw_rset` attributes are + added and the `w` attribute will be set at rendering time to a write + function to use. + """ + __registry__ = 'views' + + templatable = True + # content_type = 'application/xhtml+xml' # text/xhtml' + binary = False + add_to_breadcrumbs = True + category = 'view' + paginable = True + + def __init__(self, req=None, rset=None, **kwargs): + super(View, self).__init__(req, rset=rset, **kwargs) + self.w = None + + @property + def content_type(self): + return self._cw.html_content_type() + + def set_stream(self, w=None): + if self.w is not None: + return + if w is None: + if self.binary: + self._stream = stream = BytesIO() + else: + self._stream = stream = UStringIO() + w = stream.write + else: + stream = None + self.w = w + return stream + + # main view interface ##################################################### + + def render(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 is 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 + """ + # XXX use .cw_row/.cw_col + row = context.get('row') + if row is not None: + context.setdefault('col', 0) + view_func = self.cell_call + else: + view_func = self.call + stream = self.set_stream(w) + try: + view_func(**context) + except Exception: + self.debug('view call %s failed (context=%s)', view_func, context) + raise + # return stream content if we have created it + if stream is not None: + return self._stream.getvalue() + + def tal_render(self, template, variables): + """render a precompiled page template with variables in the given + dictionary as context + """ + from cubicweb.ext.tal import CubicWebContext + context = CubicWebContext() + context.update({'self': self, 'rset': self.cw_rset, '_' : self._cw._, + 'req': self._cw, 'user': self._cw.user}) + context.update(variables) + output = UStringIO() + template.expand(context, output) + return output.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.cw_rset + if rset is None: + raise NotImplementedError("%r an rset is required" % self) + wrap = self.templatable and len(rset) > 1 and self.add_div_section + # avoid re-selection if rset of size 1, we already have the most + # specific view + if rset.rowcount != 1: + kwargs.setdefault('initargs', self.cw_extra_kwargs) + for i in range(len(rset)): + if wrap: + self.w(u'
') + self.wview(self.__regid__, rset, row=i, **kwargs) + if wrap: + self.w(u"
") + else: + if wrap: + self.w(u'
') + kwargs.setdefault('col', 0) + self.cell_call(row=0, **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(repr(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.cw_extra_kwargs.get('is_primary', self.__regid__ == '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. + """ + rset = self.cw_rset + if rset is None: + return self._cw.build_url('view', vid=self.__regid__) + coltypes = rset.column_types(0) + if len(coltypes) == 1: + etype = next(iter(coltypes)) + if not self._cw.vreg.schema.eschema(etype).final: + if len(rset) == 1: + entity = rset.get_entity(0, 0) + return entity.absolute_url(vid=self.__regid__) + # don't want to generate / url if there is some restriction + # on something else than the entity type + restr = rset.syntax_tree().children[0].where + # XXX norestriction is not correct here. For instance, in cases like + # "Any P,N WHERE P is Project, P name N" norestriction should equal + # True + norestriction = (isinstance(restr, nodes.Relation) and + restr.is_types_restriction()) + if norestriction: + return self._cw.build_url(etype.lower(), vid=self.__regid__) + return self._cw.build_url('view', rql=rset.printable_rql(), vid=self.__regid__) + + def set_request_content_type(self): + """set the content type returned by this view""" + self._cw.set_content_type(self.content_type) + + # view utilities ########################################################## + + def wview(self, __vid, rset=None, __fallback_vid=None, **kwargs): + """shortcut to self.view method automatically passing self.w as argument + """ + self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) + + def whead(self, data): + self._cw.html_headers.write(data) + + def wdata(self, data): + """simple helper that escapes `data` and writes into `self.w`""" + self.w(xml_escape(data)) + + 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._cw.form.get('vtitle') + if vtitle: + return self._cw._(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._cw._(vtitle) + rset = self.cw_rset + if rset and rset.rowcount: + if rset.rowcount == 1: + try: + entity = rset.complete_entity(0, 0) + # use long_title to get context information if any + clabel = entity.dc_long_title() + except NotAnEntity: + clabel = display_name(self._cw, rset.description[0][0]) + clabel = u'%s (%s)' % (clabel, vtitle) + else : + etypes = rset.column_types(0) + if len(etypes) == 1: + etype = next(iter(etypes)) + clabel = display_name(self._cw, etype, 'plural') + else : + clabel = u'#[*] (%s)' % vtitle + else: + clabel = vtitle + return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title')) + + def field(self, label, value, row=True, show_label=True, w=None, tr=True, + table=False): + """read-only field""" + if w is None: + w = self.w + if table: + w(u'') + else: + w(u'
') + if show_label and label: + if tr: + label = display_name(self._cw, label) + if table: + w(u'%s' % label) + else: + w(u'%s ' % label) + if table: + if not (show_label and label): + w(u'%s' % value) + else: + w(u'%s' % value) + else: + w(u'%s
' % value) + + + +# concrete views base classes ################################################# + +class EntityView(View): + """base class for views applying on an entity (i.e. uniform result set)""" + __select__ = non_final_entity() + category = _('entityview') + + def call(self, **kwargs): + if self.cw_rset is None: + # * cw_extra_kwargs is the place where extra selection arguments are + # stored + # * when calling req.view('somevid', entity=entity), 'entity' ends + # up in cw_extra_kwargs and kwargs + # + # handle that to avoid a TypeError with a sanity check + # + # Notice that could probably be avoided by handling entity_call in + # .render + entity = self.cw_extra_kwargs.pop('entity') + if 'entity' in kwargs: + assert kwargs.pop('entity') is entity + self.entity_call(entity, **kwargs) + else: + super(EntityView, self).call(**kwargs) + + def cell_call(self, row, col, **kwargs): + self.entity_call(self.cw_rset.get_entity(row, col), **kwargs) + + def entity_call(self, entity, **kwargs): + raise NotImplementedError('%r %r' % (self.__regid__, self.__class__)) + + +class StartupView(View): + """base class for views which doesn't need a particular result set to be + displayed (so they can always be displayed!) + """ + __select__ = none_rset() + + category = _('startupview') + + 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) + """ + __select__ = none_rset() | non_final_entity() + + default_rql = None + + def __init__(self, req, rset=None, **kwargs): + super(EntityStartupView, self).__init__(req, rset=rset, **kwargs) + if rset is None: + # this instance is not in the "entityview" category + self.category = 'startupview' + + def startup_rql(self): + """return some rql to be executed if the result set is None""" + return self.default_rql + + def no_entities(self, **kwargs): + """override to display something when no entities were found""" + pass + + def call(self, **kwargs): + """override call to execute rql returned by the .startup_rql method if + necessary + """ + rset = self.cw_rset + if rset is None: + rset = self.cw_rset = self._cw.execute(self.startup_rql()) + if rset: + for i in range(len(rset)): + self.wview(self.__regid__, rset, row=i, **kwargs) + else: + self.no_entities(**kwargs) + + +class AnyRsetView(View): + """base class for views applying on any non empty result sets""" + __select__ = nonempty_rset() + + category = _('anyrsetview') + + def columns_labels(self, mainindex=0, tr=True): + """compute the label of the rset colums + + The logic is based on :meth:`~rql.stmts.Union.get_description`. + + :param mainindex: The index of the main variable. This is an hint to get + more accurate label for various situation + :type mainindex: int + + :param tr: Should the label be translated ? + :type tr: boolean + """ + if tr: + translate = partial(display_name, self._cw) + else: + translate = lambda val, *args,**kwargs: val + # XXX [0] because of missing Union support + rql_syntax_tree = self.cw_rset.syntax_tree() + rqlstdescr = rql_syntax_tree.get_description(mainindex, translate)[0] + labels = [] + for colidx, label in enumerate(rqlstdescr): + labels.append(self.column_label(colidx, label, translate)) + return labels + + def column_label(self, colidx, default, translate_func=None): + """return the label of a specified columns index + + Overwrite me if you need to compute specific label. + + :param colidx: The index of the column the call computes a label for. + :type colidx: int + + :param default: Default value. If ``"Any"`` the default value will be + recomputed as coma separated list for all possible + etypes name. + :type colidx: string + + :param translate_func: A function used to translate name. + :type colidx: function + """ + label = default + if label == 'Any': + etypes = self.cw_rset.column_types(colidx) + if translate_func is not None: + etypes = map(translate_func, etypes) + label = u','.join(etypes) + return label + + + +# concrete template base classes ############################################## + +class MainTemplate(View): + """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 + """ + + doctype = '' + + def set_stream(self, w=None): + if self.w is not None: + return + if w is None: + if self.binary: + self._stream = stream = BytesIO() + else: + self._stream = stream = HTMLStream(self._cw) + 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'' + + def linkable(self): + return False + +# concrete component base classes ############################################# + +class ReloadableMixIn(object): + """simple mixin for reloadable parts of UI""" + + @property + def domid(self): + return domid(self.__regid__) + + +class Component(ReloadableMixIn, View): + """base class for components""" + __registry__ = 'components' + __select__ = yes() + + # XXX huummm, much probably useless (should be...) + htmlclass = 'mainRelated' + @property + def cssclass(self): + return '%s %s' % (self.htmlclass, domid(self.__regid__)) + + # XXX should rely on ReloadableMixIn.domid + @property + def domid(self): + return '%sComponent' % domid(self.__regid__) + + +class Adapter(AppObject): + """base class for adapters""" + __registry__ = 'adapters' + + +class EntityAdapter(Adapter): + """base class for entity adapters (eg adapt an entity to an interface)""" + def __init__(self, _cw, **kwargs): + try: + self.entity = kwargs.pop('entity') + except KeyError: + self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0, + kwargs.get('col') or 0) + Adapter.__init__(self, _cw, **kwargs)