179 if start >= self.total: |
145 if start >= self.total: |
180 return self.no_next_page_link |
146 return self.no_next_page_link |
181 stop = start + self.page_size - 1 |
147 stop = start + self.page_size - 1 |
182 url = xml_escape(self.page_url(path, params, start, stop)) |
148 url = xml_escape(self.page_url(path, params, start, stop)) |
183 return self.next_page_link_templ % (url, title, content) |
149 return self.next_page_link_templ % (url, title, content) |
|
150 |
|
151 |
|
152 # new contextual components system ############################################# |
|
153 |
|
154 def override_ctx(cls, **kwargs): |
|
155 cwpdefs = cls.cw_property_defs.copy() |
|
156 cwpdefs['context'] = cwpdefs['context'].copy() |
|
157 cwpdefs['context'].update(kwargs) |
|
158 return cwpdefs |
|
159 |
|
160 |
|
161 class EmptyComponent(Exception): |
|
162 """some selectable component has actually no content and should not be |
|
163 rendered |
|
164 """ |
|
165 |
|
166 |
|
167 class Link(object): |
|
168 """a link to a view or action in the ui. |
|
169 |
|
170 Use this rather than `cw.web.htmlwidgets.BoxLink`. |
|
171 |
|
172 Note this class could probably be avoided with a proper DOM on the server |
|
173 side. |
|
174 """ |
|
175 newstyle = True |
|
176 |
|
177 def __init__(self, href, label, **attrs): |
|
178 self.href = href |
|
179 self.label = label |
|
180 self.attrs = attrs |
|
181 |
|
182 def __unicode__(self): |
|
183 return tags.a(self.label, href=self.href, **self.attrs) |
|
184 |
|
185 def render(self, w): |
|
186 w(tags.a(self.label, href=self.href, **self.attrs)) |
|
187 |
|
188 |
|
189 class Separator(object): |
|
190 """a menu separator. |
|
191 |
|
192 Use this rather than `cw.web.htmlwidgets.BoxSeparator`. |
|
193 """ |
|
194 newstyle = True |
|
195 |
|
196 def render(self, w): |
|
197 w(u'<hr class="boxSeparator"/>') |
|
198 |
|
199 |
|
200 def _bwcompatible_render_item(w, item): |
|
201 if hasattr(item, 'render'): |
|
202 if getattr(item, 'newstyle', False): |
|
203 if isinstance(item, Separator): |
|
204 w(u'</ul>') |
|
205 item.render(w) |
|
206 w(u'<ul>') |
|
207 else: |
|
208 w(u'<li>') |
|
209 item.render(w) |
|
210 w(u'</li>') |
|
211 else: |
|
212 item.render(w) # XXX displays <li> by itself |
|
213 else: |
|
214 w(u'<li>%s</li>' % item) |
|
215 |
|
216 |
|
217 class Layout(Component): |
|
218 __regid__ = 'layout' |
|
219 __abstract__ = True |
|
220 |
|
221 def init_rendering(self): |
|
222 """init view for rendering. Return true if we should go on, false |
|
223 if we should stop now. |
|
224 """ |
|
225 view = self.cw_extra_kwargs['view'] |
|
226 try: |
|
227 view.init_rendering() |
|
228 except Unauthorized, ex: |
|
229 self.warning("can't render %s: %s", view, ex) |
|
230 return False |
|
231 except EmptyComponent: |
|
232 return False |
|
233 return True |
|
234 |
|
235 |
|
236 class CtxComponent(AppObject): |
|
237 """base class for contextual components. The following contexts are |
|
238 predefined: |
|
239 |
|
240 * boxes: 'left', 'incontext', 'right' |
|
241 * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom' |
|
242 * other: 'ctxtoolbar' |
|
243 |
|
244 The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar' |
|
245 contexts are handled by the default primary view, others by the default main |
|
246 template. |
|
247 |
|
248 All subclasses may not support all those contexts (for instance if it can't |
|
249 be displayed as box, or as a toolbar icon). You may restrict allowed context |
|
250 as follows: |
|
251 |
|
252 .. sourcecode:: python |
|
253 |
|
254 class MyComponent(CtxComponent): |
|
255 cw_property_defs = override_ctx(CtxComponent, |
|
256 vocabulary=[list of contexts]) |
|
257 context = 'my default context' |
|
258 |
|
259 You can configure a component's default context by simply giving an |
|
260 appropriate value to the `context` class attribute, as seen above. |
|
261 """ |
|
262 __registry__ = 'ctxcomponents' |
|
263 __select__ = ~no_cnx() |
|
264 |
|
265 categories_in_order = () |
|
266 cw_property_defs = { |
|
267 _('visible'): dict(type='Boolean', default=True, |
|
268 help=_('display the box or not')), |
|
269 _('order'): dict(type='Int', default=99, |
|
270 help=_('display order of the box')), |
|
271 _('context'): dict(type='String', default='left', |
|
272 vocabulary=(_('left'), _('incontext'), _('right'), |
|
273 _('navtop'), _('navbottom'), |
|
274 _('navcontenttop'), _('navcontentbottom'), |
|
275 _('ctxtoolbar')), |
|
276 help=_('context where this component should be displayed')), |
|
277 } |
|
278 visible = True |
|
279 order = 0 |
|
280 context = 'left' |
|
281 contextual = False |
|
282 title = None |
|
283 |
|
284 # XXX support kwargs for compat with old boxes which gets the view as |
|
285 # argument |
|
286 def render(self, w, **kwargs): |
|
287 if hasattr(self, 'call'): |
|
288 warn('[3.10] should not anymore implement call on %s, see new CtxComponent api' |
|
289 % self.__class__, DeprecationWarning) |
|
290 self.w = w |
|
291 def wview(__vid, rset=None, __fallback_vid=None, **kwargs): |
|
292 self._cw.view(__vid, rset, __fallback_vid, w=self.w, **kwargs) |
|
293 self.wview = wview |
|
294 self.call(**kwargs) |
|
295 return |
|
296 getlayout = self._cw.vreg['components'].select |
|
297 layout = getlayout('layout', self._cw, **self.layout_select_args()) |
|
298 layout.render(w) |
|
299 |
|
300 def layout_select_args(self): |
|
301 try: |
|
302 # XXX ensure context is given when the component is reloaded through |
|
303 # ajax |
|
304 context = self.cw_extra_kwargs['context'] |
|
305 except KeyError: |
|
306 context = self.cw_propval('context') |
|
307 return dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col, |
|
308 view=self, context=context) |
|
309 |
|
310 def init_rendering(self): |
|
311 """init rendering callback: that's the good time to check your component |
|
312 has some content to display. If not, you can still raise |
|
313 :exc:`EmptyComponent` to inform it should be skipped. |
|
314 |
|
315 Also, :exc:`Unauthorized` will be catched, logged, then the component |
|
316 will be skipped. |
|
317 """ |
|
318 self.items = [] |
|
319 |
|
320 @property |
|
321 def domid(self): |
|
322 """return the HTML DOM identifier for this component""" |
|
323 return domid(self.__regid__) |
|
324 |
|
325 @property |
|
326 def cssclass(self): |
|
327 """return the CSS class name for this component""" |
|
328 return domid(self.__regid__) |
|
329 |
|
330 def render_title(self, w): |
|
331 """return the title for this component""" |
|
332 if self.title: |
|
333 w(self._cw._(self.title)) |
|
334 |
|
335 def render_body(self, w): |
|
336 """return the body (content) for this component""" |
|
337 raise NotImplementedError() |
|
338 |
|
339 def render_items(self, w, items=None, klass=u'boxListing'): |
|
340 if items is None: |
|
341 items = self.items |
|
342 assert items |
|
343 w(u'<ul class="%s">' % klass) |
|
344 for item in items: |
|
345 _bwcompatible_render_item(w, item) |
|
346 w(u'</ul>') |
|
347 |
|
348 def append(self, item): |
|
349 self.items.append(item) |
|
350 |
|
351 def action_link(self, action): |
|
352 return self.link(self._cw._(action.title), action.url()) |
|
353 |
|
354 def link(self, title, url, **kwargs): |
|
355 if self._cw.selected(url): |
|
356 try: |
|
357 kwargs['klass'] += ' selected' |
|
358 except KeyError: |
|
359 kwargs['klass'] = 'selected' |
|
360 return Link(url, title, **kwargs) |
|
361 |
|
362 def separator(self): |
|
363 return Separator() |
|
364 |
|
365 @deprecated('[3.10] use action_link() / link()') |
|
366 def box_action(self, action): # XXX action_link |
|
367 return self.build_link(self._cw._(action.title), action.url()) |
|
368 |
|
369 @deprecated('[3.10] use action_link() / link()') |
|
370 def build_link(self, title, url, **kwargs): |
|
371 if self._cw.selected(url): |
|
372 try: |
|
373 kwargs['klass'] += ' selected' |
|
374 except KeyError: |
|
375 kwargs['klass'] = 'selected' |
|
376 return tags.a(title, href=url, **kwargs) |
|
377 |
|
378 |
|
379 class EntityCtxComponent(CtxComponent): |
|
380 """base class for boxes related to a single entity""" |
|
381 __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset() |
|
382 context = 'incontext' |
|
383 contextual = True |
|
384 |
|
385 def __init__(self, *args, **kwargs): |
|
386 super(EntityCtxComponent, self).__init__(*args, **kwargs) |
|
387 try: |
|
388 entity = kwargs['entity'] |
|
389 except KeyError: |
|
390 entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0) |
|
391 self.entity = entity |
|
392 |
|
393 def layout_select_args(self): |
|
394 args = super(EntityCtxComponent, self).layout_select_args() |
|
395 args['entity'] = self.entity |
|
396 return args |
|
397 |
|
398 @property |
|
399 def domid(self): |
|
400 return domid(self.__regid__) + unicode(self.entity.eid) |
|
401 |
|
402 def lazy_view_holder(self, w, entity, oid, registry='views'): |
|
403 """add a holder and return an url that may be used to replace this |
|
404 holder by the html generate by the view specified by registry and |
|
405 identifier. Registry defaults to 'views'. |
|
406 """ |
|
407 holderid = '%sHolder' % self.domid |
|
408 w(u'<div id="%s"></div>' % holderid) |
|
409 params = self.cw_extra_kwargs.copy() |
|
410 params.pop('view', None) |
|
411 params.pop('entity', None) |
|
412 form = params.pop('formparams', {}) |
|
413 form['pageid'] = self._cw.pageid |
|
414 if entity.has_eid(): |
|
415 eid = entity.eid |
|
416 else: |
|
417 eid = None |
|
418 form['etype'] = entity.__regid__ |
|
419 form['tempEid'] = entity.eid |
|
420 args = [json_dumps(x) for x in (registry, oid, eid, params)] |
|
421 return self._cw.ajax_replace_url( |
|
422 holderid, fname='render', arg=args, **form) |
|
423 |
|
424 |
|
425 # high level abstract classes ################################################## |
|
426 |
|
427 class RQLCtxComponent(CtxComponent): |
|
428 """abstract box for boxes displaying the content of a rql query not related |
|
429 to the current result set. |
|
430 |
|
431 Notice that this class's init_rendering implemention is overwriting context |
|
432 result set (eg `cw_rset`) with the result set returned by execution of |
|
433 `to_display_rql()`. |
|
434 """ |
|
435 rql = None |
|
436 |
|
437 def to_display_rql(self): |
|
438 """return arguments to give to self._cw.execute, as a tuple, to build |
|
439 the result set to be displayed by this box. |
|
440 """ |
|
441 assert self.rql is not None, self.__regid__ |
|
442 return (self.rql,) |
|
443 |
|
444 def init_rendering(self): |
|
445 super(RQLCtxComponent, self).init_rendering() |
|
446 self.cw_rset = self._cw.execute(*self.to_display_rql()) |
|
447 if not self.cw_rset: |
|
448 raise EmptyComponent() |
|
449 |
|
450 def render_body(self, w): |
|
451 rset = self.cw_rset |
|
452 if len(rset[0]) == 2: |
|
453 items = [] |
|
454 for i, (eid, label) in enumerate(rset): |
|
455 entity = rset.get_entity(i, 0) |
|
456 items.append(self.link(label, entity.absolute_url())) |
|
457 else: |
|
458 items = [self.link(e.dc_title(), e.absolute_url()) |
|
459 for e in rset.entities()] |
|
460 self.render_items(w, items) |
|
461 |
|
462 |
|
463 class EditRelationMixIn(ReloadableMixIn): |
|
464 def box_item(self, entity, etarget, rql, label): |
|
465 """builds HTML link to edit relation between `entity` and `etarget`""" |
|
466 args = {role(self)[0] : entity.eid, target(self)[0] : etarget.eid} |
|
467 url = self._cw.user_rql_callback((rql, args)) |
|
468 # for each target, provide a link to edit the relation |
|
469 return u'[<a href="%s" class="action">%s</a>] %s' % ( |
|
470 xml_escape(url), label, etarget.view('incontext')) |
|
471 |
|
472 def related_boxitems(self, entity): |
|
473 rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
474 return [self.box_item(entity, etarget, rql, u'-') |
|
475 for etarget in self.related_entities(entity)] |
|
476 |
|
477 def related_entities(self, entity): |
|
478 return entity.related(self.rtype, role(self), entities=True) |
|
479 |
|
480 def unrelated_boxitems(self, entity): |
|
481 rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % self.rtype |
|
482 return [self.box_item(entity, etarget, rql, u'+') |
|
483 for etarget in self.unrelated_entities(entity)] |
|
484 |
|
485 def unrelated_entities(self, entity): |
|
486 """returns the list of unrelated entities, using the entity's |
|
487 appropriate vocabulary function |
|
488 """ |
|
489 skip = set(unicode(e.eid) for e in entity.related(self.rtype, role(self), |
|
490 entities=True)) |
|
491 skip.add(None) |
|
492 skip.add(INTERNAL_FIELD_VALUE) |
|
493 filteretype = getattr(self, 'etype', None) |
|
494 entities = [] |
|
495 form = self._cw.vreg['forms'].select('edition', self._cw, |
|
496 rset=self.cw_rset, |
|
497 row=self.cw_row or 0) |
|
498 field = form.field_by_name(self.rtype, role(self), entity.e_schema) |
|
499 for _, eid in field.vocabulary(form): |
|
500 if eid not in skip: |
|
501 entity = self._cw.entity_from_eid(eid) |
|
502 if filteretype is None or entity.__regid__ == filteretype: |
|
503 entities.append(entity) |
|
504 return entities |
|
505 |
|
506 # XXX should be a view usable using uicfg |
|
507 class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent): |
|
508 """base class for boxes which let add or remove entities linked by a given |
|
509 relation |
|
510 |
|
511 subclasses should define at least id, rtype and target class attributes. |
|
512 """ |
|
513 def render_title(self, w): |
|
514 w(display_name(self._cw, self.rtype, role(self), |
|
515 context=self.entity.__regid__)) |
|
516 |
|
517 def render_body(self, w): |
|
518 self._cw.add_js('cubicweb.ajax.js') |
|
519 related = self.related_boxitems(self.entity) |
|
520 unrelated = self.unrelated_boxitems(self.entity) |
|
521 self.items.extend(related) |
|
522 if related and unrelated: |
|
523 self.items.append(u'<hr class="boxSeparator"/>') |
|
524 self.items.extend(unrelated) |
|
525 self.render_items(w) |
|
526 |
|
527 |
|
528 class AjaxEditRelationCtxComponent(EntityCtxComponent): |
|
529 __select__ = EntityCtxComponent.__select__ & ( |
|
530 partial_relation_possible(action='add') | partial_has_related_entities()) |
|
531 |
|
532 # view used to display related entties |
|
533 item_vid = 'incontext' |
|
534 # values separator when multiple values are allowed |
|
535 separator = ',' |
|
536 # msgid of the message to display when some new relation has been added/removed |
|
537 added_msg = None |
|
538 removed_msg = None |
|
539 |
|
540 # class attributes below *must* be set in concret classes (additionaly to |
|
541 # rtype / role [/ target_etype]. They should correspond to js_* methods on |
|
542 # the json controller |
|
543 |
|
544 # function(eid) |
|
545 # -> expected to return a list of values to display as input selector |
|
546 # vocabulary |
|
547 fname_vocabulary = None |
|
548 |
|
549 # function(eid, value) |
|
550 # -> handle the selector's input (eg create necessary entities and/or |
|
551 # relations). If the relation is multiple, you'll get a list of value, else |
|
552 # a single string value. |
|
553 fname_validate = None |
|
554 |
|
555 # function(eid, linked entity eid) |
|
556 # -> remove the relation |
|
557 fname_remove = None |
|
558 |
|
559 def __init__(self, *args, **kwargs): |
|
560 super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs) |
|
561 self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype) |
|
562 |
|
563 def render_title(self, w): |
|
564 w(self.rdef.rtype.display_name(self._cw, self.role, |
|
565 context=self.entity.__regid__)) |
|
566 |
|
567 def render_body(self, w): |
|
568 req = self._cw |
|
569 entity = self.entity |
|
570 related = entity.related(self.rtype, self.role) |
|
571 if self.role == 'subject': |
|
572 mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid) |
|
573 maydel = self.rdef.has_perm(req, 'delete', fromeid=entity.eid) |
|
574 else: |
|
575 mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid) |
|
576 maydel = self.rdef.has_perm(req, 'delete', toeid=entity.eid) |
|
577 if mayadd or maydel: |
|
578 req.add_js(('jquery.ui.js', 'cubicweb.widgets.js')) |
|
579 req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js')) |
|
580 req.add_css('jquery.ui.css') |
|
581 _ = req._ |
|
582 if related: |
|
583 w(u'<table class="ajaxEditRelationTable">') |
|
584 for rentity in related.entities(): |
|
585 # for each related entity, provide a link to remove the relation |
|
586 subview = rentity.view(self.item_vid) |
|
587 if maydel: |
|
588 jscall = unicode(js.ajaxBoxRemoveLinkedEntity( |
|
589 self.__regid__, entity.eid, rentity.eid, |
|
590 self.fname_remove, |
|
591 self.removed_msg and _(self.removed_msg))) |
|
592 w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>' |
|
593 '<td class="entity"> %s</td></tr>' % (xml_escape(jscall), |
|
594 subview)) |
|
595 else: |
|
596 w(u'<tr><td class="entity">%s</td></tr>' % (subview)) |
|
597 w(u'</table>') |
|
598 else: |
|
599 w(_('no related entity')) |
|
600 if mayadd: |
|
601 multiple = self.rdef.role_cardinality(self.role) in '*+' |
|
602 w(u'<table><tr><td>') |
|
603 jscall = unicode(js.ajaxBoxShowSelector( |
|
604 self.__regid__, entity.eid, self.fname_vocabulary, |
|
605 self.fname_validate, self.added_msg and _(self.added_msg), |
|
606 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]), |
|
607 multiple and self.separator)) |
|
608 w('<a class="button sglink" href="javascript: %s">%s</a>' % ( |
|
609 xml_escape(jscall), |
|
610 multiple and _('add_relation') or _('update_relation'))) |
|
611 w(u'</td><td>') |
|
612 w(u'<div id="%sHolder"></div>' % self.domid) |
|
613 w(u'</td></tr></table>') |
|
614 |
|
615 |
|
616 class RelatedObjectsCtxComponent(EntityCtxComponent): |
|
617 """a contextual component to display entities related to another""" |
|
618 __select__ = EntityCtxComponent.__select__ & partial_has_related_entities() |
|
619 context = 'navcontentbottom' |
|
620 rtype = None |
|
621 role = 'subject' |
|
622 |
|
623 vid = 'list' |
|
624 |
|
625 def render_body(self, w): |
|
626 rset = self.entity.related(self.rtype, role(self)) |
|
627 self._cw.view(self.vid, rset, w=w) |
|
628 |
|
629 |
|
630 # old contextual components, deprecated ######################################## |
|
631 |
|
632 class EntityVComponent(Component): |
|
633 """abstract base class for additinal components displayed in content |
|
634 headers and footer according to: |
|
635 |
|
636 * the displayed entity's type |
|
637 * a context (currently 'header' or 'footer') |
|
638 |
|
639 it should be configured using .accepts, .etype, .rtype, .target and |
|
640 .context class attributes |
|
641 """ |
|
642 __metaclass__ = class_deprecated |
|
643 __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)' |
|
644 |
|
645 __registry__ = 'ctxcomponents' |
|
646 __select__ = one_line_rset() |
|
647 |
|
648 cw_property_defs = { |
|
649 _('visible'): dict(type='Boolean', default=True, |
|
650 help=_('display the component or not')), |
|
651 _('order'): dict(type='Int', default=99, |
|
652 help=_('display order of the component')), |
|
653 _('context'): dict(type='String', default='navtop', |
|
654 vocabulary=(_('navtop'), _('navbottom'), |
|
655 _('navcontenttop'), _('navcontentbottom'), |
|
656 _('ctxtoolbar')), |
|
657 help=_('context where this component should be displayed')), |
|
658 } |
|
659 |
|
660 context = 'navcontentbottom' |
|
661 |
|
662 def call(self, view=None): |
|
663 if self.cw_rset is None: |
|
664 self.entity_call(self.cw_extra_kwargs.pop('entity')) |
|
665 else: |
|
666 self.cell_call(0, 0, view=view) |
|
667 |
|
668 def cell_call(self, row, col, view=None): |
|
669 self.entity_call(self.cw_rset.get_entity(row, col), view=view) |
|
670 |
|
671 def entity_call(self, entity, view=None): |
|
672 raise NotImplementedError() |
184 |
673 |
185 |
674 |
186 class RelatedObjectsVComponent(EntityVComponent): |
675 class RelatedObjectsVComponent(EntityVComponent): |
187 """a section to display some related entities""" |
676 """a section to display some related entities""" |
188 __select__ = EntityVComponent.__select__ & partial_has_related_entities() |
677 __select__ = EntityVComponent.__select__ & partial_has_related_entities() |