15 # |
15 # |
16 # You should have received a copy of the GNU Lesser General Public License along |
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/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """The edit controller, automatically handling entity form submitting""" |
18 """The edit controller, automatically handling entity form submitting""" |
19 |
19 |
20 |
|
21 |
|
22 from warnings import warn |
20 from warnings import warn |
23 from collections import defaultdict |
21 from collections import defaultdict |
24 |
22 |
25 from datetime import datetime |
23 from datetime import datetime |
26 |
24 |
27 from six import text_type |
25 from six import text_type |
28 |
26 |
29 from logilab.common.deprecation import deprecated |
|
30 from logilab.common.graph import ordered_nodes |
27 from logilab.common.graph import ordered_nodes |
31 |
28 |
32 from rql.utils import rqlvar_maker |
29 from rql.utils import rqlvar_maker |
33 |
30 |
34 from cubicweb import _, Binary, ValidationError, UnknownEid |
31 from cubicweb import _, ValidationError, UnknownEid |
35 from cubicweb.view import EntityAdapter |
32 from cubicweb.view import EntityAdapter |
36 from cubicweb.predicates import is_instance |
33 from cubicweb.predicates import is_instance |
37 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, |
34 from cubicweb.web import RequestError, NothingToEdit, ProcessFormError |
38 ProcessFormError) |
|
39 from cubicweb.web.views import basecontrollers, autoform |
35 from cubicweb.web.views import basecontrollers, autoform |
40 |
36 |
41 |
37 |
42 class IEditControlAdapter(EntityAdapter): |
38 class IEditControlAdapter(EntityAdapter): |
43 __regid__ = 'IEditControl' |
39 __regid__ = 'IEditControl' |
196 req.data['pending_inlined'] = defaultdict(set) |
193 req.data['pending_inlined'] = defaultdict(set) |
197 req.data['pending_others'] = set() |
194 req.data['pending_others'] = set() |
198 req.data['pending_composite_delete'] = set() |
195 req.data['pending_composite_delete'] = set() |
199 try: |
196 try: |
200 for formparams in self._ordered_formparams(): |
197 for formparams in self._ordered_formparams(): |
201 eid = self.edit_entity(formparams) |
198 self.edit_entity(formparams) |
202 except (RequestError, NothingToEdit) as ex: |
199 except (RequestError, NothingToEdit) as ex: |
203 if '__linkto' in req.form and 'eid' in req.form: |
200 if '__linkto' in req.form and 'eid' in req.form: |
204 self.execute_linkto() |
201 self.execute_linkto() |
205 elif '__delete' not in req.form: |
202 elif '__delete' not in req.form: |
206 raise ValidationError(None, {None: text_type(ex)}) |
203 raise ValidationError(None, {None: text_type(ex)}) |
234 try: |
231 try: |
235 entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0) |
232 entity = self._cw.execute(rql, rqlquery.kwargs).get_entity(0, 0) |
236 neweid = entity.eid |
233 neweid = entity.eid |
237 except ValidationError as ex: |
234 except ValidationError as ex: |
238 self._to_create[eid] = ex.entity |
235 self._to_create[eid] = ex.entity |
239 if self._cw.ajax_request: # XXX (syt) why? |
236 if self._cw.ajax_request: # XXX (syt) why? |
240 ex.entity = eid |
237 ex.entity = eid |
241 raise |
238 raise |
242 self._to_create[eid] = neweid |
239 self._to_create[eid] = neweid |
243 return neweid |
240 return neweid |
244 |
241 |
266 # inbetween, use cubicweb standard formid for inlined forms |
263 # inbetween, use cubicweb standard formid for inlined forms |
267 formid = 'edition' |
264 formid = 'edition' |
268 form = req.vreg['forms'].select(formid, req, entity=entity) |
265 form = req.vreg['forms'].select(formid, req, entity=entity) |
269 eid = form.actual_eid(entity.eid) |
266 eid = form.actual_eid(entity.eid) |
270 editedfields = formparams['_cw_entity_fields'] |
267 editedfields = formparams['_cw_entity_fields'] |
271 form.formvalues = {} # init fields value cache |
268 form.formvalues = {} # init fields value cache |
272 for field in form.iter_modified_fields(editedfields, entity): |
269 for field in form.iter_modified_fields(editedfields, entity): |
273 self.handle_formfield(form, field, rqlquery) |
270 self.handle_formfield(form, field, rqlquery) |
274 # if there are some inlined field which were waiting for this entity's |
271 # if there are some inlined field which were waiting for this entity's |
275 # creation, add relevant data to the rqlquery |
272 # creation, add relevant data to the rqlquery |
276 for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): |
273 for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): |
277 rqlquery.set_inlined(field.name, form_.edited_entity.eid) |
274 rqlquery.set_inlined(field.name, form_.edited_entity.eid) |
278 if not rqlquery.canceled: |
275 if not rqlquery.canceled: |
279 if self.errors: |
276 if self.errors: |
280 errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) |
277 errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors) |
281 raise ValidationError(valerror_eid(entity.eid), errors) |
278 raise ValidationError(valerror_eid(entity.eid), errors) |
282 if eid is None: # creation or copy |
279 if eid is None: # creation or copy |
283 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) |
280 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) |
284 elif rqlquery.edited: # edition of an existant entity |
281 elif rqlquery.edited: # edition of an existant entity |
285 self.check_concurrent_edition(formparams, eid) |
282 self.check_concurrent_edition(formparams, eid) |
286 self._update_entity(eid, rqlquery) |
283 self._update_entity(eid, rqlquery) |
287 else: |
284 else: |
288 self.errors = [] |
285 self.errors = [] |
289 if is_main_entity: |
286 if is_main_entity: |
292 # XXX deprecate? |
289 # XXX deprecate? |
293 todelete = req.list_form_param('__delete', formparams, pop=True) |
290 todelete = req.list_form_param('__delete', formparams, pop=True) |
294 autoform.delete_relations(req, todelete) |
291 autoform.delete_relations(req, todelete) |
295 if '__cloned_eid' in formparams: |
292 if '__cloned_eid' in formparams: |
296 entity.copy_relations(int(formparams['__cloned_eid'])) |
293 entity.copy_relations(int(formparams['__cloned_eid'])) |
297 if is_main_entity: # only execute linkto for the main entity |
294 if is_main_entity: # only execute linkto for the main entity |
298 self.execute_linkto(entity.eid) |
295 self.execute_linkto(entity.eid) |
299 return eid |
296 return eid |
300 |
297 |
301 def handle_formfield(self, form, field, rqlquery=None): |
298 def handle_formfield(self, form, field, rqlquery=None): |
302 entity = form.edited_entity |
299 entity = form.edited_entity |
303 eschema = entity.e_schema |
300 eschema = entity.e_schema |
304 try: |
301 try: |
305 for field, value in field.process_posted(form): |
302 for field, value in field.process_posted(form): |
306 if not ( |
303 if not ((field.role == 'subject' and field.name in eschema.subjrels) |
307 (field.role == 'subject' and field.name in eschema.subjrels) |
304 or |
308 or |
305 (field.role == 'object' and field.name in eschema.objrels)): |
309 (field.role == 'object' and field.name in eschema.objrels)): |
|
310 continue |
306 continue |
311 |
307 |
312 rschema = self._cw.vreg.schema.rschema(field.name) |
308 rschema = self._cw.vreg.schema.rschema(field.name) |
313 if rschema.final: |
309 if rschema.final: |
314 rqlquery.set_attribute(field.name, value) |
310 rqlquery.set_attribute(field.name, value) |
315 continue |
311 continue |
316 |
312 |
317 if entity.has_eid(): |
313 if entity.has_eid(): |
318 origvalues = set(data[0] for data in entity.related(field.name, field.role).rows) |
314 origvalues = set(row[0] for row in entity.related(field.name, field.role).rows) |
319 else: |
315 else: |
320 origvalues = set() |
316 origvalues = set() |
321 if value is None or value == origvalues: |
317 if value is None or value == origvalues: |
322 continue # not edited / not modified / to do later |
318 continue # not edited / not modified / to do later |
323 |
319 |
324 unlinked_eids = origvalues - value |
320 unlinked_eids = origvalues - value |
325 |
321 |
326 if unlinked_eids: |
322 if unlinked_eids: |
327 # Special handling of composite relation removal |
323 # Special handling of composite relation removal |
331 if rschema.inlined and rqlquery is not None and field.role == 'subject': |
327 if rschema.inlined and rqlquery is not None and field.role == 'subject': |
332 self.handle_inlined_relation(form, field, value, origvalues, rqlquery) |
328 self.handle_inlined_relation(form, field, value, origvalues, rqlquery) |
333 elif form.edited_entity.has_eid(): |
329 elif form.edited_entity.has_eid(): |
334 self.handle_relation(form, field, value, origvalues) |
330 self.handle_relation(form, field, value, origvalues) |
335 else: |
331 else: |
336 form._cw.data['pending_others'].add( (form, field) ) |
332 form._cw.data['pending_others'].add((form, field)) |
337 |
333 |
338 except ProcessFormError as exc: |
334 except ProcessFormError as exc: |
339 self.errors.append((field, exc)) |
335 self.errors.append((field, exc)) |
340 |
336 |
341 def handle_composite_removal(self, form, field, |
337 def handle_composite_removal(self, form, field, |
385 self.handle_relation(form, field, values, origvalues) |
381 self.handle_relation(form, field, values, origvalues) |
386 |
382 |
387 def handle_relation(self, form, field, values, origvalues): |
383 def handle_relation(self, form, field, values, origvalues): |
388 """handle edition for the (rschema, x) relation of the given entity |
384 """handle edition for the (rschema, x) relation of the given entity |
389 """ |
385 """ |
390 etype = form.edited_entity.e_schema |
|
391 rschema = self._cw.vreg.schema.rschema(field.name) |
386 rschema = self._cw.vreg.schema.rschema(field.name) |
392 if field.role == 'subject': |
387 if field.role == 'subject': |
393 desttype = rschema.objects(etype)[0] |
|
394 card = rschema.rdef(etype, desttype).cardinality[0] |
|
395 subjvar, objvar = 'X', 'Y' |
388 subjvar, objvar = 'X', 'Y' |
396 else: |
389 else: |
397 desttype = rschema.subjects(etype)[0] |
|
398 card = rschema.rdef(desttype, etype).cardinality[1] |
|
399 subjvar, objvar = 'Y', 'X' |
390 subjvar, objvar = 'Y', 'X' |
400 eid = form.edited_entity.eid |
391 eid = form.edited_entity.eid |
401 if field.role == 'object' or not rschema.inlined or not values: |
392 if field.role == 'object' or not rschema.inlined or not values: |
402 # this is not an inlined relation or no values specified, |
393 # this is not an inlined relation or no values specified, |
403 # explicty remove relations |
394 # explicty remove relations |
417 redirect_info = set() |
408 redirect_info = set() |
418 eidtypes = tuple(eidtypes) |
409 eidtypes = tuple(eidtypes) |
419 for eid, etype in eidtypes: |
410 for eid, etype in eidtypes: |
420 entity = self._cw.entity_from_eid(eid, etype) |
411 entity = self._cw.entity_from_eid(eid, etype) |
421 path, params = entity.cw_adapt_to('IEditControl').after_deletion_path() |
412 path, params = entity.cw_adapt_to('IEditControl').after_deletion_path() |
422 redirect_info.add( (path, tuple(params.items())) ) |
413 redirect_info.add((path, tuple(params.items()))) |
423 entity.cw_delete() |
414 entity.cw_delete() |
424 if len(redirect_info) > 1: |
415 if len(redirect_info) > 1: |
425 # In the face of ambiguity, refuse the temptation to guess. |
416 # In the face of ambiguity, refuse the temptation to guess. |
426 self._after_deletion_path = 'view', () |
417 self._after_deletion_path = 'view', () |
427 else: |
418 else: |
428 self._after_deletion_path = next(iter(redirect_info)) |
419 self._after_deletion_path = next(iter(redirect_info)) |
429 if len(eidtypes) > 1: |
420 if len(eidtypes) > 1: |
430 self._cw.set_message(self._cw._('entities deleted')) |
421 self._cw.set_message(self._cw._('entities deleted')) |
431 else: |
422 else: |
432 self._cw.set_message(self._cw._('entity deleted')) |
423 self._cw.set_message(self._cw._('entity deleted')) |
433 |
|
434 |
424 |
435 def check_concurrent_edition(self, formparams, eid): |
425 def check_concurrent_edition(self, formparams, eid): |
436 req = self._cw |
426 req = self._cw |
437 try: |
427 try: |
438 form_ts = datetime.utcfromtimestamp(float(formparams['__form_generation_time'])) |
428 form_ts = datetime.utcfromtimestamp(float(formparams['__form_generation_time'])) |
444 # We only mark the message for translation but the actual |
434 # We only mark the message for translation but the actual |
445 # translation will be handled by the Validation mechanism... |
435 # translation will be handled by the Validation mechanism... |
446 msg = _("Entity %(eid)s has changed since you started to edit it." |
436 msg = _("Entity %(eid)s has changed since you started to edit it." |
447 " Reload the page and reapply your changes.") |
437 " Reload the page and reapply your changes.") |
448 # ... this is why we pass the formats' dict as a third argument. |
438 # ... this is why we pass the formats' dict as a third argument. |
449 raise ValidationError(eid, {None: msg}, {'eid' : eid}) |
439 raise ValidationError(eid, {None: msg}, {'eid': eid}) |
450 |
440 |
451 def _action_apply(self): |
441 def _action_apply(self): |
452 self._default_publish() |
442 self._default_publish() |
453 self.reset() |
443 self.reset() |
454 |
444 |