Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #16117 -- Added decorators for admin action and display functions. #13532

Merged
merged 1 commit into from
Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 5 additions & 5 deletions django/contrib/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.contrib.admin.decorators import register
from django.contrib.admin.decorators import action, display, register
from django.contrib.admin.filters import (
AllValuesFieldListFilter, BooleanFieldListFilter, ChoicesFieldListFilter,
DateFieldListFilter, EmptyFieldListFilter, FieldListFilter, ListFilter,
Expand All @@ -11,10 +11,10 @@
from django.utils.module_loading import autodiscover_modules

__all__ = [
"register", "ModelAdmin", "HORIZONTAL", "VERTICAL", "StackedInline",
"TabularInline", "AdminSite", "site", "ListFilter", "SimpleListFilter",
"FieldListFilter", "BooleanFieldListFilter", "RelatedFieldListFilter",
"ChoicesFieldListFilter", "DateFieldListFilter",
"action", "display", "register", "ModelAdmin", "HORIZONTAL", "VERTICAL",
"StackedInline", "TabularInline", "AdminSite", "site", "ListFilter",
"SimpleListFilter", "FieldListFilter", "BooleanFieldListFilter",
"RelatedFieldListFilter", "ChoicesFieldListFilter", "DateFieldListFilter",
"AllValuesFieldListFilter", "EmptyFieldListFilter",
"RelatedOnlyFieldListFilter", "autodiscover",
]
Expand Down
9 changes: 5 additions & 4 deletions django/contrib/admin/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@

from django.contrib import messages
from django.contrib.admin import helpers
from django.contrib.admin.decorators import action
from django.contrib.admin.utils import model_ngettext
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.utils.translation import gettext as _, gettext_lazy


@action(
permissions=['delete'],
description=gettext_lazy('Delete selected %(verbose_name_plural)s'),
)
def delete_selected(modeladmin, request, queryset):
"""
Default action which deletes the selected objects.
Expand Down Expand Up @@ -73,7 +78,3 @@ def delete_selected(modeladmin, request, queryset):
"admin/%s/delete_selected_confirmation.html" % app_label,
"admin/delete_selected_confirmation.html"
], context)


delete_selected.allowed_permissions = ('delete',)
delete_selected.short_description = gettext_lazy("Delete selected %(verbose_name_plural)s")
73 changes: 73 additions & 0 deletions django/contrib/admin/decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,76 @@
def action(function=None, *, permissions=None, description=None):
ngnpope marked this conversation as resolved.
Show resolved Hide resolved
"""
Conveniently add attributes to an action function::

@admin.action(
permissions=['publish'],
description='Mark selected stories as published',
)
def make_published(self, request, queryset):
queryset.update(status='p')

This is equivalent to setting some attributes (with the original, longer
names) on the function directly::

def make_published(self, request, queryset):
queryset.update(status='p')
make_published.allowed_permissions = ['publish']
make_published.short_description = 'Mark selected stories as published'
"""
def decorator(func):
if permissions is not None:
func.allowed_permissions = permissions
if description is not None:
func.short_description = description
return func
if function is None:
return decorator
else:
return decorator(function)


def display(function=None, *, boolean=None, ordering=None, description=None, empty_value=None):
"""
Conveniently add attributes to a display function::

@admin.display(
boolean=True,
ordering='-publish_date',
description='Is Published?',
)
def is_published(self, obj):
return obj.publish_date is not None

This is equivalent to setting some attributes (with the original, longer
names) on the function directly::

def is_published(self, obj):
return obj.publish_date is not None
is_published.boolean = True
is_published.admin_order_field = '-publish_date'
is_published.short_description = 'Is Published?'
"""
def decorator(func):
if boolean is not None and empty_value is not None:
raise ValueError(
'The boolean and empty_value arguments to the @display '
'decorator are mutually exclusive.'
)
if boolean is not None:
func.boolean = boolean
if ordering is not None:
func.admin_order_field = ordering
if description is not None:
func.short_description = description
if empty_value is not None:
func.empty_value_display = empty_value
return func
if function is None:
return decorator
else:
return decorator(function)


def register(*models, site=None):
"""
Register the given model(s) classes and wrapped ModelAdmin class with
Expand Down
3 changes: 2 additions & 1 deletion django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.contrib.admin.checks import (
BaseModelAdminChecks, InlineModelAdminChecks, ModelAdminChecks,
)
from django.contrib.admin.decorators import display
from django.contrib.admin.exceptions import DisallowedModelAdminToField
from django.contrib.admin.templatetags.admin_urls import add_preserved_filters
from django.contrib.admin.utils import (
Expand Down Expand Up @@ -848,12 +849,12 @@ def log_deletion(self, request, object, object_repr):
action_flag=DELETION,
)

@display(description=mark_safe('<input type="checkbox" id="action-toggle">'))
def action_checkbox(self, obj):
"""
A list_display column containing a checkbox widget.
"""
return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk))
action_checkbox.short_description = mark_safe('<input type="checkbox" id="action-toggle">')

