Allow archive units not linked to a parent
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 19 Oct 2016 23:07:28 +0200
changeset 1886 0a8f060a5ce6
parent 1852 5e1e74aab8ec
child 1887 38c8d9a12bcc
Allow archive units not linked to a parent This prepare the 'seda components' functionality from SAEM. For now, we don't support data object as component, since most metadata moved to its related archive unit with SEDA2. We also have to change data object parent relation's cardinality since in case of an archive unit as component, we can't have this relation to the transfer entity.
entities/__init__.py
entities/itree.py
hooks.py
schema/__init__.py
schema/seda2.py
test/test_entities.py
test/test_schema.py
test/testutils.py
xsd2yams.py
--- a/entities/__init__.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/entities/__init__.py	Wed Oct 19 23:07:28 2016 +0200
@@ -38,6 +38,10 @@
         # entity is expected to be a contained entity, not the container itself
         container = entity.cw_adapt_to('IContained').container
         parent = entity.cw_adapt_to('IContained').parent
+        if container is None:
+            # entity may be both container and contained, and in this case is a container
+            assert entity.cw_adapt_to('IContainer')
+            container = entity
     else:
         req = entity._cw
         # but parent entity, retrieved through linkto, may be the container itself or a
@@ -55,11 +59,13 @@
                 # unable to get parent eid for now :(
                 return None, None
         parent = req.entity_from_eid(parent_eid)
-        icontainer = parent.cw_adapt_to('IContainer')
-        if icontainer is None:
-            container = parent.cw_adapt_to('IContained').container
-        else:
-            container = icontainer.container
+        # handle IContained first: in case entity support both interface we want to go to the
+        # uppermost parent
+        icontained = parent.cw_adapt_to('IContained')
+        if icontained is not None and icontained.container:
+            container = icontained.container
+        elif parent.cw_adapt_to('IContainer'):
+            container = parent
     return parent, container
 
 
@@ -77,14 +83,22 @@
     entity = _transfer_from_context(rset, entity)
     if entity is None:
         return 0
-    return int(entity.simplified_profile)
+    if entity.cw_etype == 'SEDAArchiveUnit':
+        # XXX archive unit component, for now suppose it's "simplified"
+        return 1
+    return 1 if entity.simplified_profile else 0
 
 
 @objectify_predicate
 def full_seda2_profile(cls, req, rset=None, entity=None, **kwargs):
     """Predicate returning 1 score if context entity is within a full seda2 profile."""
     entity = _transfer_from_context(rset, entity)
-    return int(not entity.simplified_profile)
+    if entity is None:
+        return 1
+    if entity.cw_etype == 'SEDAArchiveUnit':
+        # XXX archive unit component, for now suppose it's "simplified"
+        return 0
+    return 0 if entity.simplified_profile else 1
 
 
 def rule_type_from_etype(etype):
@@ -115,5 +129,6 @@
 
 def registration_callback(vreg):
     vreg.register(IContainer.build_class('SEDAArchiveTransfer'))
+    vreg.register(IContainer.build_class('SEDAArchiveUnit'))  # archive unit may also be a container
     for etype, parent_relations in sorted(seda_profile_container_def(vreg.schema)):
         DirectLinkIContained.register_class(vreg, etype, parent_relations)
--- a/entities/itree.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/entities/itree.py	Wed Oct 19 23:07:28 2016 +0200
@@ -79,6 +79,8 @@
 
     def parent(self):
         parent = self.entity.cw_adapt_to('IContained').parent
+        if parent is None:
+            return None
         if parent.cw_etype == 'SEDAArchiveTransfer':
             return parent
         return parent_archive_unit(parent)
--- a/hooks.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/hooks.py	Wed Oct 19 23:07:28 2016 +0200
@@ -46,11 +46,17 @@
             # climb up to the uppermost parent
             while peid in data:
                 peid = data[peid]
-            adapter = cnx.entity_from_eid(peid).cw_adapt_to('IContained')
+            entity = cnx.entity_from_eid(peid)
+            adapter = entity.cw_adapt_to('IContained')
             if adapter is None:  # we reached the container
                 containers[eid] = peid
             else:  # the adapter must know the container at this point
