From 4073886e484ecbe23565bc09fb71ff12d01c2fb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 22 Nov 2021 16:57:17 +0100
Subject: [PATCH 2/2] api: expose card relations in schema (#58448)

---
 tests/api/test_carddef.py | 159 ++++++++++++++++++++++++++++++++++++++
 wcs/formdef.py            |  35 +++++++++
 2 files changed, 194 insertions(+)

diff --git a/tests/api/test_carddef.py b/tests/api/test_carddef.py
index 62911a1a..d7d11d11 100644
--- a/tests/api/test_carddef.py
+++ b/tests/api/test_carddef.py
@@ -13,9 +13,11 @@ from quixote import get_publisher
 from wcs import fields, qommon
 from wcs.api_access import ApiAccess
 from wcs.api_utils import sign_url
+from wcs.blocks import BlockDef
 from wcs.carddef import CardDef
 from wcs.categories import CardDefCategory
 from wcs.data_sources import NamedDataSource
+from wcs.formdef import FormDef
 from wcs.qommon.afterjobs import AfterJob
 from wcs.qommon.http_request import HTTPRequest
 from wcs.qommon.upload_storage import PicklableUpload
@@ -222,6 +224,163 @@ def test_cards(pub, local_user):
     resp = get_app(pub).get('/api/cards/test/@schema', status=403)
 
 
+def test_carddef_schema_relations(pub):
+    FormDef.wipe()
+    CardDef.wipe()
+    BlockDef.wipe()
+
+    formdef = FormDef()
+    formdef.name = 'formdef'
+    formdef.fields = [
+        fields.ItemField(
+            id='0', label='unknown', type='item', varname='unknown', data_source={'type': 'carddef:unknown'}
+        ),
+        fields.ItemField(id='1', label='card1', type='item', data_source={'type': 'carddef:carddef-1'}),
+        fields.ItemField(
+            id='2', label='card2', type='item', varname='card2', data_source={'type': 'carddef:carddef-2'}
+        ),
+        fields.ItemsField(id='3', label='cards1', type='items', data_source={'type': 'carddef:carddef-1'}),
+        fields.ItemsField(
+            id='4', label='cards2', type='items', varname='cards2', data_source={'type': 'carddef:carddef-2'}
+        ),
+    ]
+    formdef.store()
+
+    carddef1 = CardDef()
+    carddef1.name = 'carddef 1'
+    carddef1.fields = [
+        fields.ItemField(
+            id='0', label='unknown', type='item', varname='unknown', data_source={'type': 'carddef:unknown'}
+        ),
+        fields.ItemField(
+            id='1', label='card2', type='item', varname='card2', data_source={'type': 'carddef:carddef-2'}
+        ),
+        fields.ItemField(
+            id='2', label='card3', type='item', data_source={'type': 'carddef:carddef-3'}
+        ),  # no varname
+        fields.ItemsField(
+            id='3', label='cards2', type='items', varname='cards2', data_source={'type': 'carddef:carddef-2'}
+        ),
+        fields.ItemsField(
+            id='4', label='cards3', type='items', data_source={'type': 'carddef:carddef-3'}
+        ),  # no varname
+    ]
+    carddef1.store()
+
+    carddef2 = CardDef()
+    carddef2.name = 'carddef 2'
+    carddef2.store()
+
+    carddef3 = CardDef()
+    carddef3.name = 'carddef 3'
+    carddef3.fields = [
+        # no varnames
+        fields.ItemField(id='1', label='card2', type='item', data_source={'type': 'carddef:carddef-2'}),
+        fields.ItemsField(id='2', label='cards2', type='items', data_source={'type': 'carddef:carddef-2'}),
+    ]
+    carddef3.store()
+
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-1/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'card2', 'type': 'item', 'obj': 'carddef:carddef-2', 'reverse': False},
+        {'varname': 'cards2', 'type': 'items', 'obj': 'carddef:carddef-2', 'reverse': False},
+    ]
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-2/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'card2', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': True},
+        {'varname': 'cards2', 'type': 'items', 'obj': 'carddef:carddef-1', 'reverse': True},
+    ]
+
+    # custom views ?
+    carddef1.fields[1] = fields.ItemField(
+        id='1', label='card2', type='item', varname='card2', data_source={'type': 'carddef:carddef-2:view'}
+    )
+    carddef1.store()
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-1/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'card2', 'type': 'item', 'obj': 'carddef:carddef-2', 'reverse': False},
+        {'varname': 'cards2', 'type': 'items', 'obj': 'carddef:carddef-2', 'reverse': False},
+    ]
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-2/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'card2', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': True},
+        {'varname': 'cards2', 'type': 'items', 'obj': 'carddef:carddef-1', 'reverse': True},
+    ]
+
+    # circular relation ?
+    carddef1.fields = [
+        fields.ItemField(
+            id='1', label='card1', type='item', varname='card1', data_source={'type': 'carddef:carddef-1'}
+        ),
+    ]
+    carddef1.store()
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-1/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'card1', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': False},
+        {'varname': 'card1', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': True},
+    ]
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-2/@schema'), status=200)
+    assert resp.json['relations'] == []
+
+    # block field
+    block1 = BlockDef()
+    block1.name = 'block 1'
+    block1.fields = [
+        fields.ItemField(
+            id='0', label='unknown', type='item', varname='unknown', data_source={'type': 'carddef:unknown'}
+        ),
+        fields.ItemField(
+            id='1',
+            label='card1',
+            type='item',
+            varname='card1',
+            data_source={'type': 'carddef:carddef-1'},
+        ),
+        fields.ItemField(
+            id='2',
+            label='card2',
+            type='item',
+            varname='card2',
+            data_source={'type': 'carddef:carddef-2'},
+        ),
+        fields.ItemsField(
+            id='3', label='cards1', type='items', varname='cards1', data_source={'type': 'carddef:carddef-1'}
+        ),
+        fields.ItemsField(
+            id='4', label='cards2', type='items', varname='cards2', data_source={'type': 'carddef:carddef-2'}
+        ),
+    ]
+    block1.store()
+
+    # no varname on block field
+    carddef1.fields = [fields.BlockField(id='1', label='block', type='block:%s' % block1.slug)]
+    carddef1.store()
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-1/@schema'), status=200)
+    assert resp.json['relations'] == []
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-2/@schema'), status=200)
+    assert resp.json['relations'] == []
+
+    # with varname
+    carddef1.fields = [
+        fields.BlockField(id='1', label='block', varname='block1', type='block:%s' % block1.slug)
+    ]
+    carddef1.store()
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-1/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'block1_card1', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': False},
+        {'varname': 'block1_card1', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': True},
+        {'varname': 'block1_card2', 'type': 'item', 'obj': 'carddef:carddef-2', 'reverse': False},
+        {'varname': 'block1_cards1', 'type': 'items', 'obj': 'carddef:carddef-1', 'reverse': False},
+        {'varname': 'block1_cards1', 'type': 'items', 'obj': 'carddef:carddef-1', 'reverse': True},
+        {'varname': 'block1_cards2', 'type': 'items', 'obj': 'carddef:carddef-2', 'reverse': False},
+    ]
+    resp = get_app(pub).get(sign_uri('/api/cards/carddef-2/@schema'), status=200)
+    assert resp.json['relations'] == [
+        {'varname': 'block1_card2', 'type': 'item', 'obj': 'carddef:carddef-1', 'reverse': True},
+        {'varname': 'block1_cards2', 'type': 'items', 'obj': 'carddef:carddef-1', 'reverse': True},
+    ]
+
+
 @pytest.mark.parametrize('auth', ['signature', 'http-basic'])
 def test_cards_import_csv(pub, local_user, auth):
     pub.role_class.wipe()
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 4d81ab12..149ab21c 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -949,6 +949,8 @@ class FormDef(StorableObject):
         return d
 
     def export_to_json(self, include_id=False, indent=None):
