common/view.py
author Nicolas Chauvat <nicolas.chauvat@logilab.fr>
Wed, 03 Jun 2009 19:45:46 +0200
branch3.0
changeset 2028 b2b787595c5c
parent 370 7e76f651314b
child 472 958805c342b6
permissions -rw-r--r--
closing 3.0 branch merged into trunk.

"""abstract views and templates classes for CubicWeb web client


:organization: Logilab
:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"

from cStringIO import StringIO

from logilab.mtconverter import html_escape

from cubicweb import NotAnEntity, NoSelectableObject
from cubicweb.common.registerers import accepts_registerer, priority_registerer
from cubicweb.common.selectors import (chainfirst, match_user_group, accept,
                                       nonempty_rset, empty_rset, none_rset)
from cubicweb.common.appobject import AppRsetObject, ComponentMixIn
from cubicweb.common.utils import UStringIO, HTMLStream

_ = unicode

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

CW_XHTML_EXTENSIONS = '''[
  <!ATTLIST html xmlns:cubicweb CDATA  #FIXED \'http://www.logilab.org/2008/cubicweb\'  >

<!ENTITY % coreattrs
 "id          ID             #IMPLIED
  class       CDATA          #IMPLIED
  style       CDATA   #IMPLIED
  title       CDATA         #IMPLIED

 cubicweb:sortvalue         CDATA   #IMPLIED
 cubicweb:target            CDATA   #IMPLIED
 cubicweb:limit             CDATA   #IMPLIED
 cubicweb:type              CDATA   #IMPLIED
 cubicweb:loadtype          CDATA   #IMPLIED
 cubicweb:wdgtype           CDATA   #IMPLIED
 cubicweb:initfunc          CDATA   #IMPLIED
 cubicweb:inputid           CDATA   #IMPLIED
 cubicweb:tindex            CDATA   #IMPLIED
 cubicweb:inputname         CDATA   #IMPLIED
 cubicweb:value             CDATA   #IMPLIED
 cubicweb:required          CDATA   #IMPLIED
 cubicweb:accesskey         CDATA   #IMPLIED
 cubicweb:maxlength         CDATA   #IMPLIED
 cubicweb:variables         CDATA   #IMPLIED
 cubicweb:displayactions    CDATA   #IMPLIED
 cubicweb:fallbackvid       CDATA   #IMPLIED
 cubicweb:vid               CDATA   #IMPLIED
 cubicweb:rql               CDATA   #IMPLIED
 cubicweb:actualrql         CDATA   #IMPLIED
 cubicweb:rooteid           CDATA   #IMPLIED   
 cubicweb:dataurl           CDATA   #IMPLIED
 cubicweb:size              CDATA   #IMPLIED   
 cubicweb:tlunit            CDATA   #IMPLIED
 cubicweb:loadurl           CDATA   #IMPLIED
 cubicweb:uselabel          CDATA   #IMPLIED
  "> ] '''

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

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

class View(AppRsetObject):
    """abstract view class, used as base for every renderable object such
    as views, templates, some components...web

    A view is instantiated to render a [part of a] result set. View
    subclasses may be parametred using the following class attributes:
    
    * `templatable` indicates if the view may be embeded in a main
      template or if it has to be rendered standalone (i.e. XML for
      instance)
    * if the view is not templatable, it should set the `content_type` class
      attribute to the correct MIME type (text/xhtml by default)
    * the `category` attribute may be used in the interface to regroup related
      objects together

    At instantiation time, the standard `req`, `rset`, and `cursor`
    attributes are added and the `w` attribute will be set at rendering
    time to a write function to use.
    """
    __registry__ = 'views'
    
    templatable = True
    need_navigation = True
    # content_type = 'application/xhtml+xml' # text/xhtml'
    binary = False
    add_to_breadcrumbs = True
    category = 'view'
    
    def __init__(self, req, rset):
        super(View, self).__init__(req, rset)
        self.w = None

    @property
    def content_type(self):
        if self.req.xhtml_browser():
            return 'application/xhtml+xml'
        return 'text/html'
    
    def set_stream(self, w=None):
        if self.w is not None:
            return
        if w is None:
            if self.binary:
                self._stream = stream = StringIO()
            else:
                self._stream = stream = UStringIO()
            w = stream.write
        else:
            stream = None
        self.w = w
        return stream

    # main view interface #####################################################
            
    def dispatch(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 [and col] are 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
        """
        row, col = context.get('row'), context.get('col')
        if row is not None:
            context.setdefault('col', 0)
            view_func = self.cell_call
        else:
            view_func = self.call
        stream = self.set_stream(w)
        # stream = self.set_stream(context)
        view_func(**context)
        # return stream content if we have created it
        if stream is not None:
            return self._stream.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.rset
        if rset is None:
            raise NotImplementedError, self
        wrap = self.templatable and len(rset) > 1 and self.add_div_section
        for i in xrange(len(rset)):
            if wrap:
                self.w(u'<div class="section">')
            self.wview(self.id, rset, row=i, **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, 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.id == '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.
        """
        try:
            return self.build_url(vid=self.id, rql=self.req.form['rql'])
        except KeyError:
            return self.build_url(vid=self.id)

    def set_request_content_type(self):
        """set the content type returned by this view"""
        self.req.set_content_type(self.content_type)

    # view utilities ##########################################################
    
    def view(self, __vid, rset, __fallback_vid=None, **kwargs):
        """shortcut to self.vreg.render method avoiding to pass self.req"""
        try:
            view = self.vreg.select_view(__vid, self.req, rset, **kwargs)
        except NoSelectableObject:
            if __fallback_vid is None:
                raise
            view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs)
        return view.dispatch(**kwargs)
    
    def wview(self, __vid, rset, __fallback_vid=None, **kwargs):
        """shortcut to self.view method automatically passing self.w as argument
        """
        self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)

    def whead(self, data):
        self.req.html_headers.write(data)

    def wdata(self, data):
        """simple helper that escapes `data` and writes into `self.w`"""
        self.w(html_escape(data))

    def action(self, actionid, row=0):
        """shortcut to get action object with id `actionid`"""
        return self.vreg.select_action(actionid, self.req, self.rset,
                                       row=row)

    def action_url(self, actionid, label=None, row=0):
        """simple method to be able to display `actionid` as a link anywhere
        """
        action = self.vreg.select_action(actionid, self.req, self.rset,
                                         row=row)
        if action:
            label = label or self.req._(action.title)
            return u'<a href="%s">%s</a>' % (html_escape(action.url()), label)
        return u''
    
    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.req.form.get('vtitle')
        if vtitle:
            return self.req._(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.req._(vtitle)
        rset = self.rset
        if rset and rset.rowcount:
            if rset.rowcount == 1:
                try:
                    entity = self.complete_entity(0)
                    # use long_title to get context information if any
                    clabel = entity.dc_long_title()
                except NotAnEntity:
                    clabel = display_name(self.req, rset.description[0][0])
                    clabel = u'%s (%s)' % (clabel, vtitle)
            else :
                etypes = rset.column_types(0)
                if len(etypes) == 1:
                    etype = iter(etypes).next()
                    clabel = display_name(self.req, etype, 'plural')
                else :
                    clabel = u'#[*] (%s)' % vtitle
        else:
            clabel = vtitle
        return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title'))

    def output_url_builder( self, name, url, args ):
        self.w(u'<script language="JavaScript"><!--\n' \
               u'function %s( %s ) {\n' % (name, ','.join(args) ) )
        url_parts = url.split("%s")
        self.w(u' url="%s"' % url_parts[0] )
        for arg, part in zip(args, url_parts[1:]):
            self.w(u'+str(%s)' % arg )
            if part:
                self.w(u'+"%s"' % part)
        self.w('\n document.window.href=url;\n')
        self.w('}\n-->\n</script>\n')

    def create_url(self, etype, **kwargs):
        """ return the url of the entity creation form for a given entity type"""
        return self.req.build_url('add/%s'%etype, **kwargs)

        
# concrete views base classes #################################################

class EntityView(View):
    """base class for views applying on an entity (i.e. uniform result set)
    """
    __registerer__ = accepts_registerer
    __selectors__ = (accept,)
    category = 'entityview'
    
    def field(self, label, value, row=True, show_label=True, w=None, tr=True):
        """ read-only field """
        if w is None:
            w = self.w
        if row:
            w(u'<div class="row">')
        if show_label:
            if tr:
                label = display_name(self.req, label)
            w(u'<span class="label">%s</span>' % label)
        w(u'<div class="field">%s</div>' % value)
        if row:
            w(u'</div>')

        
class StartupView(View):
    """base class for views which doesn't need a particular result set
    to be displayed (so they can always be displayed !)
    """
    __registerer__ = priority_registerer
    __selectors__ = (match_user_group, none_rset)
    require_groups = ()
    category = 'startupview'
    
    def url(self):
        """return the url associated with this view. We can omit rql here"""
        return self.build_url('view', vid=self.id)

    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)
    """
    __registerer__ = accepts_registerer
    __selectors__ = (chainfirst(none_rset, accept),)
    
    default_rql = None
    
    def __init__(self, req, rset):
        super(EntityStartupView, self).__init__(req, rset)
        if rset is None:
            # this instance is not in the "entityview" category
            self.category = 'startupview'

    def startup_rql(self):
        """return some rql to be executedif the result set is None"""
        return self.default_rql
    
    def call(self, **kwargs):
        """override call to execute rql returned by the .startup_rql
        method if necessary
        """
        if self.rset is None:
            self.rset = self.req.execute(self.startup_rql())
        rset = self.rset
        for i in xrange(len(rset)):
            self.wview(self.id, rset, row=i, **kwargs)

    def url(self):
        """return the url associated with this view. We can omit rql if we
        are on a result set on which we do not apply.
        """
        if not self.__select__(self.req, self.rset):
            return self.build_url(vid=self.id)
        return super(EntityStartupView, self).url()

    
class AnyRsetView(View):
    """base class for views applying on any non empty result sets"""
    __registerer__ = priority_registerer
    __selectors__ = (nonempty_rset,)
    
    category = 'anyrsetview'
    
    def columns_labels(self, tr=True):
        if tr:
            translate = display_name
        else:
            translate = lambda req, val: val
        rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support
        labels = []
        for colindex, attr in enumerate(rqlstdescr):
            # compute column header
            if colindex == 0 or attr == 'Any': # find a better label
                label = ','.join(translate(self.req, et)
                                 for et in self.rset.column_types(colindex))
            else:
                label = translate(self.req, attr)
            labels.append(label)
        return labels
    

class EmptyRsetView(View):
    """base class for views applying on any empty result sets"""
    __registerer__ = priority_registerer
    __selectors__ = (empty_rset,)


# concrete template base classes ##############################################

class Template(View):
    """a template is almost like a view, except that by default a template
    is only used globally (i.e. no result set adaptation)
    """
    __registry__ = 'templates'
    __registerer__ = priority_registerer
    __selectors__ = (match_user_group,)

    require_groups = ()
    
    def template(self, oid, **kwargs):
        """shortcut to self.registry.render method on the templates registry"""
        w = kwargs.pop('w', self.w)
        self.vreg.render('templates', oid, self.req, w=w, **kwargs)


class MainTemplate(Template):
    """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
    """

    base_doctype = STRICT_DOCTYPE

    @property
    def doctype(self):
        if self.req.xhtml_browser():
            return self.base_doctype % CW_XHTML_EXTENSIONS
        return self.base_doctype % ''

    def set_stream(self, w=None, templatable=True):
        if templatable and self.w is not None:
            return

        if w is None:
            if self.binary:
                self._stream = stream = StringIO()
            elif not templatable:
                # not templatable means we're using a non-html view, we don't
                # want the HTMLStream stuff to interfere during data generation
                self._stream = stream = UStringIO()
            else:
                self._stream = stream = HTMLStream(self.req)
            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''

# viewable components base classes ############################################

class VComponent(ComponentMixIn, View):
    """base class for displayable components"""
    property_defs = {
        'visible':  dict(type='Boolean', default=True,
                         help=_('display the component or not')),}

class SingletonVComponent(VComponent):
    """base class for displayable unique components"""
    __registerer__ = priority_registerer