diff --git a/network-api/networkapi/nav/blocks.py b/network-api/networkapi/nav/blocks.py index e89b216dc90..d5cc0573a69 100644 --- a/network-api/networkapi/nav/blocks.py +++ b/network-api/networkapi/nav/blocks.py @@ -1,3 +1,5 @@ +from collections import OrderedDict + from wagtail import blocks from wagtail.telepath import register @@ -8,7 +10,7 @@ ) -class NavLinkValue(BaseLinkValue): +class NavItemValue(BaseLinkValue): @property def open_in_new_window(self) -> bool: link_to = self.get("link_to") @@ -17,14 +19,50 @@ def open_in_new_window(self) -> bool: return False -class NavLinkBlock(BaseLinkBlock): +class NavItem(BaseLinkBlock): description = blocks.CharBlock(required=False, max_length=100) + def __init__(self, local_blocks=None, **kwargs): + # Use __init__ method to change the order of the blocks when constructing + # them through inheritance + super().__init__(local_blocks, **kwargs) + self.child_blocks = self.base_blocks.copy() + child_blocks = OrderedDict( + { + "label": self.child_blocks.pop("label"), + "description": self.child_blocks.pop("description"), + } + ) + child_blocks.update({k: v for k, v in self.child_blocks.items()}) + self.child_blocks = child_blocks + class Meta: - value_class = NavLinkValue + value_class = NavItemValue label = "Navigation Link" icon = "link" template = "nav/blocks/nav_link_block.html" -register(BaseLinkBlockAdapter(), NavLinkBlock) +register(BaseLinkBlockAdapter(), NavItem) + + +class NavButton(BaseLinkBlock): + class Meta: + value_class = NavItemValue + label = "Navigation Button" + icon = "link" + template = "nav/blocks/nav_button_block.html" + + +register(BaseLinkBlockAdapter(), NavButton) + + +class NavColumn(blocks.StructBlock): + title = blocks.CharBlock(max_length=100) + links = blocks.ListBlock(NavItem, min_num=1, max_num=4) + button = blocks.ListBlock(NavButton, required=False, min_num=0, max_num=1) + + class Meta: + label = "Navigation Column" + icon = "list-ul" + template = "nav/blocks/nav_column_block.html" diff --git a/network-api/networkapi/nav/factories.py b/network-api/networkapi/nav/factories.py index 5be975fa689..935bf6f84c8 100644 --- a/network-api/networkapi/nav/factories.py +++ b/network-api/networkapi/nav/factories.py @@ -2,10 +2,10 @@ import wagtail_factories from wagtail import models as wagtail_models -from networkapi.nav.blocks import NavLinkBlock, NavLinkValue +from networkapi.nav import blocks as nav_blocks -class NavLinkBlockFactory(wagtail_factories.StructBlockFactory): +class NavItemFactory(wagtail_factories.StructBlockFactory): """Factory for NavLinkBlock. Use traits to create instances based on the type of link needed: @@ -22,12 +22,12 @@ class NavLinkBlockFactory(wagtail_factories.StructBlockFactory): """ class Meta: - model = NavLinkBlock + model = nav_blocks.NavItem @classmethod def _construct_struct_value(cls, block_class, params): """Use NavLinkValue to create the StructValue instance.""" - return NavLinkValue( + return nav_blocks.NavItemValue( block_class(), [(name, value) for name, value in params.items()], ) @@ -51,3 +51,62 @@ class Params: page = None external_url = "" relative_url = "" + + +class NavButtonFactory(wagtail_factories.StructBlockFactory): + """Factory for NavButtonBlock. + + Use traits to create instances based on the type of link needed: + - page_link: link to a page + - external_url_link: link to a custom external URL + - relative_url_link: link to a relative URL + + Example: + ``` + block = NavButtonBlockFactory(page_link=True) + block = NavButtonBlockFactory(external_url_link=True) + block = NavButtonBlockFactory(relative_url_link=True) + ``` + """ + + class Meta: + model = nav_blocks.NavButton + + @classmethod + def _construct_struct_value(cls, block_class, params): + """Use NavLinkValue to create the StructValue instance.""" + return nav_blocks.NavItemValue( + block_class(), + [(name, value) for name, value in params.items()], + ) + + class Params: + page_link = factory.Trait( + link_to="page", + page=factory.Iterator(wagtail_models.Page.objects.filter(locale_id="1")), + ) + external_url_link = factory.Trait(link_to="external_url", external_url=factory.Faker("url")) + relative_url_link = factory.Trait(link_to="relative_url", relative_url=f'/{factory.Faker("uri_path")}') + + label = factory.Faker("sentence", nb_words=3) + + # Setup default link as external URL (it won't pass validation without a link type defined though + # so it's still necessary to use the factory with traits) + link_to = "external_url" + # Set all link types to None by default. Only define the needed link type in the factory + # trait to avoid conflicts + page = None + external_url = "" + relative_url = "" + + +class NavColumnFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = nav_blocks.NavColumn + + class Params: + with_button = False + + title = factory.Faker("sentence", nb_words=3) + links = factory.List([factory.SubFactory(NavItemFactory) for _ in range(4)]) + button = factory.LazyAttribute(lambda o: [NavButtonFactory()] if o.with_button else []) diff --git a/network-api/networkapi/nav/tests/test_blocks.py b/network-api/networkapi/nav/tests/test_blocks.py index b1a04c798a7..92ab08aa114 100644 --- a/network-api/networkapi/nav/tests/test_blocks.py +++ b/network-api/networkapi/nav/tests/test_blocks.py @@ -1,23 +1,23 @@ from django.test import TestCase -from wagtail.blocks import StreamBlockValidationError +from wagtail.blocks import StreamBlockValidationError, StructBlockValidationError from wagtail.models import Locale, Page -from networkapi.nav.blocks import NavLinkBlock -from networkapi.nav.factories import NavLinkBlockFactory +from networkapi.nav import blocks as nav_blocks +from networkapi.nav import factories as nav_factories -class TestLinkBlock(TestCase): +class TestNavItemBlock(TestCase): def test_default(self): - """Assert that default NavLinkBlock factory works and is an external URL.""" - block = NavLinkBlockFactory() + """Assert that default nav_blocks.NavItem factory works and is an external URL.""" + block = nav_factories.NavItemFactory() # Assert that the page link is custom URL and that it is correct url = block["external_url"] self.assertEqual(block.url, url) def test_page_link(self): - """Create a NavLinkBlock with a page link.""" - block = NavLinkBlockFactory(page_link=True) + """Create a nav_blocks.NavItem with a page link.""" + block = nav_factories.NavItemFactory(page_link=True) page = block["page"] self.assertIsNotNone(page) @@ -32,8 +32,8 @@ def test_page_link(self): self.assertEqual(block["relative_url"], "") def test_external_url_link(self): - """Create a NavLinkBlock with a custom/external URL.""" - block = NavLinkBlockFactory(external_url_link=True) + """Create a nav_blocks.NavItem with a custom/external URL.""" + block = nav_factories.NavItemFactory(external_url_link=True) # Assert that the URL is a URL url = block["external_url"] @@ -46,8 +46,8 @@ def test_external_url_link(self): self.assertEqual(block["relative_url"], "") def test_relative_url_link(self): - """Create a NavLinkBlock with a relative URL.""" - block = NavLinkBlockFactory(relative_url_link=True) + """Create a nav_blocks.NavItem with a relative URL.""" + block = nav_factories.NavItemFactory(relative_url_link=True) # Assert that the URL is a URL url = block["relative_url"] @@ -61,5 +61,92 @@ def test_relative_url_link(self): def test_needs_to_provide_at_least_one_link(self): with self.assertRaises(StreamBlockValidationError): - block = NavLinkBlockFactory() - NavLinkBlock().clean(block) + block = nav_factories.NavItemFactory() + nav_blocks.NavItem().clean(block) + + +class TestNavButton(TestCase): + def test_default(self): + """Assert that default nav_blocks.NavButton factory works and is an external URL.""" + block = nav_factories.NavButtonFactory() + + # Assert that the page link is custom URL and that it is correct + url = block["external_url"] + self.assertEqual(block.url, url) + + def test_page_link(self): + """Create a nav_blocks.NavButton with a page link.""" + block = nav_factories.NavButtonFactory(page_link=True) + + page = block["page"] + self.assertIsNotNone(page) + self.assertTrue(isinstance(page, Page)) + default_locale = Locale.get_default() + self.assertEqual(page.locale, default_locale) + + # Assert that other fields are empty + self.assertEqual(block["external_url"], "") + self.assertEqual(block["relative_url"], "") + + def test_external_url_link(self): + """Create a nav_blocks.NavButton with a custom/external URL.""" + block = nav_factories.NavButtonFactory(external_url_link=True) + + # Assert that the URL is a URL + url = block["external_url"] + self.assertIsNotNone(url) + + # Assert that other fields are empty + self.assertIsNone(block["page"]) + self.assertEqual(block["relative_url"], "") + + def test_relative_url_link(self): + """Create a nav_blocks.NavButton with a relative URL.""" + block = nav_factories.NavButtonFactory(relative_url_link=True) + + # Assert that the URL is a URL + url = block["relative_url"] + self.assertIsNotNone(url) + + # Assert that other fields are empty + self.assertIsNone(block["page"]) + self.assertEqual(block["external_url"], "") + + def test_needs_to_provide_at_least_one_link(self): + with self.assertRaises(StreamBlockValidationError): + block = nav_factories.NavItemFactory() + nav_blocks.NavItem().clean(block) + + +class TestNavColumnBlock(TestCase): + def test_default(self): + """Assert that default nav_blocks.NavColumn factory works and is an external URL.""" + block = nav_factories.NavColumnFactory() + + self.assertEqual(len(block["links"]), 4) + for link in block["links"]: + self.assertIsInstance(link.block, nav_blocks.NavItem) + self.assertIsInstance(link, nav_blocks.NavItemValue) + self.assertCountEqual(block["button"], []) + + def test_with_button(self): + """Create a nav_blocks.NavColumn with a button.""" + block = nav_factories.NavColumnFactory(with_button=True) + + self.assertEqual(len(block["button"]), 1) + self.assertIsInstance(block["button"][0].block, nav_blocks.NavButton) + + def test_needs_to_provide_at_least_one_link(self): + with self.assertRaises(StructBlockValidationError): + block = nav_factories.NavColumnFactory(links=[]) + nav_blocks.NavColumn().clean(block) + + def test_needs_to_provide_at_most_four_links(self): + with self.assertRaises(StructBlockValidationError): + block = nav_factories.NavColumnFactory(links=[nav_factories.NavItemFactory() for _ in range(5)]) + nav_blocks.NavColumn().clean(block) + + def test_cannot_have_more_than_one_button(self): + with self.assertRaises(StructBlockValidationError): + block = nav_factories.NavColumnFactory(button=[nav_factories.NavButtonFactory() for _ in range(2)]) + nav_blocks.NavColumn().clean(block)