Skip to content

Commit 0c90ce1

Browse files
authored
fix: Objects with special symbols in primary key 404-ed (#110)
for case if object in database has any of special symbols https://github.com/django/django/blob/master/django/contrib/admin/utils.py#L17 clicking on action button causes 404 error, as in SingleObjectMixin there are already parsed kwargs from url, and they are unquoted made unquoting kwargs
1 parent 6b61513 commit 0c90ce1

File tree

7 files changed

+115
-3
lines changed

7 files changed

+115
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ __pycache__
55
.tox/
66
*.db
77
*.sqlite
8-
8+
.venv
9+
.idea
910

1011
# Private files
1112
.env

django_object_actions/tests/test_admin.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Integration tests that actually try and use the tools setup in admin.py
33
"""
4+
from django.contrib.admin.utils import quote
45
from django.http import HttpResponse
56
from mock import patch
67

@@ -10,7 +11,11 @@
1011
from django.core.urlresolvers import reverse # < DJANGO1.10
1112

1213
from .tests import LoggedInTestCase
13-
from example_project.polls.factories import CommentFactory, PollFactory
14+
from example_project.polls.factories import (
15+
CommentFactory,
16+
PollFactory,
17+
RelatedDataFactory,
18+
)
1419

1520

1621
class CommentTests(LoggedInTestCase):
@@ -44,6 +49,21 @@ def test_action_on_a_model_with_slash_in_pk_works(self, mock_view):
4449
self.assertEqual(mock_view.call_args[1]["pk"], "pk/slash")
4550

4651

52+
class ExtraTests(LoggedInTestCase):
53+
def test_action_on_a_model_with_complex_id(self):
54+
related_data = RelatedDataFactory()
55+
related_data_url = reverse(
56+
"admin:polls_relateddata_change", args=(related_data.pk,)
57+
)
58+
action_url = "/admin/polls/relateddata/{}/actions/fill_up/".format(
59+
quote(related_data.pk)
60+
)
61+
62+
response = self.client.get(action_url)
63+
self.assertNotEqual(response.status_code, 404)
64+
self.assertRedirects(response, related_data_url)
65+
66+
4767
class ChangeTests(LoggedInTestCase):
4868
def test_buttons_load(self):
4969
url = "/admin/polls/choice/"

django_object_actions/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.conf.urls import url
55
from django.contrib import messages
6+
from django.contrib.admin.utils import unquote
67
from django.db.models.query import QuerySet
78
from django.http import Http404, HttpResponseRedirect
89
from django.http.response import HttpResponseBase
@@ -243,6 +244,10 @@ def back_url(self):
243244
raise NotImplementedError
244245

245246
def get(self, request, tool, **kwargs):
247+
# Fix for case if there are special symbols in object pk
248+
for k, v in self.kwargs.items():
249+
self.kwargs[k] = unquote(v)
250+
246251
try:
247252
view = self.actions[tool]
248253
except KeyError:

example_project/polls/admin.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from django_object_actions import DjangoObjectActions, takes_instance_or_queryset
1212

13-
from .models import Choice, Poll, Comment
13+
from .models import Choice, Poll, Comment, RelatedData
1414

1515

1616
class ChoiceAdmin(DjangoObjectActions, admin.ModelAdmin):
@@ -160,5 +160,24 @@ def hodor(self, request, obj):
160160
admin.site.register(Comment, CommentAdmin)
161161

162162

163+
class RelatedDataAdmin(DjangoObjectActions, admin.ModelAdmin):
164+
165+
# Object actions
166+
################
167+
168+
def fill_up(self, request, obj):
169+
if not obj.extra_data:
170+
# bail because we need a comment
171+
obj.extra_data = "hodor"
172+
else:
173+
obj.extra_data = ""
174+
obj.save()
175+
176+
change_actions = ("fill_up",)
177+
178+
179+
admin.site.register(RelatedData, RelatedDataAdmin)
180+
181+
163182
support_admin = AdminSite(name="support")
164183
support_admin.register(Poll, PollAdmin)

example_project/polls/factories.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import random
2+
import string
3+
14
import factory
25
from django.contrib.auth import get_user_model
36
from django.utils import timezone
@@ -36,3 +39,23 @@ class Meta:
3639
class CommentFactory(factory.DjangoModelFactory):
3740
class Meta:
3841
model = models.Comment
42+
43+
44+
def get_random_string(length):
45+
letters = string.ascii_lowercase
46+
result_str = "".join(random.choice(letters) for i in range(length))
47+
return result_str
48+
49+
50+
class RelatedDataFactory(factory.DjangoModelFactory):
51+
id = factory.lazy_attribute(
52+
lambda __: "{}:{}-{}!{}".format(
53+
get_random_string(2),
54+
get_random_string(2),
55+
get_random_string(2),
56+
get_random_string(2),
57+
)
58+
)
59+
60+
class Meta:
61+
model = models.RelatedData
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Generated by Django 3.1 on 2020-08-05 02:39
2+
3+
from django.db import migrations, models
4+
import uuid
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("polls", "0001_initial"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="RelatedData",
16+
fields=[
17+
(
18+
"id",
19+
models.CharField(max_length=32, primary_key=True, serialize=False),
20+
),
21+
("extra_data", models.TextField(blank=True, default="")),
22+
],
23+
),
24+
migrations.AlterField(
25+
model_name="comment",
26+
name="uuid",
27+
field=models.UUIDField(
28+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
29+
),
30+
),
31+
migrations.AlterField(
32+
model_name="poll",
33+
name="pub_date",
34+
field=models.DateTimeField(verbose_name="date published"),
35+
),
36+
]

example_project/polls/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ class Comment(models.Model):
3535

3636
def __str__(self):
3737
return self.comment or ""
38+
39+
40+
class RelatedData(models.Model):
41+
id = models.CharField(primary_key=True, max_length=32)
42+
extra_data = models.TextField(blank=True, default="")
43+
44+
def __str__(self):
45+
return self.extra_data or self.id

0 commit comments

Comments
 (0)