Skip to content

Commit

Permalink
Serialization for the Navbar tools
Browse files Browse the repository at this point in the history
  • Loading branch information
alexrudy committed May 6, 2024
1 parent 2809406 commit 6101c03
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 49 deletions.
31 changes: 31 additions & 0 deletions docs/api/bootlace.table.EditColumn.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
EditColumn
==========

.. currentmodule:: bootlace.table

.. autoclass:: EditColumn
:show-inheritance:

.. rubric:: Attributes Summary

.. autosummary::

~EditColumn.attribute
~EditColumn.endpoint
~EditColumn.heading

.. rubric:: Methods Summary

.. autosummary::

~EditColumn.cell

.. rubric:: Attributes Documentation

.. autoattribute:: attribute
.. autoattribute:: endpoint
.. autoattribute:: heading

.. rubric:: Methods Documentation

.. automethod:: cell
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test-all:

# Run lints
lint:
flake8
pre-commit run --all-files

# Run mypy
mypy:
Expand Down
9 changes: 6 additions & 3 deletions src/bootlace/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,18 @@
class LinkBase(abc.ABC):
text: MaybeTaggable

@abc.abstractproperty
@property
@abc.abstractmethod
def active(self) -> bool:
raise NotImplementedError("LinkBase.active must be implemented in a subclass")

@abc.abstractproperty
@property
@abc.abstractmethod
def enabled(self) -> bool:
raise NotImplementedError("LinkBase.enabled must be implemented in a subclass")

@abc.abstractproperty
@property
@abc.abstractmethod
def url(self) -> str:
raise NotImplementedError("LinkBase.url must be implemented in a subclass")

Expand Down
16 changes: 16 additions & 0 deletions src/bootlace/nav/bar.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

import attrs
from dominate import tags
from dominate.dom_tag import dom_tag
Expand Down Expand Up @@ -36,6 +38,20 @@ class NavBar(NavElement):
#: Whether the navbar should be fluid (e.g. full width)
fluid: bool = True

def serialize(self) -> dict[str, Any]:
data = super().serialize()
data["items"] = [item.serialize() for item in self.items]
data["expand"] = self.expand.value if self.expand else None
data["color"] = self.color.value if self.color else None
return data

@classmethod
def deserialize(cls, data: dict[str, Any]) -> NavElement:
data["items"] = [NavElement.deserialize(item) for item in data["items"]]
data["expand"] = SizeClass(data["expand"]) if data["expand"] else None
data["color"] = ColorClass(data["color"]) if data["color"] else None
return cls(**data)

def __tag__(self) -> tags.html_tag:
nav = tags.nav(cls="navbar")
if self.expand:
Expand Down
43 changes: 43 additions & 0 deletions src/bootlace/nav/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import enum
import warnings
from typing import Any
from typing import Self

import attrs
from dominate import tags
Expand Down Expand Up @@ -33,6 +34,26 @@ class NavAlignment(enum.Enum):
class NavElement:
"""Base class for nav components"""

_NAV_ELEMENT_REGISTRY: dict[str, type["NavElement"]] = {}

def __init_subclass__(cls) -> None:
cls._NAV_ELEMENT_REGISTRY[cls.__name__] = cls

def serialize(self) -> dict[str, Any]:
"""Serialize the element to a dictionary"""
data = attrs.asdict(self) # type: ignore
data["__type__"] = self.__class__.__name__
return data

@classmethod
def deserialize(cls, data: dict[str, Any]) -> "NavElement":
"""Deserialize an element from a dictionary"""
if cls is NavElement:
element_cls = cls._NAV_ELEMENT_REGISTRY.get(data["__type__"], NavElement)
del data["__type__"]
return element_cls.deserialize(data)
return cls(**data)

@property
def active(self) -> bool:
"""Whether the element is active"""
Expand Down Expand Up @@ -72,6 +93,18 @@ class Link(NavElement):
#: The ID of the element
id: str = attrs.field(factory=element_id.factory("nav-link"))

def serialize(self) -> dict[str, Any]:
data = super().serialize()
data["link"] = attrs.asdict(self.link)
data["link"]["__type__"] = self.link.__class__.__name__
return data

@classmethod
def deserialize(cls, data: dict[str, Any]) -> Self:
link_cls = getattr(links, data["link"].pop("__type__"))
data["link"] = link_cls(**data["link"])
return cls(**data)

@classmethod
def with_url(cls, url: str, text: str | Image, **kwargs: Any) -> "Link":
"""Create a link with a URL."""
Expand Down Expand Up @@ -135,6 +168,16 @@ class SubGroup(NavElement):

items: list[NavElement] = attrs.field(factory=list)

def serialize(self) -> dict[str, Any]:
data = super().serialize()
data["items"] = [item.serialize() for item in self.items]
return data

@classmethod
def deserialize(cls, data: dict[str, Any]) -> Self:
data["items"] = [NavElement.deserialize(item) for item in data["items"]]
return cls(**data)

@property
def active(self) -> bool:
return any(item.active for item in self.items)
14 changes: 14 additions & 0 deletions src/bootlace/nav/nav.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import warnings
from typing import Any
from typing import Self

import attrs
from dominate import tags
Expand All @@ -24,6 +26,18 @@ class Nav(SubGroup):
#: The alignment of the elments in the nav
alignment: NavAlignment = NavAlignment.DEFAULT

def serialize(self) -> dict[str, Any]:
data = super().serialize()
data["style"] = self.style.name
data["alignment"] = self.alignment.name
return data

