Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UUID bulk update #224

Merged
merged 5 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/source/_ext/djangodocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,13 @@ def finish(self):
templatebuiltins = {
"ttags": [
n
for ((t, n), (l, a)) in xrefs.items()
if t == "templatetag" and l == "ref/templates/builtins"
for ((t, n), (l, a)) in xrefs.items() # noqa
if t == "templatetag" and l == "ref/templates/builtins" # noqa
],
"tfilters": [
n
for ((t, n), (l, a)) in xrefs.items()
if t == "templatefilter" and l == "ref/templates/builtins"
for ((t, n), (l, a)) in xrefs.items() # noqa
if t == "templatefilter" and l == "ref/templates/builtins" # noqa
],
}
outfilename = os.path.join(self.outdir, "templatebuiltins.js")
Expand Down
20 changes: 17 additions & 3 deletions src/adminactions/bulk_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.core.exceptions import ValidationError
from django.core.files.utils import FileProxyMixin
from django.core.validators import FileExtensionValidator
from django.db import models
from django.db.transaction import atomic
from django.forms import Media
from django.http import HttpResponseRedirect
Expand Down Expand Up @@ -260,13 +261,12 @@

if header:
reader = csv.DictReader(codecs.iterdecode(f, "utf-8"), **(csv_options or {}))
for k, v in mapping.items():
for _k, v in mapping.items():
if v not in reader.fieldnames:
raise ValidationError(_("%s column is not present in the file") % v)
else:
reader = csv.reader(codecs.iterdecode(f, "utf-8"), **(csv_options or {}))
mapping = {k: int(v) - 1 for k, v in mapping.items()}

reverse = {v: k for k, v in mapping.items()}
with atomic():
for i, row in enumerate(reader, 1):
Expand All @@ -278,7 +278,21 @@
for colname, value in row.items():
field = reverse[colname]
if field not in indexes:
changes[field] = [getattr(obj, field), value]
model_field = queryset.model._meta.get_field(field)
if model_field.is_relation and model_field.many_to_one:
related_model = model_field.related_model
related_field_name = (
model_field.to_fields[0] if model_field.to_fields else "pk"
)
related_field = related_model._meta.get_field(related_field_name)

if isinstance(related_field, models.UUIDField):
try:
value = related_model.objects.get(**{related_field_name: value})
except related_model.DoesNotExist:
raise ValidationError(

Check warning on line 293 in src/adminactions/bulk_update.py

View check run for this annotation

Codecov / codecov/patch

src/adminactions/bulk_update.py#L292-L293

Added lines #L292 - L293 were not covered by tests
f"No instance of {related_model._meta.verbose_name} found with {related_field_name} = {value}"
)
setattr(obj, field, value)
else:
for i, value in enumerate(row):
Expand Down
29 changes: 29 additions & 0 deletions tests/demo/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Generated by Django 2.0.1 on 2018-01-29 00:00

import uuid

