Browse 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 2 years ago
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.db import models
 from django.utils.translation import gettext_lazy as _
 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.images.edit_handlers import ImageChooserPanel
+from wagtail.snippets.edit_handlers import SnippetChooserPanel
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.images import get_image_model_string
 from wagtail.images import get_image_model_string
-
 from coderedcms.fields import MonospaceField
 from coderedcms.fields import MonospaceField
 from coderedcms.settings import crx_settings
 from coderedcms.settings import crx_settings
+from coderedcms.models.snippet_models import Navbar, Footer
 
 
 
 
 @register_setting(icon='cr-desktop')
 @register_setting(icon='cr-desktop')
-class LayoutSettings(BaseSetting):
+class LayoutSettings(ClusterableModel, BaseSetting):
     """
     """
     Branding, navbar, and theme settings.
     Branding, navbar, and theme settings.
     """
     """
@@ -106,6 +110,11 @@ class LayoutSettings(BaseSetting):
             ],
             ],
             heading=_('Branding')
             heading=_('Branding')
         ),
         ),
+        InlinePanel(
+            'site_navbar',
+            help_text=_('Choose one or more navbars for your site.'),
+            heading=_('Site Navbars')
+        ),
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
                 FieldPanel('navbar_color_scheme'),
                 FieldPanel('navbar_color_scheme'),
@@ -119,6 +128,11 @@ class LayoutSettings(BaseSetting):
             ],
             ],
             heading=_('Site Navbar Layout')
             heading=_('Site Navbar Layout')
         ),
         ),
+        InlinePanel(
+            'site_footer',
+            help_text=_('Choose one or more footers for your site.'),
+            heading=_('Site Footers')
+        ),
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
                 FieldPanel('frontend_theme'),
                 FieldPanel('frontend_theme'),
@@ -155,6 +169,42 @@ class LayoutSettings(BaseSetting):
             self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT
             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')
 @register_setting(icon='cr-google')
 class AnalyticsSettings(BaseSetting):
 class AnalyticsSettings(BaseSetting):
     """
     """

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

@@ -1,6 +1,8 @@
 {% load wagtailcore_tags coderedcms_tags %}
 {% load wagtailcore_tags coderedcms_tags %}
-<footer>
 
 
+{% if settings.coderedcms.LayoutSettings.site_footer %}
+
+<footer>
 {% get_footers as footers %}
 {% get_footers as footers %}
 {% for footer in 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 %}>
     <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 %}
         {% endfor %}
     </div>
     </div>
 {% endfor %}
 {% endfor %}
-
 </footer>
 </footer>
+
+{% endif %}

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

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

+ 18 - 7
coderedcms/templatetags/coderedcms_tags.py

@@ -3,6 +3,7 @@ import random
 
 
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup
 from django import template
 from django import template
+from django.db.models.query import QuerySet
 from django.forms import ClearableFileInput
 from django.forms import ClearableFileInput
 from django.utils.html import mark_safe
 from django.utils.html import mark_safe
 from wagtail.core.models import Collection
 from wagtail.core.models import Collection
@@ -11,7 +12,7 @@ from wagtail.images.models import Image
 from coderedcms import utils, __version__
 from coderedcms import utils, __version__
 from coderedcms.blocks import CoderedAdvSettings
 from coderedcms.blocks import CoderedAdvSettings
 from coderedcms.forms import SearchForm
 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 crx_settings as crx_settings_obj
 from coderedcms.settings import get_bootstrap_setting
 from coderedcms.settings import get_bootstrap_setting
 from coderedcms.models.wagtailsettings_models import LayoutSettings
 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
 @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 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.
 **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.
 **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 Link With Sub-Links
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 **Document**: The document you want to link to that will be downloaded.
 **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.
     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:
 .. _footer:
 
 
 Customizing the 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.
         What our footer looks like on the website.
         Remember, we have done minimal styling on the site.
         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
 Similarly, it is advisable to initially copy the contents of `Wagtail CRX
 footer.html`_, but not necessary.
 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
 .. _Wagtail CRX footer.html: https://github.com/coderedcorp/coderedcms/blob/dev/coderedcms/templates/coderedcms/snippets/footer.html