|
1 """Set of HTML generic base views: |
|
2 |
|
3 * noresult, final |
|
4 * primary, sidebox |
|
5 * secondary, oneline, incontext, outofcontext, text |
|
6 * list |
|
7 * xml, rss |
|
8 |
|
9 |
|
10 :organization: Logilab |
|
11 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
12 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
13 """ |
|
14 __docformat__ = "restructuredtext en" |
|
15 |
|
16 from time import timezone |
|
17 |
|
18 from rql import nodes |
|
19 |
|
20 from logilab.common.decorators import cached |
|
21 from logilab.mtconverter import html_escape, TransformError |
|
22 |
|
23 from cubicweb import Unauthorized, NoSelectableObject, typed_eid |
|
24 from cubicweb.common.selectors import (yes_selector, anyrset_selector, accept_selector, |
|
25 onelinerset_selector, searchstate_selector, |
|
26 req_form_params_selector, accept_rset_selector) |
|
27 from cubicweb.common.uilib import (cut, printable_value, UnicodeCSVWriter, |
|
28 ajax_replace_url, rql_for_eid) |
|
29 from cubicweb.common.view import EntityView, AnyRsetView, EmptyRsetView |
|
30 from cubicweb.web.httpcache import MaxAgeHTTPCacheManager |
|
31 from cubicweb.web.views import vid_from_rset, linksearch_select_url, linksearch_match |
|
32 |
|
33 _ = unicode |
|
34 |
|
35 |
|
36 class NullView(AnyRsetView): |
|
37 """default view when no result has been found""" |
|
38 id = 'null' |
|
39 __select__ = classmethod(yes_selector) |
|
40 def call(self, **kwargs): |
|
41 pass |
|
42 cell_call = call |
|
43 |
|
44 |
|
45 class NoResultView(EmptyRsetView): |
|
46 """default view when no result has been found""" |
|
47 id = 'noresult' |
|
48 |
|
49 def call(self, **kwargs): |
|
50 self.w(u'<div class="searchMessage"><strong>%s</strong></div>\n' |
|
51 % self.req._('No result matching query')) |
|
52 |
|
53 |
|
54 class FinalView(AnyRsetView): |
|
55 """display values without any transformation (i.e. get a number for |
|
56 entities) |
|
57 """ |
|
58 id = 'final' |
|
59 |
|
60 def cell_call(self, row, col, props=None, displaytime=False): |
|
61 etype = self.rset.description[row][col] |
|
62 value = self.rset.rows[row][col] |
|
63 if etype == 'String': |
|
64 entity, rtype = self.rset.related_entity(row, col) |
|
65 if entity is not None: |
|
66 # yes ! |
|
67 self.w(entity.printable_value(rtype, value)) |
|
68 return |
|
69 if etype in ('Time', 'Interval'): |
|
70 _ = self.req._ |
|
71 # value is DateTimeDelta but we have no idea about what is the |
|
72 # reference date here, so we can only approximate years and months |
|
73 if value.days > 730: # 2 years |
|
74 self.w(_('%d years') % (value.days // 365)) |
|
75 elif value.days > 60: # 2 months |
|
76 self.w(_('%d months') % (value.days // 30)) |
|
77 elif value.days > 14: # 2 weeks |
|
78 self.w(_('%d weeks') % (value.days // 7)) |
|
79 elif value.days > 2: |
|
80 self.w(_('%s days') % int(value.days)) |
|
81 elif value.hours > 2: |
|
82 self.w(_('%s hours') % int(value.hours)) |
|
83 elif value.minutes >= 2: |
|
84 self.w(_('%s minutes') % int(value.minutes)) |
|
85 else: |
|
86 self.w(_('%s seconds') % int(value.seconds)) |
|
87 return |
|
88 self.wdata(printable_value(self.req, etype, value, props, displaytime=displaytime)) |
|
89 |
|
90 |
|
91 class EditableFinalView(FinalView): |
|
92 """same as FinalView but enables inplace-edition when possible""" |
|
93 id = 'editable-final' |
|
94 |
|
95 def cell_call(self, row, col, props=None, displaytime=False): |
|
96 etype = self.rset.description[row][col] |
|
97 value = self.rset.rows[row][col] |
|
98 entity, rtype = self.rset.related_entity(row, col) |
|
99 if entity is not None: |
|
100 self.w(entity.view('reledit', rtype=rtype)) |
|
101 else: |
|
102 super(EditableFinalView, self).cell_call(row, col, props, displaytime) |
|
103 |
|
104 PRIMARY_SKIP_RELS = set(['is', 'is_instance_of', 'identity', |
|
105 'owned_by', 'created_by', |
|
106 'in_state', 'wf_info_for', 'require_permission', |
|
107 'from_entity', 'to_entity', |
|
108 'see_also']) |
|
109 |
|
110 class PrimaryView(EntityView): |
|
111 """the full view of an non final entity""" |
|
112 id = 'primary' |
|
113 title = _('primary') |
|
114 show_attr_label = True |
|
115 show_rel_label = True |
|
116 skip_none = True |
|
117 skip_attrs = ('eid', 'creation_date', 'modification_date') |
|
118 skip_rels = () |
|
119 main_related_section = True |
|
120 |
|
121 def html_headers(self): |
|
122 """return a list of html headers (eg something to be inserted between |
|
123 <head> and </head> of the returned page |
|
124 |
|
125 by default primary views are indexed |
|
126 """ |
|
127 return [] |
|
128 |
|
129 def cell_call(self, row, col): |
|
130 self.row = row |
|
131 self.render_entity(self.complete_entity(row, col)) |
|
132 |
|
133 def render_entity(self, entity): |
|
134 """return html to display the given entity""" |
|
135 siderelations = [] |
|
136 self.render_entity_title(entity) |
|
137 self.render_entity_metadata(entity) |
|
138 # entity's attributes and relations, excluding meta data |
|
139 # if the entity isn't meta itself |
|
140 self.w(u'<table border="0" width="100%">') |
|
141 self.w(u'<tr>') |
|
142 self.w(u'<td style="width:75%" valign="top">') |
|
143 self.w(u'<div class="mainInfo">') |
|
144 self.render_entity_attributes(entity, siderelations) |
|
145 self.w(u'</div>') |
|
146 self.w(u'<div class="navcontenttop">') |
|
147 for comp in self.vreg.possible_vobjects('contentnavigation', |
|
148 self.req, self.rset, |
|
149 view=self, context='navcontenttop'): |
|
150 comp.dispatch(w=self.w, view=self) |
|
151 self.w(u'</div>') |
|
152 if self.main_related_section: |
|
153 self.render_entity_relations(entity, siderelations) |
|
154 self.w(u'</td>') |
|
155 # side boxes |
|
156 self.w(u'<td valign="top">') |
|
157 self.render_side_related(entity, siderelations) |
|
158 self.w(u'</td>') |
|
159 self.w(u'<td valign="top">') |
|
160 self.w(u'</td>') |
|
161 self.w(u'</tr>') |
|
162 self.w(u'</table>') |
|
163 self.w(u'<div class="navcontentbottom">') |
|
164 for comp in self.vreg.possible_vobjects('contentnavigation', |
|
165 self.req, self.rset, |
|
166 view=self, context='navcontentbottom'): |
|
167 comp.dispatch(w=self.w, view=self) |
|
168 self.w(u'</div>') |
|
169 |
|
170 def iter_attributes(self, entity): |
|
171 for rschema, targetschema in entity.e_schema.attribute_definitions(): |
|
172 attr = rschema.type |
|
173 if attr in self.skip_attrs: |
|
174 continue |
|
175 yield rschema, targetschema |
|
176 |
|
177 def iter_relations(self, entity): |
|
178 skip = set(self.skip_rels) |
|
179 skip.update(PRIMARY_SKIP_RELS) |
|
180 for rschema, targetschemas, x in entity.e_schema.relation_definitions(): |
|
181 if rschema.type in skip: |
|
182 continue |
|
183 yield rschema, targetschemas, x |
|
184 |
|
185 def render_entity_title(self, entity): |
|
186 title = self.content_title(entity) # deprecate content_title? |
|
187 if title: |
|
188 self.w(u'<h1><span class="etype">%s</span> %s</h1>' |
|
189 % (entity.dc_type().capitalize(), title)) |
|
190 |
|
191 def content_title(self, entity): |
|
192 """default implementation return an empty string""" |
|
193 return u'' |
|
194 |
|
195 def render_entity_metadata(self, entity): |
|
196 entity.view('metadata', w=self.w) |
|
197 summary = self.summary(entity) # deprecate summary? |
|
198 if summary: |
|
199 self.w(u'<div class="summary">%s</div>' % summary) |
|
200 |
|
201 def summary(self, entity): |
|
202 """default implementation return an empty string""" |
|
203 return u'' |
|
204 |
|
205 |
|
206 def render_entity_attributes(self, entity, siderelations): |
|
207 for rschema, targetschema in self.iter_attributes(entity): |
|
208 attr = rschema.type |
|
209 if targetschema.type in ('Password', 'Bytes'): |
|
210 continue |
|
211 try: |
|
212 wdg = entity.get_widget(attr) |
|
213 except Exception, ex: |
|
214 value = entity.printable_value(attr, entity[attr], targetschema.type) |
|
215 else: |
|
216 value = wdg.render(entity) |
|
217 if self.skip_none and (value is None or value == ''): |
|
218 continue |
|
219 if rschema.meta: |
|
220 continue |
|
221 self._render_related_entities(entity, rschema, value) |
|
222 |
|
223 def render_entity_relations(self, entity, siderelations): |
|
224 if hasattr(self, 'get_side_boxes_defs'): |
|
225 return |
|
226 eschema = entity.e_schema |
|
227 maxrelated = self.req.property_value('navigation.related-limit') |
|
228 for rschema, targetschemas, x in self.iter_relations(entity): |
|
229 try: |
|
230 related = entity.related(rschema.type, x, limit=maxrelated+1) |
|
231 except Unauthorized: |
|
232 continue |
|
233 if not related: |
|
234 continue |
|
235 if self.is_side_related(rschema, eschema): |
|
236 siderelations.append((rschema, related, x)) |
|
237 continue |
|
238 self._render_related_entities(entity, rschema, related, x) |
|
239 |
|
240 def render_side_related(self, entity, siderelations): |
|
241 """display side related relations: |
|
242 non-meta in a first step, meta in a second step |
|
243 """ |
|
244 if hasattr(self, 'get_side_boxes_defs'): |
|
245 for label, rset in self.get_side_boxes_defs(entity): |
|
246 if rset: |
|
247 self.w(u'<div class="sideRelated">') |
|
248 self.wview('sidebox', rset, title=label) |
|
249 self.w(u'</div>') |
|
250 elif siderelations: |
|
251 self.w(u'<div class="sideRelated">') |
|
252 for relatedinfos in siderelations: |
|
253 # if not relatedinfos[0].meta: |
|
254 # continue |
|
255 self._render_related_entities(entity, *relatedinfos) |
|
256 self.w(u'</div>') |
|
257 for box in self.vreg.possible_vobjects('boxes', self.req, entity.rset, |
|
258 col=entity.col, row=entity.row, |
|
259 view=self, context='incontext'): |
|
260 try: |
|
261 box.dispatch(w=self.w, col=entity.col, row=entity.row) |
|
262 except NotImplementedError: |
|
263 # much probably a context insensitive box, which only implements |
|
264 # .call() and not cell_call() |
|
265 box.dispatch(w=self.w) |
|
266 |
|
267 def is_side_related(self, rschema, eschema): |
|
268 return rschema.meta and \ |
|
269 not rschema.schema_relation() == eschema.schema_entity() |
|
270 |
|
271 def _render_related_entities(self, entity, rschema, related, |
|
272 role='subject'): |
|
273 if rschema.is_final(): |
|
274 value = related |
|
275 show_label = self.show_attr_label |
|
276 else: |
|
277 if not related: |
|
278 return |
|
279 show_label = self.show_rel_label |
|
280 # if not too many entities, show them all in a list |
|
281 maxrelated = self.req.property_value('navigation.related-limit') |
|
282 if related.rowcount <= maxrelated: |
|
283 if related.rowcount == 1: |
|
284 value = self.view('incontext', related, row=0) |
|
285 elif 1 < related.rowcount <= 5: |
|
286 value = self.view('csv', related) |
|
287 else: |
|
288 value = '<div>' + self.view('simplelist', related) + '</div>' |
|
289 # else show links to display related entities |
|
290 else: |
|
291 rql = related.printable_rql() |
|
292 related.limit(maxrelated) |
|
293 value = '<div>' + self.view('simplelist', related) |
|
294 value += '[<a href="%s">%s</a>]' % (self.build_url(rql=rql), |
|
295 self.req._('see them all')) |
|
296 value += '</div>' |
|
297 label = display_name(self.req, rschema.type, role) |
|
298 self.field(label, value, show_label=show_label, w=self.w, tr=False) |
|
299 |
|
300 |
|
301 class SideBoxView(EntityView): |
|
302 """side box usually displaying some related entities in a primary view""" |
|
303 id = 'sidebox' |
|
304 |
|
305 def call(self, boxclass='sideBox', title=u''): |
|
306 """display a list of entities by calling their <item_vid> view |
|
307 """ |
|
308 if title: |
|
309 self.w(u'<div class="sideBoxTitle"><span>%s</span></div>' % title) |
|
310 self.w(u'<div class="%s"><div class="sideBoxBody">' % boxclass) |
|
311 # if not too much entities, show them all in a list |
|
312 maxrelated = self.req.property_value('navigation.related-limit') |
|
313 if self.rset.rowcount <= maxrelated: |
|
314 if len(self.rset) == 1: |
|
315 self.wview('incontext', self.rset, row=0) |
|
316 elif 1 < len(self.rset) < 5: |
|
317 self.wview('csv', self.rset) |
|
318 else: |
|
319 self.wview('simplelist', self.rset) |
|
320 # else show links to display related entities |
|
321 else: |
|
322 self.rset.limit(maxrelated) |
|
323 rql = self.rset.printable_rql(encoded=False) |
|
324 self.wview('simplelist', self.rset) |
|
325 self.w(u'[<a href="%s">%s</a>]' % (self.build_url(rql=rql), |
|
326 self.req._('see them all'))) |
|
327 self.w(u'</div>\n</div>\n') |
|
328 |
|
329 |
|
330 |
|
331 class SecondaryView(EntityView): |
|
332 id = 'secondary' |
|
333 title = _('secondary') |
|
334 |
|
335 def cell_call(self, row, col): |
|
336 """the secondary view for an entity |
|
337 secondary = icon + view(oneline) |
|
338 """ |
|
339 entity = self.entity(row, col) |
|
340 self.w(u' ') |
|
341 self.wview('oneline', self.rset, row=row, col=col) |
|
342 |
|
343 class OneLineView(EntityView): |
|
344 id = 'oneline' |
|
345 title = _('oneline') |
|
346 |
|
347 def cell_call(self, row, col): |
|
348 """the one line view for an entity: linked text view |
|
349 """ |
|
350 entity = self.entity(row, col) |
|
351 self.w(u'<a href="%s">' % html_escape(entity.absolute_url())) |
|
352 self.w(html_escape(self.view('text', self.rset, row=row, col=col))) |
|
353 self.w(u'</a>') |
|
354 |
|
355 class TextView(EntityView): |
|
356 """the simplest text view for an entity |
|
357 """ |
|
358 id = 'text' |
|
359 title = _('text') |
|
360 accepts = 'Any', |
|
361 def call(self, **kwargs): |
|
362 """the view is called for an entire result set, by default loop |
|
363 other rows of the result set and call the same view on the |
|
364 particular row |
|
365 |
|
366 Views applicable on None result sets have to override this method |
|
367 """ |
|
368 rset = self.rset |
|
369 if rset is None: |
|
370 raise NotImplementedError, self |
|
371 for i in xrange(len(rset)): |
|
372 self.wview(self.id, rset, row=i, **kwargs) |
|
373 if len(rset) > 1: |
|
374 self.w(u"\n") |
|
375 |
|
376 def cell_call(self, row, col=0, **kwargs): |
|
377 entity = self.entity(row, col) |
|
378 self.w(cut(entity.dc_title(), |
|
379 self.req.property_value('navigation.short-line-size'))) |
|
380 |
|
381 class MetaDataView(EntityView): |
|
382 """paragraph view of some metadata""" |
|
383 id = 'metadata' |
|
384 accepts = 'Any', |
|
385 show_eid = True |
|
386 |
|
387 def cell_call(self, row, col): |
|
388 _ = self.req._ |
|
389 entity = self.entity(row, col) |
|
390 self.w(u'<div class="metadata">') |
|
391 if self.show_eid: |
|
392 self.w(u'#%s - ' % entity.eid) |
|
393 if entity.modification_date != entity.creation_date: |
|
394 self.w(u'<span>%s</span> ' % _('latest update on')) |
|
395 self.w(u'<span class="value">%s</span>, ' |
|
396 % self.format_date(entity.modification_date)) |
|
397 # entities from external source may not have a creation date (eg ldap) |
|
398 if entity.creation_date: |
|
399 self.w(u'<span>%s</span> ' % _('created on')) |
|
400 self.w(u'<span class="value">%s</span>' |
|
401 % self.format_date(entity.creation_date)) |
|
402 if entity.creator: |
|
403 creatoreid = entity.creator.eid |
|
404 self.w(u' <span>%s</span> ' % _('by')) |
|
405 self.w(u'<span class="value">%s</span>' % entity.creator.name()) |
|
406 else: |
|
407 creatoreid = None |
|
408 try: |
|
409 owners = ','.join(u.name() for u in entity.owned_by |
|
410 if u.eid != creatoreid) |
|
411 if owners: |
|
412 self.w(u', <span>%s</span> ' % _('owned by')) |
|
413 self.w(u'<span class="value">%s</span>' % owners) |
|
414 except Unauthorized: |
|
415 pass |
|
416 self.w(u'</div>') |
|
417 |
|
418 |
|
419 # new default views for finner control in general views , to use instead of |
|
420 # oneline / secondary |
|
421 |
|
422 class InContextTextView(TextView): |
|
423 id = 'textincontext' |
|
424 title = None # not listed as a possible view |
|
425 def cell_call(self, row, col): |
|
426 entity = self.entity(row, col) |
|
427 self.w(entity.dc_title()) |
|
428 |
|
429 class OutOfContextTextView(InContextTextView): |
|
430 id = 'textoutofcontext' |
|
431 |
|
432 def cell_call(self, row, col): |
|
433 entity = self.entity(row, col) |
|
434 self.w(entity.dc_long_title()) |
|
435 |
|
436 |
|
437 class InContextView(EntityView): |
|
438 id = 'incontext' |
|
439 |
|
440 def cell_call(self, row, col): |
|
441 entity = self.entity(row, col) |
|
442 desc = cut(entity.dc_description(), 50) |
|
443 self.w(u'<a href="%s" title="%s">' % (html_escape(entity.absolute_url()), |
|
444 html_escape(desc))) |
|
445 self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col))) |
|
446 self.w(u'</a>') |
|
447 |
|
448 |
|
449 class OutOfContextView(EntityView): |
|
450 id = 'outofcontext' |
|
451 |
|
452 def cell_call(self, row, col): |
|
453 self.w(u'<a href="%s">' % self.entity(row, col).absolute_url()) |
|
454 self.w(html_escape(self.view('textoutofcontext', self.rset, row=row, col=col))) |
|
455 self.w(u'</a>') |
|
456 |
|
457 class NotClickableInContextView(EntityView): |
|
458 id = 'incontext' |
|
459 accepts = ('State',) |
|
460 def cell_call(self, row, col): |
|
461 self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col))) |
|
462 |
|
463 ## class NotClickableOutOfContextView(EntityView): |
|
464 ## id = 'outofcontext' |
|
465 ## accepts = ('State',) |
|
466 ## def cell_call(self, row, col): |
|
467 ## self.w(html_escape(self.view('textoutofcontext', self.rset, row=row))) |
|
468 |
|
469 |
|
470 # list and table related views ################################################ |
|
471 |
|
472 class ListView(EntityView): |
|
473 id = 'list' |
|
474 title = _('list') |
|
475 item_vid = 'listitem' |
|
476 |
|
477 def call(self, klass=None, title=None, subvid=None, listid=None, **kwargs): |
|
478 """display a list of entities by calling their <item_vid> view |
|
479 |
|
480 :param listid: the DOM id to use for the root element |
|
481 """ |
|
482 if subvid is None and 'subvid' in self.req.form: |
|
483 subvid = self.req.form.pop('subvid') # consume it |
|
484 if listid: |
|
485 listid = u' id="%s"' % listid |
|
486 else: |
|
487 listid = u'' |
|
488 if title: |
|
489 self.w(u'<div%s class="%s"><h4>%s</h4>\n' % (listid, klass or 'section', title)) |
|
490 self.w(u'<ul>\n') |
|
491 else: |
|
492 self.w(u'<ul%s class="%s">\n' % (listid, klass or 'section')) |
|
493 for i in xrange(self.rset.rowcount): |
|
494 self.cell_call(row=i, col=0, vid=subvid, **kwargs) |
|
495 self.w(u'</ul>\n') |
|
496 if title: |
|
497 self.w(u'</div>\n') |
|
498 |
|
499 def cell_call(self, row, col=0, vid=None, **kwargs): |
|
500 self.w(u'<li>') |
|
501 self.wview(self.item_vid, self.rset, row=row, col=col, vid=vid, **kwargs) |
|
502 self.w(u'</li>\n') |
|
503 |
|
504 def url(self): |
|
505 """overrides url method so that by default, the view list is called |
|
506 with sorted entities |
|
507 """ |
|
508 coltypes = self.rset.column_types(0) |
|
509 # don't want to generate the rql if there is some restriction on |
|
510 # something else than the entity type |
|
511 if len(coltypes) == 1: |
|
512 # XXX norestriction is not correct here. For instance, in cases like |
|
513 # Any P,N WHERE P is Project, P name N |
|
514 # norestriction should equal True |
|
515 restr = self.rset.syntax_tree().children[0].where |
|
516 norestriction = (isinstance(restr, nodes.Relation) and |
|
517 restr.is_types_restriction()) |
|
518 if norestriction: |
|
519 etype = iter(coltypes).next() |
|
520 return self.build_url(etype.lower(), vid=self.id) |
|
521 if len(self.rset) == 1: |
|
522 entity = self.rset.get_entity(0, 0) |
|
523 return self.build_url(entity.rest_path(), vid=self.id) |
|
524 return self.build_url(rql=self.rset.printable_rql(), vid=self.id) |
|
525 |
|
526 |
|
527 class ListItemView(EntityView): |
|
528 id = 'listitem' |
|
529 |
|
530 @property |
|
531 def redirect_vid(self): |
|
532 if self.req.search_state[0] == 'normal': |
|
533 return 'outofcontext' |
|
534 return 'outofcontext-search' |
|
535 |
|
536 def cell_call(self, row, col, vid=None, **kwargs): |
|
537 if not vid: |
|
538 vid = self.redirect_vid |
|
539 try: |
|
540 self.wview(vid, self.rset, row=row, col=col, **kwargs) |
|
541 except NoSelectableObject: |
|
542 if vid == self.redirect_vid: |
|
543 raise |
|
544 kwargs.pop('done', None) |
|
545 self.wview(self.redirect_vid, self.rset, row=row, col=col, **kwargs) |
|
546 |
|
547 |
|
548 class SimpleListView(ListItemView): |
|
549 """list without bullets""" |
|
550 id = 'simplelist' |
|
551 redirect_vid = 'incontext' |
|
552 |
|
553 |
|
554 class CSVView(SimpleListView): |
|
555 id = 'csv' |
|
556 redirect_vid = 'incontext' |
|
557 |
|
558 def call(self, **kwargs): |
|
559 rset = self.rset |
|
560 for i in xrange(len(rset)): |
|
561 self.cell_call(i, 0, vid=kwargs.get('vid')) |
|
562 if i < rset.rowcount-1: |
|
563 self.w(u", ") |
|
564 |
|
565 |
|
566 class TreeItemView(ListItemView): |
|
567 accepts = ('Any',) |
|
568 id = 'treeitem' |
|
569 |
|
570 def cell_call(self, row, col): |
|
571 self.wview('incontext', self.rset, row=row, col=col) |
|
572 |
|
573 |
|
574 # xml and xml/rss views ####################################################### |
|
575 |
|
576 class XmlView(EntityView): |
|
577 id = 'xml' |
|
578 title = _('xml') |
|
579 templatable = False |
|
580 content_type = 'text/xml' |
|
581 xml_root = 'rset' |
|
582 item_vid = 'xmlitem' |
|
583 |
|
584 def cell_call(self, row, col): |
|
585 self.wview(self.item_vid, self.rset, row=row, col=col) |
|
586 |
|
587 def call(self): |
|
588 """display a list of entities by calling their <item_vid> view |
|
589 """ |
|
590 self.w(u'<?xml version="1.0" encoding="%s"?>\n' % self.req.encoding) |
|
591 self.w(u'<%s size="%s">\n' % (self.xml_root, len(self.rset))) |
|
592 for i in xrange(self.rset.rowcount): |
|
593 self.cell_call(i, 0) |
|
594 self.w(u'</%s>\n' % self.xml_root) |
|
595 |
|
596 |
|
597 class XmlItemView(EntityView): |
|
598 id = 'xmlitem' |
|
599 |
|
600 def cell_call(self, row, col): |
|
601 """ element as an item for an xml feed """ |
|
602 entity = self.complete_entity(row, col) |
|
603 self.w(u'<%s>\n' % (entity.e_schema)) |
|
604 for rschema, attrschema in entity.e_schema.attribute_definitions(): |
|
605 attr = rschema.type |
|
606 try: |
|
607 value = entity[attr] |
|
608 except KeyError: |
|
609 # Bytes |
|
610 continue |
|
611 if value is not None: |
|
612 if attrschema == 'Bytes': |
|
613 from base64 import b64encode |
|
614 value = '<![CDATA[%s]]>' % b64encode(value.getvalue()) |
|
615 elif isinstance(value, basestring): |
|
616 value = value.replace('&', '&').replace('<', '<') |
|
617 self.w(u' <%s>%s</%s>\n' % (attr, value, attr)) |
|
618 self.w(u'</%s>\n' % (entity.e_schema)) |
|
619 |
|
620 |
|
621 class RssView(XmlView): |
|
622 id = 'rss' |
|
623 title = _('rss') |
|
624 templatable = False |
|
625 content_type = 'text/xml' |
|
626 http_cache_manager = MaxAgeHTTPCacheManager |
|
627 cache_max_age = 60*60*2 # stay in http cache for 2 hours by default |
|
628 |
|
629 def cell_call(self, row, col): |
|
630 self.wview('rssitem', self.rset, row=row, col=col) |
|
631 |
|
632 def call(self): |
|
633 """display a list of entities by calling their <item_vid> view""" |
|
634 req = self.req |
|
635 self.w(u'<?xml version="1.0" encoding="%s"?>\n' % req.encoding) |
|
636 self.w(u'''<rdf:RDF |
|
637 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
|
638 xmlns:dc="http://purl.org/dc/elements/1.1/" |
|
639 xmlns="http://purl.org/rss/1.0/" |
|
640 >''') |
|
641 self.w(u' <channel rdf:about="%s">\n' % html_escape(req.url())) |
|
642 self.w(u' <title>%s RSS Feed</title>\n' % html_escape(self.page_title())) |
|
643 self.w(u' <description>%s</description>\n' % html_escape(req.form.get('vtitle', ''))) |
|
644 params = req.form.copy() |
|
645 params.pop('vid', None) |
|
646 self.w(u' <link>%s</link>\n' % html_escape(self.build_url(**params))) |
|
647 self.w(u' <items>\n') |
|
648 self.w(u' <rdf:Seq>\n') |
|
649 for entity in self.rset.entities(): |
|
650 self.w(u' <rdf:li resource="%s" />\n' % html_escape(entity.absolute_url())) |
|
651 self.w(u' </rdf:Seq>\n') |
|
652 self.w(u' </items>\n') |
|
653 self.w(u' </channel>\n') |
|
654 for i in xrange(self.rset.rowcount): |
|
655 self.cell_call(i, 0) |
|
656 self.w(u'</rdf:RDF>') |
|
657 |
|
658 |
|
659 class RssItemView(EntityView): |
|
660 id = 'rssitem' |
|
661 date_format = '%%Y-%%m-%%dT%%H:%%M%+03i:00' % (timezone / 3600) |
|
662 |
|
663 def cell_call(self, row, col): |
|
664 entity = self.complete_entity(row, col) |
|
665 self.w(u'<item rdf:about="%s">\n' % html_escape(entity.absolute_url())) |
|
666 self._marker('title', entity.dc_long_title()) |
|
667 self._marker('link', entity.absolute_url()) |
|
668 self._marker('description', entity.dc_description()) |
|
669 self._marker('dc:date', entity.dc_date(self.date_format)) |
|
670 self._marker('author', entity.dc_authors()) |
|
671 self.w(u'</item>\n') |
|
672 |
|
673 def _marker(self, marker, value): |
|
674 if value: |
|
675 self.w(u' <%s>%s</%s>\n' % (marker, html_escape(value), marker)) |
|
676 |
|
677 |
|
678 class CSVMixIn(object): |
|
679 """mixin class for CSV views""" |
|
680 templatable = False |
|
681 content_type = "text/comma-separated-values" |
|
682 binary = True # avoid unicode assertion |
|
683 csv_params = {'dialect': 'excel', |
|
684 'quotechar': '"', |
|
685 'delimiter': ';', |
|
686 'lineterminator': '\n'} |
|
687 |
|
688 def set_request_content_type(self): |
|
689 """overriden to set a .csv filename""" |
|
690 self.req.set_content_type(self.content_type, filename='cubicwebexport.csv') |
|
691 |
|
692 def csvwriter(self, **kwargs): |
|
693 params = self.csv_params.copy() |
|
694 params.update(kwargs) |
|
695 return UnicodeCSVWriter(self.w, self.req.encoding, **params) |
|
696 |
|
697 |
|
698 class CSVRsetView(CSVMixIn, AnyRsetView): |
|
699 """dumps rset in CSV""" |
|
700 id = 'csvexport' |
|
701 title = _('csv export') |
|
702 |
|
703 def call(self): |
|
704 writer = self.csvwriter() |
|
705 writer.writerow(self.get_headers_labels()) |
|
706 descr = self.rset.description |
|
707 for rowindex, row in enumerate(self.rset): |
|
708 csvrow = [] |
|
709 for colindex, val in enumerate(row): |
|
710 etype = descr[rowindex][colindex] |
|
711 if val is not None and not self.schema.eschema(etype).is_final(): |
|
712 # csvrow.append(val) # val is eid in that case |
|
713 content = self.view('textincontext', self.rset, |
|
714 row=rowindex, col=colindex) |
|
715 else: |
|
716 content = self.view('final', self.rset, displaytime=True, |
|
717 row=rowindex, col=colindex) |
|
718 csvrow.append(content) |
|
719 writer.writerow(csvrow) |
|
720 |
|
721 def get_headers_labels(self): |
|
722 rqlstdescr = self.rset.syntax_tree().get_description()[0] # XXX missing Union support |
|
723 labels = [] |
|
724 for colindex, attr in enumerate(rqlstdescr): |
|
725 # compute column header |
|
726 if colindex == 0 or attr == 'Any': # find a better label |
|
727 label = ','.join(display_name(self.req, et) |
|
728 for et in self.rset.column_types(colindex)) |
|
729 else: |
|
730 label = display_name(self.req, attr) |
|
731 labels.append(label) |
|
732 return labels |
|
733 |
|
734 |
|
735 class CSVEntityView(CSVMixIn, EntityView): |
|
736 """dumps rset's entities (with full set of attributes) in CSV""" |
|
737 id = 'ecsvexport' |
|
738 title = _('csv entities export') |
|
739 |
|
740 def call(self): |
|
741 """ |
|
742 the generated CSV file will have a table per entity type |
|
743 found in the resultset. ('table' here only means empty |
|
744 lines separation between table contents) |
|
745 """ |
|
746 req = self.req |
|
747 rows_by_type = {} |
|
748 writer = self.csvwriter() |
|
749 rowdef_by_type = {} |
|
750 for index in xrange(len(self.rset)): |
|
751 entity = self.complete_entity(index) |
|
752 if entity.e_schema not in rows_by_type: |
|
753 rowdef_by_type[entity.e_schema] = [rs for rs, as in entity.e_schema.attribute_definitions() |
|
754 if as.type != 'Bytes'] |
|
755 rows_by_type[entity.e_schema] = [[display_name(req, rschema.type) |
|
756 for rschema in rowdef_by_type[entity.e_schema]]] |
|
757 rows = rows_by_type[entity.e_schema] |
|
758 rows.append([entity.printable_value(rs.type, format='text/plain') |
|
759 for rs in rowdef_by_type[entity.e_schema]]) |
|
760 for etype, rows in rows_by_type.items(): |
|
761 writer.writerows(rows) |
|
762 # use two empty lines as separator |
|
763 writer.writerows([[], []]) |
|
764 |
|
765 |
|
766 ## Work in progress ########################################################### |
|
767 |
|
768 class SearchForAssociationView(EntityView): |
|
769 """view called by the edition view when the user asks |
|
770 to search for something to link to the edited eid |
|
771 """ |
|
772 id = 'search-associate' |
|
773 title = _('search for association') |
|
774 __selectors__ = (onelinerset_selector, searchstate_selector, accept_selector) |
|
775 accepts = ('Any',) |
|
776 search_states = ('linksearch',) |
|
777 |
|
778 def cell_call(self, row, col): |
|
779 rset, vid, divid, paginate = self.filter_box_context_info() |
|
780 self.w(u'<div id="%s">' % divid) |
|
781 self.pagination(self.req, rset, w=self.w) |
|
782 self.wview(vid, rset) |
|
783 self.w(u'</div>') |
|
784 |
|
785 @cached |
|
786 def filter_box_context_info(self): |
|
787 entity = self.entity(0, 0) |
|
788 role, eid, rtype, etype = self.req.search_state[1] |
|
789 assert entity.eid == typed_eid(eid) |
|
790 # the default behaviour is to fetch all unrelated entities and display |
|
791 # them. Use fetch_order and not fetch_unrelated_order as sort method |
|
792 # since the latter is mainly there to select relevant items in the combo |
|
793 # box, it doesn't give interesting result in this context |
|
794 rql = entity.unrelated_rql(rtype, etype, role, |
|
795 ordermethod='fetch_order', |
|
796 vocabconstraints=False) |
|
797 rset = self.req.execute(rql, {'x' : entity.eid}, 'x') |
|
798 #vid = vid_from_rset(self.req, rset, self.schema) |
|
799 return rset, 'list', "search-associate-content", True |
|
800 |
|
801 |
|
802 class OutOfContextSearch(EntityView): |
|
803 id = 'outofcontext-search' |
|
804 def cell_call(self, row, col): |
|
805 entity = self.entity(row, col) |
|
806 erset = entity.as_rset() |
|
807 if linksearch_match(self.req, erset): |
|
808 self.w(u'<a href="%s" title="%s">%s</a> <a href="%s" title="%s">[...]</a>' % ( |
|
809 html_escape(linksearch_select_url(self.req, erset)), |
|
810 self.req._('select this entity'), |
|
811 html_escape(entity.view('textoutofcontext')), |
|
812 html_escape(entity.absolute_url(vid='primary')), |
|
813 self.req._('view detail for this entity'))) |
|
814 else: |
|
815 entity.view('outofcontext', w=self.w) |
|
816 |
|
817 |
|
818 class EditRelationView(EntityView): |
|
819 """Note: This is work in progress |
|
820 |
|
821 This view is part of the edition view refactoring. |
|
822 It is still too big and cluttered with strange logic, but it's a start |
|
823 |
|
824 The main idea is to be able to call an edition view for a specific |
|
825 relation. For example : |
|
826 self.wview('editrelation', person_rset, rtype='firstname') |
|
827 self.wview('editrelation', person_rset, rtype='works_for') |
|
828 """ |
|
829 id = 'editrelation' |
|
830 |
|
831 __selectors__ = (req_form_params_selector,) |
|
832 form_params = ('rtype',) |
|
833 |
|
834 # TODO: inlineview, multiple edit, (widget view ?) |
|
835 def cell_call(self, row, col, rtype=None, role='subject', targettype=None, |
|
836 showlabel=True): |
|
837 self.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') ) |
|
838 entity = self.entity(row, col) |
|
839 rtype = self.req.form.get('rtype', rtype) |
|
840 showlabel = self.req.form.get('showlabel', showlabel) |
|
841 assert rtype is not None, "rtype is mandatory for 'edirelation' view" |
|
842 targettype = self.req.form.get('targettype', targettype) |
|
843 role = self.req.form.get('role', role) |
|
844 mode = entity.rtags.get_mode(rtype, targettype, role) |
|
845 if mode == 'create': |
|
846 return |
|
847 category = entity.rtags.get_category(rtype, targettype, role) |
|
848 if category in ('generated', 'metadata'): |
|
849 return |
|
850 elif category in ('primary', 'secondary'): |
|
851 if hasattr(entity, '%s_format' % rtype): |
|
852 formatwdg = entity.get_widget('%s_format' % rtype, role) |
|
853 self.w(formatwdg.edit_render(entity)) |
|
854 self.w(u'<br/>') |
|
855 wdg = entity.get_widget(rtype, role) |
|
856 if showlabel: |
|
857 self.w(u'%s' % wdg.render_label(entity)) |
|
858 self.w(u'%s %s %s' % |
|
859 (wdg.render_error(entity), wdg.edit_render(entity), |
|
860 wdg.render_help(entity),)) |
|
861 elif category == 'generic': |
|
862 self._render_generic_relation(entity, rtype, role) |
|
863 else: |
|
864 self.error("oops, wrong category %s", category) |
|
865 |
|
866 def _render_generic_relation(self, entity, relname, role): |
|
867 text = self.req.__('add %s %s %s' % (entity.e_schema, relname, role)) |
|
868 # pending operations |
|
869 operations = self.req.get_pending_operations(entity, relname, role) |
|
870 if operations['insert'] or operations['delete'] or 'unfold' in self.req.form: |
|
871 self.w(u'<h3>%s</h3>' % text) |
|
872 self._render_generic_relation_form(operations, entity, relname, role) |
|
873 else: |
|
874 divid = "%s%sreledit" % (relname, role) |
|
875 url = ajax_replace_url(divid, rql_for_eid(entity.eid), 'editrelation', |
|
876 {'unfold' : 1, 'relname' : relname, 'role' : role}) |
|
877 self.w(u'<a href="%s">%s</a>' % (url, text)) |
|
878 self.w(u'<div id="%s"></div>' % divid) |
|
879 |
|
880 |
|
881 def _build_opvalue(self, entity, relname, target, role): |
|
882 if role == 'subject': |
|
883 return '%s:%s:%s' % (entity.eid, relname, target) |
|
884 else: |
|
885 return '%s:%s:%s' % (target, relname, entity.eid) |
|
886 |
|
887 |
|
888 def _render_generic_relation_form(self, operations, entity, relname, role): |
|
889 rqlexec = self.req.execute |
|
890 for optype, targets in operations.items(): |
|
891 for target in targets: |
|
892 self._render_pending(optype, entity, relname, target, role) |
|
893 opvalue = self._build_opvalue(entity, relname, target, role) |
|
894 self.w(u'<a href="javascript: addPendingDelete(\'%s\', %s);">-</a> ' |
|
895 % (opvalue, entity.eid)) |
|
896 rset = rqlexec('Any X WHERE X eid %(x)s', {'x': target}, 'x') |
|
897 self.wview('oneline', rset) |
|
898 # now, unrelated ones |
|
899 self._render_unrelated_selection(entity, relname, role) |
|
900 |
|
901 def _render_pending(self, optype, entity, relname, target, role): |
|
902 opvalue = self._build_opvalue(entity, relname, target, role) |
|
903 self.w(u'<input type="hidden" name="__%s" value="%s" />' |
|
904 % (optype, opvalue)) |
|
905 if optype == 'insert': |
|
906 checktext = '-' |
|
907 else: |
|
908 checktext = '+' |
|
909 rset = self.req.execute('Any X WHERE X eid %(x)s', {'x': target}, 'x') |
|
910 self.w(u"""[<a href="javascript: cancelPending%s('%s:%s:%s')">%s</a>""" |
|
911 % (optype.capitalize(), relname, target, role, |
|
912 self.view('oneline', rset))) |
|
913 |
|
914 def _render_unrelated_selection(self, entity, relname, role): |
|
915 rschema = self.schema.rschema(relname) |
|
916 if role == 'subject': |
|
917 targettypes = rschema.objects(entity.e_schema) |
|
918 else: |
|
919 targettypes = rschema.subjects(entity.e_schema) |
|
920 self.w(u'<select onselect="addPendingInsert(this.selected.value);">') |
|
921 for targettype in targettypes: |
|
922 unrelated = entity.unrelated(relname, targettype, role) # XXX limit |
|
923 for rowindex, row in enumerate(unrelated): |
|
924 teid = row[0] |
|
925 opvalue = self._build_opvalue(entity, relname, teid, role) |
|
926 self.w(u'<option name="__insert" value="%s>%s</option>' |
|
927 % (opvalue, self.view('text', unrelated, row=rowindex))) |
|
928 self.w(u'</select>') |
|
929 |
|
930 |
|
931 class TextSearchResultView(EntityView): |
|
932 """this view is used to display full-text search |
|
933 |
|
934 It tries to highlight part of data where the search word appears. |
|
935 |
|
936 XXX: finish me (fixed line width, fixed number of lines, CSS, etc.) |
|
937 """ |
|
938 id = 'tsearch' |
|
939 |
|
940 |
|
941 def cell_call(self, row, col, **kwargs): |
|
942 entity = self.complete_entity(row, col) |
|
943 self.w(entity.view('incontext')) |
|
944 searched = self.rset.searched_text() |
|
945 if searched is None: |
|
946 return |
|
947 searched = searched.lower() |
|
948 highlighted = '<b>%s</b>' % searched |
|
949 for attr in entity.e_schema.indexable_attributes(): |
|
950 try: |
|
951 value = html_escape(entity.printable_value(attr, format='text/plain').lower()) |
|
952 except TransformError, ex: |
|
953 continue |
|
954 except: |
|
955 continue |
|
956 if searched in value: |
|
957 contexts = [] |
|
958 for ctx in value.split(searched): |
|
959 if len(ctx) > 30: |
|
960 contexts.append(u'...' + ctx[-30:]) |
|
961 else: |
|
962 contexts.append(ctx) |
|
963 value = u'\n' + highlighted.join(contexts) |
|
964 self.w(value.replace('\n', '<br/>')) |
|
965 |
|
966 |
|
967 class EntityRelationView(EntityView): |
|
968 accepts = () |
|
969 vid = 'list' |
|
970 |
|
971 def cell_call(self, row, col): |
|
972 if self.target == 'object': |
|
973 role = 'subject' |
|
974 else: |
|
975 role = 'object' |
|
976 rset = self.rset.get_entity(row, col).related(self.rtype, role) |
|
977 self.w(u'<h1>%s</h1>' % self.req._(self.title).capitalize()) |
|
978 self.w(u'<div class="mainInfo">') |
|
979 self.wview(self.vid, rset, 'noresult') |
|
980 self.w(u'</div>') |
|
981 |
|
982 |
|
983 class TooltipView(OneLineView): |
|
984 """A entity view used in a tooltip""" |
|
985 id = 'tooltip' |
|
986 title = None # don't display in possible views |
|
987 def cell_call(self, row, col): |
|
988 self.wview('oneline', self.rset, row=row, col=col) |
|
989 |
|
990 try: |
|
991 from cubicweb.web.views.tableview import TableView |
|
992 from logilab.common.deprecation import class_moved |
|
993 TableView = class_moved(TableView) |
|
994 except ImportError: |
|
995 pass # gae has no tableview module (yet) |