+ + {{ person.first_name }} {{ person.last_name }} + +
++ {{ person.role }}{% if person.department %} — {{ person.department }}{% endif %} +
+From b38f629a073b8d374876ef06261a695c3b418ab3 Mon Sep 17 00:00:00 2001 From: Pra26nav <24sharmap_2@rbunagpur.in> Date: Fri, 6 Mar 2026 20:42:18 +0530 Subject: [PATCH 1/6] Add People section - PersonIndexPage and PersonPage --- ...erson_first_name_alter_person_last_name.py | 23 ++ bakerydemo/base/models.py | 7 +- bakerydemo/breads/tests/test_bread_page.py | 43 +++ bakerydemo/people/__init__.py | 1 + bakerydemo/people/apps.py | 7 + ...xpage_alter_personpage_options_and_more.py | 95 ++++++ bakerydemo/people/models.py | 281 ++++++------------ bakerydemo/people/tests/__init__.py | 1 + bakerydemo/people/tests/test_people_page.py | 100 +++++++ bakerydemo/static/css/main.css | 140 +++++++-- bakerydemo/static/js/main.js | 131 ++++++-- bakerydemo/templates/includes/header.html | 74 +++-- .../templates/people/person_index_page.html | 45 +++ bakerydemo/templates/people/person_page.html | 84 +++--- 14 files changed, 719 insertions(+), 313 deletions(-) create mode 100644 bakerydemo/base/migrations/0027_alter_person_first_name_alter_person_last_name.py create mode 100644 bakerydemo/breads/tests/test_bread_page.py create mode 100644 bakerydemo/people/apps.py create mode 100644 bakerydemo/people/migrations/0004_personindexpage_alter_personpage_options_and_more.py create mode 100644 bakerydemo/people/tests/__init__.py create mode 100644 bakerydemo/people/tests/test_people_page.py create mode 100644 bakerydemo/templates/people/person_index_page.html diff --git a/bakerydemo/base/migrations/0027_alter_person_first_name_alter_person_last_name.py b/bakerydemo/base/migrations/0027_alter_person_first_name_alter_person_last_name.py new file mode 100644 index 000000000..8ee3622f1 --- /dev/null +++ b/bakerydemo/base/migrations/0027_alter_person_first_name_alter_person_last_name.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.3 on 2026-03-06 04:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0026_alter_formpage_body_alter_gallerypage_body_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='person', + name='first_name', + field=models.CharField(default='', max_length=100), + ), + migrations.AlterField( + model_name='person', + name='last_name', + field=models.CharField(default='', max_length=100), + ), + ] diff --git a/bakerydemo/base/models.py b/bakerydemo/base/models.py index dfd73147c..c4d07074a 100644 --- a/bakerydemo/base/models.py +++ b/bakerydemo/base/models.py @@ -64,8 +64,10 @@ class Person( https://github.com/wagtail/django-modelcluster """ - first_name = models.CharField("First name", max_length=254) - last_name = models.CharField("Last name", max_length=254) + # first_name = models.CharField("First name", max_length=254) + # last_name = models.CharField("Last name", max_length=254) + first_name = models.CharField(max_length=100, default='') + last_name = models.CharField(max_length=100, default='') job_title = models.CharField("Job title", max_length=254) image = models.ForeignKey( @@ -641,3 +643,4 @@ def get_task_states_user_can_moderate(self, user, **kwargs): @classmethod def get_description(cls): return _("Only a specific user can approve this task") + diff --git a/bakerydemo/breads/tests/test_bread_page.py b/bakerydemo/breads/tests/test_bread_page.py new file mode 100644 index 000000000..53e71bfec --- /dev/null +++ b/bakerydemo/breads/tests/test_bread_page.py @@ -0,0 +1,43 @@ +from wagtail.models import Page, Site +from wagtail.test.utils import WagtailPageTestCase + +from bakerydemo.breads.models import BreadPage + + +class BreadPageRenderTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + """ + Create the page tree and site once for all tests in this class. + + Steps: + 1. Identify the root page. + 2. Create a Site pointing at the root. + 3. Create a BreadPage instance with the required fields. + 4. Add it to the tree and publish so it is live. + """ + # 1. Identify the root page + cls.root = Page.get_first_root_node() + + # 2. Create the Site that will serve our test pages + cls.site = Site.objects.create( + hostname="testserver", + root_page=cls.root, + is_default_site=True, + ) + + # 3. Create the BreadPage instance we want to test + cls.bread_page = BreadPage( + title="Test bread", + slug="test-bread", + introduction="A test bread page.", + ) + + # 4. Add to the tree and publish so it is live + cls.root.add_child(instance=cls.bread_page) + cls.bread_page.save_revision().publish() + + def test_bread_page_renders(self): + response = self.client.get(self.bread_page.url) + self.assertEqual(response.status_code, 200) + diff --git a/bakerydemo/people/__init__.py b/bakerydemo/people/__init__.py index e69de29bb..d3f5a12fa 100644 --- a/bakerydemo/people/__init__.py +++ b/bakerydemo/people/__init__.py @@ -0,0 +1 @@ + diff --git a/bakerydemo/people/apps.py b/bakerydemo/people/apps.py new file mode 100644 index 000000000..03c6a6463 --- /dev/null +++ b/bakerydemo/people/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PeopleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bakerydemo.people" + diff --git a/bakerydemo/people/migrations/0004_personindexpage_alter_personpage_options_and_more.py b/bakerydemo/people/migrations/0004_personindexpage_alter_personpage_options_and_more.py new file mode 100644 index 000000000..4245dc07e --- /dev/null +++ b/bakerydemo/people/migrations/0004_personindexpage_alter_personpage_options_and_more.py @@ -0,0 +1,95 @@ +# Generated by Django 6.0.3 on 2026-03-06 04:26 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('people', '0003_personpage_social_links'), + ('wagtailcore', '0096_referenceindex_referenceindex_source_object_and_more'), + ('wagtailimages', '0027_image_description'), + ] + + operations = [ + migrations.CreateModel( + name='PersonIndexPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ('intro', wagtail.fields.RichTextField(blank=True)), + ], + options={ + 'verbose_name': 'Person Index Page', + }, + bases=('wagtailcore.page',), + ), + migrations.AlterModelOptions( + name='personpage', + options={'verbose_name': 'Person Page'}, + ), + migrations.RemoveField( + model_name='personpage', + name='body', + ), + migrations.RemoveField( + model_name='personpage', + name='image', + ), + migrations.RemoveField( + model_name='personpage', + name='introduction', + ), + migrations.RemoveField( + model_name='personpage', + name='location', + ), + migrations.RemoveField( + model_name='personpage', + name='social_links', + ), + migrations.AddField( + model_name='personpage', + name='bio', + field=wagtail.fields.RichTextField(blank=True), + ), + migrations.AddField( + model_name='personpage', + name='department', + field=models.CharField(blank=True, max_length=200), + ), + migrations.AddField( + model_name='personpage', + name='email', + field=models.EmailField(blank=True, max_length=254), + ), + migrations.AddField( + model_name='personpage', + name='first_name', + field=models.CharField(default='', max_length=100), + ), + migrations.AddField( + model_name='personpage', + name='last_name', + field=models.CharField(default='', max_length=100), + ), + migrations.AddField( + model_name='personpage', + name='linkedin_url', + field=models.URLField(blank=True), + ), + migrations.AddField( + model_name='personpage', + name='photo', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'), + ), + migrations.AddField( + model_name='personpage', + name='role', + field=models.CharField(default='', max_length=200), + ), + migrations.DeleteModel( + name='PeopleIndexPage', + ), + ] diff --git a/bakerydemo/people/models.py b/bakerydemo/people/models.py index 82e1119e1..c479ca5d3 100644 --- a/bakerydemo/people/models.py +++ b/bakerydemo/people/models.py @@ -1,189 +1,92 @@ -from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator -from django.db import models -from wagtail.admin.panels import FieldPanel -from wagtail.api import APIField -from wagtail.blocks import ChoiceBlock, StructBlock, StructValue, URLBlock -from wagtail.fields import StreamField -from wagtail.models import Page -from wagtail.search import index - -from bakerydemo.base.blocks import BaseStreamBlock - -from ..breads.models import Country - - -class SocialMediaValue(StructValue): - def get_platform_label(self): - return dict(self.block.child_blocks["platform"].field.choices).get( - self["platform"] - ) - - -class SocialMediaBlock(StructBlock): - """ - Block for social media links - """ - - platform = ChoiceBlock( - choices=[ - ("github", "GitHub"), - ("twitter", "Twitter/X"), - ("linkedin", "LinkedIn"), - ("instagram", "Instagram"), - ("facebook", "Facebook"), - ("mastodon", "Mastodon"), - ("website", "Personal Website"), - ], - help_text="Select the social media platform", - ) - url = URLBlock( - label="URL", - help_text="Full URL to your profile (e.g., https://github.com/username)", - ) - - class Meta: - icon = "link" - label = "Social Media Link" - value_class = SocialMediaValue - - -class PersonPage(Page): - """ - Detail view for a specific person - """ - - introduction = models.TextField(help_text="Text to describe the page", blank=True) - image = models.ForeignKey( - "wagtailimages.Image", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - help_text="Landscape mode only; horizontal width between 1000px and 3000px.", - ) - body = StreamField(BaseStreamBlock(), verbose_name="Page body", blank=True) - - location = models.ForeignKey( - Country, - on_delete=models.SET_NULL, - null=True, - blank=True, - ) - - social_links = StreamField( - [("social", SocialMediaBlock())], - blank=True, - help_text="Add social media profiles", - ) - - content_panels = Page.content_panels + [ - FieldPanel("introduction"), - FieldPanel("image"), - FieldPanel("location"), - FieldPanel("body"), - FieldPanel("social_links"), - ] - - search_fields = Page.search_fields + [ - index.SearchField("introduction"), - index.SearchField("body"), - ] - - parent_page_types = ["PeopleIndexPage"] - - api_fields = [ - APIField("introduction"), - APIField("image"), - APIField("body"), - APIField("location"), - APIField("social_links"), - ] - - def get_context(self, request): - context = super(PersonPage, self).get_context(request) - - platform_block = SocialMediaBlock().child_blocks["platform"] - platform_labels = dict(platform_block.field.choices) - social_links = [ - { - "platform": link.value["platform"], - "label": platform_labels.get( - link.value["platform"], link.value["platform"] - ), - "url": link.value["url"], - } - for link in (self.social_links or []) - ] - - context["social_links"] = social_links - - return context - - -class PeopleIndexPage(Page): - """ - Index page for people. - - Lists all People objects with pagination. - """ - - introduction = models.TextField(help_text="Text to describe the page", blank=True) - image = models.ForeignKey( - "wagtailimages.Image", - null=True, - blank=True, - on_delete=models.SET_NULL, - related_name="+", - help_text="Landscape mode only; horizontal width between 1000px and 3000px.", - ) - - content_panels = Page.content_panels + [ - FieldPanel("introduction"), - FieldPanel("image"), - ] - - # Can only have PersonPage children - subpage_types = ["PersonPage"] - - api_fields = [ - APIField("introduction"), - APIField("image"), - ] - - # Returns a queryset of PersonPage objects that are live, that are direct - # descendants of this index page with most recent first - def get_people(self): - return ( - PersonPage.objects.live() - .descendant_of(self) - .order_by("-first_published_at") - ) - - # Allows child objects (e.g. PersonPage objects) to be accessible via the - # template - def children(self): - return self.get_children().specific().live() - - # Pagination for the index page - def paginate(self, request, *args): - page = request.GET.get("page") - paginator = Paginator(self.get_people(), 12) - try: - pages = paginator.page(page) - except PageNotAnInteger: - pages = paginator.page(1) - except EmptyPage: - pages = paginator.page(paginator.num_pages) - return pages - - # Returns the above to the get_context method that is used to populate the - # template - def get_context(self, request): - context = super(PeopleIndexPage, self).get_context(request) - - # PersonPage objects (get_people) are passed through pagination - people = self.paginate(request, self.get_people()) - - context["people"] = people - - return context +from django.db import models +from wagtail.admin.panels import FieldPanel, MultiFieldPanel +from wagtail.fields import RichTextField +from wagtail.models import Page +from wagtail.search import index + + +class PersonIndexPage(Page): + intro = RichTextField(blank=True) + + content_panels = Page.content_panels + [ + FieldPanel("intro"), + ] + + subpage_types = ["PersonPage"] + + class Meta: + verbose_name = "Person Index Page" + + def get_context(self, request): + context = super().get_context(request) + context["people"] = ( + PersonPage.objects.child_of(self).live().order_by("last_name", "first_name") + ) + return context + + +class PersonPage(Page): + first_name = models.CharField(max_length=100, default="") + last_name = models.CharField(max_length=100, default="") + role = models.CharField(max_length=200, default="") + department = models.CharField(max_length=200, blank=True) + bio = RichTextField(blank=True) + photo = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + email = models.EmailField(blank=True) + linkedin_url = models.URLField(blank=True) + + parent_page_types = ["PersonIndexPage"] + subpage_types = [] + + search_fields = Page.search_fields + [ + index.SearchField("first_name"), + index.SearchField("last_name"), + index.SearchField("role"), + index.SearchField("bio"), + ] + + content_panels = Page.content_panels + [ + MultiFieldPanel( + [ + FieldPanel("first_name"), + FieldPanel("last_name"), + ], + heading="Name", + ), + MultiFieldPanel( + [ + FieldPanel("role"), + FieldPanel("department"), + ], + heading="Role", + ), + MultiFieldPanel( + [ + FieldPanel("photo"), + ], + heading="Photo", + ), + MultiFieldPanel( + [ + FieldPanel("bio"), + ], + heading="Bio", + ), + MultiFieldPanel( + [ + FieldPanel("email"), + FieldPanel("linkedin_url"), + ], + heading="Contact", + ), + ] + + class Meta: + verbose_name = "Person Page" + diff --git a/bakerydemo/people/tests/__init__.py b/bakerydemo/people/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/bakerydemo/people/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/bakerydemo/people/tests/test_people_page.py b/bakerydemo/people/tests/test_people_page.py new file mode 100644 index 000000000..f76d58934 --- /dev/null +++ b/bakerydemo/people/tests/test_people_page.py @@ -0,0 +1,100 @@ +from django.contrib.auth.models import User +from wagtail.models import Page, Site +from wagtail.test.utils import WagtailPageTestCase + +from bakerydemo.people.models import PersonIndexPage, PersonPage + + +class PersonIndexPageRenderTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index_page = PersonIndexPage( + title="People", + slug="people", + intro="
Meet our team
", + ) + cls.root.add_child(instance=cls.index_page) + cls.index_page.save_revision().publish() + + cls.person_page = PersonPage( + title="Jane Doe", + slug="jane-doe", + first_name="Jane", + last_name="Doe", + role="Head Baker", + department="Kitchen", + bio="Experienced artisan baker.
", + ) + cls.index_page.add_child(instance=cls.person_page) + cls.person_page.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin", email="test@example.com", password="password" + ) + self.client.login(username="testadmin", password="password") + + def test_person_index_page_renders(self): + response = self.client.get(self.index_page.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "people/person_index_page.html") + self.assertContains(response, "Meet our team") + + +class PersonPageRenderTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index_page = PersonIndexPage( + title="People Detail", + slug="people-detail", + ) + cls.root.add_child(instance=cls.index_page) + cls.index_page.save_revision().publish() + + cls.person_page = PersonPage( + title="John Smith", + slug="john-smith", + first_name="John", + last_name="Smith", + role="Pastry Chef", + department="Pastry", + bio="Creates delicious pastries.
", + ) + cls.index_page.add_child(instance=cls.person_page) + cls.person_page.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin2", email="test2@example.com", password="password" + ) + self.client.login(username="testadmin2", password="password") + + def test_person_page_renders(self): + response = self.client.get(self.person_page.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "people/person_page.html") + self.assertContains(response, "John Smith") + self.assertContains(response, "Pastry Chef") + diff --git a/bakerydemo/static/css/main.css b/bakerydemo/static/css/main.css index 0f5fece64..945224145 100644 --- a/bakerydemo/static/css/main.css +++ b/bakerydemo/static/css/main.css @@ -41,6 +41,22 @@ each detail view /* stylelint-enable */ --font-sm: 1rem; --font-md: 1.125rem; + /* Theme tokens */ + --color-bg: var(--white); + --color-text: var(--dark); + --color-border: var(--border-grey); + --color-surface: var(--cream); + /* Layout */ + --max-width: 1400px; + --grid-gutter: 20px; +} + +/* Dark theme overrides (applied when JS sets data-theme="dark" on ) */ +html[data-theme='dark'] { + --color-bg: #121212; + --color-text: #e0e0e0; + --color-border: #333333; + --color-surface: #1e1e1e; } html { @@ -59,8 +75,8 @@ body { line-height: 1.5; padding-top: 0; padding-bottom: 0; - background: var(--white); - color: var(--dark); + background: var(--color-bg); + color: var(--color-text); min-height: 100vh; } @@ -449,7 +465,7 @@ caption { .header { padding: 0; width: 100%; - background: var(--white); + background: var(--color-surface); z-index: 10; } @@ -603,14 +619,9 @@ caption { .navigation__search-icon { display: block; position: absolute; - right: 13px; - top: 13px; -} - -@media (min-width: 768px) { - .navigation__search-icon { - top: 11px; - } + left: 13px; + top: 50%; + transform: translateY(-50%); } .navigation__search-input { @@ -631,14 +642,42 @@ caption { display: none; } +/* Theme toggle button */ +.theme-toggle { + margin-left: 20px; + border-radius: 999px; + border: 1px solid var(--border-grey); + background-color: transparent; + color: var(--dark); + font-size: 0.875rem; + padding: 4px 12px; + cursor: pointer; +} + +.theme-toggle:focus-visible { + outline: 2px solid var(--orange); + outline-offset: 2px; +} + +html[data-theme='dark'] .theme-toggle { + color: var(--color-text, #e0e0e0); + border-color: var(--color-border, #333333); +} + @media (min-width: 1150px) { .navigation__desktop { - display: block; + display: flex; + flex-direction: row; + align-items: center; + margin-left: auto; + flex: 1; + min-width: 0; } .navigation__search { display: block; - margin: 0 0 0 auto; + margin-left: auto; + flex-shrink: 0; } .navigation__items { @@ -776,7 +815,7 @@ hr { footer { padding: 55px 0 30px; - background-color: var(--white); + background-color: var(--color-surface); } .footer__icon a { @@ -802,16 +841,35 @@ footer { } .container { - width: auto; - padding-left: 20px; - padding-right: 20px; + width: 100%; + max-width: var(--max-width); + margin-inline: auto; + padding-inline: var(--grid-gutter); } @media (min-width: 768px) { - .container { - max-width: 1400px; - padding-left: 40px; - padding-right: 40px; + :root { + --grid-gutter: 40px; + } +} + +.grid { + display: grid; + gap: var(--grid-gutter); + grid-template-columns: 1fr; +} + +/* Tablet */ +@media (min-width: 768px) { + .grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Desktop */ +@media (min-width: 1200px) { + .grid { + grid-template-columns: repeat(3, 1fr); } } @@ -2099,3 +2157,43 @@ input[type='radio'] { max-height: 100vh; margin-inline: auto; } + + +/* Search form: relative so absolute clear button doesn't affect flex layout */ +.navigation__search form, +.navigation__mobile-search form { + position: relative; + display: flex; + align-items: center; +} + +/* Clear button: right side of input, vertically centered; no layout shift (absolute) */ +.search-clear-btn { + position: absolute; + right: 40px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + font-size: 1.5rem; + color: var(--grey); + cursor: pointer; + padding: 0; + line-height: 1; + z-index: 5; + transition: color 0.2s ease; +} + +.search-clear-btn:hover { + color: var(--orange); +} + +.is-hidden { + display: none !important; +} + +/* Input padding so text doesn't overlap clear button and search icon */ +.navigation__search-input { + padding-left: 50px !important; + padding-right: 65px !important; +} \ No newline at end of file diff --git a/bakerydemo/static/js/main.js b/bakerydemo/static/js/main.js index 7492deb76..cd5c83319 100644 --- a/bakerydemo/static/js/main.js +++ b/bakerydemo/static/js/main.js @@ -1,24 +1,113 @@ -document.addEventListener('DOMContentLoaded', () => { - const navigation = document.querySelector('[data-navigation]'); - const mobileNavigation = navigation.querySelector('[data-mobile-navigation]'); - const body = document.querySelector('body'); - const mobileNavigationToggle = navigation.querySelector( - '[data-mobile-navigation-toggle]', - ); - - function toggleMobileNavigation() { - if (mobileNavigation.hidden) { - body.classList.add('no-scroll'); - mobileNavigation.hidden = false; - mobileNavigationToggle.setAttribute('aria-expanded', 'true'); - } else { - body.classList.remove('no-scroll'); - mobileNavigation.hidden = true; - mobileNavigationToggle.setAttribute('aria-expanded', 'false'); + +// document.addEventListener('DOMContentLoaded', function() { +// const searchInput = document.querySelector('.navigation__search-input'); +// const clearBtn = document.getElementById('search-clear'); + +// if (searchInput && clearBtn) { +// // Show/Hide button based on input value +// searchInput.addEventListener('input', () => { +// if (searchInput.value.length > 0) { +// clearBtn.classList.remove('is-hidden'); +// } else { +// clearBtn.classList.add('is-hidden'); +// } +// }); + +// // Clear input when clicked +// clearBtn.addEventListener('click', () => { +// searchInput.value = ''; +// clearBtn.classList.add('is-hidden'); +// searchInput.focus(); +// }); +// } +// }); + + +// document.addEventListener('DOMContentLoaded', () => { +// const navigation = document.querySelector('[data-navigation]'); +// const mobileNavigation = navigation.querySelector('[data-mobile-navigation]'); +// const body = document.querySelector('body'); +// const mobileNavigationToggle = navigation.querySelector( +// '[data-mobile-navigation-toggle]', +// ); + + +// function toggleMobileNavigation() { +// if (mobileNavigation.hidden) { +// body.classList.add('no-scroll'); +// mobileNavigation.hidden = false; +// mobileNavigationToggle.setAttribute('aria-expanded', 'true'); +// } else { +// body.classList.remove('no-scroll'); +// mobileNavigation.hidden = true; +// mobileNavigationToggle.setAttribute('aria-expanded', 'false'); +// } +// } + +// mobileNavigationToggle.addEventListener('click', () => { +// toggleMobileNavigation(); +// }); +// }); +document.addEventListener('DOMContentLoaded', function() { + // Select all potential search inputs (mobile and desktop) + const searchInputs = document.querySelectorAll('.navigation__search-input'); + // Select the reset buttons + const clearBtnDesktop = document.getElementById('search-clear'); + const clearBtnMobile = document.getElementById('search-clear-mobile'); + + searchInputs.forEach(input => { + input.addEventListener('input', () => { + const currentBtn = input.id === 'mobile-search-input' ? clearBtnMobile : clearBtnDesktop; + + if (currentBtn) { + // Show button if text exists, hide if empty + if (input.value.length > 0) { + currentBtn.classList.remove('is-hidden'); + } else { + currentBtn.classList.add('is-hidden'); + } + } + }); + }); + + // Clear button click: clear input, hide button, refocus input + if (clearBtnDesktop) { + clearBtnDesktop.addEventListener('click', () => { + const desktopInput = document.querySelector('.navigation__search .navigation__search-input'); + if (desktopInput) { + desktopInput.value = ''; + clearBtnDesktop.classList.add('is-hidden'); + desktopInput.focus(); + } + }); + } + if (clearBtnMobile) { + clearBtnMobile.addEventListener('click', () => { + const mobileInput = document.getElementById('mobile-search-input'); + if (mobileInput) { + mobileInput.value = ''; + clearBtnMobile.classList.add('is-hidden'); + mobileInput.focus(); + } + }); + } + + // Theme: apply stored preference on load + const storedTheme = localStorage.getItem('theme'); + if (storedTheme === 'dark' || storedTheme === 'light') { + document.documentElement.setAttribute('data-theme', storedTheme); } - } - mobileNavigationToggle.addEventListener('click', () => { - toggleMobileNavigation(); - }); + // Minimal vanilla JS theme toggle (supports multiple buttons if needed) + const themeToggles = document.querySelectorAll('[data-theme-toggle]'); + + themeToggles.forEach((toggle) => { + toggle.addEventListener('click', () => { + const current = document.documentElement.getAttribute('data-theme') || 'light'; + const next = current === 'dark' ? 'light' : 'dark'; + + document.documentElement.setAttribute('data-theme', next); + localStorage.setItem('theme', next); + }); + }); }); diff --git a/bakerydemo/templates/includes/header.html b/bakerydemo/templates/includes/header.html index 8c547bcf2..a36e00e75 100644 --- a/bakerydemo/templates/includes/header.html +++ b/bakerydemo/templates/includes/header.html @@ -1,4 +1,4 @@ -{% load navigation_tags %} +{% load navigation_tags wagtailcore_tags %}+ {{ person.role }}{% if person.department %} — {{ person.department }}{% endif %} +
+No people have been added yet.
+ {% endfor %} +- {{ page.introduction }} -
- {% endif %} ++ {{ page.role }}{% if page.department %} — {{ page.department }}{% endif %} +
+ + {% if page.bio %} +Full details here.
", + source="Bakery News", + ) + self.index.add_child(instance=self.release) + self.release.save_revision().publish() + + def test_press_release_page_status_code(self): + response = self.client.get(self.release.url) + self.assertEqual(response.status_code, 200) + + def test_press_release_page_uses_correct_template(self): + response = self.client.get(self.release.url) + self.assertTemplateUsed( + response, + "press_releases/press_release_page.html" + ) + + def test_press_release_shows_title(self): + response = self.client.get(self.release.url) + self.assertContains(response, "Bakery Wins Award") +# Create your tests here. diff --git a/bakerydemo/press_releases/views.py b/bakerydemo/press_releases/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/bakerydemo/press_releases/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/bakerydemo/settings/base.py b/bakerydemo/settings/base.py index 18173618b..c67e252c9 100644 --- a/bakerydemo/settings/base.py +++ b/bakerydemo/settings/base.py @@ -78,6 +78,7 @@ "django.contrib.staticfiles", "django.contrib.sitemaps", "bakerydemo.people", + "bakerydemo.press_releases", ] MIDDLEWARE = [ diff --git a/bakerydemo/templates/press_releases/press_release_index_page.html b/bakerydemo/templates/press_releases/press_release_index_page.html new file mode 100644 index 000000000..1061441be --- /dev/null +++ b/bakerydemo/templates/press_releases/press_release_index_page.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load wagtailcore_tags %} + +{% block content %} +{{ release.intro }}
+ {% if release.source %} + {{ release.source }} + {% endif %} +{fake.paragraph(nb_sentences=5)}
", + source=fake.random_element(SOURCES), + contact_email=fake.company_email(), + slug=fake.unique.slug(), + ) + index.add_child(instance=release) + release.save_revision().publish() + self.stdout.write(f"Created: {release.title}") + + self.stdout.write(self.style.SUCCESS( + f'Successfully created {count} press release pages' + )) \ No newline at end of file From 133d11806418aa1c22c399722704c8748067e2fc Mon Sep 17 00:00:00 2001 From: Pra26nav <24sharmap_2@rbunagpur.in> Date: Tue, 10 Mar 2026 18:39:13 +0530 Subject: [PATCH 6/6] Add tests for press_releases app --- bakerydemo/press_releases/tests.py | 67 ++++++++++---- .../tests/test_press_release_page.py | 87 +++++++++++++++++++ 2 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 bakerydemo/press_releases/tests/test_press_release_page.py diff --git a/bakerydemo/press_releases/tests.py b/bakerydemo/press_releases/tests.py index 0411ac075..fbdf431e1 100644 --- a/bakerydemo/press_releases/tests.py +++ b/bakerydemo/press_releases/tests.py @@ -1,18 +1,36 @@ +from django.contrib.auth.models import User +from wagtail.models import Page, Site from wagtail.test.utils import WagtailPageTestCase -from wagtail.models import Page from bakerydemo.press_releases.models import PressReleaseIndexPage, PressReleasePage import datetime class PressReleaseIndexPageTest(WagtailPageTestCase): - def setUp(self): - root = Page.objects.first() - self.index = PressReleaseIndexPage( + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index = PressReleaseIndexPage( title="Press Releases", - slug="press-releases", + slug="press-releases-test", + ) + cls.root.add_child(instance=cls.index) + cls.index.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin", email="test@example.com", password="password" ) - root.add_child(instance=self.index) - self.index.save_revision().publish() + self.client.login(username="testadmin", password="password") def test_index_page_status_code(self): response = self.client.get(self.index.url) @@ -27,16 +45,26 @@ def test_index_page_uses_correct_template(self): class PressReleasePageTest(WagtailPageTestCase): - def setUp(self): - root = Page.objects.first() - self.index = PressReleaseIndexPage( + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index = PressReleaseIndexPage( title="Press Releases", - slug="press-releases", + slug="press-releases-page-test", ) - root.add_child(instance=self.index) - self.index.save_revision().publish() + cls.root.add_child(instance=cls.index) + cls.index.save_revision().publish() - self.release = PressReleasePage( + cls.release = PressReleasePage( title="Bakery Wins Award", slug="bakery-wins-award", date=datetime.date.today(), @@ -44,8 +72,15 @@ def setUp(self): body="Full details here.
", source="Bakery News", ) - self.index.add_child(instance=self.release) - self.release.save_revision().publish() + cls.index.add_child(instance=cls.release) + cls.release.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin2", email="test2@example.com", password="password" + ) + self.client.login(username="testadmin2", password="password") def test_press_release_page_status_code(self): response = self.client.get(self.release.url) diff --git a/bakerydemo/press_releases/tests/test_press_release_page.py b/bakerydemo/press_releases/tests/test_press_release_page.py new file mode 100644 index 000000000..72f0deda8 --- /dev/null +++ b/bakerydemo/press_releases/tests/test_press_release_page.py @@ -0,0 +1,87 @@ +from django.contrib.auth.models import User +from wagtail.models import Page, Site +from wagtail.test.utils import WagtailPageTestCase + +from bakerydemo.press_releases.models import PressReleaseIndexPage, PressReleasePage + + +class PressReleaseIndexPageRenderTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index_page = PressReleaseIndexPage( + title="Press Releases", + slug="press-releases-test", + intro="Latest news
", + ) + cls.root.add_child(instance=cls.index_page) + cls.index_page.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin", email="test@example.com", password="password" + ) + self.client.login(username="testadmin", password="password") + + def test_press_release_index_page_renders(self): + response = self.client.get(self.index_page.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "press_releases/press_release_index_page.html") + self.assertContains(response, "Latest news") + + +class PressReleasePageRenderTest(WagtailPageTestCase): + @classmethod + def setUpTestData(cls): + cls.root = Page.get_first_root_node() + + cls.site, _ = Site.objects.update_or_create( + hostname="testserver", + defaults={ + "root_page": cls.root, + "is_default_site": True, + }, + ) + + cls.index_page = PressReleaseIndexPage( + title="Press Releases Detail", + slug="press-releases-detail-test", + ) + cls.root.add_child(instance=cls.index_page) + cls.index_page.save_revision().publish() + + cls.press_release_page = PressReleasePage( + title="Bakery Opens New Location", + slug="bakery-opens-new-location", + date="2026-01-15", + intro="The bakery is expanding.", + body="We are excited to announce a new location.
", + source="PR Newswire", + contact_email="press@example.com", + ) + cls.index_page.add_child(instance=cls.press_release_page) + cls.press_release_page.save_revision().publish() + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser( + username="testadmin2", email="test2@example.com", password="password" + ) + self.client.login(username="testadmin2", password="password") + + def test_press_release_page_renders(self): + response = self.client.get(self.press_release_page.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "press_releases/press_release_page.html") + self.assertContains(response, "Bakery Opens New Location") + self.assertContains(response, "PR Newswire")