@staticmethod
def _get_action_description(func, name):
Expand Down
16 changes: 10 additions & 6 deletions docs/intro/tutorial07.txt
Original file line number Diff line number Diff line change
Expand Up @@ -228,22 +228,26 @@ of an arbitrary method is not supported. Also note that the column header for
underscores replaced with spaces), and that each line contains the string
representation of the output.

You can improve that by giving that method (in :file:`polls/models.py`) a few
attributes, as follows:
You can improve that by using the :func:`~django.contrib.admin.display`
decorator on that method (in :file:`polls/models.py`), as follows:

.. code-block:: python
:caption: polls/models.py

from django.contrib import admin

class Question(models.Model):
# ...
@admin.display(
boolean=True,
ordering='pub_date',
description='Published recently?',
)
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
was_published_recently.admin_order_field = 'pub_date'
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'

For more information on these method properties, see
For more information on the properties configurable via the decorator, see
:attr:`~django.contrib.admin.ModelAdmin.list_display`.

Edit your :file:`polls/admin.py` file again and add an improvement to the
Expand Down
87 changes: 72 additions & 15 deletions docs/ref/contrib/admin/actions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,32 @@ That's actually all there is to writing an action! However, we'll take one
more optional-but-useful step and give the action a "nice" title in the admin.
By default, this action would appear in the action list as "Make published" --
the function name, with underscores replaced by spaces. That's fine, but we
can provide a better, more human-friendly name by giving the
``make_published`` function a ``short_description`` attribute::
can provide a better, more human-friendly name by using the
:func:`~django.contrib.admin.action` decorator on the ``make_published``
function::

from django.contrib import admin

...

@admin.action(description='Mark selected stories as published')
def make_published(modeladmin, request, queryset):
queryset.update(status='p')
make_published.short_description = "Mark selected stories as published"

.. note::

This might look familiar; the admin's ``list_display`` option uses the
same technique to provide human-readable descriptions for callback
functions registered there, too.
This might look familiar; the admin's
:attr:`~django.contrib.admin.ModelAdmin.list_display` option uses a similar
technique with the :func:`~django.contrib.admin.display` decorator to
provide human-readable descriptions for callback functions registered
there, too.

.. versionchanged:: 3.2

The ``description`` argument to the :func:`~django.contrib.admin.action`
decorator is equivalent to setting the ``short_description`` attribute on
the action function directly in previous versions. Setting the attribute
directly is still supported for backward compatibility.

Adding actions to the :class:`ModelAdmin`
-----------------------------------------
Expand All @@ -122,9 +136,9 @@ the action and its registration would look like::
from django.contrib import admin
from myapp.models import Article

@admin.action(description='Mark selected stories as published')
def make_published(modeladmin, request, queryset):
queryset.update(status='p')
make_published.short_description = "Mark selected stories as published"

class ArticleAdmin(admin.ModelAdmin):
list_display = ['title', 'status']
Expand Down Expand Up @@ -171,9 +185,9 @@ You can do it like this::

actions = ['make_published']

@admin.action(description='Mark selected stories as published')
def make_published(self, request, queryset):
queryset.update(status='p')
make_published.short_description = "Mark selected stories as published"

Notice first that we've moved ``make_published`` into a method and renamed the
``modeladmin`` parameter to ``self``, and second that we've now put the string
Expand Down Expand Up @@ -364,20 +378,20 @@ Setting permissions for actions
-------------------------------

