From b40a820a601603633575fba6bd4c0dc2d9e84685 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 30 Aug 2022 23:51:14 +0200
Subject: [PATCH] manager: associate a page to a card model (#68675)

---
 combo/apps/wcs/utils.py                      |   8 +
 combo/data/models.py                         |  12 ++
 combo/manager/forms.py                       |  43 +++++
 combo/manager/static/css/combo.manager.scss  |   4 +
 combo/manager/templates/combo/page_view.html |  19 ++-
 combo/manager/urls.py                        |   5 +
 combo/manager/views.py                       |  11 ++
 tests/wcs/test_card.py                       | 167 ++++++-------------
 8 files changed, 145 insertions(+), 124 deletions(-)

diff --git a/combo/apps/wcs/utils.py b/combo/apps/wcs/utils.py
index d7c50bef..79116179 100644
--- a/combo/apps/wcs/utils.py
+++ b/combo/apps/wcs/utils.py
@@ -102,3 +102,11 @@ def get_matching_pages_from_card_slug(card_slug, order=True):
     if not order:
         return matching_pages
     return Page.get_as_reordered_flat_hierarchy(matching_pages)
+
+
+def get_wcs_matching_card_model(sub_slug):
+    card_models = get_wcs_options('/api/cards/@list')
+    for carddef_reference, card_label in card_models:
+        card_id = '%s_id' % carddef_reference.split(':')[1]
+        if '<%s>' % card_id in sub_slug or sub_slug == card_id:
+            return carddef_reference, card_label
diff --git a/combo/data/models.py b/combo/data/models.py
index f62b489c..735a778a 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -64,6 +64,7 @@ from django.utils.timezone import now
 from django.utils.translation import gettext_lazy as _
 
 from combo import utils
+from combo.apps.wcs.utils import get_wcs_matching_card_model, is_wcs_enabled
 from combo.utils import NothingInCacheException
 
 from .fields import RichTextField, TemplatableURLField
@@ -273,6 +274,17 @@ class Page(models.Model):
             return None
         return os.path.splitext(self.picture.name)[-1]
 
+    def get_sub_slug_details(self):
+        if not self.sub_slug:
+            return
+
+        if not is_wcs_enabled(None):
+            return self.sub_slug, None
+        result = get_wcs_matching_card_model(self.sub_slug)
+        if not result:
+            return self.sub_slug, None
+        return self.sub_slug.replace('-', '_'), result[1]
+
     def save(self, *args, **kwargs):
         if 'update_fields' in kwargs:
             return super().save(*args, **kwargs)
diff --git a/combo/manager/forms.py b/combo/manager/forms.py
index a97dfbc0..950fc4ac 100644
--- a/combo/manager/forms.py
+++ b/combo/manager/forms.py
@@ -26,6 +26,7 @@ from django.template.loader import TemplateDoesNotExist, get_template
 from django.utils.translation import gettext_lazy as _
 
 from combo import utils
+from combo.apps.wcs.utils import get_wcs_matching_card_model, get_wcs_options
 from combo.data.forms import get_page_choices
 from combo.data.models import Page, ParentContentCell, SiteSettings, compile_sub_slug
 
@@ -151,6 +152,14 @@ class PageEditSlugForm(forms.ModelForm):
         model = Page
         fields = ('slug', 'sub_slug')
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if self.instance.sub_slug:
+            result = get_wcs_matching_card_model(self.instance.sub_slug)
+            if result:
+                del self.fields['sub_slug']
+
     def clean_slug(self):
         value = self.cleaned_data.get('slug')
         if self.instance.slug == value:
@@ -172,6 +181,40 @@ class PageEditSlugForm(forms.ModelForm):
         return value
 
 
+class PageEditLinkedCardForm(forms.ModelForm):
+    carddef_reference = forms.ChoiceField(
+        label=_('Linked card model'),
+        required=False,
+    )
+
+    class Meta:
+        model = Page
+        fields = []
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        card_models = get_wcs_options('/api/cards/@list')
+        self.fields['carddef_reference'].choices = [('', '-----')] + card_models
+
+        if self.instance.sub_slug:
+            result = get_wcs_matching_card_model(self.instance.sub_slug)
+            if result:
+                self.initial['carddef_reference'] = result[0]
+
+    def save(self, commit=True, *args, **kwargs):
+        super().save(commit=False, *args, **kwargs)
+
+        if self.cleaned_data.get('carddef_reference'):
+            self.instance.sub_slug = '%s_id' % self.cleaned_data['carddef_reference'].split(':')[1]
+        else:
+            self.instance.sub_slug = ''
+
+        if commit:
+            self.instance.save()
+        return self.instance
+
+
 class PageEditDescriptionForm(forms.ModelForm):
     class Meta:
         model = Page
diff --git a/combo/manager/static/css/combo.manager.scss b/combo/manager/static/css/combo.manager.scss
index 29ebc673..093648fe 100644
--- a/combo/manager/static/css/combo.manager.scss
+++ b/combo/manager/static/css/combo.manager.scss
@@ -481,6 +481,10 @@ span.extra-info {
 	opacity: 0.5;
 }
 
+h2 span.extra-info {
+	font-size: 60%;
+}
+
 img.page-picture {
 	max-width: 100%;
 	max-height: 250px;
diff --git a/combo/manager/templates/combo/page_view.html b/combo/manager/templates/combo/page_view.html
index 739810ba..c10b2e17 100644
--- a/combo/manager/templates/combo/page_view.html
+++ b/combo/manager/templates/combo/page_view.html
@@ -4,7 +4,9 @@
 {% load thumbnail %}
 
 {% block appbar %}
-  <h2>{% trans 'Page' %} - {{ object.title }}</h2>
+  {% with object.get_sub_slug_details as sub_slug_details %}
+    <h2>{% trans 'Page' %} - {{ object.title }}{% if with_wcs and sub_slug_details.1 %} <span class="extra-info">({% blocktrans with card_model=sub_slug_details.1 %}page linked to card model "{{ card_model }}"{% endblocktrans %})</span>{% endif %}</h2>
+  {% endwith %}
   <span class="actions">
     <a class="extra-actions-menu-opener"></a>
     <a class="action-see-online" href="{{ object.get_online_url }}">{% trans 'See online' %}</a>
@@ -12,6 +14,9 @@
       <li><a class="action-history" href="{% url 'combo-manager-page-history' pk=object.id %}">{% trans 'History' %}</a></li>
       <li><a {% if page_has_subpages %}rel="popup" data-autoclose-dialog="true" {% endif %}class="action-export" href="{% url 'combo-manager-page-export' pk=object.id %}">{% trans 'Export' %}</a></li>
       <li><a class="action-edit-page-variables" rel="popup" href="{% url 'combo-manager-page-edit-extra-variables' pk=object.id %}">{% trans "Edit extra page variables" %}</a></li>
+      {% if with_wcs %}
+        <li><a class="action-edit-page-linked-card" rel="popup" href="{% url 'combo-manager-page-edit-linked-card' pk=object.id %}">{% trans "Link a card model" %}</a></li>
+      {% endif %}
       {% if perms.data.add_page %}
         <li><a class="action-add-child" rel="popup" href="{% url 'combo-manager-page-add-child' pk=object.id %}">{% trans 'Add a child page' %}</a></li>
         <li><a class="action-edit-roles" rel="popup" href="{% url 'combo-manager-page-edit-roles' pk=object.id %}">{% trans 'Manage edit roles' %}</a></li>
@@ -40,11 +45,13 @@
         (<a rel="popup" href="{% url 'combo-manager-page-edit-title' pk=object.id %}">{% trans 'change' %}</a>)
       </p>
 
-      <p>
-        <label>{% trans 'Slug:' %}</label>
-        <tt>{{ object.slug }}{% if object.sub_slug %}/<span class="subslug">{{ object.sub_slug }}</span>{% endif %}</tt>
-        (<a rel="popup" href="{% url 'combo-manager-page-edit-slug' pk=object.id %}">{% trans 'change' %}</a>)
-      </p>
+      {% with object.get_sub_slug_details as sub_slug_details %}
+        <p>
+          <label>{% trans 'Slug:' %}</label>
+          <tt>{{ object.slug }}{% if sub_slug_details and not sub_slug_details.1 %}/<span class="subslug">{{ sub_slug_details.0 }}</span>{% endif %}</tt>
+          (<a rel="popup" href="{% url 'combo-manager-page-edit-slug' pk=object.id %}">{% trans 'change' %}</a>)
+        </p>
+      {% endwith %}
 
       <p>
         <label>{% trans 'Description:' %}</label>
diff --git a/combo/manager/urls.py b/combo/manager/urls.py
index 9b9ab838..0042e4db 100644
--- a/combo/manager/urls.py
+++ b/combo/manager/urls.py
@@ -52,6 +52,11 @@ urlpatterns = [
         name='combo-manager-page-edit-include-in-navigation',
     ),
     re_path(r'^pages/(?P<pk>\d+)/slug$', views.page_edit_slug, name='combo-manager-page-edit-slug'),
+    re_path(
+        r'^pages/(?P<pk>\d+)/linked-card$',
+        views.page_edit_linked_card,
+        name='combo-manager-page-edit-linked-card',
+    ),
     re_path(r'^pages/(?P<pk>\d+)/title$', views.page_edit_title, name='combo-manager-page-edit-title'),
     re_path(
         r'^pages/(?P<pk>\d+)/description$',
diff --git a/combo/manager/views.py b/combo/manager/views.py
index 14003071..05191e0d 100644
--- a/combo/manager/views.py
+++ b/combo/manager/views.py
@@ -54,6 +54,7 @@ from django.views.generic import (
 )
 
 from combo import plugins
+from combo.apps.wcs.utils import is_wcs_enabled
 from combo.data.library import get_cell_class
 from combo.data.models import CellBase, LinkListCell, Page, PageSnapshot, ParentContentCell, SiteSettings
 from combo.data.utils import (
@@ -75,6 +76,7 @@ from .forms import (
     PageEditDescriptionForm,
     PageEditExtraVariablesFormSet,
     PageEditIncludeInNavigationForm,
+    PageEditLinkedCardForm,
     PageEditPictureForm,
     PageEditRedirectionForm,
     PageEditRolesForm,
@@ -335,6 +337,14 @@ class PageEditSlugView(PageEditView):
 page_edit_slug = PageEditSlugView.as_view()
 
 
+class PageEditLinkedCardView(PageEditView):
+    form_class = PageEditLinkedCardForm
+    comment = _('changed linked card')
+
+
+page_edit_linked_card = PageEditLinkedCardView.as_view()
+
+
 class PageEditDescriptionView(PageEditView):
     form_class = PageEditDescriptionForm
     comment = _('changed description')
@@ -441,6 +451,7 @@ class PageView(ManagedPageMixin, DetailView):
         context['cell_type_groups'] = list(cell_type_groups.items())
         context['cell_type_groups'].sort(key=lambda x: x[0])
         context['page_has_subpages'] = self.object.get_children().exists()
+        context['with_wcs'] = is_wcs_enabled(None)
 
         self.object.prefetched_cells = cells
         placeholders = []
diff --git a/tests/wcs/test_card.py b/tests/wcs/test_card.py
index 24af35c6..9a3afa2c 100644
--- a/tests/wcs/test_card.py
+++ b/tests/wcs/test_card.py
@@ -3154,124 +3154,6 @@ def test_card_cell_assets(mock_send, settings, app, admin_user):
 
 
 def test_page_snapshot_with_old_card_cells():
-    """
-    page_serialization = {
-        'cells': [{
-            'model': 'data.parentcontentcell',
-            'fields': {
-                'slug': '',
-                'order': 0,
-                'groups': [],
-                'public': True,
-                'condition': None,
-                'placeholder': 'footer',
-                'template_name': None,
-                'extra_css_class': '',
-                'last_update_timestamp': '2022-08-08T13:55:29.599Z',
-                'restricted_to_unlogged': False,
-                'page': 14
-            }
-        }, {
-            'model': 'wcs.wcscardinfoscell',
-            'fields': {
-                'slug': 'enfant',
-                'limit': None,
-                'order': 1,
-                'groups': [],
-                'public': True,
-                'card_ids': '',
-                'condition': '',
-                'title_type': 'auto',
-                'placeholder': 'content',
-                'custom_title': '',
-                'display_mode': 'card',
-                'without_user': False,
-                'custom_schema': {},
-                'only_for_user': False,
-                'template_name': None,
-                'extra_css_class': '',
-                'carddef_reference': 'eservices:test-related-enfant',
-                'related_card_path': '',
-                'last_update_timestamp': '2022-08-11T13:57:43.362Z',
-                'restricted_to_unlogged': False,
-                'page': 14
-            }
-        }, {
-            'model': 'wcs.wcscardinfoscell',
-            'fields': {
-                'slug': '',
-                'limit': None,
-                'order': 2,
-                'groups': [],
-                'public': True,
-                'card_ids': '',
-                'condition': '',
-                'title_type': 'auto',
-                'placeholder': 'content',
-                'custom_title': '',
-                'display_mode': 'card',
-                'without_user': False,
-                'custom_schema': {},
-                'only_for_user': False,
-                'template_name': None,
-                'extra_css_class': '',
-                'carddef_reference': 'eservices:test-related-adulte:par-defaut',
-                'related_card_path': 'enfant/block_parents_parent',
-                'last_update_timestamp': '2022-08-08T19:53:02.097Z',
-                'restricted_to_unlogged': False,
-                'page': 14
-            }
-        }, {
-            'model': 'wcs.wcscardinfoscell',
-            'fields': {
-                'slug': '',
-                'limit': None,
-                'order': 3,
-                'groups': [],
-                'public': True,
-                'card_ids': '',
-                'condition': '',
-                'title_type': 'empty',
-                'placeholder': 'content',
-                'custom_title': 'Les parents !',
-                'display_mode': 'table',
-                'without_user': False,
-                'custom_schema': {},
-                'only_for_user': False,
-                'template_name': None,
-                'extra_css_class': '',
-                'carddef_reference': 'eservices:test-related-adulte:par-defaut',
-                'related_card_path': 'enfant/block_parents_parent',
-                'last_update_timestamp': '2022-08-11T13:07:35.757Z',
-                'restricted_to_unlogged': False,
-                'page': 14
-            }
-        }],
-        'fields': {
-            'slug': 'related-enfant',
-            'order': 6,
-            'title': 'Related enfant',
-            'groups': [],
-            'parent': ['search-related'],
-            'public': True,
-            'picture': '',
-            'snapshot': None,
-            'sub_slug': 'test-related-enfant_id',
-            'edit_role': None,
-            'description': '',
-            'redirect_url': '',
-            'template_name': 'standard',
-            'extra_variables': {},
-            'creation_timestamp': '2022-08-08T13:55:29.502Z',
-            'subpages_edit_role': None,
-            'placeholder_options': {},
-            'last_update_timestamp': '2022-08-11T13:57:43.369Z',
-            'exclude_from_navigation': True
-        },
-        'model': 'data.page',
-        'pk': 14
-    }
-    """
     page = Page.objects.create(title='xxx', slug='test_snapshots', template_name='standard')
     PageSnapshot.take(page)
     snapshot = page.pagesnapshot_set.first()
@@ -3415,3 +3297,52 @@ def test_export_import_card_cell_with_page_link():
     new_card_page = Page.objects.get(slug='card')
     new_cell = WcsCardCell.objects.get()
     assert new_cell.custom_schema['cells'][0]['page'] == ''
+
+
+@mock.patch('requests.Session.send', side_effect=mocked_requests_send)
+def test_page_edit_linked_card(mock_send, app, admin_user):
+    page = Page.objects.create(title='One', slug='one', template_name='two')
+
+    app = login(app)
+    resp = app.get('/manage/pages/%s/' % page.pk)
+
+    resp = resp.click(href='.*/linked-card')
+    assert [o[0] for o in resp.form['carddef_reference'].options] == [
+        '',
+        'default:card_model_1',
+        'default:card_model_2',
+        'default:card_model_3',
+        'default:card_a',
+        'default:card_b',
+        'default:card_c',
+        'default:card_d',
+        'default:card_e',
+        'other:card_model_1',
+        'other:card_model_2',
+        'other:card_model_3',
+        'other:card_a',
+        'other:card_b',
+        'other:card_c',
+        'other:card_d',
+        'other:card_e',
+    ]
+    resp.form['carddef_reference'] = 'default:card_model_3'
+    resp = resp.form.submit().follow()
+    page.refresh_from_db()
+    assert page.sub_slug == 'card_model_3_id'
+    resp = app.get('/manage/pages/%s/slug' % page.pk)
+    assert 'sub_slug' not in resp.context['form'].fields
+
+    resp = app.get('/manage/pages/%s/linked-card' % page.pk)
+    assert resp.form['carddef_reference'].value == 'default:card_model_3'
+    resp.form['carddef_reference'] = ''
+    resp = resp.form.submit().follow()
+    page.refresh_from_db()
+    assert page.sub_slug == ''
+
+    resp = app.get('/manage/pages/%s/slug' % page.pk)
+    assert 'sub_slug' in resp.context['form'].fields
+    resp.form['sub_slug'] = 'foobar'
+    resp = resp.form.submit().follow()
+    page.refresh_from_db()
+    assert page.sub_slug == 'foobar'
-- 
2.35.1

