|
1 # copyright 2003-2014 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 """ |
|
19 Public API of the PrimaryView class |
|
20 ```````````````````````````````````` |
|
21 .. autoclass:: cubicweb.web.views.primary.PrimaryView |
|
22 |
|
23 Views that may be used to display an entity's attribute or relation |
|
24 ``````````````````````````````````````````````````````````````````` |
|
25 |
|
26 Yoy may easily the display of an attribute or relation by simply configuring the |
|
27 view using one of `primaryview_display_ctrl` or `reledit_ctrl` to use one of the |
|
28 views describled below. For instance: |
|
29 |
|
30 .. sourcecode:: python |
|
31 |
|
32 primaryview_display_ctrl.tag_attribute(('Foo', 'bar'), {'vid': 'attribute'}) |
|
33 |
|
34 |
|
35 .. autoclass:: AttributeView |
|
36 .. autoclass:: URLAttributeView |
|
37 .. autoclass:: VerbatimAttributeView |
|
38 """ |
|
39 |
|
40 __docformat__ = "restructuredtext en" |
|
41 from cubicweb import _ |
|
42 |
|
43 from warnings import warn |
|
44 |
|
45 from logilab.common.deprecation import deprecated |
|
46 from logilab.mtconverter import xml_escape |
|
47 |
|
48 from cubicweb import Unauthorized, NoSelectableObject |
|
49 from cubicweb.utils import support_args |
|
50 from cubicweb.predicates import match_kwargs, match_context |
|
51 from cubicweb.view import EntityView |
|
52 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, display_name |
|
53 from cubicweb.web import component |
|
54 from cubicweb.web.views import uicfg |
|
55 |
|
56 |
|
57 class PrimaryView(EntityView): |
|
58 """ |
|
59 The basic layout of a primary view is as in the :ref:`primary_view_layout` |
|
60 section. This layout is actually drawn by the `render_entity` method. |
|
61 |
|
62 The methods you may want to modify while customizing a ``PrimaryView`` |
|
63 are: |
|
64 |
|
65 .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_title |
|
66 .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_attributes |
|
67 .. automethod:: cubicweb.web.views.primary.PrimaryView.render_entity_relations |
|
68 .. automethod:: cubicweb.web.views.primary.PrimaryView.render_side_boxes |
|
69 |
|
70 The placement of relations in the relations section or in side boxes |
|
71 can be controlled through the :ref:`primary_view_configuration` mechanism. |
|
72 |
|
73 .. automethod:: cubicweb.web.views.primary.PrimaryView.content_navigation_components |
|
74 |
|
75 Also, please note that by setting the following attributes in your |
|
76 subclass, you can already customize some of the rendering: |
|
77 |
|
78 :attr:`show_attr_label` |
|
79 Renders the attribute label next to the attribute value if set to `True`. |
|
80 Otherwise, does only display the attribute value. |
|
81 |
|
82 :attr:`show_rel_label` |
|
83 Renders the relation label next to the relation value if set to `True`. |
|
84 Otherwise, does only display the relation value. |
|
85 |
|
86 :attr:`main_related_section` |
|
87 Renders the relations of the entity if set to `True`. |
|
88 |
|
89 A good practice is for you to identify the content of your entity type for |
|
90 which the default rendering does not answer your need so that you can focus |
|
91 on the specific method (from the list above) that needs to be modified. We |
|
92 do not advise you to overwrite ``render_entity`` unless you want a |
|
93 completely different layout. |
|
94 """ |
|
95 |
|
96 __regid__ = 'primary' |
|
97 title = _('primary') |
|
98 show_attr_label = True |
|
99 show_rel_label = True |
|
100 rsection = None |
|
101 display_ctrl = None |
|
102 main_related_section = True |
|
103 |
|
104 def html_headers(self): |
|
105 """return a list of html headers (eg something to be inserted between |
|
106 <head> and </head> of the returned page |
|
107 |
|
108 by default primary views are indexed |
|
109 """ |
|
110 return [] |
|
111 |
|
112 def entity_call(self, entity, **kwargs): |
|
113 entity.complete() |
|
114 uicfg_reg = self._cw.vreg['uicfg'] |
|
115 if self.rsection is None: |
|
116 self.rsection = uicfg_reg.select('primaryview_section', |
|
117 self._cw, entity=entity) |
|
118 if self.display_ctrl is None: |
|
119 self.display_ctrl = uicfg_reg.select('primaryview_display_ctrl', |
|
120 self._cw, entity=entity) |
|
121 self.render_entity(entity) |
|
122 |
|
123 def render_entity(self, entity): |
|
124 self.render_entity_toolbox(entity) |
|
125 self.render_entity_title(entity) |
|
126 # entity's attributes and relations, excluding meta data |
|
127 # if the entity isn't meta itself |
|
128 if self.is_primary(): |
|
129 boxes = self._prepare_side_boxes(entity) |
|
130 else: |
|
131 boxes = None |
|
132 if boxes or hasattr(self, 'render_side_related'): |
|
133 self.w(u'<table width="100%"><tr><td style="width: 75%">') |
|
134 |
|
135 self.w(u'<div class="mainInfo">') |
|
136 self.content_navigation_components('navcontenttop') |
|
137 self.render_entity_attributes(entity) |
|
138 if self.main_related_section: |
|
139 self.render_entity_relations(entity) |
|
140 self.content_navigation_components('navcontentbottom') |
|
141 self.w(u'</div>') |
|
142 # side boxes |
|
143 if boxes or hasattr(self, 'render_side_related'): |
|
144 self.w(u'</td><td>') |
|
145 self.w(u'<div class="primaryRight">') |
|
146 self.render_side_boxes(boxes) |
|
147 self.w(u'</div>') |
|
148 self.w(u'</td></tr></table>') |
|
149 |
|
150 def content_navigation_components(self, context): |
|
151 """This method is applicable only for entity type implementing the |
|
152 interface `IPrevNext`. This interface is for entities which can be |
|
153 linked to a previous and/or next entity. This method will render the |
|
154 navigation links between entities of this type, either at the top or at |
|
155 the bottom of the page given the context (navcontent{top|bottom}). |
|
156 """ |
|
157 self.w(u'<div class="%s">' % context) |
|
158 for comp in self._cw.vreg['ctxcomponents'].poss_visible_objects( |
|
159 self._cw, rset=self.cw_rset, view=self, context=context): |
|
160 # XXX bw compat code |
|
161 try: |
|
162 comp.render(w=self.w, row=self.cw_row, view=self) |
|
163 except TypeError: |
|
164 comp.render(w=self.w) |
|
165 self.w(u'</div>') |
|
166 |
|
167 def render_entity_title(self, entity): |
|
168 """Renders the entity title, by default using entity's |
|
169 :meth:`dc_title()` method. |
|
170 """ |
|
171 title = xml_escape(entity.dc_title()) |
|
172 if title: |
|
173 if self.is_primary(): |
|
174 self.w(u'<h1>%s</h1>' % title) |
|
175 else: |
|
176 atitle = self._cw._('follow this link for more information on this %s') % entity.dc_type() |
|
177 self.w(u'<h4><a href="%s" title="%s">%s</a></h4>' |
|
178 % (entity.absolute_url(), atitle, title)) |
|
179 |
|
180 def render_entity_toolbox(self, entity): |
|
181 self.content_navigation_components('ctxtoolbar') |
|
182 |
|
183 def render_entity_attributes(self, entity): |
|
184 """Renders all attributes and relations in the 'attributes' section. |
|
185 """ |
|
186 display_attributes = [] |
|
187 for rschema, _, role, dispctrl in self._section_def(entity, 'attributes'): |
|
188 vid = dispctrl.get('vid', 'reledit') |
|
189 if rschema.final or vid == 'reledit' or dispctrl.get('rtypevid'): |
|
190 value = entity.view(vid, rtype=rschema.type, role=role, |
|
191 initargs={'dispctrl': dispctrl}) |
|
192 else: |
|
193 rset = self._relation_rset(entity, rschema, role, dispctrl) |
|
194 if rset: |
|
195 value = self._cw.view(vid, rset) |
|
196 else: |
|
197 value = None |
|
198 if value is not None and value != '': |
|
199 display_attributes.append( (rschema, role, dispctrl, value) ) |
|
200 if display_attributes: |
|
201 self.w(u'<table>') |
|
202 for rschema, role, dispctrl, value in display_attributes: |
|
203 label = self._rel_label(entity, rschema, role, dispctrl) |
|
204 self.render_attribute(label, value, table=True) |
|
205 self.w(u'</table>') |
|
206 |
|
207 def render_attribute(self, label, value, table=False): |
|
208 self.field(label, value, tr=False, table=table) |
|
209 |
|
210 def render_entity_relations(self, entity): |
|
211 """Renders all relations in the 'relations' section.""" |
|
212 defaultlimit = self._cw.property_value('navigation.related-limit') |
|
213 for rschema, tschemas, role, dispctrl in self._section_def(entity, 'relations'): |
|
214 if rschema.final or dispctrl.get('rtypevid'): |
|
215 vid = dispctrl.get('vid', 'reledit') |
|
216 try: |
|
217 rview = self._cw.vreg['views'].select( |
|
218 vid, self._cw, rset=entity.cw_rset, row=entity.cw_row, |
|
219 col=entity.cw_col, dispctrl=dispctrl, |
|
220 rtype=rschema, role=role) |
|
221 except NoSelectableObject: |
|
222 continue |
|
223 value = rview.render(row=entity.cw_row, col=entity.cw_col, |
|
224 rtype=rschema.type, role=role) |
|
225 else: |
|
226 vid = dispctrl.get('vid', 'autolimited') |
|
227 limit = dispctrl.get('limit', defaultlimit) if vid == 'autolimited' else None |
|
228 if limit is not None: |
|
229 limit += 1 # need one more so the view can check if there is more than the limit |
|
230 rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit) |
|
231 if not rset: |
|
232 continue |
|
233 try: |
|
234 rview = self._cw.vreg['views'].select( |
|
235 vid, self._cw, rset=rset, dispctrl=dispctrl) |
|
236 except NoSelectableObject: |
|
237 continue |
|
238 value = rview.render() |
|
239 label = self._rel_label(entity, rschema, role, dispctrl) |
|
240 self.render_relation(label, value) |
|
241 |
|
242 def render_relation(self, label, value): |
|
243 self.w(u'<div class="section">') |
|
244 if label: |
|
245 self.w(u'<h4>%s</h4>' % label) |
|
246 self.w(value) |
|
247 self.w(u'</div>') |
|
248 |
|
249 def render_side_boxes(self, boxes): |
|
250 """Renders side boxes on the right side of the content. This will |
|
251 generate a box for each relation in the 'sidebox' section, as well as |
|
252 explicit box appobjects selectable in this context. |
|
253 """ |
|
254 for box in boxes: |
|
255 try: |
|
256 box.render(w=self.w, row=self.cw_row) |
|
257 except TypeError: |
|
258 box.render(w=self.w) |
|
259 |
|
260 def _prepare_side_boxes(self, entity): |
|
261 sideboxes = [] |
|
262 boxesreg = self._cw.vreg['ctxcomponents'] |
|
263 defaultlimit = self._cw.property_value('navigation.related-limit') |
|
264 for rschema, tschemas, role, dispctrl in self._section_def(entity, 'sideboxes'): |
|
265 vid = dispctrl.get('vid', 'autolimited') |
|
266 limit = defaultlimit if vid == 'autolimited' else None |
|
267 rset = self._relation_rset(entity, rschema, role, dispctrl, limit=limit) |
|
268 if not rset: |
|
269 continue |
|
270 label = self._rel_label(entity, rschema, role, dispctrl) |
|
271 box = boxesreg.select('rsetbox', self._cw, rset=rset, |
|
272 vid=vid, title=label, dispctrl=dispctrl, |
|
273 context='incontext') |
|
274 sideboxes.append(box) |
|
275 sideboxes += boxesreg.poss_visible_objects( |
|
276 self._cw, rset=self.cw_rset, view=self, |
|
277 context='incontext') |
|
278 # XXX since we've two sorted list, it may be worth using bisect |
|
279 def get_order(x): |
|
280 if 'order' in x.cw_property_defs: |
|
281 return x.cw_propval('order') |
|
282 # default to 9999 so view boxes occurs after component boxes |
|
283 return x.cw_extra_kwargs.get('dispctrl', {}).get('order', 9999) |
|
284 return sorted(sideboxes, key=get_order) |
|
285 |
|
286 def _section_def(self, entity, where): |
|
287 rdefs = [] |
|
288 eschema = entity.e_schema |
|
289 for rschema, tschemas, role in eschema.relation_definitions(True): |
|
290 if rschema in VIRTUAL_RTYPES: |
|
291 continue |
|
292 matchtschemas = [] |
|
293 for tschema in tschemas: |
|
294 section = self.rsection.etype_get(eschema, rschema, role, |
|
295 tschema) |
|
296 if section == where: |
|
297 matchtschemas.append(tschema) |
|
298 if matchtschemas: |
|
299 dispctrl = self.display_ctrl.etype_get(eschema, rschema, role, '*') |
|
300 rdefs.append( (rschema, matchtschemas, role, dispctrl) ) |
|
301 return sorted(rdefs, key=lambda x: x[-1]['order']) |
|
302 |
|
303 def _relation_rset(self, entity, rschema, role, dispctrl, limit=None): |
|
304 try: |
|
305 rset = entity.related(rschema.type, role, limit=limit) |
|
306 except Unauthorized: |
|
307 return |
|
308 if 'filter' in dispctrl: |
|
309 rset = dispctrl['filter'](rset) |
|
310 return rset |
|
311 |
|
312 def _rel_label(self, entity, rschema, role, dispctrl): |
|
313 if rschema.final: |
|
314 showlabel = dispctrl.get('showlabel', self.show_attr_label) |
|
315 else: |
|
316 showlabel = dispctrl.get('showlabel', self.show_rel_label) |
|
317 if showlabel: |
|
318 if dispctrl.get('label'): |
|
319 label = self._cw._(dispctrl['label']) |
|
320 else: |
|
321 label = display_name(self._cw, rschema.type, role, |
|
322 context=entity.cw_etype) |
|
323 return label |
|
324 return u'' |
|
325 |
|
326 |
|
327 class RelatedView(EntityView): |
|
328 """Display a rset, usually containing entities linked to another entity |
|
329 being displayed. |
|
330 |
|
331 It will try to display nicely according to the number of items in the result |
|
332 set. |
|
333 |
|
334 XXX include me in the doc |
|
335 """ |
|
336 __regid__ = 'autolimited' |
|
337 |
|
338 def call(self, **kwargs): |
|
339 if 'dispctrl' in self.cw_extra_kwargs: |
|
340 if 'limit' in self.cw_extra_kwargs['dispctrl']: |
|
341 limit = self.cw_extra_kwargs['dispctrl']['limit'] |
|
342 else: |
|
343 limit = self._cw.property_value('navigation.related-limit') |
|
344 list_limit = self.cw_extra_kwargs['dispctrl'].get('use_list_limit', 5) |
|
345 subvid = self.cw_extra_kwargs['dispctrl'].get('subvid', 'incontext') |
|
346 else: |
|
347 limit = list_limit = None |
|
348 subvid = 'incontext' |
|
349 if limit is None or self.cw_rset.rowcount <= limit: |
|
350 if self.cw_rset.rowcount == 1: |
|
351 self.wview(subvid, self.cw_rset, row=0) |
|
352 elif list_limit is None or 1 < self.cw_rset.rowcount <= list_limit: |
|
353 self.wview('csv', self.cw_rset, subvid=subvid) |
|
354 else: |
|
355 self.w(u'<div>') |
|
356 self.wview('simplelist', self.cw_rset, subvid=subvid) |
|
357 self.w(u'</div>') |
|
358 # else show links to display related entities |
|
359 else: |
|
360 rql = self.cw_rset.printable_rql() |
|
361 rset = self.cw_rset.limit(limit) # remove extra entity |
|
362 if list_limit is None: |
|
363 self.wview('csv', rset, subvid=subvid) |
|
364 self.w(u'[<a href="%s">%s</a>]' % ( |
|
365 xml_escape(self._cw.build_url(rql=rql, vid=subvid)), |
|
366 self._cw._('see them all'))) |
|
367 else: |
|
368 self.w(u'<div>') |
|
369 self.wview('simplelist', rset, subvid=subvid) |
|
370 self.w(u'[<a href="%s">%s</a>]' % ( |
|
371 xml_escape(self._cw.build_url(rql=rql, vid=subvid)), |
|
372 self._cw._('see them all'))) |
|
373 self.w(u'</div>') |
|
374 |
|
375 |
|
376 class AttributeView(EntityView): |
|
377 """:__regid__: *attribute* |
|
378 |
|
379 This view is generally used to disable the *reledit* feature. It works on |
|
380 both relations and attributes. |
|
381 """ |
|
382 __regid__ = 'attribute' |
|
383 __select__ = EntityView.__select__ & match_kwargs('rtype') |
|
384 |
|
385 def entity_call(self, entity, rtype, role='subject', **kwargs): |
|
386 if self._cw.vreg.schema.rschema(rtype).final: |
|
387 self.w(entity.printable_value(rtype)) |
|
388 else: |
|
389 dispctrl = uicfg.primaryview_display_ctrl.etype_get( |
|
390 entity.e_schema, rtype, role, '*') |
|
391 rset = entity.related(rtype, role) |
|
392 if rset: |
|
393 self.wview('autolimited', rset, initargs={'dispctrl': dispctrl}) |
|
394 |
|
395 |
|
396 class URLAttributeView(EntityView): |
|
397 """:__regid__: *urlattr* |
|
398 |
|
399 This view will wrap an attribute value (hence expect a string) into an '<a>' |
|
400 HTML tag to display a clickable link. |
|
401 """ |
|
402 __regid__ = 'urlattr' |
|
403 __select__ = EntityView.__select__ & match_kwargs('rtype') |
|
404 |
|
405 def entity_call(self, entity, rtype, **kwargs): |
|
406 url = entity.printable_value(rtype) |
|
407 if url: |
|
408 self.w(u'<a href="%s">%s</a>' % (url, url)) |
|
409 |
|
410 |
|
411 class VerbatimAttributeView(EntityView): |
|
412 """:__regid__: *verbatimattr* |
|
413 |
|
414 This view will wrap an attribute value into an '<pre>' HTML tag to display |
|
415 arbitrary text where EOL will be respected. It usually make sense for |
|
416 attributes whose value is a multi-lines string where new lines matters. |
|
417 """ |
|
418 __regid__ = 'verbatimattr' |
|
419 __select__ = EntityView.__select__ & match_kwargs('rtype') |
|
420 |
|
421 def entity_call(self, entity, rtype, **kwargs): |
|
422 value = entity.printable_value(rtype) |
|
423 if value: |
|
424 self.w(u'<pre>%s</pre>' % value) |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 class ToolbarLayout(component.Layout): |
|
431 # XXX include me in the doc |
|
432 __select__ = match_context('ctxtoolbar') |
|
433 |
|
434 def render(self, w): |
|
435 if self.init_rendering(): |
|
436 self.cw_extra_kwargs['view'].render_body(w) |
|
437 |
|
438 |
|
439 ## default primary ui configuration ########################################### |
|
440 |
|
441 _pvs = uicfg.primaryview_section |
|
442 for rtype in META_RTYPES: |
|
443 _pvs.tag_subject_of(('*', rtype, '*'), 'hidden') |
|
444 _pvs.tag_object_of(('*', rtype, '*'), 'hidden') |