-                containers[eid] = adapter.container.eid
+                if adapter.container is not None:
+                    containers[eid] = adapter.container.eid
+                else:
+                    assert entity.cw_adapt_to('IContainer'), \
+                        "can't retrieve container for entity %s" % entity
+                    containers[eid] = peid
         # turn previous mapping into <container eid>: [<list of contained eids>]
         contained = defaultdict(list)
         for eid, ceid in containers.items():
@@ -244,7 +250,7 @@
                 profiles.add(entity)
             else:
                 container = entity.cw_adapt_to('IContained').container
-                if container is not None:
+                if container is not None and container.cw_etype == 'SEDAArchiveTransfer':
                     profiles.add(container)
         with cnx.deny_all_hooks_but():
             for profile in profiles:
--- a/schema/__init__.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/schema/__init__.py	Wed Oct 19 23:07:28 2016 +0200
@@ -129,11 +129,12 @@
 def post_build_callback(schema):
     from cubes.seda import seda_profile_container_def, iter_all_rdefs
 
-    container_etype = 'SEDAArchiveTransfer'
+    container_etypes = ('SEDAArchiveTransfer', 'SEDAArchiveUnit')
     for etype, parent_rdefs in seda_profile_container_def(schema):
         # add relation to the container from every entity type within the compound graph
-        container_rdef = RelationDefinition(etype, 'container', container_etype)
-        schema.add_relation_def(container_rdef)
+        for container_etype in container_etypes:
+            container_rdef = RelationDefinition(etype, 'container', container_etype)
+            schema.add_relation_def(container_rdef)
         eschema = schema[etype]
         # set permissions on entity types from the compound graph according to permission on the
         # container entity
@@ -142,6 +143,13 @@
                 action, ('managers', ERQLExpression('U has_{action}_permission C, '
                                                     'X container C'.format(action=action)))
             )
+    for action in ('update', 'delete'):
+        schema['SEDAArchiveUnit'].set_action_permissions(
+            action, ('managers',
+                     ERQLExpression('U has_{action}_permission C, '
+                                    'X container C'.format(action=action)),
+                     ERQLExpression('NOT EXISTS(X container C), U in_group G, '
+                                    'G name IN ("managers", "users")')))
     # set permissions on all relation defs related to the compound graph according to permission on
     # the container entity
     for rdef, role in iter_all_rdefs(schema, 'SEDAArchiveTransfer'):
@@ -149,7 +157,7 @@
             target_etype, var = rdef.subject, 'S'
         else:
             target_etype, var = rdef.object, 'O'
-        if target_etype == container_etype:
+        if target_etype == 'SEDAArchiveTransfer':
             expr = 'U has_update_permission {0}'.format(var)
         else:
             expr = 'U has_update_permission C, {0} container C'.format(var)
--- a/schema/seda2.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/schema/seda2.py	Wed Oct 19 23:07:28 2016 +0200
@@ -431,7 +431,7 @@
     name = 'seda_binary_data_object'
     subject = 'SEDABinaryDataObject'
     object = 'SEDAArchiveTransfer'
-    cardinality = '1*'
+    cardinality = '?*'
     composite = fulltext_container = 'object'
     inlined = True
     constraints = []
@@ -447,7 +447,7 @@
     name = 'seda_physical_data_object'
     subject = 'SEDAPhysicalDataObject'
     object = 'SEDAArchiveTransfer'
-    cardinality = '1*'
+    cardinality = '?*'
     composite = fulltext_container = 'object'
     inlined = True
     constraints = []
@@ -542,7 +542,7 @@
     name = 'seda_archive_unit'
     subject = 'SEDAArchiveUnit'
     object = 'SEDAArchiveTransfer'
-    cardinality = '1*'
+    cardinality = '?*'
     composite = fulltext_container = 'object'
     inlined = True
     constraints = []
@@ -1364,7 +1364,7 @@
     name = 'seda_archive_unit'
     subject = 'SEDAArchiveUnit'
     object = 'SEDASeqAltArchiveUnitArchiveUnitRefIdManagement'
