[PATCH 5 of 6 eac] Delete a "ternary" relation between two AuthorityRecord when one side is deleted

Denis Laxalde denis.laxalde at logilab.fr
Tue Oct 30 11:14:57 CET 2018


# HG changeset patch
# User Denis Laxalde <denis.laxalde at logilab.fr>
# Date 1540891067 -3600
#      Tue Oct 30 10:17:47 2018 +0100
# Node ID 07d07d112d61e82a1750d8f6422e8f796318b54f
# Parent  4ec35c9cdd51ff4dc6c58f3fc8778598ea97ff4a
# Available At http://hg.logilab.org/review/cubes/eac
#              hg pull http://hg.logilab.org/review/cubes/eac -r 07d07d112d61
Delete a "ternary" relation between two AuthorityRecord when one side is deleted

We have several "ternary" (or "qualified") relations defined in
schema, i.e. an entity type with two required relationships, e.g.
AssociationRelation entity type and association_from and association_to
relation definitions. When such a "ternary" relation involved two
AuthorityRecord entities on each this (as AssociationRelation for
instance), it's not possible to delete one of them as this would break
carninality of one of the relationship. Thus leading to the following
error message:

  ValidationError: <eid> (association_to-subject): at least one relation
    association_to is required on AssociationRelation (<eid>)

To sort this out and allow deletion of an AuthorityRecord entity
involved in such "ternary" relations, we implement a new hook that
deleted these "ternary" relations (entities actually) when one of their
target is deleted. When this is not allowed, per permissions
constraints, we issue a validation error indicating which items blocks
the transaction.

diff --git a/cubicweb_eac/hooks.py b/cubicweb_eac/hooks.py
--- a/cubicweb_eac/hooks.py
+++ b/cubicweb_eac/hooks.py
@@ -15,9 +15,55 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 """cubicweb-eac specific hooks and operations"""
 
+from cubicweb import (
+    _,
+    ValidationError,
+)
+from cubicweb.predicates import is_instance
 from cubicweb.server import hook
 
 
+class DeleteTernaryRelationsHook(hook.Hook):
+    """When an AuthorityRecord entity is being deleted we need to delete all
+    "qualified" (ternary) relations with another AuthorityRecord, otherwise
+    we'll get a validation error about one side of the ternary relation being
+    missing.
+    """
+    __select__ = hook.Hook.__select__ & is_instance('AuthorityRecord')
+    __regid__ = 'eac.delete-ternary-relations'
+    events = ('before_delete_entity', )
+
+    def __call__(self):
+        errors, msgargs = {}, {}
+        for rtype in (
+            'association_from',
+            'association_to',
+            'chronological_predecessor',
+            'chronological_successor',
+            'hierarchical_parent',
+            'hierarchical_child',
+        ):
+            rset = self.entity.related(rtype, role='object')
+            if not rset:
+                continue
+            relation = rset.one()
+            if self._cw.deleted_in_transaction(relation.eid):
+                continue
+            if relation.cw_has_perm('delete'):
+                self.info('deleting "%s" as %s-object is being deleted',
+                          relation.dc_title(), rtype)
+                relation.cw_delete()
+            else:
+                argname = '{}-subject'.format(rtype)
+                errors[rtype] = _(
+                    '"%({})s" would need to be deleted alongside '
+                    'the AuthorityRecord but this is disallowed'
+                ).format(argname)
+                msgargs[argname] = relation.dc_title()
+        if errors:
+            raise ValidationError(self.entity.eid, errors, msgargs)
+
+
 # ensure that equivalent_concept Concept has vocabulary_source defined ####################
 
 class EnsureVocabularySource(hook.Hook):
diff --git a/cubicweb_eac/i18n/en.po b/cubicweb_eac/i18n/en.po
--- a/cubicweb_eac/i18n/en.po
+++ b/cubicweb_eac/i18n/en.po
@@ -7,6 +7,12 @@ msgstr ""
 "Generated-By: pygettext.py 1.5\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
+#, python-format
+msgid ""
+"\"%({})s\" would need to be deleted alongside the AuthorityRecord but this "
+"is disallowed"
+msgstr ""
+
 msgid "A source used to establish the description of an AuthorityRecord"
 msgstr ""
 
diff --git a/cubicweb_eac/i18n/fr.po b/cubicweb_eac/i18n/fr.po
--- a/cubicweb_eac/i18n/fr.po
+++ b/cubicweb_eac/i18n/fr.po
@@ -7,6 +7,14 @@ msgstr ""
 "Generated-By: pygettext.py 1.5\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
+#, python-format
+msgid ""
+"\"%({})s\" would need to be deleted alongside the AuthorityRecord but this "
+"is disallowed"
+msgstr ""
+"\"%({})s\" devrait être supprimé avec la notice d'authorité mais ce n'est "
+"pas permis"
+
 msgid "A source used to establish the description of an AuthorityRecord"
 msgstr ""
 "Une source utilisée pour l'établissement de la description d'une notice"
diff --git a/test/test_hooks.py b/test/test_hooks.py
new file mode 100644
--- /dev/null
+++ b/test/test_hooks.py
@@ -0,0 +1,55 @@
+
+from cubicweb import ValidationError
+from cubicweb.devtools.testlib import CubicWebTC
+
+from cubicweb_eac import testutils
+
+
+class TernaryRelationDeletionHookTC(CubicWebTC):
+
+    def setup_database(self):
+        with self.admin_access.cnx() as cnx:
+            self.arecord1_eid = testutils.authority_record(cnx, u'alice').eid
+            self.arecord2_eid = testutils.authority_record(cnx, u'bob').eid
+            cnx.create_entity('AssociationRelation',
+                              entry=u'alice and bob are friends',
+                              association_from=self.arecord1_eid,
+                              association_to=self.arecord2_eid)
+            cnx.create_entity('ChronologicalRelation',
+                              entry=u'alice is bob\'s mother',
+                              chronological_predecessor=self.arecord1_eid,
+                              chronological_successor=self.arecord2_eid)
+            cnx.commit()
+
+    def test(self):
+        with self.admin_access.cnx() as cnx:
+            with self.assertLogs('cubicweb.hook', level='INFO') as cm:
+                cnx.execute('DELETE AuthorityRecord X WHERE X eid %(x)s',
+                            {'x': self.arecord2_eid})
+            expected_msgs = [
+                'deleting "alice and bob are friends" as association_to-object is being deleted',
+                'deleting "alice is bob\'s mother" as chronological_successor-object is being deleted',  # noqa: E501
+            ]
+            self.assertCountEqual([r.message for r in cm.records], expected_msgs)
+            cnx.commit()
+            rset = cnx.find('AssociationRelation')
+            self.assertFalse(rset)
+
+    def test_unauthorized(self):
+        with self.admin_access.cnx() as cnx:
+            with self.temporary_permissions(AssociationRelation={'delete': ()},
+                                            ChronologicalRelation={'delete': ()}):
+                with self.assertRaises(ValidationError) as cm:
+                    cnx.execute('DELETE AuthorityRecord X WHERE X eid %(x)s',
+                                {'x': self.arecord2_eid})
+        errors = str(cm.exception).splitlines()[1:]  # first line contain entity's eid.
+        expected_msgs = [
+            '* chronological_successor: "alice is bob\'s mother" would need to be deleted alongside the AuthorityRecord but this is disallowed',  # noqa: E501
+            '* association_to: "alice and bob are friends" would need to be deleted alongside the AuthorityRecord but this is disallowed',  # noqa: E501
+        ]
+        self.assertCountEqual(errors, expected_msgs)
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()


More information about the saem-devel mailing list