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