[edit controller] Cancel RQL queries to be performed on entities to be deleted
authorFlorent Cayré <florent.cayre@logilab.fr>
Mon, 07 Dec 2015 11:58:17 +0100
changeset 11064 113e9da47afc
parent 11063 de20b0903d7d
child 11065 c7dbd10648e6
[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.
web/test/data/schema.py
web/test/unittest_application.py
web/views/editcontroller.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'
--- 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))
--- 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 <edited=%r restrictions=%r kwargs=%r>' % (
             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(