Fixed #16117 -- Added decorators for admin action and display functions.
Refs #25134, #32099.
This commit is contained in:
parent
83fcfc9ec8
commit
9204485396
|
@ -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,
|
||||
|
@ -11,10 +11,10 @@ from django.contrib.admin.sites import AdminSite, site
|
|||
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",
|
||||
]
|
||||
|
|
|
@ -4,12 +4,17 @@ Built-in, globally-available admin actions.
|
|||
|
||||
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.
|
||||
|
@ -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")
|
||||
|
|
|
@ -1,3 +1,76 @@
|
|||
def action(function=None, *, permissions=None, description=None):
|
||||
"""
|
||||
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
|
||||
|
|
|
@ -12,6 +12,7 @@ from django.contrib.admin import helpers, widgets
|
|||
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 (
|
||||
|
@ -848,12 +849,12 @@ class ModelAdmin(BaseModelAdmin):
|
|||
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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
-----------------------------------------
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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`
|
||||
|
@ -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'
|
||||
|
||||
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.
|
||||
|
|
|
@ -256,10 +256,17 @@ subclass::
|
|||
class AuthorAdmin(admin.ModelAdmin):
|
||||
fields = ('name', 'title', 'view_birth_date')
|
||||
|
||||
@admin.display(empty_value='???')
|
||||
def view_birth_date(self, obj):
|
||||
return obj.birth_date
|
||||
|
||||
view_birth_date.empty_value_display = '???'
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``empty_value`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator is equivalent to
|
||||
setting the ``empty_value_display`` attribute on the display function
|
||||
directly in previous versions. Setting the attribute directly is still
|
||||
supported for backward compatibility.
|
||||
|
||||
.. attribute:: ModelAdmin.exclude
|
||||
|
||||
|
@ -551,7 +558,9 @@ subclass::
|
|||
If you don't set ``list_display``, the admin site will display a single
|
||||
column that displays the ``__str__()`` representation of each object.
|
||||
|
||||
There are four types of values that can be used in ``list_display``:
|
||||
There are four types of values that can be used in ``list_display``. All
|
||||
but the simplest may use the :func:`~django.contrib.admin.display`
|
||||
decorator is used to customize how the field is presented:
|
||||
|
||||
* The name of a model field. For example::
|
||||
|
||||
|
@ -560,9 +569,9 @@ subclass::
|
|||
|
||||
* A callable that accepts one argument, the model instance. For example::
|
||||
|
||||
@admin.display(description='Name')
|
||||
def upper_case_name(obj):
|
||||
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
|
||||
upper_case_name.short_description = 'Name'
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = (upper_case_name,)
|
||||
|
@ -573,9 +582,9 @@ subclass::
|
|||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('upper_case_name',)
|
||||
|
||||
@admin.display(description='Name')
|
||||
def upper_case_name(self, obj):
|
||||
return ("%s %s" % (obj.first_name, obj.last_name)).upper()
|
||||
upper_case_name.short_description = 'Name'
|
||||
|
||||
* A string representing a model attribute or method (without any required
|
||||
arguments). For example::
|
||||
|
@ -587,9 +596,9 @@ subclass::
|
|||
name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
@admin.display(description='Birth decade')
|
||||
def decade_born_in(self):
|
||||
return '%d’s' % (self.birthday.year // 10 * 10)
|
||||
decade_born_in.short_description = 'Birth decade'
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'decade_born_in')
|
||||
|
@ -624,6 +633,7 @@ subclass::
|
|||
last_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
@admin.display
|
||||
def colored_name(self):
|
||||
return format_html(
|
||||
'<span style="color: #{};">{} {}</span>',
|
||||
|
@ -637,7 +647,17 @@ subclass::
|
|||
|
||||
* As some examples have already demonstrated, when using a callable, a
|
||||
model method, or a ``ModelAdmin`` method, you can customize the column's
|
||||
title by adding a ``short_description`` attribute to the callable.
|
||||
title by wrapping the callable with the
|
||||
:func:`~django.contrib.admin.display` decorator and passing the
|
||||
``description`` argument.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``description`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator is equivalent to
|
||||
setting the ``short_description`` attribute on the display function
|
||||
directly in previous versions. Setting the attribute directly is
|
||||
still supported for backward compatibility.
|
||||
|
||||
* If the value of a field is ``None``, an empty string, or an iterable
|
||||
without elements, Django will display ``-`` (a dash). You can override
|
||||
|
@ -657,17 +677,23 @@ subclass::
|
|||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'birth_date_view')
|
||||
|
||||
@admin.display(empty_value='unknown')
|
||||
def birth_date_view(self, obj):
|
||||
return obj.birth_date
|
||||
|
||||
birth_date_view.empty_value_display = 'unknown'
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``empty_value`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator is equivalent to
|
||||
setting the ``empty_value_display`` attribute on the display function
|
||||
directly in previous versions. Setting the attribute directly is
|
||||
still supported for backward compatibility.
|
||||
|
||||
* If the string given is a method of the model, ``ModelAdmin`` or a
|
||||
callable that returns ``True``, ``False``, or ``None``, Django will
|
||||
display a pretty "yes", "no", or "unknown" icon if you give the method a
|
||||
``boolean`` attribute whose value is ``True``.
|
||||
|
||||
Here's a full example model::
|
||||
display a pretty "yes", "no", or "unknown" icon if you wrap the method
|
||||
with the :func:`~django.contrib.admin.display` decorator passing the
|
||||
``boolean`` argument with the value set to ``True``::
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
|
@ -676,13 +702,21 @@ subclass::
|
|||
first_name = models.CharField(max_length=50)
|
||||
birthday = models.DateField()
|
||||
|
||||
@admin.display(boolean=True)
|
||||
def born_in_fifties(self):
|
||||
return 1950 <= self.birthday.year < 1960
|
||||
born_in_fifties.boolean = True
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'born_in_fifties')
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``boolean`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator is equivalent to
|
||||
setting the ``boolean`` attribute on the display function directly in
|
||||
previous versions. Setting the attribute directly is still supported
|
||||
for backward compatibility.
|
||||
|
||||
* The ``__str__()`` method is just as valid in ``list_display`` as any
|
||||
other model method, so it's perfectly OK to do this::
|
||||
|
||||
|
@ -692,11 +726,10 @@ subclass::
|
|||
fields can't be used in sorting (because Django does all the sorting
|
||||
at the database level).
|
||||
|
||||
However, if an element of ``list_display`` represents a certain
|
||||
database field, you can indicate this fact by setting the
|
||||
``admin_order_field`` attribute of the item.
|
||||
|
||||
For example::
|
||||
However, if an element of ``list_display`` represents a certain database
|
||||
field, you can indicate this fact by using the
|
||||
:func:`~django.contrib.admin.display` decorator on the method, passing
|
||||
the ``ordering`` argument::
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
|
@ -706,6 +739,7 @@ subclass::
|
|||
first_name = models.CharField(max_length=50)
|
||||
color_code = models.CharField(max_length=6)
|
||||
|
||||
@admin.display(ordering='first_name')
|
||||
def colored_first_name(self):
|
||||
return format_html(
|
||||
'<span style="color: #{};">{}</span>',
|
||||
|
@ -713,23 +747,21 @@ subclass::
|
|||
self.first_name,
|
||||
)
|
||||
|
||||
colored_first_name.admin_order_field = 'first_name'
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('first_name', 'colored_first_name')
|
||||
|
||||
The above will tell Django to order by the ``first_name`` field when
|
||||
trying to sort by ``colored_first_name`` in the admin.
|
||||
|
||||
To indicate descending order with ``admin_order_field`` you can use a
|
||||
hyphen prefix on the field name. Using the above example, this would
|
||||
look like::
|
||||
To indicate descending order with the ``ordering`` argument you can use a
|
||||
hyphen prefix on the field name. Using the above example, this would look
|
||||
like::
|
||||
|
||||
colored_first_name.admin_order_field = '-first_name'
|
||||
@admin.display(ordering='-first_name')
|
||||
|
||||
``admin_order_field`` supports query lookups to sort by values on related
|
||||
models. This example includes an "author first name" column in the list
|
||||
display and allows sorting it by first name::
|
||||
The ``ordering`` argument supports query lookups to sort by values on
|
||||
related models. This example includes an "author first name" column in
|
||||
the list display and allows sorting it by first name::
|
||||
|
||||
class Blog(models.Model):
|
||||
title = models.CharField(max_length=255)
|
||||
|
@ -738,13 +770,12 @@ subclass::
|
|||
class BlogAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'author', 'author_first_name')
|
||||
|
||||
@admin.display(ordering='author__first_name')
|
||||
def author_first_name(self, obj):
|
||||
return obj.author.first_name
|
||||
|
||||
author_first_name.admin_order_field = 'author__first_name'
|
||||
|
||||
:doc:`Query expressions </ref/models/expressions>` may be used in
|
||||
``admin_order_field``. For example::
|
||||
:doc:`Query expressions </ref/models/expressions>` may be used with the
|
||||
``ordering`` argument::
|
||||
|
||||
from django.db.models import Value
|
||||
from django.db.models.functions import Concat
|
||||
|
@ -753,22 +784,41 @@ subclass::
|
|||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
|
||||
@admin.display(ordering=Concat('first_name', Value(' '), 'last_name'))
|
||||
def full_name(self):
|
||||
return self.first_name + ' ' + self.last_name
|
||||
full_name.admin_order_field = Concat('first_name', Value(' '), 'last_name')
|
||||
|
||||
* Elements of ``list_display`` can also be properties. Please note however,
|
||||
that due to the way properties work in Python, setting
|
||||
``short_description`` or ``admin_order_field`` on a property is only
|
||||
possible when using the ``property()`` function and **not** with the
|
||||
``@property`` decorator.
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
For example::
|
||||
The ``ordering`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator is equivalent to
|
||||
setting the ``admin_order_field`` attribute on the display function
|
||||
directly in previous versions. Setting the attribute directly is
|
||||
still supported for backward compatibility.
|
||||
|
||||
* Elements of ``list_display`` can also be properties::
|
||||
|
||||
class Person(models.Model):
|
||||
first_name = models.CharField(max_length=50)
|
||||
last_name = models.CharField(max_length=50)
|
||||
|
||||
@property
|
||||
@admin.display(
|
||||
ordering='last_name',
|
||||
description='Full name of the person',
|
||||
)
|
||||
def full_name(self):
|
||||
return self.first_name + ' ' + self.last_name
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('full_name',)
|
||||
|
||||
Note that ``@property`` must be above ``@display``. If you're using the
|
||||
old way -- setting the display-related attributes directly rather than
|
||||
using the :func:`~django.contrib.admin.display` decorator -- be aware
|
||||
that the ``property()`` function and **not** the ``@property`` decorator
|
||||
must be used::
|
||||
|
||||
def my_property(self):
|
||||
return self.first_name + ' ' + self.last_name
|
||||
my_property.short_description = "Full name of the person"
|
||||
|
@ -776,10 +826,6 @@ subclass::
|
|||
|
||||
full_name = property(my_property)
|
||||
|
||||
class PersonAdmin(admin.ModelAdmin):
|
||||
list_display = ('full_name',)
|
||||
|
||||
|
||||
* The field names in ``list_display`` will also appear as CSS classes in
|
||||
the HTML output, in the form of ``column-<field_name>`` on each ``<th>``
|
||||
element. This can be used to set column widths in a CSS file for example.
|
||||
|
@ -1239,6 +1285,8 @@ subclass::
|
|||
class PersonAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ('address_report',)
|
||||
|
||||
# description functions like a model field's verbose_name
|
||||
@admin.display(description='Address')
|
||||
def address_report(self, instance):
|
||||
# assuming get_full_address() returns a list of strings
|
||||
# for each line of the address and you want to separate each
|
||||
|
@ -1249,9 +1297,6 @@ subclass::
|
|||
((line,) for line in instance.get_full_address()),
|
||||
) or mark_safe("<span class='errors'>I can't determine this address.</span>")
|
||||
|
||||
# short_description functions like a model field's verbose_name
|
||||
address_report.short_description = "Address"
|
||||
|
||||
.. attribute:: ModelAdmin.save_as
|
||||
|
||||
Set ``save_as`` to enable a "save as new" feature on admin change forms.
|
||||
|
@ -1360,8 +1405,9 @@ subclass::
|
|||
.. attribute:: ModelAdmin.sortable_by
|
||||
|
||||
By default, the change list page allows sorting by all model fields (and
|
||||
callables that have the ``admin_order_field`` property) specified in
|
||||
:attr:`list_display`.
|
||||
callables that use the ``ordering`` argument to the
|
||||
:func:`~django.contrib.admin.display` decorator or have the
|
||||
``admin_order_field`` attribute) specified in :attr:`list_display`.
|
||||
|
||||
If you want to disable sorting for some columns, set ``sortable_by`` to
|
||||
a collection (e.g. ``list``, ``tuple``, or ``set``) of the subset of
|
||||
|
@ -3337,6 +3383,50 @@ The action in the examples above match the last part of the URL names for
|
|||
object which has an ``app_label`` and ``model_name`` attributes and is usually
|
||||
supplied by the admin views for the current model.
|
||||
|
||||
The ``display`` decorator
|
||||
=========================
|
||||
|
||||
.. function:: display(*, boolean=None, ordering=None, description=None, empty_value=None)
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
This decorator can be used for setting specific attributes on custom
|
||||
display functions that can be used with
|
||||
:attr:`~django.contrib.admin.ModelAdmin.list_display` or
|
||||
:attr:`~django.contrib.admin.ModelAdmin.readonly_fields`::
|
||||
|
||||
@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?'
|
||||
|
||||
Also note that the ``empty_value`` decorator parameter maps to the
|
||||
``empty_value_display`` attribute assigned directly to the function. It
|
||||
cannot be used in conjunction with ``boolean`` -- they are mutually
|
||||
exclusive.
|
||||
|
||||
Use of this decorator is not compulsory to make a display 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.display
|
||||
def published_year(self, obj):
|
||||
return obj.publish_date.year
|
||||
|
||||
In this case it will add no attributes to the function.
|
||||
|
||||
.. currentmodule:: django.contrib.admin.views.decorators
|
||||
|
||||
The ``staff_member_required`` decorator
|
||||
|
|
|
@ -141,6 +141,28 @@ Django </topics/cache>`.
|
|||
|
||||
.. _pymemcache: https://pypi.org/project/pymemcache/
|
||||
|
||||
New decorators for the admin site
|
||||
---------------------------------
|
||||
|
||||
The new :func:`~django.contrib.admin.display` decorator allows for easily
|
||||
adding options to custom display functions that can be used with
|
||||
:attr:`~django.contrib.admin.ModelAdmin.list_display` or
|
||||
:attr:`~django.contrib.admin.ModelAdmin.readonly_fields`.
|
||||
|
||||
Likewise, the new :func:`~django.contrib.admin.action` decorator allows for
|
||||
easily adding options to action functions that can be used with
|
||||
:attr:`~django.contrib.admin.ModelAdmin.actions`.
|
||||
|
||||
Using the ``@display`` decorator has the advantage that it is now
|
||||
possible to use the ``@property`` decorator when needing to specify attributes
|
||||
on the custom method. Prior to this it was necessary to use the ``property()``
|
||||
function instead after assigning the required attributes to the method.
|
||||
|
||||
Using decorators has the advantage that these options are more discoverable as
|
||||
they can be suggested by completion utilities in code editors. They are merely
|
||||
a convenience and still set the same attributes on the functions under the
|
||||
hood.
|
||||
|
||||
Minor features
|
||||
--------------
|
||||
|
||||
|
|
|
@ -389,12 +389,14 @@ verbose names Django performs by looking at the model's class name::
|
|||
verbose_name = _('my thing')
|
||||
verbose_name_plural = _('my things')
|
||||
|
||||
Model methods ``short_description`` attribute values
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Model methods ``description`` argument to the ``@display`` decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For model methods, you can provide translations to Django and the admin site
|
||||
with the ``short_description`` attribute::
|
||||
with the ``description`` argument to the :func:`~django.contrib.admin.display`
|
||||
decorator::
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -406,9 +408,9 @@ with the ``short_description`` attribute::
|
|||
verbose_name=_('kind'),
|
||||
)
|
||||
|
||||
@admin.display(description=_('Is it a mouse?'))
|
||||
def is_mouse(self):
|
||||
return self.kind.type == MOUSE_TYPE
|
||||
is_mouse.short_description = _('Is it a mouse?')
|
||||
|
||||
Working with lazy translation objects
|
||||
-------------------------------------
|
||||
|
|
|
@ -19,6 +19,7 @@ class EventAdmin(admin.ModelAdmin):
|
|||
date_hierarchy = 'date'
|
||||
list_display = ['event_date_func']
|
||||
|
||||
@admin.display
|
||||
def event_date_func(self, event):
|
||||
return event.date
|
||||
|
||||
|
@ -171,6 +172,6 @@ class EmptyValueChildAdmin(admin.ModelAdmin):
|
|||
empty_value_display = '-empty-'
|
||||
list_display = ('name', 'age_display', 'age')
|
||||
|
||||
@admin.display(empty_value='†')
|
||||
def age_display(self, obj):
|
||||
return obj.age
|
||||
age_display.empty_value_display = '†'
|
||||
|
|
|
@ -692,6 +692,7 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||
self.assertEqual(errors, [])
|
||||
|
||||
def test_readonly_on_method(self):
|
||||
@admin.display
|
||||
def my_function(obj):
|
||||
pass
|
||||
|
||||
|
@ -705,6 +706,7 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||
class SongAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ("readonly_method_on_modeladmin",)
|
||||
|
||||
@admin.display
|
||||
def readonly_method_on_modeladmin(self, obj):
|
||||
pass
|
||||
|
||||
|
@ -717,6 +719,7 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||
|
||||
def __getattr__(self, item):
|
||||
if item == "dynamic_method":
|
||||
@admin.display
|
||||
def method(obj):
|
||||
pass
|
||||
return method
|
||||
|
@ -777,6 +780,7 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||
|
||||
def test_extra(self):
|
||||
class SongAdmin(admin.ModelAdmin):
|
||||
@admin.display
|
||||
def awesome_song(self, instance):
|
||||
if instance.title == "Born to Run":
|
||||
return "Best Ever!"
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from django.contrib import admin
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -28,9 +29,9 @@ class Article(models.Model):
|
|||
def test_from_model(self):
|
||||
return "nothing"
|
||||
|
||||
@admin.display(description='not What you Expect')
|
||||
def test_from_model_with_override(self):
|
||||
return "nothing"
|
||||
test_from_model_with_override.short_description = "not What you Expect"
|
||||
|
||||
|
||||
class ArticleProxy(Article):
|
||||
|
|
|
@ -3,6 +3,7 @@ from decimal import Decimal
|
|||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import helpers
|
||||
from django.contrib.admin.utils import (
|
||||
NestedObjects, display_for_field, display_for_value, flatten,
|
||||
|
@ -293,9 +294,9 @@ class UtilsTests(SimpleTestCase):
|
|||
self.assertEqual(label_for_field('site_id', Article), 'Site id')
|
||||
|
||||
class MockModelAdmin:
|
||||
@admin.display(description='not Really the Model')
|
||||
def test_from_model(self, obj):
|
||||
return "nothing"
|
||||
test_from_model.short_description = "not Really the Model"
|
||||
|
||||
self.assertEqual(
|
||||
label_for_field("test_from_model", Article, model_admin=MockModelAdmin),
|
||||
|
@ -323,13 +324,11 @@ class UtilsTests(SimpleTestCase):
|
|||
label_for_field('nonexistent', Article, form=ArticleForm()),
|
||||
|
||||
def test_label_for_property(self):
|
||||
# NOTE: cannot use @property decorator, because of
|
||||
# AttributeError: 'property' object has no attribute 'short_description'
|
||||
class MockModelAdmin:
|
||||
def my_property(self):
|
||||
@property
|
||||
@admin.display(description='property short description')
|
||||
def test_from_property(self):
|
||||
return "this if from property"
|
||||
my_property.short_description = 'property short description'
|
||||
test_from_property = property(my_property)
|
||||
|
||||
self.assertEqual(
|
||||
label_for_field("test_from_property", Article, model_admin=MockModelAdmin),
|
||||
|
|
|
@ -49,6 +49,7 @@ from .models import (
|
|||
)
|
||||
|
||||
|
||||
@admin.display(ordering='date')
|
||||
def callable_year(dt_value):
|
||||
try:
|
||||
return dt_value.year
|
||||
|
@ -56,9 +57,6 @@ def callable_year(dt_value):
|
|||
return None
|
||||
|
||||
|
||||
callable_year.admin_order_field = 'date'
|
||||
|
||||
|
||||
class ArticleInline(admin.TabularInline):
|
||||
model = Article
|
||||
fk_name = 'section'
|
||||
|
@ -138,25 +136,24 @@ class ArticleAdmin(ArticleAdminWithExtraUrl):
|
|||
|
||||
# These orderings aren't particularly useful but show that expressions can
|
||||
# be used for admin_order_field.
|
||||
@admin.display(ordering=models.F('date') + datetime.timedelta(days=3))
|
||||
def order_by_expression(self, obj):
|
||||
return obj.model_year
|
||||
order_by_expression.admin_order_field = models.F('date') + datetime.timedelta(days=3)
|
||||
|
||||
@admin.display(ordering=models.F('date'))
|
||||
def order_by_f_expression(self, obj):
|
||||
return obj.model_year
|
||||
order_by_f_expression.admin_order_field = models.F('date')
|
||||
|
||||
@admin.display(ordering=models.F('date').asc(nulls_last=True))
|
||||
def order_by_orderby_expression(self, obj):
|
||||
return obj.model_year
|
||||
order_by_orderby_expression.admin_order_field = models.F('date').asc(nulls_last=True)
|
||||
|
||||
def changelist_view(self, request):
|
||||
return super().changelist_view(request, extra_context={'extra_var': 'Hello!'})
|
||||
|
||||
@admin.display(ordering='date', description=None)
|
||||
def modeladmin_year(self, obj):
|
||||
return obj.date.year
|
||||
modeladmin_year.admin_order_field = 'date'
|
||||
modeladmin_year.short_description = None
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
EmailMessage(
|
||||
|
@ -216,6 +213,7 @@ class ThingAdmin(admin.ModelAdmin):
|
|||
class InquisitionAdmin(admin.ModelAdmin):
|
||||
list_display = ('leader', 'country', 'expected', 'sketch')
|
||||
|
||||
@admin.display
|
||||
def sketch(self, obj):
|
||||
# A method with the same name as a reverse accessor.
|
||||
return 'list-display-sketch'
|
||||
|
@ -280,6 +278,7 @@ class SubscriberAdmin(admin.ModelAdmin):
|
|||
SubscriberAdmin.overridden = True
|
||||
super().delete_queryset(request, queryset)
|
||||
|
||||
@admin.action
|
||||
def mail_admin(self, request, selected):
|
||||
EmailMessage(
|
||||
'Greetings from a ModelAdmin action',
|
||||
|
@ -289,6 +288,7 @@ class SubscriberAdmin(admin.ModelAdmin):
|
|||
).send()
|
||||
|
||||
|
||||
@admin.action(description='External mail (Another awesome action)')
|
||||
def external_mail(modeladmin, request, selected):
|
||||
EmailMessage(
|
||||
'Greetings from a function action',
|
||||
|
@ -298,32 +298,23 @@ def external_mail(modeladmin, request, selected):
|
|||
).send()
|
||||
|
||||
|
||||
external_mail.short_description = 'External mail (Another awesome action)'
|
||||
|
||||
|
||||
@admin.action(description='Redirect to (Awesome action)')
|
||||
def redirect_to(modeladmin, request, selected):
|
||||
from django.http import HttpResponseRedirect
|
||||
return HttpResponseRedirect('/some-where-else/')
|
||||
|
||||
|
||||
redirect_to.short_description = 'Redirect to (Awesome action)'
|
||||
|
||||
|
||||
@admin.action(description='Download subscription')
|
||||
def download(modeladmin, request, selected):
|
||||
buf = StringIO('This is the content of the file')
|
||||
return StreamingHttpResponse(FileWrapper(buf))
|
||||
|
||||
|
||||
download.short_description = 'Download subscription'
|
||||
|
||||
|
||||
@admin.action(description='No permission to run')
|
||||
def no_perm(modeladmin, request, selected):
|
||||
return HttpResponse(content='No permission to perform this action', status=403)
|
||||
|
||||
|
||||
no_perm.short_description = 'No permission to run'
|
||||
|
||||
|
||||
class ExternalSubscriberAdmin(admin.ModelAdmin):
|
||||
actions = [redirect_to, external_mail, download, no_perm]
|
||||
|
||||
|
@ -441,6 +432,7 @@ class LinkInline(admin.TabularInline):
|
|||
|
||||
readonly_fields = ("posted", "multiline", "readonly_link_content")
|
||||
|
||||
@admin.display
|
||||
def multiline(self, instance):
|
||||
return "InlineMultiline\ntest\nstring"
|
||||
|
||||
|
@ -501,19 +493,22 @@ class PostAdmin(admin.ModelAdmin):
|
|||
LinkInline
|
||||
]
|
||||
|
||||
@admin.display
|
||||
def coolness(self, instance):
|
||||
if instance.pk:
|
||||
return "%d amount of cool." % instance.pk
|
||||
else:
|
||||
return "Unknown coolness."
|
||||
|
||||
@admin.display(description='Value in $US')
|
||||
def value(self, instance):
|
||||
return 1000
|
||||
value.short_description = 'Value in $US'
|
||||
|
||||
@admin.display
|
||||
def multiline(self, instance):
|
||||
return "Multiline\ntest\nstring"
|
||||
|
||||
@admin.display
|
||||
def multiline_html(self, instance):
|
||||
return mark_safe("Multiline<br>\nhtml<br>\ncontent")
|
||||
|
||||
|
@ -655,9 +650,9 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin):
|
|||
list_display = ('name', 'age', 'is_employee', 'colored_name')
|
||||
ordering = ('name',)
|
||||
|
||||
@admin.display(ordering='name')
|
||||
def colored_name(self, obj):
|
||||
return format_html('<span style="color: #ff00ff;">{}</span>', obj.name)
|
||||
colored_name.admin_order_field = 'name'
|
||||
|
||||
|
||||
class PluggableSearchPersonAdmin(admin.ModelAdmin):
|
||||
|
@ -706,20 +701,18 @@ class AdminOrderedModelMethodAdmin(admin.ModelAdmin):
|
|||
|
||||
|
||||
class AdminOrderedAdminMethodAdmin(admin.ModelAdmin):
|
||||
@admin.display(ordering='order')
|
||||
def some_admin_order(self, obj):
|
||||
return obj.order
|
||||
some_admin_order.admin_order_field = 'order'
|
||||
ordering = ('order',)
|
||||
list_display = ('stuff', 'some_admin_order')
|
||||
|
||||
|
||||
@admin.display(ordering='order')
|
||||
def admin_ordered_callable(obj):
|
||||
return obj.order
|
||||
|
||||
|
||||
admin_ordered_callable.admin_order_field = 'order'
|
||||
|
||||
|
||||
class AdminOrderedCallableAdmin(admin.ModelAdmin):
|
||||
ordering = ('order',)
|
||||
list_display = ('stuff', admin_ordered_callable)
|
||||
|
@ -814,6 +807,7 @@ class UnchangeableObjectAdmin(admin.ModelAdmin):
|
|||
return [p for p in urlpatterns if p.name and not p.name.endswith("_change")]
|
||||
|
||||
|
||||
@admin.display
|
||||
def callable_on_unknown(obj):
|
||||
return obj.unknown
|
||||
|
||||
|
@ -831,21 +825,27 @@ class MessageTestingAdmin(admin.ModelAdmin):
|
|||
actions = ["message_debug", "message_info", "message_success",
|
||||
"message_warning", "message_error", "message_extra_tags"]
|
||||
|
||||
@admin.action
|
||||
def message_debug(self, request, selected):
|
||||
self.message_user(request, "Test debug", level="debug")
|
||||
|
||||
@admin.action
|
||||
def message_info(self, request, selected):
|
||||
self.message_user(request, "Test info", level="info")
|
||||
|
||||
@admin.action
|
||||
def message_success(self, request, selected):
|
||||
self.message_user(request, "Test success", level="success")
|
||||
|
||||
@admin.action
|
||||
def message_warning(self, request, selected):
|
||||
self.message_user(request, "Test warning", level="warning")
|
||||
|
||||
@admin.action
|
||||
def message_error(self, request, selected):
|
||||
self.message_user(request, "Test error", level="error")
|
||||
|
||||
@admin.action
|
||||
def message_extra_tags(self, request, selected):
|
||||
self.message_user(request, "Test tags", extra_tags="extra_tag")
|
||||
|
||||
|
@ -1156,9 +1156,9 @@ class ArticleAdmin6(admin.ModelAdmin):
|
|||
)
|
||||
sortable_by = ('date', callable_year)
|
||||
|
||||
@admin.display(ordering='date')
|
||||
def modeladmin_year(self, obj):
|
||||
return obj.date.year
|
||||
modeladmin_year.admin_order_field = 'date'
|
||||
|
||||
|
||||
class ActorAdmin6(admin.ModelAdmin):
|
||||
|
|
|
@ -3,6 +3,7 @@ import os
|
|||
import tempfile
|
||||
import uuid
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import (
|
||||
GenericForeignKey, GenericRelation,
|
||||
|
@ -45,20 +46,18 @@ class Article(models.Model):
|
|||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
@admin.display(ordering='date', description='')
|
||||
def model_year(self):
|
||||
return self.date.year
|
||||
model_year.admin_order_field = 'date'
|
||||
model_year.short_description = ''
|
||||
|
||||
@admin.display(ordering='-date', description='')
|
||||
def model_year_reversed(self):
|
||||
return self.date.year
|
||||
model_year_reversed.admin_order_field = '-date'
|
||||
model_year_reversed.short_description = ''
|
||||
|
||||
def property_year(self):
|
||||
@property
|
||||
@admin.display(ordering='date')
|
||||
def model_property_year(self):
|
||||
return self.date.year
|
||||
property_year.admin_order_field = 'date'
|
||||
model_property_year = property(property_year)
|
||||
|
||||
@property
|
||||
def model_month(self):
|
||||
|
@ -746,9 +745,9 @@ class AdminOrderedModelMethod(models.Model):
|
|||
order = models.IntegerField()
|
||||
stuff = models.CharField(max_length=200)
|
||||
|
||||
@admin.display(ordering='order')
|
||||
def some_order(self):
|
||||
return self.order
|
||||
some_order.admin_order_field = 'order'
|
||||
|
||||
|
||||
class AdminOrderedAdminMethod(models.Model):
|
||||
|
|
|
@ -7,6 +7,7 @@ from urllib.parse import parse_qsl, urljoin, urlparse
|
|||
|
||||
import pytz
|
||||
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import AdminSite, ModelAdmin
|
||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||
from django.contrib.admin.models import ADDITION, DELETION, LogEntry
|
||||
|
@ -751,6 +752,17 @@ class AdminViewBasicTest(AdminViewBasicTestCase):
|
|||
response = self.client.get(reverse('admin:admin_views_post_changelist'))
|
||||
self.assertContains(response, 'icon-unknown.svg')
|
||||
|
||||
def test_display_decorator_with_boolean_and_empty_value(self):
|
||||
msg = (
|
||||
'The boolean and empty_value arguments to the @display decorator '
|
||||
'are mutually exclusive.'
|
||||
)
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
class BookAdmin(admin.ModelAdmin):
|
||||
@admin.display(boolean=True, empty_value='(Missing)')
|
||||
def is_published(self, obj):
|
||||
return obj.publish_date is not None
|
||||
|
||||
def test_i18n_language_non_english_default(self):
|
||||
"""
|
||||
Check if the JavaScript i18n view returns an empty language catalog
|
||||
|
|
|
@ -27,6 +27,7 @@ class AdminActionsTests(TestCase):
|
|||
class BandAdmin(admin.ModelAdmin):
|
||||
actions = ['custom_action']
|
||||
|
||||
@admin.action
|
||||
def custom_action(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
|
@ -60,6 +61,7 @@ class AdminActionsTests(TestCase):
|
|||
class AdminBase(admin.ModelAdmin):
|
||||
actions = ['custom_action']
|
||||
|
||||
@admin.action
|
||||
def custom_action(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
|
@ -78,13 +80,14 @@ class AdminActionsTests(TestCase):
|
|||
self.assertEqual(action_names, ['delete_selected'])
|
||||
|
||||
def test_global_actions_description(self):
|
||||
@admin.action(description='Site-wide admin action 1.')
|
||||
def global_action_1(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
@admin.action
|
||||
def global_action_2(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
global_action_1.short_description = 'Site-wide admin action 1.'
|
||||
admin_site = admin.AdminSite()
|
||||
admin_site.add_action(global_action_1)
|
||||
admin_site.add_action(global_action_2)
|
||||
|
@ -103,30 +106,28 @@ class AdminActionsTests(TestCase):
|
|||
)
|
||||
|
||||
def test_actions_replace_global_action(self):
|
||||
@admin.action(description='Site-wide admin action 1.')
|
||||
def global_action_1(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
@admin.action(description='Site-wide admin action 2.')
|
||||
def global_action_2(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
global_action_1.short_description = 'Site-wide admin action 1.'
|
||||
global_action_2.short_description = 'Site-wide admin action 2.'
|
||||
admin.site.add_action(global_action_1, name='custom_action_1')
|
||||
admin.site.add_action(global_action_2, name='custom_action_2')
|
||||
|
||||
@admin.action(description='Local admin action 1.')
|
||||
def custom_action_1(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
custom_action_1.short_description = 'Local admin action 1.'
|
||||
|
||||
class BandAdmin(admin.ModelAdmin):
|
||||
actions = [custom_action_1, 'custom_action_2']
|
||||
|
||||
@admin.action(description='Local admin action 2.')
|
||||
def custom_action_2(self, request, queryset):
|
||||
pass
|
||||
|
||||
custom_action_2.short_description = 'Local admin action 2.'
|
||||
|
||||
ma = BandAdmin(Band, admin.site)
|
||||
self.assertEqual(ma.check(), [])
|
||||
self.assertEqual(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import BooleanFieldListFilter, SimpleListFilter
|
||||
from django.contrib.admin.options import VERTICAL, ModelAdmin, TabularInline
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
|
@ -499,10 +500,12 @@ class ListDisplayTests(CheckTestCase):
|
|||
)
|
||||
|
||||
def test_valid_case(self):
|
||||
@admin.display
|
||||
def a_callable(obj):
|
||||
pass
|
||||
|
||||
class TestModelAdmin(ModelAdmin):
|
||||
@admin.display
|
||||
def a_method(self, obj):
|
||||
pass
|
||||
list_display = ('name', 'decade_published_in', 'a_method', a_callable)
|
||||
|
@ -563,10 +566,12 @@ class ListDisplayLinksCheckTests(CheckTestCase):
|
|||
)
|
||||
|
||||
def test_valid_case(self):
|
||||
@admin.display
|
||||
def a_callable(obj):
|
||||
pass
|
||||
|
||||
class TestModelAdmin(ModelAdmin):
|
||||
@admin.display
|
||||
def a_method(self, obj):
|
||||
pass
|
||||
list_display = ('name', 'decade_published_in', 'a_method', a_callable)
|
||||
|
@ -1417,11 +1422,10 @@ class AutocompleteFieldsTests(CheckTestCase):
|
|||
class ActionsCheckTests(CheckTestCase):
|
||||
|
||||
def test_custom_permissions_require_matching_has_method(self):
|
||||
@admin.action(permissions=['custom'])
|
||||
def custom_permission_action(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
custom_permission_action.allowed_permissions = ('custom',)
|
||||
|
||||
class BandAdmin(ModelAdmin):
|
||||
actions = (custom_permission_action,)
|
||||
|
||||
|
@ -1433,6 +1437,7 @@ class ActionsCheckTests(CheckTestCase):
|
|||
)
|
||||
|
||||
def test_actions_not_unique(self):
|
||||
@admin.action
|
||||
def action(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
|
@ -1447,9 +1452,11 @@ class ActionsCheckTests(CheckTestCase):
|
|||
)
|
||||
|
||||
def test_actions_unique(self):
|
||||
@admin.action
|
||||
def action1(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
@admin.action
|
||||
def action2(modeladmin, request, queryset):
|
||||
pass
|
||||
|
||||
|
|
Loading…
Reference in New Issue