web/views/cwproperties.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Specific views for CWProperty (eg site/user preferences"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 from cubicweb import _
       
    22 
       
    23 from logilab.mtconverter import xml_escape
       
    24 
       
    25 from logilab.common.decorators import cached
       
    26 
       
    27 from cubicweb import UnknownProperty
       
    28 from cubicweb.predicates import (one_line_rset, none_rset, is_instance,
       
    29                                  match_user_groups, logged_user_in_rset)
       
    30 from cubicweb.view import StartupView
       
    31 from cubicweb.web import stdmsgs
       
    32 from cubicweb.web.form import FormViewMixIn
       
    33 from cubicweb.web.formfields import FIELDS, StringField
       
    34 from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton,
       
    35                                       FieldWidget)
       
    36 from cubicweb.web.views import uicfg, primary, formrenderers, editcontroller
       
    37 from cubicweb.web.views.ajaxcontroller import ajaxfunc
       
    38 
       
    39 uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden')
       
    40 
       
    41 # some string we want to be internationalizable for nicer display of property
       
    42 # groups
       
    43 _('navigation')
       
    44 _('ui')
       
    45 _('boxes')
       
    46 _('components')
       
    47 _('ctxcomponents')
       
    48 _('navigation.combobox-limit')
       
    49 _('navigation.page-size')
       
    50 _('navigation.related-limit')
       
    51 _('navigation.short-line-size')
       
    52 _('ui.date-format')
       
    53 _('ui.datetime-format')
       
    54 _('ui.default-text-format')
       
    55 _('ui.fckeditor')
       
    56 _('ui.float-format')
       
    57 _('ui.language')
       
    58 _('ui.time-format')
       
    59 _('open all')
       
    60 _('ui.main-template')
       
    61 _('ui.site-title')
       
    62 _('ui.encoding')
       
    63 _('category')
       
    64 
       
    65 
       
    66 def make_togglable_link(nodeid, label):
       
    67     """builds a HTML link that switches the visibility & remembers it"""
       
    68     return u'<a href="javascript: togglePrefVisibility(\'%s\')">%s</a>' % (
       
    69         nodeid, label)
       
    70 
       
    71 def css_class(someclass):
       
    72     return someclass and 'class="%s"' % someclass or ''
       
    73 
       
    74 
       
    75 class CWPropertyPrimaryView(primary.PrimaryView):
       
    76     __select__ = is_instance('CWProperty')
       
    77     skip_none = False
       
    78 
       
    79 
       
    80 class SystemCWPropertiesForm(FormViewMixIn, StartupView):
       
    81     """site-wide properties edition form"""
       
    82     __regid__ = 'systempropertiesform'
       
    83     __select__ = none_rset() & match_user_groups('managers')
       
    84     form_buttons = [SubmitButton()]
       
    85 
       
    86     title = _('site configuration')
       
    87     category = 'startupview'
       
    88 
       
    89     def linkable(self):
       
    90         return True
       
    91 
       
    92     def url(self):
       
    93         """return the url associated with this view. We can omit rql here"""
       
    94         return self._cw.build_url('view', vid=self.__regid__)
       
    95 
       
    96     def _cookie_name(self, somestr):
       
    97         return str('%s_property_%s' % (self._cw.vreg.config.appid, somestr))
       
    98 
       
    99     def _group_status(self, group, default=u'hidden'):
       
   100         """return css class name 'hidden' (collapsed), or '' (open)"""
       
   101         cookies = self._cw.get_cookie()
       
   102         cookiename = self._cookie_name(group)
       
   103         cookie = cookies.get(cookiename)
       
   104         if cookie is None:
       
   105             self._cw.set_cookie(cookiename, default, maxage=None)
       
   106             status = default
       
   107         else:
       
   108             status = cookie.value
       
   109         return status
       
   110 
       
   111     def call(self, **kwargs):
       
   112         self._cw.add_js(('cubicweb.preferences.js',
       
   113                          'cubicweb.edition.js', 'cubicweb.ajax.js'))
       
   114         self._cw.add_css('cubicweb.preferences.css')
       
   115         values = self.defined_keys
       
   116         mainopts, groupedopts = self.group_properties()
       
   117         # precompute all forms first to consume error message
       
   118         mainforms, groupedforms = self.build_forms(mainopts, groupedopts)
       
   119         _ = self._cw._
       
   120         self.w(u'<h1>%s</h1>\n' % _(self.title))
       
   121         for label, group, form in sorted((_(g), g, f)
       
   122                                          for g, f in mainforms.items()):
       
   123             self.wrap_main_form(group, label, form)
       
   124         for label, group, objects in sorted((_(g), g, o)
       
   125                                             for g, o in groupedforms.items()):
       
   126             self.wrap_grouped_form(group, label, objects)
       
   127 
       
   128     @property
       
   129     @cached
       
   130     def cwprops_rset(self):
       
   131         return self._cw.execute('Any P,K,V WHERE P is CWProperty, P pkey K, '
       
   132                                 'P value V, NOT P for_user U')
       
   133 
       
   134     @property
       
   135     def defined_keys(self):
       
   136         values = {}
       
   137         for i, entity in enumerate(self.cwprops_rset.entities()):
       
   138             values[entity.pkey] = i
       
   139         return values
       
   140 
       
   141     def group_properties(self):
       
   142         mainopts, groupedopts = {}, {}
       
   143         vreg = self._cw.vreg
       
   144         # "self._regid__=='systempropertiesform'" to skip site wide properties on
       
   145         # user's preference but not site's configuration
       
   146         for key in vreg.user_property_keys(self.__regid__=='systempropertiesform'):
       
   147             parts = key.split('.')
       
   148             if parts[0] in vreg and len(parts) >= 3:
       
   149                 # appobject configuration
       
   150                 reg = parts[0]
       
   151                 propid = parts[-1]
       
   152                 oid = '.'.join(parts[1:-1])
       
   153                 groupedopts.setdefault(reg, {}).setdefault(oid, []).append(key)
       
   154             else:
       
   155                 mainopts.setdefault(parts[0], []).append(key)
       
   156         return mainopts, groupedopts
       
   157 
       
   158     def build_forms(self, mainopts, groupedopts):
       
   159         mainforms, groupedforms = {}, {}
       
   160         for group, keys in mainopts.items():
       
   161             mainforms[group] = self.form(group, keys, False)
       
   162         for group, objects in groupedopts.items():
       
   163             groupedforms[group] = {}
       
   164             for oid, keys in objects.items():
       
   165                 groupedforms[group][oid] = self.form(group + '_' + oid, keys, True)
       
   166         return mainforms, groupedforms
       
   167 
       
   168     def entity_for_key(self, key):
       
   169         values = self.defined_keys
       
   170         if key in values:
       
   171             entity = self.cwprops_rset.get_entity(values[key], 0)
       
   172         else:
       
   173             entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
       
   174             entity.eid = next(self._cw.varmaker)
       
   175             entity.cw_attr_cache['pkey'] = key
       
   176             entity.cw_attr_cache['value'] = self._cw.vreg.property_value(key)
       
   177         return entity
       
   178 
       
   179     def form(self, formid, keys, splitlabel=False):
       
   180         form = self._cw.vreg['forms'].select(
       
   181             'composite', self._cw, domid=formid, action=self._cw.build_url(),
       
   182             form_buttons=self.form_buttons,
       
   183             onsubmit="return validatePrefsForm('%s')" % formid,
       
   184             submitmsg=self._cw._('changes applied'))
       
   185         path = self._cw.relative_path()
       
   186         if '?' in path:
       
   187             path, params = path.split('?', 1)
       
   188             form.add_hidden('__redirectparams', params)
       
   189         form.add_hidden('__redirectpath', path)
       
   190         for key in keys:
       
   191             self.form_row(form, key, splitlabel)
       
   192         renderer = self._cw.vreg['formrenderers'].select('cwproperties', self._cw,
       
   193                                                      display_progress_div=False)
       
   194         data = []
       
   195         form.render(w=data.append, renderer=renderer)
       
   196         return u'\n'.join(data)
       
   197 
       
   198     def form_row(self, form, key, splitlabel):
       
   199         entity = self.entity_for_key(key)
       
   200         if splitlabel:
       
   201             label = key.split('.')[-1]
       
   202         else:
       
   203             label = key
       
   204         subform = self._cw.vreg['forms'].select('base', self._cw, entity=entity,
       
   205                                                 mainform=False)
       
   206         subform.append_field(PropertyValueField(name='value', label=label, role='subject',
       
   207                                                 eidparam=True))
       
   208         subform.add_hidden('pkey', key, eidparam=True, role='subject')
       
   209         form.add_subform(subform)
       
   210         return subform
       
   211 
       
   212     def wrap_main_form(self, group, label, form):
       
   213         status = css_class(self._group_status(group))
       
   214         self.w(u'<div class="propertiesform">%s</div>\n' %
       
   215                (make_togglable_link('fieldset_' + group, label)))
       
   216         self.w(u'<div id="fieldset_%s" %s>' % (group, status))
       
   217         self.w(u'<fieldset class="preferences">')
       
   218         self.w(form)
       
   219         self.w(u'</fieldset></div>')
       
   220 
       
   221     def wrap_grouped_form(self, group, label, objects):
       
   222         status = css_class(self._group_status(group))
       
   223         self.w(u'<div class="propertiesform">%s</div>\n' %
       
   224           (make_togglable_link('fieldset_' + group, label)))
       
   225         self.w(u'<div id="fieldset_%s" %s>' % (group, status))
       
   226         sorted_objects = sorted((self._cw.__('%s_%s' % (group, o)), o, f)
       
   227                                 for o, f in objects.items())
       
   228         for label, oid, form in sorted_objects:
       
   229             self.wrap_object_form(group, oid, label, form)
       
   230         self.w(u'</div>')
       
   231 
       
   232     def wrap_object_form(self, group, oid, label, form):
       
   233         w = self.w
       
   234         w(u'<div class="component">')
       
   235         w(u'''<div class="componentLink"><a href="javascript:$.noop();"
       
   236                    onclick="javascript:toggleVisibility('field_%(oid)s_%(group)s')"
       
   237                    class="componentTitle">%(label)s</a>''' % {'label':label, 'oid':oid, 'group':group})
       
   238         w(u''' (<div class="openlink"><a href="javascript:$.noop();"
       
   239                 onclick="javascript:openFieldset('fieldset_%(group)s')">%(label)s</a></div>)'''
       
   240                   % {'label':self._cw._('open all'), 'group':group})
       
   241         w(u'</div>')
       
   242         docmsgid = '%s_%s_description' % (group, oid)
       
   243         doc = self._cw._(docmsgid)
       
   244         if doc != docmsgid:
       
   245             w(u'<div class="helper">%s</div>' % xml_escape(doc).capitalize())
       
   246         w(u'</div>')
       
   247         w(u'<fieldset id="field_%(oid)s_%(group)s" class="%(group)s preferences hidden">'
       
   248           % {'oid':oid, 'group':group})
       
   249         w(form)
       
   250         w(u'</fieldset>')
       
   251 
       
   252 
       
   253 class CWPropertiesForm(SystemCWPropertiesForm):
       
   254     """user's preferences properties edition form"""
       
   255     __regid__ = 'propertiesform'
       
   256     __select__ = (
       
   257         (none_rset() & match_user_groups('users','managers'))
       
   258         | (one_line_rset() & match_user_groups('users') & logged_user_in_rset())
       
   259         | (one_line_rset() & match_user_groups('managers') & is_instance('CWUser'))
       
   260         )
       
   261 
       
   262     title = _('user preferences')
       
   263 
       
   264     @property
       
   265     def user(self):
       
   266         if self.cw_rset is None:
       
   267             return self._cw.user
       
   268         return self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   269 
       
   270     @property
       
   271     @cached
       
   272     def cwprops_rset(self):
       
   273         return self._cw.execute('Any P,K,V WHERE P is CWProperty, P pkey K, P value V,'
       
   274                                 'P for_user U, U eid %(x)s', {'x': self.user.eid})
       
   275 
       
   276     def form_row(self, form, key, splitlabel):
       
   277         subform = super(CWPropertiesForm, self).form_row(form, key, splitlabel)
       
   278         # if user is in the managers group and the property is being created,
       
   279         # we have to set for_user explicitly
       
   280         if not subform.edited_entity.has_eid() and self.user.matching_groups('managers'):
       
   281             subform.add_hidden('for_user', self.user.eid, eidparam=True, role='subject')
       
   282         return subform
       
   283 
       
   284 # cwproperty form objects ######################################################
       
   285 
       
   286 class PlaceHolderWidget(FieldWidget):
       
   287 
       
   288     def render(self, form, field, renderer):
       
   289         domid = field.dom_id(form)
       
   290         # empty span as well else html validation fail (label is refering to
       
   291         # this id)
       
   292         return '<div id="div:%s"><span id="%s">%s</span></div>' % (
       
   293             domid, domid, form._cw._('select a key first'))
       
   294 
       
   295 
       
   296 class NotEditableWidget(FieldWidget):
       
   297     def __init__(self, value, msg=None):
       
   298         self.value = value
       
   299         self.msg = msg
       
   300 
       
   301     def render(self, form, field, renderer):
       
   302         domid = field.dom_id(form)
       
   303         value = '<span class="value" id="%s">%s</span>' % (domid, self.value)
       
   304         if self.msg:
       
   305             value += '<div class="helper">%s</div>' % self.msg
       
   306         return value
       
   307 
       
   308 
       
   309 class PropertyKeyField(StringField):
       
   310     """specific field for CWProperty.pkey to set the value widget according to
       
   311     the selected key
       
   312     """
       
   313     widget = Select
       
   314 
       
   315     def render(self, form, renderer):
       
   316         wdg = self.get_widget(form)
       
   317         # pylint: disable=E1101
       
   318         wdg.attrs['tabindex'] = form._cw.next_tabindex()
       
   319         wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % (
       
   320             form.edited_entity.eid, form._cw.next_tabindex())
       
   321         return wdg.render(form, self, renderer)
       
   322 
       
   323     def vocabulary(self, form):
       
   324         entity = form.edited_entity
       
   325         _ = form._cw._
       
   326         if entity.has_eid():
       
   327             return [(_(entity.pkey), entity.pkey)]
       
   328         choices = entity._cw.vreg.user_property_keys()
       
   329         return [(u'', u'')] + sorted(zip((_(v) for v in choices), choices))
       
   330 
       
   331 
       
   332 class PropertyValueField(StringField):
       
   333     """specific field for CWProperty.value  which will be different according to
       
   334     the selected key type and vocabulary information
       
   335     """
       
   336     widget = PlaceHolderWidget
       
   337 
       
   338     def render(self, form, renderer=None, tabindex=None):
       
   339         wdg = self.get_widget(form)
       
   340         if tabindex is not None:
       
   341             wdg.attrs['tabindex'] = tabindex
       
   342         return wdg.render(form, self, renderer)
       
   343 
       
   344     def form_init(self, form):
       
   345         entity = form.edited_entity
       
   346         if not (entity.has_eid() or 'pkey' in entity.cw_attr_cache):
       
   347             # no key set yet, just include an empty div which will be filled
       
   348             # on key selection
       
   349             return
       
   350         try:
       
   351             pdef = form._cw.vreg.property_info(entity.pkey)
       
   352         except UnknownProperty as ex:
       
   353             form.warning('%s (you should probably delete that property '
       
   354                          'from the database)', ex)
       
   355             msg = form._cw._('you should probably delete that property')
       
   356             self.widget = NotEditableWidget(entity.printable_value('value'),
       
   357                                             '%s (%s)' % (msg, ex))
       
   358             return
       
   359         if entity.pkey.startswith('system.'):
       
   360             msg = form._cw._('value associated to this key is not editable '
       
   361                              'manually')
       
   362             self.widget = NotEditableWidget(entity.printable_value('value'), msg)
       
   363         # XXX race condition when used from CWPropertyForm, should not rely on
       
   364         # instance attributes
       
   365         self.value = pdef['default']
       
   366         self.help = pdef['help']
       
   367         vocab = pdef['vocabulary']
       
   368         if vocab is not None:
       
   369             if callable(vocab):
       
   370                 # list() just in case its a generator function
       
   371                 self.choices = list(vocab())
       
   372             else:
       
   373                 self.choices = vocab
       
   374             wdg = Select()
       
   375         elif pdef['type'] == 'String': # else we'll get a TextArea by default
       
   376             wdg = TextInput()
       
   377         else:
       
   378             field = FIELDS[pdef['type']]()
       
   379             wdg = field.widget
       
   380             if pdef['type'] == 'Boolean':
       
   381                 self.choices = field.vocabulary(form)
       
   382         self.widget = wdg
       
   383 
       
   384 
       
   385 class CWPropertiesFormRenderer(formrenderers.FormRenderer):
       
   386     """specific renderer for properties"""
       
   387     __regid__ = 'cwproperties'
       
   388 
       
   389     def open_form(self, form, values):
       
   390         err = '<div class="formsg"></div>'
       
   391         return super(CWPropertiesFormRenderer, self).open_form(form, values) + err
       
   392 
       
   393     def _render_fields(self, fields, w, form):
       
   394         for field in fields:
       
   395             w(u'<div class="preffield">\n')
       
   396             if self.display_label:
       
   397                 w(u'%s' % self.render_label(form, field))
       
   398             error = form.field_error(field)
       
   399             if error:
       
   400                 w(u'<span class="error">%s</span>' % error)
       
   401             w(u'%s' % self.render_help(form, field))
       
   402             w(u'<div class="prefinput">')
       
   403             w(field.render(form, self))
       
   404             w(u'</div>')
       
   405             w(u'</div>')
       
   406 
       
   407     def render_buttons(self, w, form):
       
   408         w(u'<div>\n')
       
   409         for button in form.form_buttons:
       
   410             w(u'%s\n' % button.render(form))
       
   411         w(u'</div>')
       
   412 
       
   413 
       
   414 class CWPropertyIEditControlAdapter(editcontroller.IEditControlAdapter):
       
   415     __select__ = is_instance('CWProperty')
       
   416 
       
   417     def after_deletion_path(self):
       
   418         """return (path, parameters) which should be used as redirect
       
   419         information when this entity is being deleted
       
   420         """
       
   421         return 'view', {}
       
   422 
       
   423 
       
   424 @ajaxfunc(output_type='xhtml')
       
   425 def prop_widget(self, propkey, varname, tabindex=None):
       
   426     """specific method for CWProperty handling"""
       
   427     entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
       
   428     entity.eid = varname
       
   429     entity.pkey = propkey
       
   430     form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
       
   431     form.build_context()
       
   432     vfield = form.field_by_name('value', 'subject')
       
   433     renderer = formrenderers.FormRenderer(self._cw)
       
   434     return vfield.render(form, renderer, tabindex=tabindex) \
       
   435            + renderer.render_help(form, vfield)
       
   436 
       
   437 _afs = uicfg.autoform_section
       
   438 _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden')
       
   439 _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden')
       
   440 _aff = uicfg.autoform_field
       
   441 _aff.tag_attribute(('CWProperty', 'pkey'), PropertyKeyField)
       
   442 _aff.tag_attribute(('CWProperty', 'value'), PropertyValueField)