From a24bd7b3f76752ae657c689feb20abc545afbf4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 23 Nov 2021 16:57:01 +0100
Subject: [PATCH] wcs: card cell & multiple card identifiers (#58862)

---
 combo/apps/wcs/forms.py                    |   2 +-
 combo/apps/wcs/migrations/0043_card_ids.py |  21 +++
 combo/apps/wcs/models.py                   |  39 ++++--
 combo/data/models.py                       |   3 +-
 combo/public/templatetags/combo.py         |   4 +-
 tests/test_wcs.py                          | 154 ++++++++++++++++-----
 6 files changed, 175 insertions(+), 48 deletions(-)
 create mode 100644 combo/apps/wcs/migrations/0043_card_ids.py

diff --git a/combo/apps/wcs/forms.py b/combo/apps/wcs/forms.py
index c14c96b3..1aefeb32 100644
--- a/combo/apps/wcs/forms.py
+++ b/combo/apps/wcs/forms.py
@@ -71,7 +71,7 @@ class WcsCardInfoCellForm(forms.ModelForm):
 
     class Meta:
         model = WcsCardInfosCell
-        fields = ('carddef_reference', 'title_type', 'custom_title', 'card_id', 'custom_schema')
+        fields = ('carddef_reference', 'title_type', 'custom_title', 'card_ids', 'custom_schema')
         widgets = {
             'custom_schema': forms.HiddenInput(),
         }
diff --git a/combo/apps/wcs/migrations/0043_card_ids.py b/combo/apps/wcs/migrations/0043_card_ids.py
new file mode 100644
index 00000000..c6d36179
--- /dev/null
+++ b/combo/apps/wcs/migrations/0043_card_ids.py
@@ -0,0 +1,21 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wcs', '0042_current_forms_custom_title'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='wcscardinfoscell',
+            old_name='card_id',
+            new_name='card_ids',
+        ),
+        migrations.AlterField(
+            model_name='wcscardinfoscell',
+            name='card_ids',
+            field=models.CharField(blank=True, max_length=1000, verbose_name='Card Identifiers'),
+        ),
+    ]
diff --git a/combo/apps/wcs/models.py b/combo/apps/wcs/models.py
index 46907afb..481f7658 100644
--- a/combo/apps/wcs/models.py
+++ b/combo/apps/wcs/models.py
@@ -24,7 +24,7 @@ from django.contrib.postgres.fields import JSONField
 from django.db import models
 from django.forms import Select
 from django.forms import models as model_forms
-from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
+from django.template import Context, RequestContext, Template, TemplateSyntaxError, VariableDoesNotExist
 from django.utils.safestring import mark_safe
 from django.utils.text import slugify
 from django.utils.translation import ugettext_lazy as _
@@ -899,7 +899,7 @@ class WcsCardsCell(CardMixin, WcsBlurpMixin, CellBase):
 @register_cell_class
 class WcsCardInfosCell(CardMixin, CellBase):
     carddef_reference = models.CharField(_('Card Model'), max_length=150)
-    card_id = models.CharField(_('Card Identifier'), max_length=1000, blank=True)
+    card_ids = models.CharField(_('Card Identifiers'), max_length=1000, blank=True)
     without_user = models.BooleanField(_('Ignore the logged-in user'), default=False)
     custom_schema = JSONField(blank=True, default=dict)
 
@@ -961,16 +961,39 @@ class WcsCardInfosCell(CardMixin, CellBase):
 
         populate_cache()
 
-    def get_card_id(self, context):
-        if self.card_id:
+    @property
+    def global_context_key(self):
+        return '%s-card-ids' % self.get_reference()
+
+    def modify_global_context(self, context, request):
+        if 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):
+        return len(context[self.global_context_key])
+
+    def get_card_ids(self, original_context, request):
+        if self.card_ids:
             try:
