Parcourir la source

Multisite navbars and footers (#408)

* Updated the LayoutSettings model to include a navbar chooser and footer chooser.
* Updated the get_footer and get_navbar functions to pull in the choice from settings.
* Updated the templates to pull in the choices and also what to do if no choice is made.
* Multiple sites can use the same navbar/footer if chosen in the Layout Settings for each site.
* Simple implementation for select site navbar and footer.
* Migration to set navbars and footers for existing sites.

Co-authored-by: Roxanna Coldiron <roxanna@coderedcorp.com>
Co-authored-by: Vince Salvino <salvino@coderedcorp.com>
Roxanna Coldiron il y a 2 ans
Parent
commit
0be59a79a9

+ 81 - 0
coderedcms/migrations/0029_multinavs.py

@@ -0,0 +1,81 @@
+from django.db import migrations, models
+import django.db.models.deletion
+import modelcluster.fields
+
+
+def add_navbar_orderables(apps, schema_editor):
+    Site = apps.get_model('wagtailcore', 'Site')
+    LayoutSettings = apps.get_model('coderedcms', 'LayoutSettings')
+    Navbar = apps.get_model('coderedcms', 'Navbar')
+    NavbarOrderable = apps.get_model('coderedcms', 'NavbarOrderable')
+    # If it's a new site, this migration will not run.
+    try:
+        site = Site.objects.get(is_default_site=True)
+        layout = LayoutSettings.objects.get(site=site)
+    except (Site.DoesNotExist, LayoutSettings.DoesNotExist):
+        return
+    current_navs = Navbar.objects.all()
+    db_alias = schema_editor.connection.alias
+    layout.site_navbar = []
+    layout.save()
+    for nav in current_navs:
+        NavbarOrderable.objects.using(db_alias).create(navbar_chooser=layout, navbar=nav)
+
+
+def add_footer_orderables(apps, schema_editor):
+    Site = apps.get_model('wagtailcore', 'Site')
+    LayoutSettings = apps.get_model('coderedcms', 'LayoutSettings')
+    Footer = apps.get_model('coderedcms', 'Footer')
+    FooterOrderable = apps.get_model('coderedcms', 'FooterOrderable')
+    # If it's a new site, this migration will not run.
+    try:
+        site = Site.objects.get(is_default_site=True)
+        layout = LayoutSettings.objects.get(site=site)
+    except (Site.DoesNotExist, LayoutSettings.DoesNotExist):
+        return
+    current_footers = Footer.objects.all()
+    db_alias = schema_editor.connection.alias
+    layout.site_footer = []
+    layout.save()
+    for footer in current_footers:
+        FooterOrderable.objects.using(db_alias).create(footer_chooser=layout, footer=footer)
+
+
+class Migration(migrations.Migration):
+
+    atomic = False
+
+    dependencies = [
+        ('coderedcms', '0028_auto_20220609_1532'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='NavbarOrderable',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
+                ('navbar', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='coderedcms.navbar')),
+                ('navbar_chooser', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_navbar', to='coderedcms.layoutsettings', verbose_name='Site Navbars')),
+            ],
+            options={
+                'ordering': ['sort_order'],
+                'abstract': False,
+            },
+        ),
+        migrations.CreateModel(
+            name='FooterOrderable',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
+                ('footer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='coderedcms.footer')),
+                ('footer_chooser', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_footer', to='coderedcms.layoutsettings', verbose_name='Site Footers')),
+            ],
+            options={
+                'ordering': ['sort_order'],
+                'abstract': False,
+            },
+        ),
+        migrations.RunPython(add_navbar_orderables),
+        migrations.RunPython(add_footer_orderables)
+    ]

+ 195 - 0
coderedcms/models/tests/test_navbars_and_footers.py

@@ -0,0 +1,195 @@
+# what imports do I need?
+# possibly use https://pypi.org/project/django-test-migrations/ instead?
+
+
+from django.test import Client
+
+# from django_test_migrations.contrib.unittest_case import MigratorTestCase
+# from django.db.migrations.executor import MigrationExecutor
+# from django.db import connection
+
+from wagtail.tests.utils import WagtailPageTests
+from wagtail.core.models import Site
+
+from coderedcms.tests.testapp.models import WebPage
+from coderedcms.models.snippet_models import Footer, Navbar
+from coderedcms.models.wagtailsettings_models import (
+    LayoutSettings,
+    NavbarOrderable,
+    FooterOrderable,
+)
+
+
+class NavbarFooterTestCase(WagtailPageTests):
+    """
+    Test that the relevant navbar chooser settings appear in the homepage HTML.
+    """
+
+    model = WebPage
+
+    def setUp(self):
+        # HTTP client.
+        self.client = Client()
+
+        # Use home page and default site.
+        self.site = Site.objects.filter(is_default_site=True)[0]
+        self.homepage = WebPage.objects.get(url_path="/home/")
+
+        # create 2 nav snippets
+        self.navbar = Navbar.objects.create(name="Nav1", custom_id="Nav1")
+        self.navbar2 = Navbar.objects.create(name="Nav2", custom_id="Nav2")
+        self.footer = Footer.objects.create(name="Footer1", custom_id="Footer1")
+        self.footer2 = Footer.objects.create(name="Footer2", custom_id="Footer2")
+
+        # Populate settings.
+        self.settings = LayoutSettings.for_site(self.site)
+        # layout = self.settings
+        self.navbarorderable = NavbarOrderable.objects.create(
+            sort_order=0,
+            navbar_chooser=LayoutSettings.objects.get(id=self.settings.id),
+            navbar=Navbar.objects.get(id=self.navbar.id),
+        )
+        self.footerorderable = FooterOrderable.objects.create(
+            sort_order=0,
+            footer_chooser=LayoutSettings.objects.get(id=self.settings.id),
+            footer=Footer.objects.get(id=self.footer.id),
+        )
+        # save settings
+        self.settings.save()
+
+    def test_get(self):
+        """
+        Tests to make sure the page serves a 200 from a GET request.
+        """
+        response = self.client.get(self.homepage.url, follow=True)
+        self.assertEqual(response.status_code, 200)
+
+    def test_navbar(self):
+        """
+        Make sure navbar is on homepage.
+        """
+        response = self.client.get(self.homepage.url, follow=True)
+        # Checks if specified HTML is within response
+        # https://docs.djangoproject.com/en/3.2/topics/testing/tools/#django.test.SimpleTestCase.assertContains
+        self.assertContains(
+            response,
+            text=f'<ul class="navbar-nav" id="{self.navbar.custom_id}">',
+            status_code=200,
+            html=True,
+        )
+        self.assertNotContains(
+            response,
+            text=f'<ul class="navbar-nav" id="{self.navbar2.custom_id}">',
+            status_code=200,
+            html=True,
+        )
+
+    def test_multi_navbars(self):
+        """
+        Adds another navbar and checks if it shows on page.
+        """
+        self.navbarorderable2 = NavbarOrderable.objects.create(
+            sort_order=1,
+            navbar_chooser=LayoutSettings.objects.get(id=self.settings.id),
+            navbar=Navbar.objects.get(id=self.navbar2.id),
+        )
+        # get the navbar (orderable)
+        self.settings.save()
+        # update settings for using 2 navs, then check that both navbars show and in right order
+        response = self.client.get(self.homepage.url, follow=True)
+        self.assertContains(
+            response,
+            text=f'<ul class="navbar-nav" id="{self.navbar.custom_id}">'
+            f'</ul><ul class="navbar-nav" id="{self.navbar2.custom_id}">',
+            status_code=200,
+            html=True,
+        )
+
+    def test_footer(self):
+        """
+        Make sure footer is on homepage.
+        """
+        response = self.client.get(self.homepage.url, follow=True)
+
+        self.assertContains(
+            response, text=f'<div id="{self.footer.custom_id}">', status_code=200, html=True
+        )
+        self.assertNotContains(
+            response, text=f'<div id="{self.footer2.custom_id}">', status_code=200, html=True
+        )
+
+    def test_multi_footers(self):
+        """
+        Adds another footer to settings and checks if it shows on page.
+        """
+        self.footerorderable2 = FooterOrderable.objects.create(
+            sort_order=1,
+            footer_chooser=LayoutSettings.objects.get(id=self.settings.id),
+            footer=Footer.objects.get(id=self.footer2.id),
+        )
+        # get the footer (orderable)
+        self.settings.save()
+        # update settings for using 2 footers, then check that both footers show
+        response = self.client.get(self.homepage.url, follow=True)
+        self.assertContains(
+            response,
+            text=f'<div id="{self.footer.custom_id}"></div><div id="{self.footer2.custom_id}">',
+            status_code=200,
+            html=True,
+        )
+
+
+# # Set up
+# class TestMigrations(TestCase):
+#     @property
+#     def app(self):
+#         return apps.get_containing_app_config(type(self).__module__).name
+
+#     migrate_from = None
+#     migrate_to = None
+
+#     def setUp(self):
+#         assert self.migrate_from and self.migrate_to, \
+#         "TestCase '{}' must define migrate_from and
+#         migrate_to properties".format(type(self).__name__)
+#         self.migrate_from = [(self.app, self.migrate_from)]
+#         self.migrate_to = [(self.app, self.migrate_to)]
+#         executor = MigrationExecutor(connection)
+#         old_apps = executor.loader.project_state(self.migrate_from).apps
+
+#         # Reverse to the original migration
+#         executor.migrate(self.migrate_from)
+
+#         self.setUpBeforeMigration(old_apps)
+
+#         # Run the migration to test
+#         executor = MigrationExecutor(connection)
+#         executor.loader.build_graph()  # reload.
+#         executor.migrate(self.migrate_to)
+
+#         self.apps = executor.loader.project_state(self.migrate_to).apps
+
+
+# # Need to create Site that has navbar and footer the current way?
+# class NavsandFootersTestCase(TestMigrations):
+
+#     # migrate_from = '0024_analyticssettings'
+#     # migrate_to = '0025_multinavs.py'
+
+#     def setUpBeforeMigration(self, apps):
+#         # self.site = Site.objects.filter(is_default_site=True)[0]
+#         Navbar = apps.get_model('coderedcms', 'Navbar')
+#         Navbar.id = Navbar.objects.create(
+#             name="Main Nav",
+#             menu_items = StreamField([('external_link', 'item1'), ('external_link', 'item2') ])
+#             # menu_items = build content here
+#         )
+#         Footer = apps.get_model('coderedcms', 'Footer')
+#         Footer.id = Footer.objects.create(
+#             name="Main Footer",
+#             content=StreamField([('text', 'this is a footer')])
+#             # content = build content here
+#         )
+
+#    def t_navs_footers_migrated(self):
+#           NavbarOrderable = apps.get_model('coderedcms', 'NavbarOrderable')

+ 53 - 3
coderedcms/models/wagtailsettings_models.py

@@ -6,17 +6,21 @@ Global project or developer settings should be defined in coderedcms.settings.py
 
 from django.db import models
 from django.utils.translation import gettext_lazy as _
-from wagtail.admin.edit_handlers import FieldPanel, HelpPanel, MultiFieldPanel
+from modelcluster.fields import ParentalKey
+from modelcluster.models import ClusterableModel
+from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, HelpPanel, MultiFieldPanel
+from wagtail.core.models import Orderable
 from wagtail.images.edit_handlers import ImageChooserPanel
+from wagtail.snippets.edit_handlers import SnippetChooserPanel
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.images import get_image_model_string
-
 from coderedcms.fields import MonospaceField
 from coderedcms.settings import crx_settings
+from coderedcms.models.snippet_models import Navbar, Footer
 
 
 @register_setting(icon='cr-desktop')
-class LayoutSettings(BaseSetting):
+class LayoutSettings(ClusterableModel, BaseSetting):
     """
     Branding, navbar, and theme settings.
     """
@@ -106,6 +110,11 @@ class LayoutSettings(BaseSetting):
             ],
             heading=_('Branding')
         ),