import demo.models
import django.db.models.deletion
from django.conf import settings
Expand Down Expand Up @@ -39,6 +41,10 @@ class Migration(migrations.Migration):
("generic_ip", models.GenericIPAddressField()),
("url", models.URLField()),
("text", models.TextField()),
(
"uuid",
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
),
("unique", models.CharField(max_length=255, unique=True)),
("nullable", models.CharField(max_length=255, null=True)),
("blank", models.CharField(blank=True, max_length=255, null=True)),
Expand Down Expand Up @@ -82,6 +88,29 @@ class Migration(migrations.Migration):
),
],
),
migrations.CreateModel(
name="DemoRelated",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"demo",
models.ForeignKey(
on_delete=models.deletion.CASCADE,
to="demo.DemoModel",
related_name="related",
to_field="uuid",
),
),
],
),
migrations.CreateModel(
name="UserDetail",
fields=[
Expand Down
15 changes: 15 additions & 0 deletions tests/demo/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import uuid

from admin_extra_urls.api import button
from admin_extra_urls.mixins import ExtraUrlMixin
from django.contrib.admin import ModelAdmin, site
Expand Down Expand Up @@ -29,6 +31,7 @@ class DemoModel(models.Model):
generic_ip = models.GenericIPAddressField()
url = models.URLField()
text = models.TextField()
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)

unique = models.CharField(max_length=255, unique=True)
nullable = models.CharField(max_length=255, null=True)
Expand Down Expand Up @@ -59,6 +62,13 @@ class Meta:
app_label = "demo"


class DemoRelated(models.Model):
demo = models.ForeignKey(DemoModel, on_delete=models.CASCADE, related_name="related", to_field="uuid")

class Meta:
app_label = "demo"


class UserDetailModelAdmin(ExtraUrlMixin, ModelAdmin):
list_display = [f.name for f in UserDetail._meta.fields]

Expand Down Expand Up @@ -88,6 +98,10 @@ class DemoOneToOneAdmin(ExtraUrlMixin, AdminActionPermMixin, ModelAdmin):
pass


class DemoRelatedAdmin(ExtraUrlMixin, AdminActionPermMixin, ModelAdmin):
pass


class TestMassUpdateForm(MassUpdateForm):
pass

Expand All @@ -98,4 +112,5 @@ class DemoModelMassUpdateForm(MassUpdateForm):

site.register(DemoModel, DemoModelAdmin)
site.register(DemoOneToOne, DemoOneToOneAdmin)
site.register(DemoRelated, DemoRelatedAdmin)
site.register(UserDetail, UserDetailModelAdmin)
77 changes: 76 additions & 1 deletion tests/test_bulk_update.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import csv
from pathlib import Path

from demo.models import DemoModel
from demo.models import DemoModel, DemoOneToOne, DemoRelated
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase
Expand All @@ -23,6 +23,9 @@ class BulkUpdate(SelectRowsMixin, CheckSignalsMixin, WebTestMixin):
csrf_checks = True

_selected_rows = [0, 1]
_selectedr_rows = [
0,
]

action_name = "bulk_update"
sender_model = DemoModel
Expand Down Expand Up @@ -64,6 +67,37 @@ def _run_action(self, steps=2, **kwargs):
res = res.forms["bulk-update"].submit("apply")
return res

def _run_action_related_model(self, steps=2, **kwargs):
selected_rows = kwargs.pop("selected_rows", self._selectedr_rows)
select_across = kwargs.pop("select_across", False)
with user_grant_permission(
self.user,
["demo.change_demorelated", "demo.adminactions_bulkupdate_demorelated"],
):
res = self.app.get("/", user="user", auto_follow=False)
res = res.click("Demo related")
# print(res)
if steps >= 1:
form = res.forms["changelist-form"]
form["action"] = "bulk_update"
form["select_across"] = select_across
self._select_rows(form, selected_rows)
res = form.submit()
if steps >= 2:
res.forms["bulk-update"]["_file"] = Upload(
str(Path(__file__).parent / "related_model_bulk_update.csv")
)
res.forms["bulk-update"]["fld-id"] = "id"
res.forms["bulk-update"]["fld-index_field"] = ["id"]
res.forms["bulk-update"]["fld-demo"] = "demo_uuid"
res.forms["bulk-update"]["csv-delimiter"] = ","
res.forms["bulk-update"]["csv-quoting"] = csv.QUOTE_NONE

for k, v in kwargs.items():
res.forms["bulk-update"][k] = v
res = res.forms["bulk-update"].submit("apply")
return res

def test_simulate(self):
res = self._run_action(
**{
Expand Down Expand Up @@ -184,6 +218,47 @@ 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_bulk_update_with_one_to_one_field(self):
demo_model_instance = G(DemoModel, char="InitialValue", integer=123)
demo_one_to_one_instance = G(DemoOneToOne, demo=demo_model_instance)
csv_data = f"pk,one_to_one_id\n{demo_model_instance.pk},{demo_one_to_one_instance.pk}"
res = self._run_action(
**{
"_file": Upload(
"data.csv",
csv_data.encode(),
"text/csv",
),
"fld-onetoone": "one_to_one_id",
}
)
self.assertTrue(
DemoModel.objects.filter(pk=demo_model_instance.pk, onetoone=demo_one_to_one_instance).exists()
)
self.assertEqual(res.status_code, 200)

def test_bulk_update_with_foreign_key(self):
demo_model_instance = G(DemoModel, char="InitialValue", integer=123)
demo_related_instance = G(DemoRelated, demo=demo_model_instance)
new_demo_model_instance = G(DemoModel, char="NewValue", integer=456)
csv_data = f"id,demo_uuid\n{demo_related_instance.pk},{new_demo_model_instance.uuid}"

res = self._run_action_related_model(
**{
"_file": Upload(
"data.csv",
csv_data.encode(),
"text/csv",
),
"fld-demo": "demo_uuid",
}
)

self.assertTrue(
DemoRelated.objects.filter(pk=demo_related_instance.pk, demo=new_demo_model_instance).exists()
)
self.assertEqual(res.status_code, 200)


class BulkUpdateMemoryFileUploadHandlerTest(BulkUpdate, TestCase):
handler = "django.core.files.uploadhandler.MemoryFileUploadHandler"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def test_permission_needed(app, admin, demomodels, action):

@pytest.mark.django_db()
def test_permissions(admin):
assert Permission.objects.filter(codename__startswith="adminactions").count() == 63
assert Permission.objects.filter(codename__startswith="adminactions").count() == 70

with user_grant_permission(admin, ["demo.adminactions_export_demomodel"]):
assert admin.get_all_permissions() == set(["demo.adminactions_export_demomodel"])
Loading