diff -r 058bb3dc685f -r 0b59724cb3f2 cubicweb/web/views/cwproperties.py --- /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 . +"""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'%s' % ( + 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'

%s

\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'
%s
\n' % + (make_togglable_link('fieldset_' + group, label))) + self.w(u'
' % (group, status)) + self.w(u'
') + self.w(form) + self.w(u'
') + + def wrap_grouped_form(self, group, label, objects): + status = css_class(self._group_status(group)) + self.w(u'
%s
\n' % + (make_togglable_link('fieldset_' + group, label))) + self.w(u'
' % (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'
') + + def wrap_object_form(self, group, oid, label, form): + w = self.w + w(u'
') + w(u'''') + docmsgid = '%s_%s_description' % (group, oid) + doc = self._cw._(docmsgid) + if doc != docmsgid: + w(u'
%s
' % xml_escape(doc).capitalize()) + w(u'
') + w(u'') + + +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 '
%s
' % ( + 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 = '%s' % (domid, self.value) + if self.msg: + value += '
%s
' % 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 = '
' + return super(CWPropertiesFormRenderer, self).open_form(form, values) + err + + def _render_fields(self, fields, w, form): + for field in fields: + w(u'
\n') + if self.display_label: + w(u'%s' % self.render_label(form, field)) + error = form.field_error(field) + if error: + w(u'%s' % error) + w(u'%s' % self.render_help(form, field)) + w(u'
') + w(field.render(form, self)) + w(u'
') + w(u'
') + + def render_buttons(self, w, form): + w(u'
\n') + for button in form.form_buttons: + w(u'%s\n' % button.render(form)) + w(u'
') + + +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)