@classmethod
def deserialize(cls, data: dict[str, Any]) -> Self:
data["style"] = NavStyle[data["style"]]
data["alignment"] = NavAlignment[data["alignment"]]
return super().deserialize(data)

def __tag__(self) -> tags.html_tag:
active_endpoint = next((item for item in self.items if item.active), None)
ul = tags.ul(cls="nav", id=self.id)
Expand Down
3 changes: 2 additions & 1 deletion src/bootlace/table/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
from .columns import CheckColumn
from .columns import Column
from .columns import Datetime
from .columns import EditColumn

__all__ = ["Table", "ColumnBase", "Heading", "Column", "CheckColumn", "Datetime"]
__all__ = ["Table", "ColumnBase", "Heading", "Column", "CheckColumn", "Datetime", "EditColumn"]
11 changes: 10 additions & 1 deletion src/bootlace/table/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,17 @@ def cell(self, value: Any) -> dom_tag:

@attrs.define
class Datetime(ColumnBase):
"""A column which shows a datetime attribute as an ISO formatted string."""
"""A column which shows a datetime attribute as an ISO formatted string.
This column can also be used for date or time objects.
A format string can be provided to format the datetime object."""

format: str | None = attrs.field(default=None)

def cell(self, value: Any) -> dom_tag:
"""Return the cell for the column as an HTML tag."""
if self.format:
return text(getattr(value, self.attribute).strftime(self.format))

return text(getattr(value, self.attribute).isoformat())
3 changes: 3 additions & 0 deletions src/bootlace/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ def __call__(self, scope: str) -> str:
def factory(self, scope: str) -> functools.partial:
return functools.partial(self, scope)

def reset(self) -> None:
self.scopes.clear()


ids = HtmlIDScope()

Expand Down
38 changes: 38 additions & 0 deletions tests/nav/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from pathlib import Path

import pytest

from bootlace.nav import bar
from bootlace.nav import elements
from bootlace.util import ids


def get_fixture(name: str) -> str:
Expand All @@ -24,3 +28,37 @@ class DisabledLink(elements.Link):
@property
def enabled(self) -> bool:
return False


@pytest.fixture
def nav() -> bar.Nav:
ids.reset()

nav = bar.NavBar(
items=[
bar.Brand.with_url(url="#", text="Navbar"),
bar.NavBarCollapse(
id="navbarSupportedContent",
items=[
bar.NavBarNav(
items=[
CurrentLink.with_url(url="#", text="Home"),
elements.Link.with_url(url="#", text="Link"),
elements.Dropdown(
title="Dropdown",
items=[
elements.Link.with_url(url="#", text="Action"),
elements.Link.with_url(url="#", text="Another action"),
elements.Separator(),
elements.Link.with_url(url="#", text="Separated link"),
],
),
DisabledLink.with_url(url="#", text="Disabled"),
]
),
bar.NavBarSearch(),
],
),
],
)
return nav
26 changes: 11 additions & 15 deletions tests/nav/test_nav.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from .conftest import CurrentLink
from .conftest import DisabledLink
from .conftest import get_fixture
Expand All @@ -7,43 +9,37 @@
from bootlace.util import render


def test_base_nav() -> None:

@pytest.fixture
def nav() -> elements.Nav:
nav = elements.Nav()
nav.items.append(CurrentLink.with_url(url="#", text="Active"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(DisabledLink.with_url(url="#", text="Disabled"))
return nav


def test_base_nav(nav: elements.Nav) -> None:
source = render(nav)

expected = get_fixture("nav.html")

assert_same_html(expected_html=expected, actual_html=str(source))


def test_nav_tabs() -> None:

nav = elements.Nav(style=NavStyle.TABS)
nav.items.append(CurrentLink.with_url(url="#", text="Active"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(DisabledLink.with_url(url="#", text="Disabled"))
def test_nav_tabs(nav: elements.Nav) -> None:

nav.style = NavStyle.TABS
source = render(nav)

expected = get_fixture("nav_tabs.html")

assert_same_html(expected_html=expected, actual_html=str(source))


def test_nav_pills() -> None:
def test_nav_pills(nav: elements.Nav) -> None:

nav = elements.Nav(style=NavStyle.PILLS)
nav.items.append(CurrentLink.with_url(url="#", text="Active"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(elements.Link.with_url(url="#", text="Link"))
nav.items.append(DisabledLink.with_url(url="#", text="Disabled"))
nav.style = NavStyle.PILLS

source = render(nav)

Expand Down
29 changes: 1 addition & 28 deletions tests/nav/test_navbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,7 @@
from bootlace.util import render


def test_navbar() -> None:
nav = bar.NavBar(
items=[
bar.Brand.with_url(url="#", text="Navbar"),
bar.NavBarCollapse(
id="navbarSupportedContent",
items=[
bar.NavBarNav(
items=[
CurrentLink.with_url(url="#", text="Home"),
elements.Link.with_url(url="#", text="Link"),
elements.Dropdown(
title="Dropdown",
items=[
elements.Link.with_url(url="#", text="Action"),
elements.Link.with_url(url="#", text="Another action"),
elements.Separator(),
elements.Link.with_url(url="#", text="Separated link"),
],
),
DisabledLink.with_url(url="#", text="Disabled"),
]
),
bar.NavBarSearch(),
],
),
],
)
def test_navbar(nav: bar.NavBar) -> None:

source = render(nav)

Expand Down
Loading

0 comments on commit 6101c03

Please sign in to comment.