|
1 # -*- coding: utf-8 -*- |
|
2 """default templates for CubicWeb web client |
|
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 logilab.mtconverter import html_escape |
|
11 |
|
12 from cubicweb import NoSelectableObject, ObjectNotFound |
|
13 from cubicweb.common.view import Template, MainTemplate, NOINDEX, NOFOLLOW |
|
14 from cubicweb.common.selectors import nfentity_selector |
|
15 from cubicweb.common.utils import make_uid |
|
16 |
|
17 from cubicweb.web.views.baseviews import vid_from_rset |
|
18 |
|
19 # main templates ############################################################## |
|
20 |
|
21 |
|
22 class LogInOutTemplate(MainTemplate): |
|
23 |
|
24 def call(self): |
|
25 self.set_request_content_type() |
|
26 w = self.w |
|
27 self.write_doctype() |
|
28 lang = self.req.lang |
|
29 self.template_header('text/html', self.req._('login_action')) |
|
30 w(u'<body>\n') |
|
31 self.content(w) |
|
32 w(u'</body>') |
|
33 |
|
34 def template_header(self, content_type, view=None, page_title='', additional_headers=()): |
|
35 w = self.whead |
|
36 # explictly close the <base> tag to avoid IE 6 bugs while browsing DOM |
|
37 w(u'<base href="%s"></base>' % html_escape(self.req.base_url())) |
|
38 w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n' |
|
39 % (content_type, self.req.encoding)) |
|
40 w(NOINDEX) |
|
41 w(NOFOLLOW) |
|
42 w(u'\n'.join(additional_headers) + u'\n') |
|
43 self.template('htmlheader', rset=self.rset) |
|
44 w(u'<title>%s</title>\n' % html_escape(page_title)) |
|
45 |
|
46 |
|
47 class LogInTemplate(LogInOutTemplate): |
|
48 id = 'login' |
|
49 title = 'log in' |
|
50 |
|
51 def content(self, w): |
|
52 self.template('logform', rset=self.rset, id='loginBox', klass='') |
|
53 |
|
54 |
|
55 class LoggedOutTemplate(LogInOutTemplate): |
|
56 id = 'loggedout' |
|
57 title = 'logged out' |
|
58 |
|
59 def content(self, w): |
|
60 msg = self.req._('you have been logged out') |
|
61 w(u'<h1 class="noborder">%s</h1>\n' % msg) |
|
62 if self.config['anonymous-user']: |
|
63 indexurl = self.build_url('view', vid='index', __message=msg) |
|
64 w(u'<p><a href="%s">%s</a><p>' % ( |
|
65 html_escape(indexurl), |
|
66 self.req._('go back to the index page'))) |
|
67 |
|
68 |
|
69 class TheMainTemplate(MainTemplate): |
|
70 """default main template : |
|
71 |
|
72 - call header / footer templates |
|
73 - build result set |
|
74 - guess and call an appropriate view through the view manager |
|
75 """ |
|
76 id = 'main' |
|
77 |
|
78 def _select_view_and_rset(self): |
|
79 req = self.req |
|
80 if self.rset is None and not hasattr(req, '_rql_processed'): |
|
81 req._rql_processed = True |
|
82 rset = self.process_rql(req.form.get('rql')) |
|
83 else: |
|
84 rset = self.rset |
|
85 # handle special "method" param when necessary |
|
86 # XXX this should probably not be in the template (controller ?), however: |
|
87 # * we need to have the displayed rset |
|
88 # * we don't want to handle it in each view |
|
89 if rset and rset.rowcount == 1 and '__method' in req.form: |
|
90 entity = rset.get_entity(0, 0) |
|
91 try: |
|
92 method = getattr(entity, req.form.pop('__method')) |
|
93 method() |
|
94 except Exception, ex: |
|
95 self.exception('while handling __method') |
|
96 req.set_message(req._("error while handling __method: %s") % req._(ex)) |
|
97 vid = req.form.get('vid') or vid_from_rset(req, rset, self.schema) |
|
98 try: |
|
99 view = self.vreg.select_view(vid, req, rset) |
|
100 except ObjectNotFound: |
|
101 self.warning("the view %s could not be found", vid) |
|
102 req.set_message(req._("The view %s could not be found") % vid) |
|
103 vid = vid_from_rset(req, rset, self.schema) |
|
104 view = self.vreg.select_view(vid, req, rset) |
|
105 except NoSelectableObject: |
|
106 if rset: |
|
107 req.set_message(req._("The view %s can not be applied to this query") % vid) |
|
108 else: |
|
109 req.set_message(req._("You have no access to this view or it's not applyable to current data")) |
|
110 self.warning("the view %s can not be applied to this query", vid) |
|
111 vid = vid_from_rset(req, rset, self.schema) |
|
112 view = self.vreg.select_view(vid, req, rset) |
|
113 return view, rset |
|
114 |
|
115 def call(self): |
|
116 view, rset = self._select_view_and_rset() |
|
117 req = self.req |
|
118 # update breadcrumps **before** validating cache, unless the view |
|
119 # specifies explicitly it should not be added to breadcrumb or the |
|
120 # view is a binary view |
|
121 if view.add_to_breadcrumbs and not view.binary: |
|
122 req.update_breadcrumbs() |
|
123 view.set_http_cache_headers() |
|
124 req.validate_cache() |
|
125 with_templates = not view.binary and view.templatable and \ |
|
126 not req.form.has_key('__notemplate') |
|
127 if not with_templates: |
|
128 view.set_request_content_type() |
|
129 self.set_stream(templatable=False) |
|
130 else: |
|
131 self.set_request_content_type() |
|
132 content_type = self.content_type |
|
133 self.template_header(content_type, view) |
|
134 if view.binary: |
|
135 # have to replace our unicode stream using view's binary stream |
|
136 view.dispatch() |
|
137 assert self._stream, 'duh, template used as a sub-view ?? (%s)' % self._stream |
|
138 self._stream = view._stream |
|
139 else: |
|
140 view.dispatch(w=self.w) |
|
141 if with_templates: |
|
142 self.template_footer(view) |
|
143 |
|
144 |
|
145 def process_rql(self, rql): |
|
146 """execute rql if specified""" |
|
147 if rql: |
|
148 self.ensure_ro_rql(rql) |
|
149 if not isinstance(rql, unicode): |
|
150 rql = unicode(rql, self.req.encoding) |
|
151 pp = self.vreg.select_component('magicsearch', self.req) |
|
152 self.rset = pp.process_query(rql, self.req) |
|
153 return self.rset |
|
154 return None |
|
155 |
|
156 def template_header(self, content_type, view=None, page_title='', additional_headers=()): |
|
157 page_title = page_title or view.page_title() |
|
158 additional_headers = additional_headers or view.html_headers() |
|
159 self.template_html_header(content_type, page_title, additional_headers) |
|
160 self.template_body_header(view) |
|
161 # display entity type restriction component |
|
162 etypefilter = self.vreg.select_component('etypenavigation', |
|
163 self.req, self.rset) |
|
164 if etypefilter and etypefilter.propval('visible'): |
|
165 etypefilter.dispatch(w=self.w) |
|
166 self.pagination(self.req, self.rset, self.w, not view.need_navigation) |
|
167 self.w(u'<div id="contentmain">\n') |
|
168 |
|
169 def template_html_header(self, content_type, page_title, additional_headers=()): |
|
170 w = self.whead |
|
171 lang = self.req.lang |
|
172 self.write_doctype() |
|
173 w(u'<base href="%s" />' % html_escape(self.req.base_url())) |
|
174 w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n' |
|
175 % (content_type, self.req.encoding)) |
|
176 w(u'\n'.join(additional_headers) + u'\n') |
|
177 self.template('htmlheader', rset=self.rset) |
|
178 if page_title: |
|
179 w(u'<title>%s</title>\n' % html_escape(page_title)) |
|
180 |
|
181 def template_body_header(self, view): |
|
182 w = self.w |
|
183 w(u'<body>\n') |
|
184 self.template('header', rset=self.rset, view=view) |
|
185 w(u'<div id="page"><table width="100%" border="0" id="mainLayout"><tr>\n') |
|
186 self.nav_column(view, 'left') |
|
187 w(u'<td id="contentcol">\n') |
|
188 rqlcomp = self.vreg.select_component('rqlinput', self.req, self.rset) |
|
189 if rqlcomp: |
|
190 rqlcomp.dispatch(w=self.w, view=view) |
|
191 msgcomp = self.vreg.select_component('applmessages', self.req, self.rset) |
|
192 if msgcomp: |
|
193 msgcomp.dispatch(w=self.w) |
|
194 self.content_header(view) |
|
195 w(u'<div id="pageContent">\n') |
|
196 vtitle = self.req.form.get('vtitle') |
|
197 if vtitle: |
|
198 w(u'<h1 class="vtitle">%s</h1>\n' % vtitle) |
|
199 |
|
200 def template_footer(self, view=None): |
|
201 self.w(u'</div>\n') # close id=contentmain |
|
202 self.w(u'</div>\n') # closes id=pageContent |
|
203 self.content_footer(view) |
|
204 self.w(u'</td>\n') |
|
205 self.nav_column(view, 'right') |
|
206 self.w(u'</tr></table></div>\n') |
|
207 self.template('footer', rset=self.rset) |
|
208 self.w(u'</body>') |
|
209 |
|
210 def nav_column(self, view, context): |
|
211 boxes = list(self.vreg.possible_vobjects('boxes', self.req, self.rset, |
|
212 view=view, context=context)) |
|
213 if boxes: |
|
214 self.w(u'<td class="navcol"><div class="navboxes">\n') |
|
215 for box in boxes: |
|
216 box.dispatch(w=self.w, view=view) |
|
217 self.w(u'</div></td>\n') |
|
218 |
|
219 def content_header(self, view=None): |
|
220 """by default, display informal messages in content header""" |
|
221 self.template('contentheader', rset=self.rset, view=view) |
|
222 |
|
223 def content_footer(self, view=None): |
|
224 self.template('contentfooter', rset=self.rset, view=view) |
|
225 |
|
226 |
|
227 class ErrorTemplate(TheMainTemplate): |
|
228 """fallback template if an internal error occured during displaying the |
|
229 main template. This template may be called for authentication error, |
|
230 which means that req.cnx and req.user may not be set. |
|
231 """ |
|
232 id = 'error' |
|
233 |
|
234 def call(self): |
|
235 """display an unexpected error""" |
|
236 self.set_request_content_type() |
|
237 self.req.reset_headers() |
|
238 view = self.vreg.select_view('error', self.req, self.rset) |
|
239 self.template_header(self.content_type, view, self.req._('an error occured'), |
|
240 [NOINDEX, NOFOLLOW]) |
|
241 view.dispatch(w=self.w) |
|
242 self.template_footer(view) |
|
243 |
|
244 def template_header(self, content_type, view=None, page_title='', additional_headers=()): |
|
245 w = self.whead |
|
246 lang = self.req.lang |
|
247 self.write_doctype() |
|
248 w(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n' |
|
249 % (content_type, self.req.encoding)) |
|
250 w(u'\n'.join(additional_headers)) |
|
251 self.template('htmlheader', rset=self.rset) |
|
252 w(u'<title>%s</title>\n' % html_escape(page_title)) |
|
253 self.w(u'<body>\n') |
|
254 |
|
255 def template_footer(self, view=None): |
|
256 self.w(u'</body>') |
|
257 |
|
258 |
|
259 class SimpleMainTemplate(TheMainTemplate): |
|
260 |
|
261 id = 'main-no-top' |
|
262 |
|
263 def template_header(self, content_type, view=None, page_title='', additional_headers=()): |
|
264 page_title = page_title or view.page_title() |
|
265 additional_headers = additional_headers or view.html_headers() |
|
266 whead = self.whead |
|
267 lang = self.req.lang |
|
268 self.write_doctype() |
|
269 whead(u'<meta http-equiv="content-type" content="%s; charset=%s"/>\n' |
|
270 % (content_type, self.req.encoding)) |
|
271 whead(u'\n'.join(additional_headers) + u'\n') |
|
272 self.template('htmlheader', rset=self.rset) |
|
273 w = self.w |
|
274 w(u'<title>%s</title>\n' % html_escape(page_title)) |
|
275 w(u'<body>\n') |
|
276 w(u'<div id="page">') |
|
277 w(u'<table width="100%" height="100%" border="0"><tr>\n') |
|
278 w(u'<td class="navcol">\n') |
|
279 self.topleft_header() |
|
280 boxes = list(self.vreg.possible_vobjects('boxes', self.req, self.rset, |
|
281 view=view, context='left')) |
|
282 if boxes: |
|
283 w(u'<div class="navboxes">\n') |
|
284 for box in boxes: |
|
285 box.dispatch(w=w) |
|
286 self.w(u'</div>\n') |
|
287 w(u'</td>') |
|
288 w(u'<td id="contentcol" rowspan="2">') |
|
289 w(u'<div id="pageContent">\n') |
|
290 vtitle = self.req.form.get('vtitle') |
|
291 if vtitle: |
|
292 w(u'<h1 class="vtitle">%s</h1>' % (vtitle)) |
|
293 |
|
294 def topleft_header(self): |
|
295 self.w(u'<table id="header"><tr>\n') |
|
296 self.w(u'<td>') |
|
297 self.vreg.select_component('logo', self.req, self.rset).dispatch(w=self.w) |
|
298 self.w(u'</td>\n') |
|
299 self.w(u'</tr></table>\n') |
|
300 |
|
301 # page parts templates ######################################################## |
|
302 |
|
303 class HTMLHeader(Template): |
|
304 """default html headers""" |
|
305 id = 'htmlheader' |
|
306 |
|
307 def call(self, **kwargs): |
|
308 self.favicon() |
|
309 self.stylesheets() |
|
310 self.javascripts() |
|
311 self.alternates() |
|
312 self.pageid() |
|
313 |
|
314 def favicon(self): |
|
315 favicon = self.req.external_resource('FAVICON', None) |
|
316 if favicon: |
|
317 self.whead(u'<link rel="shortcut icon" href="%s"/>\n' % favicon) |
|
318 |
|
319 def stylesheets(self): |
|
320 req = self.req |
|
321 add_css = req.add_css |
|
322 for css in req.external_resource('STYLESHEETS'): |
|
323 add_css(css, localfile=False) |
|
324 for css in req.external_resource('STYLESHEETS_PRINT'): |
|
325 add_css(css, u'print', localfile=False) |
|
326 for css in req.external_resource('IE_STYLESHEETS'): |
|
327 add_css(css, localfile=False, ieonly=True) |
|
328 |
|
329 def javascripts(self): |
|
330 for jscript in self.req.external_resource('JAVASCRIPTS'): |
|
331 self.req.add_js(jscript, localfile=False) |
|
332 |
|
333 def alternates(self): |
|
334 # nfentity_selector is used by the rss icon box as well |
|
335 if nfentity_selector(self, self.req, self.rset): |
|
336 url = self.build_url(rql=self.limited_rql(), vid='rss') |
|
337 self.whead(u'<link rel="alternate" type="application/rss+xml" title="RSS feed" href="%s"/>\n' |
|
338 % html_escape(url)) |
|
339 |
|
340 def pageid(self): |
|
341 req = self.req |
|
342 pid = make_uid(id(req)) |
|
343 req.pageid = pid |
|
344 req.html_headers.define_var('pageid', pid); |
|
345 |
|
346 |
|
347 class HTMLPageHeader(Template): |
|
348 """default html page header""" |
|
349 id = 'header' |
|
350 |
|
351 def call(self, view, **kwargs): |
|
352 self.main_header(view) |
|
353 self.w(u''' |
|
354 <div id="stateheader">''') |
|
355 self.state_header() |
|
356 self.w(u''' |
|
357 </div> |
|
358 ''') |
|
359 |
|
360 def main_header(self, view): |
|
361 """build the top menu with authentification info and the rql box""" |
|
362 self.w(u'<table id="header"><tr>\n') |
|
363 self.w(u'<td id="firstcolumn">') |
|
364 self.vreg.select_component('logo', self.req, self.rset).dispatch(w=self.w) |
|
365 self.w(u'</td>\n') |
|
366 # appliname and breadcrumbs |
|
367 self.w(u'<td id="headtext">') |
|
368 comp = self.vreg.select_component('appliname', self.req, self.rset) |
|
369 if comp and comp.propval('visible'): |
|
370 comp.dispatch(w=self.w) |
|
371 comp = self.vreg.select_component('breadcrumbs', self.req, self.rset, view=view) |
|
372 if comp and comp.propval('visible'): |
|
373 comp.dispatch(w=self.w, view=view) |
|
374 self.w(u'</td>') |
|
375 # logged user and help |
|
376 self.w(u'<td>\n') |
|
377 comp = self.vreg.select_component('loggeduserlink', self.req, self.rset) |
|
378 comp.dispatch(w=self.w) |
|
379 self.w(u'</td><td>') |
|
380 helpcomp = self.vreg.select_component('help', self.req, self.rset) |
|
381 if helpcomp: # may not be available if Card is not defined in the schema |
|
382 helpcomp.dispatch(w=self.w) |
|
383 self.w(u'</td>') |
|
384 # lastcolumn |
|
385 self.w(u'<td id="lastcolumn">') |
|
386 self.w(u'</td>\n') |
|
387 self.w(u'</tr></table>\n') |
|
388 self.template('logform', rset=self.rset, id='popupLoginBox', klass='hidden', |
|
389 title=False, message=False) |
|
390 |
|
391 def state_header(self): |
|
392 state = self.req.search_state |
|
393 if state[0] == 'normal': |
|
394 return |
|
395 _ = self.req._ |
|
396 value = self.view('oneline', self.req.eid_rset(state[1][1])) |
|
397 msg = ' '.join((_("searching for"), |
|
398 display_name(self.req, state[1][3]), |
|
399 _("to associate with"), value, |
|
400 _("by relation"), '"', |
|
401 display_name(self.req, state[1][2], state[1][0]), |
|
402 '"')) |
|
403 return self.w(u'<div class="stateMessage">%s</div>' % msg) |
|
404 |
|
405 |
|
406 |
|
407 class HTMLPageFooter(Template): |
|
408 """default html page footer: include logo if any, and close the HTML body |
|
409 """ |
|
410 id = 'footer' |
|
411 |
|
412 def call(self, **kwargs): |
|
413 req = self.req |
|
414 self.w(u'<div class="footer">') |
|
415 # XXX Take object from the registry if in there? would be |
|
416 # better anyway |
|
417 from cubicweb.web.views.wdoc import ChangeLogView |
|
418 self.w(u'<a href="%s">%s</a> | ' % (req.build_url('changelog'), |
|
419 req._(ChangeLogView.title).lower())) |
|
420 self.w(u'<a href="%s">%s</a> | ' % (req.build_url('doc/about'), |
|
421 req._('about this site'))) |
|
422 self.w(u'© 2001-2008 <a href="http://www.logilab.fr">Logilab S.A.</a>') |
|
423 self.w(u'</div>') |
|
424 |
|
425 |
|
426 class HTMLContentHeader(Template): |
|
427 """default html page content header: |
|
428 * include message component if selectable for this request |
|
429 * include selectable content navigation components |
|
430 """ |
|
431 id = 'contentheader' |
|
432 |
|
433 def call(self, view, **kwargs): |
|
434 """by default, display informal messages in content header""" |
|
435 components = self.vreg.possible_vobjects('contentnavigation', |
|
436 self.req, self.rset, |
|
437 view=view, context='navtop') |
|
438 if components: |
|
439 self.w(u'<div id="contentheader">') |
|
440 for comp in components: |
|
441 comp.dispatch(w=self.w, view=view) |
|
442 self.w(u'</div><div class="clear"></div>') |
|
443 |
|
444 |
|
445 class HTMLContentFooter(Template): |
|
446 """default html page content footer: include selectable content navigation |
|
447 components |
|
448 """ |
|
449 id = 'contentfooter' |
|
450 |
|
451 def call(self, view, **kwargs): |
|
452 components = self.vreg.possible_vobjects('contentnavigation', |
|
453 self.req, self.rset, |
|
454 view=view, context='navbottom') |
|
455 if components: |
|
456 self.w(u'<div id="contentfooter">') |
|
457 for comp in components: |
|
458 comp.dispatch(w=self.w, view=view) |
|
459 self.w(u'</div>') |
|
460 |
|
461 |
|
462 class LogFormTemplate(Template): |
|
463 id = 'logform' |
|
464 title = 'log in' |
|
465 |
|
466 def call(self, id, klass, title=True, message=True): |
|
467 self.req.add_css('cubicweb.login.css') |
|
468 self.w(u'<div id="%s" class="%s">' % (id, klass)) |
|
469 if title: |
|
470 self.w(u'<div id="loginTitle">%s</div>' |
|
471 % self.req.property_value('ui.site-title')) |
|
472 self.w(u'<div id="loginContent">\n') |
|
473 |
|
474 if message: |
|
475 self.display_message() |
|
476 if self.config['auth-mode'] == 'http': |
|
477 # HTTP authentication |
|
478 pass |
|
479 else: |
|
480 # Cookie authentication |
|
481 self.login_form(id) |
|
482 self.w(u'</div></div>\n') |
|
483 |
|
484 def display_message(self): |
|
485 message = self.req.message |
|
486 if message: |
|
487 self.w(u'<div class="simpleMessage">%s</div>\n' % message) |
|
488 |
|
489 def login_form(self, id): |
|
490 _ = self.req._ |
|
491 self.w(u'<form method="post" action="%s" id="login_form">\n' |
|
492 % html_escape(login_form_url(self.config, self.req))) |
|
493 self.w(u'<table>\n') |
|
494 self.w(u'<tr>\n') |
|
495 self.w(u'<td><label for="__login">%s</label></td>' % _('login')) |
|
496 self.w(u'<td><input name="__login" id="__login" class="data" type="text" /></td>') |
|
497 self.w(u'</tr><tr>\n') |
|
498 self.w(u'<td><label for="__password" >%s</label></td>' % _('password')) |
|
499 self.w(u'<td><input name="__password" id="__password" class="data" type="password" /></td>\n') |
|
500 self.w(u'</tr><tr>\n') |
|
501 self.w(u'<td> </td><td><input type="submit" class="loginButton right" value="%s" />\n</td>' % _('log in')) |
|
502 self.w(u'</tr>\n') |
|
503 self.w(u'</table>\n') |
|
504 self.w(u'</form>\n') |
|
505 # XXX doesn't seem to work, rewrite this |
|
506 self.w(u'''<script type="text/javascript">if(document.getElementById("%s").className != "hidden") |
|
507 {$('login_form').__login.focus()}</script>''' % id) |
|
508 |
|
509 |
|
510 def login_form_url(config, req): |
|
511 if req.https: |
|
512 return req.url() |
|
513 if config.get('https-url'): |
|
514 return req.url().replace(req.base_url(), config['https-url']) |
|
515 return req.url() |
|
516 |