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/bulk_update.py b/src/adminactions/bulk_update.py index f73e7a5..ffbdddd 100644 --- a/src/adminactions/bulk_update.py +++ b/src/adminactions/bulk_update.py @@ -64,14 +64,13 @@ 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"] - }) + 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 @@ -221,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 bc5b749..0825ae3 100644 --- a/src/adminactions/config.py +++ b/src/adminactions/config.py @@ -6,3 +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" +) diff --git a/src/adminactions/mass_update.py b/src/adminactions/mass_update.py index 2b6aba5..c6bf7a7 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,6 +167,7 @@ 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) @@ -173,12 +175,14 @@ def __init__(self, *args, **kwargs): 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 +437,8 @@ def _get_sample(): messages.error(request, str(e)) return - mass_update_form = getattr(modeladmin, "mass_update_form", MassUpdateForm) + 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/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/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_bulk_update.py b/tests/test_bulk_update.py index 0ced97f..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 @@ -143,7 +142,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 +164,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..e38a6b4 100644 --- a/tests/test_mass_update.py +++ b/tests/test_mass_update.py @@ -3,16 +3,24 @@ from unittest import skipIf from unittest.mock import patch -from demo.models import DemoModel +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 +from django.test.utils import override_settings from django.urls import reverse from django_dynamic_fixture import G from django_webtest import WebTestMixin 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 @@ -21,11 +29,30 @@ ] -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" @@ -77,6 +104,38 @@ 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()