20 """ |
20 """ |
21 |
21 |
22 __docformat__ = "restructuredtext en" |
22 __docformat__ = "restructuredtext en" |
23 _ = unicode |
23 _ = unicode |
24 |
24 |
25 from logilab.common.deprecation import class_renamed |
25 from logilab.common.deprecation import class_deprecated, class_renamed |
26 from logilab.mtconverter import xml_escape |
26 from logilab.mtconverter import xml_escape |
27 |
27 |
28 from cubicweb import role |
28 from cubicweb import Unauthorized, role, tags |
29 from cubicweb.utils import json_dumps |
29 from cubicweb.uilib import js, domid |
30 from cubicweb.view import Component |
30 from cubicweb.view import ReloadableMixIn, Component |
31 from cubicweb.selectors import ( |
31 from cubicweb.selectors import (no_cnx, paginated_rset, one_line_rset, |
32 paginated_rset, one_line_rset, primary_view, match_context_prop, |
32 non_final_entity, partial_relation_possible, |
33 partial_has_related_entities) |
33 partial_has_related_entities) |
34 |
34 from cubicweb.appobject import AppObject |
35 |
35 from cubicweb.web import htmlwidgets, stdmsgs |
36 class EntityVComponent(Component): |
36 |
37 """abstract base class for additinal components displayed in content |
37 |
38 headers and footer according to: |
38 # abstract base class for navigation components ################################ |
39 |
|
40 * the displayed entity's type |
|
41 * a context (currently 'header' or 'footer') |
|
42 |
|
43 it should be configured using .accepts, .etype, .rtype, .target and |
|
44 .context class attributes |
|
45 """ |
|
46 |
|
47 __registry__ = 'contentnavigation' |
|
48 __select__ = one_line_rset() & primary_view() & match_context_prop() |
|
49 |
|
50 cw_property_defs = { |
|
51 _('visible'): dict(type='Boolean', default=True, |
|
52 help=_('display the component or not')), |
|
53 _('order'): dict(type='Int', default=99, |
|
54 help=_('display order of the component')), |
|
55 _('context'): dict(type='String', default='navtop', |
|
56 vocabulary=(_('navtop'), _('navbottom'), |
|
57 _('navcontenttop'), _('navcontentbottom'), |
|
58 _('ctxtoolbar')), |
|
59 help=_('context where this component should be displayed')), |
|
60 } |
|
61 |
|
62 context = 'navcontentbottom' |
|
63 |
|
64 def call(self, view=None): |
|
65 if self.cw_rset is None: |
|
66 self.entity_call(self.cw_extra_kwargs.pop('entity')) |
|
67 else: |
|
68 self.cell_call(0, 0, view=view) |
|
69 |
|
70 def cell_call(self, row, col, view=None): |
|
71 self.entity_call(self.cw_rset.get_entity(row, col), view=view) |
|
72 |
|
73 def entity_call(self, entity, view=None): |
|
74 raise NotImplementedError() |
|
75 |
|
76 |
39 |
77 class NavigationComponent(Component): |
40 class NavigationComponent(Component): |
78 """abstract base class for navigation components""" |
41 """abstract base class for navigation components""" |
79 __regid__ = 'navigation' |
42 __regid__ = 'navigation' |
80 __select__ = paginated_rset() |
43 __select__ = paginated_rset() |
173 if start >= self.total: |
135 if start >= self.total: |
174 return self.no_next_page_link |
136 return self.no_next_page_link |
175 stop = start + self.page_size - 1 |
137 stop = start + self.page_size - 1 |
176 url = xml_escape(self.page_url(path, params, start, stop)) |
138 url = xml_escape(self.page_url(path, params, start, stop)) |
177 return self.next_page_link_templ % (url, title, content) |
139 return self.next_page_link_templ % (url, title, content) |
|
140 |
|
141 |
|
142 # new contextual components system ############################################# |
|
143 |
|
144 def override_ctx(cls, **kwargs): |
|
145 cwpdefs = cls.cw_property_defs.copy() |
|
146 cwpdefs['context'] = cwpdefs['context'].copy() |
|
147 cwpdefs['context'].update(kwargs) |
|
148 return cwpdefs |
|
149 |
|
150 |
|
151 class EmptyComponent(Exception): |
|
152 """some selectable component has actually no content and should not be |
|
153 rendered |
|
154 """ |
|
155 |
|
156 class Layout(Component): |
|
157 __regid__ = 'layout' |
|
158 __abstract__ = True |
|
159 |
|
160 def init_rendering(self): |
|
161 """init view for rendering. Return true if we should go on, false |
|
162 if we should stop now. |
|
163 """ |
|
164 view = self.cw_extra_kwargs['view'] |
|
165 try: |
|
166 view.init_rendering() |
|
167 except Unauthorized, ex: |
|
168 self.warning("can't render %s: %s", view, ex) |
|
169 return False |
|
170 except EmptyComponent: |
|
171 return False |
|
172 return True |
|
173 |
|
174 |
|
175 class CtxComponent(AppObject): |
|
176 """base class for contextual compontents. The following contexts are |
|
177 predefined: |
|
178 |
|
179 * boxes: 'left', 'incontext', 'right' |
|
180 * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' |
|
181 * other: 'ctxtoolbar' |
|
182 |
|
183 The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' |
|
184 context are handled by the default primary view, others by the default main |
|
185 template. |
|
186 |
|
187 All subclasses may not support all those contexts (for instance if it can't |
|
188 be displayed as box, or as a toolbar icon). You may restrict allowed context |
|
189 as followed: |
|
190 |
|
191 .. sourcecode:: python |
|
192 |
|
193 class MyComponent(CtxComponent): |
|
194 cw_property_defs = override_ctx(CtxComponent, |
|
195 vocabulary=[list of contexts]) |
|
196 context = 'my default context' |
|
197 |
|
198 You can configure default component's context by simply giving appropriate |
|
199 value to the `context` class attribute, as seen above. |
|
200 """ |
|
201 __registry__ = 'ctxcomponents' |
|
202 __select__ = ~no_cnx() |
|
203 |
|
204 categories_in_order = () |
|
205 cw_property_defs = { |
|
206 _('visible'): dict(type='Boolean', default=True, |
|
207 help=_('display the box or not')), |
|
208 _('order'): dict(type='Int', default=99, |
|
209 help=_('display order of the box')), |
|
210 _('context'): dict(type='String', default='left', |
|
211 vocabulary=(_('left'), _('incontext'), _('right'), |
|
212 _('navtop'), _('navbottom'), |
|
213 _('navcontenttop'), _('navcontentbottom'), |
|
214 _('ctxtoolbar')), |
|
215 help=_('context where this component should be displayed')), |
|
216 } |
|
217 context = 'left' |
|
218 contextual = False |
|
219 title = None |
|
220 |
|
221 # XXX support kwargs for compat with old boxes which gets the view as |
|
222 # argument |
|
223 def render(self, w, **kwargs): |
|
224 getlayout = self._cw.vreg['components'].select |
|
225 try: |
|
226 # XXX ensure context is given when the component is reloaded through |
|
227 # ajax |
|
228 context = self.cw_extra_kwargs['context'] |
|
229 except KeyError: |
|
230 context = self.cw_propval('context') |
|
231 layout = getlayout('layout', self._cw, rset=self.cw_rset, |
|
232 row=self.cw_row, col=self.cw_col, |
|
233 view=self, context=context) |
|
234 layout.render(w) |
|
235 |
|
236 def init_rendering(self): |
|
237 """init rendering callback: that's the good time to check your component |
|
238 has some content to display. If not, you can still raise |
|
239 :exc:`EmptyComponent` to inform it should be skipped. |
|
240 |
|
241 Also, :exc:`Unauthorized` will be catched, logged, then the component |
|
242 will be skipped. |
|
243 """ |
|
244 self.items = [] |
|
245 |
|
246 @property |
|
247 def domid(self): |
|
248 """return the HTML DOM identifier for this component""" |
|
249 return domid(self.__regid__) |
|
250 |
|
251 @property |
|
252 def cssclass(self): |
|
253 """return the CSS class name for this component""" |
|
254 return domid(self.__regid__) |
|
255 |
|
256 def render_title(self, w): |
|
257 """return the title for this component""" |
|
258 if self.title: |
|
259 w(self._cw._(self.title)) |
|
260 |
|
261 def render_body(self, w): |
|
262 """return the body (content) for this component""" |
|
263 raise NotImplementedError() |
|
264 |
|
265 def render_items(self, w, items=None, klass=u'boxListing'): |
|
266 if items is None: |
|
267 items = self.items |
|
268 assert items |
|
269 w(u'<ul class="%s">' % klass) |
|
270 for item in items: |
|
271 if hasattr(item, 'render'): |
|
272 item.render(w) # XXX display <li> by itself |
|
273 else: |
|
274 w(u'<li>') |
|
275 w(item) |
|
276 w(u'</li>') |
|
277 w(u'</ul>') |
|
278 |
|
279 def append(self, item): |
|
280 self.items.append(item) |
|
281 |
|
282 def box_action(self, action): # XXX action_link |
|
283 return self.build_link(self._cw._(action.title), action.url()) |
|
284 |
|
285 def build_link(self, title, url, **kwargs): |
|
286 if self._cw.selected(url): |
|
287 try: |
|
288 kwargs['klass'] += ' selected' |
|
289 except KeyError: |
|
290 kwargs['klass'] = 'selected' |
|
291 return tags.a(title, href=url, **kwargs) |
|
292 |
|
293 |
|
294 class EntityCtxComponent(CtxComponent): |
|
295 """base class for boxes related to a single entity""" |
|
296 __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset() |
|
297 context = 'incontext' |
|
298 contextual = True |
|
299 |
|
300 def __init__(self, *args, **kwargs): |
|
301 super(EntityCtxComponent, self).__init__(*args, **kwargs) |
|
302 try: |
|
303 entity = kwargs['entity'] |
|
304 except KeyError: |
|
305 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
306 self.entity = entity |
|
307 |
|
308 @property |
|
309 def domid(self): |
|
310 return domid(self.__regid__) + unicode(self.entity.eid) |
|
311 |
|
312 |
|
313 # high level abstract classes ################################################## |
|
314 |
|
315 class RQLCtxComponent(CtxComponent): |
|
316 """abstract box for boxes displaying the content of a rql query not |
|
317 related to the current result set. |
|
318 """ |
|
319 rql = None |
|
320 |
|
321 def to_display_rql(self): |
|
322 assert self.rql is not None, self.__regid__ |
|
323 return (self.rql,) |
|
324 |
|
325 def init_rendering(self): |
|
326 rset = self._cw.execute(*self.to_display_rql()) |
|
327 if not rset: |
|
328 raise EmptyComponent() |
|
329 if len(rset[0]) == 2: |
|
330 self.items = [] |
|
331 for i, (eid, label) in enumerate(rset): |
|
332 entity = rset.get_entity(i, 0) |
|
333 self.items.append(self.build_link(label, entity.absolute_url())) |
|
334 else: |
|
335 self.items = [self.build_link(e.dc_title(), e.absolute_url()) |
|
336 for e in rset.entities()] |
|
337 |
|
338 def render_body(self, w): |
|
339 self.render_items(w) |
|
340 |
|
341 |
|
342 class EditRelationMixIn(ReloadableMixIn): |
|
343 def box_item(self, entity, etarget, rql, label): |
|
344 """builds HTML link to edit relation between `entity` and `etarget`""" |
|
345 role, target = get_role(self), get_target(self) |
|
346 args = {role[0] : entity.eid, target[0] : etarget.eid} |
|
347 url = self._cw.user_rql_callback((rql, args)) |
|
348 # for each target, provide a link to edit the relation |
|
349 return u'[<a href="%s">%s</a>] %s' % (xml_escape(url), label, |
|
350 etarget.view('incontext')) |
|
351 |
|
352 def related_boxitems(self, entity): |
|
353 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
354 return [self.box_item(entity, etarget, rql, u'-') |
|
355 for etarget in self.related_entities(entity)] |
|
356 |
|
357 def related_entities(self, entity): |
|
358 return entity.related(self.rtype, get_role(self), entities=True) |
|
359 |
|
360 def unrelated_boxitems(self, entity): |
|
361 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
362 return [self.box_item(entity, etarget, rql, u'+') |
|
363 for etarget in self.unrelated_entities(entity)] |
|
364 |
|
365 def unrelated_entities(self, entity): |
|
366 """returns the list of unrelated entities, using the entity's |
|
367 appropriate vocabulary function |
|
368 """ |
|
369 skip = set(unicode(e.eid) for e in entity.related(self.rtype, get_role(self), |
|
370 entities=True)) |
|
371 skip.add(None) |
|
372 skip.add(INTERNAL_FIELD_VALUE) |
|
373 filteretype = getattr(self, 'etype', None) |
|
374 entities = [] |
|
375 form = self._cw.vreg['forms'].select('edition', self._cw, |
|
376 rset=self.cw_rset, |
|
377 row=self.cw_row or 0) |
|
378 field = form.field_by_name(self.rtype, get_role(self), entity.e_schema) |
|
379 for _, eid in field.vocabulary(form): |
|
380 if eid not in skip: |
|
381 entity = self._cw.entity_from_eid(eid) |
|
382 if filteretype is None or entity.__regid__ == filteretype: |
|
383 entities.append(entity) |
|
384 return entities |
|
385 |
|
386 |
|
387 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent): |
|
388 """base class for boxes which let add or remove entities linked by a given |
|
389 relation |
|
390 |
|
391 subclasses should define at least id, rtype and target class attributes. |
|
392 """ |
|
393 def render_title(self, w): |
|
394 return display_name(self._cw, self.rtype, get_role(self), |
|
395 context=self.entity.__regid__) |
|
396 |
|
397 def render_body(self, w): |
|
398 self._cw.add_js('cubicweb.ajax.js') |
|
399 related = self.related_boxitems(self.entity) |
|
400 unrelated = self.unrelated_boxitems(self.entity) |
|
401 self.items.extend(related) |
|
402 if related and unrelated: |
|
403 self.items.append(htmlwidgets.BoxSeparator()) |
|
404 self.items.extend(unrelated) |
|
405 self.render_items(w) |
|
406 |
|
407 |
|
408 class AjaxEditRelationCtxComponent(EntityCtxComponent): |
|
409 __select__ = EntityCtxComponent.__select__ & ( |
|
410 partial_relation_possible(action='add') | partial_has_related_entities()) |
|
411 |
|
412 # view used to display related entties |
|
413 item_vid = 'incontext' |
|
414 # values separator when multiple values are allowed |
|
415 separator = ',' |
|
416 # msgid of the message to display when some new relation has been added/removed |
|
417 added_msg = None |
|
418 removed_msg = None |
|
419 |
|
420 # class attributes below *must* be set in concret classes (additionaly to |
|
421 # rtype / role [/ target_etype]. They should correspond to js_* methods on |
|
422 # the json controller |
|
423 |
|
424 # function(eid) |
|
425 # -> expected to return a list of values to display as input selector |
|
426 # vocabulary |
|
427 fname_vocabulary = None |
|
428 |
|
429 # function(eid, value) |
|
430 # -> handle the selector's input (eg create necessary entities and/or |
|
431 # relations). If the relation is multiple, you'll get a list of value, else |
|
432 # a single string value. |
|
433 fname_validate = None |
|
434 |
|
435 # function(eid, linked entity eid) |
|
436 # -> remove the relation |
|
437 fname_remove = None |
|
438 |
|
439 def __init__(self, *args, **kwargs): |
|
440 super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs) |
|
441 self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype) |
|
442 |
|
443 def render_title(self, w): |
|
444 w(self.rdef.rtype.display_name(self._cw, self.role, |
|
445 context=self.entity.__regid__)) |
|
446 |
|
447 def render_body(self, w): |
|
448 req = self._cw |
|
449 entity = self.entity |
|
450 related = entity.related(self.rtype, self.role) |
|
451 if self.role == 'subject': |
|
452 mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) |
|
453 maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid) |
|
454 else: |
|
455 mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) |
|
456 maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid) |
|
457 if mayadd or maydel: |
|
458 req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
|
459 _ = req._ |
|
460 if related: |
|
461 w(u'<table>') |
|
462 for rentity in related.entities(): |
|
463 # for each related entity, provide a link to remove the relation |
|
464 subview = rentity.view(self.item_vid) |
|
465 if maydel: |
|
466 jscall = unicode(js.ajaxBoxRemoveLinkedEntity( |
|
467 self.__regid__, entity.eid, rentity.eid, |
|
468 self.fname_remove, |
|
469 self.removed_msg and _(self.removed_msg))) |
|
470 w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>' |
|
471 '<td class="tagged"> %s</td></tr>' % (xml_escape(jscall), |
|
472 subview)) |
|
473 else: |
|
474 w(u'<tr><td class="tagged">%s</td></tr>' % (subview)) |
|
475 w(u'</table>') |
|
476 else: |
|
477 w(_('no related entity')) |
|
478 if mayadd: |
|
479 req.add_js('jquery.autocomplete.js') |
|
480 req.add_css('jquery.autocomplete.css') |
|
481 multiple = self.rdef.role_cardinality(self.role) in '*+' |
|
482 w(u'<table><tr><td>') |
|
483 jscall = unicode(js.ajaxBoxShowSelector( |
|
484 self.__regid__, entity.eid, self.fname_vocabulary, |
|
485 self.fname_validate, self.added_msg and _(self.added_msg), |
|
486 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
|
487 multiple and self.separator)) |
|
488 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
|
489 xml_escape(jscall), |
|
490 multiple and _('add_relation') or _('update_relation'))) |
|
491 w(u'</td><td>') |
|
492 w(u'<div id="%sHolder"></div>' % self.domid) |
|
493 w(u'</td></tr></table>') |
|
494 |
|
495 |
|
496 # old contextual components, deprecated ######################################## |
|
497 |
|
498 class EntityVComponent(Component): |
|
499 """abstract base class for additinal components displayed in content |
|
500 headers and footer according to: |
|
501 |
|
502 * the displayed entity's type |
|
503 * a context (currently 'header' or 'footer') |
|
504 |
|
505 it should be configured using .accepts, .etype, .rtype, .target and |
|
506 .context class attributes |
|
507 """ |
|
508 __metaclass__ = class_deprecated |
|
509 __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)' |
|
510 |
|
511 __registry__ = 'ctxcomponents' |
|
512 __select__ = one_line_rset() |
|
513 |
|
514 cw_property_defs = { |
|
515 _('visible'): dict(type='Boolean', default=True, |
|
516 help=_('display the component or not')), |
|
517 _('order'): dict(type='Int', default=99, |
|
518 help=_('display order of the component')), |
|
519 _('context'): dict(type='String', default='navtop', |
|
520 vocabulary=(_('navtop'), _('navbottom'), |
|
521 _('navcontenttop'), _('navcontentbottom'), |
|
522 _('ctxtoolbar')), |
|
523 help=_('context where this component should be displayed')), |
|
524 } |
|
525 |
|
526 context = 'navcontentbottom' |
|
527 |
|
528 def call(self, view=None): |
|
529 if self.cw_rset is None: |
|
530 self.entity_call(self.cw_extra_kwargs.pop('entity')) |
|
531 else: |
|
532 self.cell_call(0, 0, view=view) |
|
533 |
|
534 def cell_call(self, row, col, view=None): |
|
535 self.entity_call(self.cw_rset.get_entity(row, col), view=view) |
|
536 |
|
537 def entity_call(self, entity, view=None): |
|
538 raise NotImplementedError() |
178 |
539 |
179 |
540 |
180 class RelatedObjectsVComponent(EntityVComponent): |
541 class RelatedObjectsVComponent(EntityVComponent): |
181 """a section to display some related entities""" |
542 """a section to display some related entities""" |
182 __select__ = EntityVComponent.__select__ & partial_has_related_entities() |
543 __select__ = EntityVComponent.__select__ & partial_has_related_entities() |