Skip to content

Commit

Permalink
Main Menu (#41)
Browse files Browse the repository at this point in the history
* Front-end hacked together to complete the back-end

* Navigation app added, models created for navigation settings

* Navigation bar on the front-end test

* Front-end code reverted due to the front-end main menu being done separately

* New line

* Code formatted

* Type hinted and code formatted

* Code refactored, abstract link class created, validation messages added, section restricted to 3, URL label added, icon for navigation settings added and restricting it to so only 1 main menu can be created

* Migration file

* Main menu modelling complete, restricting user to be able to only create one main menu instance in the wagtail admin. Migration file added.

* LinkBlock extracted out of core.blocks.related and moved to core.blocks.base to be reusable across the project. LinkBlock from core.blocks.base is being used instead of the abstract class originally defined in models.py.

* Code formatted, zombie code removed, unused imports removed and MainMenuViewSet added to restrict creating more than one main menu instance

* Front-end integrated

* Type hints added

* Nav template issues fixed

* gettext_lazy added

* Preview added

* Use the title provided by the user over the default title from the page

* help text update

* Type hints added

* Migration file regenerated

* Tests added

* TODO removed and translatable added

* Description help text updated

* Tests setup

* Example StreamField value setting in test_highlights_streamfield_limit

* Debug

* Errors being raised when adding more than 3 highlights

* Committing working factories to save

* Clean method for ColumnBlock added and tests updated

* Updated tests

* test_section_streamfield_limit test working

* Added more tests, factories updated

* Removed tests

* Max num code removed

* Code formatted

* Utilising pageurl to point to get internal wagtail pages

* Code refactored

* Reverting format for md files

* Reverting md files formatting

* Lint errors corrected

* Unused import removed and base template updated

* Base template update

* Type checking added back in

* Template code updated to make sure to only display live wagtail pages

* Main menu live preview fixed

* MegaLinter errors fixed

* Using a preview template for live previews

* Mark NavigationSettings as allowed for write in ExternalEnvRouter

* Use our own BaseSiteSetting which works with the ExternalEnvRouter

* RevisionMixin implementation and mypy fix

* Blocks refactor

* Code linted

* Tests for main menu viewset added

* Type ignore added

* Duplicate page and external URL validation for sections and topic links within columns complete.

* Duplicate page and URL for columns complete.

* test_forms in place

* Update test_clean_highlights_no_duplicate test

* Tests complete for forms.py

* test_forms.py code clean up

* Example usage zombie code removed

* Code formatted and refactored

* Code refactored, type hinted, tests refactored.

* gettext translation added

* LinkBlock Factory moved

* De-nest the blocks tests folder
  • Loading branch information
sanjeevz3009 authored Jan 29, 2025
1 parent 49f95e6 commit e921656
Show file tree
Hide file tree
Showing 18 changed files with 1,491 additions and 272 deletions.
82 changes: 82 additions & 0 deletions cms/core/blocks/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from wagtail.blocks import (
CharBlock,
PageChooserBlock,
StreamBlockValidationError,
StructBlock,
StructValue,
URLBlock,
)


class LinkBlockStructValue(StructValue):
"""Custom StructValue for link blocks."""

@cached_property
def link(self) -> dict | None:
"""A convenience property that returns the block value in a consistent way,
regardless of the chosen values (be it a Wagtail page or external link).
"""
value = None
title = self.get("title")
desc = self.get("description")
has_description = "description" in self

if external_url := self.get("external_url"):
value = {"url": external_url, "text": title}
if has_description:
value["description"] = desc

if (page := self.get("page")) and page.live:
value = {"url": page.url, "text": title or page.title}
if has_description:
value["description"] = desc or getattr(page.specific_deferred, "summary", "")

return value


class LinkBlock(StructBlock):
"""Link block with page or link validation."""

page = PageChooserBlock(required=False)
external_url = URLBlock(required=False, label="or External Link")
title = CharBlock(
help_text="Populate when adding an external link. "
"When choosing a page, you can leave it blank to use the page's own title",
required=False,
)

class Meta:
icon = "link"
value_class = LinkBlockStructValue

def clean(self, value: LinkBlockStructValue) -> LinkBlockStructValue:
"""Validate that either a page or external link is provided, and that external links have a title."""
value = super().clean(value)
page = value["page"]
external_url = value["external_url"]
errors = {}
non_block_errors = ErrorList()

# Require exactly one link
if not page and not external_url:
error = ValidationError(_("Either Page or External Link is required."), code="invalid")
errors["page"] = ErrorList([error])
errors["external_url"] = ErrorList([error])
non_block_errors.append(ValidationError(_("Missing required fields")))
elif page and external_url:
error = ValidationError(_("Please select either a page or a URL, not both."), code="invalid")
errors["page"] = ErrorList([error])
errors["external_url"] = ErrorList([error])

# Require title for external links
if not page and external_url and not value["title"]:
errors["title"] = ErrorList([ValidationError(_("Title is required for external links."), code="invalid")])

if errors:
raise StreamBlockValidationError(block_errors=errors, non_block_errors=non_block_errors)

return value
85 changes: 3 additions & 82 deletions cms/core/blocks/related.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,15 @@
from typing import TYPE_CHECKING, Any

from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
from django.utils.functional import cached_property
from django.utils.text import slugify
from django.utils.translation import gettext as _
from wagtail.blocks import (
CharBlock,
ListBlock,
PageChooserBlock,
StreamBlockValidationError,
StructBlock,
StructValue,
URLBlock,
)
from wagtail.blocks import CharBlock, ListBlock

from .base import LinkBlock

if TYPE_CHECKING:
from wagtail.blocks.list_block import ListValue


class LinkBlockStructValue(StructValue):
"""Custom StructValue for link blocks."""

@cached_property
def link(self) -> dict | None:
"""A convenience property that returns the block value in a consistent way,
regardless of the chosen values (be it a Wagtail page or external link).
"""
value = None
title = self.get("title")
desc = self.get("description")
has_description = "description" in self

if external_url := self.get("external_url"):
value = {"url": external_url, "text": title}
if has_description:
value["description"] = desc

if (page := self.get("page")) and page.live:
value = {"url": page.url, "text": title or page.title}
if has_description:
value["description"] = desc or getattr(page.specific_deferred, "summary", "")

return value


class LinkBlock(StructBlock):
"""Related link block with page or link validation."""

page = PageChooserBlock(required=False)
external_url = URLBlock(required=False, label="or External Link")
title = CharBlock(
help_text="Populate when adding an external link. "
"When choosing a page, you can leave it blank to use the page's own title",
required=False,
)

class Meta:
icon = "link"
value_class = LinkBlockStructValue

def clean(self, value: LinkBlockStructValue) -> LinkBlockStructValue:
"""Validate that either a page or external link is provided, and that external links have a title."""
value = super().clean(value)
page = value["page"]
external_url = value["external_url"]
errors = {}
non_block_errors = ErrorList()

# Require exactly one link
if not page and not external_url:
error = ValidationError(_("Either Page or External Link is required."), code="invalid")
errors["page"] = ErrorList([error])
errors["external_url"] = ErrorList([error])
non_block_errors.append(ValidationError(_("Missing required fields")))
elif page and external_url:
error = ValidationError(_("Please select either a page or a URL, not both."), code="invalid")
errors["page"] = ErrorList([error])
errors["external_url"] = ErrorList([error])

# Require title for external links
if not page and external_url and not value["title"]:
errors["title"] = ErrorList([ValidationError(_("Title is required for external links."), code="invalid")])

if errors:
raise StreamBlockValidationError(block_errors=errors, non_block_errors=non_block_errors)

return value


class RelatedContentBlock(LinkBlock):
"""Related content block with page or link validation."""

Expand Down
17 changes: 16 additions & 1 deletion cms/core/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import wagtail_factories
from wagtail import blocks
from wagtail.rich_text import RichText
from wagtail_factories.blocks import BlockFactory
from wagtail_factories.blocks import BlockFactory, PageChooserBlockFactory, StructBlockFactory

from cms.core.blocks.base import LinkBlock
from cms.core.models import ContactDetails


Expand Down Expand Up @@ -72,3 +73,17 @@ class SectionBlockFactory(wagtail_factories.StructBlockFactory):

title = factory.Faker("text", max_nb_chars=50)
content = factory.SubFactory(SectionContentBlockFactory)


class LinkBlockFactory(StructBlockFactory):
"""Factory for LinkBlock."""

class Meta:
model = LinkBlock

title = factory.Faker("text", max_nb_chars=20)
page = None
external_url = factory.Faker("url")

class Params:
with_page = factory.Trait(page=factory.SubFactory(PageChooserBlockFactory), external_url=None)
Loading

0 comments on commit e921656

Please sign in to comment.