diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/component.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/cubicweb/web/component.py Sat Jan 16 13:48:51 2016 +0100 @@ -0,0 +1,752 @@ +# 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 . +"""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'%s' + selected_page_link_templ = u'%s' + 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'%s' % + (self.prev_icon_url, self._cw._('there is no previous page'))) + + @property + def no_next_page_link(self): + return (u'%s' % + (self.next_icon_url, self._cw._('there is no next page'))) + + @property + def no_content_prev_link(self): + return (u'%s' % ( + (self.prev_icon_url, self._cw._('no content prev link')))) + + @property + def no_content_next_link(self): + return (u'%s' % + (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'
') + + +def _bwcompatible_render_item(w, item): + if hasattr(item, 'render'): + if getattr(item, 'newstyle', False): + if isinstance(item, Separator): + w(u'') + item.render(w) + w(u'