# HG changeset patch # User Florent Cayré # Date 1449485897 -3600 # Node ID 113e9da47afc821572acc17bcbb78da237e786e5 # Parent de20b0903d7df46c2bfc21178adac945352662e3 [edit controller] Cancel RQL queries to be performed on entities to be deleted Composite relation removal via the EditController trigger an entity removal. Changes on such a to-be-removed entity must be cancelled as they may trigger integrity errors before the deletion occurs. Closes #8529868. diff -r de20b0903d7d -r 113e9da47afc web/test/data/schema.py --- a/web/test/data/schema.py Fri Dec 04 14:56:20 2015 +0100 +++ b/web/test/data/schema.py Mon Dec 07 11:58:17 2015 +0100 @@ -102,7 +102,7 @@ object = 'Filesystem' class Directory(EntityType): - name = String() + name = String(required=True) class parent_directory(RelationDefinition): name = 'parent' diff -r de20b0903d7d -r 113e9da47afc web/test/unittest_application.py --- a/web/test/unittest_application.py Fri Dec 04 14:56:20 2015 +0100 +++ b/web/test/unittest_application.py Mon Dec 07 11:58:17 2015 +0100 @@ -258,18 +258,70 @@ self.assertEqual(forminfo['values'], req.form) def _edit_parent(self, dir_eid, parent_eid, role='subject', - etype='Directory'): + etype='Directory', **kwargs): parent_eid = parent_eid or '__cubicweb_internal_field__' with self.admin_access.web_request() as req: req.form = { 'eid': unicode(dir_eid), '__maineid': unicode(dir_eid), '__type:%s' % dir_eid: etype, - '_cw_entity_fields:%s' % dir_eid: 'parent-%s' % role, 'parent-%s:%s' % (role, dir_eid): parent_eid, } + req.form.update(kwargs) + req.form['_cw_entity_fields:%s' % dir_eid] = ','.join( + ['parent-%s' % role] + + [key.split(':')[0] + for key in kwargs.keys() + if not key.startswith('_')]) self.expect_redirect_handle_request(req) + def test_create_and_link_directories(self): + with self.admin_access.web_request() as req: + req.form = { + 'eid': (u'A', u'B'), + '__maineid': u'A', + '__type:A': 'Directory', + '__type:B': 'Directory', + 'parent-subject:B': u'A', + 'name-subject:A': u'topd', + 'name-subject:B': u'subd', + '_cw_entity_fields:A': 'name-subject', + '_cw_entity_fields:B': 'parent-subject,name-subject', + } + self.expect_redirect_handle_request(req) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', name=u'topd')) + self.assertTrue(cnx.find('Directory', name=u'subd')) + self.assertEqual(1, cnx.execute( + 'Directory SUBD WHERE SUBD parent TOPD,' + ' SUBD name "subd", TOPD name "topd"').rowcount) + + def test_create_subentity(self): + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + cnx.commit() + + with self.admin_access.web_request() as req: + req.form = { + 'eid': (unicode(topd.eid), u'B'), + '__maineid': unicode(topd.eid), + '__type:%s' % topd.eid: 'Directory', + '__type:B': 'Directory', + 'parent-object:%s' % topd.eid: u'B', + 'name-subject:B': u'subd', + '_cw_entity_fields:%s' % topd.eid: 'parent-object', + '_cw_entity_fields:B': 'name-subject', + } + self.expect_redirect_handle_request(req) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', name=u'topd')) + self.assertTrue(cnx.find('Directory', name=u'subd')) + self.assertEqual(1, cnx.execute( + 'Directory SUBD WHERE SUBD parent TOPD,' + ' SUBD name "subd", TOPD name "topd"').rowcount) + def test_subject_subentity_removal(self): """Editcontroller: detaching a composite relation removes the subentity (edit from the subject side) @@ -280,7 +332,8 @@ sub2 = cnx.create_entity('Directory', name=u'sub2', parent=topd) cnx.commit() - self._edit_parent(sub1.eid, parent_eid=None) + attrs = {'name-subject:%s' % sub1.eid: ''} + self._edit_parent(sub1.eid, parent_eid=None, **attrs) with self.admin_access.repo_cnx() as cnx: self.assertTrue(cnx.find('Directory', eid=topd.eid)) diff -r de20b0903d7d -r 113e9da47afc web/views/editcontroller.py --- a/web/views/editcontroller.py Fri Dec 04 14:56:20 2015 +0100 +++ b/web/views/editcontroller.py Mon Dec 07 11:58:17 2015 +0100 @@ -75,12 +75,14 @@ self.edited = [] self.restrictions = [] self.kwargs = {} + self.canceled = False def __repr__(self): return ('Query ' % ( self.edited, self.restrictions, self.kwargs)) def insert_query(self, etype): + assert not self.canceled if self.edited: rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited)) else: @@ -90,6 +92,7 @@ return rql def update_query(self, eid): + assert not self.canceled varmaker = rqlvar_maker() var = varmaker.next() while var in self.kwargs: @@ -268,13 +271,16 @@ # creation, add relevant data to the rqlquery for form_, field in req.data['pending_inlined'].pop(entity.eid, ()): rqlquery.set_inlined(field.name, form_.edited_entity.eid) - if self.errors: - errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors) - raise ValidationError(valerror_eid(entity.eid), errors) - if eid is None: # creation or copy - entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) - elif rqlquery.edited: # edition of an existant entity - self._update_entity(eid, rqlquery) + if not rqlquery.canceled: + if self.errors: + errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors) + raise ValidationError(valerror_eid(entity.eid), errors) + if eid is None: # creation or copy + entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery) + elif rqlquery.edited: # edition of an existant entity + self._update_entity(eid, rqlquery) + else: + self.errors = [] if is_main_entity: self.notify_edited(entity) if '__delete' in formparams: @@ -315,7 +321,7 @@ if unlinked_eids: # Special handling of composite relation removal self.handle_composite_removal( - form, field, unlinked_eids) + form, field, unlinked_eids, rqlquery) if rschema.inlined and rqlquery is not None and field.role == 'subject': self.handle_inlined_relation(form, field, value, origvalues, rqlquery) @@ -327,7 +333,7 @@ except ProcessFormError as exc: self.errors.append((field, exc)) - def handle_composite_removal(self, form, field, removed_values): + def handle_composite_removal(self, form, field, removed_values, rqlquery): """ In EditController-handled forms, when the user removes a composite relation, it triggers the removal of the related entity in the @@ -349,6 +355,9 @@ else: targettype = unlinked_entity.e_schema to_be_removed = form.edited_entity + self.info('Edition of %s is cancelled (deletion requested)', + to_be_removed) + rqlquery.canceled = True self.info('Scheduling removal of %s as composite relation ' '%s was removed', to_be_removed, rdef) form._cw.data['pending_composite_delete'].add(