cubicweb/web/views/primary.py
changeset 11057 0b59724cb3f2
parent 10666 7f6b5f023884
child 11767 432f87a63057
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/primary.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,444 @@
+# copyright 2003-2014 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/>.
+"""
+Public API of the PrimaryView class
+````````````````````````````````````
+.. autoclass:: cubicweb.web.views.primary.PrimaryView
+
+Views that may be used to display an entity's attribute or relation
+```````````````````````````````````````````````````````````````````
+
+Yoy may easily the display of an attribute or relation by simply configuring the
+view using one of `primaryview_display_ctrl` or `reledit_ctrl` to use one of the
+views describled below. For instance:
+
+.. sourcecode:: python
+
+    primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'})
+
+
+.. autoclass:: AttributeView
+.. autoclass:: URLAttributeView
+.. autoclass:: VerbatimAttributeView
+"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from warnings import warn
+
+from logilab.common.deprecation import deprecated
+from logilab.mtconverter import xml_escape
+
+from cubicweb import Unauthorized, NoSelectableObject
+from cubicweb.utils import support_args
+from cubicweb.predicates import match_kwargs, match_context
+from cubicweb.view import EntityView
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name
+from cubicweb.web import component
+from cubicweb.web.views import uicfg
+
+
+class PrimaryView(EntityView):
+    """
+    The basic layout of a primary view is as in the :ref:`primary_view_layout`
+    section. This layout is actually drawn by the `render_entity` method.
+
+    The methods you may want to modify while customizing a ``PrimaryView``
+    are:
+
+    .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_title
+    .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_attributes
+    .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_relations
+    .. automethod:: cubicweb.web.views.primary.PrimaryView.render_side_boxes
+
+    The placement of relations in the relations section or in side boxes
+    can be controlled through the :ref:`primary_view_configuration` mechanism.
+
+    .. automethod:: cubicweb.web.views.primary.PrimaryView.content_navigation_components
+
+    Also, please note that by setting the following attributes in your
+    subclass, you can already customize some of the rendering:
+
+    :attr:`show_attr_label`
+        Renders the attribute label next to the attribute value if set to `True`.
+        Otherwise, does only display the attribute value.
+
+    :attr:`show_rel_label`
+        Renders the relation label next to the relation value if set to `True`.
+        Otherwise, does only display the relation value.
+
+    :attr:`main_related_section`
+        Renders the relations of the entity if set to `True`.
+
+    A good practice is for you to identify the content of your entity type for
+    which the default rendering does not answer your need so that you can focus
+    on the specific method (from the list above) that needs to be modified. We
+    do not advise you to overwrite ``render_entity`` unless you want a
+    completely different layout.
+    """
+
+    __regid__ = 'primary'
+    title = _('primary')
+    show_attr_label = True
+    show_rel_label = True
+    rsection = None
+    display_ctrl = None
+    main_related_section = True
+
+    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 primary views are indexed
+        """
+        return []
+
+    def entity_call(self, entity, **kwargs):
+        entity.complete()
+        uicfg_reg = self._cw.vreg['uicfg']
+        if self.rsection is None:
+            self.rsection = uicfg_reg.select('primaryview_section',
+                                             self._cw, entity=entity)
+        if self.display_ctrl is None:
+            self.display_ctrl = uicfg_reg.select('primaryview_display_ctrl',
+                                                 self._cw, entity=entity)
+        self.render_entity(entity)
+
+    def render_entity(self, entity):
+        self.render_entity_toolbox(entity)
+        self.render_entity_title(entity)
+        # entity's attributes and relations, excluding meta data
+        # if the entity isn't meta itself
+        if self.is_primary():
+            boxes = self._prepare_side_boxes(entity)
+        else:
+            boxes = None
+        if boxes or hasattr(self, 'render_side_related'):
+            self.w(u'<table width="100%"><tr><td style="width: 75%">')
+
+        self.w(u'<div class="mainInfo">')
+        self.content_navigation_components('navcontenttop')
+        self.render_entity_attributes(entity)
+        if self.main_related_section:
+            self.render_entity_relations(entity)
+        self.content_navigation_components('navcontentbottom')
+        self.w(u'</div>')
+        # side boxes
+        if boxes or hasattr(self, 'render_side_related'):
+            self.w(u'</td><td>')
+            self.w(u'<div class="primaryRight">')
+            self.render_side_boxes(boxes)
+            self.w(u'</div>')
+            self.w(u'</td></tr></table>')
+
+    def content_navigation_components(self, context):
+        """This method is applicable only for entity type implementing the
+        interface `IPrevNext`. This interface is for entities which can be
+        linked to a previous and/or next entity. This method will render the
+        navigation links between entities of this type, either at the top or at
+        the bottom of the page given the context (navcontent{top|bottom}).
+        """
+        self.w(u'<div class="%s">' % context)
+        for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects(
+            self._cw, rset=self.cw_rset, view=self, context=context):
+            # XXX bw compat code
+            try:
+                comp.render(w=self.w, row=self.cw_row, view=self)
+            except TypeError:
+                comp.render(w=self.w)
+        self.w(u'</div>')
+
+    def render_entity_title(self, entity):
+        """Renders the entity title, by default using entity's
+        :meth:`dc_title()` method.
+        """
+        title = xml_escape(entity.dc_title())
+        if title:
+            if self.is_primary():
+                self.w(u'<h1>%s</h1>' % title)
+            else:
+                atitle = self._cw._('follow this link for more information on this %s') % entity.dc_type()
+                self.w(u'<h4><a href="%s" title="%s">%s</a></h4>'
+                       % (entity.absolute_url(), atitle, title))
+
+    def render_entity_toolbox(self, entity):
+        self.content_navigation_components('ctxtoolbar')
+
+    def render_entity_attributes(self, entity):
+        """Renders all attributes and relations in the 'attributes' section. 
+        """
+        display_attributes = []
+        for rschema, _, role, dispctrl in self._section_def(entity, 'attributes'):
+            vid = dispctrl.get('vid', 'reledit')
+            if rschema.final or vid == 'reledit' or dispctrl.get('rtypevid'):
+                value = entity.view(vid, rtype=rschema.type, role=role,
+                                    initargs={'dispctrl': dispctrl})
+            else:
+                rset = self._relation_rset(entity, rschema, role, dispctrl)
+                if rset:
+                    value = self._cw.view(vid, rset)
+                else:
+                    value = None
+            if value is not None and value != '':
+                display_attributes.append( (rschema, role, dispctrl, value) )
+        if display_attributes:
+            self.w(u'<table>')
+            for rschema, role, dispctrl, value in display_attributes:
+                label = self._rel_label(entity, rschema, role, dispctrl)
+                self.render_attribute(label, value, table=True)
+            self.w(u'</table>')
+
+    def render_attribute(self, label, value, table=False):
+        self.field(label, value, tr=False, table=table)
+
+    def render_entity_relations(self, entity):
+        """Renders all relations in the 'relations' section."""
+        defaultlimit = self._cw.property_value('navigation.related-limit')
+        for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'):
+            if rschema.final or dispctrl.get('rtypevid'):
+                vid = dispctrl.get('vid', 'reledit')
+                try:
+                    rview = self._cw.vreg['views'].select(
+                        vid, self._cw, rset=entity.cw_rset, row=entity.cw_row,
+                        col=entity.cw_col, dispctrl=dispctrl,
+                        rtype=rschema, role=role)
+                except NoSelectableObject:
+                    continue
+                value = rview.render(row=entity.cw_row, col=entity.cw_col,
+                                     rtype=rschema.type, role=role)
+            else:
+                vid = dispctrl.get('vid', 'autolimited')
+                limit = dispctrl.get('limit', defaultlimit) if vid == 'autolimited' else None
+                if limit is not None:
+                    limit += 1 # need one more so the view can check if there is more than the limit
+                rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
+                if not rset:
+                    continue
+                try:
+                    rview = self._cw.vreg['views'].select(
+                        vid, self._cw, rset=rset, dispctrl=dispctrl)
+                except NoSelectableObject:
+                    continue
+                value = rview.render()
+            label = self._rel_label(entity, rschema, role, dispctrl)
+            self.render_relation(label, value)
+
+    def render_relation(self, label, value):
+        self.w(u'<div class="section">')
+        if label:
+            self.w(u'<h4>%s</h4>' % label)
+        self.w(value)
+        self.w(u'</div>')
+
+    def render_side_boxes(self, boxes):
+        """Renders side boxes on the right side of the content. This will
+        generate a box for each relation in the 'sidebox' section, as well as
+        explicit box appobjects selectable in this context.
+        """
+        for box in boxes:
+            try:
+                box.render(w=self.w, row=self.cw_row)
+            except TypeError:
+                box.render(w=self.w)
+
+    def _prepare_side_boxes(self, entity):
+        sideboxes = []
+        boxesreg = self._cw.vreg['ctxcomponents']
+        defaultlimit = self._cw.property_value('navigation.related-limit')
+        for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'):
+            vid = dispctrl.get('vid', 'autolimited')
+            limit = defaultlimit if vid == 'autolimited' else None
+            rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit)
+            if not rset:
+                continue
+            label = self._rel_label(entity, rschema, role, dispctrl)
+            box = boxesreg.select('rsetbox', self._cw, rset=rset,
+                                  vid=vid, title=label, dispctrl=dispctrl,
+                                  context='incontext')
+            sideboxes.append(box)
+        sideboxes += boxesreg.poss_visible_objects(
+             self._cw, rset=self.cw_rset, view=self,
+             context='incontext')
+        # XXX since we've two sorted list, it may be worth using bisect
+        def get_order(x):
+            if 'order' in x.cw_property_defs:
+                return x.cw_propval('order')
+            # default to 9999 so view boxes occurs after component boxes
+            return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999)
+        return sorted(sideboxes, key=get_order)
+
+    def _section_def(self, entity, where):
+        rdefs = []
+        eschema = entity.e_schema
+        for rschema, tschemas, role in eschema.relation_definitions(True):
+            if rschema in VIRTUAL_RTYPES:
+                continue
+            matchtschemas = []
+            for tschema in tschemas:
+                section = self.rsection.etype_get(eschema, rschema, role,
+                                                  tschema)
+                if section == where:
+                    matchtschemas.append(tschema)
+            if matchtschemas:
+                dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*')
+                rdefs.append( (rschema, matchtschemas, role, dispctrl) )
+        return sorted(rdefs, key=lambda x: x[-1]['order'])
+
+    def _relation_rset(self, entity, rschema, role, dispctrl, limit=None):
+        try:
+            rset = entity.related(rschema.type, role, limit=limit)
+        except Unauthorized:
+            return
+        if 'filter' in dispctrl:
+            rset = dispctrl['filter'](rset)
+        return rset
+
+    def _rel_label(self, entity, rschema, role, dispctrl):
+        if rschema.final:
+            showlabel = dispctrl.get('showlabel', self.show_attr_label)
+        else:
+            showlabel = dispctrl.get('showlabel', self.show_rel_label)
+        if showlabel:
+            if dispctrl.get('label'):
+                label = self._cw._(dispctrl['label'])
+            else:
+                label = display_name(self._cw, rschema.type, role,
+                                     context=entity.cw_etype)
+            return label
+        return u''
+
+
+class RelatedView(EntityView):
+    """Display a rset, usually containing entities linked to another entity
+    being displayed.
+
+    It will try to display nicely according to the number of items in the result
+    set.
+
+    XXX include me in the doc
+    """
+    __regid__ = 'autolimited'
+
+    def call(self, **kwargs):
+        if 'dispctrl' in self.cw_extra_kwargs:
+            if 'limit' in self.cw_extra_kwargs['dispctrl']:
+                limit = self.cw_extra_kwargs['dispctrl']['limit']
+            else:
+                limit = self._cw.property_value('navigation.related-limit')
+            list_limit = self.cw_extra_kwargs['dispctrl'].get('use_list_limit', 5)
+            subvid = self.cw_extra_kwargs['dispctrl'].get('subvid', 'incontext')
+        else:
+            limit = list_limit = None
+            subvid = 'incontext'
+        if limit is None or self.cw_rset.rowcount <= limit:
+            if self.cw_rset.rowcount == 1:
+                self.wview(subvid, self.cw_rset, row=0)
+            elif list_limit is None or 1 < self.cw_rset.rowcount <= list_limit:
+                self.wview('csv', self.cw_rset, subvid=subvid)
+            else:
+                self.w(u'<div>')
+                self.wview('simplelist', self.cw_rset, subvid=subvid)
+                self.w(u'</div>')
+        # else show links to display related entities
+        else:
+            rql = self.cw_rset.printable_rql()
+            rset = self.cw_rset.limit(limit) # remove extra entity
+            if list_limit is None:
+                self.wview('csv', rset, subvid=subvid)
+                self.w(u'[<a href="%s">%s</a>]' % (
+                    xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
+                    self._cw._('see them all')))
+            else:
+                self.w(u'<div>')
+                self.wview('simplelist', rset, subvid=subvid)
+                self.w(u'[<a href="%s">%s</a>]' % (
+                    xml_escape(self._cw.build_url(rql=rql, vid=subvid)),
+                    self._cw._('see them all')))
+                self.w(u'</div>')
+
+
+class AttributeView(EntityView):
+    """:__regid__: *attribute*
+
+    This view is generally used to disable the *reledit* feature. It works on
+    both relations and attributes.
+    """
+    __regid__ = 'attribute'
+    __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+    def entity_call(self, entity, rtype, role='subject', **kwargs):
+        if self._cw.vreg.schema.rschema(rtype).final:
+            self.w(entity.printable_value(rtype))
+        else:
+            dispctrl = uicfg.primaryview_display_ctrl.etype_get(
+                entity.e_schema, rtype, role, '*')
+            rset = entity.related(rtype, role)
+            if rset:
+                self.wview('autolimited', rset, initargs={'dispctrl': dispctrl})
+
+
+class URLAttributeView(EntityView):
+    """:__regid__: *urlattr*
+
+    This view will wrap an attribute value (hence expect a string) into an '<a>'
+    HTML tag to display a clickable link.
+    """
+    __regid__ = 'urlattr'
+    __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+    def entity_call(self, entity, rtype, **kwargs):
+        url = entity.printable_value(rtype)
+        if url:
+            self.w(u'<a href="%s">%s</a>' % (url, url))
+
+
+class VerbatimAttributeView(EntityView):
+    """:__regid__: *verbatimattr*
+
+    This view will wrap an attribute value into an '<pre>' HTML tag to display
+    arbitrary text where EOL will be respected. It usually make sense for
+    attributes whose value is a multi-lines string where new lines matters.
+    """
+    __regid__ = 'verbatimattr'
+    __select__ = EntityView.__select__ & match_kwargs('rtype')
+
+    def entity_call(self, entity, rtype, **kwargs):
+        value = entity.printable_value(rtype)
+        if value:
+            self.w(u'<pre>%s</pre>' % value)
+
+
+
+
+
+class ToolbarLayout(component.Layout):
+    # XXX include me in the doc
+    __select__ = match_context('ctxtoolbar')
+
+    def render(self, w):
+        if self.init_rendering():
+            self.cw_extra_kwargs['view'].render_body(w)
+
+
+## default primary ui configuration ###########################################
+
+_pvs = uicfg.primaryview_section
+for rtype in META_RTYPES:
+    _pvs.tag_subject_of(('*', rtype, '*'), 'hidden')
+    _pvs.tag_object_of(('*', rtype, '*'), 'hidden')