Skip to content

Commit 0be59a7

Browse files
rcoldironRoxanna Coldironvsalvino
authored
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>
1 parent c83f249 commit 0be59a7

10 files changed

Lines changed: 385 additions & 12 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from django.db import migrations, models
2+
import django.db.models.deletion
3+
import modelcluster.fields
4+
5+
6+
def add_navbar_orderables(apps, schema_editor):
7+
Site = apps.get_model('wagtailcore', 'Site')
8+
LayoutSettings = apps.get_model('coderedcms', 'LayoutSettings')
9+
Navbar = apps.get_model('coderedcms', 'Navbar')
10+
NavbarOrderable = apps.get_model('coderedcms', 'NavbarOrderable')
11+
# If it's a new site, this migration will not run.
12+
try:
13+
site = Site.objects.get(is_default_site=True)
14+
layout = LayoutSettings.objects.get(site=site)
15+
except (Site.DoesNotExist, LayoutSettings.DoesNotExist):
16+
return
17+
current_navs = Navbar.objects.all()
18+
db_alias = schema_editor.connection.alias
19+
layout.site_navbar = []
20+
layout.save()
21+
for nav in current_navs:
22+
NavbarOrderable.objects.using(db_alias).create(navbar_chooser=layout, navbar=nav)
23+
24+
25+
def add_footer_orderables(apps, schema_editor):
26+
Site = apps.get_model('wagtailcore', 'Site')
27+
LayoutSettings = apps.get_model('coderedcms', 'LayoutSettings')
28+
Footer = apps.get_model('coderedcms', 'Footer')
29+
FooterOrderable = apps.get_model('coderedcms', 'FooterOrderable')
30+
# If it's a new site, this migration will not run.
31+
try:
32+
site = Site.objects.get(is_default_site=True)
33+
layout = LayoutSettings.objects.get(site=site)
34+
except (Site.DoesNotExist, LayoutSettings.DoesNotExist):
35+
return
36+
current_footers = Footer.objects.all()
37+
db_alias = schema_editor.connection.alias
38+
layout.site_footer = []
39+
layout.save()
40+
for footer in current_footers:
41+
FooterOrderable.objects.using(db_alias).create(footer_chooser=layout, footer=footer)
42+
43+
44+
class Migration(migrations.Migration):
45+
46+
atomic = False
47+
48+
dependencies = [
49+
('coderedcms', '0028_auto_20220609_1532'),
50+
]
51+
52+
operations = [
53+
migrations.CreateModel(
54+
name='NavbarOrderable',
55+
fields=[
56+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
57+
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
58+
('navbar', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='coderedcms.navbar')),
59+
('navbar_chooser', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_navbar', to='coderedcms.layoutsettings', verbose_name='Site Navbars')),
60+
],
61+
options={
62+
'ordering': ['sort_order'],
63+
'abstract': False,
64+
},
65+
),
66+
migrations.CreateModel(
67+
name='FooterOrderable',
68+
fields=[
69+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
70+
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
71+
('footer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='coderedcms.footer')),
72+
('footer_chooser', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='site_footer', to='coderedcms.layoutsettings', verbose_name='Site Footers')),
73+
],
74+
options={
75+
'ordering': ['sort_order'],
76+
'abstract': False,
77+
},
78+
),
79+
migrations.RunPython(add_navbar_orderables),
80+
migrations.RunPython(add_footer_orderables)
81+
]
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# what imports do I need?
2+
# possibly use https://pypi.org/project/django-test-migrations/ instead?
3+
4+
5+
from django.test import Client
6+
7+
# from django_test_migrations.contrib.unittest_case import MigratorTestCase
8+
# from django.db.migrations.executor import MigrationExecutor
9+
# from django.db import connection
10+
11+
from wagtail.tests.utils import WagtailPageTests
12+
from wagtail.core.models import Site
13+
14+
from coderedcms.tests.testapp.models import WebPage
15+
from coderedcms.models.snippet_models import Footer, Navbar
16+
from coderedcms.models.wagtailsettings_models import (
17+
LayoutSettings,
18+
NavbarOrderable,
19+
FooterOrderable,
20+
)
21+
22+
23+
class NavbarFooterTestCase(WagtailPageTests):
24+
"""
25+
Test that the relevant navbar chooser settings appear in the homepage HTML.
26+
"""
27+
28+
model = WebPage
29+
30+
def setUp(self):
31+
# HTTP client.
32+
self.client = Client()
33+
34+
# Use home page and default site.
35+
self.site = Site.objects.filter(is_default_site=True)[0]
36+
self.homepage = WebPage.objects.get(url_path="/home/")
37+
38+
# create 2 nav snippets
39+
self.navbar = Navbar.objects.create(name="Nav1", custom_id="Nav1")
40+
self.navbar2 = Navbar.objects.create(name="Nav2", custom_id="Nav2")
41+
self.footer = Footer.objects.create(name="Footer1", custom_id="Footer1")
42+
self.footer2 = Footer.objects.create(name="Footer2", custom_id="Footer2")
43+
44+
# Populate settings.
45+
self.settings = LayoutSettings.for_site(self.site)
46+
# layout = self.settings
47+
self.navbarorderable = NavbarOrderable.objects.create(
48+
sort_order=0,
49+
navbar_chooser=LayoutSettings.objects.get(id=self.settings.id),
50+
navbar=Navbar.objects.get(id=self.navbar.id),
51+
)
52+
self.footerorderable = FooterOrderable.objects.create(
53+
sort_order=0,
54+
footer_chooser=LayoutSettings.objects.get(id=self.settings.id),
55+
footer=Footer.objects.get(id=self.footer.id),
56+
)
57+
# save settings
58+
self.settings.save()
59+
60+
def test_get(self):
61+
"""
62+
Tests to make sure the page serves a 200 from a GET request.
63+
"""
64+
response = self.client.get(self.homepage.url, follow=True)
65+
self.assertEqual(response.status_code, 200)
66+
67+
def test_navbar(self):
68+
"""
69+
Make sure navbar is on homepage.
70+
"""
71+
response = self.client.get(self.homepage.url, follow=True)
72+
# Checks if specified HTML is within response
73+
# https://docs.djangoproject.com/en/3.2/topics/testing/tools/#django.test.SimpleTestCase.assertContains
74+
self.assertContains(
75+
response,
76+
text=f'<ul class="navbar-nav" id="{self.navbar.custom_id}">',
77+
status_code=200,
78+
html=True,
79+
)
80+
self.assertNotContains(
81+
response,
82+
text=f'<ul class="navbar-nav" id="{self.navbar2.custom_id}">',
83+
status_code=200,
84+
html=True,
85+
)
86+
87+
def test_multi_navbars(self):
88+
"""
89+
Adds another navbar and checks if it shows on page.
90+
"""
91+
self.navbarorderable2 = NavbarOrderable.objects.create(
92+
sort_order=1,
93+
navbar_chooser=LayoutSettings.objects.get(id=self.settings.id),
94+
navbar=Navbar.objects.get(id=self.navbar2.id),
95+
)
96+
# get the navbar (orderable)
97+
self.settings.save()
98+
# update settings for using 2 navs, then check that both navbars show and in right order
99+
response = self.client.get(self.homepage.url, follow=True)
100+
self.assertContains(
101+
response,
102+
text=f'<ul class="navbar-nav" id="{self.navbar.custom_id}">'
103+
f'</ul><ul class="navbar-nav" id="{self.navbar2.custom_id}">',
104+
status_code=200,
105+
html=True,
106+
)
107+
108+
def test_footer(self):
109+
"""
110+
Make sure footer is on homepage.
111+
"""
112+
response = self.client.get(self.homepage.url, follow=True)
113+
114+
self.assertContains(
115+
response, text=f'<div id="{self.footer.custom_id}">', status_code=200, html=True
116+
)
117+
self.assertNotContains(
118+
response, text=f'<div id="{self.footer2.custom_id}">', status_code=200, html=True
119+
)
120+
121+
def test_multi_footers(self):
122+
"""
123+
Adds another footer to settings and checks if it shows on page.
124+
"""
125+
self.footerorderable2 = FooterOrderable.objects.create(
126+
sort_order=1,
127+
footer_chooser=LayoutSettings.objects.get(id=self.settings.id),
128+
footer=Footer.objects.get(id=self.footer2.id),
129+
)
130+
# get the footer (orderable)
131+
self.settings.save()
132+
# update settings for using 2 footers, then check that both footers show
133+
response = self.client.get(self.homepage.url, follow=True)
134+
self.assertContains(
135+
response,
136+
text=f'<div id="{self.footer.custom_id}"></div><div id="{self.footer2.custom_id}">',
137+
status_code=200,
138+
html=True,
139+
)
140+
141+
142+
# # Set up
143+
# class TestMigrations(TestCase):
144+
# @property
145+
# def app(self):
146+
# return apps.get_containing_app_config(type(self).__module__).name
147+
148+
# migrate_from = None
149+
# migrate_to = None
150+
151+
# def setUp(self):
152+
# assert self.migrate_from and self.migrate_to, \
153+
# "TestCase '{}' must define migrate_from and
154+
# migrate_to properties".format(type(self).__name__)
155+
# self.migrate_from = [(self.app, self.migrate_from)]
156+
# self.migrate_to = [(self.app, self.migrate_to)]
157+
# executor = MigrationExecutor(connection)
158+
# old_apps = executor.loader.project_state(self.migrate_from).apps
159+
160+
# # Reverse to the original migration
161+
# executor.migrate(self.migrate_from)
162+
163+
# self.setUpBeforeMigration(old_apps)
164+
165+
# # Run the migration to test
166+
# executor = MigrationExecutor(connection)
167+
# executor.loader.build_graph() # reload.
168+
# executor.migrate(self.migrate_to)
169+
170+
# self.apps = executor.loader.project_state(self.migrate_to).apps
171+
172+
173+
# # Need to create Site that has navbar and footer the current way?
174+
# class NavsandFootersTestCase(TestMigrations):
175+
176+
# # migrate_from = '0024_analyticssettings'
177+
# # migrate_to = '0025_multinavs.py'
178+
179+
# def setUpBeforeMigration(self, apps):
180+
# # self.site = Site.objects.filter(is_default_site=True)[0]
181+
# Navbar = apps.get_model('coderedcms', 'Navbar')
182+
# Navbar.id = Navbar.objects.create(
183+
# name="Main Nav",
184+
# menu_items = StreamField([('external_link', 'item1'), ('external_link', 'item2') ])
185+
# # menu_items = build content here
186+
# )
187+
# Footer = apps.get_model('coderedcms', 'Footer')
188+
# Footer.id = Footer.objects.create(
189+
# name="Main Footer",
190+
# content=StreamField([('text', 'this is a footer')])
191+
# # content = build content here
192+
# )
193+
194+
# def t_navs_footers_migrated(self):
195+
# NavbarOrderable = apps.get_model('coderedcms', 'NavbarOrderable')