Actions may limit their availability to users with specific permissions by
setting an ``allowed_permissions`` attribute on the action function::
wrapping the action function with the :func:`~django.contrib.admin.action`
decorator and passing the ``permissions`` argument::

@admin.action(permissions=['change'])
def make_published(modeladmin, request, queryset):
queryset.update(status='p')
make_published.allowed_permissions = ('change',)

The ``make_published()`` action will only be available to users that pass the
:meth:`.ModelAdmin.has_change_permission` check.

If ``allowed_permissions`` has more than one permission, the action will be
available as long as the user passes at least one of the checks.
If ``permissions`` has more than one permission, the action will be available
as long as the user passes at least one of the checks.

Available values for ``allowed_permissions`` and the corresponding method
checks are:
Available values for ``permissions`` and the corresponding method checks are:

- ``'add'``: :meth:`.ModelAdmin.has_add_permission`
- ``'change'``: :meth:`.ModelAdmin.has_change_permission`
Expand All @@ -395,12 +409,55 @@ For example::
class ArticleAdmin(admin.ModelAdmin):
actions = ['make_published']

@admin.action(permissions=['publish'])
def make_published(self, request, queryset):
queryset.update(status='p')
make_published.allowed_permissions = ('publish',)

def has_publish_permission(self, request):
"""Does the user have the publish permission?"""
opts = self.opts
codename = get_permission_codename('publish', opts)
return request.user.has_perm('%s.%s' % (opts.app_label, codename))

.. versionchanged:: 3.2

The ``permissions`` argument to the :func:`~django.contrib.admin.action`
decorator is equivalent to setting the ``allowed_permissions`` attribute on
the action function directly in previous versions. Setting the attribute
directly is still supported for backward compatibility.

The ``action`` decorator
========================

.. function:: action(*, permissions=None, description=None)

.. versionadded:: 3.2

This decorator can be used for setting specific attributes on custom action
functions that can be used with
:attr:`~django.contrib.admin.ModelAdmin.actions`::

@admin.action(
permissions=['publish'],
description='Mark selected stories as published',
)
def make_published(self, request, queryset):
queryset.update(status='p')

This is equivalent to setting some attributes (with the original, longer
names) on the function directly::

def make_published(self, request, queryset):
queryset.update(status='p')
make_published.allowed_permissions = ['publish']
make_published.short_description = 'Mark selected stories as published'
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I come to this documentation (or for @display) for the first time, it tells me about setting these attributes but not what they mean, and nor is there an obvious link to where to read about them. It makes it a bit hard to use @action or @display - it still feels like I have to know about both ways.

Perhaps the decorator reference should become the canonical place where the meanings of the arguments and attributes are documented? At least, they can be briefly described with links to "more info" sections below, with each of the examples. The current structure is a fairly messy since it seems to have grown organically.

For actions:

  • short_description is under "Writing action functions" only
  • allowed_permissions has a whole section "Setting permissions for actions"

For display, everything is listed in bullet points under the ModelAdmin.list_display documentation (a giant list that starts "A few special cases"):

  • boolean - this unnecessarily re-spells what kinds of strings and functions are allowed in list_display
  • admin_order_field
  • short_description - this only has one sentence saying "As some examples have already demonstrated" which doesn't feel very easy to use as a reference since it's saying "go read above"
  • empty_value_display - this goes off on a bit of a tangent documenting the ability to change it for the whole admin site and the model admin class too. It would probably be best as its own section that documentation for the other two classes link to.

I am suggesting a fair amount of docs rearrangement here. I am happy to shunt it to a second PR and help on that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It is quite impenetrable and definitely needs rejigging. I tried not to move stuff around too much at this stage, however, and kept everything in the same place.

I think a follow up PR to improve the whole of this section would be a good idea - the large bulleted list doesn't feel the best way to present the options.


Use of this decorator is not compulsory to make an action function, but it
can be useful to use it without arguments as a marker in your source to
identify the purpose of the function::

@admin.action
def make_inactive(self, request, queryset):
queryset.update(is_active=False)

In this case it will add no attributes to the function.