Skip to content

Commit 5638f99

Browse files
authored
feat: provide action decorator to pass label, description and atts to the admin method (#141)
Add an `@action` decorator that behave's like Django's `admin.action` decorator[^1] to clean up customizing object actions. [closes #115](#115) Also relates to #107 [^1]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/actions/#django.contrib.admin.action
1 parent 10e4743 commit 5638f99

File tree

5 files changed

+139
-23
lines changed

5 files changed

+139
-23
lines changed

README.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ our templates.
2222
In your admin.py:
2323

2424
```python
25-
from django_object_actions import DjangoObjectActions
25+
from django_object_actions import DjangoObjectActions, action
2626

2727
class ArticleAdmin(DjangoObjectActions, admin.ModelAdmin):
28+
@action(label="Publish", description="Submit this article") # optional
2829
def publish_this(self, request, obj):
2930
publish_obj(obj)
30-
publish_this.label = "Publish" # optional
31-
publish_this.short_description = "Submit this article" # optional
3231

3332
change_actions = ('publish_this', )
3433
```
@@ -49,10 +48,12 @@ views too. There, you'll get a queryset like a regular [admin action][admin acti
4948
from django_object_actions import DjangoObjectActions
5049

5150
class MyModelAdmin(DjangoObjectActions, admin.ModelAdmin):
51+
@action(
52+
label="This will be the label of the button", # optional
53+
description="This will be the tooltip of the button" # optional
54+
)
5255
def toolfunc(self, request, obj):
5356
pass
54-
toolfunc.label = "This will be the label of the button" # optional
55-
toolfunc.short_description = "This will be the tooltip of the button" # optional
5657

5758
def make_published(modeladmin, request, queryset):
5859
queryset.update(status='p')
@@ -93,8 +94,18 @@ class RobotAdmin(DjangoObjectActions, admin.ModelAdmin):
9394

9495
### Customizing *Object Actions*
9596

96-
To give the action some a helpful title tooltip, add a
97-
`short_description` attribute, similar to how admin actions work:
97+
To give the action some a helpful title tooltip, you can use the `action` decorator
98+
and set the description argument.
99+
100+
```python
101+
@action(description="Increment the vote count by one")
102+
def increment_vote(self, request, obj):
103+
obj.votes = obj.votes + 1
104+
obj.save()
105+
```
106+
107+
Alternatively, you can also add a `short_description` attribute,
108+
similar to how admin actions work:
98109

99110
```python
100111
def increment_vote(self, request, obj):
@@ -107,6 +118,15 @@ By default, Django Object Actions will guess what to label the button
107118
based on the name of the function. You can override this with a `label`
108119
attribute:
109120

121+
```python
122+
@action(label="Vote++")
123+
def increment_vote(self, request, obj):
124+
obj.votes = obj.votes + 1
125+
obj.save()
126+
```
127+
128+
or
129+
110130
```python
111131
def increment_vote(self, request, obj):
112132
obj.votes = obj.votes + 1
@@ -119,6 +139,15 @@ by adding a Django widget style
119139
[attrs](https://docs.djangoproject.com/en/stable/ref/forms/widgets/#django.forms.Widget.attrs)
120140
attribute:
121141

142+
```python
143+
@action(attrs = {'class': 'addlink'})
144+
def increment_vote(self, request, obj):
145+
obj.votes = obj.votes + 1
146+
obj.save()
147+
```
148+
149+
or
150+
122151
```python
123152
def increment_vote(self, request, obj):
124153
obj.votes = obj.votes + 1

django_object_actions/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
BaseDjangoObjectActions,
88
DjangoObjectActions,
99
takes_instance_or_queryset,
10+
action,
1011
)

django_object_actions/tests/test_utils.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
from django.test import TestCase
44
from example_project.polls.models import Poll
55

6-
from ..utils import BaseDjangoObjectActions, BaseActionView, takes_instance_or_queryset
6+
from ..utils import (
7+
BaseDjangoObjectActions,
8+
BaseActionView,
9+
takes_instance_or_queryset,
10+
action,
11+
)
712

813

914
class BaseDjangoObjectActionsTest(TestCase):
@@ -122,3 +127,37 @@ def myfunc(foo, bar, queryset):
122127
queryset = myfunc(None, None, queryset=self.obj)
123128
# the resulting queryset only has one item and it's self.obj
124129
self.assertEqual(queryset.get(), self.obj)
130+
131+
132+
class DecoratorActionTest(TestCase):
133+
def test_decorated(self):
134+
# setup
135+
@action(description="First action of this admin site.")
136+
def action_1(modeladmin, request, queryset):
137+
pass
138+
139+
@action(permissions=["do_action2"])
140+
def action_2(modeladmin, request, queryset):
141+
pass
142+
143+
@action(label="Third action")
144+
def action_3(modeladmin, request, queryset):
145+
pass
146+
147+
@action(
148+
attrs={
149+
"class": "addlink",
150+
}
151+
)
152+
def action_4(modeladmin, request, queryset):
153+
pass
154+
155+
self.assertEqual(action_1.short_description, "First action of this admin site.")
156+
self.assertEqual(action_2.allowed_permissions, ["do_action2"])
157+
self.assertEqual(action_3.label, "Third action")
158+
self.assertEqual(
159+
action_4.attrs,
160+
{
161+
"class": "addlink",
162+
},
163+
)

django_object_actions/utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,48 @@ def decorated_function(self, request, queryset):
311311
return func(self, request, queryset)
312312

313313
return decorated_function
314+
315+
316+
def action(
317+
function=None, *, permissions=None, description=None, label=None, attrs=None
318+
):
319+
"""
320+
Conveniently add attributes to an action function::
321+
322+
@action(
323+
permissions=['publish'],
324+
description='Mark selected stories as published',
325+
label='Publish'
326+
)
327+
def make_published(self, request, queryset):
328+
queryset.update(status='p')
329+
330+
This is equivalent to setting some attributes (with the original, longer
331+
names) on the function directly::
332+
333+
def make_published(self, request, queryset):
334+
queryset.update(status='p')
335+
make_published.allowed_permissions = ['publish']
336+
make_published.short_description = 'Mark selected stories as published'
337+
make_published.label = 'Publish'
338+
339+
This is the django-object-actions equivalent of
340+
https://docs.djangoproject.com
341+
/en/dev/ref/contrib/admin/actions/#django.contrib.admin.action
342+
"""
343+
344+
def decorator(func):
345+
if permissions is not None:
346+
func.allowed_permissions = permissions
347+
if description is not None:
348+
func.short_description = description
349+
if label is not None:
350+
func.label = label
351+
if attrs is not None:
352+
func.attrs = attrs
353+
return func
354+
355+
if function is None:
356+
return decorator
357+
else:
358+
return decorator(function)

example_project/polls/admin.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from django.http import HttpResponseRedirect
55
from django.urls import reverse
66

7-
from django_object_actions import DjangoObjectActions, takes_instance_or_queryset
7+
from django_object_actions import (
8+
DjangoObjectActions,
9+
takes_instance_or_queryset,
10+
action,
11+
)
812

913
from .models import Choice, Poll, Comment, RelatedData
1014

@@ -15,38 +19,37 @@ class ChoiceAdmin(DjangoObjectActions, admin.ModelAdmin):
1519
# Actions
1620
#########
1721

22+
@action(
23+
description="+1",
24+
label="vote++",
25+
attrs={
26+
"test": '"foo&bar"',
27+
"Robert": '"); DROP TABLE Students; ', # 327
28+
"class": "addlink",
29+
},
30+
)
1831
@takes_instance_or_queryset
1932
def increment_vote(self, request, queryset):
2033
queryset.update(votes=F("votes") + 1)
2134

22-
increment_vote.short_description = "+1"
23-
increment_vote.label = "vote++"
24-
increment_vote.attrs = {
25-
"test": '"foo&bar"',
26-
"Robert": '"); DROP TABLE Students; ', # 327
27-
"class": "addlink",
28-
}
29-
3035
actions = ["increment_vote"]
3136

3237
# Object actions
3338
################
3439

40+
@action(description="-1")
3541
def decrement_vote(self, request, obj):
3642
obj.votes -= 1
3743
obj.save()
3844

39-
decrement_vote.short_description = "-1"
40-
4145
def delete_all(self, request, queryset):
4246
self.message_user(request, "just kidding!")
4347

48+
@action(description="0")
4449
def reset_vote(self, request, obj):
4550
obj.votes = 0
4651
obj.save()
4752

48-
reset_vote.short_description = "0"
49-
5053
def edit_poll(self, request, obj):
5154
url = reverse("admin:polls_poll_change", args=(obj.poll.pk,))
5255
return HttpResponseRedirect(url)
@@ -101,6 +104,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
101104
# Object actions
102105
################
103106

107+
@action(label="Delete All Choices")
104108
def delete_all_choices(self, request, obj):
105109
from django.shortcuts import render
106110

@@ -111,8 +115,6 @@ def delete_all_choices(self, request, obj):
111115
self.message_user(request, "All choices deleted")
112116
return render(request, "clear_choices.html", {"object": obj})
113117

114-
delete_all_choices.label = "Delete All Choices"
115-
116118
def question_mark(self, request, obj):
117119
"""Add a question mark."""
118120
obj.question = obj.question + "?"

0 commit comments

Comments
 (0)