From 4c4580f79e493cc0757d1f5233a08d9ad9abef25 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 3 Apr 2020 14:43:58 +0200
Subject: [PATCH] misc: get cells from private placeholders in parent
 acquisition (#41276)

---
 combo/data/models.py  | 16 +++++++++++++---
 combo/public/views.py |  8 +++++++-
 tests/test_public.py  | 36 +++++++++++++++++++++++++++++++++++-
 3 files changed, 55 insertions(+), 5 deletions(-)

diff --git a/combo/data/models.py b/combo/data/models.py
index a295707f..d5cd4219 100644
--- a/combo/data/models.py
+++ b/combo/data/models.py
@@ -39,7 +39,7 @@ from django.core import serializers
 from django.db import models, transaction
 from django.db.models.base import ModelBase
 from django.db.models.signals import pre_save, post_save, post_delete
-from django.db.models import Max
+from django.db.models import Max, Q
 from django.dispatch import receiver
 from django.forms import models as model_forms
 from django import forms
@@ -569,6 +569,7 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
     default_form_class = None
     manager_form_factory_kwargs = {}
     manager_form_template = 'combo/cell_form.html'
+    children_placeholder_prefix = None
 
     visible = True
     user_dependant = False
@@ -715,12 +716,15 @@ class CellBase(six.with_metaclass(CellMeta, models.Model)):
         if load_contenttypes:
             # populate ContentType cache
             ContentType.objects.get_for_models(*cell_classes)
+        extra_filter = kwargs.pop('extra_filter', None)
         for klass in cell_classes:
             if klass is None:
                 continue
             if cell_filter and not cell_filter(klass):
                 continue
             cells_queryset = klass.objects.filter(**kwargs)
+            if extra_filter:
+                cells_queryset = cells_queryset.filter(extra_filter)
             if select_related:
                 cells_queryset = cells_queryset.select_related(
                     *select_related.get('__all__', []),
@@ -1210,6 +1214,7 @@ class LinkListCell(CellBase):
 
     template_name = 'combo/link-list-cell.html'
     manager_form_template = 'combo/manager/link-list-cell-form.html'
+    children_placeholder_prefix = '_linkslist:'
 
     invalid_reason_codes = {
         'data_link_invalid': _('Invalid link'),
@@ -1220,7 +1225,7 @@ class LinkListCell(CellBase):
 
     @property
     def link_placeholder(self):
-        return '_linkslist:{}'.format(self.pk)
+        return self.children_placeholder_prefix + str(self.pk)
 
     def get_items(self, prefetch_validity_info=False):
         return CellBase.get_cells(
@@ -1381,7 +1386,12 @@ class ParentContentCell(CellBase):
         cells_by_page = {}
         for page in pages:
             cells_by_page[page.id] = []
-        for cell in list(CellBase.get_cells(placeholder=self.placeholder, page__in=pages)):
+        # get cells from placeholder + cells in private placeholders that may
+        # be used by actual cells.
+        placeholder_filter = Q(placeholder=self.placeholder)
+        for klass in CellBase.get_cell_classes(lambda x: bool(x.children_placeholder_prefix)):
+            placeholder_filter |= Q(placeholder__startswith=klass.children_placeholder_prefix)
+        for cell in CellBase.get_cells(page__in=pages, extra_filter=placeholder_filter):
             cells_by_page[cell.page_id].append(cell)
 
         cells = cells_by_page[pages[-1].id]
diff --git a/combo/public/views.py b/combo/public/views.py
index ea82b6e5..f4f27d89 100644
--- a/combo/public/views.py
+++ b/combo/public/views.py
@@ -174,11 +174,17 @@ def extend_with_parent_cells(cells, hierarchy):
     if len(hierarchy) == 1 and hierarchy[0].slug == 'index':
         # home page cannot contain parent cells
         return
+    seen = {}
     for cell in cells[:]:
         if not isinstance(cell, ParentContentCell):
             continue
         idx = cells.index(cell)
-        cells[idx:idx+1] = cell.get_parents_cells(hierarchy=hierarchy[:-1])
+        parent_cells = cell.get_parents_cells(hierarchy=hierarchy[:-1])
+        # keep cells that were not already seen and mark cells as seen,
+        # simultaneously.
+        parent_cells = [seen.setdefault((x.__class__, x.id), True) and x
+                for x in parent_cells if (x.__class__, x.id) not in seen]
+        cells[idx:idx + 1] = parent_cells
 
 
 def should_check_badges():
diff --git a/tests/test_public.py b/tests/test_public.py
index 5a41f127..367b5c01 100644
--- a/tests/test_public.py
+++ b/tests/test_public.py
@@ -25,7 +25,7 @@ import requests
 
 from combo.wsgi import application
 from combo.data.models import (Page, CellBase, TextCell, ParentContentCell,
-        FeedCell, LinkCell, ConfigJsonCell, Redirect, JsonCell)
+        FeedCell, LinkCell, LinkListCell, ConfigJsonCell, Redirect, JsonCell)
 from combo.apps.family.models import FamilyInfosCell
 
 pytestmark = pytest.mark.django_db
@@ -199,6 +199,40 @@ def test_page_footer_acquisition(app):
     resp = app.get('/sixth/', status=200)
     assert resp.text.count('BARFOO') == 1
 
+
+def test_list_of_links_acquisition(app):
+    Page.objects.all().delete()
+    index_page = Page(title='Home', slug='index', template_name='standard')
+    index_page.save()
+
+    cell = LinkListCell.objects.create(order=0, page=index_page, placeholder='footer')
+    LinkCell.objects.create(
+        page=index_page,
+        placeholder=cell.link_placeholder,
+        title='Example Site',
+        url='http://example.net/',
+        order=0,
+    )
+    LinkCell.objects.create(
+        page=index_page,
+        placeholder=cell.link_placeholder,
+        title='Example2 Site',
+        url='http://example.org/',
+        order=1,
+    )
+
+    resp = app.get('/', status=200)
+    assert resp.text.count('Example Site') == 1
+    assert resp.text.count('Example2 Site') == 1
+
+    page = Page(title='Second', slug='second', template_name='standard')
+    page.save()
+    ParentContentCell(page=page, placeholder='footer', order=0).save()
+    resp = app.get('/second/', status=200)
+    assert resp.text.count('Example Site') == 1
+    assert resp.text.count('Example2 Site') == 1
+
+
 def test_page_redirect(app):
     Page.objects.all().delete()
     page = Page(title='Elsewhere', slug='elsewhere', template_name='standard',
-- 
2.26.0

