cubicweb/web/htmlwidgets.py
author Denis Laxalde <denis.laxalde@logilab.fr>
Fri, 05 Apr 2019 17:58:19 +0200
changeset 12567 26744ad37953
parent 11057 0b59724cb3f2
child 12825 71a842bdf81d
permissions -rw-r--r--
Drop python2 support This mostly consists in removing the dependency on "six" and updating the code to use only Python3 idioms. Notice that we previously used TemporaryDirectory from cubicweb.devtools.testlib for compatibility with Python2. We now directly import it from tempfile.

# copyright 2003-2010 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/>.
"""html widgets

those are in cubicweb since we need to know available widgets at schema
serialization time
"""

import random
from math import floor

from logilab.mtconverter import xml_escape
from logilab.common.deprecation import class_deprecated

from cubicweb.utils import UStringIO
from cubicweb.uilib import toggle_action, htmlescape
from cubicweb.web import jsonize
from cubicweb.web.component import _bwcompatible_render_item

# XXX HTMLWidgets should have access to req (for datadir / static urls,
#     i18n strings, etc.)
class HTMLWidget(object):

    def _initialize_stream(self, w=None):
        if w:
            self.w = w
        else:
            self._stream = UStringIO()
            self.w = self._stream.write

    def _render(self):
        raise NotImplementedError

    def render(self, w=None):
        self._initialize_stream(w)
        self._render()
        if w is None:
            return self._stream.getvalue()

    def is_empty(self):
        return False


class BoxWidget(HTMLWidget): # XXX Deprecated

    def __init__(self, title, id, items=None, _class="boxFrame",
                 islist=True, shadow=True, escape=True):
        self.title = title
        self.id = id
        self.items = items or []
        self._class = _class
        self.islist = islist
        self.shadow = shadow
        self.escape = escape

    def __len__(self):
        return len(self.items)

    def is_empty(self):
        return len(self) == 0

    def append(self, item):
        self.items.append(item)

    def extend(self, items):
        self.items.extend(items)

    title_class = 'boxTitle'
    main_div_class = 'boxContent'
    listing_class = 'boxListing'

    def box_begin_content(self):
        self.w(u'<div class="%s">\n' % self.main_div_class)
        if self.islist:
            self.w(u'<ul class="%s">' % self.listing_class)

    def box_end_content(self):
        if self.islist:
            self.w(u'</ul>\n')
        self.w(u'</div>\n')
        if self.shadow:
            self.w(u'<div class="shadow">&#160;</div>')

    def _render(self):
        if self.id:
            self.w(u'<div class="%s" id="%s">' % (self._class, self.id))
        else:
            self.w(u'<div class="%s">' % self._class)
        if self.title:
            if self.escape:
                title = '<span>%s</span>' % xml_escape(self.title)
            else:
                title = '<span>%s</span>' % self.title
            self.w(u'<div class="%s">%s</div>' % (self.title_class, title))
        if self.items:
            self.box_begin_content()
            for item in self.items:
                _bwcompatible_render_item(self.w, item)
            self.box_end_content()
        self.w(u'</div>')


class SideBoxWidget(BoxWidget, metaclass=class_deprecated):
    """default CubicWeb's sidebox widget"""
    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'

    title_class = u'sideBoxTitle'
    main_div_class = u'sideBoxBody'
    listing_class = ''

    def __init__(self, title, id=None):
        super(SideBoxWidget, self).__init__(title, id=id, _class='sideBox',
                                            shadow=False)


class MenuWidget(BoxWidget):

    main_div_class = 'menuContent'
    listing_class = 'menuListing'

    def box_end_content(self):
        if self.islist:
            self.w(u'</ul>\n')
        self.w(u'</div>\n')


class RawBoxItem(HTMLWidget): # XXX deprecated
    """a simple box item displaying raw data"""

    def __init__(self, label, liclass=None):
        self.label = label
        self.liclass = liclass

    def _start_li(self):
        if self.liclass is None:
            return u'<li>'
        else:
            return u'<li class="%s">' % self.liclass

    def _render(self):
        self.w(u'%s%s</li>' % (self._start_li(), self.label))


class BoxMenu(RawBoxItem):
    """a menu in a box"""

    link_class = 'boxMenu'

    def __init__(self, label, items=None, isitem=True, liclass=None, ident=None,
                 link_class=None):
        super(BoxMenu, self).__init__(label, liclass)
        self.items = items or []
        self.isitem = isitem
        self.ident = ident or u'boxmenu_%s' % label.replace(' ', '_').replace("'", '')
        if link_class:
            self.link_class = link_class

    def append(self, item):
        self.items.append(item)

    def _begin_menu(self, ident):
        self.w(u'<ul id="%s" class="hidden">' % ident)

    def _end_menu(self):
        self.w(u'</ul>')

    def _render(self):
        if self.isitem:
            self.w(self._start_li())
        ident = self.ident
        self.w(u'<a href="%s" class="%s">%s</a>' % (
            toggle_action(ident), self.link_class, self.label))
        self._begin_menu(ident)
        for item in self.items:
            _bwcompatible_render_item(self.w, item)
        self._end_menu()
        if self.isitem:
            self.w(u'</li>')


