Skip to content

Commit a616d37

Browse files
author
Wiktor Latanowicz
committed
Invoke actions on POST only
1 parent 3e42b3b commit a616d37

File tree

9 files changed

+57
-39
lines changed

9 files changed

+57
-39
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

33
<!--next-version-placeholder-->
4+
### Feature
5+
* Drop support for GET method. All action are now invoked with POST method.
6+
7+
### Breaking
8+
* When dealing with a secondary form in action, you cannot simply check the http method to determine if the form should be rendered or processed. You need to check for specific form inputs in POST payload.
49

510
## v4.1.0 (2022-11-14)
611
### Feature

django_object_actions/templates/django_object_actions/change_form.html

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
{% for tool in objectactions %}
66
<li class="objectaction-item" data-tool-name="{{ tool.name }}">
77
{% url tools_view_name pk=object_id tool=tool.name as action_url %}
8-
<a href="{% add_preserved_filters action_url %}" title="{{ tool.standard_attrs.title }}"
9-
{% for k, v in tool.custom_attrs.items %}
10-
{{ k }}="{{ v }}"
11-
{% endfor %}
12-
class="{{ tool.standard_attrs.class }}">
13-
{{ tool.label|capfirst }}
14-
</a>
8+
<form name="{{ tool.name }}__form" method="post" action="{% add_preserved_filters action_url %}">
9+
{% csrf_token %}
10+
<a href="javascript:window.document.forms['{{ tool.name }}__form'].submit()" title="{{ tool.standard_attrs.title }}"
11+
{% for k, v in tool.custom_attrs.items %}
12+
{{ k }}="{{ v }}"
13+
{% endfor %}
14+
class="{{ tool.standard_attrs.class }}">
15+
{{ tool.label|capfirst }}
16+
</a>
17+
</form>
1518
</li>
1619
{% endfor %}
1720
{{ block.super }}

django_object_actions/templates/django_object_actions/change_list.html

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
{% for tool in objectactions %}
66
<li class="objectaction-item" data-tool-name="{{ tool.name }}">
77
{% url tools_view_name tool=tool.name as action_url %}
8-
<a href="{% add_preserved_filters action_url %}" title="{{ tool.standard_attrs.title }}"
9-
{% for k, v in tool.custom_attrs.items %}
10-
{{ k }}="{{ v }}"
11-
{% endfor %}
12-
class="{{ tool.standard_attrs.class }}">
13-
{{ tool.label|capfirst }}
14-
</a>
8+
<form name="{{ tool.name }}__form" method="post" action="{% add_preserved_filters action_url %}">
9+
{% csrf_token %}
10+
<a href="javascript:window.document.forms['{{ tool.name }}__form'].submit()" title="{{ tool.standard_attrs.title }}"
11+
{% for k, v in tool.custom_attrs.items %}
12+
{{ k }}="{{ v }}"
13+
{% endfor %}
14+
class="{{ tool.standard_attrs.class }}">
15+
{{ tool.label|capfirst }}
16+
</a>
17+
</form>
1518
</li>
1619
{% endfor %}
1720
{{ block.super }}

django_object_actions/tests/test_admin.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,25 @@ def test_action_on_a_model_with_uuid_pk_works(self):
2121
action_url = "/admin/polls/comment/{0}/actions/hodor/".format(comment.pk)
2222
# sanity check that url has a uuid
2323
self.assertIn("-", action_url)
24-
response = self.client.get(action_url)
24+
response = self.client.post(action_url)
2525
self.assertRedirects(response, comment_url)
2626

27-
@patch("django_object_actions.utils.ChangeActionView.get")
27+
@patch("django_object_actions.utils.ChangeActionView.post")
2828
def test_action_on_a_model_with_arbitrary_pk_works(self, mock_view):
2929
mock_view.return_value = HttpResponse()
3030
action_url = "/admin/polls/comment/{0}/actions/hodor/".format(" i am a pk ")
3131