-                return Template(self.card_id).render(Context(context))
+                context = RequestContext(request)
+                context.push(original_context)
+                ids = Template(self.card_ids).render(context).split(',')
+                return [i.strip() for i in ids if i.strip()]
             except (VariableDoesNotExist, TemplateSyntaxError):
-                return None
+                return []
 
         card_slug = self.carddef_reference.split(':')[1]
         card_id = '%s_id' % card_slug
-        return context.get(card_id) or None
+        if original_context.get(card_id):
+            return [original_context[card_id]]
+        return []
+
+    def get_card_id(self, context):
+        repeat_index = getattr(self, 'repeat_index', context.get('repeat_index'))
+        if repeat_index is not None:
+            return context[self.global_context_key][repeat_index]
+        return None
 
     def get_extra_manager_context(self):
         extra_context = super().get_extra_manager_context()
@@ -992,7 +1015,7 @@ class WcsCardInfosCell(CardMixin, CellBase):
 
         card_id = self.get_card_id(context)
         if not card_id:
-            if self.card_id:  # template defined, but result is empty or None
+            if self.card_ids:  # template defined, but result is empty or None
                 extra_context['card_not_found'] = True
             return extra_context
         card_slug = self.carddef_reference.split(':')[1]
diff --git a/combo/data/models.py b/combo/data/models.py
index cd5362a6..67ed17ef 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -2104,8 +2104,7 @@ class ConfigJsonCell(JsonCellBase):
     def make_global(self):
         return settings.JSON_CELL_TYPES[self.key].get('make_global', False)
 
-    @property
-    def repeat(self):
+    def get_repeat_template(self, context):
         return settings.JSON_CELL_TYPES[self.key].get('repeat')
 
     @property
diff --git a/combo/public/templatetags/combo.py b/combo/public/templatetags/combo.py
index 6d8bfe7f..2fa21f8c 100644
--- a/combo/public/templatetags/combo.py
+++ b/combo/public/templatetags/combo.py
@@ -108,8 +108,8 @@ def placeholder(context, placeholder_name, **options):
         context['skeleton'] = ''
 
     if not context.get('placeholder_search_mode'):
-        for cell in [x for x in context['cells'] if hasattr(x, 'repeat')]:
-            repeat_template = cell.repeat
+        for cell in [x for x in context['cells'] if hasattr(x, 'get_repeat_template')]:
+            repeat_template = cell.get_repeat_template(context)
             if not repeat_template:
                 continue
 
diff --git a/tests/test_wcs.py b/tests/test_wcs.py
index e5ff9bcb..8e86c9fd 100644
--- a/tests/test_wcs.py
+++ b/tests/test_wcs.py
@@ -21,7 +21,6 @@ from requests.models import Response
 from combo.apps.search.engines import engines
 from combo.apps.search.models import SearchCell
 from combo.apps.search.utils import index_site, search_site
