[PATCH 1 of 2 compound] Backport utility functions from the saem_ref cube

Sylvain Thenault sylvain.thenault at logilab.fr
Tue Mar 21 09:59:17 CET 2017


# HG changeset patch
# User Sylvain Thénault <sylvain.thenault at logilab.fr>
# Date 1488957508 -3600
#      Wed Mar 08 08:18:28 2017 +0100
# Node ID e6bc30cf82138177545fa76d28c7ba3e672303e9
# Parent  ed8dc2bddcbe5235f84f2831f5e9acc752cf659f
Backport utility functions from the saem_ref cube

and write some tests about graph traversal functions.

diff --git a/cubicweb_compound/utils.py b/cubicweb_compound/utils.py
new file mode 100644
--- /dev/null
+++ b/cubicweb_compound/utils.py
@@ -0,0 +1,142 @@
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact at logilab.fr
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+"""Library functions to ease usual tasks one want to achieve using compound
+based entities graph:
+
+* traversing the graph (:func:`optional_relations`, :func:`optional_mandatory_rdefs`,
+  :func:`graph_relations`),
+
+* setting permisssions (:func:`graph_set_etypes_update_permissions`,
+  :func:`graph_set_write_rdefs_permissions`).
+"""
+
+from six.moves import reduce
+
+from cubicweb import neg_role
+from cubicweb.schema import ERQLExpression, RRQLExpression
+
+
+def graph_relations(schema, parent_structure):
+    """Given a parent structure of a composite graph (and a schema object),
+    return relation information `(rtype, role)` sets where `role` is the role
+    of the child in the relation for the following kinds of relations:
+
+    * structural relations,
+    * optional relations (cardinality of the child not in '1*'),
+    * mandatory relations (cardinality of the child in '1*').
+    """
+    def concat_sets(sets):
+        """Concatenate sets"""
+        return reduce(lambda x, y: x | y, sets, set())
+
+    optionals = concat_sets(
+        optional_relations(schema, parent_structure).values())
+    mandatories = set([
+        (rdef.rtype, neg_role(role))
+        for rdef, role in mandatory_rdefs(schema, parent_structure)])
+    structurals = concat_sets(map(set, parent_structure.values()))
+    return structurals, optionals, mandatories
+
+
+def optional_relations(schema, graph_structure):
+    """Return a dict with optional relations information in a CompositeGraph.
+
+    Keys are names of entity types in the graph for which a relation type has
+    no mandatory (cardinality in '1+') relation definitions and values is a
+    set of respective `(rtype, role)` tuples.
+    """
+    optionals = dict()
+    for etype, relations in graph_structure.iteritems():
+        for (rtype, role), targets in relations.iteritems():
+            for target in targets:
+                rdef = schema[rtype].role_rdef(etype, target, role)
+                if rdef.role_cardinality(role) in '1+':
+                    break
+            else:
+                optionals.setdefault(etype, set()).add((rtype, role))
+    return optionals
+
+
+def mandatory_rdefs(schema, graph_structure):
+    """Yield non-optional relation definitions (and the role of the parent in
+    the relation) in a graph structure.
+    """
+    visited = set()
+    for etype, relations in graph_structure.iteritems():
+        for (rtype, role), targets in relations.iteritems():
+            for target in targets:
+                rdef = schema[rtype].role_rdef(etype, target, role)
+                if rdef in visited:
+                    continue
+                visited.add(rdef)
+                if rdef.role_cardinality(role) in '1+':
+                    yield rdef, neg_role(role)
+
+
+def graph_set_etypes_update_permissions(schema, graph, etype):
+    """Add `action` permissions for all entity types in the composite `graph`
+    with root `etype`. Respective permissions that are inserted on each
+    entity type are relative to the "parent" in the relation from this
+    entity type walking up to the graph root.
+
+    So for instance, calling `set_etype_permissions('R', 'update')`
+    on a schema where `A related_to B` and `R root_of B` one will get:
+
+    * "U has_update_permission R, R root_of X" for `B` entity type and,
+    * "U has_update_permission P, X related_to P" for `A` entity type.
+
+    If an entity type in the graph is reachable through multiple relations, a
+    permission for each of this relation will be inserted so that if any of
+    these match, the permission check will succeed.
+    """
+    structure = graph.parent_structure(etype)
+    optionals = optional_relations(schema, structure)
+    for child, relations in structure.iteritems():
+        skiprels = optionals.get(child, set())
+        exprs = []
+        for rtype, role in relations:
+            if (rtype, role) in skiprels:
+                continue
+            relexpr = _rel_expr(rtype, role)
+            exprs.append('{relexpr}, U has_update_permission A'.format(relexpr=relexpr))
+        if exprs:
+            for action in ('update', 'delete'):
+                schema[child].set_action_permissions(action,
+                                                     tuple(ERQLExpression(e) for e in exprs))
+
+
+def graph_set_write_rdefs_permissions(schema, graph, etype):
+    """Set 'add' and 'delete' permissions for all mandatory relation definitions
+    in the composite `graph` with root `etype`.
+
+    Respective permissions that are inserted on each relation definition are
+    relative to the "parent" in the relation from this entity type walking up to
+    the graph root.
+
+    Relations which are not mandatory or which are not part of the graph
+    structure should be handled manually.
+    """
+    structure = graph.parent_structure(etype)
+    for rdef, parent_role in mandatory_rdefs(schema, structure):
+        var = {'object': 'O', 'subject': 'S'}[parent_role]
+        expr = 'U has_update_permission {0}'.format(var)
+        for action in ('add', 'delete'):
+            rdef.set_action_permissions(action, (RRQLExpression(expr), ))
+
+
+def _rel_expr(rtype, role):
+    return {'subject': 'X {rtype} A',
+            'object': 'A {rtype} X'}[role].format(rtype=rtype)
diff --git a/test/test_utils.py b/test/test_utils.py
new file mode 100644
--- /dev/null
+++ b/test/test_utils.py
@@ -0,0 +1,81 @@
+# copyright 2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr -- mailto:contact at logilab.fr
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-compound tests"""
+
+from cubicweb.devtools.testlib import CubicWebTC
+
+from cubicweb_compound import CompositeGraph
+from cubicweb_compound import utils
+
+
+class LibFunctionTC(CubicWebTC):
+
+    def test_graph_relations(self):
+        graph = CompositeGraph(self.schema)
+        structure = graph.parent_structure('Agent')
+        structurals, optionals, mandatories = utils.graph_relations(self.schema, structure)
+        expected = set([
+            ('event', 'object'),
+            ('biography', 'object'),
+            ('relates', 'object'),
+            ('account', 'object'),
+            ('narrated_by', 'subject'),
+            ('comments', 'subject'),
+        ])
+        self.assertEqual(structurals, expected)
+        expected = set([
+            ('narrated_by', 'subject'),
+            ('relates', 'object'),
+        ])
+        self.assertEqual(optionals, expected)
+        mandatories = [(str(rdef), role) for rdef, role in mandatories]
+        expected = [
+            ('account', 'object'),
+            ('biography', 'object'),
+            ('comments', 'subject'),
+            ('event', 'object'),
+        ]
+        self.assertEqual(sorted(mandatories), expected)
+
+    def test_optional_relations(self):
+        graph = CompositeGraph(self.schema)
+        structure = graph.parent_structure('Agent')
+        optional = utils.optional_relations(self.schema, structure)
+        expected = {
+            'Anecdote': set([('narrated_by', 'subject')]),
+            'Event': set([('relates', 'object')]),
+        }
+        self.assertEqual(optional, expected)
+
+    def test_mandatory_rdefs(self):
+        graph = CompositeGraph(self.schema)
+        structure = graph.parent_structure('Agent')
+        mandatory = [(str(rdef), role) for rdef, role in utils.mandatory_rdefs(self.schema,
+                                                                               structure)]
+        expected = [
+            ('relation Agent account OnlineAccount', 'subject'),
+            ('relation Agent biography Biography', 'subject'),
+            ('relation Biography event Anecdote', 'subject'),
+            ('relation Biography event Event', 'subject'),
+            ('relation Comment comments Anecdote', 'object'),
+            ('relation Comment comments Comment', 'object'),
+        ]
+        self.assertEqual(sorted(mandatory), expected)
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()


More information about the saem-devel mailing list