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

Refs #25134, #32099.
This commit is contained in:
Nick Pope 2021-01-13 16:19:22 +00:00 committed by GitHub
parent 83fcfc9ec8
commit 9204485396
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 422 additions and 148 deletions

View File

@ -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",
]

View File

@ -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")

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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.

View File

@ -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 '%ds' % (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,44 +726,42 @@ 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.
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::
For example::
from django.contrib import admin
from django.db import models
from django.utils.html import format_html
from django.contrib import admin
from django.db import models
from django.utils.html import format_html
class Person(models.Model):
first_name = models.CharField(max_length=50)
color_code = models.CharField(max_length=6)
class Person(models.Model):
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>',
self.color_code,
self.first_name,
)
def colored_first_name(self):
return format_html(
'<span style="color: #{};">{}</span>',
self.color_code,
self.first_name,
)
colored_first_name.admin_order_field = 'first_name'
class PersonAdmin(admin.ModelAdmin):
list_display = ('first_name', 'colored_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,32 +784,47 @@ 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)
def my_property(self):
@property
@admin.display(
ordering='last_name',
description='Full name of the person',
)
def full_name(self):
return self.first_name + ' ' + self.last_name
my_property.short_description = "Full name of the person"
my_property.admin_order_field = 'last_name'
full_name = property(my_property)
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"
my_property.admin_order_field = 'last_name'
full_name = property(my_property)
* 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>``
@ -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

View File

@ -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
--------------

View File

@ -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
-------------------------------------

View File

@ -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='&dagger;')
def age_display(self, obj):
return obj.age
age_display.empty_value_display = '&dagger;'

View File

@ -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!"

View File

@ -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):

View File

@ -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),

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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(

View File

@ -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