cubicweb/web/views/primary.py
changeset 11057 0b59724cb3f2
parent 10666 7f6b5f023884
child 11767 432f87a63057
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 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 """
       
    19 Public API of the PrimaryView class
       
    20 ````````````````````````````````````
       
    21 .. autoclass:: cubicweb.web.views.primary.PrimaryView
       
    22 
       
    23 Views that may be used to display an entity's attribute or relation
       
    24 ```````````````````````````````````````````````````````````````````
       
    25 
       
    26 Yoy may easily the display of an attribute or relation by simply configuring the
       
    27 view using one of `primaryview_display_ctrl` or `reledit_ctrl` to use one of the
       
    28 views describled below. For instance:
       
    29 
       
    30 .. sourcecode:: python
       
    31 
       
    32     primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'})
       
    33 
       
    34 
       
    35 .. autoclass:: AttributeView
       
    36 .. autoclass:: URLAttributeView
       
    37 .. autoclass:: VerbatimAttributeView
       
    38 """
       
    39 
       
    40 __docformat__ = "restructuredtext en"
       
    41 from cubicweb import _
       
    42 
       
    43 from warnings import warn
       
    44 
       
    45 from logilab.common.deprecation import deprecated
       
    46 from logilab.mtconverter import xml_escape
       
    47 
       
    48 from cubicweb import Unauthorized, NoSelectableObject
       
    49 from cubicweb.utils import support_args
       
    50 from cubicweb.predicates import match_kwargs, match_context
       
    51 from cubicweb.view import EntityView
       
    52 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
       
    53 from cubicweb.web import component
       
    54 from cubicweb.web.views import uicfg
       
    55 
       
    56 
       
    57 class PrimaryView(EntityView):
       
    58     """
       
    59     The basic layout of a primary view is as in the :ref:`primary_view_layout`
       
    60     section. This layout is actually drawn by the `render_entity` method.
       
    61 
       
    62     The methods you may want to modify while customizing a ``PrimaryView``
       
    63     are:
       
    64 
       
    65     .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_title
       
    66     .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_attributes
       
    67     .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_relations
       
    68     .. automethod:: cubicweb.web.views.primary.PrimaryView.render_side_boxes
       
    69 
       
    70     The placement of relations in the relations section or in side boxes
       
    71     can be controlled through the :ref:`primary_view_configuration` mechanism.
       
    72 
       
    73     .. automethod:: cubicweb.web.views.primary.PrimaryView.content_navigation_components
       
    74 
       
    75     Also, please note that by setting the following attributes in your
       
    76     subclass, you can already customize some of the rendering:
       
    77 
       
    78     :attr:`show_attr_label`
       
    79         Renders the attribute label next to the attribute value if set to `True`.
       
    80         Otherwise, does only display the attribute value.
       
    81 
       
    82     :attr:`show_rel_label`
       
    83         Renders the relation label next to the relation value if set to `True`.
       
    84         Otherwise, does only display the relation value.
       
    85 
       
    86     :attr:`main_related_section`
       
    87         Renders the relations of the entity if set to `True`.
       
    88 
       
    89     A good practice is for you to identify the content of your entity type for
       
    90     which the default rendering does not answer your need so that you can focus
       
    91     on the specific method (from the list above) that needs to be modified. We
       
    92     do not advise you to overwrite ``render_entity`` unless you want a
       
    93     completely different layout.
       
    94     """
       
    95 
       
    96     __regid__ = 'primary'
       
    97     title = _('primary')
       
    98     show_attr_label = True
       
    99     show_rel_label = True
       
   100     rsection = None
       
   101     display_ctrl = None
       
   102     main_related_section = True
       
   103 
       
   104     def html_headers(self):
       
   105         """return a list of html headers (eg something to be inserted between
       
   106         <head> and </head> of the returned page
       
   107 
       
   108         by default primary views are indexed
       
   109         """
       
   110         return []
       
   111 
       
   112     def entity_call(self, entity, **kwargs):
       
   113         entity.complete()
       
   114         uicfg_reg = self._cw.vreg['uicfg']
       
   115         if self.rsection is None:
       
   116             self.rsection = uicfg_reg.select('primaryview_section',
       
   117                                              self._cw, entity=entity)
       
   118         if self.display_ctrl is None:
       
   119             self.display_ctrl = uicfg_reg.select('primaryview_display_ctrl',
       
   120                                                  self._cw, entity=entity)
       
   121         self.render_entity(entity)
       
   122 
       
   123     def render_entity(self, entity):
       
   124         self.render_entity_toolbox(entity)
       
   125         self.render_entity_title(entity)
       
   126         # entity's attributes and relations, excluding meta data
       
   127         # if the entity isn't meta itself
       
   128         if self.is_primary():
       
   129             boxes = self._prepare_side_boxes(entity)
       
   130         else:
       
   131             boxes = None
       
   132         if boxes or hasattr(self, 'render_side_related'):
       
   133             self.w(u'<table width="100%"><tr><td style="width: 75%">')
       
   134 
       
   135         self.w(u'<div class="mainInfo">')
       
   136         self.content_navigation_components('navcontenttop')
       
   137         self.render_entity_attributes(entity)
       
   138         if self.main_related_section:
       
   139             self.render_entity_relations(entity)
       
   140         self.content_navigation_components('navcontentbottom')
       
   141         self.w(u'</div>')
       
   142         # side boxes
       
   143         if boxes or hasattr(self, 'render_side_related'):
       
   144             self.w(u'</td><td>')
       
   145             self.w(u'<div class="primaryRight">')
       
   146             self.render_side_boxes(boxes)
       
   147             self.w(u'</div>')
       
   148             self.w(u'</td></tr></table>')
       
   149 
       
   150     def content_navigation_components(self, context):
       
   151         """This method is applicable only for entity type implementing the
       
   152         interface `IPrevNext`. This interface is for entities which can be
       
   153         linked to a previous and/or next entity. This method will render the
       
   154         navigation links between entities of this type, either at the top or at
       
   155         the bottom of the page given the context (navcontent{top|bottom}).
       
   156         """
       
   157         self.w(u'<div class="%s">' % context)
       
   158         for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   159             self._cw, rset=self.cw_rset, view=self, context=context):
       
   160             # XXX bw compat code
       
   161             try:
       
   162                 comp.render(w=self.w, row=self.cw_row, view=self)
       
   163             except TypeError:
       
   164                 comp.render(w=self.w)
       
   165         self.w(u'</div>')
       
   166 
       
   167     def render_entity_title(self, entity):
       
   168         """Renders the entity title, by default using entity's
       
   169         :meth:`dc_title()` method.
       
   170         """
       
   171         title = xml_escape(entity.dc_title())
       
   172         if title:
       
   173             if self.is_primary():
       
   174                 self.w(u'<h1>%s</h1>' % title)
       
   175             else:
       
   176                 atitle = self._cw._('follow this link for more information on this %s') % entity.dc_type()
       
   177                 self.w(u'<h4><a href="%s" title="%s">%s</a></h4>'
       
   178                        % (entity.absolute_url(), atitle, title))
       
   179 
       
   180     def render_entity_toolbox(self, entity):
       
   181         self.content_navigation_components('ctxtoolbar')
       
   182 
       
   183     def render_entity_attributes(self, entity):
       
   184         """Renders all attributes and relations in the 'attributes' section. 
       
   185         """
       
   186         display_attributes = []
       
   187         for rschema, _, role, dispctrl in self._section_def(entity, 'attributes'):
       
   188             vid = dispctrl.get('vid', 'reledit')
       
   189             if rschema.final or vid == 'reledit' or dispctrl.get('rtypevid'):
       
   190                 value = entity.view(vid, rtype=rschema.type, role=role,
       
   191                                     initargs={'dispctrl': dispctrl})
       
   192             else:
       
   193                 rset = self._relation_rset(entity, rschema, role, dispctrl)
       
   194                 if rset:
       
   195                     value = self._cw.view(vid, rset)
       
   196                 else:
       
   197                     value = None
       
   198             if value is not None and value != '':
       
   199                 display_attributes.append( (rschema, role, dispctrl, value) )
       
   200         if display_attributes:
       
   201             self.w(u'<table>')
       
   202             for rschema, role, dispctrl, value in display_attributes:
       
   203                 label = self._rel_label(entity, rschema, role, dispctrl)
       
   204                 self.render_attribute(label, value, table=True)
       
   205             self.w(u'</table>')
       
   206 
       
   207     def render_attribute(self, label, value, table=False):
       
   208         self.field(label, value, tr=False, table=table)
       
   209 
       
   210     def render_entity_relations(self, entity):
       
   211         """Renders all relations in the 'relations' section."""
       
   212         defaultlimit = self._cw.property_value('navigation.related-limit')
       
   213         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
       
   214             if rschema.final or dispctrl.get('rtypevid'):
       
   215                 vid = dispctrl.get('vid', 'reledit')
       
   216                 try:
       
   217                     rview = self._cw.vreg['views'].select(
       
   218                         vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
       
   219                         col=entity.cw_col, dispctrl=dispctrl,
       
   220                         rtype=rschema, role=role)
       
   221                 except NoSelectableObject:
       
   222                     continue
       
   223                 value = rview.render(row=entity.cw_row, col=entity.cw_col,
       
   224                                      rtype=rschema.type, role=role)
       
   225             else:
       
   226                 vid = dispctrl.get('vid', 'autolimited')
       
   227                 limit = dispctrl.get('limit', defaultlimit) if vid == 'autolimited' else None
       
   228                 if limit is not None:
       
   229                     limit += 1 # need one more so the view can check if there is more than the limit
       
   230                 rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
       
   231                 if not rset:
       
   232                     continue
       
   233                 try:
       
   234                     rview = self._cw.vreg['views'].select(
       
   235                         vid, self._cw, rset=rset, dispctrl=dispctrl)
       
   236                 except NoSelectableObject:
       
   237                     continue
       
   238                 value = rview.render()
       
   239             label = self._rel_label(entity, rschema, role, dispctrl)
       
   240             self.render_relation(label, value)
       
   241 
       
   242     def render_relation(self, label, value):
       
   243         self.w(u'<div class="section">')
       
   244         if label:
       
   245             self.w(u'<h4>%s</h4>' % label)
       
   246         self.w(value)
       
   247         self.w(u'</div>')
       
   248 
       
   249     def render_side_boxes(self, boxes):
       
   250         """Renders side boxes on the right side of the content. This will
       
   251         generate a box for each relation in the 'sidebox' section, as well as
       
   252         explicit box appobjects selectable in this context.
       
   253         """
       
   254         for box in boxes:
       
   255             try:
       
   256                 box.render(w=self.w, row=self.cw_row)
       
   257             except TypeError:
       
   258                 box.render(w=self.w)
       
   259 
       
   260     def _prepare_side_boxes(self, entity):
       
   261         sideboxes = []
       
   262         boxesreg = self._cw.vreg['ctxcomponents']
       
   263         defaultlimit = self._cw.property_value('navigation.related-limit')
       
   264         for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'):
       
   265             vid = dispctrl.get('vid', 'autolimited')
       
   266             limit = defaultlimit if vid == 'autolimited' else None
       
   267             rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
       
   268             if not rset:
       
   269                 continue
       
   270             label = self._rel_label(entity, rschema, role, dispctrl)
       
   271             box = boxesreg.select('rsetbox', self._cw, rset=rset,
       
   272                                   vid=vid, title=label, dispctrl=dispctrl,
       
   273                                   context='incontext')
       
   274             sideboxes.append(box)
       
   275         sideboxes += boxesreg.poss_visible_objects(
       
   276              self._cw, rset=self.cw_rset, view=self,
       
   277              context='incontext')
       
   278         # XXX since we've two sorted list, it may be worth using bisect
       
   279         def get_order(x):
       
   280             if 'order' in x.cw_property_defs:
       
   281                 return x.cw_propval('order')
       
   282             # default to 9999 so view boxes occurs after component boxes
       
   283             return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999)
       
   284         return sorted(sideboxes, key=get_order)
       
   285 
       
   286     def _section_def(self, entity, where):
       
   287         rdefs = []
       
   288         eschema = entity.e_schema
       
   289         for rschema, tschemas, role in eschema.relation_definitions(True):
       
   290             if rschema in VIRTUAL_RTYPES:
       
   291                 continue
       
   292             matchtschemas = []
       
   293             for tschema in tschemas:
       
   294                 section = self.rsection.etype_get(eschema, rschema, role,
       
   295                                                   tschema)
       
   296                 if section == where:
       
   297                     matchtschemas.append(tschema)
       
   298             if matchtschemas:
       
   299                 dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*')
       
   300                 rdefs.append( (rschema, matchtschemas, role, dispctrl) )
       
   301         return sorted(rdefs, key=lambda x: x[-1]['order'])
       
   302 
       
   303     def _relation_rset(self, entity, rschema, role, dispctrl, limit=None):
       
   304         try:
       
   305             rset = entity.related(rschema.type, role, limit=limit)
       
   306         except Unauthorized:
       
   307             return
       
   308         if 'filter' in dispctrl:
       
   309             rset = dispctrl['filter'](rset)
       
   310         return rset
       
   311 
       
   312     def _rel_label(self, entity, rschema, role, dispctrl):
       
   313         if rschema.final:
       
   314             showlabel = dispctrl.get('showlabel', self.show_attr_label)
       
   315         else:
       
   316             showlabel = dispctrl.get('showlabel', self.show_rel_label)
       
   317         if showlabel:
       
   318             if dispctrl.get('label'):
       
   319                 label = self._cw._(dispctrl['label'])
       
   320             else:
       
   321                 label = display_name(self._cw, rschema.type, role,
       
   322                                      context=entity.cw_etype)
       
   323             return label
       
   324         return u''
       
   325 
       
   326 
       
   327 class RelatedView(EntityView):
       
   328     """Display a rset, usually containing entities linked to another entity
       
   329     being displayed.
       
   330 
       
   331     It will try to display nicely according to the number of items in the result
       
   332     set.
       
   333 
       
   334     XXX include me in the doc
       
   335     """
       
   336     __regid__ = 'autolimited'
       
   337 
       
   338     def call(self, **kwargs):
       
   339         if 'dispctrl' in self.cw_extra_kwargs:
       
   340             if 'limit' in self.cw_extra_kwargs['dispctrl']:
       
   341                 limit = self.cw_extra_kwargs['dispctrl']['limit']
       
   342             else:
       
   343                 limit = self._cw.property_value('navigation.related-limit')
       
   344             list_limit = self.cw_extra_kwargs['dispctrl'].get('use_list_limit', 5)
       
   345             subvid = self.cw_extra_kwargs['dispctrl'].get('subvid', 'incontext')
       
   346         else:
       
   347             limit = list_limit = None
       
   348             subvid = 'incontext'
       
   349         if limit is None or self.cw_rset.rowcount <= limit:
       
   350             if self.cw_rset.rowcount == 1:
       
   351                 self.wview(subvid, self.cw_rset, row=0)
       
   352             elif list_limit is None or 1 < self.cw_rset.rowcount <= list_limit:
       
   353                 self.wview('csv', self.cw_rset, subvid=subvid)
       
   354             else:
       
   355                 self.w(u'<div>')
       
   356                 self.wview('simplelist', self.cw_rset, subvid=subvid)
       
   357                 self.w(u'</div>')
       
   358         # else show links to display related entities
       
   359         else:
       
   360             rql = self.cw_rset.printable_rql()
       
   361             rset = self.cw_rset.limit(limit) # remove extra entity
       
   362             if list_limit is None:
       
   363                 self.wview('csv', rset, subvid=subvid)
       
   364                 self.w(u'[<a href="%s">%s</a>]' % (
       
   365                     xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
       
   366                     self._cw._('see them all')))
       
   367             else:
       
   368                 self.w(u'<div>')
       
   369                 self.wview('simplelist', rset, subvid=subvid)
       
   370                 self.w(u'[<a href="%s">%s</a>]' % (
       
   371                     xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
       
   372                     self._cw._('see them all')))
       
   373                 self.w(u'</div>')
       
   374 
       
   375 
       
   376 class AttributeView(EntityView):
       
   377     """:__regid__: *attribute*
       
   378 
       
   379     This view is generally used to disable the *reledit* feature. It works on
       
   380     both relations and attributes.
       
   381     """
       
   382     __regid__ = 'attribute'
       
   383     __select__ = EntityView.__select__ & match_kwargs('rtype')
       
   384 
       
   385     def entity_call(self, entity, rtype, role='subject', **kwargs):
       
   386         if self._cw.vreg.schema.rschema(rtype).final:
       
   387             self.w(entity.printable_value(rtype))
       
   388         else:
       
   389             dispctrl = uicfg.primaryview_display_ctrl.etype_get(
       
   390                 entity.e_schema, rtype, role, '*')
       
   391             rset = entity.related(rtype, role)
       
   392             if rset:
       
   393                 self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
       
   394 
       
   395 
       
   396 class URLAttributeView(EntityView):
       
   397     """:__regid__: *urlattr*
       
   398 
       
   399     This view will wrap an attribute value (hence expect a string) into an '<a>'
       
   400     HTML tag to display a clickable link.
       
   401     """
       
   402     __regid__ = 'urlattr'
       
   403     __select__ = EntityView.__select__ & match_kwargs('rtype')
       
   404 
       
   405     def entity_call(self, entity, rtype, **kwargs):
       
   406         url = entity.printable_value(rtype)
       
   407         if url:
       
   408             self.w(u'<a href="%s">%s</a>' % (url, url))
       
   409 
       
   410 
       
   411 class VerbatimAttributeView(EntityView):
       
   412     """:__regid__: *verbatimattr*
       
   413 
       
   414     This view will wrap an attribute value into an '<pre>' HTML tag to display
       
   415     arbitrary text where EOL will be respected. It usually make sense for
       
   416     attributes whose value is a multi-lines string where new lines matters.
       
   417     """
       
   418     __regid__ = 'verbatimattr'
       
   419     __select__ = EntityView.__select__ & match_kwargs('rtype')
       
   420 
       
   421     def entity_call(self, entity, rtype, **kwargs):
       
   422         value = entity.printable_value(rtype)
       
   423         if value:
       
   424             self.w(u'<pre>%s</pre>' % value)
       
   425 
       
   426 
       
   427 
       
   428 
       
   429 
       
   430 class ToolbarLayout(component.Layout):
       
   431     # XXX include me in the doc
       
   432     __select__ = match_context('ctxtoolbar')
       
   433 
       
   434     def render(self, w):
       
   435         if self.init_rendering():
       
   436             self.cw_extra_kwargs['view'].render_body(w)
       
   437 
       
   438 
       
   439 ## default primary ui configuration ###########################################
       
   440 
       
   441 _pvs = uicfg.primaryview_section
       
   442 for rtype in META_RTYPES:
       
   443     _pvs.tag_subject_of(('*', rtype, '*'), 'hidden')
       
   444     _pvs.tag_object_of(('*', rtype, '*'), 'hidden')