|
1 """The automatic entity form. |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2001-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 |
|
9 from logilab.common.decorators import iclassmethod |
|
10 |
|
11 from cubicweb import typed_eid |
|
12 from cubicweb.web import stdmsgs, uicfg |
|
13 from cubicweb.web.form import FieldNotFound, EntityFieldsForm |
|
14 from cubicweb.web.formwidgets import Button, SubmitButton |
|
15 _ = unicode |
|
16 |
|
17 class AutomaticEntityForm(EntityFieldsForm): |
|
18 """base automatic form to edit any entity. |
|
19 |
|
20 Designed to be flly generated from schema but highly configurable through: |
|
21 * rtags (rcategories, rfields, rwidgets, inlined, rpermissions) |
|
22 * various standard form parameters |
|
23 |
|
24 You can also easily customise it by adding/removing fields in |
|
25 AutomaticEntityForm instances. |
|
26 """ |
|
27 id = 'edition' |
|
28 |
|
29 cwtarget = 'eformframe' |
|
30 cssclass = 'entityForm' |
|
31 copy_nav_params = True |
|
32 form_buttons = [SubmitButton(stdmsgs.BUTTON_OK), |
|
33 Button(stdmsgs.BUTTON_APPLY, cwaction='apply'), |
|
34 Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] |
|
35 attrcategories = ('primary', 'secondary') |
|
36 # class attributes below are actually stored in the uicfg module since we |
|
37 # don't want them to be reloaded |
|
38 rcategories = uicfg.rcategories |
|
39 rfields = uicfg.rfields |
|
40 rwidgets = uicfg.rwidgets |
|
41 rinlined = uicfg.rinlined |
|
42 rpermissions_overrides = uicfg.rpermissions_overrides |
|
43 |
|
44 @classmethod |
|
45 def vreg_initialization_completed(cls): |
|
46 """set default category tags for relations where it's not yet defined in |
|
47 the category relation tags |
|
48 """ |
|
49 for eschema in cls.schema.entities(): |
|
50 for rschema, tschemas, role in eschema.relation_definitions(True): |
|
51 for tschema in tschemas: |
|
52 if role == 'subject': |
|
53 X, Y = eschema, tschema |
|
54 card = rschema.rproperty(X, Y, 'cardinality')[0] |
|
55 composed = rschema.rproperty(X, Y, 'composite') == 'object' |
|
56 else: |
|
57 X, Y = tschema, eschema |
|
58 card = rschema.rproperty(X, Y, 'cardinality')[1] |
|
59 composed = rschema.rproperty(X, Y, 'composite') == 'subject' |
|
60 if not cls.rcategories.rtag(rschema, role, X, Y): |
|
61 if card in '1+': |
|
62 if not rschema.is_final() and composed: |
|
63 category = 'generated' |
|
64 else: |
|
65 category = 'primary' |
|
66 elif rschema.is_final(): |
|
67 category = 'secondary' |
|
68 else: |
|
69 category = 'generic' |
|
70 cls.rcategories.set_rtag(category, rschema, role, X, Y) |
|
71 |
|
72 @classmethod |
|
73 def erelations_by_category(cls, entity, categories=None, permission=None, rtags=None): |
|
74 """return a list of (relation schema, target schemas, role) matching |
|
75 categories and permission |
|
76 """ |
|
77 if categories is not None: |
|
78 if not isinstance(categories, (list, tuple, set, frozenset)): |
|
79 categories = (categories,) |
|
80 if not isinstance(categories, (set, frozenset)): |
|
81 categories = frozenset(categories) |
|
82 eschema = entity.e_schema |
|
83 if rtags is None: |
|
84 rtags = cls.rcategories |
|
85 permsoverrides = cls.rpermissions_overrides |
|
86 if entity.has_eid(): |
|
87 eid = entity.eid |
|
88 else: |
|
89 eid = None |
|
90 for rschema, targetschemas, role in eschema.relation_definitions(True): |
|
91 # check category first, potentially lower cost than checking |
|
92 # permission which may imply rql queries |
|
93 if categories is not None: |
|
94 targetschemas = [tschema for tschema in targetschemas |
|
95 if rtags.etype_rtag(eschema, rschema, role, tschema) in categories] |
|
96 if not targetschemas: |
|
97 continue |
|
98 if permission is not None: |
|
99 # tag allowing to hijack the permission machinery when |
|
100 # permission is not verifiable until the entity is actually |
|
101 # created... |
|
102 if eid is None and '%s_on_new' % permission in permsoverrides.etype_rtags(eschema, rschema, role): |
|
103 yield (rschema, targetschemas, role) |
|
104 continue |
|
105 if rschema.is_final(): |
|
106 if not rschema.has_perm(entity.req, permission, eid): |
|
107 continue |
|
108 elif role == 'subject': |
|
109 if not ((eid is None and rschema.has_local_role(permission)) or |
|
110 rschema.has_perm(entity.req, permission, fromeid=eid)): |
|
111 continue |
|
112 # on relation with cardinality 1 or ?, we need delete perm as well |
|
113 # if the relation is already set |
|
114 if (permission == 'add' |
|
115 and rschema.cardinality(eschema, targetschemas[0], role) in '1?' |
|
116 and eid and entity.related(rschema.type, role) |
|
117 and not rschema.has_perm(entity.req, 'delete', fromeid=eid, |
|
118 toeid=entity.related(rschema.type, role)[0][0])): |
|
119 continue |
|
120 elif role == 'object': |
|
121 if not ((eid is None and rschema.has_local_role(permission)) or |
|
122 rschema.has_perm(entity.req, permission, toeid=eid)): |
|
123 continue |
|
124 # on relation with cardinality 1 or ?, we need delete perm as well |
|
125 # if the relation is already set |
|
126 if (permission == 'add' |
|
127 and rschema.cardinality(targetschemas[0], eschema, role) in '1?' |
|
128 and eid and entity.related(rschema.type, role) |
|
129 and not rschema.has_perm(entity.req, 'delete', toeid=eid, |
|
130 fromeid=entity.related(rschema.type, role)[0][0])): |
|
131 continue |
|
132 yield (rschema, targetschemas, role) |
|
133 |
|
134 @classmethod |
|
135 def esrelations_by_category(cls, entity, categories=None, permission=None): |
|
136 """filter out result of relations_by_category(categories, permission) by |
|
137 removing final relations |
|
138 |
|
139 return a sorted list of (relation's label, relation'schema, role) |
|
140 """ |
|
141 result = [] |
|
142 for rschema, ttypes, role in cls.erelations_by_category( |
|
143 entity, categories, permission): |
|
144 if rschema.is_final(): |
|
145 continue |
|
146 result.append((rschema.display_name(entity.req, role), rschema, role)) |
|
147 return sorted(result) |
|
148 |
|
149 @iclassmethod |
|
150 def field_by_name(cls_or_self, name, role='subject', eschema=None): |
|
151 """return field with the given name and role. If field is not explicitly |
|
152 defined for the form but `eclass` is specified, guess_field will be |
|
153 called. |
|
154 """ |
|
155 try: |
|
156 return super(AutomaticEntityForm, cls_or_self).field_by_name(name, role) |
|
157 except FieldNotFound: # XXX should raise more explicit exception |
|
158 if eschema is None or not name in cls_or_self.schema: |
|
159 raise |
|
160 rschema = cls_or_self.schema.rschema(name) |
|
161 fieldcls = cls_or_self.rfields.etype_rtag(eschema, rschema, role) |
|
162 if fieldcls: |
|
163 return fieldcls(name=name, role=role, eidparam=True) |
|
164 widget = cls_or_self.rwidgets.etype_rtag(eschema, rschema, role) |
|
165 if widget: |
|
166 field = guess_field(eschema, rschema, role, |
|
167 eidparam=True, widget=widget) |
|
168 else: |
|
169 field = guess_field(eschema, rschema, role, eidparam=True) |
|
170 if field is None: |
|
171 raise |
|
172 return field |
|
173 |
|
174 def __init__(self, *args, **kwargs): |
|
175 super(AutomaticEntityForm, self).__init__(*args, **kwargs) |
|
176 entity = self.edited_entity |
|
177 if entity.has_eid(): |
|
178 entity.complete() |
|
179 for rschema, role in self.editable_attributes(): |
|
180 try: |
|
181 self.field_by_name(rschema.type, role) |
|
182 continue # explicitly specified |
|
183 except FieldNotFound: |
|
184 # has to be guessed |
|
185 try: |
|
186 field = self.field_by_name(rschema.type, role, |
|
187 eschema=entity.e_schema) |
|
188 self.fields.append(field) |
|
189 except FieldNotFound: |
|
190 # meta attribute such as <attr>_format |
|
191 continue |
|
192 self.maxrelitems = self.req.property_value('navigation.related-limit') |
|
193 self.force_display = bool(self.req.form.get('__force_display')) |
|
194 |
|
195 @property |
|
196 def related_limit(self): |
|
197 if self.force_display: |
|
198 return None |
|
199 return self.maxrelitems + 1 |
|
200 |
|
201 def relations_by_category(self, categories=None, permission=None): |
|
202 """return a list of (relation schema, target schemas, role) matching |
|
203 given category(ies) and permission |
|
204 """ |
|
205 return self.erelations_by_category(self.edited_entity, categories, |
|
206 permission) |
|
207 |
|
208 def inlined_relations(self): |
|
209 """return a list of (relation schema, target schemas, role) matching |
|
210 given category(ies) and permission |
|
211 """ |
|
212 # we'll need an initialized varmaker if there are some inlined relation |
|
213 self.initialize_varmaker() |
|
214 return self.erelations_by_category(self.edited_entity, True, 'add', self.rinlined) |
|
215 |
|
216 def srelations_by_category(self, categories=None, permission=None): |
|
217 """filter out result of relations_by_category(categories, permission) by |
|
218 removing final relations |
|
219 |
|
220 return a sorted list of (relation's label, relation'schema, role) |
|
221 """ |
|
222 return self.esrelations_by_category(self.edited_entity, categories, |
|
223 permission) |
|
224 |
|
225 def action(self): |
|
226 """return the form's action attribute. Default to validateform if not |
|
227 explicitly overriden. |
|
228 """ |
|
229 try: |
|
230 return self._action |
|
231 except AttributeError: |
|
232 return self.build_url('validateform') |
|
233 |
|
234 def set_action(self, value): |
|
235 """override default action""" |
|
236 self._action = value |
|
237 |
|
238 action = property(action, set_action) |
|
239 |
|
240 def editable_attributes(self): |
|
241 """return a list of (relation schema, role) to edit for the entity""" |
|
242 return [(rschema, x) for rschema, _, x in self.relations_by_category( |
|
243 self.attrcategories, 'add') if rschema != 'eid'] |
|
244 |
|
245 def relations_table(self): |
|
246 """yiels 3-tuples (rtype, target, related_list) |
|
247 where <related_list> itself a list of : |
|
248 - node_id (will be the entity element's DOM id) |
|
249 - appropriate javascript's togglePendingDelete() function call |
|
250 - status 'pendingdelete' or '' |
|
251 - oneline view of related entity |
|
252 """ |
|
253 entity = self.edited_entity |
|
254 pending_deletes = self.req.get_pending_deletes(entity.eid) |
|
255 for label, rschema, role in self.srelations_by_category('generic', 'add'): |
|
256 relatedrset = entity.related(rschema, role, limit=self.related_limit) |
|
257 if rschema.has_perm(self.req, 'delete'): |
|
258 toggable_rel_link_func = toggable_relation_link |
|
259 else: |
|
260 toggable_rel_link_func = lambda x, y, z: u'' |
|
261 related = [] |
|
262 for row in xrange(relatedrset.rowcount): |
|
263 nodeid = relation_id(entity.eid, rschema, role, |
|
264 relatedrset[row][0]) |
|
265 if nodeid in pending_deletes: |
|
266 status = u'pendingDelete' |
|
267 label = '+' |
|
268 else: |
|
269 status = u'' |
|
270 label = 'x' |
|
271 dellink = toggable_rel_link_func(entity.eid, nodeid, label) |
|
272 eview = self.view('oneline', relatedrset, row=row) |
|
273 related.append((nodeid, dellink, status, eview)) |
|
274 yield (rschema, role, related) |
|
275 |
|
276 def restore_pending_inserts(self, cell=False): |
|
277 """used to restore edition page as it was before clicking on |
|
278 'search for <some entity type>' |
|
279 """ |
|
280 eid = self.edited_entity.eid |
|
281 cell = cell and "div_insert_" or "tr" |
|
282 pending_inserts = set(self.req.get_pending_inserts(eid)) |
|
283 for pendingid in pending_inserts: |
|
284 eidfrom, rtype, eidto = pendingid.split(':') |
|
285 if typed_eid(eidfrom) == eid: # subject |
|
286 label = display_name(self.req, rtype, 'subject') |
|
287 reid = eidto |
|
288 else: |
|
289 label = display_name(self.req, rtype, 'object') |
|
290 reid = eidfrom |
|
291 jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \ |
|
292 % (pendingid, cell, eid) |
|
293 rset = self.req.eid_rset(reid) |
|
294 eview = self.view('text', rset, row=0) |
|
295 # XXX find a clean way to handle baskets |
|
296 if rset.description[0][0] == 'Basket': |
|
297 eview = '%s (%s)' % (eview, display_name(self.req, 'Basket')) |
|
298 yield rtype, pendingid, jscall, label, reid, eview |
|
299 |
|
300 # should_* method extracted to allow overriding |
|
301 |
|
302 def should_inline_relation_form(self, rschema, targettype, role): |
|
303 """return true if the given relation with entity has role and a |
|
304 targettype target should be inlined |
|
305 """ |
|
306 return self.rinlined.etype_rtag(self.edited_entity.id, rschema, role, targettype) |
|
307 |
|
308 def should_display_inline_creation_form(self, rschema, existant, card): |
|
309 """return true if a creation form should be inlined |
|
310 |
|
311 by default true if there is no related entity and we need at least one |
|
312 """ |
|
313 return not existant and card in '1+' |
|
314 |
|
315 def should_display_add_new_relation_link(self, rschema, existant, card): |
|
316 """return true if we should add a link to add a new creation form |
|
317 (through ajax call) |
|
318 |
|
319 by default true if there is no related entity or if the relation has |
|
320 multiple cardinality |
|
321 """ |
|
322 return not existant or card in '+*' |
|
323 |
|
324 |
|
325 def etype_relation_field(etype, rtype, role='subject'): |
|
326 eschema = AutomaticEntityForm.schema.eschema(etype) |
|
327 return AutomaticEntityForm.field_by_name(rtype, role, eschema) |