-from combo.apps.wcs.context_processors import Cards
 from combo.apps.wcs.models import (
     BackofficeSubmissionCell,
     CategoriesCell,
@@ -1616,7 +1615,6 @@ def test_cards_cell_render_user(mock_send, context, nocache):
     cell.carddef_reference = 'default:card_model_1'
     cell.save()
 
-    context['card_model_1_id'] = 11
     context['synchronous'] = True  # to get fresh content
 
     assert context['request'].user is None
@@ -1828,6 +1826,9 @@ def test_card_cell_render(mock_send, context):
     assert '<p>Unknown Card</p>' in result
 
     context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    cell.modify_global_context(context, request)
+    cell.repeat_index = 0
 
     # query should fail as nothing is cached
     cache.clear()
@@ -1897,6 +1898,9 @@ def test_card_cell_render_text_field(mock_send, context):
     cell.save()
 
     context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    cell.modify_global_context(context, request)
+    cell.repeat_index = 0
     context['synchronous'] = True  # to get fresh content
 
     result = cell.render(context)
@@ -1927,6 +1931,9 @@ def test_card_cell_render_email_field(mock_send, context):
     cell.save()
 
     context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    cell.modify_global_context(context, request)
+    cell.repeat_index = 0
     context['synchronous'] = True  # to get fresh content
 
     result = cell.render(context)
@@ -1952,6 +1959,9 @@ def test_card_cell_render_custom_schema(mock_send, context):
     cell.save()
 
     context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    cell.modify_global_context(context, request)
+    cell.repeat_index = 0
     context['synchronous'] = True  # to get fresh content
 
     result = cell.render(context)
@@ -1994,58 +2004,128 @@ def test_card_cell_render_custom_schema(mock_send, context):
 
 
 @mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
-def test_card_cell_render_identifier(mock_send, context, nocache):
-    page = Page.objects.create(title='xxx', template_name='standard')
+def test_card_cell_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(page=page, placeholder='content', order=0)
     cell.carddef_reference = 'default:card_model_1'
     cell.save()
 
-    context['card_model_1_id'] = 11
-    context['synchronous'] = True  # to get fresh content
+    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()
-    result = cell.render(context)
+    resp = app.get(page.get_online_url() + '11/')
+    assert len(resp.context['cells']) == 1
+    assert resp.context['cells'][0].pk == cell.pk
+    assert resp.context['cells'][0].repeat_index == 0
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert cell_resp.context['repeat_index'] == 0
+    assert 'Card Model 1' in cell_resp
+    assert '<p>Unknown Card</p>' not in cell_resp
+    assert len(mock_send.call_args_list) == 1
     assert '/api/cards/card_model_1/11/' in mock_send.call_args_list[0][0][0].url
-    assert 'Card Model 1' in result
-    assert '<p>Unknown Card</p>' not in result
 
-    # with identifier
-    cell.card_id = '42'
+    # with identifiers
+    page.sub_slug = ''
+    page.save()
+    cell.card_ids = '42'
     cell.save()
     mock_send.reset_mock()
-    result = cell.render(context)
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    assert resp.context['cells'][0].pk == cell.pk
+    assert resp.context['cells'][0].repeat_index == 0
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert cell_resp.context['repeat_index'] == 0
+    assert 'Card Model 1' in cell_resp
+    assert '<p>Unknown Card</p>' in cell_resp
+    assert len(mock_send.call_args_list) == 1
     assert '/api/cards/card_model_1/42/' in mock_send.call_args_list[0][0][0].url
-    assert 'Card Model 1' in result
-    assert '<p>Unknown Card</p>' in result
 
-    context['cards'] = Cards()
-    cell.card_id = '{% cards|objects:"card_model_1"|last|get:"id" %}'  # syntax error
+    cell.card_ids = '42, , 35'
     cell.save()
     mock_send.reset_mock()
-    result = cell.render(context)
-    assert mock_send.call_args_list == []
-    assert result.replace('\n', '') == ''  # empty-cell
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 2
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    for i in range(0, 2):
+        assert resp.context['cells'][i].pk == cell.pk
+        assert resp.context['cells'][i].repeat_index == i
+        cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[i])
+        assert cell_resp.context['repeat_index'] == i
+        assert 'Card Model 1' in cell_resp
+        assert '<p>Unknown Card</p>' in cell_resp
+    assert len(mock_send.call_args_list) == 2
+    assert '/api/cards/card_model_1/42/' in mock_send.call_args_list[0][0][0].url
+    assert '/api/cards/card_model_1/35/' in mock_send.call_args_list[1][0][0].url
 
-    del context['card_not_found']
-    cell.card_id = '{{ cards|objects:"card_model_1"|last|get:"id" }}'
+    cell.card_ids = '{% cards|objects:"card_model_1"|last|get:"id" %}'  # syntax error
     cell.save()
     mock_send.reset_mock()
-    result = cell.render(context)
-    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
-    assert '/api/cards/card_model_1/13/' in mock_send.call_args_list[1][0][0].url
-    assert 'Card Model 1' in result
-    assert '<p>Unknown Card</p>' in result
+    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 cell_resp.text.replace('\n', '') == ''  # empty-cell
 