coderedcms/models/wagtailsettings_models.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66

77
from django.db import models
88
from django.utils.translation import gettext_lazy as _
9-
from wagtail.admin.edit_handlers import FieldPanel, HelpPanel, MultiFieldPanel
9+
from modelcluster.fields import ParentalKey
10+
from modelcluster.models import ClusterableModel
11+
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, HelpPanel, MultiFieldPanel
12+
from wagtail.core.models import Orderable
1013
from wagtail.images.edit_handlers import ImageChooserPanel
14+
from wagtail.snippets.edit_handlers import SnippetChooserPanel
1115
from wagtail.contrib.settings.models import BaseSetting, register_setting
1216
from wagtail.images import get_image_model_string
13-
1417
from coderedcms.fields import MonospaceField
1518
from coderedcms.settings import crx_settings
19+
from coderedcms.models.snippet_models import Navbar, Footer
1620

1721

1822
@register_setting(icon='cr-desktop')
19-
class LayoutSettings(BaseSetting):
23+
class LayoutSettings(ClusterableModel, BaseSetting):
2024
"""
2125
Branding, navbar, and theme settings.
2226
"""
@@ -106,6 +110,11 @@ class Meta:
106110
],
107111
heading=_('Branding')
108112
),
113+
InlinePanel(
114+
'site_navbar',
115+
help_text=_('Choose one or more navbars for your site.'),
116+
heading=_('Site Navbars')
117+
),
109118
MultiFieldPanel(
110119
[
111120
FieldPanel('navbar_color_scheme'),
@@ -119,6 +128,11 @@ class Meta:
119128
],
120129
heading=_('Site Navbar Layout')
121130
),
131+
InlinePanel(
132+
'site_footer',
133+
help_text=_('Choose one or more footers for your site.'),
134+
heading=_('Site Footers')
135+
),
122136
MultiFieldPanel(
123137
[
124138
FieldPanel('frontend_theme'),
@@ -155,6 +169,42 @@ def __init__(self, *args, **kwargs):
155169
self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT
156170

157171

172+
class NavbarOrderable(Orderable, models.Model):
173+
navbar_chooser = ParentalKey(
174+
LayoutSettings,
175+
related_name="site_navbar",
176+
verbose_name=_('Site Navbars')
177+
)
178+
navbar = models.ForeignKey(
179+
Navbar,
180+
blank=True,
181+
null=True,
182+
on_delete=models.CASCADE,
183+
)
184+
185+
panels = [
186+
SnippetChooserPanel("navbar")
187+
]
188+
189+
190+
class FooterOrderable(Orderable, models.Model):
191+
footer_chooser = ParentalKey(
192+
LayoutSettings,
193+
related_name="site_footer",
194+
verbose_name=_('Site Footers')
195+
)
196+
footer = models.ForeignKey(
197+
Footer,
198+
blank=True,
199+
null=True,
200+
on_delete=models.CASCADE,
201+
)
202+
203+
panels = [
204+
SnippetChooserPanel("footer")
205+
]
206+
207+
158208
@register_setting(icon='cr-google')
159209
class AnalyticsSettings(BaseSetting):
160210
"""
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{% load wagtailcore_tags coderedcms_tags %}
2-
<footer>
32

3+
{% if settings.coderedcms.LayoutSettings.site_footer %}
4+
5+
<footer>
46
{% get_footers as footers %}
57
{% for footer in footers %}
68
<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 @@
911
{% endfor %}
1012
</div>
1113
{% endfor %}
12-
1314
</footer>
15+
16+
{% endif %}

0 commit comments

Comments
 (0)