Skip to content

Commit

Permalink
Merge branch 'feature/mass_update_enh' into develop
Browse files Browse the repository at this point in the history
* feature/mass_update_enh:
  lint
  lint
  updates tests
  lint
  lint
  lint
  updates
  • Loading branch information
saxix committed Aug 23, 2023
2 parents b843d48 + 6dd26f7 commit e554428
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 71 deletions.
7 changes: 7 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
Release <dev>
=============
* 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"
Expand Down
17 changes: 8 additions & 9 deletions src/adminactions/bulk_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/adminactions/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
19 changes: 12 additions & 7 deletions src/adminactions/mass_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _

Expand Down Expand Up @@ -166,19 +167,22 @@ 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())
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/adminactions/static/adminactions/css/bulkupdate.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions tests/demo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.db import models

from adminactions.helpers import AdminActionPermMixin
from adminactions.mass_update import MassUpdateForm


class SubclassedImageField(models.ImageField):
Expand Down Expand Up @@ -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)
105 changes: 52 additions & 53 deletions tests/test_bulk_update.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"]
Expand All @@ -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()
63 changes: 61 additions & 2 deletions tests/test_mass_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit e554428

Please sign in to comment.