From 56291211ff6496fc026b7f88ff1459c47618287d Mon Sep 17 00:00:00 2001 From: Matti Lamppu Date: Wed, 7 Dec 2022 17:01:39 +0200 Subject: [PATCH] Support help texts for fields Also improves formatting for deeply nested data. --- .../admin_data_item_page.html | 96 +++++++++--- .../templates/admin_data_views/dict_item.html | 40 ++--- .../templates/admin_data_views/item.html | 16 +- .../templates/admin_data_views/list_item.html | 44 +++--- .../templatetags/admin_data_utils.py | 46 +++++- admin_data_views/typing.py | 21 ++- pyproject.toml | 2 +- tests/django/settings.py | 5 + tests/django/urls.py | 143 +++++++++++++++++- tests/test_utils.py | 122 +++++++++++++++ tests/test_views.py | 10 +- 11 files changed, 466 insertions(+), 79 deletions(-) diff --git a/admin_data_views/templates/admin_data_views/admin_data_item_page.html b/admin_data_views/templates/admin_data_views/admin_data_item_page.html index 7bf6ed2..b774d97 100644 --- a/admin_data_views/templates/admin_data_views/admin_data_item_page.html +++ b/admin_data_views/templates/admin_data_views/admin_data_item_page.html @@ -5,11 +5,63 @@ {{ block.super }} {% endblock %} @@ -33,33 +85,33 @@ {% block content %}
{% csrf_token %} -
+
{% if image %} {% endif %} {% for section in data %}
{% if section.name %} -

{{ section.name }}

+

{{ section.name }}

