diff --git a/README.creole b/README.creole index f4b8dd9..1d69b7e 100644 --- a/README.creole +++ b/README.creole @@ -157,6 +157,7 @@ Files are separated into: "/src/" and "/development/" == history * [[https://github.com/jedie/PyInventory/compare/v0.9.4...master|compare v0.9.4...master]] **dev** +** Group item: default "automatic" mode and can be disabled by filter action ** tbc * [[https://github.com/jedie/PyInventory/compare/v0.9.3...v0.9.4|v0.9.4 - 15.09.2021]] ** Pin {{{psycopg < 2.9}}} because of https://github.com/psycopg/psycopg2/issues/1293 diff --git a/README.rst b/README.rst index 30bf6a2..b18bee9 100644 --- a/README.rst +++ b/README.rst @@ -220,6 +220,8 @@ history * `compare v0.9.4...master `_ **dev** + * Group item: default "automatic" mode and can be disabled by filter action + * tbc * `v0.9.4 - 15.09.2021 `_ @@ -401,4 +403,4 @@ donation ------------ -``Note: this file is generated from README.creole 2021-09-15 21:28:39 with "python-creole"`` \ No newline at end of file +``Note: this file is generated from README.creole 2021-09-29 19:16:20 with "python-creole"`` \ No newline at end of file diff --git a/src/inventory/admin/base.py b/src/inventory/admin/base.py index 2968c25..34ef156 100644 --- a/src/inventory/admin/base.py +++ b/src/inventory/admin/base.py @@ -3,6 +3,7 @@ from reversion_compare.admin import CompareVersionAdmin class BaseUserAdmin(CompareVersionAdmin): def get_changelist(self, request, **kwargs): + self.request = request self.user = request.user return super().get_changelist(request, **kwargs) diff --git a/src/inventory/admin/item.py b/src/inventory/admin/item.py index ed95e03..9fc3e22 100644 --- a/src/inventory/admin/item.py +++ b/src/inventory/admin/item.py @@ -1,7 +1,8 @@ +import logging + import tagulous from adminsortable2.admin import SortableInlineAdminMixin from django.contrib import admin -from django.contrib.admin.views.main import ChangeList from django.template.loader import render_to_string from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ @@ -14,6 +15,9 @@ from inventory.models import ItemLinkModel, ItemModel from inventory.models.item import ItemFileModel, ItemImageModel +logger = logging.getLogger(__name__) + + class UserInlineMixin: def get_queryset(self, request): qs = super().get_queryset(request) @@ -62,14 +66,48 @@ class ItemModelResource(ModelResource): model = ItemModel -class ItemModelChangeList(ChangeList): - def get_queryset(self, request): - """ - List always the base instances - """ - qs = super().get_queryset(request) - qs = qs.filter(parent__isnull=True) - return qs +class GroupItemsListFilter(admin.SimpleListFilter): + title = _('Group Items') + parameter_name = 'grouping' + + GET_KEY_AUTO = 'auto' + GET_KEY_NO = 'no' + + def lookups(self, request, model_admin): + return ( + (self.GET_KEY_AUTO, _('Automatic')), + (self.GET_KEY_NO, _('No')), + ) + + def value(self): + return super().value() or self.GET_KEY_AUTO + + def queryset(self, request, queryset): + auto_mode = self.value() == self.GET_KEY_AUTO + if auto_mode: + request.group_items = not request.GET.keys() + else: + request.group_items = self.value() != self.GET_KEY_NO + + logger.info('Group items: %r (auto mode: %r)', request.group_items, auto_mode) + + if request.group_items: + queryset = queryset.filter(parent__isnull=True) + + return queryset + + def choices(self, changelist): + for lookup, title in self.lookup_choices: + if lookup == self.GET_KEY_AUTO: + query_string = changelist.get_query_string(remove=[self.parameter_name]) + else: + query_string = changelist.get_query_string({self.parameter_name: lookup}) + + yield { + 'selected': self.value() == lookup, + 'query_string': query_string, + 'display': title, + } @admin.register(ItemModel) @@ -88,13 +126,19 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin): return qs def column_item(self, obj): - # TODO: annotate "sub_items" ! - qs = ItemModel.objects.filter(user=self.user) - qs = qs.filter(parent=obj).sort() context = { 'base_item': obj, - 'sub_items': qs } + + if self.request.group_items: # Attribute added in GroupItemsListFilter.queryset() + logger.debug('Display sub items inline') + # TODO: annotate "sub_items" ! + qs = ItemModel.objects.filter( + user=self.user # user added in BaseUserAdmin.get_changelist() + ) + qs = qs.filter(parent=obj).sort() + context['sub_items'] = qs + return render_to_string( template_name='admin/inventory/item/column_item.html', context=context, @@ -111,7 +155,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin): ) ordering = ('kind', 'producer', 'name') list_display_links = None - list_filter = ('kind', 'location', 'producer', 'tags') + list_filter = (GroupItemsListFilter, 'kind', 'location', 'producer', 'tags') search_fields = ('name', 'description', 'kind__name', 'tags__name') fieldsets = ( (_('Internals'), { @@ -156,9 +200,5 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin): readonly_fields = ('id', 'create_dt', 'update_dt', 'user') inlines = (ItemImageModelInline, ItemFileModelInline, ItemLinkModelInline) - def get_changelist(self, request, **kwargs): - self.user = request.user - return ItemModelChangeList - tagulous.admin.enhance(ItemModel, ItemModelAdmin) diff --git a/src/inventory/locale/de/LC_MESSAGES/django.mo b/src/inventory/locale/de/LC_MESSAGES/django.mo index ab2cd59..799b4c5 100644 Binary files a/src/inventory/locale/de/LC_MESSAGES/django.mo and b/src/inventory/locale/de/LC_MESSAGES/django.mo differ diff --git a/src/inventory/locale/de/LC_MESSAGES/django.po b/src/inventory/locale/de/LC_MESSAGES/django.po index 2c6126f..9288606 100644 --- a/src/inventory/locale/de/LC_MESSAGES/django.po +++ b/src/inventory/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-28 18:31+0200\n" -"PO-Revision-Date: 2021-04-28 18:29+0200\n" +"POT-Creation-Date: 2021-09-29 19:19+0200\n" +"PO-Revision-Date: 2021-09-29 19:19+0200\n" "Last-Translator: Jens Diemer\n" "Language-Team: \n" "Language: de\n" @@ -18,6 +18,15 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3\n" +msgid "Group Items" +msgstr "Gegenstände Gruppieren" + +msgid "Automatic" +msgstr "Automatisch" + +msgid "No" +msgstr "" + msgid "ItemModel.verbose_name_plural" msgstr "Gegenstände" @@ -188,7 +197,7 @@ msgid "ItemFileModel.file.verbose_name" msgstr "Datei" msgid "ItemFileModel.file.help_text" -msgstr "" +msgstr " " msgid "ItemFileModel.verbose_name" msgstr "Datei" diff --git a/src/inventory/locale/en/LC_MESSAGES/django.po b/src/inventory/locale/en/LC_MESSAGES/django.po index dd39656..d5a6d76 100644 --- a/src/inventory/locale/en/LC_MESSAGES/django.po +++ b/src/inventory/locale/en/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-28 18:31+0200\n" +"POT-Creation-Date: 2021-09-29 19:19+0200\n" "PO-Revision-Date: 2021-04-28 18:30+0200\n" "Last-Translator: Jens Diemer\n" "Language-Team: \n" @@ -18,6 +18,15 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 2.3\n" +msgid "Group Items" +msgstr "" + +msgid "Automatic" +msgstr "" + +msgid "No" +msgstr "" + msgid "ItemModel.verbose_name_plural" msgstr "Items" diff --git a/src/inventory_project/settings/tests.py b/src/inventory_project/settings/tests.py index 9bd4fb3..99f415a 100644 --- a/src/inventory_project/settings/tests.py +++ b/src/inventory_project/settings/tests.py @@ -1,4 +1,6 @@ -from inventory_project.settings.base import * # noqa +# flake8: noqa: E405, F403 + +from inventory_project.settings.base import * DATABASES = { @@ -11,3 +13,7 @@ DATABASES = { SECRET_KEY = 'No individual secret for tests ;)' DEBUG = True + +LOGGING['formatters']['colored']['format'] = ( + '%(log_color)s%(name)s %(levelname)8s %(cut_path)s:%(lineno)-3s %(message)s' +) diff --git a/src/inventory_project/tests/test_admin_item.py b/src/inventory_project/tests/test_admin_item.py index 7ba9fb1..b791412 100644 --- a/src/inventory_project/tests/test_admin_item.py +++ b/src/inventory_project/tests/test_admin_item.py @@ -1,6 +1,13 @@ -from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin +import datetime +import logging +from unittest import mock + +from bx_django_utils.test_utils.datetime import MockDatetimeGenerator +from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin, assert_html_response_snapshot from django.contrib.auth.models import User +from django.template.defaulttags import CsrfTokenNode from django.test import TestCase +from django.utils import timezone from django_tools.unittest_utils.mockup import ImageDummy from model_bakery import baker @@ -22,7 +29,8 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): @classmethod def setUpTestData(cls): cls.normaluser = baker.make( - User, is_staff=True, is_active=True, is_superuser=False + User, username='NormalUser', + is_staff=True, is_active=True, is_superuser=False ) assert cls.normaluser.user_permissions.count() == 0 group = get_or_create_normal_user_group()[0] @@ -136,3 +144,78 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): assert image.name == 'test.png' assert image.item == item assert image.user_id == self.normaluser.pk + + def test_auto_group_items(self): + self.client.force_login(self.normaluser) + + offset = datetime.timedelta(minutes=1) + with mock.patch.object(timezone, 'now', MockDatetimeGenerator(offset=offset)): + for main_item_no in range(1, 3): + main_item = ItemModel.objects.create( + id=f'00000000-000{main_item_no}-0000-0000-000000000000', + user=self.normaluser, + name=f'main item {main_item_no}' + ) + main_item.full_clean() + for sub_item_no in range(1, 3): + sub_item = ItemModel.objects.create( + id=f'00000000-000{main_item_no}-000{sub_item_no}-0000-000000000000', + user=self.normaluser, + parent=main_item, + name=f'sub item {main_item_no}.{sub_item_no}' + ) + sub_item.full_clean() + + names = list(ItemModel.objects.order_by('id').values_list('name', flat=True)) + assert names == [ + 'main item 1', 'sub item 1.1', 'sub item 1.2', + 'main item 2', 'sub item 2.1', 'sub item 2.2', + ] + + # Default mode, without any GET parameter -> group "automatic": + + with mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'), \ + self.assertLogs(logger='inventory', level=logging.DEBUG) as logs: + response = self.client.get( + path='/admin/inventory/itemmodel/', + ) + assert response.status_code == 200 + self.assert_html_parts(response, parts=( + f'Select Item to change | PyInventory v{__version__}', + + '' + 'main item 1', + + '
  • ' + 'sub item 1.1
  • ', + )) + assert logs.output == [ + 'INFO:inventory.admin.item:Group items: True (auto mode: True)', + 'DEBUG:inventory.admin.item:Display sub items inline', + 'DEBUG:inventory.admin.item:Display sub items inline' + ] + assert_html_response_snapshot(response=response) + + # Search should disable grouping: + + with mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'), \ + self.assertLogs(logger='inventory', level=logging.DEBUG) as logs: + response = self.client.get( + path='/admin/inventory/itemmodel/?q=sub+item+2.', + ) + assert response.status_code == 200 + self.assert_html_parts(response, parts=( + '', + '2 results (6 total)', + + '' + 'sub item 2.1', + + '' + 'sub item 2.2', + )) + assert logs.output == [ + # grouping disabled? + 'INFO:inventory.admin.item:Group items: False (auto mode: True)' + ] + assert_html_response_snapshot(response=response) diff --git a/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html b/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html new file mode 100644 index 0000000..e0512fc --- /dev/null +++ b/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html @@ -0,0 +1,160 @@ + + + +Select Item to change | PyInventory v0.9.4 + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    +

    Select Item to change

    +
    + +
    +
    + +
    +

    Filter

    +

    By Group Items

    + +
    +
    MockedCsrfTokenNode +
    + + + 0 of 2 selected +
    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    Kind
    +
    +
    +
    Producer
    +
    +
    +
    Items
    +
    +
    + +
    +
    + +
    +
    + +
    +
      main item 1 + +--Jan. 1, 2000, 1:01 a.m.
      main item 2 + +--Jan. 1, 2000, 1:04 a.m.
    +
    +

    +2 Items +

    +
    +
    +
    +
    +
    + + +
    + + + \ No newline at end of file diff --git a/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html b/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html new file mode 100644 index 0000000..d33cd08 --- /dev/null +++ b/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html @@ -0,0 +1,152 @@ + + + +Select Item to change | PyInventory v0.9.4 + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    +

    Select Item to change

    +
    + +
    +
    + +
    +

    Filter

    +

    By Group Items

    + +
    +
    MockedCsrfTokenNode +
    + + + 0 of 2 selected +
    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    Kind
    +
    +
    +
    Producer
    +
    +
    +
    Items
    +
    +
    + +
    +
    + +
    +
    + +
    +
      sub item 2.1 +--Jan. 1, 2000, 1:05 a.m.
      sub item 2.2 +--Jan. 1, 2000, 1:06 a.m.
    +
    +

    +2 Items +

    +
    +
    +
    +
    +
    + + +
    + + + \ No newline at end of file