cubicweb/web/views/basetemplates.py
changeset 11057 0b59724cb3f2
parent 10666 7f6b5f023884
child 11280 eb1d74ce6b61
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2012 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 """default templates for CubicWeb web client"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 from cubicweb import _
       
    22 
       
    23 from logilab.mtconverter import xml_escape
       
    24 from logilab.common.deprecation import class_renamed
       
    25 from logilab.common.registry import objectify_predicate
       
    26 from logilab.common.decorators import classproperty
       
    27 
       
    28 from cubicweb.predicates import match_kwargs, no_cnx, anonymous_user
       
    29 from cubicweb.view import View, MainTemplate, NOINDEX, NOFOLLOW, StartupView
       
    30 from cubicweb.utils import UStringIO
       
    31 from cubicweb.schema import display_name
       
    32 from cubicweb.web import component, formfields as ff, formwidgets as fw
       
    33 from cubicweb.web.views import forms
       
    34 
       
    35 # main templates ##############################################################
       
    36 
       
    37 class LogInOutTemplate(MainTemplate):
       
    38 
       
    39     def call(self):
       
    40         self.set_request_content_type()
       
    41         w = self.w
       
    42         self.write_doctype()
       
    43         self.template_header('text/html', self._cw._('login_action'))
       
    44         w(u'<body>\n')
       
    45         self.content(w)
       
    46         w(u'</body>')
       
    47 
       
    48     def template_header(self, content_type, view=None, page_title='', additional_headers=()):
       
    49         w = self.whead
       
    50         # explictly close the <base> tag to avoid IE 6 bugs while browsing DOM
       
    51         w(u'<base href="%s"></base>' % xml_escape(self._cw.base_url()))
       
    52         w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
       
    53           % (content_type, self._cw.encoding))
       
    54         w(NOINDEX)
       
    55         w(NOFOLLOW)
       
    56         w(u'\n'.join(additional_headers) + u'\n')
       
    57         self.wview('htmlheader', rset=self.cw_rset)
       
    58         w(u'<title>%s</title>\n' % xml_escape(page_title))
       
    59 
       
    60     def content(self):
       
    61         raise NotImplementedError()
       
    62 
       
    63 
       
    64 class LogInTemplate(LogInOutTemplate):
       
    65     __regid__ = 'login'
       
    66     __select__ = anonymous_user()
       
    67     title = 'log in'
       
    68 
       
    69     def content(self, w):
       
    70         self.wview('logform', rset=self.cw_rset, id='loginBox', klass='')
       
    71 
       
    72 
       
    73 class LoggedOutTemplate(StartupView):
       
    74     __regid__ = 'loggedout'
       
    75     __select__ = anonymous_user()
       
    76     title = 'logged out'
       
    77 
       
    78     def call(self):
       
    79         msg = self._cw._('you have been logged out')
       
    80         if self._cw.cnx:
       
    81             comp = self._cw.vreg['components'].select('applmessages', self._cw)
       
    82             comp.render(w=self.w, msg=msg)
       
    83             self.wview('index')
       
    84         else:
       
    85             self.w(u'<h2>%s</h2>' % msg)
       
    86 
       
    87 
       
    88 @objectify_predicate
       
    89 def modal_view(cls, req, rset, *args, **kwargs):
       
    90     if req.form.get('__modal', None):
       
    91         return 1
       
    92 
       
    93 @objectify_predicate
       
    94 def templatable_view(cls, req, rset, *args, **kwargs):
       
    95     view = kwargs.pop('view', None)
       
    96     if view is None:
       
    97         return 1
       
    98     if view.binary:
       
    99         return 0
       
   100     if '__notemplate' in req.form:
       
   101         return 0
       
   102     return view.templatable
       
   103 
       
   104 
       
   105 class NonTemplatableViewTemplate(MainTemplate):
       
   106     """main template for any non templatable views (xml, binaries, etc.)"""
       
   107     __regid__ = 'main-template'
       
   108     __select__ = ~templatable_view()
       
   109 
       
   110     def call(self, view):
       
   111         view.set_request_content_type()
       
   112         view.set_stream()
       
   113         if (('__notemplate' in self._cw.form)
       
   114             and view.templatable
       
   115             and view.content_type == self._cw.html_content_type()):
       
   116             view.w(u'<div>')
       
   117             view.render()
       
   118             view.w(u'</div>')
       
   119         else:
       
   120             view.render()
       
   121         # have to replace our stream by view's stream (which may be a binary
       
   122         # stream)
       
   123         self._stream = view._stream
       
   124 
       
   125 
       
   126 class ModalMainTemplate(MainTemplate):
       
   127     """ a no-decoration main template for standard views
       
   128     that typically live in a modal context """
       
   129     __regid__ = 'main-template'
       
   130     __select__ = templatable_view() & modal_view()
       
   131 
       
   132     def call(self, view):
       
   133         view.set_request_content_type()
       
   134         view.render(w=self.w)
       
   135 
       
   136 
       
   137 class TheMainTemplate(MainTemplate):
       
   138     """default main template :
       
   139 
       
   140     - call header / footer templates
       
   141     """
       
   142     __regid__ = 'main-template'
       
   143     __select__ = templatable_view()
       
   144 
       
   145     def call(self, view):
       
   146         self.set_request_content_type()
       
   147         self.template_header(self.content_type, view)
       
   148         w = self.w
       
   149         w(u'<div id="pageContent">\n')
       
   150         vtitle = self._cw.form.get('vtitle')
       
   151         if vtitle:
       
   152             w(u'<div class="vtitle">%s</div>\n' % xml_escape(vtitle))
       
   153         # display entity type restriction component
       
   154         etypefilter = self._cw.vreg['components'].select_or_none(
       
   155             'etypenavigation', self._cw, rset=self.cw_rset)
       
   156         if etypefilter and etypefilter.cw_propval('visible'):
       
   157             etypefilter.render(w=w)
       
   158         nav_html = UStringIO()
       
   159         if view and not view.handle_pagination:
       
   160             view.paginate(w=nav_html.write)
       
   161         w(nav_html.getvalue())
       
   162         w(u'<div id="contentmain">\n')
       
   163         view.render(w=w)
       
   164         w(u'</div>\n') # close id=contentmain
       
   165         w(nav_html.getvalue())
       
   166         w(u'</div>\n') # closes id=pageContent
       
   167         self.template_footer(view)
       
   168 
       
   169     def template_header(self, content_type, view=None, page_title='', additional_headers=()):
       
   170         page_title = page_title or view.page_title()
       
   171         additional_headers = additional_headers or view.html_headers()
       
   172         self.template_html_header(content_type, page_title, additional_headers)
       
   173         self.template_body_header(view)
       
   174 
       
   175     def template_html_header(self, content_type, page_title, additional_headers=()):
       
   176         w = self.whead
       
   177         lang = self._cw.lang
       
   178         self.write_doctype()
       
   179         self._cw.html_headers.define_var('BASE_URL', self._cw.base_url())
       
   180         self._cw.html_headers.define_var('DATA_URL', self._cw.datadir_url)
       
   181         w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
       
   182           % (content_type, self._cw.encoding))
       
   183         w(u'\n'.join(additional_headers) + u'\n')
       
   184         self.wview('htmlheader', rset=self.cw_rset)
       
   185         if page_title:
       
   186             w(u'<title>%s</title>\n' % xml_escape(page_title))
       
   187 
       
   188     def template_body_header(self, view):
       
   189         w = self.w
       
   190         w(u'<body>\n')
       
   191         self.wview('header', rset=self.cw_rset, view=view)
       
   192         w(u'<div id="page"><table width="100%" border="0" id="mainLayout"><tr>\n')
       
   193         self.nav_column(view, 'left')
       
   194         w(u'<td id="contentColumn">\n')
       
   195         components = self._cw.vreg['components']
       
   196         rqlcomp = components.select_or_none('rqlinput', self._cw, rset=self.cw_rset)
       
   197         if rqlcomp:
       
   198             rqlcomp.render(w=self.w, view=view)
       
   199         msgcomp = components.select_or_none('applmessages', self._cw, rset=self.cw_rset)
       
   200         if msgcomp:
       
   201             msgcomp.render(w=self.w)
       
   202         self.content_header(view)
       
   203 
       
   204     def template_footer(self, view=None):
       
   205         self.content_footer(view)
       
   206         self.w(u'</td>\n')
       
   207         self.nav_column(view, 'right')
       
   208         self.w(u'</tr></table></div>\n')
       
   209         self.wview('footer', rset=self.cw_rset)
       
   210         self.w(u'</body>')
       
   211 
       
   212     def nav_column(self, view, context):
       
   213         boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   214             self._cw, rset=self.cw_rset, view=view, context=context))
       
   215         if boxes:
       
   216             getlayout = self._cw.vreg['components'].select
       
   217             self.w(u'<td id="navColumn%s"><div class="navboxes">\n' % context.capitalize())
       
   218             for box in boxes:
       
   219                 box.render(w=self.w, view=view)
       
   220             self.w(u'</div></td>\n')
       
   221 
       
   222     def content_header(self, view=None):
       
   223         """by default, display informal messages in content header"""
       
   224         self.wview('contentheader', rset=self.cw_rset, view=view)
       
   225 
       
   226     def content_footer(self, view=None):
       
   227         self.wview('contentfooter', rset=self.cw_rset, view=view)
       
   228 
       
   229 
       
   230 class ErrorTemplate(TheMainTemplate):
       
   231     """fallback template if an internal error occurred during displaying the main
       
   232     template. This template may be called for authentication error, which means
       
   233     that req.cnx and req.user may not be set.
       
   234     """
       
   235     __regid__ = 'error-template'
       
   236 
       
   237     def call(self):
       
   238         """display an unexpected error"""
       
   239         self.set_request_content_type()
       
   240         self._cw.reset_headers()
       
   241         view = self._cw.vreg['views'].select('error', self._cw, rset=self.cw_rset)
       
   242         self.template_header(self.content_type, view, self._cw._('an error occurred'),
       
   243                              [NOINDEX, NOFOLLOW])
       
   244         view.render(w=self.w)
       
   245         self.template_footer(view)
       
   246 
       
   247     def template_header(self, content_type, view=None, page_title='', additional_headers=()):
       
   248         w = self.whead
       
   249         lang = self._cw.lang
       
   250         self.write_doctype()
       
   251         w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
       
   252           % (content_type, self._cw.encoding))
       
   253         w(u'\n'.join(additional_headers))
       
   254         self.wview('htmlheader', rset=self.cw_rset)
       
   255         w(u'<title>%s</title>\n' % xml_escape(page_title))
       
   256         self.w(u'<body>\n')
       
   257 
       
   258     def template_footer(self, view=None):
       
   259         self.w(u'</body>')
       
   260 
       
   261 
       
   262 class SimpleMainTemplate(TheMainTemplate):
       
   263 
       
   264     __regid__ = 'main-no-top'
       
   265 
       
   266     def template_header(self, content_type, view=None, page_title='', additional_headers=()):
       
   267         page_title = page_title or view.page_title()
       
   268         additional_headers = additional_headers or view.html_headers()
       
   269         whead = self.whead
       
   270         lang = self._cw.lang
       
   271         self.write_doctype()
       
   272         whead(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n'
       
   273               % (content_type, self._cw.encoding))
       
   274         whead(u'\n'.join(additional_headers) + u'\n')
       
   275         self.wview('htmlheader', rset=self.cw_rset)
       
   276         w = self.w
       
   277         whead(u'<title>%s</title>\n' % xml_escape(page_title))
       
   278         w(u'<body>\n')
       
   279         w(u'<div id="page">')
       
   280         w(u'<table width="100%" border="0" id="mainLayout"><tr>\n')
       
   281         w(u'<td id="navColumnLeft">\n')
       
   282         self.topleft_header()
       
   283         boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   284             self._cw, rset=self.cw_rset, view=view, context='left'))
       
   285         if boxes:
       
   286             w(u'<div class="navboxes">\n')
       
   287             for box in boxes:
       
   288                 box.render(w=w)
       
   289             self.w(u'</div>\n')
       
   290         w(u'</td>')
       
   291         w(u'<td id="contentColumn" rowspan="2">')
       
   292 
       
   293     def topleft_header(self):
       
   294         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
       
   295                                                           rset=self.cw_rset)
       
   296         if logo and logo.cw_propval('visible'):
       
   297             w = self.w
       
   298             w(u'<table id="header"><tr>\n')
       
   299             w(u'<td>')
       
   300             logo.render(w=w)
       
   301             w(u'</td>\n')
       
   302             w(u'</tr></table>\n')
       
   303 
       
   304 
       
   305 # page parts templates ########################################################
       
   306 
       
   307 class HTMLHeader(View):
       
   308     """default html headers"""
       
   309     __regid__ = 'htmlheader'
       
   310 
       
   311     def call(self, **kwargs):
       
   312         self.favicon()
       
   313         self.stylesheets()
       
   314         self.javascripts()
       
   315         self.alternates()
       
   316 
       
   317     def favicon(self):
       
   318         favicon = self._cw.uiprops.get('FAVICON', None)
       
   319         if favicon:
       
   320             self.whead(u'<link rel="shortcut icon" href="%s"/>\n' % favicon)
       
   321 
       
   322     def stylesheets(self):
       
   323         req = self._cw
       
   324         add_css = req.add_css
       
   325         for css in req.uiprops['STYLESHEETS']:
       
   326             add_css(css, localfile=False)
       
   327         for css in req.uiprops['STYLESHEETS_PRINT']:
       
   328             add_css(css, u'print', localfile=False)
       
   329         for css in req.uiprops['STYLESHEETS_IE']:
       
   330             add_css(css, localfile=False, ieonly=True)
       
   331 
       
   332     def javascripts(self):
       
   333         for jscript in self._cw.uiprops['JAVASCRIPTS']:
       
   334             self._cw.add_js(jscript, localfile=False)
       
   335 
       
   336     def alternates(self):
       
   337         urlgetter = self._cw.vreg['components'].select_or_none('rss_feed_url',
       
   338                                                            self._cw, rset=self.cw_rset)
       
   339         if urlgetter is not None:
       
   340             self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n'
       
   341                        %  xml_escape(urlgetter.feed_url()))
       
   342 
       
   343 
       
   344 class HTMLPageHeader(View):
       
   345     """default html page header"""
       
   346     __regid__ = 'header'
       
   347     main_cell_components = ('appliname', 'breadcrumbs')
       
   348     headers = (('headtext', 'header-left'),
       
   349                ('header-center', 'header-center'),
       
   350                ('header-right', 'header-right')
       
   351                )
       
   352 
       
   353     def call(self, view, **kwargs):
       
   354         self.main_header(view)
       
   355         self.w(u'<div id="stateheader">')
       
   356         self.state_header()
       
   357         self.w(u'</div>')
       
   358 
       
   359     def main_header(self, view):
       
   360         """build the top menu with authentification info and the rql box"""
       
   361         w = self.w
       
   362         w(u'<table id="header"><tr>\n')
       
   363         for colid, context in self.headers:
       
   364             w(u'<td id="%s">' % colid)
       
   365             components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   366                 self._cw, rset=self.cw_rset, view=view, context=context)
       
   367             for comp in components:
       
   368                 comp.render(w=w)
       
   369                 w(u'&#160;')
       
   370             w(u'</td>')
       
   371         w(u'</tr></table>\n')
       
   372 
       
   373     def state_header(self):
       
   374         state = self._cw.search_state
       
   375         if state[0] == 'normal':
       
   376             return
       
   377         _ = self._cw._
       
   378         value = self._cw.view('oneline', self._cw.eid_rset(state[1][1]))
       
   379         msg = ' '.join((_("searching for"),
       
   380                         display_name(self._cw, state[1][3]),
       
   381                         _("to associate with"), value,
       
   382                         _("by relation"), '"',
       
   383                         display_name(self._cw, state[1][2], state[1][0]),
       
   384                         '"'))
       
   385         return self.w(u'<div class="stateMessage">%s</div>' % msg)
       
   386 
       
   387 
       
   388 class HTMLPageFooter(View):
       
   389     """default html page footer: include footer actions"""
       
   390     __regid__ = 'footer'
       
   391 
       
   392     def call(self, **kwargs):
       
   393         self.w(u'<div id="footer">')
       
   394         self.footer_content()
       
   395         self.w(u'</div>')
       
   396 
       
   397     def footer_content(self):
       
   398         actions = self._cw.vreg['actions'].possible_actions(self._cw,
       
   399                                                             rset=self.cw_rset)
       
   400         footeractions = actions.get('footer', ())
       
   401         for i, action in enumerate(footeractions):
       
   402             self.w(u'<a href="%s">%s</a>' % (action.url(),
       
   403                                              self._cw._(action.title)))
       
   404             if i < (len(footeractions) - 1):
       
   405                 self.w(u' | ')
       
   406 
       
   407 class HTMLContentHeader(View):
       
   408     """default html page content header:
       
   409     * include message component if selectable for this request
       
   410     * include selectable content navigation components
       
   411     """
       
   412     __regid__ = 'contentheader'
       
   413 
       
   414     def call(self, view, **kwargs):
       
   415         """by default, display informal messages in content header"""
       
   416         components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   417             self._cw, rset=self.cw_rset, view=view, context='navtop')
       
   418         if components:
       
   419             self.w(u'<div id="contentheader">')
       
   420             for comp in components:
       
   421                 comp.render(w=self.w, view=view)
       
   422             self.w(u'</div><div class="clear"></div>')
       
   423 
       
   424 
       
   425 class HTMLContentFooter(View):
       
   426     """default html page content footer: include selectable content navigation
       
   427     components
       
   428     """
       
   429     __regid__ = 'contentfooter'
       
   430 
       
   431     def call(self, view, **kwargs):
       
   432         components = self._cw.vreg['ctxcomponents'].poss_visible_objects(
       
   433             self._cw, rset=self.cw_rset, view=view, context='navbottom')
       
   434         if components:
       
   435             self.w(u'<div id="contentfooter">')
       
   436             for comp in components:
       
   437                 comp.render(w=self.w, view=view)
       
   438             self.w(u'</div>')
       
   439 
       
   440 class BaseLogForm(forms.FieldsForm):
       
   441     """Abstract Base login form to be used by any login form
       
   442     """
       
   443     __abstract__ = True
       
   444 
       
   445     __regid__ = 'logform'
       
   446     domid = 'loginForm'
       
   447     needs_css = ('cubicweb.login.css',)
       
   448 
       
   449     onclick_base = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
       
   450     onclick_args = (None, None)
       
   451 
       
   452     @classproperty
       
   453     def form_buttons(cls):
       
   454         # we use a property because sub class will need to define their own onclick_args.
       
   455         # Therefor we can't juste make the string formating when instanciating this class
       
   456         onclick = cls.onclick_base % cls.onclick_args
       
   457         form_buttons = [fw.SubmitButton(label=_('log in'),
       
   458                                     attrs={'class': 'loginButton'}),
       
   459                         fw.ResetButton(label=_('cancel'),
       
   460                                        attrs={'class': 'loginButton',
       
   461                                               'onclick': onclick}),]
       
   462         ## Can't shortcut next access because __dict__ is a "dictproxy" which 
       
   463         ## does not support items assignement.
       
   464         # cls.__dict__['form_buttons'] = form_buttons
       
   465         return form_buttons
       
   466 
       
   467     def form_action(self):
       
   468         if self.action is None:
       
   469             # reuse existing redirection if it exists
       
   470             target = self._cw.form.get('postlogin_path',
       
   471                                        self._cw.relative_path())
       
   472             url_args = {}
       
   473             if target and target != '/':
       
   474                 url_args['postlogin_path'] = target
       
   475             return self._cw.build_url('login', __secure__=True, **url_args)
       
   476         return super(BaseLogForm, self).form_action()
       
   477 
       
   478 class LogForm(BaseLogForm):
       
   479     """Simple login form that send username and password
       
   480     """
       
   481     __regid__ = 'logform'
       
   482     domid = 'loginForm'
       
   483     needs_css = ('cubicweb.login.css',)
       
   484     # XXX have to recall fields name since python is mangling __login/__password
       
   485     __login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
       
   486     __password = ff.StringField('__password', label=_('password'),
       
   487                                 widget=fw.PasswordSingleInput({'class': 'data'}))
       
   488 
       
   489     onclick_args =  ('popupLoginBox', '__login')
       
   490 
       
   491 
       
   492 class LogFormView(View):
       
   493     # XXX an awful lot of hardcoded assumptions there
       
   494     #     makes it unobvious to reuse/specialize
       
   495     __regid__ = 'logform'
       
   496     __select__ = match_kwargs('id', 'klass')
       
   497 
       
   498     title = 'log in'
       
   499 
       
   500     def call(self, id, klass, title=True, showmessage=True):
       
   501         w = self.w
       
   502         w(u'<div id="%s" class="%s">' % (id, klass))
       
   503         if title:
       
   504             stitle = self._cw.property_value('ui.site-title')
       
   505             if stitle:
       
   506                 stitle = xml_escape(stitle)
       
   507             else:
       
   508                 stitle = u'&#160;'
       
   509             w(u'<div class="loginTitle">%s</div>' % stitle)
       
   510         w(u'<div class="loginContent">\n')
       
   511         if showmessage and self._cw.message:
       
   512             w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
       
   513         config = self._cw.vreg.config
       
   514         if config['auth-mode'] != 'http':
       
   515             self.login_form(id) # Cookie authentication
       
   516         w(u'</div>')
       
   517         w(u'</div>\n')
       
   518 
       
   519     def login_form(self, id):
       
   520         cw = self._cw
       
   521         form = cw.vreg['forms'].select('logform', cw)
       
   522         if cw.vreg.config['allow-email-login']:
       
   523             label = cw._('login or email')
       
   524         else:
       
   525             label = cw.pgettext('CWUser', 'login')
       
   526         form.field_by_name('__login').label = label
       
   527         form.render(w=self.w, table_class='', display_progress_div=False)
       
   528         cw.html_headers.add_onload('jQuery("#__login:visible").focus()')
       
   529 
       
   530 LogFormTemplate = class_renamed('LogFormTemplate', LogFormView)