+        from wcs.carddef import CardDef
+
         charset = get_publisher().site_charset
         root = {}
         root['name'] = force_text(self.name, charset)
@@ -993,6 +995,39 @@ class FormDef(StorableObject):
         if self.required_authentication_contexts:
             root['required_authentication_contexts'] = self.required_authentication_contexts[:]
 
+        if isinstance(self, CardDef):
+            card_relations = []
+            for objdef, relations in self.build_relations_by_ref().items():
+                print(objdef, relations)
+                if not objdef.startswith('carddef:'):
+                    continue
+                try:
+                    CardDef.get_by_urlname(objdef.split(':')[1])
+                except KeyError:
+                    continue
+                for relation in relations:
+                    if not relation['varname']:
+                        continue
+                    card_relations.append(
+                        {
+                            'varname': relation['varname'],
+                            'type': relation['type'],
+                            'obj': objdef,
+                            'reverse': False,
+                        }
+                    )
+
+            for relation in self.reverse_relations or []:
+                if not relation['obj'].startswith('carddef:'):
+                    continue
+                if not relation['varname']:
+                    continue
+                rel = relation.copy()
+                rel.update({'reverse': True})
+                card_relations.append(rel)
+
+            root['relations'] = sorted(card_relations, key=itemgetter('varname'))
+
         return json.dumps(root, indent=indent, sort_keys=True, cls=JSONEncoder)
 
     @classmethod
-- 
2.33.0

