27 from logilab.common.deprecation import deprecated |
27 from logilab.common.deprecation import deprecated |
28 from logilab.common.graph import ordered_nodes |
28 from logilab.common.graph import ordered_nodes |
29 |
29 |
30 from rql.utils import rqlvar_maker |
30 from rql.utils import rqlvar_maker |
31 |
31 |
32 from cubicweb import Binary, ValidationError |
32 from cubicweb import Binary, ValidationError, UnknownEid |
33 from cubicweb.view import EntityAdapter |
33 from cubicweb.view import EntityAdapter |
34 from cubicweb.predicates import is_instance |
34 from cubicweb.predicates import is_instance |
35 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, |
35 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, |
36 ProcessFormError) |
36 ProcessFormError) |
37 from cubicweb.web.views import basecontrollers, autoform |
37 from cubicweb.web.views import basecontrollers, autoform |
75 class RqlQuery(object): |
75 class RqlQuery(object): |
76 def __init__(self): |
76 def __init__(self): |
77 self.edited = [] |
77 self.edited = [] |
78 self.restrictions = [] |
78 self.restrictions = [] |
79 self.kwargs = {} |
79 self.kwargs = {} |
|
80 self.canceled = False |
80 |
81 |
81 def __repr__(self): |
82 def __repr__(self): |
82 return ('Query <edited=%r restrictions=%r kwargs=%r>' % ( |
83 return ('Query <edited=%r restrictions=%r kwargs=%r>' % ( |
83 self.edited, self.restrictions, self.kwargs)) |
84 self.edited, self.restrictions, self.kwargs)) |
84 |
85 |
85 def insert_query(self, etype): |
86 def insert_query(self, etype): |
|
87 assert not self.canceled |
86 if self.edited: |
88 if self.edited: |
87 rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited)) |
89 rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited)) |
88 else: |
90 else: |
89 rql = 'INSERT %s X' % etype |
91 rql = 'INSERT %s X' % etype |
90 if self.restrictions: |
92 if self.restrictions: |
91 rql += ' WHERE %s' % ','.join(self.restrictions) |
93 rql += ' WHERE %s' % ','.join(self.restrictions) |
92 return rql |
94 return rql |
93 |
95 |
94 def update_query(self, eid): |
96 def update_query(self, eid): |
|
97 assert not self.canceled |
95 varmaker = rqlvar_maker() |
98 varmaker = rqlvar_maker() |
96 var = varmaker.next() |
99 var = varmaker.next() |
97 while var in self.kwargs: |
100 while var in self.kwargs: |
98 var = varmaker.next() |
101 var = varmaker.next() |
99 rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.edited), var) |
102 rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.edited), var) |
188 # those two data variables are used to handle relation from/to entities |
191 # those two data variables are used to handle relation from/to entities |
189 # which doesn't exist at time where the entity is edited and that |
192 # which doesn't exist at time where the entity is edited and that |
190 # deserves special treatment |
193 # deserves special treatment |
191 req.data['pending_inlined'] = defaultdict(set) |
194 req.data['pending_inlined'] = defaultdict(set) |
192 req.data['pending_others'] = set() |
195 req.data['pending_others'] = set() |
|
196 req.data['pending_composite_delete'] = set() |
193 try: |
197 try: |
194 for formparams in self._ordered_formparams(): |
198 for formparams in self._ordered_formparams(): |
195 eid = self.edit_entity(formparams) |
199 eid = self.edit_entity(formparams) |
196 except (RequestError, NothingToEdit) as ex: |
200 except (RequestError, NothingToEdit) as ex: |
197 if '__linkto' in req.form and 'eid' in req.form: |
201 if '__linkto' in req.form and 'eid' in req.form: |
206 for form_, field in req.data.pop('pending_others'): |
210 for form_, field in req.data.pop('pending_others'): |
207 self.handle_formfield(form_, field) |
211 self.handle_formfield(form_, field) |
208 # then execute rql to set all relations |
212 # then execute rql to set all relations |
209 for querydef in self.relations_rql: |
213 for querydef in self.relations_rql: |
210 self._cw.execute(*querydef) |
214 self._cw.execute(*querydef) |
|
215 # delete pending composite |
|
216 for entity in req.data['pending_composite_delete']: |
|
217 entity.cw_delete() |
211 # XXX this processes *all* pending operations of *all* entities |
218 # XXX this processes *all* pending operations of *all* entities |
212 if '__delete' in req.form: |
219 if '__delete' in req.form: |
213 todelete = req.list_form_param('__delete', req.form, pop=True) |
220 todelete = req.list_form_param('__delete', req.form, pop=True) |
214 if todelete: |
221 if todelete: |
215 autoform.delete_relations(self._cw, todelete) |
222 autoform.delete_relations(self._cw, todelete) |
262 self.handle_formfield(form, field, rqlquery) |
269 self.handle_formfield(form, field, rqlquery) |
263 # if there are some inlined field which were waiting for this entity's |
270 # if there are some inlined field which were waiting for this entity's |
264 # creation, add relevant data to the rqlquery |
271 # creation, add relevant data to the rqlquery |
265 for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): |
272 for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): |
266 rqlquery.set_inlined(field.name, form_.edited_entity.eid) |
273 rqlquery.set_inlined(field.name, form_.edited_entity.eid) |
267 if self.errors: |
274 if not rqlquery.canceled: |
268 errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors) |
275 if self.errors: |
269 raise ValidationError(valerror_eid(entity.eid), errors) |
276 errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors) |
270 if eid is None: # creation or copy |
277 raise ValidationError(valerror_eid(entity.eid), errors) |
271 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) |
278 if eid is None: # creation or copy |
272 elif rqlquery.edited: # edition of an existant entity |
279 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) |
273 self.check_concurrent_edition(formparams, eid) |
280 elif rqlquery.edited: # edition of an existant entity |
274 self._update_entity(eid, rqlquery) |
281 self.check_concurrent_edition(formparams, eid) |
|
282 self._update_entity(eid, rqlquery) |
|
283 else: |
|
284 self.errors = [] |
275 if is_main_entity: |
285 if is_main_entity: |
276 self.notify_edited(entity) |
286 self.notify_edited(entity) |
277 if '__delete' in formparams: |
287 if '__delete' in formparams: |
278 # XXX deprecate? |
288 # XXX deprecate? |
279 todelete = req.list_form_param('__delete', formparams, pop=True) |
289 todelete = req.list_form_param('__delete', formparams, pop=True) |
283 if is_main_entity: # only execute linkto for the main entity |
293 if is_main_entity: # only execute linkto for the main entity |
284 self.execute_linkto(entity.eid) |
294 self.execute_linkto(entity.eid) |
285 return eid |
295 return eid |
286 |
296 |
287 def handle_formfield(self, form, field, rqlquery=None): |
297 def handle_formfield(self, form, field, rqlquery=None): |
288 eschema = form.edited_entity.e_schema |
298 entity = form.edited_entity |
|
299 eschema = entity.e_schema |
289 try: |
300 try: |
290 for field, value in field.process_posted(form): |
301 for field, value in field.process_posted(form): |
291 if not ( |
302 if not ( |
292 (field.role == 'subject' and field.name in eschema.subjrels) |
303 (field.role == 'subject' and field.name in eschema.subjrels) |
293 or |
304 or |
294 (field.role == 'object' and field.name in eschema.objrels)): |
305 (field.role == 'object' and field.name in eschema.objrels)): |
295 continue |
306 continue |
|
307 |
296 rschema = self._cw.vreg.schema.rschema(field.name) |
308 rschema = self._cw.vreg.schema.rschema(field.name) |
297 if rschema.final: |
309 if rschema.final: |
298 rqlquery.set_attribute(field.name, value) |
310 rqlquery.set_attribute(field.name, value) |
|
311 continue |
|
312 |
|
313 if entity.has_eid(): |
|
314 origvalues = set(data[0] for data in entity.related(field.name, field.role).rows) |
299 else: |
315 else: |
300 if form.edited_entity.has_eid(): |
316 origvalues = set() |
301 origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True)) |
317 if value is None or value == origvalues: |
302 else: |
318 continue # not edited / not modified / to do later |
303 origvalues = set() |
319 |
304 if value is None or value == origvalues: |
320 unlinked_eids = origvalues - value |
305 continue # not edited / not modified / to do later |
321 |
306 if rschema.inlined and rqlquery is not None and field.role == 'subject': |
322 if unlinked_eids: |
307 self.handle_inlined_relation(form, field, value, origvalues, rqlquery) |
323 # Special handling of composite relation removal |
308 elif form.edited_entity.has_eid(): |
324 self.handle_composite_removal( |
309 self.handle_relation(form, field, value, origvalues) |
325 form, field, unlinked_eids, value, rqlquery) |
310 else: |
326 |
311 form._cw.data['pending_others'].add( (form, field) ) |
327 if rschema.inlined and rqlquery is not None and field.role == 'subject': |
|
328 self.handle_inlined_relation(form, field, value, origvalues, rqlquery) |
|
329 elif form.edited_entity.has_eid(): |
|
330 self.handle_relation(form, field, value, origvalues) |
|
331 else: |
|
332 form._cw.data['pending_others'].add( (form, field) ) |
|
333 |
312 except ProcessFormError as exc: |
334 except ProcessFormError as exc: |
313 self.errors.append((field, exc)) |
335 self.errors.append((field, exc)) |
|
336 |
|
337 def handle_composite_removal(self, form, field, |
|
338 removed_values, new_values, rqlquery): |
|
339 """ |
|
340 In EditController-handled forms, when the user removes a composite |
|
341 relation, it triggers the removal of the related entity in the |
|
342 composite. This is where this happens. |
|
343 |
|
344 See for instance test_subject_subentity_removal in |
|
345 web/test/unittest_application.py. |
|
346 """ |
|
347 rschema = self._cw.vreg.schema.rschema(field.name) |
|
348 new_value_etypes = set() |
|
349 # the user could have included nonexisting eids in the POST; don't crash. |
|
350 for eid in new_values: |
|
351 try: |
|
352 new_value_etypes.add(self._cw.entity_from_eid(eid).cw_etype) |
|
353 except UnknownEid: |
|
354 continue |
|
355 for unlinked_eid in removed_values: |
|
356 unlinked_entity = self._cw.entity_from_eid(unlinked_eid) |
|
357 rdef = rschema.role_rdef(form.edited_entity.cw_etype, |
|
358 unlinked_entity.cw_etype, |
|
359 field.role) |
|
360 if rdef.composite is not None: |
|
361 if rdef.composite == field.role: |
|
362 to_be_removed = unlinked_entity |
|
363 else: |
|
364 if unlinked_entity.cw_etype in new_value_etypes: |
|
365 # This is a same-rdef re-parenting: do not remove the entity |
|
366 continue |
|
367 to_be_removed = form.edited_entity |
|
368 self.info('Edition of %s is cancelled (deletion requested)', |
|
369 to_be_removed) |
|
370 rqlquery.canceled = True |
|
371 self.info('Scheduling removal of %s as composite relation ' |
|
372 '%s was removed', to_be_removed, rdef) |
|
373 form._cw.data['pending_composite_delete'].add(to_be_removed) |
314 |
374 |
315 def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): |
375 def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): |
316 """handle edition for the (rschema, x) relation of the given entity |
376 """handle edition for the (rschema, x) relation of the given entity |
317 """ |
377 """ |
318 if values: |
378 if values: |