32-
self.client.get(action_url)
32+
self.client.post(action_url)
3333

3434
self.assertTrue(mock_view.called)
3535
self.assertEqual(mock_view.call_args[1]["pk"], " i am a pk ")
3636

37-
@patch("django_object_actions.utils.ChangeActionView.get")
37+
@patch("django_object_actions.utils.ChangeActionView.post")
3838
def test_action_on_a_model_with_slash_in_pk_works(self, mock_view):
3939
mock_view.return_value = HttpResponse()
4040
action_url = "/admin/polls/comment/{0}/actions/hodor/".format("pk/slash")
4141

42-
self.client.get(action_url)
42+
self.client.post(action_url)
4343

4444
self.assertTrue(mock_view.called)
4545
self.assertEqual(mock_view.call_args[1]["pk"], "pk/slash")
@@ -55,7 +55,7 @@ def test_action_on_a_model_with_complex_id(self):
5555
quote(related_data.pk)
5656
)
5757

58-
response = self.client.get(action_url)
58+
response = self.client.post(action_url)
5959
self.assertNotEqual(response.status_code, 404)
6060
self.assertRedirects(response, related_data_url)
6161

@@ -76,12 +76,12 @@ def test_changelist_template_context(self):
7676

7777
def test_changelist_action_view(self):
7878
url = "/admin/polls/choice/actions/delete_all/"
79-
response = self.client.get(url)
79+
response = self.client.post(url)
8080
self.assertRedirects(response, "/admin/polls/choice/")
8181

8282
def test_changelist_nonexistent_action(self):
8383
url = "/admin/polls/choice/actions/xyzzy/"
84-
response = self.client.get(url)
84+
response = self.client.post(url)
8585
self.assertEqual(response.status_code, 404)
8686

8787
def test_get_changelist_can_remove_action(self):
@@ -94,13 +94,23 @@ def test_get_changelist_can_remove_action(self):
9494
response = self.client.get(admin_change_url)
9595
self.assertIn(action_url, response.rendered_content)
9696

97-
response = self.client.get(action_url) # Click on the button
97+
response = self.client.post(action_url) # Click on the button
9898
self.assertRedirects(response, admin_change_url)
9999

100100
# button is not in the admin anymore
101101
response = self.client.get(admin_change_url)
102102
self.assertNotIn(action_url, response.rendered_content)
103103

104+
def test_changelist_get_method_action_view(self):
105+
url = "/admin/polls/choice/actions/delete_all/"
106+
response = self.client.get(url)
107+
self.assertEqual(response.status_code, 405)
108+
109+
def test_changelist_get_method_nonexistent_action(self):
110+
url = "/admin/polls/choice/actions/xyzzy/"
111+
response = self.client.get(url)
112+
self.assertEqual(response.status_code, 405)
113+
104114

105115
class ChangeListTests(LoggedInTestCase):
106116
def test_changelist_template_context(self):
@@ -122,5 +132,5 @@ def test_redirect_back_from_secondary_admin(self):
122132
action_url = "/support/polls/poll/1/actions/question_mark/"
123133
self.assertTrue(admin_change_url.startswith("/support/"))
124134

125-
response = self.client.get(action_url)
135+
response = self.client.post(action_url)
126136
self.assertRedirects(response, admin_change_url)

django_object_actions/tests/test_urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ def test_changelist_action_is_rendered(self):
1111
response = self.client.get(reverse("admin:polls_choice_changelist"))
1212
self.assertEqual(response.status_code, 200)
1313
self.assertIn(
14-
b'href="/admin/polls/choice/actions/delete_all/"', response.content
14+
b'action="/admin/polls/choice/actions/delete_all/"', response.content
1515
)