class PopupBoxMenu(BoxMenu):
    """like BoxMenu but uses div and specific css class
    in order to behave like a popup menu
    """
    link_class = 'popupMenu'

    def _begin_menu(self, ident):
        self.w(u'<div class="popupWrapper"><div id="%s" class="hidden popup"><ul>' % ident)

    def _end_menu(self):
        self.w(u'</ul></div></div>')


class BoxField(HTMLWidget, metaclass=class_deprecated):
    """couples label / value meant to be displayed in a box"""
    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
    def __init__(self, label, value):
        self.label = label
        self.value = value

    def _render(self):
        self.w(u'<li><div><span class="label">%s</span>&#160;'
               u'<span class="value">%s</span></div></li>'
               % (self.label, self.value))


class BoxSeparator(HTMLWidget, metaclass=class_deprecated):
    """a menu separator"""
    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'

    def _render(self):
        self.w(u'</ul><hr class="boxSeparator"/><ul>')


class BoxLink(HTMLWidget, metaclass=class_deprecated):
    """a link in a box"""
    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
    def __init__(self, href, label, _class='', title='', ident='', escape=False):
        self.href = href
        if escape:
            self.label = xml_escape(label)
        else:
            self.label = label
        self._class = _class or ''
        self.title = title
        self.ident = ident

    def _render(self):
        link = u'<a href="%s" title="%s">%s</a>' % (
            xml_escape(self.href), xml_escape(self.title), self.label)
        if self.ident:
            self.w(u'<li id="%s" class="%s">%s</li>\n' % (self.ident, self._class, link))
        else:
            self.w(u'<li class="%s">%s</li>\n' % (self._class, link))


class BoxHtml(HTMLWidget, metaclass=class_deprecated):
    """a form in a box"""
    __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
    def __init__(self, rawhtml):
        self.rawhtml = rawhtml

    def _render(self):
        self.w(self.rawhtml)


class TableColumn(object):
    def __init__(self, name, rset_sortcol):
        """
        :param name: the column's name
        :param rset_sortcol: the model's column used to sort this column view
        """
        self.name = name
        self.cellrenderers = []
        self.rset_sortcol = rset_sortcol
        self.cell_attrs = {}

    def append_renderer(self, cellvid, colindex):
        # XXX (adim) : why do we need colindex here ?
        self.cellrenderers.append( (cellvid, colindex) )

    def add_attr(self, attr, value):
        self.cell_attrs[attr] = value


class SimpleTableModel(object):
    """
    uses a list of lists as a storage backend

    NB: the model expectes the cellvid passed to
    TableColumn.append_renderer to be a callable accepting a single
    argument and returning a unicode object
    """
    def __init__(self, rows):
        self._rows = rows

    def get_rows(self):
        return self._rows

    def render_cell(self, cellvid, rowindex, colindex, w):
        value = self._rows[rowindex][colindex]
        w(cellvid(value))

    @htmlescape
    @jsonize
    def sortvalue(self, rowindex, colindex):
        value =  self._rows[rowindex][colindex]
        if value is None:
            return u''
        elif isinstance(value, int):
            return u'%09d' % value
        else:
            return unicode(value)


class TableWidget(HTMLWidget):
    """
    Display data in a Table with sortable column.

    When using remember to include the required css and js with:

    self._cw.add_js('jquery.tablesorter.js')
    self._cw.add_css(('cubicweb.tablesorter.css', 'cubicweb.tableview.css'))
    """
    highlight = "onmouseover=\"$(this).addClass('highlighted');\" " \
                "onmouseout=\"$(this).removeClass('highlighted');\""

    def __init__(self, model):
        self.model = model
        self.columns = []

    def append_column(self, column):
        """
        :type column: TableColumn
        """
        self.columns.append(column)

    def _render(self):
        self.w(u'<table class="listing">')
        self.w(u'<thead>')
        self.w(u'<tr class="header">')
        for column in self.columns:
            attrs = ('%s="%s"' % (name, value) for name, value in column.cell_attrs.items())
            self.w(u'<th %s>%s</th>' % (' '.join(attrs), column.name or u''))
        self.w(u'</tr>')
        self.w(u'</thead><tbody>')
        for rowindex in range(len(self.model.get_rows())):
            klass = (rowindex%2==1) and 'odd' or 'even'
            self.w(u'<tr class="%s" %s>' % (klass, self.highlight))
            for column, sortvalue in self.itercols(rowindex):
                attrs = dict(column.cell_attrs)
                attrs["cubicweb:sortvalue"] = sortvalue
                attrs = ('%s="%s"' % (name, value) for name, value in attrs.items())
                self.w(u'<td %s>' % (' '.join(attrs)))
                for cellvid, colindex in column.cellrenderers:
                    self.model.render_cell(cellvid, rowindex, colindex, w=self.w)
                self.w(u'</td>')
            self.w(u'</tr>')
        self.w(u'</tbody>')
        self.w(u'</table>')

    def itercols(self, rowindex):
        for column in self.columns:
            yield column, self.model.sortvalue(rowindex, column.rset_sortcol)