--- a/view.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,534 +0,0 @@
-# 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 <http://www.gnu.org/licenses/>.
-"""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'<meta name="ROBOTS" content="NOINDEX" />'
-NOFOLLOW = u'<meta name="ROBOTS" content="NOFOLLOW" />'
-
-TRANSITIONAL_DOCTYPE_NOEXT = u'<!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 = u'<!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
-
-# 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 <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(u'<div class="section">')
- self.wview(self.__regid__, rset, row=i, **kwargs)
- if wrap:
- self.w(u"</div>")
- else:
- if wrap:
- self.w(u'<div class="section">')
- kwargs.setdefault('col', 0)
- self.cell_call(row=0, **kwargs)
- if wrap:
- self.w(u"</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):
- 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
- <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 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'<tr class="entityfield">')
- else:
- w(u'<div class="entityfield">')
- if show_label and label:
- if tr:
- label = display_name(self._cw, label)
- if table:
- w(u'<th>%s</th>' % label)
- else:
- w(u'<span class="label">%s</span> ' % label)
- if table:
- if not (show_label and label):
- w(u'<td colspan="2">%s</td></tr>' % value)
- else:
- w(u'<td>%s</td></tr>' % value)
- else:
- w(u'<span>%s</span></div>' % 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
- <head> and </head> 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 = '<!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 = 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)