From 23e6009b9911f342c096b0f6c9187333a7c49bca Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 17:15:52 +0200 Subject: [PATCH 1/7] updates --- CHANGES | 7 ++ src/adminactions/config.py | 1 + src/adminactions/mass_update.py | 20 ++-- .../static/adminactions/css/bulkupdate.css | 1 + .../adminactions/css/bulkupdate.css.map | 1 + tests/test_bulk_update.py | 104 +++++++++--------- tests/test_mass_update.py | 6 +- 7 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 src/adminactions/static/adminactions/css/bulkupdate.css create mode 100644 src/adminactions/static/adminactions/css/bulkupdate.css.map diff --git a/CHANGES b/CHANGES index 8084408..7bb64af 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,10 @@ +Release +============= +* new `MassUpdateForm.sort_fields`. Make optional MassUpdateForm fields sorting +* make possible globally customize MassUpdateForm +* removes async feature from Bulk update + + Release 2.1 =========== * new action "Bulk Update" diff --git a/src/adminactions/config.py b/src/adminactions/config.py index bc5b749..51cd85f 100644 --- a/src/adminactions/config.py +++ b/src/adminactions/config.py @@ -6,3 +6,4 @@ settings, "AA_PERMISSION_HANDLER", AA_PERMISSION_CREATE_USE_SIGNAL ) AA_ENABLE_LOG = getattr(settings, "AA_ENABLE_LOG", True) +AA_MASSUPDATE_FORM = getattr(settings, "AA_MASSUPDATE_FORM", "adminactions.mass_update.MassUpdateForm") diff --git a/src/adminactions/mass_update.py b/src/adminactions/mass_update.py index 2b6aba5..2838654 100644 --- a/src/adminactions/mass_update.py +++ b/src/adminactions/mass_update.py @@ -22,6 +22,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import render from django.utils.encoding import smart_str +from django.utils.module_loading import import_string from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ @@ -166,19 +167,21 @@ class MassUpdateForm(GenericActionForm): required=False, help_text=_("if checked use obj.save() instead of manager.update()"), ) - + sort_fields = True def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._errors = None self.update_using_queryset_allowed = True if not celery_present: self.fields["_async"].widget = forms.HiddenInput() - self.fields = { - k: v - for k, v in sorted( - self.fields.items(), key=lambda item: item[1].label or "" - ) - } + + if self.sort_fields: + self.fields = { + k: v + for k, v in sorted( + self.fields.items(), key=lambda item: item[1].label or "" + ) + } def _get_validation_exclusions(self): exclude = list(super()._get_validation_exclusions()) @@ -433,7 +436,8 @@ def _get_sample(): messages.error(request, str(e)) return - mass_update_form = getattr(modeladmin, "mass_update_form", MassUpdateForm) + formClass = import_string(config.AA_MASSUPDATE_FORM) + mass_update_form = getattr(modeladmin, "mass_update_form", formClass) mass_update_fields = getattr(modeladmin, "mass_update_fields", None) mass_update_exclude = getattr(modeladmin, "mass_update_exclude", None) if mass_update_fields and mass_update_exclude: diff --git a/src/adminactions/static/adminactions/css/bulkupdate.css b/src/adminactions/static/adminactions/css/bulkupdate.css new file mode 100644 index 0000000..95af94e --- /dev/null +++ b/src/adminactions/static/adminactions/css/bulkupdate.css @@ -0,0 +1 @@ +table.bulk-update{border:1px solid #c9c9c9}table.bulk-update td{border-left:1px solid #c9c9c9}table.bulk-update select.func_select{min-width:100px}table th.title{background-color:var(--header-bg)}#col1{float:left;width:70%}#col1 table{width:90%}#col2{float:right;width:30%}#col2 table{width:90%}#col2 select{width:100%}/*# sourceMappingURL=bulkupdate.css.map */ diff --git a/src/adminactions/static/adminactions/css/bulkupdate.css.map b/src/adminactions/static/adminactions/css/bulkupdate.css.map new file mode 100644 index 0000000..f83c282 --- /dev/null +++ b/src/adminactions/static/adminactions/css/bulkupdate.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["bulkupdate.scss"],"names":[],"mappings":"AACA,kBACI,yBAEA,qBACI,8BAGJ,qCACI,gBAIJ,eACI,kCAGR,MACI,WAEA,UACA,YACI,UAIR,MACI,YACA,UACA,YACI,UAIR,aACI","file":"bulkupdate.css"} \ No newline at end of file diff --git a/tests/test_bulk_update.py b/tests/test_bulk_update.py index 0ced97f..cfc4d7f 100644 --- a/tests/test_bulk_update.py +++ b/tests/test_bulk_update.py @@ -143,7 +143,7 @@ def test_messages(self): assert "Updated" in messages[0] def test_index_required(self): - res = self._run_action(**{"_async": 0, "_validate": 0, "fld-index_field": []}) + res = self._run_action(**{"_validate": 0, "fld-index_field": []}) assert res.status_code == 200 assert res.context["map_form"].errors == { "index_field": ["Please select one or more index fields"] @@ -165,54 +165,54 @@ def test_wrong_mapping(self): messages = [m.message for m in list(res.context["messages"])] assert messages[0] == "['miss column is not present in the file']" - def test_async_qs(self): - # Create handler - G(DemoModel, id=1, char="char1", integer=100) - G(DemoModel, id=2, char="char2", integer=101) - G(DemoModel, id=3, char="char3", integer=102) - - res = self._run_action( - **{ - "_async": 1, - "_validate": 0, - "_file": Upload( - "data.csv", - b"pk,name,number\n1,aaa,111\n2,bbb,222\n3,ccc,333", - "text/csv", - ), - "fld-index_field": ["id"], - "fld-id": "pk", - "fld-char": "name", - "fld-integer": "number", - } - ) - assert res.status_code == 302, res.showbrowser() - assert DemoModel.objects.filter(id=1, char="char1").exists() - - @patch("adminactions.bulk_update.adminaction_end.send") - @patch("adminactions.bulk_update.adminaction_start.send") - @patch("adminactions.bulk_update.adminaction_requested.send") - def test_async_single(self, req, start, end): - G(DemoModel, id=1, char="char1", integer=100) - G(DemoModel, id=2, char="char2", integer=101) - G(DemoModel, id=3, char="char3", integer=102) - res = self._run_action( - **{ - "_async": 1, - "_validate": 1, - "select_across": 1, - "_file": Upload( - "data.csv", - b"pk,name,number\n1,aaa,111\n2,bbb,222\n3,ccc,333", - "text/csv", - ), - "fld-char": "name", - "fld-integer": "number", - } - ) - assert res.status_code == 302 - assert req.called - assert start.called - assert end.called - assert DemoModel.objects.filter(char="aaa").exists() - assert DemoModel.objects.filter(char="bbb").exists() + # def test_async_qs(self): + # # Create handler + # G(DemoModel, id=1, char="char1", integer=100) + # G(DemoModel, id=2, char="char2", integer=101) + # G(DemoModel, id=3, char="char3", integer=102) + # + # res = self._run_action( + # **{ + # "_async": 1, + # "_validate": 0, + # "_file": Upload( + # "data.csv", + # b"pk,name,number\n1,aaa,111\n2,bbb,222\n3,ccc,333", + # "text/csv", + # ), + # "fld-index_field": ["id"], + # "fld-id": "pk", + # "fld-char": "name", + # "fld-integer": "number", + # } + # ) + # assert res.status_code == 302, res.showbrowser() + # assert DemoModel.objects.filter(id=1, char="char1").exists() + # + # @patch("adminactions.bulk_update.adminaction_end.send") + # @patch("adminactions.bulk_update.adminaction_start.send") + # @patch("adminactions.bulk_update.adminaction_requested.send") + # def test_async_single(self, req, start, end): + # G(DemoModel, id=1, char="char1", integer=100) + # G(DemoModel, id=2, char="char2", integer=101) + # G(DemoModel, id=3, char="char3", integer=102) + # res = self._run_action( + # **{ + # "_async": 1, + # "_validate": 1, + # "select_across": 1, + # "_file": Upload( + # "data.csv", + # b"pk,name,number\n1,aaa,111\n2,bbb,222\n3,ccc,333", + # "text/csv", + # ), + # "fld-char": "name", + # "fld-integer": "number", + # } + # ) + # assert res.status_code == 302 + # assert req.called + # assert start.called + # assert end.called + # assert DemoModel.objects.filter(char="aaa").exists() + # assert DemoModel.objects.filter(char="bbb").exists() diff --git a/tests/test_mass_update.py b/tests/test_mass_update.py index 297dc7b..f3f26e8 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -21,11 +21,15 @@ ] -def test_operationmanager(): +def test_operationmanager_get(): assert OPERATIONS[fields.IntegerField] == OPERATIONS[fields.BigIntegerField] assert OPERATIONS[fields.BooleanField] == OPERATIONS[fields.NullBooleanField] +def test_operationmanager_get_for_field(): + assert list(OPERATIONS[fields.CharField].keys()) == ['set', 'set null', 'upper', 'lower', 'capitalize', 'trim'] + assert list(OPERATIONS.get_for_field(fields.CharField(null=True)).keys()) == ['set', 'set null', 'upper', 'lower', 'capitalize', 'trim'] + class MassUpdateTest(SelectRowsMixin, CheckSignalsMixin, WebTestMixin, TestCase): fixtures = ["adminactions", "demoproject"] urls = "demo.urls" From c5e2c5ed4bf42b13158c03123ea42255e8d4b588 Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 17:26:29 +0200 Subject: [PATCH 2/7] lint --- src/adminactions/bulk_update.py | 6 ++---- src/adminactions/mass_update.py | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/adminactions/bulk_update.py b/src/adminactions/bulk_update.py index f73e7a5..617fb17 100644 --- a/src/adminactions/bulk_update.py +++ b/src/adminactions/bulk_update.py @@ -64,14 +64,12 @@ class BulkUpdateForm(forms.Form): # label="Date format", required=True, help_text=_("Date format") # ) - @property def media(self): """Return all media required to render the widgets on this form.""" media = Media(js=["adminactions/js/bulkupdate.js"], - css={"all": - ["adminactions/css/bulkupdate.css"] - }) + css={"all": ["adminactions/css/bulkupdate.css"]} + ) for field in self.fields.values(): media = media + field.widget.media return media diff --git a/src/adminactions/mass_update.py b/src/adminactions/mass_update.py index 2838654..d5c4d1c 100644 --- a/src/adminactions/mass_update.py +++ b/src/adminactions/mass_update.py @@ -168,6 +168,7 @@ class MassUpdateForm(GenericActionForm): help_text=_("if checked use obj.save() instead of manager.update()"), ) sort_fields = True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._errors = None From 97da76c810fef5e2bf0b2df1e9f533157feed069 Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 17:30:27 +0200 Subject: [PATCH 3/7] lint --- src/adminactions/bulk_update.py | 15 ++++++++------- src/adminactions/config.py | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/adminactions/bulk_update.py b/src/adminactions/bulk_update.py index 617fb17..ffbdddd 100644 --- a/src/adminactions/bulk_update.py +++ b/src/adminactions/bulk_update.py @@ -67,9 +67,10 @@ class BulkUpdateForm(forms.Form): @property def media(self): """Return all media required to render the widgets on this form.""" - media = Media(js=["adminactions/js/bulkupdate.js"], - css={"all": ["adminactions/css/bulkupdate.css"]} - ) + media = Media( + js=["adminactions/js/bulkupdate.js"], + css={"all": ["adminactions/css/bulkupdate.css"]}, + ) for field in self.fields.values(): media = media + field.widget.media return media @@ -219,10 +220,10 @@ def bulk_update(modeladmin, request, queryset): # noqa "map_form": map_form, "action_short_description": bulk_update.short_description, "title": "%s (%s)" - % ( - bulk_update.short_description.capitalize(), - smart_str(modeladmin.opts.verbose_name_plural), - ), + % ( + bulk_update.short_description.capitalize(), + smart_str(modeladmin.opts.verbose_name_plural), + ), "change": True, "is_popup": False, "save_as": False, diff --git a/src/adminactions/config.py b/src/adminactions/config.py index 51cd85f..0825ae3 100644 --- a/src/adminactions/config.py +++ b/src/adminactions/config.py @@ -6,4 +6,6 @@ settings, "AA_PERMISSION_HANDLER", AA_PERMISSION_CREATE_USE_SIGNAL ) AA_ENABLE_LOG = getattr(settings, "AA_ENABLE_LOG", True) -AA_MASSUPDATE_FORM = getattr(settings, "AA_MASSUPDATE_FORM", "adminactions.mass_update.MassUpdateForm") +AA_MASSUPDATE_FORM = getattr( + settings, "AA_MASSUPDATE_FORM", "adminactions.mass_update.MassUpdateForm" +) From 4013c88ff0e6e22fc6824eef6fd9b5f401a6f797 Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 17:33:33 +0200 Subject: [PATCH 4/7] lint --- tests/test_bulk_update.py | 1 - tests/test_mass_update.py | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/test_bulk_update.py b/tests/test_bulk_update.py index cfc4d7f..fe71c91 100644 --- a/tests/test_bulk_update.py +++ b/tests/test_bulk_update.py @@ -1,6 +1,5 @@ import csv from pathlib import Path -from unittest.mock import patch from demo.models import DemoModel from django.contrib.auth.models import User diff --git a/tests/test_mass_update.py b/tests/test_mass_update.py index f3f26e8..8cd9c40 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -27,8 +27,23 @@ def test_operationmanager_get(): def test_operationmanager_get_for_field(): - assert list(OPERATIONS[fields.CharField].keys()) == ['set', 'set null', 'upper', 'lower', 'capitalize', 'trim'] - assert list(OPERATIONS.get_for_field(fields.CharField(null=True)).keys()) == ['set', 'set null', 'upper', 'lower', 'capitalize', 'trim'] + assert list(OPERATIONS[fields.CharField].keys()) == [ + "set", + "set null", + "upper", + "lower", + "capitalize", + "trim", + ] + assert list(OPERATIONS.get_for_field(fields.CharField(null=True)).keys()) == [ + "set", + "set null", + "upper", + "lower", + "capitalize", + "trim", + ] + class MassUpdateTest(SelectRowsMixin, CheckSignalsMixin, WebTestMixin, TestCase): fixtures = ["adminactions", "demoproject"] From 0fd44f7a42796c6dbeb3d002f3f288ed4fb9fc92 Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 20:48:49 +0200 Subject: [PATCH 5/7] updates tests --- src/adminactions/mass_update.py | 4 +-- tests/demo/models.py | 9 +++++++ tests/test_mass_update.py | 44 ++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/adminactions/mass_update.py b/src/adminactions/mass_update.py index d5c4d1c..c6bf7a7 100644 --- a/src/adminactions/mass_update.py +++ b/src/adminactions/mass_update.py @@ -437,8 +437,8 @@ def _get_sample(): messages.error(request, str(e)) return - formClass = import_string(config.AA_MASSUPDATE_FORM) - mass_update_form = getattr(modeladmin, "mass_update_form", formClass) + defaultFormClass = import_string(config.AA_MASSUPDATE_FORM) + mass_update_form = getattr(modeladmin, "mass_update_form", defaultFormClass) mass_update_fields = getattr(modeladmin, "mass_update_fields", None) mass_update_exclude = getattr(modeladmin, "mass_update_exclude", None) if mass_update_fields and mass_update_exclude: diff --git a/tests/demo/models.py b/tests/demo/models.py index 45df68a..0c68796 100644 --- a/tests/demo/models.py +++ b/tests/demo/models.py @@ -5,6 +5,7 @@ from django.db import models from adminactions.helpers import AdminActionPermMixin +from adminactions.mass_update import MassUpdateForm class SubclassedImageField(models.ImageField): @@ -83,6 +84,14 @@ class DemoOneToOneAdmin(ExtraUrlMixin, AdminActionPermMixin, ModelAdmin): pass +class TestMassUpdateForm(MassUpdateForm): + pass + + +class DemoModelMassUpdateForm(MassUpdateForm): + sort_fields = False + + site.register(DemoModel, DemoModelAdmin) site.register(DemoOneToOne, DemoOneToOneAdmin) site.register(UserDetail, UserDetailModelAdmin) diff --git a/tests/test_mass_update.py b/tests/test_mass_update.py index 8cd9c40..2d732e7 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -3,10 +3,19 @@ from unittest import skipIf from unittest.mock import patch -from demo.models import DemoModel +from django.conf import settings + +from adminactions import config +from demo.models import ( + DemoModel, + DemoModelAdmin, + DemoModelMassUpdateForm, + TestMassUpdateForm, +) from django.contrib.auth.models import User from django.db.models import fields from django.test import TestCase +from django.test.utils import override_settings from django.urls import reverse from django_dynamic_fixture import G from django_webtest import WebTestMixin @@ -96,6 +105,39 @@ def test_no_permission(self): res.body ) + def test_custom_modeladmin_form(self): + DemoModelAdmin.mass_update_form = DemoModelMassUpdateForm + with user_grant_permission( + self.user, + ["demo.change_demomodel", "demo.adminactions_massupdate_demomodel"], + ): + res = self.app.get("/", user="user") + res = res.click("Demo models") + form = res.forms["changelist-form"] + form["action"] = "mass_update" + self._select_rows(form, [0, 1]) + res = form.submit() + + assert isinstance(res.context["adminform"].form, DemoModelMassUpdateForm) + + def test_custom_form(self): + with override_settings(AA_MASSUPDATE_FORM="demo.models.TestMassUpdateForm"): + config.AA_MASSUPDATE_FORM = settings.AA_MASSUPDATE_FORM + with user_grant_permission( + self.user, + ["demo.change_demomodel", "demo.adminactions_massupdate_demomodel"], + ): + res = self.app.get("/", user="user") + res = res.click("Demo models") + form = res.forms["changelist-form"] + form["action"] = "mass_update" + self._select_rows(form, [0, 1]) + res = form.submit() + + config.AA_MASSUPDATE_FORM = "adminactions.mass_update.MassUpdateForm" + assert isinstance(res.context["adminform"].form, TestMassUpdateForm) + + def test_validate_on(self): self._run_action(**{"_validate": 1}) assert DemoModel.objects.filter(char="CCCCC").exists() From 72f1245afd5465fd75e08232276e88aaad7934fb Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 20:56:49 +0200 Subject: [PATCH 6/7] lint --- tests/test_mass_update.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_mass_update.py b/tests/test_mass_update.py index 2d732e7..648b9e0 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -3,15 +3,13 @@ from unittest import skipIf from unittest.mock import patch -from django.conf import settings - -from adminactions import config from demo.models import ( DemoModel, DemoModelAdmin, DemoModelMassUpdateForm, TestMassUpdateForm, ) +from django.conf import settings from django.contrib.auth.models import User from django.db.models import fields from django.test import TestCase @@ -22,6 +20,7 @@ from utils import CheckSignalsMixin, SelectRowsMixin, user_grant_permission from webtest import Upload +from adminactions import config from adminactions.compat import celery_present from adminactions.mass_update import OPERATIONS From 6dd26f71578cdf2cde80e0e6ef03a939d2f93d2a Mon Sep 17 00:00:00 2001 From: sax Date: Wed, 23 Aug 2023 20:59:50 +0200 Subject: [PATCH 7/7] lint --- tests/test_mass_update.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_mass_update.py b/tests/test_mass_update.py index 648b9e0..e38a6b4 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -136,7 +136,6 @@ def test_custom_form(self): config.AA_MASSUPDATE_FORM = "adminactions.mass_update.MassUpdateForm" assert isinstance(res.context["adminform"].form, TestMassUpdateForm) - def test_validate_on(self): self._run_action(**{"_validate": 1}) assert DemoModel.objects.filter(char="CCCCC").exists()