+        InlinePanel(
+            'site_navbar',
+            help_text=_('Choose one or more navbars for your site.'),
+            heading=_('Site Navbars')
+        ),
         MultiFieldPanel(
             [
                 FieldPanel('navbar_color_scheme'),
@@ -119,6 +128,11 @@ class LayoutSettings(BaseSetting):
             ],
             heading=_('Site Navbar Layout')
         ),
+        InlinePanel(
+            'site_footer',
+            help_text=_('Choose one or more footers for your site.'),
+            heading=_('Site Footers')
+        ),
         MultiFieldPanel(
             [
                 FieldPanel('frontend_theme'),
@@ -155,6 +169,42 @@ class LayoutSettings(BaseSetting):
             self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT
 
 
+class NavbarOrderable(Orderable, models.Model):
+    navbar_chooser = ParentalKey(
+        LayoutSettings,
+        related_name="site_navbar",
+        verbose_name=_('Site Navbars')
+    )
+    navbar = models.ForeignKey(
+        Navbar,
+        blank=True,
+        null=True,
+        on_delete=models.CASCADE,
+    )
+
+    panels = [
+        SnippetChooserPanel("navbar")
+    ]
+
+
+class FooterOrderable(Orderable, models.Model):
+    footer_chooser = ParentalKey(
+        LayoutSettings,
+        related_name="site_footer",
+        verbose_name=_('Site Footers')
+    )
+    footer = models.ForeignKey(
+        Footer,
+        blank=True,
+        null=True,
+        on_delete=models.CASCADE,
+    )
+
+    panels = [
+        SnippetChooserPanel("footer")
+    ]
+
+
 @register_setting(icon='cr-google')
 class AnalyticsSettings(BaseSetting):
     """

+ 5 - 2
coderedcms/templates/coderedcms/snippets/footer.html

@@ -1,6 +1,8 @@
 {% load wagtailcore_tags coderedcms_tags %}
-<footer>
 
+{% if settings.coderedcms.LayoutSettings.site_footer %}
+
+<footer>
 {% get_footers as footers %}
 {% for footer in footers %}
     <div {% if footer.custom_id %} id="{{footer.custom_id}}"{% endif %} {% if footer.custom_css_class %} class="{{footer.custom_css_class}}"{% endif %}>
@@ -9,5 +11,6 @@
         {% endfor %}
     </div>
 {% endfor %}
-
 </footer>
+
+{% endif %}

+ 2 - 0
coderedcms/templates/coderedcms/snippets/navbar.html

@@ -25,6 +25,7 @@
     </button>
 
     <div class="collapse navbar-collapse" id="navbar">
+      {% if settings.coderedcms.LayoutSettings.site_navbar %}
       {% get_navbars as navbars %}
       {% for navbar in navbars %}
       <ul class="navbar-nav {{navbar.custom_css_class}}"
@@ -34,6 +35,7 @@
         {% endfor %}
       </ul>
       {% endfor %}
+      {% endif %}
       {% if settings.coderedcms.LayoutSettings.navbar_search %}
       <form class="form-inline ml-auto" action="{% url 'codered_search' %}" method="GET">
           {% load bootstrap4 %}

+ 18 - 7
coderedcms/templatetags/coderedcms_tags.py

@@ -3,6 +3,7 @@ import random
 
 from bs4 import BeautifulSoup
 from django import template
+from django.db.models.query import QuerySet
 from django.forms import ClearableFileInput
 from django.utils.html import mark_safe
 from wagtail.core.models import Collection
@@ -11,7 +12,7 @@ from wagtail.images.models import Image
 from coderedcms import utils, __version__
 from coderedcms.blocks import CoderedAdvSettings
 from coderedcms.forms import SearchForm
-from coderedcms.models import Footer, Navbar
+from coderedcms.models.snippet_models import Navbar, Footer
 from coderedcms.settings import crx_settings as crx_settings_obj
 from coderedcms.settings import get_bootstrap_setting
 from coderedcms.models.wagtailsettings_models import LayoutSettings
@@ -78,14 +79,24 @@ def get_navbar_css(context):
     ])
 
 
-@register.simple_tag
-def get_navbars():
-    return Navbar.objects.all()
+@register.simple_tag(takes_context=True)
+def get_navbars(context) -> 'QuerySet[Navbar]':
+    layout = LayoutSettings.for_request(context['request'])
+    navbarorderables = layout.site_navbar.all()
+    navbars = Navbar.objects.filter(
+        navbarorderable__in=navbarorderables
+        ).order_by('navbarorderable__sort_order')
+    return navbars
 
 
-@register.simple_tag
-def get_footers():
-    return Footer.objects.all()
+@register.simple_tag(takes_context=True)
+def get_footers(context) -> 'QuerySet[Footer]':
+    layout = LayoutSettings.for_request(context['request'])
+    footerorderables = layout.site_footer.all()
+    footers = Footer.objects.filter(
+        footerorderable__in=footerorderables
+        ).order_by('footerorderable__sort_order')
+    return footers
 
 
 @register.simple_tag

+ 5 - 0
docs/features/snippets/footers.rst

@@ -15,3 +15,8 @@ Fields
 **Custom CSS class**: If you need to add a specific css class for this footer, add it here.
 **Custom ID**: If you need to add a specific ID for this footer, add it here.
 **Content**: A streamfield that contains the layout blocks for the content wall.
+
+Site Footers
+------------
+
+In **Settings > Layout**, select which Footers you want to display on your site under the Site Footers section.

+ 5 - 0
docs/features/snippets/navigation_bars.rst

@@ -36,3 +36,8 @@ External Link With Sub-Links
 Document Link With Sub-Links
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 **Document**: The document you want to link to that will be downloaded.
+
+Site Navbars
+------------
+
+In **Settings > Layout**, select which Navbars you want to display on your site under the Site Navbars section.

+ 13 - 0
docs/getting_started/tutorial03.rst

@@ -63,6 +63,12 @@ for our sweets shop!
 
     The website with our menu added. Also note that we are using some Bootstrap colors on the site.
 
+Site Navbars Settings
+---------------------
+
+After you create your navbar, go to **Settings > Layout** and scroll down to **Site Navbars**. Click on the plus sign
+to select your new navbar and add it to your site. The Save.
+
 .. _footer:
 
 Customizing the Footer
@@ -136,3 +142,10 @@ then choosing which page or external link you want. See our example below:
 
         What our footer looks like on the website.
         Remember, we have done minimal styling on the site.
+
+
+Site Footers Settings
+---------------------
+
+After you create your footer, go to **Settings > Layout** and scroll down to **Site Footers**. Click on the plus sign
+to select your new footer and add it to your site. The Save.

+ 8 - 0
docs/how_to/headers_and_footers.rst

@@ -55,4 +55,12 @@ override the ``footer.html`` file included with Wagtail CRX.
 Similarly, it is advisable to initially copy the contents of `Wagtail CRX
 footer.html`_, but not necessary.
 
+.. note::
+
+    You can now create more than one navbar menu or footer and choose which ones to render on your site. In **Settings > Layout**,
+    select your navbars in **Site Navbars**. Select your footers in **Site Footers**. The Site Navbar Layout includes settings for the whole
+    navbar, while the Site Navbar chooser allows you to choose which menu you want for your site. This features allows you to
+    select a different navbar/footer per site in a multisite installation OR render several navbars/footers in selected order
+    on a single site.
+
 .. _Wagtail CRX footer.html: https://github.com/coderedcorp/coderedcms/blob/dev/coderedcms/templates/coderedcms/snippets/footer.html