From 07dffbf4536b5aa8c7bca52e80eee6de7902c619 Mon Sep 17 00:00:00 2001 From: Naggafin Date: Mon, 6 Jan 2025 15:25:43 -0500 Subject: [PATCH 1/3] standardized/modernized view mixins --- view_breadcrumbs/generic/create.py | 8 +++++++- view_breadcrumbs/generic/delete.py | 23 +++++++++++++++++++++-- view_breadcrumbs/generic/detail.py | 27 ++++++++------------------- view_breadcrumbs/generic/list.py | 6 +++++- view_breadcrumbs/generic/update.py | 11 +++++++---- 5 files changed, 48 insertions(+), 27 deletions(-) diff --git a/view_breadcrumbs/generic/create.py b/view_breadcrumbs/generic/create.py index 8790f9f0..8fad2fac 100644 --- a/view_breadcrumbs/generic/create.py +++ b/view_breadcrumbs/generic/create.py @@ -28,11 +28,17 @@ def __create_view_name(self): def create_view_url(self): return reverse(self.__create_view_name) + @property + def create_view_label(self): + if self.add_format_string: + return self.add_format_string % {"model": self.model_name_title} + return _("Add %(model)s") % {"model": self.model_name_title} + @property def crumbs(self): return super(CreateBreadcrumbMixin, self).crumbs + [ ( - _(self.add_format_string % {"model": self.model_name_title}), + self.create_view_label, self.create_view_url, ), ] diff --git a/view_breadcrumbs/generic/delete.py b/view_breadcrumbs/generic/delete.py index c723fb4c..13768628 100644 --- a/view_breadcrumbs/generic/delete.py +++ b/view_breadcrumbs/generic/delete.py @@ -1,10 +1,15 @@ from django.urls import reverse +from django.utils.encoding import force_str +from django.utils.translation import gettext_lazy as _ from ..utils import action_view_name, classproperty -from .list import ListBreadcrumbMixin +from .detail import DetailBreadcrumbMixin -class DeleteBreadcrumbMixin(ListBreadcrumbMixin): +class DeleteBreadcrumbMixin(DetailBreadcrumbMixin): + # Home / object List / object / Delete object + delete_format_string = _("Delete %(instance)s") + @classproperty def delete_view_name(self): return action_view_name( @@ -30,3 +35,17 @@ def delete_view_url(self, instance): self.__delete_view_name, kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, ) + + def delete_view_label(self, instance): + if self.delete_format_string: + return self.delete_format_string % {'instance': force_str(instance)} + return _("Delete %(instance)s") % {'instance': force_str(instance)} + + @property + def crumbs(self): + return super(DeleteBreadcrumbMixin, self).crumbs + [ + ( + self.delete_view_label, + self.delete_view_url, + ), + ] diff --git a/view_breadcrumbs/generic/detail.py b/view_breadcrumbs/generic/detail.py index c13af189..6bfbf482 100644 --- a/view_breadcrumbs/generic/detail.py +++ b/view_breadcrumbs/generic/detail.py @@ -1,20 +1,12 @@ -from functools import partial - -from django.urls import reverse from django.utils.encoding import force_str -from django.utils.translation import gettext_lazy as _ from ..utils import action_view_name, classproperty from .list import ListBreadcrumbMixin -def _detail_view_label(instance, format_string): - return _(force_str(format_string) % {"instance": force_str(instance)}) - - class DetailBreadcrumbMixin(ListBreadcrumbMixin): # Home / object List / str(object) - detail_format_string = _("%(instance)s") + detail_format_string = "%s" @classproperty def detail_view_name(self): @@ -32,21 +24,18 @@ def __detail_view_name(self): ) def detail_view_url(self, instance): - if self.breadcrumb_use_pk: - return reverse( - self.__detail_view_name, kwargs={self.pk_url_kwarg: instance.pk} - ) - - return reverse( - self.__detail_view_name, - kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, - ) + return instance.get_absolute_url() + + def detail_view_label(self, instance): + if self.detail_format_string: + return self.detail_format_string % force_str(instance) + return force_str(instance) @property def crumbs(self): return super(DetailBreadcrumbMixin, self).crumbs + [ ( - partial(_detail_view_label, format_string=self.detail_format_string), + self.detail_view_label, self.detail_view_url, ), ] diff --git a/view_breadcrumbs/generic/list.py b/view_breadcrumbs/generic/list.py index f5a3b568..a7fb1db7 100644 --- a/view_breadcrumbs/generic/list.py +++ b/view_breadcrumbs/generic/list.py @@ -26,6 +26,10 @@ def __list_view_name(self): def list_view_url(self): return reverse(self.__list_view_name) + @property + def list_view_label(self): + return self.model_name_title_plural + @property def crumbs(self): - return [(self.model_name_title_plural, self.list_view_url)] + return [(self.list_view_label, self.list_view_url)] diff --git a/view_breadcrumbs/generic/update.py b/view_breadcrumbs/generic/update.py index 3db29626..1b3abb49 100644 --- a/view_breadcrumbs/generic/update.py +++ b/view_breadcrumbs/generic/update.py @@ -1,5 +1,3 @@ -from functools import partial - from django.urls import reverse from django.utils.encoding import force_str from django.utils.translation import gettext_lazy as _ @@ -14,7 +12,7 @@ def _update_view_label(instance, format_string): class UpdateBreadcrumbMixin(DetailBreadcrumbMixin): # Home / object List / object / Update object - update_format_str = _("Update: %(instance)s") + update_format_string = _("Update %(instance)s") @classproperty def update_view_name(self): @@ -42,11 +40,16 @@ def update_view_url(self, instance): kwargs={self.slug_url_kwarg: getattr(instance, self.slug_field)}, ) + def update_view_label(self, instance): + if self.update_format_string: + return self.update_format_string % {'instance': force_str(instance)} + return _("Update %(instance)s") % {'instance': force_str(instance)} + @property def crumbs(self): return super(UpdateBreadcrumbMixin, self).crumbs + [ ( - partial(_update_view_label, format_string=self.update_format_str), + self.update_view_label, self.update_view_url, ), ] From b66ff066cb89fec9e0fca8da65ad93b542a36d17 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:29:08 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- view_breadcrumbs/generic/delete.py | 4 ++-- view_breadcrumbs/generic/update.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/view_breadcrumbs/generic/delete.py b/view_breadcrumbs/generic/delete.py index 13768628..39c07b91 100644 --- a/view_breadcrumbs/generic/delete.py +++ b/view_breadcrumbs/generic/delete.py @@ -38,8 +38,8 @@ def delete_view_url(self, instance): def delete_view_label(self, instance): if self.delete_format_string: - return self.delete_format_string % {'instance': force_str(instance)} - return _("Delete %(instance)s") % {'instance': force_str(instance)} + return self.delete_format_string % {"instance": force_str(instance)} + return _("Delete %(instance)s") % {"instance": force_str(instance)} @property def crumbs(self): diff --git a/view_breadcrumbs/generic/update.py b/view_breadcrumbs/generic/update.py index 1b3abb49..5a3335ed 100644 --- a/view_breadcrumbs/generic/update.py +++ b/view_breadcrumbs/generic/update.py @@ -42,8 +42,8 @@ def update_view_url(self, instance): def update_view_label(self, instance): if self.update_format_string: - return self.update_format_string % {'instance': force_str(instance)} - return _("Update %(instance)s") % {'instance': force_str(instance)} + return self.update_format_string % {"instance": force_str(instance)} + return _("Update %(instance)s") % {"instance": force_str(instance)} @property def crumbs(self): From a399a8f43c41f63e866f99a4e6b5fe82c6c512cd Mon Sep 17 00:00:00 2001 From: Naggafin Date: Mon, 13 Jan 2025 09:43:10 -0500 Subject: [PATCH 3/3] added new tests --- .gitignore | 2 + .../templatetags/view_breadcrumbs.py | 1 - .../tests/unit/test_breadcrumbs.py | 58 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 546aa204..fc746246 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,14 @@ .idea/ __pycache__ .pytest_cache +*.pytest-deps *.pyc .tox/ build/ dist/ *.db +*.log .DS_Store .coverage diff --git a/view_breadcrumbs/templatetags/view_breadcrumbs.py b/view_breadcrumbs/templatetags/view_breadcrumbs.py index aed8cdbe..0d31b2af 100644 --- a/view_breadcrumbs/templatetags/view_breadcrumbs.py +++ b/view_breadcrumbs/templatetags/view_breadcrumbs.py @@ -4,7 +4,6 @@ :contact: l.mierzwa@gmail.com """ - from __future__ import unicode_literals import logging diff --git a/view_breadcrumbs/tests/unit/test_breadcrumbs.py b/view_breadcrumbs/tests/unit/test_breadcrumbs.py index 1b3a65a9..b8440b2c 100644 --- a/view_breadcrumbs/tests/unit/test_breadcrumbs.py +++ b/view_breadcrumbs/tests/unit/test_breadcrumbs.py @@ -1,3 +1,5 @@ +import inspect + from django.conf import settings from django.test import RequestFactory, TestCase, override_settings from django.utils.encoding import force_str @@ -68,6 +70,9 @@ class ActionTestMixin(object): def _get_view(self): # TODO: Move this to use the default django client. + TestModel = self.view_attrs["model"] + TestModel.get_absolute_url = lambda self: "test_model/%d" % self.pk + instance = TestModel.objects.create(name="Test") TestViewClass = self.make_crumb_cls( @@ -108,6 +113,59 @@ def test_valid_view_url(self): else: self.assertIsNotNone(view_url(view.object)) + def test_valid_view_label(self): + view = self._get_view() + view_label_name = "{}_view_label".format(self.view_name) + view_label_func = getattr(view, view_label_name) + + kwargs = {} + + # check if it's a property and get the actual function + if isinstance(view_label_func, property): + view_label_func = view_label_func.fget + + # use inspect to get the function signature if callable + if callable(view_label_func): + signature = inspect.signature(view_label_func) + if "instance" in signature.parameters: + kwargs = {"instance": view.object} + + label = ( + view_label_func(**kwargs) if callable(view_label_func) else view_label_func + ) + + match self.view_name: + case "list": + self.assertEqual(label, view.model_name_title_plural) + case "detail": + self.assertEqual( + label, view.detail_format_string % force_str(view.object) + ) + case "create": + self.assertEqual( + label, view.add_format_string % {"model": view.model_name_title} + ) + case "update": + self.assertEqual( + label, + view.update_format_string % {"instance": force_str(view.object)}, + ) + case "delete": + self.assertEqual( + label, + view.delete_format_string % {"instance": force_str(view.object)}, + ) + + new_label = "TEST" + + @property + def new_label_method(self, instance=None): + return new_label + + # monkey patch view class method to override + setattr(type(view), view_label_name, new_label_method) + self.assertEqual(getattr(view, view_label_name), new_label) + class ListViewBreadcrumbTestCase(ActionTestMixin, BaseBreadcrumbTestCase): breadcrumb_mixin_cls = ListBreadcrumbMixin