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