{% endif %} {% if section.description %} -
{{ section.description|safe }}
+
{{ section.description|safe }}
{% endif %} - {% for key, value in section.fields.items %} -
- {% if value|get_type == "dict" %} - {% include 'admin_data_views/dict_item.html' with label=key items=value only %} - {% elif value|get_type == "list" %} - {% include 'admin_data_views/list_item.html' with label=key items=value only %} - {% else %} -
- - {% include 'admin_data_views/item.html' with value=value only %} -
- {% endif %} -
+
+ {% for key, value in section|fields_with_help_texts|items %} +
+ {% if value.0|get_type == "dict" %} + {% include 'admin_data_views/dict_item.html' with label=key items=value only %} + {% elif value.0|get_type == "list" %} + {% include 'admin_data_views/list_item.html' with label=key items=value only %} + {% else %} + + {% include 'admin_data_views/item.html' with value=value only %} + {% endif %} +
+
+ {% endfor %}
- {% endfor %}
{% endfor %}
diff --git a/admin_data_views/templates/admin_data_views/dict_item.html b/admin_data_views/templates/admin_data_views/dict_item.html index b7e1e93..a67b6b9 100644 --- a/admin_data_views/templates/admin_data_views/dict_item.html +++ b/admin_data_views/templates/admin_data_views/dict_item.html @@ -1,21 +1,27 @@ {% load i18n static admin_data_utils %} -{% if label != "" %} - -{% endif %} +
+ {% if label != "" %} + + {% endif %} -
- {% for key, value in items.items %} - {% if value|get_type == "dict" %} - {% include 'admin_data_views/dict_item.html' with label=key items=value only %} - {% elif value|get_type == "list" %} - {% include 'admin_data_views/list_item.html' with label=key items=value only %} - {% else %} -
- - {% include 'admin_data_views/item.html' with value=value only %} -
- {% endif %} -
-{% endfor %} +
+ {% for key, value in items.0.items %} +
+ {% if value.0|get_type == "dict" %} + {% include 'admin_data_views/dict_item.html' with label=key items=value only %} + {% elif value.0|get_type == "list" %} + {% include 'admin_data_views/list_item.html' with label=key items=value only %} + {% else %} + + {% include 'admin_data_views/item.html' with value=value only %} + {% endif %} +
+
+ {% endfor %} +
+ + {% if items.1 %} +
{{ items.1|safe }}
+ {% endif %}
diff --git a/admin_data_views/templates/admin_data_views/item.html b/admin_data_views/templates/admin_data_views/item.html index c9022c9..a5a3183 100644 --- a/admin_data_views/templates/admin_data_views/item.html +++ b/admin_data_views/templates/admin_data_views/item.html @@ -1,5 +1,11 @@ -{% if value|length > 40 %} - -{% else %} - -{% endif %} +
+ {% if value.0|length > 40 %} + + {% else %} + + {% endif %} + + {% if value.1 %} +
{{ value.1|safe }}
+ {% endif %} +
diff --git a/admin_data_views/templates/admin_data_views/list_item.html b/admin_data_views/templates/admin_data_views/list_item.html index 002db45..8595114 100644 --- a/admin_data_views/templates/admin_data_views/list_item.html +++ b/admin_data_views/templates/admin_data_views/list_item.html @@ -1,21 +1,31 @@ {% load i18n static admin_data_utils %} -{% if label != "" %} - -{% endif %} - -
- {% for value in items %} - {% if value|get_type == "dict" %} - {% include 'admin_data_views/dict_item.html' with label="" items=value only %} - {% elif value|get_type == "list" %} - {% include 'admin_data_views/list_item.html' with label="" items=value only %} - {% else %} -
- {% include 'admin_data_views/item.html' with value=value only %} -
+ \ No newline at end of file diff --git a/admin_data_views/templatetags/admin_data_utils.py b/admin_data_views/templatetags/admin_data_utils.py index 967c542..0e38b08 100644 --- a/admin_data_views/templatetags/admin_data_utils.py +++ b/admin_data_views/templatetags/admin_data_utils.py @@ -1,9 +1,53 @@ from django import template +from ..typing import Any, Dict, DictItems, ItemsView, NestedDict, SectionData, Union + register = template.Library() @register.filter -def get_type(value) -> str: +def get_type(value: Any) -> str: + """Get item type as a string.""" return type(value).__name__ + + +@register.filter +def items(value: Dict[str, Any]) -> ItemsView[str, Any]: + """Get dict items.""" + return value.items() + + +@register.filter +def fields_with_help_texts(section_data: SectionData) -> Dict[str, DictItems]: + """Add help texts and iterate as dict items.""" + + def add_help_text(fields: NestedDict, help_texts: NestedDict) -> Dict[str, DictItems]: + formatted_fields: Dict[str, DictItems] = {} + + for key, value in fields.items(): + help_text: Union[str, NestedDict] = help_texts.get(key, "") + + if isinstance(value, dict): + if isinstance(help_text, str): + formatted_fields[key] = (add_help_text(value, {}), help_text) + else: + formatted_fields[key] = (add_help_text(value, help_text), "") + elif isinstance(value, list): + values = [] + for item in value: + if not isinstance(item, (dict, list)): + values.append((item, "")) + else: + res = add_help_text(item, {} if isinstance(help_text, str) else help_text) + if not isinstance(res, tuple): + res = (res, "") + values.append(res) + + formatted_fields[key] = (values, help_text if isinstance(help_text, str) else "") + else: + formatted_fields[key] = (value, help_text) + + return formatted_fields + + return add_help_text(section_data["fields"], section_data.get("help_texts", {})) diff --git a/admin_data_views/typing.py b/admin_data_views/typing.py index 27eb300..17cbb2d 100644 --- a/admin_data_views/typing.py +++ b/admin_data_views/typing.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List, NamedTuple, Optional, Set, TypedDict, Union +from typing import Any, Callable, Dict, ItemsView, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union __all__ = [ @@ -7,19 +7,30 @@ "AppModel", "Callable", "Dict", + "DictItems", "ItemContext", + "ItemsView", "ItemViewContext", "List", "NamedTuple", + "NestedDict", "Optional", + "SectionData", "Set", "TableContext", "TableViewContext", + "Tuple", "Union", "URLConfig", ] +NestedDict = Dict[str, Union[str, "NestedDict"]] +NestedItem = List[Union[str, NestedDict, "NestedItem"]] +DictItem = Union[str, NestedDict, NestedItem] +DictItems = Tuple[DictItem, str] + + class Perms(TypedDict): add: bool change: bool @@ -63,10 +74,14 @@ class TableViewContext(TypedDict): rows: List[List[Any]] -class SectionData(TypedDict): +class SectionDataBase(TypedDict): name: Optional[str] description: Optional[str] - fields: Dict[str, Any] + fields: NestedDict + + +class SectionData(SectionDataBase, total=False): + help_texts: NestedDict class ItemContextBase(TypedDict): diff --git a/pyproject.toml b/pyproject.toml index fa8f7d2..179e187 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-admin-data-views" -version = "0.2.6" +version = "0.3.0" description = "Add custom data views to django admin panel." authors = [ "Matti Lamppu ", diff --git a/tests/django/settings.py b/tests/django/settings.py index cd888f7..9274ded 100644 --- a/tests/django/settings.py +++ b/tests/django/settings.py @@ -135,5 +135,10 @@ "view": "tests.django.urls.buzz_view", "name": "buzz", }, + { + "route": "complex/", + "view": "tests.django.urls.complex_view", + "name": "complex", + }, ], } diff --git a/tests/django/urls.py b/tests/django/urls.py index 12db697..903d718 100644 --- a/tests/django/urls.py +++ b/tests/django/urls.py @@ -34,8 +34,6 @@ def foo_items_view(request: HttpRequest, idd: int) -> ItemContext: return ItemContext( slug=idd, title=f"This is {idd}", - subtitle=None, - image=None, data=[ { "name": None, @@ -59,7 +57,6 @@ def foo_items_view(request: HttpRequest, idd: int) -> ItemContext: def bar_list_view(request: HttpRequest) -> TableContext: return TableContext( title="Bar items", - subtitle=None, table={ "Fizz": [ItemLink("X"), ItemLink("Y")], "Buzz": ["1", "2"], @@ -72,8 +69,6 @@ def bar_items_view(request: HttpRequest) -> ItemContext: return ItemContext( slug=None, title=f"Bar page", - subtitle=None, - image=None, data=[ { "name": None, @@ -97,7 +92,6 @@ def bar_items_view(request: HttpRequest) -> ItemContext: def fizz_view(request: HttpRequest) -> TableContext: return TableContext( title="Fizz view", - subtitle=None, table={ "A": ["X", "Y"], "B": ["1", "2"], @@ -110,8 +104,6 @@ def buzz_view(request: HttpRequest) -> ItemContext: return ItemContext( slug=None, title=f"Buzz page", - subtitle=None, - image=None, data=[ { "name": None, @@ -124,4 +116,139 @@ def buzz_view(request: HttpRequest) -> ItemContext: ) +@render_with_item_view +def complex_view(request: HttpRequest) -> ItemContext: + return ItemContext( + slug="complex", + title="This is complex", + image="https://images.pexels.com/photos/355508/pexels-photo-355508.jpeg", + data=[ + { + "name": None, + "description": None, + "fields": { + "foo": "bar", + "list": ["bar", 1], + "title": { + "plain": "Send Money", + }, + "fieldset": [ + { + "label": { + "plain": "Personal Info Section", + }, + "fieldset": [ + { + "field": [ + { + "label": { + "plain": "First Name", + }, + "value": { + "plain": "Bob", + }, + "id": "a_1", + }, + { + "label": { + "plain": "Last Name", + }, + "value": { + "plain": "Hogan", + }, + "id": "a_2", + }, + ], + "id": "a_8", + } + ], + "id": "a_5", + }, + { + "label": { + "plain": "Billing Details Section", + }, + "fieldset": { + "field": { + "choices": { + "choice": { + "label": { + "plain": "Gift", + }, + "id": "a_17", + "switch": "", + }, + }, + "label": { + "plain": "Choose a category:", + }, + "value": { + "plain": "Gift", + }, + "id": "a_14", + }, + "fieldset": { + "label": { + "plain": "", + }, + "field": [ + { + "choices": { + "choice": { + "label": { + "plain": "Other", + }, + "id": "a_25", + "switch": "", + } + }, + "label": { + "plain": "Amount", + }, + "value": { + "plain": "Other", + }, + "id": "a_21", + }, + { + "label": { + "plain": "Other Amount", + }, + "value": { + "plain": "200", + }, + "id": "a_20", + }, + ], + "id": "a_26", + }, + "id": "a_13", + }, + "id": "a_12", + }, + ], + }, + "help_texts": { + "foo": "this is bar", + "list": "this is a list", + "title": { + "plain": "Plain title", + }, + "fieldset": { + "label": { + "plain": "Plain label", + }, + "fieldset": { + "field": "Fields", + "fieldset": "Nested Fieldsets", + "id": "Nested id", + }, + "id": "First ID", + }, + }, + }, + ], + ) + + urlpatterns = [path("admin/", admin.site.urls)] diff --git a/tests/test_utils.py b/tests/test_utils.py index bf677bd..7215ae6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,6 +3,8 @@ import pytest from admin_data_views.settings import admin_data_settings +from admin_data_views.templatetags.admin_data_utils import fields_with_help_texts +from admin_data_views.typing import SectionData from admin_data_views.utils import render_with_item_view, render_with_table_view @@ -69,3 +71,123 @@ def test_missing_function_for_item_view_in_from_admin_data_setting(settings): with pytest.raises(ValueError, match="Cannot find 'tests.test_utils.func' in ADMIN_DATA_VIEWS setting."): render_with_item_view(func)(None) + + +def test_fields_with_help_texts(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": "bar"}, + help_texts={"foo": "help"}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ("bar", "help"), + } + + +def test_fields_with_help_texts__nested(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": {"bar": "1"}}, + help_texts={"foo": {"bar": "help"}}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ({"bar": ("1", "help")}, ""), + } + + +def test_fields_with_help_texts__nested__main(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": {"bar": "1"}}, + help_texts={"foo": "help"}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ({"bar": ("1", "")}, "help"), + } + + +def test_fields_with_help_texts__nested__array(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": ["bar", "1"]}, + help_texts={"foo": "help"}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ( + [ + ("bar", ""), + ("1", ""), + ], + "help", + ), + } + + +def test_fields_with_help_texts__nested__array__dicts(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": [{"one": "bar"}, {"two": "1"}]}, + help_texts={"foo": "help"}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ( + [ + ( + {"one": ("bar", "")}, + "", + ), + ( + {"two": ("1", "")}, + "", + ), + ], + "help", + ), + } + + +def test_fields_with_help_texts__nested__array__dicts__sub(): + section_data = SectionData( + name=None, + description=None, + fields={"foo": [{"one": "bar"}, {"two": "1"}]}, + help_texts={"foo": {"one": "this is one", "two": "this is two"}}, + ) + + result = fields_with_help_texts(section_data) + + assert result == { + "foo": ( + [ + ( + {"one": ("bar", "this is one")}, + "", + ), + ( + {"two": ("1", "this is two")}, + "", + ), + ], + "", + ), + } diff --git a/tests/test_views.py b/tests/test_views.py index 61422ec..b2f172b 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -125,7 +125,7 @@ def test_admin_foo_item_view(django_client): assert len(sections) == 2 section_1_title = sections[0].find(name="h2") section_1_subtitle = sections[0].find(name="div", attrs={"class": "description"}) - section_1_fields = sections[0].findAll(name="div", attrs={"class": "fieldBox"}) + section_1_fields = sections[0].findAll(name="div", attrs={"class": "adv-dict-item"}) assert section_1_title is None assert section_1_subtitle is None @@ -139,7 +139,7 @@ def test_admin_foo_item_view(django_client): section_2_title = sections[1].find(name="h2") section_2_subtitle = sections[1].find(name="div", attrs={"class": "description"}) - section_2_fields = sections[1].findAll(name="div", attrs={"class": "fieldBox"}) + section_2_fields = sections[1].findAll(name="div", attrs={"class": "adv-dict-item"}) assert section_2_title.text == "This is another section" assert section_2_subtitle.text == "This is the description for this section" @@ -208,7 +208,7 @@ def test_admin_bar_item_view(django_client): assert len(sections) == 2 section_1_title = sections[0].find(name="h2") section_1_subtitle = sections[0].find(name="div", attrs={"class": "description"}) - section_1_fields = sections[0].findAll(name="div", attrs={"class": "fieldBox"}) + section_1_fields = sections[0].findAll(name="div", attrs={"class": "adv-dict-item"}) assert section_1_title is None assert section_1_subtitle is None @@ -222,7 +222,7 @@ def test_admin_bar_item_view(django_client): section_2_title = sections[1].find(name="h2") section_2_subtitle = sections[1].find(name="div", attrs={"class": "description"}) - section_2_fields = sections[1].findAll(name="div", attrs={"class": "fieldBox"}) + section_2_fields = sections[1].findAll(name="div", attrs={"class": "adv-dict-item"}) assert section_2_title.text == "This is another section" assert section_2_subtitle.text == "This is the description for this section" @@ -285,7 +285,7 @@ def test_admin_buzz_item_view(django_client): assert len(sections) == 1 section_1_title = sections[0].find(name="h2") section_1_subtitle = sections[0].find(name="div", attrs={"class": "description"}) - section_1_fields = sections[0].findAll(name="div", attrs={"class": "fieldBox"}) + section_1_fields = sections[0].findAll(name="div", attrs={"class": "adv-dict-item"}) assert section_1_title is None assert section_1_subtitle is None