mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Made changes asked in review by sarahboyce:
- Action gets queryset of 1 object as param when is change view - Add permissions test - Add documentation
This commit is contained in:
parent
3cef98620d
commit
e1fd42c4f8
@ -1681,7 +1681,16 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
if isinstance(response, HttpResponseBase):
|
if isinstance(response, HttpResponseBase):
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
# If action was executed from change view, redirect to
|
||||||
|
# list view once finished.
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
request.get_full_path()
|
||||||
|
if not change
|
||||||
|
else reverse(
|
||||||
|
"admin:%s_%s_changelist"
|
||||||
|
% (self.opts.app_label, self.opts.model_name)
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
msg = _("No action selected.")
|
msg = _("No action selected.")
|
||||||
self.message_user(request, msg, messages.WARNING)
|
self.message_user(request, msg, messages.WARNING)
|
||||||
@ -1873,12 +1882,15 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
if actions and request.POST.get("action", ""):
|
if actions and request.POST.get("action", ""):
|
||||||
action_failed = False
|
action_failed = False
|
||||||
response = self.response_action(request, obj, change=not add)
|
response = self.response_action(
|
||||||
|
request,
|
||||||
|
self.get_queryset(request).filter(id=object_id),
|
||||||
|
change=not add,
|
||||||
|
)
|
||||||
if response:
|
if response:
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
action_failed = True
|
action_failed = True
|
||||||
|
|
||||||
if action_failed:
|
if action_failed:
|
||||||
# Redirect back to the changelist page to avoid resubmitting the
|
# Redirect back to the changelist page to avoid resubmitting the
|
||||||
# form if the user refreshes the browser or uses the "No, take
|
# form if the user refreshes the browser or uses the "No, take
|
||||||
|
@ -86,14 +86,3 @@ def admin_actions_tag(parser, token):
|
|||||||
return InclusionAdminNode(
|
return InclusionAdminNode(
|
||||||
parser, token, func=admin_actions, template_name="actions.html"
|
parser, token, func=admin_actions, template_name="actions.html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@register.tag(name="change_list_object_tools")
|
|
||||||
def change_list_object_tools_tag(parser, token):
|
|
||||||
"""Display the row of change list object tools."""
|
|
||||||
return InclusionAdminNode(
|
|
||||||
parser,
|
|
||||||
token,
|
|
||||||
func=lambda context: context,
|
|
||||||
template_name="change_list_object_tools.html",
|
|
||||||
)
|
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 43 KiB |
@ -149,6 +149,24 @@ That code will give us an admin change list that looks something like this:
|
|||||||
|
|
||||||
.. image:: _images/adding-actions-to-the-modeladmin.png
|
.. image:: _images/adding-actions-to-the-modeladmin.png
|
||||||
|
|
||||||
|
The actions will be available also in the detail view but the description should be
|
||||||
|
adjusted to make sense we are talking about only one object::
|
||||||
|
|
||||||
|
@admin.action(
|
||||||
|
description="Mark selected story as published",
|
||||||
|
description_plural="Mark selected stories as published",
|
||||||
|
)
|
||||||
|
def make_published(modeladmin, request, queryset):
|
||||||
|
pass
|
||||||
|
|
||||||
|
It will looks like this:
|
||||||
|
|
||||||
|
.. image:: _images/adding-actions-to-the-modeladmin-detail-view.png
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In this case ``queryset`` will be a :class:`~django.db.models.query.QuerySet` of one object.
|
||||||
|
|
||||||
That's really all there is to it! If you're itching to write your own actions,
|
That's really all there is to it! If you're itching to write your own actions,
|
||||||
you now know enough to get started. The rest of this document covers more
|
you now know enough to get started. The rest of this document covers more
|
||||||
advanced techniques.
|
advanced techniques.
|
||||||
|
@ -6,6 +6,7 @@ from django import forms
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.admin import BooleanFieldListFilter
|
from django.contrib.admin import BooleanFieldListFilter
|
||||||
from django.contrib.admin.views.main import ChangeList
|
from django.contrib.admin.views.main import ChangeList
|
||||||
|
from django.contrib.auth import get_permission_codename
|
||||||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
||||||
from django.contrib.auth.models import Group, User
|
from django.contrib.auth.models import Group, User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -416,11 +417,10 @@ def redirect_to(modeladmin, request, selected):
|
|||||||
description_plural="Download selected subscriptions",
|
description_plural="Download selected subscriptions",
|
||||||
)
|
)
|
||||||
def download(modeladmin, request, selected):
|
def download(modeladmin, request, selected):
|
||||||
from django.db.models.query import QuerySet
|
if selected.count() > 1:
|
||||||
|
|
||||||
if isinstance(selected, QuerySet):
|
|
||||||
buf = StringIO("This is the content of the file")
|
buf = StringIO("This is the content of the file")
|
||||||
else:
|
else:
|
||||||
|
selected = selected.get()
|
||||||
buf = StringIO(f"This is the content of the file written by {selected.name}")
|
buf = StringIO(f"This is the content of the file written by {selected.name}")
|
||||||
|
|
||||||
return StreamingHttpResponse(FileWrapper(buf))
|
return StreamingHttpResponse(FileWrapper(buf))
|
||||||
@ -431,8 +431,19 @@ def no_perm(modeladmin, request, selected):
|
|||||||
return HttpResponse(content="No permission to perform this action", status=403)
|
return HttpResponse(content="No permission to perform this action", status=403)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.action(permissions=["custom"])
|
||||||
|
def custom_action(modeladmin, request, selected):
|
||||||
|
return HttpResponse(content="OK", status=200)
|
||||||
|
|
||||||
|
|
||||||
class ExternalSubscriberAdmin(admin.ModelAdmin):
|
class ExternalSubscriberAdmin(admin.ModelAdmin):
|
||||||
actions = [redirect_to, external_mail, download, no_perm]
|
actions = [redirect_to, external_mail, download, no_perm, custom_action]
|
||||||
|
|
||||||
|
def has_custom_permission(self, request):
|
||||||
|
"""Does the user have the custom permission?"""
|
||||||
|
opts = self.opts
|
||||||
|
codename = get_permission_codename("custom", opts)
|
||||||
|
return request.user.has_perm("%s.%s" % (opts.app_label, codename))
|
||||||
|
|
||||||
|
|
||||||
class PodcastAdmin(admin.ModelAdmin):
|
class PodcastAdmin(admin.ModelAdmin):
|
||||||
|
@ -3,6 +3,7 @@ import json
|
|||||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||||
from django.contrib.admin.views.main import IS_POPUP_VAR
|
from django.contrib.admin.views.main import IS_POPUP_VAR
|
||||||
from django.contrib.auth.models import Permission, User
|
from django.contrib.auth.models import Permission, User
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
@ -269,7 +270,9 @@ class AdminActionsTest(TestCase):
|
|||||||
reverse("admin:admin_views_externalsubscriber_changelist"), action_data
|
reverse("admin:admin_views_externalsubscriber_changelist"), action_data
|
||||||
)
|
)
|
||||||
content = b"".join(list(response))
|
content = b"".join(list(response))
|
||||||
self.assertEqual(content, b"This is the content of the file")
|
self.assertEqual(
|
||||||
|
content, b"This is the content of the file written by John Doe"
|
||||||
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_custom_function_action_no_perm_response(self):
|
def test_custom_function_action_no_perm_response(self):
|
||||||
@ -294,13 +297,12 @@ class AdminActionsTest(TestCase):
|
|||||||
response,
|
response,
|
||||||
"""<label>Action: <select name="action" required>
|
"""<label>Action: <select name="action" required>
|
||||||
<option value="" selected>---------</option>
|
<option value="" selected>---------</option>
|
||||||
<option value="delete_selected">Delete selected external
|
<option value="delete_selected">Delete selected external subscribers</option>
|
||||||
subscribers</option>
|
|
||||||
<option value="redirect_to">Redirect to (Awesome action)</option>
|
<option value="redirect_to">Redirect to (Awesome action)</option>
|
||||||
<option value="external_mail">External mail (Another awesome
|
<option value="external_mail">External mail (Another awesome action)</option>
|
||||||
action)</option>
|
|
||||||
<option value="download">Download selected subscriptions</option>
|
<option value="download">Download selected subscriptions</option>
|
||||||
<option value="no_perm">No permission to run</option>
|
<option value="no_perm">No permission to run</option>
|
||||||
|
<option value="custom_action">Custom action</option>
|
||||||
</select>""",
|
</select>""",
|
||||||
html=True,
|
html=True,
|
||||||
)
|
)
|
||||||
@ -550,15 +552,25 @@ class AdminActionsPermissionTests(TestCase):
|
|||||||
class AdminDetailActionsTest(TestCase):
|
class AdminDetailActionsTest(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.superuser = User.objects.create_superuser(
|
cls.user = User.objects.create_user(
|
||||||
username="super", password="secret", email="super@example.com"
|
username="user",
|
||||||
|
password="secret",
|
||||||
|
email="user@example.com",
|
||||||
|
is_staff=True,
|
||||||
)
|
)
|
||||||
cls.s1 = ExternalSubscriber.objects.create(
|
cls.s1 = ExternalSubscriber.objects.create(
|
||||||
name="John Doe", email="john@example.org"
|
name="John Doe", email="john@example.org"
|
||||||
)
|
)
|
||||||
|
content_type = ContentType.objects.get_for_model(ExternalSubscriber)
|
||||||
|
for permission_type in ("view", "add", "change", "delete"):
|
||||||
|
permission = Permission.objects.get(
|
||||||
|
codename=f"{permission_type}_externalsubscriber",
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
cls.user.user_permissions.add(permission)
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client.force_login(self.superuser)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_available_detail_actions(self):
|
def test_available_detail_actions(self):
|
||||||
"""
|
"""
|
||||||
@ -677,11 +689,16 @@ class AdminDetailActionsTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_delete_action_in_detail_view(self):
|
def test_delete_action_in_detail_view(self):
|
||||||
|
content_type = ContentType.objects.get_for_model(Subscriber)
|
||||||
|
permission = Permission.objects.get(
|
||||||
|
codename="delete_subscriber", content_type=content_type
|
||||||
|
)
|
||||||
|
self.user.user_permissions.add(permission)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||||
{"action": "delete_selected"},
|
{"action": "delete_selected"},
|
||||||
)
|
)
|
||||||
self.assertTrue(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
response,
|
||||||
"Are you sure you want to delete the selected external subscriber?",
|
"Are you sure you want to delete the selected external subscriber?",
|
||||||
@ -696,3 +713,26 @@ class AdminDetailActionsTest(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(response.status_code, 200)
|
self.assertTrue(response.status_code, 200)
|
||||||
self.assertEqual(ExternalSubscriber.objects.count(), 0)
|
self.assertEqual(ExternalSubscriber.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_permissions(self):
|
||||||
|
# User doesn't have the permission to run the custom action.
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||||
|
{"action": "custom_action"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Now user has the custom permission to run the custom action.
|
||||||
|
content_type = ContentType.objects.get_for_model(ExternalSubscriber)
|
||||||
|
permission = Permission.objects.create(
|
||||||
|
name="custom",
|
||||||
|
codename="custom_externalsubscriber",
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
self.user.user_permissions.add(permission)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("admin:admin_views_externalsubscriber_change", args=[self.s1.pk]),
|
||||||
|
{"action": "custom_action"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.content, b"OK")
|
||||||
|
Loading…
Reference in New Issue
Block a user