-    cardinality = '1*'
+    cardinality = '?*'
     composite = fulltext_container = 'object'
     inlined = True
     constraints = []
--- a/test/test_entities.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/test/test_entities.py	Wed Oct 19 23:07:28 2016 +0200
@@ -69,6 +69,23 @@
                 entity.cw_clear_all_caches()
                 self.assertEqual(entity.cw_adapt_to('IContained').container.eid, transfer.eid)
 
+    def test_archive_unit_container(self):
+        """Functional test for SEDA component clone."""
+        with self.admin_access.repo_cnx() as cnx:
+            unit, unit_alt, unit_alt_seq = create_archive_unit(None, cnx=cnx)
+            bdo = create_data_object(None, cnx=cnx)
+            cnx.create_entity('SEDADataObjectReference', user_cardinality=u'0..n',
+                              seda_data_object_reference=unit_alt_seq,
+                              seda_data_object_reference_id=bdo)
+            cnx.commit()
+
+            unit.cw_clear_all_caches()
+            self.assertEqual(unit.container, ())  # XXX arguable
+            unit_alt_seq.cw_clear_all_caches()
+            self.assertEqual(unit_alt_seq.container[0].eid, unit.eid)
+            bdo.cw_clear_all_caches()
+            self.assertEqual(bdo.container[0].eid, unit.eid)
+
 
 class FakeEntity(object):
     cw_etype = 'Whatever'
--- a/test/test_schema.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/test/test_schema.py	Wed Oct 19 23:07:28 2016 +0200
@@ -125,6 +125,19 @@
             self.assertEqual(rqlexpr.expression, 'U has_{action}_permission C, '
                              'X container C'.format(action=action))
 
+    def test_archive_unit_permissions(self):
+        """Check that permissions are correctly set on archive unit that may be either container
+        or contained."""
+        for action in ('update', 'delete'):
+            etype = self.schema['SEDAArchiveUnit']
+            rqlexpr1, rqlexpr2 = [p for p in etype.permissions[action]
+                                  if isinstance(p, ERQLExpression)]
+            self.assertEqual(rqlexpr1.expression,
+                             'U has_{action}_permission C, X container C'.format(action=action))
+            self.assertEqual(rqlexpr2.expression,
+                             'NOT EXISTS(X container C), U in_group G, '
+                             'G name IN("managers", "users")')
+
     def test_rule_default_cardinality(self):
         with self.admin_access.client_cnx() as cnx:
             transfer = cnx.create_entity('SEDAArchiveTransfer', title=u'test profile')
--- a/test/testutils.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/test/testutils.py	Wed Oct 19 23:07:28 2016 +0200
@@ -28,7 +28,7 @@
 
 
 def create_archive_unit(parent, **kwargs):
-    cnx = parent._cw
+    cnx = kwargs.pop('cnx', getattr(parent, '_cw', None))
     kwargs.setdefault('id', u'au1')
     au = cnx.create_entity('SEDAArchiveUnit', seda_archive_unit=parent, **kwargs)
     alt = cnx.create_entity('SEDAAltArchiveUnitArchiveUnitRefId',
@@ -40,7 +40,8 @@
 
 
 def create_data_object(transfer, **kwargs):
-    create = transfer._cw.create_entity
+    cnx = kwargs.pop('cnx', getattr(transfer, '_cw', None))
+    create = cnx.create_entity
     kwargs.setdefault('id', u'bdo1')
     bdo = create('SEDABinaryDataObject', seda_binary_data_object=transfer, **kwargs)
     choice = create('SEDAAltBinaryDataObjectAttachment',
--- a/xsd2yams.py	Tue Oct 18 17:28:18 2016 +0200
+++ b/xsd2yams.py	Wed Oct 19 23:07:28 2016 +0200
@@ -173,6 +173,9 @@
     'seda_description': '1?',
     'seda_comment': '1?',
     'seda_custodial_history_item': '1*',
+    'seda_archive_unit': '?*',
+    'seda_binary_data_object': '?*',
+    'seda_physical_data_object': '?*',
 }
 RTYPE_CARD = {
     'seda_custodial_history_item': '*',