cubicweb/web/views/cwproperties.py
changeset 11057 0b59724cb3f2
parent 10669 155c29e0ed1c
child 11767 432f87a63057
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/views/cwproperties.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,442 @@
+# copyright 2003-2012 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/>.
+"""Specific views for CWProperty (eg site/user preferences"""
+
+__docformat__ = "restructuredtext en"
+from cubicweb import _
+
+from logilab.mtconverter import xml_escape
+
+from logilab.common.decorators import cached
+
+from cubicweb import UnknownProperty
+from cubicweb.predicates import (one_line_rset, none_rset, is_instance,
+                                 match_user_groups, logged_user_in_rset)
+from cubicweb.view import StartupView
+from cubicweb.web import stdmsgs
+from cubicweb.web.form import FormViewMixIn
+from cubicweb.web.formfields import FIELDS, StringField
+from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
+                                      FieldWidget)
+from cubicweb.web.views import uicfg, primary, formrenderers, editcontroller
+from cubicweb.web.views.ajaxcontroller import ajaxfunc
+
+uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
+
+# some string we want to be internationalizable for nicer display of property
+# groups
+_('navigation')
+_('ui')
+_('boxes')
+_('components')
+_('ctxcomponents')
+_('navigation.combobox-limit')
+_('navigation.page-size')
+_('navigation.related-limit')
+_('navigation.short-line-size')
+_('ui.date-format')
+_('ui.datetime-format')
+_('ui.default-text-format')
+_('ui.fckeditor')
+_('ui.float-format')
+_('ui.language')
+_('ui.time-format')
+_('open all')
+_('ui.main-template')
+_('ui.site-title')
+_('ui.encoding')
+_('category')
+
+
+def make_togglable_link(nodeid, label):
+    """builds a HTML link that switches the visibility & remembers it"""
+    return u'<a href="javascript: togglePrefVisibility(\'%s\')">%s</a>' % (
+        nodeid, label)
+
+def css_class(someclass):
+    return someclass and 'class="%s"' % someclass or ''
+
+
+class CWPropertyPrimaryView(primary.PrimaryView):
+    __select__ = is_instance('CWProperty')
+    skip_none = False
+
+
+class SystemCWPropertiesForm(FormViewMixIn, StartupView):
+    """site-wide properties edition form"""
+    __regid__ = 'systempropertiesform'
+    __select__ = none_rset() & match_user_groups('managers')
+    form_buttons = [SubmitButton()]
+
+    title = _('site configuration')
+    category = 'startupview'
+
+    def linkable(self):
+        return True
+
+    def url(self):
+        """return the url associated with this view. We can omit rql here"""
+        return self._cw.build_url('view', vid=self.__regid__)
+
+    def _cookie_name(self, somestr):
+        return str('%s_property_%s' % (self._cw.vreg.config.appid, somestr))
+
+    def _group_status(self, group, default=u'hidden'):
+        """return css class name 'hidden' (collapsed), or '' (open)"""
+        cookies = self._cw.get_cookie()
+        cookiename = self._cookie_name(group)
+        cookie = cookies.get(cookiename)
+        if cookie is None:
+            self._cw.set_cookie(cookiename, default, maxage=None)
+            status = default
+        else:
+            status = cookie.value
+        return status
+
+    def call(self, **kwargs):
+        self._cw.add_js(('cubicweb.preferences.js',
+                         'cubicweb.edition.js', 'cubicweb.ajax.js'))
+        self._cw.add_css('cubicweb.preferences.css')
+        values = self.defined_keys
+        mainopts, groupedopts = self.group_properties()
+        # precompute all forms first to consume error message
+        mainforms, groupedforms = self.build_forms(mainopts, groupedopts)
+        _ = self._cw._
+        self.w(u'<h1>%s</h1>\n' % _(self.title))
+        for label, group, form in sorted((_(g), g, f)
+                                         for g, f in mainforms.items()):
+            self.wrap_main_form(group, label, form)
+        for label, group, objects in sorted((_(g), g, o)
+                                            for g, o in groupedforms.items()):
+            self.wrap_grouped_form(group, label, objects)
+
+    @property
+    @cached
+    def cwprops_rset(self):
+        return self._cw.execute('Any P,K,V WHERE P is CWProperty, P pkey K, '
+                                'P value V, NOT P for_user U')
+
+    @property
+    def defined_keys(self):
+        values = {}
+        for i, entity in enumerate(self.cwprops_rset.entities()):
+            values[entity.pkey] = i
+        return values
+
+    def group_properties(self):
+        mainopts, groupedopts = {}, {}
+        vreg = self._cw.vreg
+        # "self._regid__=='systempropertiesform'" to skip site wide properties on
+        # user's preference but not site's configuration
+        for key in vreg.user_property_keys(self.__regid__=='systempropertiesform'):
+            parts = key.split('.')
+            if parts[0] in vreg and len(parts) >= 3:
+                # appobject configuration
+                reg = parts[0]
+                propid = parts[-1]
+                oid = '.'.join(parts[1:-1])
+                groupedopts.setdefault(reg, {}).setdefault(oid, []).append(key)
+            else:
+                mainopts.setdefault(parts[0], []).append(key)
+        return mainopts, groupedopts
+
+    def build_forms(self, mainopts, groupedopts):
+        mainforms, groupedforms = {}, {}
+        for group, keys in mainopts.items():
+            mainforms[group] = self.form(group, keys, False)
+        for group, objects in groupedopts.items():
+            groupedforms[group] = {}
+            for oid, keys in objects.items():
+                groupedforms[group][oid] = self.form(group + '_' + oid, keys, True)
+        return mainforms, groupedforms
+
+    def entity_for_key(self, key):
+        values = self.defined_keys
+        if key in values:
+            entity = self.cwprops_rset.get_entity(values[key], 0)
+        else:
+            entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
+            entity.eid = next(self._cw.varmaker)
+            entity.cw_attr_cache['pkey'] = key
+            entity.cw_attr_cache['value'] = self._cw.vreg.property_value(key)
+        return entity
+
+    def form(self, formid, keys, splitlabel=False):
+        form = self._cw.vreg['forms'].select(
+            'composite', self._cw, domid=formid, action=self._cw.build_url(),
+            form_buttons=self.form_buttons,
+            onsubmit="return validatePrefsForm('%s')" % formid,
+            submitmsg=self._cw._('changes applied'))
+        path = self._cw.relative_path()
+        if '?' in path:
+            path, params = path.split('?', 1)
+            form.add_hidden('__redirectparams', params)
+        form.add_hidden('__redirectpath', path)
+        for key in keys:
+            self.form_row(form, key, splitlabel)
+        renderer = self._cw.vreg['formrenderers'].select('cwproperties', self._cw,
+                                                     display_progress_div=False)
+        data = []
+        form.render(w=data.append, renderer=renderer)
+        return u'\n'.join(data)
+
+    def form_row(self, form, key, splitlabel):
+        entity = self.entity_for_key(key)
+        if splitlabel:
+            label = key.split('.')[-1]
+        else:
+            label = key
+        subform = self._cw.vreg['forms'].select('base', self._cw, entity=entity,
+                                                mainform=False)
+        subform.append_field(PropertyValueField(name='value', label=label, role='subject',
+                                                eidparam=True))
+        subform.add_hidden('pkey', key, eidparam=True, role='subject')
+        form.add_subform(subform)
+        return subform
+
+    def wrap_main_form(self, group, label, form):
+        status = css_class(self._group_status(group))
+        self.w(u'<div class="propertiesform">%s</div>\n' %
+               (make_togglable_link('fieldset_' + group, label)))
+        self.w(u'<div id="fieldset_%s" %s>' % (group, status))
+        self.w(u'<fieldset class="preferences">')
+        self.w(form)
+        self.w(u'</fieldset></div>')
+
+    def wrap_grouped_form(self, group, label, objects):
+        status = css_class(self._group_status(group))
+        self.w(u'<div class="propertiesform">%s</div>\n' %
+          (make_togglable_link('fieldset_' + group, label)))
+        self.w(u'<div id="fieldset_%s" %s>' % (group, status))
+        sorted_objects = sorted((self._cw.__('%s_%s' % (group, o)), o, f)
+                                for o, f in objects.items())
+        for label, oid, form in sorted_objects:
+            self.wrap_object_form(group, oid, label, form)
+        self.w(u'</div>')
+
+    def wrap_object_form(self, group, oid, label, form):
+        w = self.w
+        w(u'<div class="component">')
+        w(u'''<div class="componentLink"><a href="javascript:$.noop();"
+                   onclick="javascript:toggleVisibility('field_%(oid)s_%(group)s')"
+                   class="componentTitle">%(label)s</a>''' % {'label':label, 'oid':oid, 'group':group})
+        w(u''' (<div class="openlink"><a href="javascript:$.noop();"
+                onclick="javascript:openFieldset('fieldset_%(group)s')">%(label)s</a></div>)'''
+                  % {'label':self._cw._('open all'), 'group':group})
+        w(u'</div>')
+        docmsgid = '%s_%s_description' % (group, oid)
+        doc = self._cw._(docmsgid)
+        if doc != docmsgid:
+            w(u'<div class="helper">%s</div>' % xml_escape(doc).capitalize())
+        w(u'</div>')
+        w(u'<fieldset id="field_%(oid)s_%(group)s" class="%(group)s preferences hidden">'
+          % {'oid':oid, 'group':group})
+        w(form)
+        w(u'</fieldset>')
+
+
+class CWPropertiesForm(SystemCWPropertiesForm):
+    """user's preferences properties edition form"""
+    __regid__ = 'propertiesform'
+    __select__ = (
+        (none_rset() & match_user_groups('users','managers'))
+        | (one_line_rset() & match_user_groups('users') & logged_user_in_rset())
+        | (one_line_rset() & match_user_groups('managers') & is_instance('CWUser'))
+        )
+
+    title = _('user preferences')
+
+    @property
+    def user(self):
+        if self.cw_rset is None:
+            return self._cw.user
+        return self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+
+    @property
+    @cached
+    def cwprops_rset(self):
+        return self._cw.execute('Any P,K,V WHERE P is CWProperty, P pkey K, P value V,'
+                                'P for_user U, U eid %(x)s', {'x': self.user.eid})
+
+    def form_row(self, form, key, splitlabel):
+        subform = super(CWPropertiesForm, self).form_row(form, key, splitlabel)
+        # if user is in the managers group and the property is being created,
+        # we have to set for_user explicitly
+        if not subform.edited_entity.has_eid() and self.user.matching_groups('managers'):
+            subform.add_hidden('for_user', self.user.eid, eidparam=True, role='subject')
+        return subform
+
+# cwproperty form objects ######################################################
+
+class PlaceHolderWidget(FieldWidget):
+
+    def render(self, form, field, renderer):
+        domid = field.dom_id(form)
+        # empty span as well else html validation fail (label is refering to
+        # this id)
+        return '<div id="div:%s"><span id="%s">%s</span></div>' % (
+            domid, domid, form._cw._('select a key first'))
+
+
+class NotEditableWidget(FieldWidget):
+    def __init__(self, value, msg=None):
+        self.value = value
+        self.msg = msg
+
+    def render(self, form, field, renderer):
+        domid = field.dom_id(form)
+        value = '<span class="value" id="%s">%s</span>' % (domid, self.value)
+        if self.msg:
+            value += '<div class="helper">%s</div>' % self.msg
+        return value
+
+
+class PropertyKeyField(StringField):
+    """specific field for CWProperty.pkey to set the value widget according to
+    the selected key
+    """
+    widget = Select
+
+    def render(self, form, renderer):
+        wdg = self.get_widget(form)
+        # pylint: disable=E1101
+        wdg.attrs['tabindex'] = form._cw.next_tabindex()
+        wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % (
+            form.edited_entity.eid, form._cw.next_tabindex())
+        return wdg.render(form, self, renderer)
+
+    def vocabulary(self, form):
+        entity = form.edited_entity
+        _ = form._cw._
+        if entity.has_eid():
+            return [(_(entity.pkey), entity.pkey)]
+        choices = entity._cw.vreg.user_property_keys()
+        return [(u'', u'')] + sorted(zip((_(v) for v in choices), choices))
+
+
+class PropertyValueField(StringField):
+    """specific field for CWProperty.value  which will be different according to
+    the selected key type and vocabulary information
+    """
+    widget = PlaceHolderWidget
+
+    def render(self, form, renderer=None, tabindex=None):
+        wdg = self.get_widget(form)
+        if tabindex is not None:
+            wdg.attrs['tabindex'] = tabindex
+        return wdg.render(form, self, renderer)
+
+    def form_init(self, form):
+        entity = form.edited_entity
+        if not (entity.has_eid() or 'pkey' in entity.cw_attr_cache):
+            # no key set yet, just include an empty div which will be filled
+            # on key selection
+            return
+        try:
+            pdef = form._cw.vreg.property_info(entity.pkey)
+        except UnknownProperty as ex:
+            form.warning('%s (you should probably delete that property '
+                         'from the database)', ex)
+            msg = form._cw._('you should probably delete that property')
+            self.widget = NotEditableWidget(entity.printable_value('value'),
+                                            '%s (%s)' % (msg, ex))
+            return
+        if entity.pkey.startswith('system.'):
+            msg = form._cw._('value associated to this key is not editable '
+                             'manually')
+            self.widget = NotEditableWidget(entity.printable_value('value'), msg)
+        # XXX race condition when used from CWPropertyForm, should not rely on
+        # instance attributes
+        self.value = pdef['default']
+        self.help = pdef['help']
+        vocab = pdef['vocabulary']
+        if vocab is not None:
+            if callable(vocab):
+                # list() just in case its a generator function
+                self.choices = list(vocab())
+            else:
+                self.choices = vocab
+            wdg = Select()
+        elif pdef['type'] == 'String': # else we'll get a TextArea by default
+            wdg = TextInput()
+        else:
+            field = FIELDS[pdef['type']]()
+            wdg = field.widget
+            if pdef['type'] == 'Boolean':
+                self.choices = field.vocabulary(form)
+        self.widget = wdg
+
+
+class CWPropertiesFormRenderer(formrenderers.FormRenderer):
+    """specific renderer for properties"""
+    __regid__ = 'cwproperties'
+
+    def open_form(self, form, values):
+        err = '<div class="formsg"></div>'
+        return super(CWPropertiesFormRenderer, self).open_form(form, values) + err
+
+    def _render_fields(self, fields, w, form):
+        for field in fields:
+            w(u'<div class="preffield">\n')
+            if self.display_label:
+                w(u'%s' % self.render_label(form, field))
+            error = form.field_error(field)
+            if error:
+                w(u'<span class="error">%s</span>' % error)
+            w(u'%s' % self.render_help(form, field))
+            w(u'<div class="prefinput">')
+            w(field.render(form, self))
+            w(u'</div>')
+            w(u'</div>')
+
+    def render_buttons(self, w, form):
+        w(u'<div>\n')
+        for button in form.form_buttons:
+            w(u'%s\n' % button.render(form))
+        w(u'</div>')
+
+
+class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
+    __select__ = is_instance('CWProperty')
+
+    def after_deletion_path(self):
+        """return (path, parameters) which should be used as redirect
+        information when this entity is being deleted
+        """
+        return 'view', {}
+
+
+@ajaxfunc(output_type='xhtml')
+def prop_widget(self, propkey, varname, tabindex=None):
+    """specific method for CWProperty handling"""
+    entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
+    entity.eid = varname
+    entity.pkey = propkey
+    form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
+    form.build_context()
+    vfield = form.field_by_name('value', 'subject')
+    renderer = formrenderers.FormRenderer(self._cw)
+    return vfield.render(form, renderer, tabindex=tabindex) \
+           + renderer.render_help(form, vfield)
+
+_afs = uicfg.autoform_section
+_afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
+_afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
+_aff = uicfg.autoform_field
+_aff.tag_attribute(('CWProperty', 'pkey'), PropertyKeyField)
+_aff.tag_attribute(('CWProperty', 'value'), PropertyValueField)