1 """abstract views and templates classes for CubicWeb web client |
1 """abstract views and templates classes for CubicWeb web client |
2 |
2 |
3 |
3 |
4 :organization: Logilab |
4 :organization: Logilab |
5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
5 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
7 """ |
7 """ |
8 __docformat__ = "restructuredtext en" |
8 __docformat__ = "restructuredtext en" |
9 |
9 |
10 from cStringIO import StringIO |
10 from cStringIO import StringIO |
26 |
26 |
27 CW_XHTML_EXTENSIONS = '''[ |
27 CW_XHTML_EXTENSIONS = '''[ |
28 <!ATTLIST html xmlns:cubicweb CDATA #FIXED \'http://www.logilab.org/2008/cubicweb\' > |
28 <!ATTLIST html xmlns:cubicweb CDATA #FIXED \'http://www.logilab.org/2008/cubicweb\' > |
29 |
29 |
30 <!ENTITY % coreattrs |
30 <!ENTITY % coreattrs |
31 "id ID #IMPLIED |
31 "id ID #IMPLIED |
32 class CDATA #IMPLIED |
32 class CDATA #IMPLIED |
33 style CDATA #IMPLIED |
33 style CDATA #IMPLIED |
34 title CDATA #IMPLIED |
34 title CDATA #IMPLIED |
35 |
35 |
36 cubicweb:sortvalue CDATA #IMPLIED |
36 cubicweb:sortvalue CDATA #IMPLIED |
37 cubicweb:target CDATA #IMPLIED |
37 cubicweb:target CDATA #IMPLIED |
38 cubicweb:limit CDATA #IMPLIED |
38 cubicweb:limit CDATA #IMPLIED |
51 cubicweb:displayactions CDATA #IMPLIED |
51 cubicweb:displayactions CDATA #IMPLIED |
52 cubicweb:fallbackvid CDATA #IMPLIED |
52 cubicweb:fallbackvid CDATA #IMPLIED |
53 cubicweb:vid CDATA #IMPLIED |
53 cubicweb:vid CDATA #IMPLIED |
54 cubicweb:rql CDATA #IMPLIED |
54 cubicweb:rql CDATA #IMPLIED |
55 cubicweb:actualrql CDATA #IMPLIED |
55 cubicweb:actualrql CDATA #IMPLIED |
56 cubicweb:rooteid CDATA #IMPLIED |
56 cubicweb:rooteid CDATA #IMPLIED |
57 cubicweb:dataurl CDATA #IMPLIED |
57 cubicweb:dataurl CDATA #IMPLIED |
58 cubicweb:size CDATA #IMPLIED |
58 cubicweb:size CDATA #IMPLIED |
59 cubicweb:tlunit CDATA #IMPLIED |
59 cubicweb:tlunit CDATA #IMPLIED |
60 cubicweb:loadurl CDATA #IMPLIED |
60 cubicweb:loadurl CDATA #IMPLIED |
61 cubicweb:uselabel CDATA #IMPLIED |
61 cubicweb:uselabel CDATA #IMPLIED |
|
62 cubicweb:facetargs CDATA #IMPLIED |
|
63 cubicweb:facetName CDATA #IMPLIED |
62 "> ] ''' |
64 "> ] ''' |
63 |
65 |
64 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 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' |
65 |
67 |
66 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 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' |
69 """abstract view class, used as base for every renderable object such |
71 """abstract view class, used as base for every renderable object such |
70 as views, templates, some components...web |
72 as views, templates, some components...web |
71 |
73 |
72 A view is instantiated to render a [part of a] result set. View |
74 A view is instantiated to render a [part of a] result set. View |
73 subclasses may be parametred using the following class attributes: |
75 subclasses may be parametred using the following class attributes: |
74 |
76 |
75 * `templatable` indicates if the view may be embeded in a main |
77 * `templatable` indicates if the view may be embeded in a main |
76 template or if it has to be rendered standalone (i.e. XML for |
78 template or if it has to be rendered standalone (i.e. XML for |
77 instance) |
79 instance) |
78 * if the view is not templatable, it should set the `content_type` class |
80 * if the view is not templatable, it should set the `content_type` class |
79 attribute to the correct MIME type (text/xhtml by default) |
81 attribute to the correct MIME type (text/xhtml by default) |
83 At instantiation time, the standard `req`, `rset`, and `cursor` |
85 At instantiation time, the standard `req`, `rset`, and `cursor` |
84 attributes are added and the `w` attribute will be set at rendering |
86 attributes are added and the `w` attribute will be set at rendering |
85 time to a write function to use. |
87 time to a write function to use. |
86 """ |
88 """ |
87 __registry__ = 'views' |
89 __registry__ = 'views' |
88 |
90 |
89 templatable = True |
91 templatable = True |
90 need_navigation = True |
92 need_navigation = True |
91 # content_type = 'application/xhtml+xml' # text/xhtml' |
93 # content_type = 'application/xhtml+xml' # text/xhtml' |
92 binary = False |
94 binary = False |
93 add_to_breadcrumbs = True |
95 add_to_breadcrumbs = True |
94 category = 'view' |
96 category = 'view' |
95 |
97 |
96 def __init__(self, req, rset): |
98 def __init__(self, req, rset): |
97 super(View, self).__init__(req, rset) |
99 super(View, self).__init__(req, rset) |
98 self.w = None |
100 self.w = None |
99 |
101 |
100 @property |
102 @property |
101 def content_type(self): |
103 def content_type(self): |
102 if self.req.xhtml_browser(): |
104 if self.req.xhtml_browser(): |
103 return 'application/xhtml+xml' |
105 return 'application/xhtml+xml' |
104 return 'text/html' |
106 return 'text/html' |
105 |
107 |
106 def set_stream(self, w=None): |
108 def set_stream(self, w=None): |
107 if self.w is not None: |
109 if self.w is not None: |
108 return |
110 return |
109 if w is None: |
111 if w is None: |
110 if self.binary: |
112 if self.binary: |
116 stream = None |
118 stream = None |
117 self.w = w |
119 self.w = w |
118 return stream |
120 return stream |
119 |
121 |
120 # main view interface ##################################################### |
122 # main view interface ##################################################### |
121 |
123 |
122 def dispatch(self, w=None, **context): |
124 def dispatch(self, w=None, **context): |
123 """called to render a view object for a result set. |
125 """called to render a view object for a result set. |
124 |
126 |
125 This method is a dispatched to an actual method selected |
127 This method is a dispatched to an actual method selected |
126 according to optional row and col parameters, which are locating |
128 according to optional row and col parameters, which are locating |
127 a particular row or cell in the result set: |
129 a particular row or cell in the result set: |
128 |
130 |
129 * if row [and col] are specified, `cell_call` is called |
131 * if row [and col] are specified, `cell_call` is called |
130 * if none of them is supplied, the view is considered to apply on |
132 * if none of them is supplied, the view is considered to apply on |
131 the whole result set (which may be None in this case), `call` is |
133 the whole result set (which may be None in this case), `call` is |
132 called |
134 called |
133 """ |
135 """ |
145 return self._stream.getvalue() |
147 return self._stream.getvalue() |
146 |
148 |
147 # should default .call() method add a <div classs="section"> around each |
149 # should default .call() method add a <div classs="section"> around each |
148 # rset item |
150 # rset item |
149 add_div_section = True |
151 add_div_section = True |
150 |
152 |
151 def call(self, **kwargs): |
153 def call(self, **kwargs): |
152 """the view is called for an entire result set, by default loop |
154 """the view is called for an entire result set, by default loop |
153 other rows of the result set and call the same view on the |
155 other rows of the result set and call the same view on the |
154 particular row |
156 particular row |
155 |
157 |
167 self.w(u"</div>") |
169 self.w(u"</div>") |
168 |
170 |
169 def cell_call(self, row, col, **kwargs): |
171 def cell_call(self, row, col, **kwargs): |
170 """the view is called for a particular result set cell""" |
172 """the view is called for a particular result set cell""" |
171 raise NotImplementedError, self |
173 raise NotImplementedError, self |
172 |
174 |
173 def linkable(self): |
175 def linkable(self): |
174 """return True if the view may be linked in a menu |
176 """return True if the view may be linked in a menu |
175 |
177 |
176 by default views without title are not meant to be displayed |
178 by default views without title are not meant to be displayed |
177 """ |
179 """ |
178 if not getattr(self, 'title', None): |
180 if not getattr(self, 'title', None): |
179 return False |
181 return False |
180 return True |
182 return True |
181 |
183 |
182 def is_primary(self): |
184 def is_primary(self): |
183 return self.id == 'primary' |
185 return self.id == 'primary' |
184 |
186 |
185 def url(self): |
187 def url(self): |
186 """return the url associated with this view. Should not be |
188 """return the url associated with this view. Should not be |
187 necessary for non linkable views, but a default implementation |
189 necessary for non linkable views, but a default implementation |
188 is provided anyway. |
190 is provided anyway. |
189 """ |
191 """ |
195 def set_request_content_type(self): |
197 def set_request_content_type(self): |
196 """set the content type returned by this view""" |
198 """set the content type returned by this view""" |
197 self.req.set_content_type(self.content_type) |
199 self.req.set_content_type(self.content_type) |
198 |
200 |
199 # view utilities ########################################################## |
201 # view utilities ########################################################## |
200 |
202 |
201 def view(self, __vid, rset, __fallback_vid=None, **kwargs): |
203 def view(self, __vid, rset, __fallback_vid=None, **kwargs): |
202 """shortcut to self.vreg.render method avoiding to pass self.req""" |
204 """shortcut to self.vreg.render method avoiding to pass self.req""" |
203 try: |
205 try: |
204 view = self.vreg.select_view(__vid, self.req, rset, **kwargs) |
206 view = self.vreg.select_view(__vid, self.req, rset, **kwargs) |
205 except NoSelectableObject: |
207 except NoSelectableObject: |
206 if __fallback_vid is None: |
208 if __fallback_vid is None: |
207 raise |
209 raise |
208 view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) |
210 view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) |
209 return view.dispatch(**kwargs) |
211 return view.dispatch(**kwargs) |
210 |
212 |
211 def wview(self, __vid, rset, __fallback_vid=None, **kwargs): |
213 def wview(self, __vid, rset, __fallback_vid=None, **kwargs): |
212 """shortcut to self.view method automatically passing self.w as argument |
214 """shortcut to self.view method automatically passing self.w as argument |
213 """ |
215 """ |
214 self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) |
216 self.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) |
215 |
217 |
232 row=row) |
234 row=row) |
233 if action: |
235 if action: |
234 label = label or self.req._(action.title) |
236 label = label or self.req._(action.title) |
235 return u'<a href="%s">%s</a>' % (html_escape(action.url()), label) |
237 return u'<a href="%s">%s</a>' % (html_escape(action.url()), label) |
236 return u'' |
238 return u'' |
237 |
239 |
238 def html_headers(self): |
240 def html_headers(self): |
239 """return a list of html headers (eg something to be inserted between |
241 """return a list of html headers (eg something to be inserted between |
240 <head> and </head> of the returned page |
242 <head> and </head> of the returned page |
241 |
243 |
242 by default return a meta tag to disable robot indexation of the page |
244 by default return a meta tag to disable robot indexation of the page |
243 """ |
245 """ |
244 return [NOINDEX] |
246 return [NOINDEX] |
245 |
247 |
246 def page_title(self): |
248 def page_title(self): |
247 """returns a title according to the result set - used for the |
249 """returns a title according to the result set - used for the |
248 title in the HTML header |
250 title in the HTML header |
249 """ |
251 """ |
250 vtitle = self.req.form.get('vtitle') |
252 vtitle = self.req.form.get('vtitle') |
290 |
292 |
291 def create_url(self, etype, **kwargs): |
293 def create_url(self, etype, **kwargs): |
292 """ return the url of the entity creation form for a given entity type""" |
294 """ return the url of the entity creation form for a given entity type""" |
293 return self.req.build_url('add/%s'%etype, **kwargs) |
295 return self.req.build_url('add/%s'%etype, **kwargs) |
294 |
296 |
295 |
297 |
296 # concrete views base classes ################################################# |
298 # concrete views base classes ################################################# |
297 |
299 |
298 class EntityView(View): |
300 class EntityView(View): |
299 """base class for views applying on an entity (i.e. uniform result set) |
301 """base class for views applying on an entity (i.e. uniform result set) |
300 """ |
302 """ |
301 __registerer__ = accepts_registerer |
303 __registerer__ = accepts_registerer |
302 __selectors__ = (accept,) |
304 __selectors__ = (accept,) |
303 category = 'entityview' |
305 category = 'entityview' |
304 |
306 |
305 def field(self, label, value, row=True, show_label=True, w=None, tr=True): |
307 def field(self, label, value, row=True, show_label=True, w=None, tr=True): |
306 """ read-only field """ |
308 """ read-only field """ |
307 if w is None: |
309 if w is None: |
308 w = self.w |
310 w = self.w |
309 if row: |
311 if row: |
314 w(u'<span class="label">%s</span>' % label) |
316 w(u'<span class="label">%s</span>' % label) |
315 w(u'<div class="field">%s</div>' % value) |
317 w(u'<div class="field">%s</div>' % value) |
316 if row: |
318 if row: |
317 w(u'</div>') |
319 w(u'</div>') |
318 |
320 |
319 |
321 |
320 class StartupView(View): |
322 class StartupView(View): |
321 """base class for views which doesn't need a particular result set |
323 """base class for views which doesn't need a particular result set |
322 to be displayed (so they can always be displayed !) |
324 to be displayed (so they can always be displayed !) |
323 """ |
325 """ |
324 __registerer__ = priority_registerer |
326 __registerer__ = priority_registerer |
325 __selectors__ = (match_user_group, none_rset) |
327 __selectors__ = (match_user_group, none_rset) |
326 require_groups = () |
328 require_groups = () |
327 category = 'startupview' |
329 category = 'startupview' |
328 |
330 |
329 def url(self): |
331 def url(self): |
330 """return the url associated with this view. We can omit rql here""" |
332 """return the url associated with this view. We can omit rql here""" |
331 return self.build_url('view', vid=self.id) |
333 return self.build_url('view', vid=self.id) |
332 |
334 |
333 def html_headers(self): |
335 def html_headers(self): |
343 """base class for entity views which may also be applied to None |
345 """base class for entity views which may also be applied to None |
344 result set (usually a default rql is provided by the view class) |
346 result set (usually a default rql is provided by the view class) |
345 """ |
347 """ |
346 __registerer__ = accepts_registerer |
348 __registerer__ = accepts_registerer |
347 __selectors__ = (chainfirst(none_rset, accept),) |
349 __selectors__ = (chainfirst(none_rset, accept),) |
348 |
350 |
349 default_rql = None |
351 default_rql = None |
350 |
352 |
351 def __init__(self, req, rset): |
353 def __init__(self, req, rset): |
352 super(EntityStartupView, self).__init__(req, rset) |
354 super(EntityStartupView, self).__init__(req, rset) |
353 if rset is None: |
355 if rset is None: |
354 # this instance is not in the "entityview" category |
356 # this instance is not in the "entityview" category |
355 self.category = 'startupview' |
357 self.category = 'startupview' |
356 |
358 |
357 def startup_rql(self): |
359 def startup_rql(self): |
358 """return some rql to be executedif the result set is None""" |
360 """return some rql to be executedif the result set is None""" |
359 return self.default_rql |
361 return self.default_rql |
360 |
362 |
361 def call(self, **kwargs): |
363 def call(self, **kwargs): |
362 """override call to execute rql returned by the .startup_rql |
364 """override call to execute rql returned by the .startup_rql |
363 method if necessary |
365 method if necessary |
364 """ |
366 """ |
365 if self.rset is None: |
367 if self.rset is None: |
374 """ |
376 """ |
375 if not self.__select__(self.req, self.rset): |
377 if not self.__select__(self.req, self.rset): |
376 return self.build_url(vid=self.id) |
378 return self.build_url(vid=self.id) |
377 return super(EntityStartupView, self).url() |
379 return super(EntityStartupView, self).url() |
378 |
380 |
379 |
381 |
380 class AnyRsetView(View): |
382 class AnyRsetView(View): |
381 """base class for views applying on any non empty result sets""" |
383 """base class for views applying on any non empty result sets""" |
382 __registerer__ = priority_registerer |
384 __registerer__ = priority_registerer |
383 __selectors__ = (nonempty_rset,) |
385 __selectors__ = (nonempty_rset,) |
384 |
386 |
385 category = 'anyrsetview' |
387 category = 'anyrsetview' |
386 |
388 |
387 def columns_labels(self, tr=True): |
389 def columns_labels(self, tr=True): |
388 if tr: |
390 if tr: |
389 translate = display_name |
391 translate = display_name |
390 else: |
392 else: |
391 translate = lambda req, val: val |
393 translate = lambda req, val: val |
398 for et in self.rset.column_types(colindex)) |
400 for et in self.rset.column_types(colindex)) |
399 else: |
401 else: |
400 label = translate(self.req, attr) |
402 label = translate(self.req, attr) |
401 labels.append(label) |
403 labels.append(label) |
402 return labels |
404 return labels |
403 |
405 |
404 |
406 |
405 class EmptyRsetView(View): |
407 class EmptyRsetView(View): |
406 """base class for views applying on any empty result sets""" |
408 """base class for views applying on any empty result sets""" |
407 __registerer__ = priority_registerer |
409 __registerer__ = priority_registerer |
408 __selectors__ = (empty_rset,) |
410 __selectors__ = (empty_rset,) |
417 __registry__ = 'templates' |
419 __registry__ = 'templates' |
418 __registerer__ = priority_registerer |
420 __registerer__ = priority_registerer |
419 __selectors__ = (match_user_group,) |
421 __selectors__ = (match_user_group,) |
420 |
422 |
421 require_groups = () |
423 require_groups = () |
422 |
424 |
423 def template(self, oid, **kwargs): |
425 def template(self, oid, **kwargs): |
424 """shortcut to self.registry.render method on the templates registry""" |
426 """shortcut to self.registry.render method on the templates registry""" |
425 w = kwargs.pop('w', self.w) |
427 w = kwargs.pop('w', self.w) |
426 self.vreg.render('templates', oid, self.req, w=w, **kwargs) |
428 self.vreg.render('templates', oid, self.req, w=w, **kwargs) |
427 |
429 |