common/view.py
changeset 0 b97547f5f1fa
child 237 3df2e0ae2eba
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """abstract views and templates classes for CubicWeb web client
       
     2 
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from cStringIO import StringIO
       
    11 
       
    12 from logilab.mtconverter import html_escape
       
    13 
       
    14 from cubicweb import NotAnEntity, NoSelectableObject
       
    15 from cubicweb.common.registerers import accepts_registerer, priority_registerer
       
    16 from cubicweb.common.selectors import (in_group_selector, anyrset_selector, 
       
    17                                     emptyrset_selector, accept_selector,
       
    18                                     norset_selector, chainfirst)
       
    19 from cubicweb.common.appobject import AppRsetObject, ComponentMixIn
       
    20 from cubicweb.common.utils import UStringIO, HTMLStream
       
    21 
       
    22 _ = unicode
       
    23 
       
    24 # robots control
       
    25 NOINDEX = u'<meta name="ROBOTS" content="NOINDEX" />'
       
    26 NOFOLLOW = u'<meta name="ROBOTS" content="NOFOLLOW" />'
       
    27 
       
    28 CW_XHTML_EXTENSIONS = '''[
       
    29   <!ATTLIST html xmlns:cubicweb CDATA  #FIXED \'http://www.logilab.org/2008/cubicweb\'  >
       
    30 
       
    31 <!ENTITY % coreattrs
       
    32  "id          ID             #IMPLIED
       
    33   class       CDATA          #IMPLIED
       
    34   style       CDATA   #IMPLIED
       
    35   title       CDATA         #IMPLIED
       
    36 
       
    37  cubicweb:sortvalue         CDATA   #IMPLIED
       
    38  cubicweb:target            CDATA   #IMPLIED
       
    39  cubicweb:limit             CDATA   #IMPLIED
       
    40  cubicweb:type              CDATA   #IMPLIED
       
    41  cubicweb:loadtype          CDATA   #IMPLIED
       
    42  cubicweb:wdgtype           CDATA   #IMPLIED
       
    43  cubicweb:initfunc          CDATA   #IMPLIED
       
    44  cubicweb:inputid           CDATA   #IMPLIED
       
    45  cubicweb:tindex            CDATA   #IMPLIED
       
    46  cubicweb:inputname         CDATA   #IMPLIED
       
    47  cubicweb:value             CDATA   #IMPLIED
       
    48  cubicweb:required          CDATA   #IMPLIED
       
    49  cubicweb:accesskey         CDATA   #IMPLIED
       
    50  cubicweb:maxlength         CDATA   #IMPLIED
       
    51  cubicweb:variables         CDATA   #IMPLIED
       
    52  cubicweb:displayactions    CDATA   #IMPLIED
       
    53  cubicweb:fallbackvid       CDATA   #IMPLIED
       
    54  cubicweb:vid               CDATA   #IMPLIED
       
    55  cubicweb:rql               CDATA   #IMPLIED
       
    56  cubicweb:actualrql         CDATA   #IMPLIED
       
    57  cubicweb:rooteid           CDATA   #IMPLIED   
       
    58  cubicweb:dataurl           CDATA   #IMPLIED
       
    59  cubicweb:size              CDATA   #IMPLIED   
       
    60  cubicweb:tlunit            CDATA   #IMPLIED
       
    61  cubicweb:loadurl           CDATA   #IMPLIED
       
    62  cubicweb:uselabel          CDATA   #IMPLIED
       
    63   "> ] '''
       
    64 
       
    65 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'
       
    66 
       
    67 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'
       
    68 
       
    69 class View(AppRsetObject):
       
    70     """abstract view class, used as base for every renderable object such
       
    71     as views, templates, some components...web
       
    72 
       
    73     A view is instantiated to render a [part of a] result set. View
       
    74     subclasses may be parametred using the following class attributes:
       
    75     
       
    76     * `templatable` indicates if the view may be embeded in a main
       
    77       template or if it has to be rendered standalone (i.e. XML for
       
    78       instance)
       
    79     * if the view is not templatable, it should set the `content_type` class
       
    80       attribute to the correct MIME type (text/xhtml by default)
       
    81     * the `category` attribute may be used in the interface to regroup related
       
    82       objects together
       
    83 
       
    84     At instantiation time, the standard `req`, `rset`, and `cursor`
       
    85     attributes are added and the `w` attribute will be set at rendering
       
    86     time to a write function to use.
       
    87     """
       
    88     __registry__ = 'views'
       
    89     
       
    90     templatable = True
       
    91     need_navigation = True
       
    92     # content_type = 'application/xhtml+xml' # text/xhtml'
       
    93     binary = False
       
    94     add_to_breadcrumbs = True
       
    95     category = 'view'
       
    96     
       
    97     def __init__(self, req, rset):
       
    98         super(View, self).__init__(req, rset)
       
    99         self.w = None
       
   100 
       
   101     @property
       
   102     def content_type(self):
       
   103         if self.req.xhtml_browser():
       
   104             return 'application/xhtml+xml'
       
   105         return 'text/html'
       
   106     
       
   107     def set_stream(self, w=None):
       
   108         if self.w is not None:
       
   109             return
       
   110         if w is None:
       
   111             if self.binary:
       
   112                 self._stream = stream = StringIO()
       
   113             else:
       
   114                 self._stream = stream = UStringIO()
       
   115             w = stream.write
       
   116         else:
       
   117             stream = None
       
   118         self.w = w
       
   119         return stream
       
   120 
       
   121     # main view interface #####################################################
       
   122             
       
   123     def dispatch(self, w=None, **context):
       
   124         """called to render a view object for a result set.
       
   125 
       
   126         This method is a dispatched to an actual method selected
       
   127         according to optional row and col parameters, which are locating
       
   128         a particular row or cell in the result set:
       
   129         
       
   130         * if row [and col] are specified, `cell_call` is called
       
   131         * if none of them is supplied, the view is considered to apply on
       
   132           the whole result set (which may be None in this case), `call` is
       
   133           called
       
   134         """
       
   135         row, col = context.get('row'), context.get('col')
       
   136         if row is not None:
       
   137             context.setdefault('col', 0)
       
   138             view_func = self.cell_call
       
   139         else:
       
   140             view_func = self.call
       
   141         stream = self.set_stream(w)
       
   142         # stream = self.set_stream(context)
       
   143         view_func(**context)
       
   144         # return stream content if we have created it
       
   145         if stream is not None:
       
   146             return self._stream.getvalue()
       
   147 
       
   148     # should default .call() method add a <div classs="section"> around each
       
   149     # rset item
       
   150     add_div_section = True
       
   151     
       
   152     def call(self, **kwargs):
       
   153         """the view is called for an entire result set, by default loop
       
   154         other rows of the result set and call the same view on the
       
   155         particular row
       
   156 
       
   157         Views applicable on None result sets have to override this method
       
   158         """
       
   159         rset = self.rset
       
   160         if rset is None:
       
   161             raise NotImplementedError, self
       
   162         wrap = self.templatable and len(rset) > 1 and self.add_div_section
       
   163         for i in xrange(len(rset)):
       
   164             if wrap:
       
   165                 self.w(u'<div class="section">')
       
   166             self.wview(self.id, rset, row=i, **kwargs)
       
   167             if wrap:
       
   168                 self.w(u"</div>")
       
   169 
       
   170     def cell_call(self, row, col, **kwargs):
       
   171         """the view is called for a particular result set cell"""
       
   172         raise NotImplementedError, self
       
   173         
       
   174     def linkable(self):
       
   175         """return True if the view may be linked in a menu
       
   176         
       
   177         by default views without title are not meant to be displayed
       
   178         """
       
   179         if not getattr(self, 'title', None):
       
   180             return False
       
   181         return True
       
   182 
       
   183     def is_primary(self):
       
   184         return self.id == 'primary'
       
   185     
       
   186     def url(self):
       
   187         """return the url associated with this view. Should not be
       
   188         necessary for non linkable views, but a default implementation
       
   189         is provided anyway.
       
   190         """
       
   191         try:
       
   192             return self.build_url(vid=self.id, rql=self.req.form['rql'])
       
   193         except KeyError:
       
   194             return self.build_url(vid=self.id)
       
   195 
       
   196     def set_request_content_type(self):
       
   197         """set the content type returned by this view"""
       
   198         self.req.set_content_type(self.content_type)
       
   199 
       
   200     # view utilities ##########################################################
       
   201     
       
   202     def view(self, __vid, rset, __fallback_vid=None, **kwargs):
       
   203         """shortcut to self.vreg.render method avoiding to pass self.req"""
       
   204         try:
       
   205             view = self.vreg.select_view(__vid, self.req, rset, **kwargs)
       
   206         except NoSelectableObject:
       
   207             if __fallback_vid is None:
       
   208                 raise
       
   209             view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs)
       
   210         return view.dispatch(**kwargs)
       
   211     
       
   212     def wview(self, __vid, rset, __fallback_vid=None, **kwargs):
       
   213         """shortcut to self.view method automatically passing self.w as argument
       
   214         """
       
   215         self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs)
       
   216 
       
   217     def whead(self, data):
       
   218         self.req.html_headers.write(data)
       
   219 
       
   220     def wdata(self, data):
       
   221         """simple helper that escapes `data` and writes into `self.w`"""
       
   222         self.w(html_escape(data))
       
   223 
       
   224     def action(self, actionid, row=0):
       
   225         """shortcut to get action object with id `actionid`"""
       
   226         return self.vreg.select_action(actionid, self.req, self.rset,
       
   227                                        row=row)
       
   228 
       
   229     def action_url(self, actionid, label=None, row=0):
       
   230         """simple method to be able to display `actionid` as a link anywhere
       
   231         """
       
   232         action = self.vreg.select_action(actionid, self.req, self.rset,
       
   233                                          row=row)
       
   234         if action:
       
   235             label = label or self.req._(action.title)
       
   236             return u'<a href="%s">%s</a>' % (html_escape(action.url()), label)
       
   237         return u''
       
   238     
       
   239     def html_headers(self):
       
   240         """return a list of html headers (eg something to be inserted between
       
   241         <head> and </head> of the returned page
       
   242 
       
   243         by default return a meta tag to disable robot indexation of the page
       
   244         """
       
   245         return [NOINDEX]
       
   246     
       
   247     def page_title(self):
       
   248         """returns a title according to the result set - used for the
       
   249         title in the HTML header
       
   250         """
       
   251         vtitle = self.req.form.get('vtitle')
       
   252         if vtitle:
       
   253             return self.req._(vtitle)
       
   254         # class defined title will only be used if the resulting title doesn't
       
   255         # seem clear enough
       
   256         vtitle = getattr(self, 'title', None) or u''
       
   257         if vtitle:
       
   258             vtitle = self.req._(vtitle)
       
   259         rset = self.rset
       
   260         if rset and rset.rowcount:
       
   261             if rset.rowcount == 1:
       
   262                 try:
       
   263                     entity = self.complete_entity(0)
       
   264                     # use long_title to get context information if any
       
   265                     clabel = entity.dc_long_title()
       
   266                 except NotAnEntity:
       
   267                     clabel = display_name(self.req, rset.description[0][0])
       
   268                     clabel = u'%s (%s)' % (clabel, vtitle)
       
   269             else :
       
   270                 etypes = rset.column_types(0)
       
   271                 if len(etypes) == 1:
       
   272                     etype = iter(etypes).next()
       
   273                     clabel = display_name(self.req, etype, 'plural')
       
   274                 else :
       
   275                     clabel = u'#[*] (%s)' % vtitle
       
   276         else:
       
   277             clabel = vtitle
       
   278         return u'%s (%s)' % (clabel, self.req.property_value('ui.site-title'))
       
   279 
       
   280     def output_url_builder( self, name, url, args ):
       
   281         self.w(u'<script language="JavaScript"><!--\n' \
       
   282                u'function %s( %s ) {\n' % (name, ','.join(args) ) )
       
   283         url_parts = url.split("%s")
       
   284         self.w(u' url="%s"' % url_parts[0] )
       
   285         for arg, part in zip(args, url_parts[1:]):
       
   286             self.w(u'+str(%s)' % arg )
       
   287             if part:
       
   288                 self.w(u'+"%s"' % part)
       
   289         self.w('\n document.window.href=url;\n')
       
   290         self.w('}\n-->\n</script>\n')
       
   291 
       
   292     def create_url(self, etype, **kwargs):
       
   293         """ return the url of the entity creation form for a given entity type"""
       
   294         return self.req.build_url('add/%s'%etype, **kwargs)
       
   295 
       
   296         
       
   297 # concrete views base classes #################################################
       
   298 
       
   299 class EntityView(View):
       
   300     """base class for views applying on an entity (i.e. uniform result set)
       
   301     """
       
   302     __registerer__ = accepts_registerer
       
   303     __selectors__ = (accept_selector,)
       
   304     category = 'entityview'
       
   305     
       
   306     def field(self, label, value, row=True, show_label=True, w=None, tr=True):
       
   307         """ read-only field """
       
   308         if w is None:
       
   309             w = self.w
       
   310         if row:
       
   311             w(u'<div class="row">')
       
   312         if show_label:
       
   313             if tr:
       
   314                 label = display_name(self.req, label)
       
   315             w(u'<span class="label">%s</span>' % label)
       
   316         w(u'<div class="field">%s</div>' % value)
       
   317         if row:
       
   318             w(u'</div>')
       
   319 
       
   320         
       
   321 class StartupView(View):
       
   322     """base class for views which doesn't need a particular result set
       
   323     to be displayed (so they can always be displayed !)
       
   324     """
       
   325     __registerer__ = priority_registerer
       
   326     __selectors__ = (in_group_selector, norset_selector)
       
   327     require_groups = ()
       
   328     category = 'startupview'
       
   329     
       
   330     def url(self):
       
   331         """return the url associated with this view. We can omit rql here"""
       
   332         return self.build_url('view', vid=self.id)
       
   333 
       
   334     def html_headers(self):
       
   335         """return a list of html headers (eg something to be inserted between
       
   336         <head> and </head> of the returned page
       
   337 
       
   338         by default startup views are indexed
       
   339         """
       
   340         return []
       
   341 
       
   342 
       
   343 class EntityStartupView(EntityView):
       
   344     """base class for entity views which may also be applied to None
       
   345     result set (usually a default rql is provided by the view class)
       
   346     """
       
   347     __registerer__ = accepts_registerer
       
   348     __selectors__ = (chainfirst(norset_selector, accept_selector),)
       
   349     
       
   350     default_rql = None
       
   351     
       
   352     def __init__(self, req, rset):
       
   353         super(EntityStartupView, self).__init__(req, rset)
       
   354         if rset is None:
       
   355             # this instance is not in the "entityview" category
       
   356             self.category = 'startupview'
       
   357 
       
   358     def startup_rql(self):
       
   359         """return some rql to be executedif the result set is None"""
       
   360         return self.default_rql
       
   361     
       
   362     def call(self, **kwargs):
       
   363         """override call to execute rql returned by the .startup_rql
       
   364         method if necessary
       
   365         """
       
   366         if self.rset is None:
       
   367             self.rset = self.req.execute(self.startup_rql())
       
   368         rset = self.rset
       
   369         for i in xrange(len(rset)):
       
   370             self.wview(self.id, rset, row=i, **kwargs)
       
   371 
       
   372     def url(self):
       
   373         """return the url associated with this view. We can omit rql if we
       
   374         are on a result set on which we do not apply.
       
   375         """
       
   376         if not self.__select__(self.req, self.rset):
       
   377             return self.build_url(vid=self.id)
       
   378         return super(EntityStartupView, self).url()
       
   379 
       
   380     
       
   381 class AnyRsetView(View):
       
   382     """base class for views applying on any non empty result sets"""
       
   383     __registerer__ = priority_registerer
       
   384     __selectors__ = (anyrset_selector,)
       
   385     
       
   386     category = 'anyrsetview'
       
   387     
       
   388 
       
   389 class EmptyRsetView(View):
       
   390     """base class for views applying on any empty result sets"""
       
   391     __registerer__ = priority_registerer
       
   392     __selectors__ = (emptyrset_selector,)
       
   393 
       
   394 
       
   395 # concrete template base classes ##############################################
       
   396 
       
   397 class Template(View):
       
   398     """a template is almost like a view, except that by default a template
       
   399     is only used globally (i.e. no result set adaptation)
       
   400     """
       
   401     __registry__ = 'templates'
       
   402     __registerer__ = priority_registerer
       
   403     __selectors__ = (in_group_selector,)
       
   404 
       
   405     require_groups = ()
       
   406     
       
   407     def template(self, oid, **kwargs):
       
   408         """shortcut to self.registry.render method on the templates registry"""
       
   409         w = kwargs.pop('w', self.w)
       
   410         self.vreg.render('templates', oid, self.req, w=w, **kwargs)
       
   411 
       
   412 
       
   413 class MainTemplate(Template):
       
   414     """main template are primary access point to render a full HTML page.
       
   415     There is usually at least a regular main template and a simple fallback
       
   416     one to display error if the first one failed
       
   417     """
       
   418 
       
   419     base_doctype = STRICT_DOCTYPE
       
   420 
       
   421     @property
       
   422     def doctype(self):
       
   423         if self.req.xhtml_browser():
       
   424             return self.base_doctype % CW_XHTML_EXTENSIONS
       
   425         return self.base_doctype % ''
       
   426 
       
   427     def set_stream(self, w=None, templatable=True):
       
   428         if templatable and self.w is not None:
       
   429             return
       
   430 
       
   431         if w is None:
       
   432             if self.binary:
       
   433                 self._stream = stream = StringIO()
       
   434             elif not templatable:
       
   435                 # not templatable means we're using a non-html view, we don't
       
   436                 # want the HTMLStream stuff to interfere during data generation
       
   437                 self._stream = stream = UStringIO()
       
   438             else:
       
   439                 self._stream = stream = HTMLStream(self.req)
       
   440             w = stream.write
       
   441         else:
       
   442             stream = None
       
   443         self.w = w
       
   444         return stream
       
   445 
       
   446     def write_doctype(self, xmldecl=True):
       
   447         assert isinstance(self._stream, HTMLStream)
       
   448         self._stream.doctype = self.doctype
       
   449         if not xmldecl:
       
   450             self._stream.xmldecl = u''
       
   451 
       
   452 # viewable components base classes ############################################
       
   453 
       
   454 class VComponent(ComponentMixIn, View):
       
   455     """base class for displayable components"""
       
   456     property_defs = {
       
   457         'visible':  dict(type='Boolean', default=True,
       
   458                          help=_('display the component or not')),}
       
   459 
       
   460 class SingletonVComponent(VComponent):
       
   461     """base class for displayable unique components"""
       
   462     __registerer__ = priority_registerer