|
1 # copyright 2003-2013 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 """ |
|
19 .. autodocstring:: cubicweb.web.views.autoform::AutomaticEntityForm |
|
20 |
|
21 Configuration through uicfg |
|
22 ``````````````````````````` |
|
23 |
|
24 It is possible to manage which and how an entity's attributes and relations |
|
25 will be edited in the various contexts where the automatic entity form is used |
|
26 by using proper uicfg tags. |
|
27 |
|
28 The details of the uicfg syntax can be found in the :ref:`uicfg` chapter. |
|
29 |
|
30 Possible relation tags that apply to entity forms are detailled below. |
|
31 They are all in the :mod:`cubicweb.web.uicfg` module. |
|
32 |
|
33 Attributes/relations display location |
|
34 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
|
35 |
|
36 ``autoform_section`` specifies where to display a relation in form for a given |
|
37 form type. :meth:`tag_attribute`, :meth:`tag_subject_of` and |
|
38 :meth:`tag_object_of` methods for this relation tag expect two arguments |
|
39 additionally to the relation key: a `formtype` and a `section`. |
|
40 |
|
41 `formtype` may be one of: |
|
42 |
|
43 * 'main', the main entity form (e.g. the one you get when creating or editing an |
|
44 entity) |
|
45 |
|
46 * 'inlined', the form for an entity inlined into another form |
|
47 |
|
48 * 'muledit', the table form when editing multiple entities of the same type |
|
49 |
|
50 |
|
51 section may be one of: |
|
52 |
|
53 * 'hidden', don't display (not even in a hidden input) |
|
54 |
|
55 * 'attributes', display in the attributes section |
|
56 |
|
57 * 'relations', display in the relations section, using the generic relation |
|
58 selector combobox (available in main form only, and not usable for attributes) |
|
59 |
|
60 * 'inlined', display target entity of the relation into an inlined form |
|
61 (available in main form only, and not for attributes) |
|
62 |
|
63 By default, mandatory relations are displayed in the 'attributes' section, |
|
64 others in 'relations' section. |
|
65 |
|
66 |
|
67 Change default fields |
|
68 ^^^^^^^^^^^^^^^^^^^^^ |
|
69 |
|
70 Use ``autoform_field`` to replace the default field class to use for a relation |
|
71 or attribute. You can put either a field class or instance as value (put a class |
|
72 whenether it's possible). |
|
73 |
|
74 .. Warning:: |
|
75 |
|
76 `autoform_field_kwargs` should usually be used instead of |
|
77 `autoform_field`. If you put a field instance into `autoform_field`, |
|
78 `autoform_field_kwargs` values for this relation will be ignored. |
|
79 |
|
80 |
|
81 Customize field options |
|
82 ^^^^^^^^^^^^^^^^^^^^^^^ |
|
83 |
|
84 In order to customize field options (see :class:`~cubicweb.web.formfields.Field` |
|
85 for a detailed list of options), use `autoform_field_kwargs`. This rtag takes |
|
86 a dictionary as arguments, that will be given to the field's contructor. |
|
87 |
|
88 You can then put in that dictionary any arguments supported by the field |
|
89 class. For instance: |
|
90 |
|
91 .. sourcecode:: python |
|
92 |
|
93 # Change the content of the combobox. Here `ticket_done_in_choices` is a |
|
94 # function which returns a list of elements to populate the combobox |
|
95 autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'), |
|
96 {'sort': False, |
|
97 'choices': ticket_done_in_choices}) |
|
98 |
|
99 # Force usage of a TextInput widget for the expression attribute of |
|
100 # RQLExpression entities |
|
101 autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'), |
|
102 {'widget': fw.TextInput}) |
|
103 |
|
104 .. note:: |
|
105 |
|
106 the widget argument can be either a class or an instance (the later |
|
107 case being convenient to pass the Widget specific initialisation |
|
108 options) |
|
109 |
|
110 Overriding permissions |
|
111 ^^^^^^^^^^^^^^^^^^^^^^ |
|
112 |
|
113 The `autoform_permissions_overrides` rtag provides a way to by-pass security |
|
114 checking for dark-corner case where it can't be verified properly. |
|
115 |
|
116 |
|
117 .. More about inlined forms |
|
118 .. Controlling the generic relation fields |
|
119 """ |
|
120 |
|
121 __docformat__ = "restructuredtext en" |
|
122 from cubicweb import _ |
|
123 |
|
124 from warnings import warn |
|
125 |
|
126 from six.moves import range |
|
127 |
|
128 from logilab.mtconverter import xml_escape |
|
129 from logilab.common.decorators import iclassmethod, cached |
|
130 from logilab.common.deprecation import deprecated |
|
131 from logilab.common.registry import NoSelectableObject |
|
132 |
|
133 from cubicweb import neg_role, uilib |
|
134 from cubicweb.schema import display_name |
|
135 from cubicweb.view import EntityView |
|
136 from cubicweb.predicates import ( |
|
137 match_kwargs, match_form_params, non_final_entity, |
|
138 specified_etype_implements) |
|
139 from cubicweb.utils import json_dumps |
|
140 from cubicweb.web import (stdmsgs, eid_param, |
|
141 form as f, formwidgets as fw, formfields as ff) |
|
142 from cubicweb.web.views import uicfg, forms |
|
143 from cubicweb.web.views.ajaxcontroller import ajaxfunc |
|
144 |
|
145 |
|
146 # inlined form handling ######################################################## |
|
147 |
|
148 class InlinedFormField(ff.Field): |
|
149 def __init__(self, view=None, **kwargs): |
|
150 kwargs.setdefault('label', None) |
|
151 # don't add eidparam=True since this field doesn't actually hold the |
|
152 # relation value (the subform does) hence should not be listed in |
|
153 # _cw_entity_fields |
|
154 super(InlinedFormField, self).__init__(name=view.rtype, role=view.role, |
|
155 **kwargs) |
|
156 self.view = view |
|
157 |
|
158 def render(self, form, renderer): |
|
159 """render this field, which is part of form, using the given form |
|
160 renderer |
|
161 """ |
|
162 view = self.view |
|
163 i18nctx = 'inlined:%s.%s.%s' % (form.edited_entity.e_schema, |
|
164 view.rtype, view.role) |
|
165 return u'<div class="inline-%s-%s-slot">%s</div>' % ( |
|
166 view.rtype, view.role, |
|
167 view.render(i18nctx=i18nctx, row=view.cw_row, col=view.cw_col)) |
|
168 |
|
169 def form_init(self, form): |
|
170 """method called before by build_context to trigger potential field |
|
171 initialization requiring the form instance |
|
172 """ |
|
173 if self.view.form: |
|
174 self.view.form.build_context(form.formvalues) |
|
175 |
|
176 @property |
|
177 def needs_multipart(self): |
|
178 if self.view.form: |
|
179 # take a look at inlined forms to check (recursively) if they need |
|
180 # multipart handling. |
|
181 return self.view.form.needs_multipart |
|
182 return False |
|
183 |
|
184 def has_been_modified(self, form): |
|
185 return False |
|
186 |
|
187 def process_posted(self, form): |
|
188 pass # handled by the subform |
|
189 |
|
190 |
|
191 class InlineEntityEditionFormView(f.FormViewMixIn, EntityView): |
|
192 """ |
|
193 :attr peid: the parent entity's eid hosting the inline form |
|
194 :attr rtype: the relation bridging `etype` and `peid` |
|
195 :attr role: the role played by the `peid` in the relation |
|
196 :attr pform: the parent form where this inlined form is being displayed |
|
197 """ |
|
198 __regid__ = 'inline-edition' |
|
199 __select__ = non_final_entity() & match_kwargs('peid', 'rtype') |
|
200 |
|
201 _select_attrs = ('peid', 'rtype', 'role', 'pform', 'etype') |
|
202 removejs = "removeInlinedEntity('%s', '%s', '%s')" |
|
203 |
|
204 # make pylint happy |
|
205 peid = rtype = role = pform = etype = None |
|
206 |
|
207 def __init__(self, *args, **kwargs): |
|
208 for attr in self._select_attrs: |
|
209 # don't pop attributes from kwargs, so the end-up in |
|
210 # self.cw_extra_kwargs which is then passed to the edition form (see |
|
211 # the .form method) |
|
212 setattr(self, attr, kwargs.get(attr)) |
|
213 super(InlineEntityEditionFormView, self).__init__(*args, **kwargs) |
|
214 |
|
215 def _entity(self): |
|
216 assert self.cw_row is not None, self |
|
217 return self.cw_rset.get_entity(self.cw_row, self.cw_col) |
|
218 |
|
219 @property |
|
220 def petype(self): |
|
221 assert isinstance(self.peid, int) |
|
222 pentity = self._cw.entity_from_eid(self.peid) |
|
223 return pentity.e_schema.type |
|
224 |
|
225 @property |
|
226 @cached |
|
227 def form(self): |
|
228 entity = self._entity() |
|
229 form = self._cw.vreg['forms'].select('edition', self._cw, |
|
230 entity=entity, |
|
231 formtype='inlined', |
|
232 form_renderer_id='inline', |
|
233 copy_nav_params=False, |
|
234 mainform=False, |
|
235 parent_form=self.pform, |
|
236 **self.cw_extra_kwargs) |
|
237 if self.pform is None: |
|
238 form.restore_previous_post(form.session_key()) |
|
239 #assert form.parent_form |
|
240 self.add_hiddens(form, entity) |
|
241 return form |
|
242 |
|
243 def cell_call(self, row, col, i18nctx, **kwargs): |
|
244 """ |
|
245 :param peid: the parent entity's eid hosting the inline form |
|
246 :param rtype: the relation bridging `etype` and `peid` |
|
247 :param role: the role played by the `peid` in the relation |
|
248 """ |
|
249 entity = self._entity() |
|
250 divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % ( |
|
251 self.peid, self.rtype, entity.eid) |
|
252 self.render_form(i18nctx, divonclick=divonclick, **kwargs) |
|
253 |
|
254 def _get_removejs(self): |
|
255 """ |
|
256 Don't display the remove link in edition form if the |
|
257 cardinality is 1. Handled in InlineEntityCreationFormView for |
|
258 creation form. |
|
259 """ |
|
260 entity = self._entity() |
|
261 rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype) |
|
262 card = rdef.role_cardinality(self.role) |
|
263 if card == '1': # don't display remove link |
|
264 return None |
|
265 # if cardinality is 1..n (+), dont display link to remove an inlined form for the first form |
|
266 # allowing to edit the relation. To detect so: |
|
267 # |
|
268 # * if parent form (pform) is None, we're generated through an ajax call and so we know this |
|
269 # is not the first form |
|
270 # |
|
271 # * if parent form is not None, look for previous InlinedFormField in the parent's form |
|
272 # fields |
|
273 if card == '+' and self.pform is not None: |
|
274 # retrieve all field'views handling this relation and return None if we're the first of |
|
275 # them |
|
276 first_view = next(iter((f.view for f in self.pform.fields |
|
277 if isinstance(f, InlinedFormField) |
|
278 and f.view.rtype == self.rtype and f.view.role == self.role))) |
|
279 if self == first_view: |
|
280 return None |
|
281 return self.removejs and self.removejs % ( |
|
282 self.peid, self.rtype, entity.eid) |
|
283 |
|
284 def render_form(self, i18nctx, **kwargs): |
|
285 """fetch and render the form""" |
|
286 entity = self._entity() |
|
287 divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid) |
|
288 title = self.form_title(entity, i18nctx) |
|
289 removejs = self._get_removejs() |
|
290 countkey = '%s_count' % self.rtype |
|
291 try: |
|
292 self._cw.data[countkey] += 1 |
|
293 except KeyError: |
|
294 self._cw.data[countkey] = 1 |
|
295 self.form.render(w=self.w, divid=divid, title=title, removejs=removejs, |
|
296 i18nctx=i18nctx, counter=self._cw.data[countkey] , |
|
297 **kwargs) |
|
298 |
|
299 def form_title(self, entity, i18nctx): |
|
300 return self._cw.pgettext(i18nctx, entity.cw_etype) |
|
301 |
|
302 def add_hiddens(self, form, entity): |
|
303 """to ease overriding (see cubes.vcsfile.views.forms for instance)""" |
|
304 iid = 'rel-%s-%s-%s' % (self.peid, self.rtype, entity.eid) |
|
305 # * str(self.rtype) in case it's a schema object |
|
306 # * neged_role() since role is the for parent entity, we want the role |
|
307 # of the inlined entity |
|
308 form.add_hidden(name=str(self.rtype), value=self.peid, |
|
309 role=neg_role(self.role), eidparam=True, id=iid) |
|
310 |
|
311 def keep_entity(self, form, entity): |
|
312 if not entity.has_eid(): |
|
313 return True |
|
314 # are we regenerating form because of a validation error? |
|
315 if form.form_previous_values: |
|
316 cdvalues = self._cw.list_form_param(eid_param(self.rtype, self.peid), |
|
317 form.form_previous_values) |
|
318 if unicode(entity.eid) not in cdvalues: |
|
319 return False |
|
320 return True |
|
321 |
|
322 |
|
323 class InlineEntityCreationFormView(InlineEntityEditionFormView): |
|
324 """ |
|
325 :attr etype: the entity type being created in the inline form |
|
326 """ |
|
327 __regid__ = 'inline-creation' |
|
328 __select__ = (match_kwargs('peid', 'petype', 'rtype') |
|
329 & specified_etype_implements('Any')) |
|
330 _select_attrs = InlineEntityEditionFormView._select_attrs + ('petype',) |
|
331 |
|
332 # make pylint happy |
|
333 petype = None |
|
334 |
|
335 @property |
|
336 def removejs(self): |
|
337 entity = self._entity() |
|
338 rdef = entity.e_schema.rdef(self.rtype, neg_role(self.role), self.petype) |
|
339 card = rdef.role_cardinality(self.role) |
|
340 # when one is adding an inline entity for a relation of a single card, |
|
341 # the 'add a new xxx' link disappears. If the user then cancel the addition, |
|
342 # we have to make this link appears back. This is done by giving add new link |
|
343 # id to removeInlineForm. |
|
344 if card == '?': |
|
345 divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid) |
|
346 return "removeInlineForm('%%s', '%%s', '%s', '%%s', '%s')" % ( |
|
347 self.role, divid) |
|
348 elif card in '+*': |
|
349 return "removeInlineForm('%%s', '%%s', '%s', '%%s')" % self.role |
|
350 # don't do anything for card == '1' |
|
351 |
|
352 @cached |
|
353 def _entity(self): |
|
354 try: |
|
355 cls = self._cw.vreg['etypes'].etype_class(self.etype) |
|
356 except Exception: |
|
357 self.w(self._cw._('no such entity type %s') % self.etype) |
|
358 return |
|
359 entity = cls(self._cw) |
|
360 entity.eid = next(self._cw.varmaker) |
|
361 return entity |
|
362 |
|
363 def call(self, i18nctx, **kwargs): |
|
364 self.render_form(i18nctx, **kwargs) |
|
365 |
|
366 |
|
367 class InlineAddNewLinkView(InlineEntityCreationFormView): |
|
368 """ |
|
369 :attr card: the cardinality of the relation according to role of `peid` |
|
370 """ |
|
371 __regid__ = 'inline-addnew-link' |
|
372 __select__ = (match_kwargs('peid', 'petype', 'rtype') |
|
373 & specified_etype_implements('Any')) |
|
374 |
|
375 _select_attrs = InlineEntityCreationFormView._select_attrs + ('card',) |
|
376 card = None # make pylint happy |
|
377 form = None # no actual form wrapped |
|
378 |
|
379 def call(self, i18nctx, **kwargs): |
|
380 self._cw.set_varmaker() |
|
381 divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid) |
|
382 self.w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">' |
|
383 % divid) |
|
384 js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s', '%s')" % ( |
|
385 self.peid, self.petype, self.etype, self.rtype, self.role, i18nctx) |
|
386 if self.pform.should_hide_add_new_relation_link(self.rtype, self.card): |
|
387 js = "toggleVisibility('%s'); %s" % (divid, js) |
|
388 __ = self._cw.pgettext |
|
389 self.w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>' |
|
390 % (self.rtype, self.peid, js, __(i18nctx, 'add a %s' % self.etype))) |
|
391 self.w(u'</div>') |
|
392 |
|
393 |
|
394 # generic relations handling ################################################## |
|
395 |
|
396 def relation_id(eid, rtype, role, reid): |
|
397 """return an identifier for a relation between two entities""" |
|
398 if role == 'subject': |
|
399 return u'%s:%s:%s' % (eid, rtype, reid) |
|
400 return u'%s:%s:%s' % (reid, rtype, eid) |
|
401 |
|
402 def toggleable_relation_link(eid, nodeid, label='x'): |
|
403 """return javascript snippet to delete/undelete a relation between two |
|
404 entities |
|
405 """ |
|
406 js = u"javascript: togglePendingDelete('%s', %s);" % ( |
|
407 nodeid, xml_escape(json_dumps(eid))) |
|
408 return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % ( |
|
409 js, nodeid, label) |
|
410 |
|
411 |
|
412 def get_pending_inserts(req, eid=None): |
|
413 """shortcut to access req's pending_insert entry |
|
414 |
|
415 This is where are stored relations being added while editing |
|
416 an entity. This used to be stored in a temporary cookie. |
|
417 """ |
|
418 pending = req.session.data.get('pending_insert', ()) |
|
419 return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending |
|
420 if eid is None or eid in (subj, obj)] |
|
421 |
|
422 def get_pending_deletes(req, eid=None): |
|
423 """shortcut to access req's pending_delete entry |
|
424 |
|
425 This is where are stored relations being removed while editing |
|
426 an entity. This used to be stored in a temporary cookie. |
|
427 """ |
|
428 pending = req.session.data.get('pending_delete', ()) |
|
429 return ['%s:%s:%s' % (subj, rel, obj) for subj, rel, obj in pending |
|
430 if eid is None or eid in (subj, obj)] |
|
431 |
|
432 def parse_relations_descr(rdescr): |
|
433 """parse a string describing some relations, in the form |
|
434 subjeids:rtype:objeids |
|
435 where subjeids and objeids are eids separeted by a underscore |
|
436 |
|
437 return an iterator on (subject eid, relation type, object eid) found |
|
438 """ |
|
439 for rstr in rdescr: |
|
440 subjs, rtype, objs = rstr.split(':') |
|
441 for subj in subjs.split('_'): |
|
442 for obj in objs.split('_'): |
|
443 yield int(subj), rtype, int(obj) |
|
444 |
|
445 def delete_relations(req, rdefs): |
|
446 """delete relations from the repository""" |
|
447 # FIXME convert to using the syntax subject:relation:eids |
|
448 execute = req.execute |
|
449 for subj, rtype, obj in parse_relations_descr(rdefs): |
|
450 rql = 'DELETE X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype |
|
451 execute(rql, {'x': subj, 'y': obj}) |
|
452 req.set_message(req._('relations deleted')) |
|
453 |
|
454 def insert_relations(req, rdefs): |
|
455 """insert relations into the repository""" |
|
456 execute = req.execute |
|
457 for subj, rtype, obj in parse_relations_descr(rdefs): |
|
458 rql = 'SET X %s Y where X eid %%(x)s, Y eid %%(y)s' % rtype |
|
459 execute(rql, {'x': subj, 'y': obj}) |
|
460 |
|
461 |
|
462 # ajax edition helpers ######################################################## |
|
463 @ajaxfunc(output_type='xhtml', check_pageid=True) |
|
464 def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx): |
|
465 view = self._cw.vreg['views'].select('inline-creation', self._cw, |
|
466 etype=ttype, rtype=rtype, role=role, |
|
467 peid=peid, petype=petype) |
|
468 return self._call_view(view, i18nctx=i18nctx) |
|
469 |
|
470 @ajaxfunc(output_type='json') |
|
471 def validate_form(self, action, names, values): |
|
472 return self.validate_form(action, names, values) |
|
473 |
|
474 @ajaxfunc |
|
475 def cancel_edition(self, errorurl): |
|
476 """cancelling edition from javascript |
|
477 |
|
478 We need to clear associated req's data : |
|
479 - errorurl |
|
480 - pending insertions / deletions |
|
481 """ |
|
482 self._cw.cancel_edition(errorurl) |
|
483 |
|
484 |
|
485 def _add_pending(req, eidfrom, rel, eidto, kind): |
|
486 key = 'pending_%s' % kind |
|
487 pendings = req.session.data.setdefault(key, set()) |
|
488 pendings.add( (int(eidfrom), rel, int(eidto)) ) |
|
489 |
|
490 def _remove_pending(req, eidfrom, rel, eidto, kind): |
|
491 key = 'pending_%s' % kind |
|
492 pendings = req.session.data[key] |
|
493 pendings.remove( (int(eidfrom), rel, int(eidto)) ) |
|
494 |
|
495 @ajaxfunc(output_type='json') |
|
496 def remove_pending_insert(self, args): |
|
497 eidfrom, rel, eidto = args |
|
498 _remove_pending(self._cw, eidfrom, rel, eidto, 'insert') |
|
499 |
|
500 @ajaxfunc(output_type='json') |
|
501 def add_pending_inserts(self, tripletlist): |
|
502 for eidfrom, rel, eidto in tripletlist: |
|
503 _add_pending(self._cw, eidfrom, rel, eidto, 'insert') |
|
504 |
|
505 @ajaxfunc(output_type='json') |
|
506 def remove_pending_delete(self, args): |
|
507 eidfrom, rel, eidto = args |
|
508 _remove_pending(self._cw, eidfrom, rel, eidto, 'delete') |
|
509 |
|
510 @ajaxfunc(output_type='json') |
|
511 def add_pending_delete(self, args): |
|
512 eidfrom, rel, eidto = args |
|
513 _add_pending(self._cw, eidfrom, rel, eidto, 'delete') |
|
514 |
|
515 |
|
516 class GenericRelationsWidget(fw.FieldWidget): |
|
517 |
|
518 def render(self, form, field, renderer): |
|
519 stream = [] |
|
520 w = stream.append |
|
521 req = form._cw |
|
522 _ = req._ |
|
523 __ = _ |
|
524 eid = form.edited_entity.eid |
|
525 w(u'<table id="relatedEntities">') |
|
526 for rschema, role, related in field.relations_table(form): |
|
527 # already linked entities |
|
528 if related: |
|
529 label = rschema.display_name(req, role, context=form.edited_entity.cw_etype) |
|
530 w(u'<tr><th class="labelCol">%s</th>' % label) |
|
531 w(u'<td>') |
|
532 w(u'<ul class="list-unstyled">') |
|
533 for viewparams in related: |
|
534 w(u'<li>%s<span id="span%s" class="%s">%s</span></li>' |
|
535 % (viewparams[1], viewparams[0], viewparams[2], viewparams[3])) |
|
536 if not form.force_display and form.maxrelitems < len(related): |
|
537 link = (u'<span>' |
|
538 '[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]' |
|
539 '</span>' % _('view all')) |
|
540 w(u'<li>%s</li>' % link) |
|
541 w(u'</ul>') |
|
542 w(u'</td>') |
|
543 w(u'</tr>') |
|
544 pendings = list(field.restore_pending_inserts(form)) |
|
545 if not pendings: |
|
546 w(u'<tr><th> </th><td> </td></tr>') |
|
547 else: |
|
548 for row in pendings: |
|
549 # soon to be linked to entities |
|
550 w(u'<tr id="tr%s">' % row[1]) |
|
551 w(u'<th>%s</th>' % row[3]) |
|
552 w(u'<td>') |
|
553 w(u'<a class="handle" title="%s" href="%s">[x]</a>' % |
|
554 (_('cancel this insert'), row[2])) |
|
555 w(u'<a id="a%s" class="editionPending" href="%s">%s</a>' |
|
556 % (row[1], row[4], xml_escape(row[5]))) |
|
557 w(u'</td>') |
|
558 w(u'</tr>') |
|
559 w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid) |
|
560 w(u'<th class="labelCol">') |
|
561 w(u'<select id="relationSelector_%s" tabindex="%s" ' |
|
562 'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">' |
|
563 % (eid, req.next_tabindex(), xml_escape(json_dumps(eid)))) |
|
564 w(u'<option value="">%s</option>' % _('select a relation')) |
|
565 for i18nrtype, rschema, role in field.relations: |
|
566 # more entities to link to |
|
567 w(u'<option value="%s_%s">%s</option>' % (rschema, role, i18nrtype)) |
|
568 w(u'</select>') |
|
569 w(u'</th>') |
|
570 w(u'<td id="unrelatedDivs_%s"></td>' % eid) |
|
571 w(u'</tr>') |
|
572 w(u'</table>') |
|
573 return '\n'.join(stream) |
|
574 |
|
575 |
|
576 class GenericRelationsField(ff.Field): |
|
577 widget = GenericRelationsWidget |
|
578 |
|
579 def __init__(self, relations, name='_cw_generic_field', **kwargs): |
|
580 assert relations |
|
581 kwargs['eidparam'] = True |
|
582 super(GenericRelationsField, self).__init__(name, **kwargs) |
|
583 self.relations = relations |
|
584 |
|
585 def process_posted(self, form): |
|
586 todelete = get_pending_deletes(form._cw) |
|
587 if todelete: |
|
588 delete_relations(form._cw, todelete) |
|
589 toinsert = get_pending_inserts(form._cw) |
|
590 if toinsert: |
|
591 insert_relations(form._cw, toinsert) |
|
592 return () |
|
593 |
|
594 def relations_table(self, form): |
|
595 """yiels 3-tuples (rtype, role, related_list) |
|
596 where <related_list> itself a list of : |
|
597 - node_id (will be the entity element's DOM id) |
|
598 - appropriate javascript's togglePendingDelete() function call |
|
599 - status 'pendingdelete' or '' |
|
600 - oneline view of related entity |
|
601 """ |
|
602 entity = form.edited_entity |
|
603 pending_deletes = get_pending_deletes(form._cw, entity.eid) |
|
604 for label, rschema, role in self.relations: |
|
605 related = [] |
|
606 if entity.has_eid(): |
|
607 rset = entity.related(rschema, role, limit=form.related_limit) |
|
608 if role == 'subject': |
|
609 haspermkwargs = {'fromeid': entity.eid} |
|
610 else: |
|
611 haspermkwargs = {'toeid': entity.eid} |
|
612 if rschema.has_perm(form._cw, 'delete', **haspermkwargs): |
|
613 toggleable_rel_link_func = toggleable_relation_link |
|
614 else: |
|
615 toggleable_rel_link_func = lambda x, y, z: u'' |
|
616 for row in range(rset.rowcount): |
|
617 nodeid = relation_id(entity.eid, rschema, role, |
|
618 rset[row][0]) |
|
619 if nodeid in pending_deletes: |
|
620 status, label = u'pendingDelete', '+' |
|
621 else: |
|
622 status, label = u'', 'x' |
|
623 dellink = toggleable_rel_link_func(entity.eid, nodeid, label) |
|
624 eview = form._cw.view('oneline', rset, row=row) |
|
625 related.append((nodeid, dellink, status, eview)) |
|
626 yield (rschema, role, related) |
|
627 |
|
628 def restore_pending_inserts(self, form): |
|
629 """used to restore edition page as it was before clicking on |
|
630 'search for <some entity type>' |
|
631 """ |
|
632 entity = form.edited_entity |
|
633 pending_inserts = set(get_pending_inserts(form._cw, form.edited_entity.eid)) |
|
634 for pendingid in pending_inserts: |
|
635 eidfrom, rtype, eidto = pendingid.split(':') |
|
636 pendingid = 'id' + pendingid |
|
637 if int(eidfrom) == entity.eid: # subject |
|
638 label = display_name(form._cw, rtype, 'subject', |
|
639 entity.cw_etype) |
|
640 reid = eidto |
|
641 else: |
|
642 label = display_name(form._cw, rtype, 'object', |
|
643 entity.cw_etype) |
|
644 reid = eidfrom |
|
645 jscall = "javascript: cancelPendingInsert('%s', 'tr', null, %s);" \ |
|
646 % (pendingid, entity.eid) |
|
647 rset = form._cw.eid_rset(reid) |
|
648 eview = form._cw.view('text', rset, row=0) |
|
649 yield rtype, pendingid, jscall, label, reid, eview |
|
650 |
|
651 |
|
652 class UnrelatedDivs(EntityView): |
|
653 __regid__ = 'unrelateddivs' |
|
654 __select__ = match_form_params('relation') |
|
655 |
|
656 def cell_call(self, row, col): |
|
657 entity = self.cw_rset.get_entity(row, col) |
|
658 relname, role = self._cw.form.get('relation').rsplit('_', 1) |
|
659 rschema = self._cw.vreg.schema.rschema(relname) |
|
660 hidden = 'hidden' in self._cw.form |
|
661 is_cell = 'is_cell' in self._cw.form |
|
662 self.w(self.build_unrelated_select_div(entity, rschema, role, |
|
663 is_cell=is_cell, hidden=hidden)) |
|
664 |
|
665 def build_unrelated_select_div(self, entity, rschema, role, |
|
666 is_cell=False, hidden=True): |
|
667 options = [] |
|
668 divid = 'div%s_%s_%s' % (rschema.type, role, entity.eid) |
|
669 selectid = 'select%s_%s_%s' % (rschema.type, role, entity.eid) |
|
670 if rschema.symmetric or role == 'subject': |
|
671 targettypes = rschema.objects(entity.e_schema) |
|
672 etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes)) |
|
673 else: |
|
674 targettypes = rschema.subjects(entity.e_schema) |
|
675 etypes = '/'.join(sorted(etype.display_name(self._cw) for etype in targettypes)) |
|
676 etypes = uilib.cut(etypes, self._cw.property_value('navigation.short-line-size')) |
|
677 options.append('<option>%s %s</option>' % (self._cw._('select a'), etypes)) |
|
678 options += self._get_select_options(entity, rschema, role) |
|
679 options += self._get_search_options(entity, rschema, role, targettypes) |
|
680 relname, role = self._cw.form.get('relation').rsplit('_', 1) |
|
681 return u"""\ |
|
682 <div class="%s" id="%s"> |
|
683 <select id="%s" onchange="javascript: addPendingInsert(this.options[this.selectedIndex], %s, %s, '%s');"> |
|
684 %s |
|
685 </select> |
|
686 </div> |
|
687 """ % (hidden and 'hidden' or '', divid, selectid, |
|
688 xml_escape(json_dumps(entity.eid)), is_cell and 'true' or 'null', relname, |
|
689 '\n'.join(options)) |
|
690 |
|
691 def _get_select_options(self, entity, rschema, role): |
|
692 """add options to search among all entities of each possible type""" |
|
693 options = [] |
|
694 pending_inserts = get_pending_inserts(self._cw, entity.eid) |
|
695 rtype = rschema.type |
|
696 form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity) |
|
697 field = form.field_by_name(rschema, role, entity.e_schema) |
|
698 limit = self._cw.property_value('navigation.combobox-limit') |
|
699 # NOTE: expect 'limit' arg on choices method of relation field |
|
700 for eview, reid in field.vocabulary(form, limit=limit): |
|
701 if reid is None: |
|
702 if eview: # skip blank value |
|
703 options.append('<option class="separator">-- %s --</option>' |
|
704 % xml_escape(eview)) |
|
705 elif reid != ff.INTERNAL_FIELD_VALUE: |
|
706 optionid = relation_id(entity.eid, rtype, role, reid) |
|
707 if optionid not in pending_inserts: |
|
708 # prefix option's id with letters to make valid XHTML wise |
|
709 options.append('<option id="id%s" value="%s">%s</option>' % |
|
710 (optionid, reid, xml_escape(eview))) |
|
711 return options |
|
712 |
|
713 def _get_search_options(self, entity, rschema, role, targettypes): |
|
714 """add options to search among all entities of each possible type""" |
|
715 options = [] |
|
716 _ = self._cw._ |
|
717 for eschema in targettypes: |
|
718 mode = '%s:%s:%s:%s' % (role, entity.eid, rschema.type, eschema) |
|
719 url = self._cw.build_url(entity.rest_path(), vid='search-associate', |
|
720 __mode=mode) |
|
721 options.append((eschema.display_name(self._cw), |
|
722 '<option value="%s">%s %s</option>' % ( |
|
723 xml_escape(url), _('Search for'), eschema.display_name(self._cw)))) |
|
724 return [o for l, o in sorted(options)] |
|
725 |
|
726 |
|
727 # The automatic entity form #################################################### |
|
728 |
|
729 class AutomaticEntityForm(forms.EntityFieldsForm): |
|
730 """AutomaticEntityForm is an automagic form to edit any entity. It |
|
731 is designed to be fully generated from schema but highly |
|
732 configurable through uicfg. |
|
733 |
|
734 Of course, as for other forms, you can also customise it by specifying |
|
735 various standard form parameters on selection, overriding, or |
|
736 adding/removing fields in selected instances. |
|
737 """ |
|
738 __regid__ = 'edition' |
|
739 |
|
740 cwtarget = 'eformframe' |
|
741 cssclass = 'entityForm' |
|
742 copy_nav_params = True |
|
743 form_buttons = [fw.SubmitButton(), |
|
744 fw.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), |
|
745 fw.Button(stdmsgs.BUTTON_CANCEL, |
|
746 {'class': fw.Button.css_class + ' cwjs-edition-cancel'})] |
|
747 # for attributes selection when searching in uicfg.autoform_section |
|
748 formtype = 'main' |
|
749 # set this to a list of [(relation, role)] if you want to explictily tell |
|
750 # which relations should be edited |
|
751 display_fields = None |
|
752 # action on the form tag |
|
753 _default_form_action_path = 'validateform' |
|
754 |
|
755 @iclassmethod |
|
756 def field_by_name(cls_or_self, name, role=None, eschema=None): |
|
757 """return field with the given name and role. If field is not explicitly |
|
758 defined for the form but `eclass` is specified, guess_field will be |
|
759 called. |
|
760 """ |
|
761 try: |
|
762 return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role, eschema) |
|
763 except f.FieldNotFound: |
|
764 if name == '_cw_generic_field' and not isinstance(cls_or_self, type): |
|
765 return cls_or_self._generic_relations_field() |
|
766 raise |
|
767 |
|
768 # base automatic entity form methods ####################################### |
|
769 |
|
770 def __init__(self, *args, **kwargs): |
|
771 super(AutomaticEntityForm, self).__init__(*args, **kwargs) |
|
772 self.uicfg_afs = self._cw.vreg['uicfg'].select( |
|
773 'autoform_section', self._cw, entity=self.edited_entity) |
|
774 entity = self.edited_entity |
|
775 if entity.has_eid(): |
|
776 entity.complete() |
|
777 for rtype, role in self.editable_attributes(): |
|
778 try: |
|
779 self.field_by_name(str(rtype), role) |
|
780 continue # explicitly specified |
|
781 except f.FieldNotFound: |
|
782 # has to be guessed |
|
783 try: |
|
784 field = self.field_by_name(str(rtype), role, |
|
785 eschema=entity.e_schema) |
|
786 self.fields.append(field) |
|
787 except f.FieldNotFound: |
|
788 # meta attribute such as <attr>_format |
|
789 continue |
|
790 if self.fieldsets_in_order: |
|
791 fsio = list(self.fieldsets_in_order) |
|
792 else: |
|
793 fsio = [None] |
|
794 self.fieldsets_in_order = fsio |
|
795 # add fields for relation whose target should have an inline form |
|
796 for formview in self.inlined_form_views(): |
|
797 field = self._inlined_form_view_field(formview) |
|
798 self.fields.append(field) |
|
799 if not field.fieldset in fsio: |
|
800 fsio.append(field.fieldset) |
|
801 if self.formtype == 'main': |
|
802 # add the generic relation field if necessary |
|
803 if entity.has_eid() and ( |
|
804 self.display_fields is None or |
|
805 '_cw_generic_field' in self.display_fields): |
|
806 try: |
|
807 field = self.field_by_name('_cw_generic_field') |
|
808 except f.FieldNotFound: |
|
809 # no editable relation |
|
810 pass |
|
811 else: |
|
812 self.fields.append(field) |
|
813 if not field.fieldset in fsio: |
|
814 fsio.append(field.fieldset) |
|
815 self.maxrelitems = self._cw.property_value('navigation.related-limit') |
|
816 self.force_display = bool(self._cw.form.get('__force_display')) |
|
817 fnum = len(self.fields) |
|
818 self.fields.sort(key=lambda f: f.order is None and fnum or f.order) |
|
819 |
|
820 @property |
|
821 def related_limit(self): |
|
822 if self.force_display: |
|
823 return None |
|
824 return self.maxrelitems + 1 |
|
825 |
|
826 # autoform specific fields ################################################# |
|
827 |
|
828 def _generic_relations_field(self): |
|
829 srels_by_cat = self.editable_relations() |
|
830 if not srels_by_cat: |
|
831 raise f.FieldNotFound('_cw_generic_field') |
|
832 fieldset = 'This %s:' % self.edited_entity.e_schema |
|
833 return GenericRelationsField(self.editable_relations(), |
|
834 fieldset=fieldset, label=None) |
|
835 |
|
836 def _inlined_form_view_field(self, view): |
|
837 # XXX allow more customization |
|
838 kwargs = self.uicfg_affk.etype_get(self.edited_entity.e_schema, |
|
839 view.rtype, view.role, view.etype) |
|
840 if kwargs is None: |
|
841 kwargs = {} |
|
842 return InlinedFormField(view=view, **kwargs) |
|
843 |
|
844 # methods mapping edited entity relations to fields in the form ############ |
|
845 |
|
846 def _relations_by_section(self, section, permission='add', strict=False): |
|
847 """return a list of (relation schema, target schemas, role) matching |
|
848 given category(ies) and permission |
|
849 """ |
|
850 return self.uicfg_afs.relations_by_section( |
|
851 self.edited_entity, self.formtype, section, permission, strict) |
|
852 |
|
853 def editable_attributes(self, strict=False): |
|
854 """return a list of (relation schema, role) to edit for the entity""" |
|
855 if self.display_fields is not None: |
|
856 schema = self._cw.vreg.schema |
|
857 return [(schema[rtype], role) for rtype, role in self.display_fields] |
|
858 if self.edited_entity.has_eid() and not self.edited_entity.cw_has_perm('update'): |
|
859 return [] |
|
860 action = 'update' if self.edited_entity.has_eid() else 'add' |
|
861 return [(rtype, role) for rtype, _, role in self._relations_by_section( |
|
862 'attributes', action, strict)] |
|
863 |
|
864 def editable_relations(self): |
|
865 """return a sorted list of (relation's label, relation'schema, role) for |
|
866 relations in the 'relations' section |
|
867 """ |
|
868 result = [] |
|
869 for rschema, _, role in self._relations_by_section('relations', |
|
870 strict=True): |
|
871 result.append( (rschema.display_name(self.edited_entity._cw, role, |
|
872 self.edited_entity.cw_etype), |
|
873 rschema, role) ) |
|
874 return sorted(result) |
|
875 |
|
876 def inlined_relations(self): |
|
877 """return a list of (relation schema, target schemas, role) matching |
|
878 given category(ies) and permission |
|
879 """ |
|
880 return self._relations_by_section('inlined') |
|
881 |
|
882 # inlined forms control #################################################### |
|
883 |
|
884 def inlined_form_views(self): |
|
885 """compute and return list of inlined form views (hosting the inlined |
|
886 form object) |
|
887 """ |
|
888 allformviews = [] |
|
889 entity = self.edited_entity |
|
890 for rschema, ttypes, role in self.inlined_relations(): |
|
891 # show inline forms only if there's one possible target type |
|
892 # for rschema |
|
893 if len(ttypes) != 1: |
|
894 self.warning('entity related by the %s relation should have ' |
|
895 'inlined form but there is multiple target types, ' |
|
896 'dunno what to do', rschema) |
|
897 continue |
|
898 tschema = ttypes[0] |
|
899 ttype = tschema.type |
|
900 formviews = list(self.inline_edition_form_view(rschema, ttype, role)) |
|
901 card = rschema.role_rdef(entity.e_schema, ttype, role).role_cardinality(role) |
|
902 # there is no related entity and we need at least one: we need to |
|
903 # display one explicit inline-creation view |
|
904 if self.should_display_inline_creation_form(rschema, formviews, card): |
|
905 formviews += self.inline_creation_form_view(rschema, ttype, role) |
|
906 # we can create more than one related entity, we thus display a link |
|
907 # to add new related entities |
|
908 if self.must_display_add_new_relation_link(rschema, role, tschema, |
|
909 ttype, formviews, card): |
|
910 addnewlink = self._cw.vreg['views'].select( |
|
911 'inline-addnew-link', self._cw, |
|
912 etype=ttype, rtype=rschema, role=role, card=card, |
|
913 peid=self.edited_entity.eid, |
|
914 petype=self.edited_entity.e_schema, pform=self) |
|
915 formviews.append(addnewlink) |
|
916 allformviews += formviews |
|
917 return allformviews |
|
918 |
|
919 def should_display_inline_creation_form(self, rschema, existant, card): |
|
920 """return true if a creation form should be inlined |
|
921 |
|
922 by default true if there is no related entity and we need at least one |
|
923 """ |
|
924 return not existant and card in '1+' |
|
925 |
|
926 def should_display_add_new_relation_link(self, rschema, existant, card): |
|
927 """return true if we should add a link to add a new creation form |
|
928 (through ajax call) |
|
929 |
|
930 by default true if there is no related entity or if the relation has |
|
931 multiple cardinality |
|
932 """ |
|
933 return not existant or card in '+*' |
|
934 |
|
935 def must_display_add_new_relation_link(self, rschema, role, tschema, |
|
936 ttype, existant, card): |
|
937 """return true if we must add a link to add a new creation form |
|
938 (through ajax call) |
|
939 |
|
940 by default true if there is no related entity or if the relation has |
|
941 multiple cardinality and it is permitted to add the inlined object and |
|
942 relation. |
|
943 """ |
|
944 return (self.should_display_add_new_relation_link( |
|
945 rschema, existant, card) and |
|
946 self.check_inlined_rdef_permissions( |
|
947 rschema, role, tschema, ttype)) |
|
948 |
|
949 def check_inlined_rdef_permissions(self, rschema, role, tschema, ttype): |
|
950 """return true if permissions are granted on the inlined object and |
|
951 relation""" |
|
952 if not tschema.has_perm(self._cw, 'add'): |
|
953 return False |
|
954 entity = self.edited_entity |
|
955 rdef = entity.e_schema.rdef(rschema, role, ttype) |
|
956 if entity.has_eid(): |
|
957 if role == 'subject': |
|
958 rdefkwargs = {'fromeid': entity.eid} |
|
959 else: |
|
960 rdefkwargs = {'toeid': entity.eid} |
|
961 return rdef.has_perm(self._cw, 'add', **rdefkwargs) |
|
962 return rdef.may_have_permission('add', self._cw) |
|
963 |
|
964 |
|
965 def should_hide_add_new_relation_link(self, rschema, card): |
|
966 """return true if once an inlined creation form is added, the 'add new' |
|
967 link should be hidden |
|
968 |
|
969 by default true if the relation has single cardinality |
|
970 """ |
|
971 return card in '1?' |
|
972 |
|
973 def inline_edition_form_view(self, rschema, ttype, role): |
|
974 """yield inline form views for already related entities through the |
|
975 given relation |
|
976 """ |
|
977 entity = self.edited_entity |
|
978 related = entity.has_eid() and entity.related(rschema, role) |
|
979 if related: |
|
980 vvreg = self._cw.vreg['views'] |
|
981 # display inline-edition view for all existing related entities |
|
982 for i, relentity in enumerate(related.entities()): |
|
983 if relentity.cw_has_perm('update'): |
|
984 yield vvreg.select('inline-edition', self._cw, |
|
985 rset=related, row=i, col=0, |
|
986 etype=ttype, rtype=rschema, role=role, |
|
987 peid=entity.eid, pform=self) |
|
988 |
|
989 def inline_creation_form_view(self, rschema, ttype, role): |
|
990 """yield inline form views to a newly related (hence created) entity |
|
991 through the given relation |
|
992 """ |
|
993 try: |
|
994 yield self._cw.vreg['views'].select('inline-creation', self._cw, |
|
995 etype=ttype, rtype=rschema, role=role, |
|
996 peid=self.edited_entity.eid, |
|
997 petype=self.edited_entity.e_schema, |
|
998 pform=self) |
|
999 except NoSelectableObject: |
|
1000 # may be raised if user doesn't have the permission to add ttype entities (no checked |
|
1001 # earlier) or if there is some custom selector on the view |
|
1002 pass |
|
1003 |
|
1004 |
|
1005 ## default form ui configuration ############################################## |
|
1006 |
|
1007 _AFS = uicfg.autoform_section |
|
1008 # use primary and not generated for eid since it has to be an hidden |
|
1009 _AFS.tag_attribute(('*', 'eid'), 'main', 'hidden') |
|
1010 _AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes') |
|
1011 _AFS.tag_attribute(('*', 'description'), 'main', 'attributes') |
|
1012 _AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden') |
|
1013 _AFS.tag_subject_of(('*', 'in_state', '*'), 'main', 'hidden') |
|
1014 for rtype in ('creation_date', 'modification_date', 'cwuri', |
|
1015 'owned_by', 'created_by', 'cw_source'): |
|
1016 _AFS.tag_subject_of(('*', rtype, '*'), 'main', 'metadata') |
|
1017 |
|
1018 _AFS.tag_subject_of(('*', 'by_transition', '*'), 'main', 'attributes') |
|
1019 _AFS.tag_subject_of(('*', 'by_transition', '*'), 'muledit', 'attributes') |
|
1020 _AFS.tag_object_of(('*', 'by_transition', '*'), 'main', 'hidden') |
|
1021 _AFS.tag_object_of(('*', 'from_state', '*'), 'main', 'hidden') |
|
1022 _AFS.tag_object_of(('*', 'to_state', '*'), 'main', 'hidden') |
|
1023 _AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'main', 'attributes') |
|
1024 _AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'muledit', 'attributes') |
|
1025 _AFS.tag_object_of(('*', 'wf_info_for', '*'), 'main', 'hidden') |
|
1026 _AFS.tag_attribute(('CWEType', 'final'), 'main', 'hidden') |
|
1027 _AFS.tag_attribute(('CWRType', 'final'), 'main', 'hidden') |
|
1028 _AFS.tag_attribute(('CWUser', 'firstname'), 'main', 'attributes') |
|
1029 _AFS.tag_attribute(('CWUser', 'surname'), 'main', 'attributes') |
|
1030 _AFS.tag_attribute(('CWUser', 'last_login_time'), 'main', 'metadata') |
|
1031 _AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'main', 'attributes') |
|
1032 _AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'muledit', 'attributes') |
|
1033 _AFS.tag_subject_of(('*', 'primary_email', '*'), 'main', 'relations') |
|
1034 _AFS.tag_subject_of(('*', 'use_email', '*'), 'main', 'inlined') |
|
1035 _AFS.tag_subject_of(('CWRelation', 'relation_type', '*'), 'main', 'inlined') |
|
1036 _AFS.tag_subject_of(('CWRelation', 'from_entity', '*'), 'main', 'inlined') |
|
1037 _AFS.tag_subject_of(('CWRelation', 'to_entity', '*'), 'main', 'inlined') |
|
1038 |
|
1039 _AFFK = uicfg.autoform_field_kwargs |
|
1040 _AFFK.tag_attribute(('RQLExpression', 'expression'), |
|
1041 {'widget': fw.TextInput}) |
|
1042 _AFFK.tag_subject_of(('TrInfo', 'wf_info_for', '*'), |
|
1043 {'widget': fw.HiddenInput}) |
|
1044 |
|
1045 def registration_callback(vreg): |
|
1046 global etype_relation_field |
|
1047 |
|
1048 def etype_relation_field(etype, rtype, role='subject'): |
|
1049 try: |
|
1050 eschema = vreg.schema.eschema(etype) |
|
1051 return AutomaticEntityForm.field_by_name(rtype, role, eschema) |
|
1052 except (KeyError, f.FieldNotFound): |
|
1053 # catch KeyError raised when etype/rtype not found in schema |
|
1054 AutomaticEntityForm.error('field for %s %s may not be found in schema' % (rtype, role)) |
|
1055 return None |
|
1056 |
|
1057 vreg.register_all(globals().values(), __name__) |