|
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) |