# HG changeset patch # User Julien Cristau # Date 1455644500 -3600 # Node ID fedcb69982afaaba84bc280c44a8eb15ed360749 # Parent ec44983e047c80507b2a32a33c8f1c42c0ccf5f5# Parent 79b124ec1157ca4bcb3bac3b2b78db36eda61316 merge changes from 3.20.13 diff -r ec44983e047c -r fedcb69982af .hgtags --- a/.hgtags Thu Jan 07 12:20:50 2016 +0100 +++ b/.hgtags Tue Feb 16 18:41:40 2016 +0100 @@ -472,6 +472,9 @@ f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 3.19.13 f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 debian/3.19.13-1 f933a38d7ab5fc6f2ad593fe1cf9985ce9d7e873 centos/3.19.13-1 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 3.19.14 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 debian/3.19.14-1 +72a0f70879ac40ea57575be90bc6427f61ce3bd6 centos/3.19.14-1 7e6b7739afe6128589ad51b0318decb767cbae36 3.20.0 7e6b7739afe6128589ad51b0318decb767cbae36 debian/3.20.0-1 7e6b7739afe6128589ad51b0318decb767cbae36 centos/3.20.0-1 @@ -511,6 +514,9 @@ 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 3.20.12 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 debian/3.20.12-1 03e8fc9f79a6e489a1b5c695eb0cd3fbb1afe9d4 centos/3.20.12-1 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a 3.20.13 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a debian/3.20.13-1 +8c5dabbcd4d9505c3a617f9dbe2b10172bdc2b3a centos/3.20.13-1 887c6eef807781560adcd4ecd2dea9011f5a6681 3.21.0 887c6eef807781560adcd4ecd2dea9011f5a6681 debian/3.21.0-1 887c6eef807781560adcd4ecd2dea9011f5a6681 centos/3.21.0-1 diff -r ec44983e047c -r fedcb69982af __pkginfo__.py diff -r ec44983e047c -r fedcb69982af cubicweb.spec diff -r ec44983e047c -r fedcb69982af debian/changelog --- a/debian/changelog Thu Jan 07 12:20:50 2016 +0100 +++ b/debian/changelog Tue Feb 16 18:41:40 2016 +0100 @@ -40,6 +40,12 @@ -- Julien Cristau Fri, 10 Jul 2015 17:04:11 +0200 +cubicweb (3.20.13-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Tue, 16 Feb 2016 17:53:29 +0100 + cubicweb (3.20.12-1) unstable; urgency=medium * new upstream release. @@ -124,6 +130,12 @@ -- Julien Cristau Tue, 06 Jan 2015 18:11:03 +0100 +cubicweb (3.19.14-1) unstable; urgency=medium + + * new upstream release + + -- Julien Cristau Tue, 16 Feb 2016 11:01:47 +0100 + cubicweb (3.19.13-1) unstable; urgency=medium * New upstream release. diff -r ec44983e047c -r fedcb69982af hooks/syncsession.py --- a/hooks/syncsession.py Thu Jan 07 12:20:50 2016 +0100 +++ b/hooks/syncsession.py Tue Feb 16 18:41:40 2016 +0100 @@ -148,7 +148,8 @@ """the observed connections set has been commited""" cwprop = self.cwprop if not cwprop.for_user: - self.cnx.vreg['propertyvalues'][cwprop.pkey] = cwprop.value + self.cnx.vreg['propertyvalues'][cwprop.pkey] = \ + self.cnx.vreg.typed_value(cwprop.pkey, cwprop.value) # if for_user is set, update is handled by a ChangeCWPropertyOp operation diff -r ec44983e047c -r fedcb69982af hooks/test/unittest_syncsession.py --- a/hooks/test/unittest_syncsession.py Thu Jan 07 12:20:50 2016 +0100 +++ b/hooks/test/unittest_syncsession.py Tue Feb 16 18:41:40 2016 +0100 @@ -69,6 +69,15 @@ req.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop"') self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'}) + def test_vreg_propertyvalues_update(self): + self.vreg.register_property( + 'test.int', type='Int', help='', sitewide=True) + with self.admin_access.repo_cnx() as cnx: + cnx.execute('INSERT CWProperty X: X pkey "test.int", X value "42"') + cnx.commit() + self.assertEqual(self.vreg.property_value('test.int'), 42) + + if __name__ == '__main__': from logilab.common.testlib import unittest_main unittest_main() diff -r ec44983e047c -r fedcb69982af server/sources/native.py --- a/server/sources/native.py Thu Jan 07 12:20:50 2016 +0100 +++ b/server/sources/native.py Tue Feb 16 18:41:40 2016 +0100 @@ -40,7 +40,7 @@ from logilab.common.decorators import cached, clear_cache from logilab.common.configuration import Method -from logilab.common.shellutils import getlogin +from logilab.common.shellutils import getlogin, ASK from logilab.database import get_db_helper, sqlgen from yams.schema import role_name @@ -1742,15 +1742,20 @@ tables = archive.read('tables.txt').splitlines() sequences = archive.read('sequences.txt').splitlines() numranges = archive.read('numranges.txt').splitlines() - file_versions = self._parse_versions(archive.read('versions.txt')) - versions = set(self._get_versions()) - if file_versions != versions: - self.logger.critical('Unable to restore : versions do not match') - self.logger.critical('Expected:\n%s', '\n'.join('%s : %s' % (cube, ver) - for cube, ver in sorted(versions))) - self.logger.critical('Found:\n%s', '\n'.join('%s : %s' % (cube, ver) - for cube, ver in sorted(file_versions))) - raise ValueError('Unable to restore : versions do not match') + archive_versions = self._parse_versions(archive.read('versions.txt')) + db_versions = set(self._get_versions()) + if archive_versions != db_versions: + self.logger.critical('Restore warning: versions do not match') + new_cubes = db_versions - archive_versions + if new_cubes: + self.logger.critical('In the db:\n%s', '\n'.join('%s: %s' % (cube, ver) + for cube, ver in sorted(new_cubes))) + old_cubes = archive_versions - db_versions + if old_cubes: + self.logger.critical('In the archive:\n%s', '\n'.join('%s: %s' % (cube, ver) + for cube, ver in sorted(old_cubes))) + if not ASK.confirm('Versions mismatch: continue anyway ?', False): + raise ValueError('Unable to restore: versions do not match') table_chunks = {} for name in archive.namelist(): if not name.startswith('tables/'): diff -r ec44983e047c -r fedcb69982af web/formfields.py --- a/web/formfields.py Thu Jan 07 12:20:50 2016 +0100 +++ b/web/formfields.py Tue Feb 16 18:41:40 2016 +0100 @@ -76,7 +76,7 @@ from yams.constraints import (SizeConstraint, StaticVocabularyConstraint, FormatConstraint) -from cubicweb import Binary, tags, uilib +from cubicweb import Binary, tags, uilib, neg_role from cubicweb.utils import support_args from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \ formwidgets as fw @@ -1200,10 +1200,13 @@ else: targetschema = rdef.subject card = rdef.role_cardinality(role) + composite = getattr(rdef, 'composite', None) kwargs['name'] = rschema.type kwargs['role'] = role kwargs['eidparam'] = True - kwargs.setdefault('required', card in '1+') + # don't mark composite relation as required, we want the composite element + # to be removed when not linked to its parent + kwargs.setdefault('required', card in '1+' and composite != neg_role(role)) if role == 'object': kwargs.setdefault('label', (eschema.type, rschema.type + '_object')) else: diff -r ec44983e047c -r fedcb69982af web/test/data/schema.py --- a/web/test/data/schema.py Thu Jan 07 12:20:50 2016 +0100 +++ b/web/test/data/schema.py Tue Feb 16 18:41:40 2016 +0100 @@ -92,6 +92,33 @@ class Ticket(EntityType): title = String(maxsize=32, required=True, fulltextindexed=True) concerns = SubjectRelation('Project', composite='object') + in_version = SubjectRelation('Version', composite='object', + cardinality='?*', inlined=True) + +class Version(EntityType): + name = String(required=True) + +class Filesystem(EntityType): + name = String() + +class DirectoryPermission(EntityType): + value = String() + +class parent_fs(RelationDefinition): + name = 'parent' + subject = 'Directory' + object = 'Filesystem' + +class Directory(EntityType): + name = String(required=True) + has_permission = SubjectRelation('DirectoryPermission', cardinality='*1', + composite='subject') + +class parent_directory(RelationDefinition): + name = 'parent' + subject = 'Directory' + object = 'Directory' + composite = 'object' class Folder(EntityType): name = String(required=True) diff -r ec44983e047c -r fedcb69982af web/test/unittest_application.py --- a/web/test/unittest_application.py Thu Jan 07 12:20:50 2016 +0100 +++ b/web/test/unittest_application.py Tue Feb 16 18:41:40 2016 +0100 @@ -23,7 +23,6 @@ from logilab.common.testlib import TestCase, unittest_main from logilab.common.decorators import clear_cache, classproperty -from cubicweb import AuthenticationError from cubicweb import view from cubicweb.devtools.testlib import CubicWebTC, real_error_handling from cubicweb.devtools.fake import FakeRequest @@ -257,6 +256,259 @@ {'login-subject': u'the value "admin" is already used, use another one'}) self.assertEqual(forminfo['values'], req.form) + def _edit_parent(self, dir_eid, parent_eid, role='subject', + 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, + '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 _edit_in_version(self, ticket_eid, version_eid, **kwargs): + version_eid = version_eid or '__cubicweb_internal_field__' + with self.admin_access.web_request() as req: + req.form = { + 'eid': unicode(ticket_eid), + '__maineid': unicode(ticket_eid), + '__type:%s' % ticket_eid: 'Ticket', + 'in_version-subject:%s' % ticket_eid: version_eid, + } + req.form.update(kwargs) + req.form['_cw_entity_fields:%s' % ticket_eid] = ','.join( + ['in_version-subject'] + + [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) + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + sub1 = cnx.create_entity('Directory', name=u'sub1', parent=topd) + sub2 = cnx.create_entity('Directory', name=u'sub2', parent=topd) + cnx.commit() + + 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)) + self.assertFalse(cnx.find('Directory', eid=sub1.eid)) + self.assertTrue(cnx.find('Directory', eid=sub2.eid)) + + def test_object_subentity_removal(self): + """Editcontroller: detaching a composite relation removes the subentity + (edit from the object side) + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + sub1 = cnx.create_entity('Directory', name=u'sub1', parent=topd) + sub2 = cnx.create_entity('Directory', name=u'sub2', parent=topd) + cnx.commit() + + self._edit_parent(topd.eid, parent_eid=sub1.eid, role='object') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=sub1.eid)) + self.assertFalse(cnx.find('Directory', eid=sub2.eid)) + + def test_reparent_subentity(self): + "Editcontroller: re-parenting a subentity does not remove it" + with self.admin_access.repo_cnx() as cnx: + top1 = cnx.create_entity('Directory', name=u'top1') + top2 = cnx.create_entity('Directory', name=u'top2') + subd = cnx.create_entity('Directory', name=u'subd', parent=top1) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=top2.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=top1.eid)) + self.assertTrue(cnx.find('Directory', eid=top2.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertEqual( + cnx.find('Directory', eid=subd.eid).one().parent[0], top2) + + def test_reparent_subentity_inlined(self): + """Editcontroller: re-parenting a subentity does not remove it + (inlined case)""" + with self.admin_access.repo_cnx() as cnx: + version1 = cnx.create_entity('Version', name=u'version1') + version2 = cnx.create_entity('Version', name=u'version2') + ticket = cnx.create_entity('Ticket', title=u'ticket', + in_version=version1) + cnx.commit() + + self._edit_in_version(ticket.eid, version_eid=version2.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Version', eid=version1.eid)) + self.assertTrue(cnx.find('Version', eid=version2.eid)) + self.assertTrue(cnx.find('Ticket', eid=ticket.eid)) + self.assertEqual( + cnx.find('Ticket', eid=ticket.eid).one().in_version[0], version2) + + def test_subject_mixed_composite_subentity_removal_1(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove non composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=topd.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, + [topd,]) + + def test_subject_mixed_composite_subentity_removal_2(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(subd.eid, parent_eid=fs.eid) + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertFalse(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + + def test_object_mixed_composite_subentity_removal_1(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove non composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(fs.eid, parent_eid=None, role='object', + etype='Filesystem') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertTrue(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + self.assertEqual(cnx.find('Directory', eid=subd.eid).one().parent, + [topd,]) + + def test_object_mixed_composite_subentity_removal_2(self): + """Editcontroller: detaching several subentities respects each rdef's + compositeness - Remove composite + """ + with self.admin_access.repo_cnx() as cnx: + topd = cnx.create_entity('Directory', name=u'topd') + fs = cnx.create_entity('Filesystem', name=u'/tmp') + subd = cnx.create_entity('Directory', name=u'subd', + parent=(topd, fs)) + cnx.commit() + + self._edit_parent(topd.eid, parent_eid=None, role='object') + + with self.admin_access.repo_cnx() as cnx: + self.assertTrue(cnx.find('Directory', eid=topd.eid)) + self.assertFalse(cnx.find('Directory', eid=subd.eid)) + self.assertTrue(cnx.find('Filesystem', eid=fs.eid)) + + def test_delete_mandatory_composite(self): + with self.admin_access.repo_cnx() as cnx: + perm = cnx.create_entity('DirectoryPermission') + mydir = cnx.create_entity('Directory', name=u'dir', + has_permission=perm) + cnx.commit() + + with self.admin_access.web_request() as req: + dir_eid = unicode(mydir.eid) + perm_eid = unicode(perm.eid) + req.form = { + 'eid': [dir_eid, perm_eid], + '__maineid' : dir_eid, + '__type:%s' % dir_eid: 'Directory', + '__type:%s' % perm_eid: 'DirectoryPermission', + '_cw_entity_fields:%s' % dir_eid: '', + '_cw_entity_fields:%s' % perm_eid: 'has_permission-object', + 'has_permission-object:%s' % perm_eid: '', + } + path, _params = self.expect_redirect_handle_request(req, 'edit') + self.assertTrue(req.find('Directory', eid=mydir.eid)) + self.assertFalse(req.find('DirectoryPermission', eid=perm.eid)) + def test_ajax_view_raise_arbitrary_error(self): class ErrorAjaxView(view.View): __regid__ = 'test.ajax.error' diff -r ec44983e047c -r fedcb69982af web/views/debug.py diff -r ec44983e047c -r fedcb69982af web/views/editcontroller.py --- a/web/views/editcontroller.py Thu Jan 07 12:20:50 2016 +0100 +++ b/web/views/editcontroller.py Tue Feb 16 18:41:40 2016 +0100 @@ -29,7 +29,7 @@ from rql.utils import rqlvar_maker -from cubicweb import Binary, ValidationError +from cubicweb import Binary, ValidationError, UnknownEid from cubicweb.view import EntityAdapter from cubicweb.predicates import is_instance from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit, @@ -77,12 +77,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: @@ -92,6 +94,7 @@ return rql def update_query(self, eid): + assert not self.canceled varmaker = rqlvar_maker() var = varmaker.next() while var in self.kwargs: @@ -190,6 +193,7 @@ # deserves special treatment req.data['pending_inlined'] = defaultdict(set) req.data['pending_others'] = set() + req.data['pending_composite_delete'] = set() try: for formparams in self._ordered_formparams(): eid = self.edit_entity(formparams) @@ -208,6 +212,9 @@ # then execute rql to set all relations for querydef in self.relations_rql: self._cw.execute(*querydef) + # delete pending composite + for entity in req.data['pending_composite_delete']: + entity.cw_delete() # XXX this processes *all* pending operations of *all* entities if '__delete' in req.form: todelete = req.list_form_param('__delete', req.form, pop=True) @@ -264,14 +271,17 @@ # 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.check_concurrent_edition(formparams, eid) - 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.check_concurrent_edition(formparams, eid) + self._update_entity(eid, rqlquery) + else: + self.errors = [] if is_main_entity: self.notify_edited(entity) if '__delete' in formparams: @@ -285,7 +295,8 @@ return eid def handle_formfield(self, form, field, rqlquery=None): - eschema = form.edited_entity.e_schema + entity = form.edited_entity + eschema = entity.e_schema try: for field, value in field.process_posted(form): if not ( @@ -293,25 +304,74 @@ or (field.role == 'object' and field.name in eschema.objrels)): continue + rschema = self._cw.vreg.schema.rschema(field.name) if rschema.final: rqlquery.set_attribute(field.name, value) + continue + + if entity.has_eid(): + origvalues = set(data[0] for data in entity.related(field.name, field.role).rows) else: - if form.edited_entity.has_eid(): - origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True)) - else: - origvalues = set() - if value is None or value == origvalues: - continue # not edited / not modified / to do later - if rschema.inlined and rqlquery is not None and field.role == 'subject': - self.handle_inlined_relation(form, field, value, origvalues, rqlquery) - elif form.edited_entity.has_eid(): - self.handle_relation(form, field, value, origvalues) - else: - form._cw.data['pending_others'].add( (form, field) ) + origvalues = set() + if value is None or value == origvalues: + continue # not edited / not modified / to do later + + unlinked_eids = origvalues - value + + if unlinked_eids: + # Special handling of composite relation removal + self.handle_composite_removal( + form, field, unlinked_eids, value, rqlquery) + + if rschema.inlined and rqlquery is not None and field.role == 'subject': + self.handle_inlined_relation(form, field, value, origvalues, rqlquery) + elif form.edited_entity.has_eid(): + self.handle_relation(form, field, value, origvalues) + else: + form._cw.data['pending_others'].add( (form, field) ) + except ProcessFormError as exc: self.errors.append((field, exc)) + def handle_composite_removal(self, form, field, + removed_values, new_values, rqlquery): + """ + In EditController-handled forms, when the user removes a composite + relation, it triggers the removal of the related entity in the + composite. This is where this happens. + + See for instance test_subject_subentity_removal in + web/test/unittest_application.py. + """ + rschema = self._cw.vreg.schema.rschema(field.name) + new_value_etypes = set() + # the user could have included nonexisting eids in the POST; don't crash. + for eid in new_values: + try: + new_value_etypes.add(self._cw.entity_from_eid(eid).cw_etype) + except UnknownEid: + continue + for unlinked_eid in removed_values: + unlinked_entity = self._cw.entity_from_eid(unlinked_eid) + rdef = rschema.role_rdef(form.edited_entity.cw_etype, + unlinked_entity.cw_etype, + field.role) + if rdef.composite is not None: + if rdef.composite == field.role: + to_be_removed = unlinked_entity + else: + if unlinked_entity.cw_etype in new_value_etypes: + # This is a same-rdef re-parenting: do not remove the entity + continue + 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(to_be_removed) + def handle_inlined_relation(self, form, field, values, origvalues, rqlquery): """handle edition for the (rschema, x) relation of the given entity """