Skip to content

Commit 1274ae7

Browse files
authored
wip to select GET or POST for actions (#168)
Another try at enforcing POST actions. This change is more gradual than #149 - when library user doesn't change default options the behavior is exactly the same as before the change, that is: 1. Action buttons send GET requests 2. Action handlers accept GET and POST requests However, user can change this behavior using `methods` and `button_type` kwargs. For example `@action(methods=['POST'], button_type='form')` results in 1. Action button sends POST requests 2. Action handler accepts only POST request Unfortunately I have this tested only within my project. Also the docs are missing. And one more thing - I think it is better to use `<input type="submit">` instead of js to submit the form. This js is need to make the buttons look the same in both versions. With proper CSS (that is beyond my ability to write ;) ) js is avoidable and we could be using pretty semantic html submit button. I took the form button template from #149.
1 parent 813687e commit 1274ae7

File tree

4 files changed

+35
-22
lines changed

4 files changed

+35
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% load add_preserved_filters from admin_urls %}
2+
3+
{% if tool.button_type == 'a' %}
4+
<a href="{% add_preserved_filters action_url %}"
5+
title="{{ tool.standard_attrs.title }}"
6+
{% for k, v in tool.custom_attrs.items %}
7+
{{ k }}="{{ v }}"
8+
{% endfor %}
9+
class="{{ tool.standard_attrs.class }}"
10+
>{{ tool.label|capfirst }}</a>
11+
{% elif tool.button_type == 'form' %}
12+
<form method="post" action="{% add_preserved_filters action_url %}">
13+
{% csrf_token %}
14+
<a href="#" onclick="this.parentNode.submit(); return false;"
15+
title="{{ tool.standard_attrs.title }}"
16+
{% for k, v in tool.custom_attrs.items %}
17+
{{ k }}="{{ v }}"
18+
{% endfor %}
19+
class="{{ tool.standard_attrs.class }}"
20+
>{{ tool.label|capfirst }}</a>
21+
</form>
22+
{% endif %}

django_object_actions/templates/django_object_actions/change_form.html

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
{% extends "admin/change_form.html" %}
2-
{% load add_preserved_filters from admin_urls %}
32

43
{% block object-tools-items %}
54
{% for tool in objectactions %}
65
<li class="objectaction-item" data-tool-name="{{ tool.name }}">
76
{% 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>
7+
{% include 'django_object_actions/action_trigger.html' %}
158
</li>
169
{% endfor %}
1710
{{ block.super }}

django_object_actions/templates/django_object_actions/change_list.html

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
{% extends "admin/change_list.html" %}
2-
{% load add_preserved_filters from admin_urls %}
32

43
{% block object-tools-items %}
54
{% for tool in objectactions %}
65
<li class="objectaction-item" data-tool-name="{{ tool.name }}">
76
{% 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>
7+
{% include 'django_object_actions/action_trigger.html' %}
158
</li>
169
{% endfor %}
1710
{{ block.super }}

django_object_actions/utils.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.contrib.admin.utils import unquote
66
from django.db.models.query import QuerySet
77
from django.http import Http404, HttpResponseRedirect
8-
from django.http.response import HttpResponseBase
8+
from django.http.response import HttpResponseBase, HttpResponseNotAllowed
99
from django.views.generic import View
1010
from django.views.generic.detail import SingleObjectMixin
1111
from django.views.generic.list import MultipleObjectMixin
@@ -159,6 +159,7 @@ def _get_tool_dict(self, tool_name):
159159
label=getattr(tool, "label", tool_name.replace("_", " ").capitalize()),
160160
standard_attrs=standard_attrs,
161161
custom_attrs=custom_attrs,
162+
button_type=tool.button_type,
162163
)
163164

164165
def _get_button_attrs(self, tool):
@@ -238,7 +239,7 @@ def back_url(self):
238239
"""
239240
raise NotImplementedError
240241

241-
def get(self, request, tool, **kwargs):
242+
def dispatch(self, request, tool, **kwargs):
242243
# Fix for case if there are special symbols in object pk
243244
for k, v in self.kwargs.items():
244245
self.kwargs[k] = unquote(v)
@@ -248,15 +249,15 @@ def get(self, request, tool, **kwargs):
248249
except KeyError:
249250
raise Http404("Action does not exist")
250251

252+
if request.method not in view.methods:
253+
return HttpResponseNotAllowed(view.methods)
254+
251255
ret = view(request, *self.view_args)
252256
if isinstance(ret, HttpResponseBase):
253257
return ret
254258

255259
return HttpResponseRedirect(self.back_url)
256260

257-
# HACK to allow POST requests too
258-
post = get
259-
260261
def message_user(self, request, message):
261262
"""
262263
Mimic Django admin actions's `message_user`.
@@ -314,7 +315,9 @@ def decorated_function(self, request, queryset):
314315

315316

316317
def action(
317-
function=None, *, permissions=None, description=None, label=None, attrs=None
318+
function=None, *, permissions=None, description=None, label=None, attrs=None,
319+
methods=('GET', 'POST'),
320+
button_type='a',
318321
):
319322
"""
320323
Conveniently add attributes to an action function:
@@ -349,6 +352,8 @@ def decorator(func):
349352
func.label = label
350353
if attrs is not None:
351354
func.attrs = attrs
355+
func.methods = methods
356+
func.button_type = button_type
352357
return func
353358

354359
if function is None:

0 commit comments

Comments
 (0)