-    # with a card_id template, but result is empty
-    del context['card_model_1_id']
-    cell.card_id = '{{ foo }}'
+    cell.card_ids = '{{ cards|objects:"card_model_1"|last|get:"id" }}'
     cell.save()
-    assert cell.get_card_id(context) == ''
     mock_send.reset_mock()
-    result = cell.render(context)
-    assert mock_send.call_args_list == []
-    assert result.replace('\n', '') == ''  # empty-cell
+    resp = app.get(page.get_online_url())
+    assert len(resp.context['cells']) == 1
+    assert resp.context['cells'][0].pk == cell.pk
+    assert resp.context['cells'][0].repeat_index == 0
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[0])
+    assert cell_resp.context['repeat_index'] == 0
+    assert len(mock_send.call_args_list) == 3
+    # page rendering
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+    # cell rendering
+    assert '/api/cards/card_model_1/list' in mock_send.call_args_list[1][0][0].url
+    assert '/api/cards/card_model_1/13/' in mock_send.call_args_list[2][0][0].url
+
+    for card_ids in [
+        '{% for card in cards|objects:"card_model_1" %}{{ card.id }},{% endfor %}',
+        '{{ cards|objects:"card_model_1"|getlist:"id"|join:"," }}',
+    ]:
+        cell.card_ids = card_ids
+        cell.save()
+        mock_send.reset_mock()
+        resp = app.get(page.get_online_url())
+        assert len(resp.context['cells']) == 3
+        extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+        for i in range(0, 3):
+            assert resp.context['cells'][i].pk == cell.pk
+            assert resp.context['cells'][i].repeat_index == i
+            cell_resp = app.get(cell_url + '?ctx=' + extra_ctx[i])
+            assert cell_resp.context['repeat_index'] == i
+        assert len(mock_send.call_args_list) == 7
+        # page rendering
+        assert '/api/cards/card_model_1/list' in mock_send.call_args_list[0][0][0].url
+        # cell rendering
+        for i in range(0, 3):
+            assert '/api/cards/card_model_1/list' in mock_send.call_args_list[i * 2 + 1][0][0].url
+            assert (
+                '/api/cards/card_model_1/%s/' % WCS_CARDS_DATA[i]['id']
+                in mock_send.call_args_list[i * 2 + 2][0][0].url
+            )
+
+    # with a card_ids template, but result is empty
+    cell.card_ids = '{{ foo }}'
+    cell.save()
+    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 cell_resp.text.replace('\n', '') == ''  # empty-cell
 
 
 @mock.patch('combo.apps.wcs.utils.requests.send', side_effect=mocked_requests_send)
@@ -2056,6 +2136,9 @@ def test_card_cell_render_user(mock_send, context, nocache):
     cell.save()
 
     context['card_model_1_id'] = 11
+    request = RequestFactory().get('/')
+    cell.modify_global_context(context, request)
+    cell.repeat_index = 0
     context['synchronous'] = True  # to get fresh content
 
     assert context['request'].user is None
@@ -2679,11 +2762,12 @@ def test_card_file_redirection(mock_send, app):
     page.save()
     cell = WcsCardInfosCell(page=page, placeholder='content', order=0)
     cell.carddef_reference = 'default:card_model_1'
-    cell.card_id = '11'
+    cell.card_ids = '11'
     cell.save()
     resp = app.get('/one/')
     ajax_cell_url = PyQuery(resp.text).find('[data-ajax-cell-url]').attr['data-ajax-cell-url']
-    resp = app.get(ajax_cell_url)
+    extra_ctx = re.findall(r'data-extra-context="(.*)"', resp.text)
+    resp = app.get(ajax_cell_url + '?ctx=' + extra_ctx[0])
     file_url = PyQuery(resp.text).find('[download]').attr['href']
     resp = app.get(file_url)
     assert 'download?f=42' in resp.location
-- 
2.33.0

