view.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """abstract views and templates classes for CubicWeb web client"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 from cubicweb import _
       
    22 
       
    23 from io import BytesIO
       
    24 from warnings import warn
       
    25 from functools import partial
       
    26 
       
    27 from six.moves import range
       
    28 
       
    29 from logilab.common.deprecation import deprecated
       
    30 from logilab.common.registry import yes
       
    31 from logilab.mtconverter import xml_escape
       
    32 
       
    33 from rql import nodes
       
    34 
       
    35 from cubicweb import NotAnEntity
       
    36 from cubicweb.predicates import non_final_entity, nonempty_rset, none_rset
       
    37 from cubicweb.appobject import AppObject
       
    38 from cubicweb.utils import UStringIO, HTMLStream
       
    39 from cubicweb.uilib import domid, js
       
    40 from cubicweb.schema import display_name
       
    41 
       
    42 # robots control
       
    43 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
       
    44 NOFOLLOW = u'<meta name="ROBOTS" content="NOFOLLOW" />'
       
    45 
       
    46 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'
       
    47 TRANSITIONAL_DOCTYPE = TRANSITIONAL_DOCTYPE_NOEXT # bw compat
       
    48 
       
    49 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'
       
    50 STRICT_DOCTYPE = STRICT_DOCTYPE_NOEXT # bw compat
       
    51 
       
    52 # base view object ############################################################
       
    53 
       
    54 class View(AppObject):
       
    55     """This class is an abstraction of a view class, used as a base class for
       
    56     every renderable object such as views, templates and other user interface
       
    57     components.
       
    58 
       
    59     A `View` is instantiated to render a result set or part of a result
       
    60     set. `View` subclasses may be parametrized using the following class
       
    61     attributes:
       
    62 
       
    63     :py:attr:`templatable` indicates if the view may be embedded in a main
       
    64       template or if it has to be rendered standalone (i.e. pure XML views must
       
    65       not be embedded in the main template of HTML pages)
       
    66     :py:attr:`content_type` if the view is not templatable, it should set the
       
    67       `content_type` class attribute to the correct MIME type (text/xhtml being
       
    68       the default)
       
    69     :py:attr:`category` this attribute may be used in the interface to regroup
       
    70       related objects (view kinds) together
       
    71 
       
    72     :py:attr:`paginable`
       
    73 
       
    74     :py:attr:`binary`
       
    75 
       
    76 
       
    77     A view writes to its output stream thanks to its attribute `w` (the
       
    78     append method of an `UStreamIO`, except for binary views).
       
    79 
       
    80     At instantiation time, the standard `_cw`, and `cw_rset` attributes are
       
    81     added and the `w` attribute will be set at rendering time to a write
       
    82     function to use.
       
    83     """
       
    84     __registry__ = 'views'
       
    85 
       
    86     templatable = True
       
    87     # content_type = 'application/xhtml+xml' # text/xhtml'
       
    88     binary = False
       
    89     add_to_breadcrumbs = True
       
    90     category = 'view'
       
    91     paginable = True
       
    92 
       
    93     def __init__(self, req=None, rset=None, **kwargs):
       
    94         super(View, self).__init__(req, rset=rset, **kwargs)
       
    95         self.w = None
       
    96 
       
    97     @property
       
    98     def content_type(self):
       
    99         return self._cw.html_content_type()
       
   100 
       
   101     def set_stream(self, w=None):
       
   102         if self.w is not None:
       
   103             return
       
   104         if w is None:
       
   105             if self.binary:
       
   106                 self._stream = stream = BytesIO()
       
   107             else:
       
   108                 self._stream = stream = UStringIO()
       
   109             w = stream.write
       
   110         else:
       
   111             stream = None
       
   112         self.w = w
       
   113         return stream
       
   114 
       
   115     # main view interface #####################################################
       
   116 
       
   117     def render(self, w=None, **context):
       
   118         """called to render a view object for a result set.
       
   119 
       
   120         This method is a dispatched to an actual method selected
       
   121         according to optional row and col parameters, which are locating
       
   122         a particular row or cell in the result set:
       
   123 
       
   124         * if row is specified, `cell_call` is called
       
   125         * if none of them is supplied, the view is considered to apply on
       
   126           the whole result set (which may be None in this case), `call` is
       
   127           called
       
   128         """
       
   129         # XXX use .cw_row/.cw_col
       
   130         row = context.get('row')
       
   131         if row is not None:
       
   132             context.setdefault('col', 0)
       
   133             view_func = self.cell_call
       
   134         else:
       
   135             view_func = self.call
       
   136         stream = self.set_stream(w)
       
   137         try:
       
   138             view_func(**context)
       
   139         except Exception:
       
   140             self.debug('view call %s failed (context=%s)', view_func, context)
       
   141             raise
       
   142         # return stream content if we have created it
       
   143         if stream is not None:
       
   144             return self._stream.getvalue()
       
   145 
       
   146     def tal_render(self, template, variables):
       
   147         """render a precompiled page template with variables in the given
       
   148         dictionary as context
       
   149         """
       
   150         from cubicweb.ext.tal import CubicWebContext
       
   151         context = CubicWebContext()
       
   152         context.update({'self': self, 'rset': self.cw_rset, '_' : self._cw._,
       
   153                         'req': self._cw, 'user': self._cw.user})
       
   154         context.update(variables)
       
   155         output = UStringIO()
       
   156         template.expand(context, output)
       
   157         return output.getvalue()
       
   158 
       
   159     # should default .call() method add a <div classs="section"> around each
       
   160     # rset item
       
   161     add_div_section = True
       
   162 
       
   163     def call(self, **kwargs):
       
   164         """the view is called for an entire result set, by default loop
       
   165         other rows of the result set and call the same view on the
       
   166         particular row
       
   167 
       
   168         Views applicable on None result sets have to override this method
       
   169         """
       
   170         rset = self.cw_rset
       
   171         if rset is None:
       
   172             raise NotImplementedError("%r an rset is required" % self)
       
   173         wrap = self.templatable and len(rset) > 1 and self.add_div_section
       
   174         # avoid re-selection if rset of size 1, we already have the most
       
   175         # specific view
       
   176         if rset.rowcount != 1:
       
   177             kwargs.setdefault('initargs', self.cw_extra_kwargs)
       
   178             for i in range(len(rset)):
       
   179                 if wrap:
       
   180                     self.w(u'<div class="section">')
       
   181                 self.wview(self.__regid__, rset, row=i, **kwargs)
       
   182                 if wrap:
       
   183                     self.w(u"</div>")
       
   184         else:
       
   185             if wrap:
       
   186                 self.w(u'<div class="section">')
       
   187             kwargs.setdefault('col', 0)
       
   188             self.cell_call(row=0, **kwargs)
       
   189             if wrap:
       
   190                 self.w(u"</div>")
       
   191 
       
   192     def cell_call(self, row, col, **kwargs):
       
   193         """the view is called for a particular result set cell"""
       
   194         raise NotImplementedError(repr(self))
       
   195 
       
   196     def linkable(self):
       
   197         """return True if the view may be linked in a menu
       
   198 
       
   199         by default views without title are not meant to be displayed
       
   200         """
       
   201         if not getattr(self, 'title', None):
       
   202             return False
       
   203         return True
       
   204 
       
   205     def is_primary(self):
       
   206         return self.cw_extra_kwargs.get('is_primary', self.__regid__ == 'primary')
       
   207 
       
   208     def url(self):
       
   209         """return the url associated with this view. Should not be
       
   210         necessary for non linkable views, but a default implementation
       
   211         is provided anyway.
       
   212         """
       
   213         rset = self.cw_rset
       
   214         if rset is None:
       
   215             return self._cw.build_url('view', vid=self.__regid__)
       
   216         coltypes = rset.column_types(0)
       
   217         if len(coltypes) == 1:
       
   218             etype = next(iter(coltypes))
       
   219             if not self._cw.vreg.schema.eschema(etype).final:
       
   220                 if len(rset) == 1:
       
   221                     entity = rset.get_entity(0, 0)
       
   222                     return entity.absolute_url(vid=self.__regid__)
       
   223             # don't want to generate /<etype> url if there is some restriction
       
   224             # on something else than the entity type
       
   225             restr = rset.syntax_tree().children[0].where
       
   226             # XXX norestriction is not correct here. For instance, in cases like
       
   227             # "Any P,N WHERE P is Project, P name N" norestriction should equal
       
   228             # True
       
   229             norestriction = (isinstance(restr, nodes.Relation) and
       
   230                              restr.is_types_restriction())
       
   231             if norestriction:
       
   232                 return self._cw.build_url(etype.lower(), vid=self.__regid__)
       
   233         return self._cw.build_url('view', rql=rset.printable_rql(), vid=self.__regid__)
       
   234 
       
   235     def set_request_content_type(self):
       
   236         """set the content type returned by this view"""
       
   237         self._cw.set_content_type(self.content_type)
       
   238 
       
   239     # view utilities ##########################################################
       
   240 
       
   241     def wview(self, __vid, rset=None, __fallback_vid=None, **kwargs):
       
   242         """shortcut to self.view method automatically passing self.w as argument
       
   243         """
       
   244         self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
       
   245 
       
   246     def whead(self, data):
       
   247         self._cw.html_headers.write(data)
       
   248 
       
   249     def wdata(self, data):
       
   250         """simple helper that escapes `data` and writes into `self.w`"""
       
   251         self.w(xml_escape(data))
       
   252 
       
   253     def html_headers(self):
       
   254         """return a list of html headers (eg something to be inserted between
       
   255         <head> and </head> of the returned page
       
   256 
       
   257         by default return a meta tag to disable robot indexation of the page
       
   258         """
       
   259         return [NOINDEX]
       
   260 
       
   261     def page_title(self):
       
   262         """returns a title according to the result set - used for the
       
   263         title in the HTML header
       
   264         """
       
   265         vtitle = self._cw.form.get('vtitle')
       
   266         if vtitle:
       
   267             return self._cw._(vtitle)
       
   268         # class defined title will only be used if the resulting title doesn't
       
   269         # seem clear enough
       
   270         vtitle = getattr(self, 'title', None) or u''
       
   271         if vtitle:
       
   272             vtitle = self._cw._(vtitle)
       
   273         rset = self.cw_rset
       
   274         if rset and rset.rowcount:
       
   275             if rset.rowcount == 1:
       
   276                 try:
       
   277                     entity = rset.complete_entity(0, 0)
       
   278                     # use long_title to get context information if any
       
   279                     clabel = entity.dc_long_title()
       
   280                 except NotAnEntity:
       
   281                     clabel = display_name(self._cw, rset.description[0][0])
       
   282                     clabel = u'%s (%s)' % (clabel, vtitle)
       
   283             else :
       
   284                 etypes = rset.column_types(0)
       
   285                 if len(etypes) == 1:
       
   286                     etype = next(iter(etypes))
       
   287                     clabel = display_name(self._cw, etype, 'plural')
       
   288                 else :
       
   289                     clabel = u'#[*] (%s)' % vtitle
       
   290         else:
       
   291             clabel = vtitle
       
   292         return u'%s (%s)' % (clabel, self._cw.property_value('ui.site-title'))
       
   293 
       
   294     def field(self, label, value, row=True, show_label=True, w=None, tr=True,
       
   295               table=False):
       
   296         """read-only field"""
       
   297         if w is None:
       
   298             w = self.w
       
   299         if table:
       
   300             w(u'<tr class="entityfield">')
       
   301         else:
       
   302             w(u'<div class="entityfield">')
       
   303         if show_label and label:
       
   304             if tr:
       
   305                 label = display_name(self._cw, label)
       
   306             if table:
       
   307                 w(u'<th>%s</th>' % label)
       
   308             else:
       
   309                 w(u'<span class="label">%s</span> ' % label)
       
   310         if table:
       
   311             if not (show_label and label):
       
   312                 w(u'<td colspan="2">%s</td></tr>' % value)
       
   313             else:
       
   314                 w(u'<td>%s</td></tr>' % value)
       
   315         else:
       
   316             w(u'<span>%s</span></div>' % value)
       
   317 
       
   318 
       
   319 
       
   320 # concrete views base classes #################################################
       
   321 
       
   322 class EntityView(View):
       
   323     """base class for views applying on an entity (i.e. uniform result set)"""
       
   324     __select__ = non_final_entity()
       
   325     category = _('entityview')
       
   326 
       
   327     def call(self, **kwargs):
       
   328         if self.cw_rset is None:
       
   329             # * cw_extra_kwargs is the place where extra selection arguments are
       
   330             #   stored
       
   331             # * when calling req.view('somevid', entity=entity), 'entity' ends
       
   332             #   up in cw_extra_kwargs and kwargs
       
   333             #
       
   334             # handle that to avoid a TypeError with a sanity check
       
   335             #
       
   336             # Notice that could probably be avoided by handling entity_call in
       
   337             # .render
       
   338             entity = self.cw_extra_kwargs.pop('entity')
       
   339             if 'entity' in kwargs:
       
   340                 assert kwargs.pop('entity') is entity
       
   341             self.entity_call(entity, **kwargs)
       
   342         else:
       
   343             super(EntityView, self).call(**kwargs)
       
   344 
       
   345     def cell_call(self, row, col, **kwargs):
       
   346         self.entity_call(self.cw_rset.get_entity(row, col), **kwargs)
       
   347 
       
   348     def entity_call(self, entity, **kwargs):
       
   349         raise NotImplementedError('%r %r' % (self.__regid__, self.__class__))
       
   350 
       
   351 
       
   352 class StartupView(View):
       
   353     """base class for views which doesn't need a particular result set to be
       
   354     displayed (so they can always be displayed!)
       
   355     """
       
   356     __select__ = none_rset()
       
   357 
       
   358     category = _('startupview')
       
   359 
       
   360     def html_headers(self):
       
   361         """return a list of html headers (eg something to be inserted between
       
   362         <head> and </head> of the returned page
       
   363 
       
   364         by default startup views are indexed
       
   365         """
       
   366         return []
       
   367 
       
   368 
       
   369 class EntityStartupView(EntityView):
       
   370     """base class for entity views which may also be applied to None
       
   371     result set (usually a default rql is provided by the view class)
       
   372     """
       
   373     __select__ = none_rset() | non_final_entity()
       
   374 
       
   375     default_rql = None
       
   376 
       
   377     def __init__(self, req, rset=None, **kwargs):
       
   378         super(EntityStartupView, self).__init__(req, rset=rset, **kwargs)
       
   379         if rset is None:
       
   380             # this instance is not in the "entityview" category
       
   381             self.category = 'startupview'
       
   382 
       
   383     def startup_rql(self):
       
   384         """return some rql to be executed if the result set is None"""
       
   385         return self.default_rql
       
   386 
       
   387     def no_entities(self, **kwargs):
       
   388         """override to display something when no entities were found"""
       
   389         pass
       
   390 
       
   391     def call(self, **kwargs):
       
   392         """override call to execute rql returned by the .startup_rql method if
       
   393         necessary
       
   394         """
       
   395         rset = self.cw_rset
       
   396         if rset is None:
       
   397             rset = self.cw_rset = self._cw.execute(self.startup_rql())
       
   398         if rset:
       
   399             for i in range(len(rset)):
       
   400                 self.wview(self.__regid__, rset, row=i, **kwargs)
       
   401         else:
       
   402             self.no_entities(**kwargs)
       
   403 
       
   404 
       
   405 class AnyRsetView(View):
       
   406     """base class for views applying on any non empty result sets"""
       
   407     __select__ = nonempty_rset()
       
   408 
       
   409     category = _('anyrsetview')
       
   410 
       
   411     def columns_labels(self, mainindex=0, tr=True):
       
   412         """compute the label of the rset colums
       
   413 
       
   414         The logic is based on :meth:`~rql.stmts.Union.get_description`.
       
   415 
       
   416         :param mainindex: The index of the main variable. This is an hint to get
       
   417                           more accurate label for various situation
       
   418         :type mainindex:  int
       
   419 
       
   420         :param tr: Should the label be translated ?
       
   421         :type tr: boolean
       
   422         """
       
   423         if tr:
       
   424             translate = partial(display_name, self._cw)
       
   425         else:
       
   426             translate = lambda val, *args,**kwargs: val
       
   427         # XXX [0] because of missing Union support
       
   428         rql_syntax_tree = self.cw_rset.syntax_tree()
       
   429         rqlstdescr = rql_syntax_tree.get_description(mainindex, translate)[0]
       
   430         labels = []
       
   431         for colidx, label in enumerate(rqlstdescr):
       
   432             labels.append(self.column_label(colidx, label, translate))
       
   433         return labels
       
   434 
       
   435     def column_label(self, colidx, default, translate_func=None):
       
   436         """return the label of a specified columns index
       
   437 
       
   438         Overwrite me if you need to compute specific label.
       
   439 
       
   440         :param colidx: The index of the column the call computes a label for.
       
   441         :type colidx:  int
       
   442 
       
   443         :param default: Default value. If ``"Any"`` the default value will be
       
   444                         recomputed as coma separated list for all possible
       
   445                         etypes name.
       
   446         :type colidx:  string
       
   447 
       
   448         :param translate_func: A function used to translate name.
       
   449         :type colidx:  function
       
   450         """
       
   451         label = default
       
   452         if label == 'Any':
       
   453             etypes = self.cw_rset.column_types(colidx)
       
   454             if translate_func is not None:
       
   455                 etypes = map(translate_func, etypes)
       
   456             label = u','.join(etypes)
       
   457         return label
       
   458 
       
   459 
       
   460 
       
   461 # concrete template base classes ##############################################
       
   462 
       
   463 class MainTemplate(View):
       
   464     """main template are primary access point to render a full HTML page.
       
   465     There is usually at least a regular main template and a simple fallback
       
   466     one to display error if the first one failed
       
   467     """
       
   468 
       
   469     doctype = '<!DOCTYPE html>'
       
   470 
       
   471     def set_stream(self, w=None):
       
   472         if self.w is not None:
       
   473             return
       
   474         if w is None:
       
   475             if self.binary:
       
   476                 self._stream = stream = BytesIO()
       
   477             else:
       
   478                 self._stream = stream = HTMLStream(self._cw)
       
   479             w = stream.write
       
   480         else:
       
   481             stream = None
       
   482         self.w = w
       
   483         return stream
       
   484 
       
   485     def write_doctype(self, xmldecl=True):
       
   486         assert isinstance(self._stream, HTMLStream)
       
   487         self._stream.doctype = self.doctype
       
   488         if not xmldecl:
       
   489             self._stream.xmldecl = u''
       
   490 
       
   491     def linkable(self):
       
   492         return False
       
   493 
       
   494 # concrete component base classes #############################################
       
   495 
       
   496 class ReloadableMixIn(object):
       
   497     """simple mixin for reloadable parts of UI"""
       
   498 
       
   499     @property
       
   500     def domid(self):
       
   501         return domid(self.__regid__)
       
   502 
       
   503 
       
   504 class Component(ReloadableMixIn, View):
       
   505     """base class for components"""
       
   506     __registry__ = 'components'
       
   507     __select__ = yes()
       
   508 
       
   509     # XXX huummm, much probably useless (should be...)
       
   510     htmlclass = 'mainRelated'
       
   511     @property
       
   512     def cssclass(self):
       
   513         return '%s %s' % (self.htmlclass, domid(self.__regid__))
       
   514 
       
   515     # XXX should rely on ReloadableMixIn.domid
       
   516     @property
       
   517     def domid(self):
       
   518         return '%sComponent' % domid(self.__regid__)
       
   519 
       
   520 
       
   521 class Adapter(AppObject):
       
   522     """base class for adapters"""
       
   523     __registry__ = 'adapters'
       
   524 
       
   525 
       
   526 class EntityAdapter(Adapter):
       
   527     """base class for entity adapters (eg adapt an entity to an interface)"""
       
   528     def __init__(self, _cw, **kwargs):
       
   529         try:
       
   530             self.entity = kwargs.pop('entity')
       
   531         except KeyError:
       
   532             self.entity = kwargs['rset'].get_entity(kwargs.get('row') or 0,
       
   533                                                     kwargs.get('col') or 0)
       
   534         Adapter.__init__(self, _cw, **kwargs)