|
1 import copy |
|
2 |
|
3 from logilab.mtconverter import xml_escape |
|
4 |
|
5 from cubicweb import neg_role |
|
6 from cubicweb.schema import display_name |
|
7 from cubicweb.utils import json |
|
8 from cubicweb.selectors import non_final_entity, match_kwargs |
|
9 from cubicweb.view import EntityView |
|
10 from cubicweb.web import uicfg, stdmsgs |
|
11 from cubicweb.web.form import FormViewMixIn, FieldNotFound |
|
12 from cubicweb.web.formwidgets import Button, SubmitButton |
|
13 |
|
14 class DummyForm(object): |
|
15 __slots__ = ('event_args',) |
|
16 def form_render(self, **_args): |
|
17 return u'' |
|
18 def render(self, *_args, **_kwargs): |
|
19 return u'' |
|
20 def append_field(self, *args): |
|
21 pass |
|
22 def field_by_name(self, rtype, role, eschema=None): |
|
23 return None |
|
24 |
|
25 class ClickAndEditFormView(FormViewMixIn, EntityView): |
|
26 __regid__ = 'doreledit' |
|
27 __select__ = non_final_entity() & match_kwargs('rtype') |
|
28 |
|
29 # ui side continuations |
|
30 _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', " |
|
31 "'%(divid)s', %(reload)s, '%(vid)s', '%(default_value)s');") |
|
32 _cancelclick = "cw.reledit.cleanupAfterCancel('%s')" |
|
33 |
|
34 # ui side actions/buttons |
|
35 _addzone = (u'<img title="%(msg)s" src="data/plus.png" ' |
|
36 u'alt="%(msg)s"/>') |
|
37 _addmsg = _('click to add a value') |
|
38 _deletezone = (u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>') |
|
39 _deletemsg = _('click to delete this value') |
|
40 _editzone = (u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>') |
|
41 _editzonemsg = _('click to edit this field') |
|
42 |
|
43 # default relation vids according to cardinality |
|
44 _one_rvid = 'incontext' |
|
45 _many_rvid = 'csv' |
|
46 |
|
47 def cell_call(self, row, col, rtype=None, role='subject', |
|
48 reload=False, # controls reloading the whole page after change |
|
49 # boolean, eid (to redirect), or |
|
50 # function taking the subject entity & returning a boolean or an eid |
|
51 rvid=None, # vid to be applied to other side of rtype (non final relations only) |
|
52 default_value=None, |
|
53 formid=None |
|
54 ): |
|
55 """display field to edit entity's `rtype` relation on click""" |
|
56 assert rtype |
|
57 assert role in ('subject', 'object'), '%s is not an acceptable role value' % role |
|
58 if self.__regid__ == 'doreledit': |
|
59 assert formid |
|
60 self._cw.add_js('cubicweb.reledit.js') |
|
61 if formid: |
|
62 self._cw.add_js('cubicweb.edition.js') |
|
63 self._cw.add_css('cubicweb.form.css') |
|
64 entity = self.cw_rset.get_entity(row, col) |
|
65 rschema = self._cw.vreg.schema[rtype] |
|
66 reload = self._compute_reload(entity, rschema, role, reload) |
|
67 default_value = self._compute_default_value(entity, rschema, role, default_value) |
|
68 # compute value, checking perms, build & display form |
|
69 divid = self._build_divid(rtype, role, entity.eid) |
|
70 if rschema.final: |
|
71 value = entity.printable_value(rtype) |
|
72 form, renderer = self._build_form(entity, rtype, role, divid, 'base', |
|
73 default_value, reload) |
|
74 if not self._should_edit_attribute(entity, rschema, form): |
|
75 self.w(value) |
|
76 return |
|
77 value = value or default_value |
|
78 field = form.field_by_name(rtype, role, entity.e_schema) |
|
79 form.append_field(field) |
|
80 self.view_form(divid, value, form, renderer) |
|
81 else: |
|
82 rvid = self._compute_best_vid(entity.e_schema, rschema, role) |
|
83 related_rset = entity.related(rtype, role) |
|
84 if related_rset: |
|
85 value = self._cw.view(rvid, related_rset) |
|
86 else: |
|
87 value = default_value |
|
88 ttypes = self._compute_ttypes(rschema, role) |
|
89 |
|
90 if not self._should_edit_relation(entity, rschema, role): |
|
91 self.w(value) |
|
92 return |
|
93 # this is for attribute-like composites (1 target type, 1 related entity at most) |
|
94 edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes) |
|
95 delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role) |
|
96 add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes) |
|
97 # compute formid |
|
98 if len(ttypes) > 1: # redundant safety belt |
|
99 formid = 'base' |
|
100 else: |
|
101 afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0]) |
|
102 # is there an afs spec that says we should edit |
|
103 # the rschema as an attribute ? |
|
104 if afs and 'main_attributes' in afs: |
|
105 formid = 'base' |
|
106 |
|
107 form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value, |
|
108 reload, dict(vid=rvid), |
|
109 edit_related, add_related and ttypes[0]) |
|
110 self.view_form(divid, value, form, renderer, edit_related, |
|
111 delete_related, add_related) |
|
112 |
|
113 def _compute_best_vid(self, eschema, rschema, role): |
|
114 if eschema.rdef(rschema, role).role_cardinality(role) in '+*': |
|
115 return self._many_rvid |
|
116 return self._one_rvid |
|
117 |
|
118 def _compute_ttypes(self, rschema, role): |
|
119 dual_role = neg_role(role) |
|
120 return getattr(rschema, '%ss' % dual_role)() |
|
121 |
|
122 def _compute_reload(self, entity, rschema, role, reload): |
|
123 rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*') |
|
124 ctrl_reload = rule.get('reload', reload) |
|
125 if callable(ctrl_reload): |
|
126 ctrl_reload = ctrl_reload(entity) |
|
127 if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False |
|
128 ctrl_reload = self._cw.build_url(ctrl_reload) |
|
129 return ctrl_reload |
|
130 |
|
131 def _compute_default_value(self, entity, rschema, role, default_value): |
|
132 etype = entity.e_schema.type |
|
133 rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*') |
|
134 ctrl_default = rule.get('default_value', default_value) |
|
135 if ctrl_default: |
|
136 return ctrl_default |
|
137 if default_value is None: |
|
138 return xml_escape(self._cw._('<%s not specified>') % |
|
139 display_name(self._cw, rschema.type, role)) |
|
140 return default_value |
|
141 |
|
142 def _is_composite(self, eschema, rschema, role): |
|
143 return eschema.rdef(rschema, role).composite == role |
|
144 |
|
145 def _may_add_related(self, rset, entity, rschema, role, ttypes): |
|
146 """ ok for attribute-like composite entities """ |
|
147 if self._is_composite(entity.e_schema, rschema, role): |
|
148 if len(ttypes) > 1: # wrong cardinality: do not handle |
|
149 return False |
|
150 ttype = ttypes[0] |
|
151 if rset: |
|
152 eschema = entity.e_schema |
|
153 if role == 'subject': |
|
154 source, target = eschema.type, ttype |
|
155 else: |
|
156 source, target = ttype, eschema.type |
|
157 card = rschema.rdef(source, target).role_cardinality(role) |
|
158 if card in '1?': |
|
159 return False |
|
160 if rschema.has_perm(self._cw, 'add', toetype=ttype): |
|
161 return True |
|
162 return False |
|
163 |
|
164 def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes): |
|
165 """ controls the edition of the related entity """ |
|
166 if len(ttypes) > 1: |
|
167 return False |
|
168 if not self._is_composite(entity.e_schema, rschema, role): |
|
169 return False |
|
170 if len(related_rset.rows) != 1: |
|
171 return False |
|
172 return related_rset.get_entity(0, 0).cw_has_perm('update') |
|
173 |
|
174 def _may_delete_related(self, related_rset, entity, rschema, role): |
|
175 # we assume may_edit_related |
|
176 assert len(related_rset.rows) == 1 |
|
177 kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} |
|
178 if not rschema.has_perm(self._cw, 'delete', **kwargs): |
|
179 return False |
|
180 for related_entity in related_rset.entities(): |
|
181 if not related_entity.cw_has_perm('delete'): |
|
182 return False |
|
183 return True |
|
184 |
|
185 def _build_edit_zone(self): |
|
186 return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))} |
|
187 |
|
188 def _build_delete_zone(self): |
|
189 return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))} |
|
190 |
|
191 def _build_add_zone(self): |
|
192 return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))} |
|
193 |
|
194 def _build_divid(self, rtype, role, entity_eid): |
|
195 """ builds an id for the root div of a reledit widget """ |
|
196 return '%s-%s-%s' % (rtype, role, entity_eid) |
|
197 |
|
198 def _build_args(self, entity, rtype, role, formid, default_value, reload, |
|
199 extradata=None): |
|
200 divid = self._build_divid(rtype, role, entity.eid) |
|
201 event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid, |
|
202 'reload' : json.dumps(reload), 'default_value' : default_value, |
|
203 'role' : role, 'vid' : u''} |
|
204 if extradata: |
|
205 event_args.update(extradata) |
|
206 return event_args |
|
207 |
|
208 def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, |
|
209 extradata=None, edit_related=False, add_related=False, **formargs): |
|
210 event_args = self._build_args(entity, rtype, role, formid, default_value, |
|
211 reload, extradata) |
|
212 cancelclick = self._cancelclick % divid |
|
213 if edit_related: |
|
214 display_fields = None |
|
215 display_label = True |
|
216 related_entity = entity.related(rtype, role).get_entity(0, 0) |
|
217 self._cw.form['eid'] = related_entity.eid |
|
218 elif add_related: |
|
219 display_fields = None |
|
220 display_label = True |
|
221 _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw) |
|
222 _new_entity.eid = self._cw.varmaker.next() |
|
223 related_entity = _new_entity |
|
224 self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role)) |
|
225 else: # base case: edition/attribute relation |
|
226 display_fields = [(rtype, role)] |
|
227 display_label = False |
|
228 related_entity = entity |
|
229 form = self._cw.vreg['forms'].select( |
|
230 formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid, |
|
231 display_fields=display_fields, formtype='inlined', |
|
232 action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'), |
|
233 cwtarget='eformframe', cssstyle='display: none', |
|
234 **formargs) |
|
235 # pass reledit arguments |
|
236 for pname, pvalue in event_args.iteritems(): |
|
237 form.add_hidden('__reledit|' + pname, pvalue) |
|
238 # handle buttons |
|
239 if form.form_buttons: # edition, delete |
|
240 form_buttons = [] |
|
241 for button in form.form_buttons: |
|
242 if not button.label.endswith('apply'): |
|
243 if button.label.endswith('cancel'): |
|
244 button = copy.deepcopy(button) |
|
245 button.cwaction = None |
|
246 button.onclick = cancelclick |
|
247 form_buttons.append(button) |
|
248 form.form_buttons = form_buttons |
|
249 else: # base |
|
250 form.form_buttons = [SubmitButton(), |
|
251 Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)] |
|
252 form.event_args = event_args |
|
253 renderer = self._cw.vreg['formrenderers'].select( |
|
254 'base', self._cw, entity=related_entity, display_label=display_label, |
|
255 display_help=False, table_class='', |
|
256 button_bar_class='buttonbar', display_progress_div=False) |
|
257 return form, renderer |
|
258 |
|
259 def _should_edit_attribute(self, entity, rschema, form): |
|
260 # examine rtags |
|
261 noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False) |
|
262 if noedit: |
|
263 return False |
|
264 rdef = entity.e_schema.rdef(rschema) |
|
265 afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object) |
|
266 if 'main_hidden' in afs: |
|
267 return False |
|
268 # check permissions |
|
269 if not entity.cw_has_perm('update'): |
|
270 return False |
|
271 rdef = entity.e_schema.rdef(rschema) |
|
272 if not rdef.has_perm(self._cw, 'update', eid=entity.eid): |
|
273 return False |
|
274 # XXX ? |
|
275 try: |
|
276 form.field_by_name(str(rschema), 'subject', entity.e_schema) |
|
277 except FieldNotFound: |
|
278 return False |
|
279 return True |
|
280 |
|
281 def _should_edit_relation(self, entity, rschema, role): |
|
282 # examine rtags |
|
283 rtype = rschema.type |
|
284 noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False) |
|
285 if noedit: |
|
286 return False |
|
287 rdef = entity.e_schema.rdef(rschema, role) |
|
288 afs = uicfg.autoform_section.etype_get( |
|
289 entity.__regid__, rschema, role, rdef.object) |
|
290 if 'main_hidden' in afs: |
|
291 return False |
|
292 perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid} |
|
293 return rschema.has_perm(self._cw, 'add', **perm_args) |
|
294 |
|
295 def view_form(self, divid, value, form=None, renderer=None, |
|
296 edit_related=False, delete_related=False, add_related=False): |
|
297 w = self.w |
|
298 w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' % |
|
299 {'id': divid, |
|
300 'out': "jQuery('#%s').addClass('hidden')" % divid, |
|
301 'over': "jQuery('#%s').removeClass('hidden')" % divid}) |
|
302 w(u'<div id="%s-value" class="editableFieldValue">' % divid) |
|
303 w(value) |
|
304 w(u'</div>') |
|
305 w(form.render(renderer=renderer)) |
|
306 w(u'<div id="%s" class="editableField hidden">' % divid) |
|
307 args = form.event_args.copy() |
|
308 if not add_related: # excludes edition |
|
309 args['formid'] = 'edition' |
|
310 w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' % |
|
311 (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg))) |
|
312 w(self._build_edit_zone()) |
|
313 w(u'</div>') |
|
314 else: |
|
315 args['formid'] = 'edition' |
|
316 w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' % |
|
317 (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg))) |
|
318 w(self._build_add_zone()) |
|
319 w(u'</div>') |
|
320 if delete_related: |
|
321 args['formid'] = 'deleteconf' |
|
322 w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' % |
|
323 (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg))) |
|
324 w(self._build_delete_zone()) |
|
325 w(u'</div>') |
|
326 w(u'</div>') |
|
327 w(u'</div>') |
|
328 |
|
329 class AutoClickAndEditFormView(ClickAndEditFormView): |
|
330 __regid__ = 'reledit' |
|
331 |
|
332 def _build_form(self, entity, rtype, role, divid, formid, default_value, reload, |
|
333 extradata=None, edit_related=False, add_related=False, **formargs): |
|
334 event_args = self._build_args(entity, rtype, role, 'base', default_value, |
|
335 reload, extradata) |
|
336 form = DummyForm() |
|
337 form.event_args = event_args |
|
338 return form, None |