--- a/web/component.py Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,752 +0,0 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""abstract component class and base components definition for CubicWeb web
-client
-"""
-
-__docformat__ = "restructuredtext en"
-from cubicweb import _
-
-from warnings import warn
-
-from six import PY3, add_metaclass, text_type
-
-from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
-from logilab.mtconverter import xml_escape
-
-from cubicweb import Unauthorized, role, target, tags
-from cubicweb.schema import display_name
-from cubicweb.uilib import js, domid
-from cubicweb.utils import json_dumps, js_href
-from cubicweb.view import ReloadableMixIn, Component
-from cubicweb.predicates import (no_cnx, paginated_rset, one_line_rset,
- non_final_entity, partial_relation_possible,
- partial_has_related_entities)
-from cubicweb.appobject import AppObject
-from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
-
-
-# abstract base class for navigation components ################################
-
-class NavigationComponent(Component):
- """abstract base class for navigation components"""
- __regid__ = 'navigation'
- __select__ = paginated_rset()
-
- cw_property_defs = {
- _('visible'): dict(type='Boolean', default=True,
- help=_('display the component or not')),
- }
-
- page_size_property = 'navigation.page-size'
- start_param = '__start'
- stop_param = '__stop'
- page_link_templ = u'<span class="slice"><a href="%s" title="%s">%s</a></span>'
- selected_page_link_templ = u'<span class="selectedSlice"><a href="%s" title="%s">%s</a></span>'
- previous_page_link_templ = next_page_link_templ = page_link_templ
-
- def __init__(self, req, rset, **kwargs):
- super(NavigationComponent, self).__init__(req, rset=rset, **kwargs)
- self.starting_from = 0
- self.total = rset.rowcount
-
- def get_page_size(self):
- try:
- return self._page_size
- except AttributeError:
- page_size = self.cw_extra_kwargs.get('page_size')
- if page_size is None:
- if 'page_size' in self._cw.form:
- page_size = int(self._cw.form['page_size'])
- else:
- page_size = self._cw.property_value(self.page_size_property)
- self._page_size = page_size
- return page_size
-
- def set_page_size(self, page_size):
- self._page_size = page_size
-
- page_size = property(get_page_size, set_page_size)
-
- def page_boundaries(self):
- try:
- stop = int(self._cw.form[self.stop_param]) + 1
- start = int(self._cw.form[self.start_param])
- except KeyError:
- start, stop = 0, self.page_size
- if start >= len(self.cw_rset):
- start, stop = 0, self.page_size
- self.starting_from = start
- return start, stop
-
- def clean_params(self, params):
- if self.start_param in params:
- del params[self.start_param]
- if self.stop_param in params:
- del params[self.stop_param]
-
- def page_url(self, path, params, start=None, stop=None):
- params = dict(params)
- params['__fromnavigation'] = 1
- if start is not None:
- params[self.start_param] = start
- if stop is not None:
- params[self.stop_param] = stop
- view = self.cw_extra_kwargs.get('view')
- if view is not None and hasattr(view, 'page_navigation_url'):
- url = view.page_navigation_url(self, path, params)
- elif path in ('json', 'ajax'):
- # 'ajax' is the new correct controller, but the old 'json'
- # controller should still be supported
- url = self.ajax_page_url(**params)
- else:
- url = self._cw.build_url(path, **params)
- # XXX hack to avoid opening a new page containing the evaluation of the
- # js expression on ajax call
- if url.startswith('javascript:'):
- url += '; $.noop();'
- return url
-
- def ajax_page_url(self, **params):
- divid = params.setdefault('divid', 'pageContent')
- params['rql'] = self.cw_rset.printable_rql()
- return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % (
- json_dumps('#'+divid), js.ajaxFuncArgs('view', params)))
-
- def page_link(self, path, params, start, stop, content):
- url = xml_escape(self.page_url(path, params, start, stop))
- if start == self.starting_from:
- return self.selected_page_link_templ % (url, content, content)
- return self.page_link_templ % (url, content, content)
-
- @property
- def prev_icon_url(self):
- return xml_escape(self._cw.data_url('go_prev.png'))
-
- @property
- def next_icon_url(self):
- return xml_escape(self._cw.data_url('go_next.png'))
-
- @property
- def no_previous_page_link(self):
- return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
- (self.prev_icon_url, self._cw._('there is no previous page')))
-
- @property
- def no_next_page_link(self):
- return (u'<img src="%s" alt="%s" class="prevnext_nogo"/>' %
- (self.next_icon_url, self._cw._('there is no next page')))
-
- @property
- def no_content_prev_link(self):
- return (u'<img src="%s" alt="%s" class="prevnext"/>' % (
- (self.prev_icon_url, self._cw._('no content prev link'))))
-
- @property
- def no_content_next_link(self):
- return (u'<img src="%s" alt="%s" class="prevnext"/>' %
- (self.next_icon_url, self._cw._('no content next link')))
-
- def previous_link(self, path, params, content=None, title=_('previous_results')):
- if not content:
- content = self.no_content_prev_link
- start = self.starting_from
- if not start :
- return self.no_previous_page_link
- start = max(0, start - self.page_size)
- stop = start + self.page_size - 1
- url = xml_escape(self.page_url(path, params, start, stop))
- return self.previous_page_link_templ % (url, self._cw._(title), content)
-
- def next_link(self, path, params, content=None, title=_('next_results')):
- if not content:
- content = self.no_content_next_link
- start = self.starting_from + self.page_size
- if start >= self.total:
- return self.no_next_page_link
- stop = start + self.page_size - 1
- url = xml_escape(self.page_url(path, params, start, stop))
- return self.next_page_link_templ % (url, self._cw._(title), content)
-
-
-# new contextual components system #############################################
-
-def override_ctx(cls, **kwargs):
- cwpdefs = cls.cw_property_defs.copy()
- cwpdefs['context'] = cwpdefs['context'].copy()
- cwpdefs['context'].update(kwargs)
- return cwpdefs
-
-
-class EmptyComponent(Exception):
- """some selectable component has actually no content and should not be
- rendered
- """
-
-
-class Link(object):
- """a link to a view or action in the ui.
-
- Use this rather than `cw.web.htmlwidgets.BoxLink`.
-
- Note this class could probably be avoided with a proper DOM on the server
- side.
- """
- newstyle = True
-
- def __init__(self, href, label, **attrs):
- self.href = href
- self.label = label
- self.attrs = attrs
-
- def __unicode__(self):
- return tags.a(self.label, href=self.href, **self.attrs)
-
- if PY3:
- __str__ = __unicode__
-
- def render(self, w):
- w(tags.a(self.label, href=self.href, **self.attrs))
-
- def __repr__(self):
- return '<%s: href=%r label=%r %r>' % (self.__class__.__name__,
- self.href, self.label, self.attrs)
-
-
-class Separator(object):
- """a menu separator.
-
- Use this rather than `cw.web.htmlwidgets.BoxSeparator`.
- """
- newstyle = True
-
- def render(self, w):
- w(u'<hr class="boxSeparator"/>')
-
-
-def _bwcompatible_render_item(w, item):
- if hasattr(item, 'render'):
- if getattr(item, 'newstyle', False):
- if isinstance(item, Separator):
- w(u'</ul>')
- item.render(w)
- w(u'<ul>')
- else:
- w(u'<li>')
- item.render(w)
- w(u'</li>')
- else:
- item.render(w) # XXX displays <li> by itself
- else:
- w(u'<li>%s</li>' % item)
-
-
-class Layout(Component):
- __regid__ = 'component_layout'
- __abstract__ = True
-
- def init_rendering(self):
- """init view for rendering. Return true if we should go on, false
- if we should stop now.
- """
- view = self.cw_extra_kwargs['view']
- try:
- view.init_rendering()
- except Unauthorized as ex:
- self.warning("can't render %s: %s", view, ex)
- return False
- except EmptyComponent:
- return False
- return True
-
-
-class LayoutableMixIn(object):
- layout_id = None # to be defined in concret class
- layout_args = {}
-
- def layout_render(self, w, **kwargs):
- getlayout = self._cw.vreg['components'].select
- layout = getlayout(self.layout_id, self._cw, **self.layout_select_args())
- layout.render(w)
-
- def layout_select_args(self):
- args = dict(rset=self.cw_rset, row=self.cw_row, col=self.cw_col,
- view=self)
- args.update(self.layout_args)
- return args
-
-
-class CtxComponent(LayoutableMixIn, AppObject):
- """base class for contextual components. The following contexts are
- predefined:
-
- * boxes: 'left', 'incontext', 'right'
- * section: 'navcontenttop', 'navcontentbottom', 'navtop', 'navbottom'
- * other: 'ctxtoolbar'
-
- The 'incontext', 'navcontenttop', 'navcontentbottom' and 'ctxtoolbar'
- contexts are handled by the default primary view, others by the default main
- template.
-
- All subclasses may not support all those contexts (for instance if it can't
- be displayed as box, or as a toolbar icon). You may restrict allowed context
- as follows:
-
- .. sourcecode:: python
-
- class MyComponent(CtxComponent):
- cw_property_defs = override_ctx(CtxComponent,
- vocabulary=[list of contexts])
- context = 'my default context'
-
- You can configure a component's default context by simply giving an
- appropriate value to the `context` class attribute, as seen above.
- """
- __registry__ = 'ctxcomponents'
- __select__ = ~no_cnx()
-
- categories_in_order = ()
- cw_property_defs = {
- _('visible'): dict(type='Boolean', default=True,
- help=_('display the box or not')),
- _('order'): dict(type='Int', default=99,
- help=_('display order of the box')),
- _('context'): dict(type='String', default='left',
- vocabulary=(_('left'), _('incontext'), _('right'),
- _('navtop'), _('navbottom'),
- _('navcontenttop'), _('navcontentbottom'),
- _('ctxtoolbar')),
- help=_('context where this component should be displayed')),
- }
- visible = True
- order = 0
- context = 'left'
- contextual = False
- title = None
- layout_id = 'component_layout'
-
- def render(self, w, **kwargs):
- self.layout_render(w, **kwargs)
-
- def layout_select_args(self):
- args = super(CtxComponent, self).layout_select_args()
- try:
- # XXX ensure context is given when the component is reloaded through
- # ajax
- args['context'] = self.cw_extra_kwargs['context']
- except KeyError:
- args['context'] = self.cw_propval('context')
- return args
-
- def init_rendering(self):
- """init rendering callback: that's the good time to check your component
- has some content to display. If not, you can still raise
- :exc:`EmptyComponent` to inform it should be skipped.
-
- Also, :exc:`Unauthorized` will be caught, logged, then the component
- will be skipped.
- """
- self.items = []
-
- @property
- def domid(self):
- """return the HTML DOM identifier for this component"""
- return domid(self.__regid__)
-
- @property
- def cssclass(self):
- """return the CSS class name for this component"""
- return domid(self.__regid__)
-
- def render_title(self, w):
- """return the title for this component"""
- if self.title:
- w(self._cw._(self.title))
-
- def render_body(self, w):
- """return the body (content) for this component"""
- raise NotImplementedError()
-
- def render_items(self, w, items=None, klass=u'boxListing'):
- if items is None:
- items = self.items
- assert items
- w(u'<ul class="%s">' % klass)
- for item in items:
- _bwcompatible_render_item(w, item)
- w(u'</ul>')
-
- def append(self, item):
- self.items.append(item)
-
- def action_link(self, action):
- return self.link(self._cw._(action.title), action.url())
-
- def link(self, title, url, **kwargs):
- if self._cw.selected(url):
- try:
- kwargs['klass'] += ' selected'
- except KeyError:
- kwargs['klass'] = 'selected'
- return Link(url, title, **kwargs)
-
- def separator(self):
- return Separator()
-
-
-class EntityCtxComponent(CtxComponent):
- """base class for boxes related to a single entity"""
- __select__ = CtxComponent.__select__ & non_final_entity() & one_line_rset()
- context = 'incontext'
- contextual = True
-
- def __init__(self, *args, **kwargs):
- super(EntityCtxComponent, self).__init__(*args, **kwargs)
- try:
- entity = kwargs['entity']
- except KeyError:
- entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
- self.entity = entity
-
- def layout_select_args(self):
- args = super(EntityCtxComponent, self).layout_select_args()
- args['entity'] = self.entity
- return args
-
- @property
- def domid(self):
- return domid(self.__regid__) + text_type(self.entity.eid)
-
- def lazy_view_holder(self, w, entity, oid, registry='views'):
- """add a holder and return a URL that may be used to replace this
- holder by the html generate by the view specified by registry and
- identifier. Registry defaults to 'views'.
- """
- holderid = '%sHolder' % self.domid
- w(u'<div id="%s"></div>' % holderid)
- params = self.cw_extra_kwargs.copy()
- params.pop('view', None)
- params.pop('entity', None)
- form = params.pop('formparams', {})
- if entity.has_eid():
- eid = entity.eid
- else:
- eid = None
- form['etype'] = entity.cw_etype
- form['tempEid'] = entity.eid
- args = [json_dumps(x) for x in (registry, oid, eid, params)]
- return self._cw.ajax_replace_url(
- holderid, fname='render', arg=args, **form)
-
-
-# high level abstract classes ##################################################
-
-class RQLCtxComponent(CtxComponent):
- """abstract box for boxes displaying the content of a rql query not related
- to the current result set.
-
- Notice that this class's init_rendering implemention is overwriting context
- result set (eg `cw_rset`) with the result set returned by execution of
- `to_display_rql()`.
- """
- rql = None
-
- def to_display_rql(self):
- """return arguments to give to self._cw.execute, as a tuple, to build
- the result set to be displayed by this box.
- """
- assert self.rql is not None, self.__regid__
- return (self.rql,)
-
- def init_rendering(self):
- super(RQLCtxComponent, self).init_rendering()
- self.cw_rset = self._cw.execute(*self.to_display_rql())
- if not self.cw_rset:
- raise EmptyComponent()
-
- def render_body(self, w):
- rset = self.cw_rset
- if len(rset[0]) == 2:
- items = []
- for i, (eid, label) in enumerate(rset):
- entity = rset.get_entity(i, 0)
- items.append(self.link(label, entity.absolute_url()))
- else:
- items = [self.link(e.dc_title(), e.absolute_url())
- for e in rset.entities()]
- self.render_items(w, items)
-
-
-class EditRelationMixIn(ReloadableMixIn):
-
- def box_item(self, entity, etarget, fname, label):
- """builds HTML link to edit relation between `entity` and `etarget`"""
- args = {role(self) : entity.eid, target(self): etarget.eid}
- # for each target, provide a link to edit the relation
- jscall = js.cw.utils.callAjaxFuncThenReload(fname,
- self.rtype,
- args['subject'],
- args['object'])
- return u'[<a href="javascript: %s" class="action">%s</a>] %s' % (
- xml_escape(text_type(jscall)), label, etarget.view('incontext'))
-
- def related_boxitems(self, entity):
- return [self.box_item(entity, etarget, 'delete_relation', u'-')
- for etarget in self.related_entities(entity)]
-
- def related_entities(self, entity):
- return entity.related(self.rtype, role(self), entities=True)
-
- def unrelated_boxitems(self, entity):
- return [self.box_item(entity, etarget, 'add_relation', u'+')
- for etarget in self.unrelated_entities(entity)]
-
- def unrelated_entities(self, entity):
- """returns the list of unrelated entities, using the entity's
- appropriate vocabulary function
- """
- skip = set(text_type(e.eid) for e in entity.related(self.rtype, role(self),
- entities=True))
- skip.add(None)
- skip.add(INTERNAL_FIELD_VALUE)
- filteretype = getattr(self, 'etype', None)
- entities = []
- form = self._cw.vreg['forms'].select('edition', self._cw,
- rset=self.cw_rset,
- row=self.cw_row or 0)
- field = form.field_by_name(self.rtype, role(self), entity.e_schema)
- for _, eid in field.vocabulary(form):
- if eid not in skip:
- entity = self._cw.entity_from_eid(eid)
- if filteretype is None or entity.cw_etype == filteretype:
- entities.append(entity)
- return entities
-
-# XXX should be a view usable using uicfg
-class EditRelationCtxComponent(EditRelationMixIn, EntityCtxComponent):
- """base class for boxes which let add or remove entities linked by a given
- relation
-
- subclasses should define at least id, rtype and target class attributes.
- """
- # to be defined in concrete classes
- rtype = None
-
- def render_title(self, w):
- w(display_name(self._cw, self.rtype, role(self),
- context=self.entity.cw_etype))
-
- def render_body(self, w):
- self._cw.add_js('cubicweb.ajax.js')
- related = self.related_boxitems(self.entity)
- unrelated = self.unrelated_boxitems(self.entity)
- self.items.extend(related)
- if related and unrelated:
- self.items.append(u'<hr class="boxSeparator"/>')
- self.items.extend(unrelated)
- self.render_items(w)
-
-
-class AjaxEditRelationCtxComponent(EntityCtxComponent):
- __select__ = EntityCtxComponent.__select__ & (
- partial_relation_possible(action='add') | partial_has_related_entities())
-
- # view used to display related entties
- item_vid = 'incontext'
- # values separator when multiple values are allowed
- separator = ','
- # msgid of the message to display when some new relation has been added/removed
- added_msg = None
- removed_msg = None
-
- # to be defined in concrete classes
- rtype = role = target_etype = None
- # class attributes below *must* be set in concrete classes (additionally to
- # rtype / role [/ target_etype]. They should correspond to js_* methods on
- # the json controller
-
- # function(eid)
- # -> expected to return a list of values to display as input selector
- # vocabulary
- fname_vocabulary = None
-
- # function(eid, value)
- # -> handle the selector's input (eg create necessary entities and/or
- # relations). If the relation is multiple, you'll get a list of value, else
- # a single string value.
- fname_validate = None
-
- # function(eid, linked entity eid)
- # -> remove the relation
- fname_remove = None
-
- def __init__(self, *args, **kwargs):
- super(AjaxEditRelationCtxComponent, self).__init__(*args, **kwargs)
- self.rdef = self.entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
-
- def render_title(self, w):
- w(self.rdef.rtype.display_name(self._cw, self.role,
- context=self.entity.cw_etype))
-
- def add_js_css(self):
- self._cw.add_js(('jquery.ui.js', 'cubicweb.widgets.js'))
- self._cw.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
- self._cw.add_css('jquery.ui.css')
- return True
-
- def render_body(self, w):
- req = self._cw
- entity = self.entity
- related = entity.related(self.rtype, self.role)
- if self.role == 'subject':
- mayadd = self.rdef.has_perm(req, 'add', fromeid=entity.eid)
- else:
- mayadd = self.rdef.has_perm(req, 'add', toeid=entity.eid)
- js_css_added = False
- if mayadd:
- js_css_added = self.add_js_css()
- _ = req._
- if related:
- maydel = None
- w(u'<table class="ajaxEditRelationTable">')
- for rentity in related.entities():
- if maydel is None:
- # Only check permission for the first related.
- if self.role == 'subject':
- fromeid, toeid = entity.eid, rentity.eid
- else:
- fromeid, toeid = rentity.eid, entity.eid
- maydel = self.rdef.has_perm(
- req, 'delete', fromeid=fromeid, toeid=toeid)
- # for each related entity, provide a link to remove the relation
- subview = rentity.view(self.item_vid)
- if maydel:
- if not js_css_added:
- js_css_added = self.add_js_css()
- jscall = text_type(js.ajaxBoxRemoveLinkedEntity(
- self.__regid__, entity.eid, rentity.eid,
- self.fname_remove,
- self.removed_msg and _(self.removed_msg)))
- w(u'<tr><td class="dellink">[<a href="javascript: %s">-</a>]</td>'
- '<td class="entity"> %s</td></tr>' % (xml_escape(jscall),
- subview))
- else:
- w(u'<tr><td class="entity">%s</td></tr>' % (subview))
- w(u'</table>')
- else:
- w(_('no related entity'))
- if mayadd:
- multiple = self.rdef.role_cardinality(self.role) in '*+'
- w(u'<table><tr><td>')
- jscall = text_type(js.ajaxBoxShowSelector(
- self.__regid__, entity.eid, self.fname_vocabulary,
- self.fname_validate, self.added_msg and _(self.added_msg),
- _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
- multiple and self.separator))
- w('<a class="button sglink" href="javascript: %s">%s</a>' % (
- xml_escape(jscall),
- multiple and _('add_relation') or _('update_relation')))
- w(u'</td><td>')
- w(u'<div id="%sHolder"></div>' % self.domid)
- w(u'</td></tr></table>')
-
-
-class RelatedObjectsCtxComponent(EntityCtxComponent):
- """a contextual component to display entities related to another"""
- __select__ = EntityCtxComponent.__select__ & partial_has_related_entities()
- context = 'navcontentbottom'
- rtype = None
- role = 'subject'
-
- vid = 'list'
-
- def render_body(self, w):
- rset = self.entity.related(self.rtype, role(self))
- self._cw.view(self.vid, rset, w=w)
-
-
-# old contextual components, deprecated ########################################
-
-@add_metaclass(class_deprecated)
-class EntityVComponent(Component):
- """abstract base class for additinal components displayed in content
- headers and footer according to:
-
- * the displayed entity's type
- * a context (currently 'header' or 'footer')
-
- it should be configured using .accepts, .etype, .rtype, .target and
- .context class attributes
- """
- __deprecation_warning__ = '[3.10] *VComponent classes are deprecated, use *CtxComponent instead (%(cls)s)'
-
- __registry__ = 'ctxcomponents'
- __select__ = one_line_rset()
-
- cw_property_defs = {
- _('visible'): dict(type='Boolean', default=True,
- help=_('display the component or not')),
- _('order'): dict(type='Int', default=99,
- help=_('display order of the component')),
- _('context'): dict(type='String', default='navtop',
- vocabulary=(_('navtop'), _('navbottom'),
- _('navcontenttop'), _('navcontentbottom'),
- _('ctxtoolbar')),
- help=_('context where this component should be displayed')),
- }
-
- context = 'navcontentbottom'
-
- def call(self, view=None):
- if self.cw_rset is None:
- self.entity_call(self.cw_extra_kwargs.pop('entity'))
- else:
- self.cell_call(0, 0, view=view)
-
- def cell_call(self, row, col, view=None):
- self.entity_call(self.cw_rset.get_entity(row, col), view=view)
-
- def entity_call(self, entity, view=None):
- raise NotImplementedError()
-
-class RelatedObjectsVComponent(EntityVComponent):
- """a section to display some related entities"""
- __select__ = EntityVComponent.__select__ & partial_has_related_entities()
-
- vid = 'list'
- # to be defined in concrete classes
- rtype = title = None
-
- def rql(self):
- """override this method if you want to use a custom rql query"""
- return None
-
- def cell_call(self, row, col, view=None):
- rql = self.rql()
- if rql is None:
- entity = self.cw_rset.get_entity(row, col)
- rset = entity.related(self.rtype, role(self))
- else:
- eid = self.cw_rset[row][col]
- rset = self._cw.execute(self.rql(), {'x': eid})
- if not rset.rowcount:
- return
- self.w(u'<div class="%s">' % self.cssclass)
- self.w(u'<h4>%s</h4>\n' % self._cw._(self.title).capitalize())
- self.wview(self.vid, rset)
- self.w(u'</div>')