django_object_actions/tests/tests.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class AppTests(LoggedInTestCase):
2222
def test_tool_func_gets_executed(self):
2323
c = Choice.objects.get(pk=1)
2424
votes = c.votes
25-
response = self.client.get(
25+
response = self.client.post(
2626
reverse("admin:polls_choice_actions", args=(1, "increment_vote"))
2727
)
2828
self.assertEqual(response.status_code, 302)
@@ -34,7 +34,7 @@ def test_tool_func_gets_executed(self):
3434
def test_tool_can_return_httpresponse(self):
3535
# we know this url works because of fixtures
3636
url = reverse("admin:polls_choice_actions", args=(2, "edit_poll"))
37-
response = self.client.get(url)
37+
response = self.client.post(url)
3838
# we expect a redirect
3939
self.assertEqual(response.status_code, 302)
4040
self.assertTrue(
@@ -45,35 +45,35 @@ def test_can_return_template(self):
4545
# This is more of a test of render_to_response than the app, but I think
4646
# it's good to document that this is something we can do.
4747
url = reverse("admin:polls_poll_actions", args=(1, "delete_all_choices"))
48-
response = self.client.get(url)
48+
response = self.client.post(url)
4949
self.assertTemplateUsed(response, "clear_choices.html")
5050

5151
def test_message_user_sends_message(self):
5252
url = reverse("admin:polls_poll_actions", args=(1, "delete_all_choices"))
5353
self.assertNotIn("messages", self.client.cookies)
54-
self.client.get(url)
54+
self.client.post(url)
5555
self.assertIn("messages", self.client.cookies)
5656

5757
def test_intermediate_page_with_post_works(self):
5858
self.assertTrue(Choice.objects.filter(poll=1).count())
5959
url = reverse("admin:polls_poll_actions", args=(1, "delete_all_choices"))
60-
response = self.client.post(url)
60+
response = self.client.post(url, data={"sure": "Sure"})
6161
self.assertEqual(response.status_code, 302)
6262
self.assertEqual(Choice.objects.filter(poll=1).count(), 0)
6363

6464
def test_undefined_tool_404s(self):
65-
response = self.client.get(
65+
response = self.client.post(
6666
reverse("admin:polls_poll_actions", args=(1, "weeeewoooooo"))
6767
)
6868
self.assertEqual(response.status_code, 404)
6969

7070
def test_key_error_tool_500s(self):
7171
self.assertRaises(
7272
KeyError,
73-
self.client.get,
73+
self.client.post,
7474
reverse("admin:polls_choice_actions", args=(1, "raise_key_error")),
7575
)
7676

7777
def test_render_button(self):
78-
response = self.client.get(reverse("admin:polls_choice_change", args=(1,)))
78+
response = self.client.post(reverse("admin:polls_choice_change", args=(1,)))
7979
self.assertEqual(response.status_code, 200)

django_object_actions/utils.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def back_url(self):
238238
"""
239239
raise NotImplementedError
240240

241-
def get(self, request, tool, **kwargs):
241+
def post(self, request, tool, **kwargs):
242242
# Fix for case if there are special symbols in object pk
243243
for k, v in self.kwargs.items():
244244
self.kwargs[k] = unquote(v)
@@ -254,9 +254,6 @@ def get(self, request, tool, **kwargs):
254254

255255
return HttpResponseRedirect(self.back_url)
256256

257-
# HACK to allow POST requests too
258-
post = get
259-
260257
def message_user(self, request, message):
261258
"""
262259
Mimic Django admin actions's `message_user`.

example_project/polls/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
108108
def delete_all_choices(self, request, obj):
109109
from django.shortcuts import render
110110

111-
if request.method == "POST":
111+
if "sure" in request.POST:
112112
obj.choice_set.all().delete()
113113
return
114114

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<form action="." method="POST">
22
{% csrf_token %}
33
Delete All Choices?
4-
<input type=submit value="Sure">
4+
<input type=submit value="Sure" name="sure">
55
</form>

0 commit comments

Comments
 (0)