From c45811deae8007371adcefe92d11f064689880d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 11 Aug 2022 16:46:26 +0200
Subject: [PATCH 6/8] wcs: render card cell with table mode (#68063)

---
 combo/apps/wcs/models.py                      |  55 +++-
 combo/apps/wcs/templates/combo/wcs/cards.html |   2 +-
 tests/wcs/test_card.py                        | 267 +++++++++++++++++-
 3 files changed, 317 insertions(+), 7 deletions(-)

diff --git a/combo/apps/wcs/models.py b/combo/apps/wcs/models.py
index 0b16d7ad..d1c8d725 100644
--- a/combo/apps/wcs/models.py
+++ b/combo/apps/wcs/models.py
@@ -34,7 +34,7 @@ from requests.exceptions import RequestException
 
 from combo.data.library import register_cell_class
 from combo.data.models import CellBase, Page
-from combo.utils import requests
+from combo.utils import NothingInCacheException, requests
 from combo.utils.misc import is_portal_agent
 
 from .utils import get_wcs_json, get_wcs_services, is_wcs_enabled
@@ -974,7 +974,6 @@ class WcsCardInfosCell(CardMixin, CellBase):
 
     is_enabled = classmethod(is_wcs_enabled)
 
-    default_template_name = 'combo/wcs/card.html'
     manager_appearance_template = 'combo/wcs/manager/card-infos-cell-form-appearance.html'
 
     class Meta:
@@ -1030,16 +1029,28 @@ class WcsCardInfosCell(CardMixin, CellBase):
 
         self.mark_as_valid()
 
+    @property
+    def default_template_name(self):
+        if self.display_mode == 'table':
+            return 'combo/wcs/cards.html'
+        return 'combo/wcs/card.html'
+
     @property
     def global_context_key(self):
         return '%s-card-ids' % self.get_reference()
 
     def modify_global_context(self, context, request):
+        if self.display_mode == 'table' and not context.get('synchronous'):
+            # don't call wcs on page loading
+            return
         if self.carddef_reference and self.global_context_key not in context:
             card_ids = self.get_card_ids(context, request)
             context[self.global_context_key] = card_ids
 
     def get_repeat_template(self, context):
+        if self.display_mode == 'table':
+            # don't repeat cell if table display mode
+            return []
         return len(context.get(self.global_context_key) or [])
 
     def get_related_card_path(self):
@@ -1401,6 +1412,9 @@ class WcsCardInfosCell(CardMixin, CellBase):
                 return self.filter_card_ids(card_ids, original_context)
 
         if self.get_all():
+            if self.display_mode == 'table':
+                # don't call wcs if table mode with all cards
+                return []
             # get all cards
             return [c['id'] for c in self.get_cards_from_ids([], original_context, synchronous=True)]
 
@@ -1437,13 +1451,44 @@ class WcsCardInfosCell(CardMixin, CellBase):
 
     def get_cell_extra_context(self, context):
         extra_context = super().get_cell_extra_context(context)
+        if self.title_type in ['auto', 'manual']:
+            # card mode: default value used if card is not found
+            extra_context['title'] = self.cached_title
+        return getattr(self, 'get_cell_extra_context_%s_mode' % self.display_mode)(context, extra_context)
+
+    def get_cell_extra_context_table_mode(self, context, extra_context):
+        if not context.get('synchronous'):
+            raise NothingInCacheException()
+
+        extra_context['paginate_by'] = self.limit or 10
+        if self.title_type == 'manual':
+            extra_context['title'] = self.custom_title or extra_context['title']
+        if not self.carddef_reference:
+            # not configured
+            return extra_context
+
+        pages_with_sub_slug = Page.objects.exclude(sub_slug='')
+        card_id = '%s_id' % self.carddef_reference.split(':')[1]
+        matching_pages = [
+            p for p in pages_with_sub_slug if '<%s>' % card_id in p.sub_slug or p.sub_slug == card_id
+        ]
+        if matching_pages:
+            card_page = matching_pages[0]
+            extra_context['card_page_base_url'] = card_page.get_online_url()
+
+        card_ids = context.get(self.global_context_key)
+        if not card_ids and self.related_card_path != '__all__':
+            extra_context['card_objects'] = []
+        else:
+            extra_context['card_objects'] = self.get_cards_from_ids(card_ids, context)
+
+        return extra_context
+
+    def get_cell_extra_context_card_mode(self, context, extra_context):
         extra_context['schema'] = self.cached_json
         extra_context['fields_by_varnames'] = {
             i['varname']: i for i in (self.cached_json.get('fields') or []) if i.get('varname')
         }
-        # default value used if card is not found
-        if self.title_type in ['auto', 'manual']:
-            extra_context['title'] = self.cached_title
 
         card_id = self.get_card_id(context)
         if not card_id:
diff --git a/combo/apps/wcs/templates/combo/wcs/cards.html b/combo/apps/wcs/templates/combo/wcs/cards.html
index 4168cc31..a7edd9f0 100644
--- a/combo/apps/wcs/templates/combo/wcs/cards.html
+++ b/combo/apps/wcs/templates/combo/wcs/cards.html
@@ -3,7 +3,7 @@
 {% block cell-content %}
 
 {% block cell-header %}
-<h2>{{ title }}</h2>
+{% if title %}<h2>{{ title|force_escape }}</h2>{% endif %}
 {% include "combo/asset_picture_fragment.html" %}
 {% endblock %}
 
diff --git a/tests/wcs/test_card.py b/tests/wcs/test_card.py
index f05ce372..bd77a17a 100644
--- a/tests/wcs/test_card.py
+++ b/tests/wcs/test_card.py
@@ -18,7 +18,7 @@ from combo.data.models import Page, TextCell, ValidityInfo
 from tests.test_manager import login
 from tests.utils import manager_submit_cell
 
-from .utils import WCS_CARDS_DATA, MockUser, MockUserWithNameId, mocked_requests_send
+from .utils import WCS_CARDS_DATA, MockedRequestResponse, MockUser, MockUserWithNameId, mocked_requests_send
 
 pytestmark = pytest.mark.django_db
 
@@ -633,6 +633,271 @@ def test_card_cell_load(mock_send):
     assert cell.cached_title == 'Card Model 1'
 
 
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_card_cell_table_mode_render(mock_send, context, app):
+    page = Page.objects.create(title='xxx', template_name='standard')
+    cell = WcsCardInfosCell.objects.create(
+        page=page,
+        placeholder='content',
+        order=0,
+        display_mode='table',
+        carddef_reference='default:card_model_1',
+    )
+
+    context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    context['synchronous'] = True  # to get fresh content
+    cell.modify_global_context(context, request)
+
+    result = cell.render(context)
+    assert '<h2>Card Model 1</h2>' in result
+    assert 'cards-card_model_1' in result
+    assert (
+        '<a href="http://127.0.0.1:8999/backoffice/data/card_model_1/11/"><span class="card-title">aa</span></a>'
+        in result
+    )
+    assert (
+        '<a href="http://127.0.0.1:8999/backoffice/data/card_model_1/12/"><span class="card-title">bb</span></a>'
+        in result
+    )
+    assert (
+        '<a href="http://127.0.0.1:8999/backoffice/data/card_model_1/13/"><span class="card-title">cc</span></a>'
+        in result
+    )
+    assert 'data-paginate-by="10"' in result
+
+    # create a page with the correct subslug
+    page = Page.objects.create(slug='foo', title='Foo', sub_slug='(?P<card_model_1_id>[a-z0-9]+)')
+
+    result = cell.render(context)
+    assert '<h2>Card Model 1</h2>' in result
+    assert '<a href="/foo/11"><span class="card-title">aa</span></a>' in result
+    assert '<a href="/foo/12"><span class="card-title">bb</span></a>' in result
+    assert '<a href="/foo/13"><span class="card-title">cc</span></a>' in result
+
+    cell.carddef_reference = 'default:card_model_1:foo'
+    cell.limit = 42
+    cell.save()
+    page.sub_slug = 'card_model_1_id'
+    page.save()
+
+    result = cell.render(context)
+    assert '<h2>Card Model 1</h2>' in result
+    assert '<a href="/foo/11"><span class="card-title">aa</span></a>' in result
+    assert '<a href="/foo/12"><span class="card-title">bb</span></a>' in result
+    assert '<a href="/foo/13"><span class="card-title">cc</span></a>' not in result
+    assert 'data-paginate-by="42"' in result
+
+    with mock.patch('combo.apps.wcs.models.requests.get') as requests_get:
+        requests_get.return_value = MockedRequestResponse(content=json.dumps({'data': []}))
+        cell.render(context)
+    assert len(requests_get.call_args_list) == 1
+    assert (
+        requests_get.call_args_list[0][0][0]
+        == '/api/cards/card_model_1/list/foo?full=on&filter-internal-id=11'
+    )
+    assert requests_get.call_args_list[0][1]['remote_service']['url'] == 'http://127.0.0.1:8999/'
+
+
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_card_cell_table_mode_render_all_cards(mock_send, nocache, app):
+    page = Page.objects.create(title='xxx', slug='foo', template_name='standard')
+    cell = WcsCardInfosCell.objects.create(
+        page=page,
+        placeholder='content',
+        order=0,
+        display_mode='table',
+        carddef_reference='default:card_model_1',
+        related_card_path='__all__',
+    )
+
+    cell_url = reverse(
+        'combo-public-ajax-page-cell',
+        kwargs={'page_pk': page.pk, 'cell_reference': cell.get_reference()},
+    )
+
+    # check url called
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert len(mock_send.call_args_list) == 1
+    # cell rendering
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    assert 'filter-internal-id' not in mock_send.call_args_list[0][0][0].url
+
+
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_card_cell_table_mode_render_identifier(mock_send, nocache, app):
+    page = Page.objects.create(
+        title='xxx', slug='foo', template_name='standard', sub_slug='(?P<card_model_1_id>[a-z0-9]+)'
+    )
+    cell = WcsCardInfosCell.objects.create(
+        page=page,
+        placeholder='content',
+        order=0,
+        display_mode='table',
+        carddef_reference='default:card_model_1',
+    )
+
+    cell_url = reverse(
+        'combo-public-ajax-page-cell',
+        kwargs={'page_pk': page.pk, 'cell_reference': cell.get_reference()},
+    )
+
+    # check url called
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url() + '11/')
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert len(mock_send.call_args_list) == 1
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    assert '&filter-internal-id=11&' in mock_send.call_args_list[0][0][0].url
+
+    # with identifiers
+    page.sub_slug = ''
+    page.save()
+    cell.card_ids = '42'
+    cell.save()
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert len(mock_send.call_args_list) == 1
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    assert '&filter-internal-id=42&' in mock_send.call_args_list[0][0][0].url
+
+    cell.card_ids = '42, , 35'
+    cell.save()
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert len(mock_send.call_args_list) == 1
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    assert '&filter-internal-id=42&filter-internal-id=35&' in mock_send.call_args_list[0][0][0].url
+
+    cell.card_ids = '{% cards|objects:"card_model_1"|last|get:"id" %}'  # syntax error
+    cell.save()
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert 'empty-message' in cell_resp
+
+    cell.card_ids = '{{ cards|objects:"card_model_1"|last|get:"id" }}'
+    cell.save()
+    mock_send.reset_mock()
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert len(mock_send.call_args_list) == 2
+    # cell rendering
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[1][0][0].url
+    assert '&filter-internal-id=13&' in mock_send.call_args_list[1][0][0].url
+
+
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_card_cell_table_mode_render_identifier_from_related(mock_send, nocache, app):
+    page = Page.objects.create(title='xxx', slug='foo', template_name='standard')
+    WcsCardInfosCell.objects.create(
+        page=page,
+        placeholder='content',
+        order=0,
+        display_mode='table',
+        slug='sluga',
+        carddef_reference='default:card_a',
+        card_ids='1',
+    )
+    cell2 = WcsCardInfosCell.objects.create(
+        page=page, placeholder='content', order=1, slug='slugb', carddef_reference='default:card_b'
+    )
+
+    cell2_url = reverse(
+        'combo-public-ajax-page-cell',
+        kwargs={'page_pk': page.pk, 'cell_reference': cell2.get_reference()},
+    )
+
+    # just do a simple test to check url calls
+    def check(urls):
+        resp = app.get(page.get_online_url())
+        assert len(resp.context['cells']) == 2
+        extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+        mock_send.reset_mock()
+        app.get(cell2_url + '?ctx=' + extra_ctx[1])
+        assert len(mock_send.call_args_list) == len(urls)
+        for j, url_parts in enumerate(urls):
+            if not isinstance(url_parts, tuple):
+                url_parts = (url_parts,)
+            for url_part in url_parts:
+                assert url_part in mock_send.call_args_list[j][0][0].url
+
+    # direct and single relation (item)
+    cell2.related_card_path = 'sluga/cardb'
+    cell2.save()
+    urls = [
+        # get first cell data
+        '/api/cards/card_a/1/',
+        # and follow cardb relation
+        '/api/cards/card_b/1/',  # get card
+    ]
+    check(urls)
+
+    # direct and multiple relation (items)
+    cell2.carddef_reference = 'default:card_b'  # reset
+    cell2.related_card_path = 'sluga/cardsb'
+    cell2.save()
+    urls = [
+        # get first cell data
+        '/api/cards/card_a/1/',
+        # and follow cardb relation
+        ('/api/cards/card_b/list', '&filter-internal-id=2&filter-internal-id=3&'),  # get cards
+    ]
+    check(urls)
+
+
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_card_cell_table_mode_render_title(mock_send, context):
+    page = Page.objects.create(title='xxx', template_name='standard')
+    cell = WcsCardInfosCell.objects.create(
+        page=page,
+        placeholder='content',
+        order=0,
+        display_mode='table',
+        carddef_reference='default:card_model_1',
+        title_type='auto',
+    )
+
+    context['synchronous'] = True  # to get fresh content
+
+    result = cell.render(context)
+    assert '<h2>Card Model 1</h2>' in result
+
+    context.pop('title')
+    cell.title_type = 'empty'
+    cell.save()
+    result = cell.render(context)
+    assert '<h2>' not in result
+
+    cell.title_type = 'manual'
+    cell.save()
+    result = cell.render(context)
+    assert '<h2>Card Model 1</h2>' in result
+
+    context.pop('title')
+    cell.custom_title = 'Foo bar !'
+    cell.save()
+    result = cell.render(context)
+    assert '<h2>Foo bar !</h2>' in result
+
+
 @mock.patch('requests.Session.send', side_effect=mocked_requests_send)
 def test_card_cell_card_mode_render(mock_send, context, app):
     page = Page.objects.create(title='xxx', template_name='standard')
-- 
2.35.1

