|
1 """Specific views for CWProperty |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 """ |
|
7 __docformat__ = "restructuredtext en" |
|
8 _ = unicode |
|
9 |
|
10 from logilab.mtconverter import html_escape |
|
11 |
|
12 from logilab.common.decorators import cached |
|
13 |
|
14 from cubicweb import UnknownProperty |
|
15 from cubicweb.selectors import (one_line_rset, none_rset, implements, |
|
16 match_user_groups) |
|
17 from cubicweb.view import StartupView |
|
18 from cubicweb.web import uicfg, stdmsgs |
|
19 from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormViewMixIn |
|
20 from cubicweb.web.formfields import FIELDS, StringField |
|
21 from cubicweb.web.formwidgets import Select, Button, SubmitButton |
|
22 from cubicweb.web.views import primary |
|
23 |
|
24 |
|
25 # some string we want to be internationalizable for nicer display of eproperty |
|
26 # groups |
|
27 _('navigation') |
|
28 _('ui') |
|
29 _('actions') |
|
30 _('boxes') |
|
31 _('components') |
|
32 _('contentnavigation') |
|
33 _('navigation.combobox-limit') |
|
34 _('navigation.page-size') |
|
35 _('navigation.related-limit') |
|
36 _('navigation.short-line-size') |
|
37 _('ui.date-format') |
|
38 _('ui.datetime-format') |
|
39 _('ui.default-text-format') |
|
40 _('ui.fckeditor') |
|
41 _('ui.float-format') |
|
42 _('ui.language') |
|
43 _('ui.time-format') |
|
44 _('open all') |
|
45 _('ui.main-template') |
|
46 _('ui.site-title') |
|
47 _('ui.encoding') |
|
48 _('category') |
|
49 |
|
50 |
|
51 def make_togglable_link(nodeid, label, cookiename): |
|
52 """builds a HTML link that switches the visibility & remembers it""" |
|
53 action = u"javascript: toggleVisibility('%s', '%s')" % \ |
|
54 (nodeid, cookiename) |
|
55 return u'<a href="%s">%s</a>' % (action, label) |
|
56 |
|
57 def css_class(someclass): |
|
58 return someclass and 'class="%s"' % someclass or '' |
|
59 |
|
60 |
|
61 class CWPropertyPrimaryView(primary.PrimaryView): |
|
62 __select__ = implements('CWProperty') |
|
63 skip_none = False |
|
64 |
|
65 |
|
66 class SystemEPropertiesForm(FormViewMixIn, StartupView): |
|
67 id = 'systemepropertiesform' |
|
68 __select__ = none_rset() & match_user_groups('managers') |
|
69 |
|
70 title = _('site configuration') |
|
71 category = 'startupview' |
|
72 |
|
73 def linkable(self): |
|
74 return True |
|
75 |
|
76 def url(self): |
|
77 """return the url associated with this view. We can omit rql here""" |
|
78 return self.build_url('view', vid=self.id) |
|
79 |
|
80 def _cookie_name(self, somestr): |
|
81 return str('%s_property_%s' % (self.config.appid, somestr)) |
|
82 |
|
83 def _group_status(self, group, default=u'hidden'): |
|
84 """return css class name 'hidden' (collapsed), or '' (open)""" |
|
85 cookies = self.req.get_cookie() |
|
86 cookiename = self._cookie_name(group) |
|
87 cookie = cookies.get(cookiename) |
|
88 if cookie is None: |
|
89 cookies[cookiename] = default |
|
90 self.req.set_cookie(cookies, cookiename, maxage=None) |
|
91 status = default |
|
92 else: |
|
93 status = cookie.value |
|
94 return status |
|
95 |
|
96 def call(self, **kwargs): |
|
97 """The default view representing the application's index""" |
|
98 self.req.add_js('cubicweb.preferences.js') |
|
99 self.req.add_css('cubicweb.preferences.css') |
|
100 vreg = self.vreg |
|
101 values = self.defined_keys |
|
102 groupedopts = {} |
|
103 mainopts = {} |
|
104 # "self.id=='systemepropertiesform'" to skip site wide properties on |
|
105 # user's preference but not site's configuration |
|
106 for key in vreg.user_property_keys(self.id=='systemepropertiesform'): |
|
107 parts = key.split('.') |
|
108 if parts[0] in vreg: |
|
109 # appobject configuration |
|
110 reg, oid, propid = parts |
|
111 groupedopts.setdefault(reg, {}).setdefault(oid, []).append(key) |
|
112 else: |
|
113 mainopts.setdefault(parts[0], []).append(key) |
|
114 # precompute form to consume error message |
|
115 for group, keys in mainopts.items(): |
|
116 mainopts[group] = self.form(keys, True) |
|
117 for group, objects in groupedopts.items(): |
|
118 for oid, keys in objects.items(): |
|
119 groupedopts[group][oid] = self.form(keys, True) |
|
120 w = self.w |
|
121 req = self.req |
|
122 _ = req._ |
|
123 w(u'<h1>%s</h1>\n' % _(self.title)) |
|
124 # we don't want this in each sub-forms |
|
125 w(u'<div id="progress">%s</div>' % self.req._('validating...')) |
|
126 for label, group, form in sorted((_(g), g, f) |
|
127 for g, f in mainopts.iteritems()): |
|
128 status = css_class(self._group_status(group)) |
|
129 w(u'<h2 class="propertiesform">%s</h2>\n' % |
|
130 (make_togglable_link('fieldset_' + group, label, |
|
131 self._cookie_name(group)))) |
|
132 w(u'<div id="fieldset_%s" %s>' % (group, status)) |
|
133 w(form) |
|
134 w(u'</div>') |
|
135 for label, group, objects in sorted((_(g), g, o) |
|
136 for g, o in groupedopts.iteritems()): |
|
137 status = css_class(self._group_status(group)) |
|
138 w(u'<h2 class="propertiesform">%s</h2>\n' % |
|
139 (make_togglable_link('fieldset_' + group, label, |
|
140 self._cookie_name(group)))) |
|
141 w(u'<div id="fieldset_%s" %s>' % (group, status)) |
|
142 for label, oid, form in sorted((self.req.__('%s_%s' % (group, o)), o, f) |
|
143 for o, f in objects.iteritems()): |
|
144 w(u'<fieldset class="subentity">') |
|
145 w(u'<legend class="componentTitle">%s</legend>\n' % label) |
|
146 docmsgid = '%s_%s_description' % (group, oid) |
|
147 doc = _(docmsgid) |
|
148 if doc != docmsgid: |
|
149 w(u'<p class="description">%s</p>' % html_escape(doc)) |
|
150 w(form) |
|
151 w(u'</fieldset>') |
|
152 w(u'</div>') |
|
153 |
|
154 @property |
|
155 @cached |
|
156 def eprops_rset(self): |
|
157 return self.req.execute('Any P,K,V WHERE P is CWProperty, P pkey K, ' |
|
158 'P value V, NOT P for_user U') |
|
159 |
|
160 @property |
|
161 def defined_keys(self): |
|
162 values = {} |
|
163 for i, entity in enumerate(self.eprops_rset.entities()): |
|
164 values[entity.pkey] = i |
|
165 return values |
|
166 |
|
167 def entity_for_key(self, key): |
|
168 values = self.defined_keys |
|
169 if key in values: |
|
170 entity = self.eprops_rset.get_entity(values[key], 0) |
|
171 else: |
|
172 entity = self.vreg.etype_class('CWProperty')(self.req, None, None) |
|
173 entity.eid = self.req.varmaker.next() |
|
174 entity['pkey'] = key |
|
175 entity['value'] = self.vreg.property_value(key) |
|
176 return entity |
|
177 |
|
178 def form(self, keys, splitlabel=False): |
|
179 buttons = [SubmitButton(), |
|
180 Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] |
|
181 form = CompositeForm(self.req, domid=None, action=self.build_url(), |
|
182 form_buttons=buttons, |
|
183 submitmsg=self.req._('changes applied')) |
|
184 path = self.req.relative_path() |
|
185 if '?' in path: |
|
186 path, params = path.split('?', 1) |
|
187 form.form_add_hidden('__redirectparams', params) |
|
188 form.form_add_hidden('__redirectpath', path) |
|
189 for key in keys: |
|
190 self.form_row(form, key, splitlabel) |
|
191 return form.form_render(display_progress_div=False) |
|
192 |
|
193 def form_row(self, form, key, splitlabel): |
|
194 entity = self.entity_for_key(key) |
|
195 if splitlabel: |
|
196 label = key.split('.')[-1] |
|
197 else: |
|
198 label = key |
|
199 subform = EntityFieldsForm(self.req, entity=entity, set_error_url=False) |
|
200 subform.append_field(PropertyValueField(name='value', label=label, |
|
201 eidparam=True)) |
|
202 subform.vreg = self.vreg |
|
203 subform.form_add_hidden('pkey', key, eidparam=True) |
|
204 form.form_add_subform(subform) |
|
205 return subform |
|
206 |
|
207 |
|
208 def is_user_prefs(cls, req, rset, row=None, col=0, **kwargs): |
|
209 return req.user.eid == rset[row or 0][col] |
|
210 |
|
211 |
|
212 class EPropertiesForm(SystemEPropertiesForm): |
|
213 id = 'epropertiesform' |
|
214 __select__ = ( |
|
215 # we don't want guests to be able to come here |
|
216 match_user_groups('users', 'managers') & |
|
217 (none_rset() | ((one_line_rset() & is_user_prefs) & |
|
218 (one_line_rset() & match_user_groups('managers')))) |
|
219 ) |
|
220 |
|
221 title = _('preferences') |
|
222 |
|
223 @property |
|
224 def user(self): |
|
225 if self.rset is None: |
|
226 return self.req.user |
|
227 return self.rset.get_entity(self.row or 0, self.col or 0) |
|
228 |
|
229 @property |
|
230 @cached |
|
231 def eprops_rset(self): |
|
232 return self.req.execute('Any P,K,V WHERE P is CWProperty, P pkey K, P value V,' |
|
233 'P for_user U, U eid %(x)s', {'x': self.user.eid}) |
|
234 |
|
235 def form_row(self, form, key, splitlabel): |
|
236 subform = super(EPropertiesForm, self).form_row(form, key, splitlabel) |
|
237 # if user is in the managers group and the property is being created, |
|
238 # we have to set for_user explicitly |
|
239 if not subform.edited_entity.has_eid() and self.user.matching_groups('managers'): |
|
240 subform.form_add_hidden('for_user', self.user.eid, eidparam=True) |
|
241 |
|
242 |
|
243 # eproperty form objects ###################################################### |
|
244 |
|
245 class PlaceHolderWidget(object): |
|
246 |
|
247 def render(self, form, field): |
|
248 domid = form.context[field]['id'] |
|
249 # empty span as well else html validation fail (label is refering to |
|
250 # this id) |
|
251 return '<div id="div:%s"><span id="%s">%s</span></div>' % ( |
|
252 domid, domid, form.req._('select a key first')) |
|
253 |
|
254 |
|
255 class NotEditableWidget(object): |
|
256 def __init__(self, value, msg=None): |
|
257 self.value = value |
|
258 self.msg = msg |
|
259 |
|
260 def render(self, form, field): |
|
261 domid = form.context[field]['id'] |
|
262 value = '<span class="value" id="%s">%s</span>' % (domid, self.value) |
|
263 if self.msg: |
|
264 value + '<div class="helper">%s</div>' % self.msg |
|
265 return value |
|
266 |
|
267 |
|
268 class PropertyKeyField(StringField): |
|
269 """specific field for CWProperty.pkey to set the value widget according to |
|
270 the selected key |
|
271 """ |
|
272 widget = Select |
|
273 |
|
274 def render(self, form, renderer): |
|
275 wdg = self.get_widget(form) |
|
276 wdg.attrs['tabindex'] = form.req.next_tabindex() |
|
277 wdg.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % ( |
|
278 form.edited_entity.eid, form.req.next_tabindex()) |
|
279 return wdg.render(form, self) |
|
280 |
|
281 def vocabulary(self, form): |
|
282 entity = form.edited_entity |
|
283 _ = form.req._ |
|
284 if entity.has_eid(): |
|
285 return [(_(entity.pkey), entity.pkey)] |
|
286 # key beginning with 'system.' should usually not be edited by hand |
|
287 choices = entity.vreg.user_property_keys() |
|
288 return [(u'', u'')] + sorted(zip((_(v) for v in choices), choices)) |
|
289 |
|
290 |
|
291 class PropertyValueField(StringField): |
|
292 """specific field for CWProperty.value which will be different according to |
|
293 the selected key type and vocabulary information |
|
294 """ |
|
295 widget = PlaceHolderWidget |
|
296 |
|
297 def render(self, form, renderer=None, tabindex=None): |
|
298 wdg = self.get_widget(form) |
|
299 if tabindex is not None: |
|
300 wdg.attrs['tabindex'] = tabindex |
|
301 return wdg.render(form, self) |
|
302 |
|
303 def form_init(self, form): |
|
304 entity = form.edited_entity |
|
305 if not (entity.has_eid() or 'pkey' in entity): |
|
306 # no key set yet, just include an empty div which will be filled |
|
307 # on key selection |
|
308 return |
|
309 try: |
|
310 pdef = form.vreg.property_info(entity.pkey) |
|
311 except UnknownProperty, ex: |
|
312 self.warning('%s (you should probably delete that property ' |
|
313 'from the database)', ex) |
|
314 msg = form.req._('you should probably delete that property') |
|
315 self.widget = NotEditableWidget(entity.printable_value('value'), |
|
316 '%s (%s)' % (msg, ex)) |
|
317 if entity.pkey.startswith('system.'): |
|
318 msg = form.req._('value associated to this key is not editable ' |
|
319 'manually') |
|
320 self.widget = NotEditableWidget(entity.printable_value('value'), msg) |
|
321 # XXX race condition when used from CWPropertyForm, should not rely on |
|
322 # instance attributes |
|
323 self.initial = pdef['default'] |
|
324 self.help = pdef['help'] |
|
325 vocab = pdef['vocabulary'] |
|
326 if vocab is not None: |
|
327 if callable(vocab): |
|
328 # list() just in case its a generator function |
|
329 self.choices = list(vocab(form.req)) |
|
330 else: |
|
331 self.choices = vocab |
|
332 wdg = Select() |
|
333 else: |
|
334 wdg = FIELDS[pdef['type']].widget() |
|
335 if pdef['type'] == 'Boolean': |
|
336 self.choices = [(form.req._('yes'), '1'), (form.req._('no'), '')] |
|
337 elif pdef['type'] in ('Float', 'Int'): |
|
338 wdg.attrs.setdefault('size', 3) |
|
339 self.widget = wdg |
|
340 |
|
341 |
|
342 uicfg.autoform_field.tag_attribute(('CWProperty', 'pkey'), PropertyKeyField) |
|
343 uicfg.autoform_field.tag_attribute(('CWProperty', 'value'), PropertyValueField) |