Source code for cubicweb.view

# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact https://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 <https://www.gnu.org/licenses/>.
"""abstract views and templates classes for CubicWeb web client"""


from cubicweb import entity
from logilab.common.deprecation import class_moved
from cubicweb import _

import re
from io import BytesIO
from warnings import warn
from functools import partial
from inspect import getframeinfo, stack

from logilab.common.registry import yes
from logilab.mtconverter import xml_escape

from rql import nodes

from cubicweb import NotAnEntity, console
from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset
from cubicweb.appobject import AppObject
from cubicweb.utils import UStringIO, HTMLStream, format_and_escape_string
from cubicweb.uilib import domid, js
from cubicweb.schema import display_name

# robots control
NOINDEX = '<meta name="ROBOTS" content="NOINDEX" />'
NOFOLLOW = '<meta name="ROBOTS" content="NOFOLLOW" />'

TRANSITIONAL_DOCTYPE_NOEXT = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
TRANSITIONAL_DOCTYPE = TRANSITIONAL_DOCTYPE_NOEXT  # bw compat

STRICT_DOCTYPE_NOEXT = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'
STRICT_DOCTYPE = STRICT_DOCTYPE_NOEXT  # bw compat


def inject_html_generating_call_on_w():
    View.debug_html_rendering = True


# base view object ############################################################


[docs]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 debug_html_rendering = False 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 w(self, text, *args, escape=True): if self._w is None: raise Exception("Error: call to %s.w before it has been set" % self) if self.debug_html_rendering: caller = getframeinfo(stack()[1][0]) if isinstance(text, str) and re.match(r"^ *<[a-zA-Z0-9]+( .*>|>)", text): to_add = ( 'cubicweb-generated-by="%s.%s" cubicweb-from-source="%s:%s"' % ( self.__module__, self.__class__.__name__, caller.filename, caller.lineno, ) ) before_space, beginning_of_html = text.split("<", 1) # when it's a tag without attribues like "<b>" if re.match(r"^ *<[a-zA-Z0-9]+>", text): tag_name, rest = beginning_of_html.split(">", 1) rest = ">" + rest else: tag_name, rest = beginning_of_html.split(" ", 1) rest = " " + rest text = "%(before_space)s<%(tag_name)s %(to_add)s%(rest)s" % { "before_space": before_space, "tag_name": tag_name, "to_add": to_add, "rest": rest, } text = format_and_escape_string(text, *args, escape=escape) return self._w(text) 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 <div classs="section"> 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('<div class="section">') self.wview(self.__regid__, rset, row=i, **kwargs) if wrap: self.w("</div>") else: if wrap: self.w('<div class="section">') kwargs.setdefault("col", 0) self.cell_call(row=0, **kwargs) if wrap: self.w("</div>") 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 /<etype> 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, *args, **kwargs): self._cw.html_headers.write(data, *args, **kwargs) 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 <head> and </head> 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 "" 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 = "%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 = "#[*] (%s)" % vtitle else: clabel = vtitle return "%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('<tr class="entityfield">') else: w('<div class="entityfield">') if show_label and label: if tr: label = display_name(self._cw, label) if table: w("<th>%s</th>", label, escape=False) else: w('<span class="label">%s</span> ', label, escape=False) if table: if not (show_label and label): w('<td colspan="2">%s</td></tr>', value, escape=False) else: w("<td>%s</td></tr>", value, escape=False) else: w("<span>%s</span></div>", value, escape=False)
# concrete views base classes #################################################
[docs]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__))
[docs]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 <head> and </head> of the returned page by default startup views are indexed """ return []
[docs]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""" 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)
[docs]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 = ",".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 = "<!DOCTYPE html>" 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 = "" 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__) # EntityAdapter moved to cubicweb.entity ###################################### EntityAdapter = class_moved(entity.EntityAdapter) # cubicweb 3.28