|
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 """abstract component class and base components definition for CubicWeb web |
|
19 client |
|
20 """ |
|
21 |
|
22 __docformat__ = "restructuredtext en" |
|
23 from cubicweb import _ |
|
24 |
|
25 from warnings import warn |
|
26 |
|
27 from six import PY3, add_metaclass, text_type |
|
28 |
|
29 from logilab.common.deprecation import class_deprecated, class_renamed, deprecated |
|
30 from logilab.mtconverter import xml_escape |
|
31 |
|
32 from cubicweb import Unauthorized, role, target, tags |
|
33 from cubicweb.schema import display_name |
|
34 from cubicweb.uilib import js, domid |
|
35 from cubicweb.utils import json_dumps, js_href |
|
36 from cubicweb.view import ReloadableMixIn, Component |
|
37 from cubicweb.predicates import (no_cnx, paginated_rset, one_line_rset, |
|
38 non_final_entity, partial_relation_possible, |
|
39 partial_has_related_entities) |
|
40 from cubicweb.appobject import AppObject |
|
41 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs |
|
42 |
|
43 |
|
44 # abstract base class for navigation components ################################ |
|
45 |
|
46 class NavigationComponent(Component): |
|
47 """abstract base class for navigation components""" |
|
48 __regid__ = 'navigation' |
|
49 __select__ = paginated_rset() |
|
50 |
|
51 cw_property_defs = { |
|
52 _('visible'): dict(type='Boolean', default=True, |
|
53 help=_('display the component or not')), |
|
54 } |
|
55 |
|
56 page_size_property = 'navigation.page-size' |
|
57 start_param = '__start' |
|
58 stop_param = '__stop' |
|
59 page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>' |
|
60 selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>' |
|
61 previous_page_link_templ = next_page_link_templ = page_link_templ |
|
62 |
|
63 def __init__(self, req, rset, **kwargs): |
|
64 super(NavigationComponent, self).__init__(req, rset=rset, **kwargs) |
|
65 self.starting_from = 0 |
|
66 self.total = rset.rowcount |
|
67 |
|
68 def get_page_size(self): |
|
69 try: |
|
70 return self._page_size |
|
71 except AttributeError: |
|
72 page_size = self.cw_extra_kwargs.get('page_size') |
|
73 if page_size is None: |
|
74 if 'page_size' in self._cw.form: |
|
75 page_size = int(self._cw.form['page_size']) |
|
76 else: |
|
77 page_size = self._cw.property_value(self.page_size_property) |
|
78 self._page_size = page_size |
|
79 return page_size |
|
80 |
|
81 def set_page_size(self, page_size): |
|
82 self._page_size = page_size |
|
83 |
|
84 page_size = property(get_page_size, set_page_size) |
|
85 |
|
86 def page_boundaries(self): |
|
87 try: |
|
88 stop = int(self._cw.form[self.stop_param]) + 1 |
|
89 start = int(self._cw.form[self.start_param]) |
|
90 except KeyError: |
|
91 start, stop = 0, self.page_size |
|
92 if start >= len(self.cw_rset): |
|
93 start, stop = 0, self.page_size |
|
94 self.starting_from = start |
|
95 return start, stop |
|
96 |
|
97 def clean_params(self, params): |
|
98 if self.start_param in params: |
|
99 del params[self.start_param] |
|
100 if self.stop_param in params: |
|
101 del params[self.stop_param] |
|
102 |
|
103 def page_url(self, path, params, start=None, stop=None): |
|
104 params = dict(params) |
|
105 params['__fromnavigation'] = 1 |
|
106 if start is not None: |
|
107 params[self.start_param] = start |
|
108 if stop is not None: |
|
109 params[self.stop_param] = stop |
|
110 view = self.cw_extra_kwargs.get('view') |
|
111 if view is not None and hasattr(view, 'page_navigation_url'): |
|
112 url = view.page_navigation_url(self, path, params) |
|
113 elif path in ('json', 'ajax'): |
|
114 # 'ajax' is the new correct controller, but the old 'json' |
|
115 # controller should still be supported |
|
116 url = self.ajax_page_url(**params) |
|
117 else: |
|
118 url = self._cw.build_url(path, **params) |
|
119 # XXX hack to avoid opening a new page containing the evaluation of the |
|
120 # js expression on ajax call |
|
121 if url.startswith('javascript:'): |
|
122 url += '; $.noop();' |
|
123 return url |
|
124 |
|
125 def ajax_page_url(self, **params): |
|
126 divid = params.setdefault('divid', 'pageContent') |
|
127 params['rql'] = self.cw_rset.printable_rql() |
|
128 return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % ( |
|
129 json_dumps('#'+divid), js.ajaxFuncArgs('view', params))) |
|
130 |
|
131 def page_link(self, path, params, start, stop, content): |
|
132 url = xml_escape(self.page_url(path, params, start, stop)) |
|
133 if start == self.starting_from: |
|
134 return self.selected_page_link_templ % (url, content, content) |
|
135 return self.page_link_templ % (url, content, content) |
|
136 |
|
137 @property |
|
138 def prev_icon_url(self): |
|
139 return xml_escape(self._cw.data_url('go_prev.png')) |
|
140 |
|
141 @property |
|
142 def next_icon_url(self): |
|
143 return xml_escape(self._cw.data_url('go_next.png')) |
|
144 |
|
145 @property |
|
146 def no_previous_page_link(self): |
|
147 return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' % |
|
148 (self.prev_icon_url, self._cw._('there is no previous page'))) |
|
149 |
|
150 @property |
|
151 def no_next_page_link(self): |
|
152 return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' % |
|
153 (self.next_icon_url, self._cw._('there is no next page'))) |
|
154 |
|
155 @property |
|
156 def no_content_prev_link(self): |
|
157 return (u'<img src="%s" alt="%s" class="prevnext"/>' % ( |
|
158 (self.prev_icon_url, self._cw._('no content prev link')))) |
|
159 |
|
160 @property |
|
161 def no_content_next_link(self): |
|
162 return (u'<img src="%s" alt="%s" class="prevnext"/>' % |
|
163 (self.next_icon_url, self._cw._('no content next link'))) |
|
164 |
|
165 def previous_link(self, path, params, content=None, title=_('previous_results')): |
|
166 if not content: |
|
167 content = self.no_content_prev_link |
|
168 start = self.starting_from |
|
169 if not start : |
|
170 return self.no_previous_page_link |
|
171 start = max(0, start - self.page_size) |
|
172 stop = start + self.page_size - 1 |
|
173 url = xml_escape(self.page_url(path, params, start, stop)) |
|
174 return self.previous_page_link_templ % (url, self._cw._(title), content) |
|
175 |
|
176 def next_link(self, path, params, content=None, title=_('next_results')): |
|
177 if not content: |
|
178 content = self.no_content_next_link |
|
179 start = self.starting_from + self.page_size |
|
180 if start >= self.total: |
|
181 return self.no_next_page_link |
|
182 stop = start + self.page_size - 1 |
|
183 url = xml_escape(self.page_url(path, params, start, stop)) |
|
184 return self.next_page_link_templ % (url, self._cw._(title), content) |
|
185 |
|
186 |
|
187 # new contextual components system ############################################# |
|
188 |
|
189 def override_ctx(cls, **kwargs): |
|
190 cwpdefs = cls.cw_property_defs.copy() |
|
191 cwpdefs['context'] = cwpdefs['context'].copy() |
|
192 cwpdefs['context'].update(kwargs) |
|
193 return cwpdefs |
|
194 |
|
195 |
|
196 class EmptyComponent(Exception): |
|
197 """some selectable component has actually no content and should not be |
|
198 rendered |
|
199 """ |
|
200 |
|
201 |
|
202 class Link(object): |
|
203 """a link to a view or action in the ui. |
|
204 |
|
205 Use this rather than `cw.web.htmlwidgets.BoxLink`. |
|
206 |
|
207 Note this class could probably be avoided with a proper DOM on the server |
|
208 side. |
|
209 """ |
|
210 newstyle = True |
|
211 |
|
212 def __init__(self, href, label, **attrs): |
|
213 self.href = href |
|
214 self.label = label |
|
215 self.attrs = attrs |
|
216 |
|
217 def __unicode__(self): |
|
218 return tags.a(self.label, href=self.href, **self.attrs) |
|
219 |
|
220 if PY3: |
|
221 __str__ = __unicode__ |
|
222 |
|
223 def render(self, w): |
|
224 w(tags.a(self.label, href=self.href, **self.attrs)) |
|
225 |
|
226 def __repr__(self): |
|
227 return '<%s: href=%r label=%r %r>' % (self.__class__.__name__, |
|
228 self.href, self.label, self.attrs) |
|
229 |
|
230 |
|
231 class Separator(object): |
|
232 """a menu separator. |
|
233 |
|
234 Use this rather than `cw.web.htmlwidgets.BoxSeparator`. |
|
235 """ |
|
236 newstyle = True |
|
237 |
|
238 def render(self, w): |
|
239 w(u'<hr class="boxSeparator"/>') |
|
240 |
|
241 |
|
242 def _bwcompatible_render_item(w, item): |
|
243 if hasattr(item, 'render'): |
|
244 if getattr(item, 'newstyle', False): |
|
245 if isinstance(item, Separator): |
|
246 w(u'</ul>') |
|
247 item.render(w) |
|
248 w(u'<ul>') |
|
249 else: |
|
250 w(u'<li>') |
|
251 item.render(w) |
|
252 w(u'</li>') |
|
253 else: |
|
254 item.render(w) # XXX displays <li> by itself |
|
255 else: |
|
256 w(u'<li>%s</li>' % item) |
|
257 |
|
258 |
|
259 class Layout(Component): |
|
260 __regid__ = 'component_layout' |
|
261 __abstract__ = True |
|
262 |
|
263 def init_rendering(self): |
|
264 """init view for rendering. Return true if we should go on, false |
|
265 if we should stop now. |
|
266 """ |
|
267 view = self.cw_extra_kwargs['view'] |
|
268 try: |
|
269 view.init_rendering() |
|
270 except Unauthorized as ex: |
|
271 self.warning("can't render %s: %s", view, ex) |
|
272 return False |
|
273 except EmptyComponent: |
|
274 return False |
|
275 return True |
|
276 |
|
277 |
|
278 class LayoutableMixIn(object): |
|
279 layout_id = None # to be defined in concret class |
|
280 layout_args = {} |
|
281 |
|
282 def layout_render(self, w, **kwargs): |
|
283 getlayout = self._cw.vreg['components'].select |
|
284 layout = getlayout(self.layout_id, self._cw, **self.layout_select_args()) |
|
285 layout.render(w) |
|
286 |
|
287 def layout_select_args(self): |
|
288 args = dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col, |
|
289 view=self) |
|
290 args.update(self.layout_args) |
|
291 return args |
|
292 |
|
293 |
|
294 class CtxComponent(LayoutableMixIn, AppObject): |
|
295 """base class for contextual components. The following contexts are |
|
296 predefined: |
|
297 |
|
298 * boxes: 'left', 'incontext', 'right' |
|
299 * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' |
|
300 * other: 'ctxtoolbar' |
|
301 |
|
302 The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' |
|
303 contexts are handled by the default primary view, others by the default main |
|
304 template. |
|
305 |
|
306 All subclasses may not support all those contexts (for instance if it can't |
|
307 be displayed as box, or as a toolbar icon). You may restrict allowed context |
|
308 as follows: |
|
309 |
|
310 .. sourcecode:: python |
|
311 |
|
312 class MyComponent(CtxComponent): |
|
313 cw_property_defs = override_ctx(CtxComponent, |
|
314 vocabulary=[list of contexts]) |
|
315 context = 'my default context' |
|
316 |
|
317 You can configure a component's default context by simply giving an |
|
318 appropriate value to the `context` class attribute, as seen above. |
|
319 """ |
|
320 __registry__ = 'ctxcomponents' |
|
321 __select__ = ~no_cnx() |
|
322 |
|
323 categories_in_order = () |
|
324 cw_property_defs = { |
|
325 _('visible'): dict(type='Boolean', default=True, |
|
326 help=_('display the box or not')), |
|
327 _('order'): dict(type='Int', default=99, |
|
328 help=_('display order of the box')), |
|
329 _('context'): dict(type='String', default='left', |
|
330 vocabulary=(_('left'), _('incontext'), _('right'), |
|
331 _('navtop'), _('navbottom'), |
|
332 _('navcontenttop'), _('navcontentbottom'), |
|
333 _('ctxtoolbar')), |
|
334 help=_('context where this component should be displayed')), |
|
335 } |
|
336 visible = True |
|
337 order = 0 |
|
338 context = 'left' |
|
339 contextual = False |
|
340 title = None |
|
341 layout_id = 'component_layout' |
|
342 |
|
343 def render(self, w, **kwargs): |
|
344 self.layout_render(w, **kwargs) |
|
345 |
|
346 def layout_select_args(self): |
|
347 args = super(CtxComponent, self).layout_select_args() |
|
348 try: |
|
349 # XXX ensure context is given when the component is reloaded through |
|
350 # ajax |
|
351 args['context'] = self.cw_extra_kwargs['context'] |
|
352 except KeyError: |
|
353 args['context'] = self.cw_propval('context') |
|
354 return args |
|
355 |
|
356 def init_rendering(self): |
|
357 """init rendering callback: that's the good time to check your component |
|
358 has some content to display. If not, you can still raise |
|
359 :exc:`EmptyComponent` to inform it should be skipped. |
|
360 |
|
361 Also, :exc:`Unauthorized` will be caught, logged, then the component |
|
362 will be skipped. |
|
363 """ |
|
364 self.items = [] |
|
365 |
|
366 @property |
|
367 def domid(self): |
|
368 """return the HTML DOM identifier for this component""" |
|
369 return domid(self.__regid__) |
|
370 |
|
371 @property |
|
372 def cssclass(self): |
|
373 """return the CSS class name for this component""" |
|
374 return domid(self.__regid__) |
|
375 |
|
376 def render_title(self, w): |
|
377 """return the title for this component""" |
|
378 if self.title: |
|
379 w(self._cw._(self.title)) |
|
380 |
|
381 def render_body(self, w): |
|
382 """return the body (content) for this component""" |
|
383 raise NotImplementedError() |
|
384 |
|
385 def render_items(self, w, items=None, klass=u'boxListing'): |
|
386 if items is None: |
|
387 items = self.items |
|
388 assert items |
|
389 w(u'<ul class="%s">' % klass) |
|
390 for item in items: |
|
391 _bwcompatible_render_item(w, item) |
|
392 w(u'</ul>') |
|
393 |
|
394 def append(self, item): |
|
395 self.items.append(item) |
|
396 |
|
397 def action_link(self, action): |
|
398 return self.link(self._cw._(action.title), action.url()) |
|
399 |
|
400 def link(self, title, url, **kwargs): |
|
401 if self._cw.selected(url): |
|
402 try: |
|
403 kwargs['klass'] += ' selected' |
|
404 except KeyError: |
|
405 kwargs['klass'] = 'selected' |
|
406 return Link(url, title, **kwargs) |
|
407 |
|
408 def separator(self): |
|
409 return Separator() |
|
410 |
|
411 |
|
412 class EntityCtxComponent(CtxComponent): |
|
413 """base class for boxes related to a single entity""" |
|
414 __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset() |
|
415 context = 'incontext' |
|
416 contextual = True |
|
417 |
|
418 def __init__(self, *args, **kwargs): |
|
419 super(EntityCtxComponent, self).__init__(*args, **kwargs) |
|
420 try: |
|
421 entity = kwargs['entity'] |
|
422 except KeyError: |
|
423 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
424 self.entity = entity |
|
425 |
|
426 def layout_select_args(self): |
|
427 args = super(EntityCtxComponent, self).layout_select_args() |
|
428 args['entity'] = self.entity |
|
429 return args |
|
430 |
|
431 @property |
|
432 def domid(self): |
|
433 return domid(self.__regid__) + text_type(self.entity.eid) |
|
434 |
|
435 def lazy_view_holder(self, w, entity, oid, registry='views'): |
|
436 """add a holder and return a URL that may be used to replace this |
|
437 holder by the html generate by the view specified by registry and |
|
438 identifier. Registry defaults to 'views'. |
|
439 """ |
|
440 holderid = '%sHolder' % self.domid |
|
441 w(u'<div id="%s"></div>' % holderid) |
|
442 params = self.cw_extra_kwargs.copy() |
|
443 params.pop('view', None) |
|
444 params.pop('entity', None) |
|
445 form = params.pop('formparams', {}) |
|
446 if entity.has_eid(): |
|
447 eid = entity.eid |
|
448 else: |
|
449 eid = None |
|
450 form['etype'] = entity.cw_etype |
|
451 form['tempEid'] = entity.eid |
|
452 args = [json_dumps(x) for x in (registry, oid, eid, params)] |
|
453 return self._cw.ajax_replace_url( |
|
454 holderid, fname='render', arg=args, **form) |
|
455 |
|
456 |
|
457 # high level abstract classes ################################################## |
|
458 |
|
459 class RQLCtxComponent(CtxComponent): |
|
460 """abstract box for boxes displaying the content of a rql query not related |
|
461 to the current result set. |
|
462 |
|
463 Notice that this class's init_rendering implemention is overwriting context |
|
464 result set (eg `cw_rset`) with the result set returned by execution of |
|
465 `to_display_rql()`. |
|
466 """ |
|
467 rql = None |
|
468 |
|
469 def to_display_rql(self): |
|
470 """return arguments to give to self._cw.execute, as a tuple, to build |
|
471 the result set to be displayed by this box. |
|
472 """ |
|
473 assert self.rql is not None, self.__regid__ |
|
474 return (self.rql,) |
|
475 |
|
476 def init_rendering(self): |
|
477 super(RQLCtxComponent, self).init_rendering() |
|
478 self.cw_rset = self._cw.execute(*self.to_display_rql()) |
|
479 if not self.cw_rset: |
|
480 raise EmptyComponent() |
|
481 |
|
482 def render_body(self, w): |
|
483 rset = self.cw_rset |
|
484 if len(rset[0]) == 2: |
|
485 items = [] |
|
486 for i, (eid, label) in enumerate(rset): |
|
487 entity = rset.get_entity(i, 0) |
|
488 items.append(self.link(label, entity.absolute_url())) |
|
489 else: |
|
490 items = [self.link(e.dc_title(), e.absolute_url()) |
|
491 for e in rset.entities()] |
|
492 self.render_items(w, items) |
|
493 |
|
494 |
|
495 class EditRelationMixIn(ReloadableMixIn): |
|
496 |
|
497 def box_item(self, entity, etarget, fname, label): |
|
498 """builds HTML link to edit relation between `entity` and `etarget`""" |
|
499 args = {role(self) : entity.eid, target(self): etarget.eid} |
|
500 # for each target, provide a link to edit the relation |
|
501 jscall = js.cw.utils.callAjaxFuncThenReload(fname, |
|
502 self.rtype, |
|
503 args['subject'], |
|
504 args['object']) |
|
505 return u'[<a href="javascript: %s" class="action">%s</a>] %s' % ( |
|
506 xml_escape(text_type(jscall)), label, etarget.view('incontext')) |
|
507 |
|
508 def related_boxitems(self, entity): |
|
509 return [self.box_item(entity, etarget, 'delete_relation', u'-') |
|
510 for etarget in self.related_entities(entity)] |
|
511 |
|
512 def related_entities(self, entity): |
|
513 return entity.related(self.rtype, role(self), entities=True) |
|
514 |
|
515 def unrelated_boxitems(self, entity): |
|
516 return [self.box_item(entity, etarget, 'add_relation', u'+') |
|
517 for etarget in self.unrelated_entities(entity)] |
|
518 |
|
519 def unrelated_entities(self, entity): |
|
520 """returns the list of unrelated entities, using the entity's |
|
521 appropriate vocabulary function |
|
522 """ |
|
523 skip = set(text_type(e.eid) for e in entity.related(self.rtype, role(self), |
|
524 entities=True)) |
|
525 skip.add(None) |
|
526 skip.add(INTERNAL_FIELD_VALUE) |
|
527 filteretype = getattr(self, 'etype', None) |
|
528 entities = [] |
|
529 form = self._cw.vreg['forms'].select('edition', self._cw, |
|
530 rset=self.cw_rset, |
|
531 row=self.cw_row or 0) |
|
532 field = form.field_by_name(self.rtype, role(self), entity.e_schema) |
|
533 for _, eid in field.vocabulary(form): |
|
534 if eid not in skip: |
|
535 entity = self._cw.entity_from_eid(eid) |
|
536 if filteretype is None or entity.cw_etype == filteretype: |
|
537 entities.append(entity) |
|
538 return entities |
|
539 |
|
540 # XXX should be a view usable using uicfg |
|
541 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent): |
|
542 """base class for boxes which let add or remove entities linked by a given |
|
543 relation |
|
544 |
|
545 subclasses should define at least id, rtype and target class attributes. |
|
546 """ |
|
547 # to be defined in concrete classes |
|
548 rtype = None |
|
549 |
|
550 def render_title(self, w): |
|
551 w(display_name(self._cw, self.rtype, role(self), |
|
552 context=self.entity.cw_etype)) |
|
553 |
|
554 def render_body(self, w): |
|
555 self._cw.add_js('cubicweb.ajax.js') |
|
556 related = self.related_boxitems(self.entity) |
|
557 unrelated = self.unrelated_boxitems(self.entity) |
|
558 self.items.extend(related) |
|
559 if related and unrelated: |
|
560 self.items.append(u'<hr class="boxSeparator"/>') |
|
561 self.items.extend(unrelated) |
|
562 self.render_items(w) |
|
563 |
|
564 |
|
565 class AjaxEditRelationCtxComponent(EntityCtxComponent): |
|
566 __select__ = EntityCtxComponent.__select__ & ( |
|
567 partial_relation_possible(action='add') | partial_has_related_entities()) |
|
568 |
|
569 # view used to display related entties |
|
570 item_vid = 'incontext' |
|
571 # values separator when multiple values are allowed |
|
572 separator = ',' |
|
573 # msgid of the message to display when some new relation has been added/removed |
|
574 added_msg = None |
|
575 removed_msg = None |
|
576 |
|
577 # to be defined in concrete classes |
|
578 rtype = role = target_etype = None |
|
579 # class attributes below *must* be set in concrete classes (additionally to |
|
580 # rtype / role [/ target_etype]. They should correspond to js_* methods on |
|
581 # the json controller |
|
582 |
|
583 # function(eid) |
|
584 # -> expected to return a list of values to display as input selector |
|
585 # vocabulary |
|
586 fname_vocabulary = None |
|
587 |
|
588 # function(eid, value) |
|
589 # -> handle the selector's input (eg create necessary entities and/or |
|
590 # relations). If the relation is multiple, you'll get a list of value, else |
|
591 # a single string value. |
|
592 fname_validate = None |
|
593 |
|
594 # function(eid, linked entity eid) |
|
595 # -> remove the relation |
|
596 fname_remove = None |
|
597 |
|
598 def __init__(self, *args, **kwargs): |
|
599 super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs) |
|
600 self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype) |
|
601 |
|
602 def render_title(self, w): |
|
603 w(self.rdef.rtype.display_name(self._cw, self.role, |
|
604 context=self.entity.cw_etype)) |
|
605 |
|
606 def add_js_css(self): |
|
607 self._cw.add_js(('jquery.ui.js', 'cubicweb.widgets.js')) |
|
608 self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
|
609 self._cw.add_css('jquery.ui.css') |
|
610 return True |
|
611 |
|
612 def render_body(self, w): |
|
613 req = self._cw |
|
614 entity = self.entity |
|
615 related = entity.related(self.rtype, self.role) |
|
616 if self.role == 'subject': |
|
617 mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) |
|
618 else: |
|
619 mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) |
|
620 js_css_added = False |
|
621 if mayadd: |
|
622 js_css_added = self.add_js_css() |
|
623 _ = req._ |
|
624 if related: |
|
625 maydel = None |
|
626 w(u'<table class="ajaxEditRelationTable">') |
|
627 for rentity in related.entities(): |
|
628 if maydel is None: |
|
629 # Only check permission for the first related. |
|
630 if self.role == 'subject': |
|
631 fromeid, toeid = entity.eid, rentity.eid |
|
632 else: |
|
633 fromeid, toeid = rentity.eid, entity.eid |
|
634 maydel = self.rdef.has_perm( |
|
635 req, 'delete', fromeid=fromeid, toeid=toeid) |
|
636 # for each related entity, provide a link to remove the relation |
|
637 subview = rentity.view(self.item_vid) |
|
638 if maydel: |
|
639 if not js_css_added: |
|
640 js_css_added = self.add_js_css() |
|
641 jscall = text_type(js.ajaxBoxRemoveLinkedEntity( |
|
642 self.__regid__, entity.eid, rentity.eid, |
|
643 self.fname_remove, |
|
644 self.removed_msg and _(self.removed_msg))) |
|
645 w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>' |
|
646 '<td class="entity"> %s</td></tr>' % (xml_escape(jscall), |
|
647 subview)) |
|
648 else: |
|
649 w(u'<tr><td class="entity">%s</td></tr>' % (subview)) |
|
650 w(u'</table>') |
|
651 else: |
|
652 w(_('no related entity')) |
|
653 if mayadd: |
|
654 multiple = self.rdef.role_cardinality(self.role) in '*+' |
|
655 w(u'<table><tr><td>') |
|
656 jscall = text_type(js.ajaxBoxShowSelector( |
|
657 self.__regid__, entity.eid, self.fname_vocabulary, |
|
658 self.fname_validate, self.added_msg and _(self.added_msg), |
|
659 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
|
660 multiple and self.separator)) |
|
661 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
|
662 xml_escape(jscall), |
|
663 multiple and _('add_relation') or _('update_relation'))) |
|
664 w(u'</td><td>') |
|
665 w(u'<div id="%sHolder"></div>' % self.domid) |
|
666 w(u'</td></tr></table>') |
|
667 |
|
668 |
|
669 class RelatedObjectsCtxComponent(EntityCtxComponent): |
|
670 """a contextual component to display entities related to another""" |
|
671 __select__ = EntityCtxComponent.__select__ & partial_has_related_entities() |
|
672 context = 'navcontentbottom' |
|
673 rtype = None |
|
674 role = 'subject' |
|
675 |
|
676 vid = 'list' |
|
677 |
|
678 def render_body(self, w): |
|
679 rset = self.entity.related(self.rtype, role(self)) |
|
680 self._cw.view(self.vid, rset, w=w) |
|
681 |
|
682 |
|
683 # old contextual components, deprecated ######################################## |
|
684 |
|
685 @add_metaclass(class_deprecated) |
|
686 class EntityVComponent(Component): |
|
687 """abstract base class for additinal components displayed in content |
|
688 headers and footer according to: |
|
689 |
|
690 * the displayed entity's type |
|
691 * a context (currently 'header' or 'footer') |
|
692 |
|
693 it should be configured using .accepts, .etype, .rtype, .target and |
|
694 .context class attributes |
|
695 """ |
|
696 __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)' |
|
697 |
|
698 __registry__ = 'ctxcomponents' |
|
699 __select__ = one_line_rset() |
|
700 |
|
701 cw_property_defs = { |
|
702 _('visible'): dict(type='Boolean', default=True, |
|
703 help=_('display the component or not')), |
|
704 _('order'): dict(type='Int', default=99, |
|
705 help=_('display order of the component')), |
|
706 _('context'): dict(type='String', default='navtop', |
|
707 vocabulary=(_('navtop'), _('navbottom'), |
|
708 _('navcontenttop'), _('navcontentbottom'), |
|
709 _('ctxtoolbar')), |
|
710 help=_('context where this component should be displayed')), |
|
711 } |
|
712 |
|
713 context = 'navcontentbottom' |
|
714 |
|
715 def call(self, view=None): |
|
716 if self.cw_rset is None: |
|
717 self.entity_call(self.cw_extra_kwargs.pop('entity')) |
|
718 else: |
|
719 self.cell_call(0, 0, view=view) |
|
720 |
|
721 def cell_call(self, row, col, view=None): |
|
722 self.entity_call(self.cw_rset.get_entity(row, col), view=view) |
|
723 |
|
724 def entity_call(self, entity, view=None): |
|
725 raise NotImplementedError() |
|
726 |
|
727 class RelatedObjectsVComponent(EntityVComponent): |
|
728 """a section to display some related entities""" |
|
729 __select__ = EntityVComponent.__select__ & partial_has_related_entities() |
|
730 |
|
731 vid = 'list' |
|
732 # to be defined in concrete classes |
|
733 rtype = title = None |
|
734 |
|
735 def rql(self): |
|
736 """override this method if you want to use a custom rql query""" |
|
737 return None |
|
738 |
|
739 def cell_call(self, row, col, view=None): |
|
740 rql = self.rql() |
|
741 if rql is None: |
|
742 entity = self.cw_rset.get_entity(row, col) |
|
743 rset = entity.related(self.rtype, role(self)) |
|
744 else: |
|
745 eid = self.cw_rset[row][col] |
|
746 rset = self._cw.execute(self.rql(), {'x': eid}) |
|
747 if not rset.rowcount: |
|
748 return |
|
749 self.w(u'<div class="%s">' % self.cssclass) |
|
750 self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize()) |
|
751 self.wview(self.vid, rset) |
|
752 self.w(u'</div>') |