Pular para o conteúdo
dicas de djangopor Regis do Python

Dica 39 - Django Admin: display decorator (Django 3.2+)

6 min de leitura
Assistir ao vídeo no YouTube: Dica 39 - Django Admin: display decorator (Django 3.2+)

Versões usadas no vídeo: Django 3.2.5 e Python 3.9 (projeto criado com o django-boilerplate).

Doc: https://docs.djangoproject.com/en/3.2/ref/contrib/admin/#the-display-decorator

O Django 3.2 trouxe uma novidade para o Admin: o decorator @admin.display. Ele serve para configurar os métodos que você coloca no list_display (ou no readonly_fields): o título da coluna, se o valor é um booleano (e deve aparecer com os ícones de "sim" e "não") e por qual campo a coluna é ordenada.

Antes dele, isso era feito com atributos pendurados na função (short_description, boolean, admin_order_field), como vimos na Dica 28 - Admin: usando short description. Esse jeito antigo ainda funciona; o decorator só deixa tudo junto, em cima do método.

A assinatura, segundo a documentação, é:

display(*, boolean=None, ordering=None, description=None, empty_value=None)

Neste tutorial vamos usar três desses parâmetros: description, boolean e ordering.

Pré-requisitos

IMPORTANTE: para mostrar essa feature, o vídeo usou um projeto separado, criado com o django-boilerplate (veja a Dica 1.1 - Django boilerplate), e não o projeto principal das dicas.

O projeto foi criado com o nome backend, escolhendo o Django 3.2 no menu do script:

git clone https://github.com/rg3915/django-boilerplate.git /tmp/django-boilerplate
cp /tmp/django-boilerplate/boilerplatesimple.sh .
source boilerplatesimple.sh backend
Select Django version:
2 - 2.2.*
3 - 3.2.*
Choose from 2, 3 [3]: 3

Confira a versão:

pip freeze | grep Django
# Django==3.2.5

O boilerplate cria a app backend/crm, com um modelo Person e o respectivo PersonAdmin.

O modelo

Em backend/crm/models.py, acrescente o campo published_date (data de publicação) ao Person. É um DateField opcional:

# backend/crm/models.py
from django.db import models
from django.urls import reverse_lazy

from backend.core.models import (
    Active,
    Address,
    Document,
    TimeStampedModel,
    UuidModel
)


class Person(UuidModel, TimeStampedModel, Address, Document, Active):
    first_name = models.CharField('nome', max_length=50)
    last_name = models.CharField('sobrenome', max_length=50, null=True, blank=True)  # noqa E501
    email = models.EmailField(null=True, blank=True)
    published_date = models.DateField('data de publicação', null=True, blank=True)

    class Meta:
        ordering = ('first_name',)
        verbose_name = 'pessoa'
        verbose_name_plural = 'pessoas'

    @property
    def full_name(self):
        return f'{self.first_name} {self.last_name or ""}'.strip()

    def __str__(self):
        return self.full_name

    def get_absolute_url(self):
        return reverse_lazy('crm:person_detail', kwargs={'pk': self.pk})

Os modelos abstratos UuidModel, TimeStampedModel, Address, Document e Active vêm do backend/core/models.py do boilerplate (o Active é o que fornece o campo booleano active).

A app crm do boilerplate vem sem migrações, então crie e aplique:

python manage.py makemigrations
python manage.py migrate
Migrations for 'crm':
  backend/crm/migrations/0001_initial.py
    - Create model Person
...
  Applying crm.0001_initial... OK

O jeito antigo: short_description

Em backend/crm/admin.py, vamos mostrar a data de publicação formatada como dd/mm/aaaa numa coluna da listagem. Para isso criamos o método get_published_date e colocamos o nome dele no list_display. Antes do Django 3.2, o título da coluna era definido assim:

# backend/crm/admin.py
from django.contrib import admin

from .models import Person


@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'email', 'get_published_date', 'active')
    search_fields = ('first_name', 'last_name', 'email')
    list_filter = ('active',)

    def get_published_date(self, obj):
        if obj.published_date:
            return obj.published_date.strftime('%d/%m/%Y')

    get_published_date.short_description = 'Data de Publicação'

O método recebe o objeto da linha (obj) e devolve o que vai aparecer na célula. Sem o short_description, o título da coluna seria o nome do método ("Get published date").

Suba o servidor, entre no Admin e cadastre uma pessoa com a data de publicação preenchida:

python manage.py runserver

A listagem mostra a coluna Data de publicação com a data no formato 15/07/2021.

O jeito novo: @admin.display(description=...)

A partir do Django 3.2, troque o atributo pelo decorator:

    @admin.display(description='Data de Publicação')
    def get_published_date(self, obj):
        if obj.published_date:
            return obj.published_date.strftime('%d/%m/%Y')

O resultado é o mesmo, mas a configuração fica junto da definição do método. Atenção à grafia: é description. No vídeo, um erro de digitação no nome do parâmetro derrubou o servidor, porque o display só aceita argumentos nomeados (boolean, ordering, description e empty_value).

Colunas booleanas: boolean=True

Agora vamos criar uma coluna que diz se a pessoa já foi publicada, ou seja, se published_date está preenchido:

    @admin.display(description='Publicado')
    def is_published(self, obj):
        return obj.published_date is not None

E acrescente 'is_published' no list_display. Repare que a coluna mostra o texto True ou False. Para mostrar os ícones verde e vermelho que o Admin usa nos campos booleanos, passe boolean=True:

    @admin.display(boolean=True, description='Publicado')
    def is_published(self, obj):
        return obj.published_date is not None

Para testar, cadastre outra pessoa (no vídeo, "Zezinho") primeiro sem data de publicação: a coluna "Publicado" mostra o ícone vermelho e a "Data de publicação" mostra -. Edite e preencha a data: o ícone fica verde.

Ordenando a coluna: ordering

Uma coluna calculada por um método não pode ser ordenada, porque o Admin não sabe qual campo do banco usar no ORDER BY. O parâmetro ordering diz isso a ele. Com '-published_date', clicar no título "Publicado" ordena pela data de publicação, da mais recente para a mais antiga:

    @admin.display(boolean=True, ordering='-published_date', description='Publicado')
    def is_published(self, obj):
        return obj.published_date is not None

No vídeo a ordem não mudou de primeira, por causa do ordering = ('first_name',) do Meta do modelo. Depois de comentar essa linha no models.py, a pessoa com data 16/07/2021 passou a aparecer antes da de 15/07/2021:

    class Meta:
        # ordering = ('first_name',)
        verbose_name = 'pessoa'
        verbose_name_plural = 'pessoas'

O admin.py completo

# backend/crm/admin.py
from django.contrib import admin

from .models import Person


@admin.register(Person)
class PersonAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'email', 'get_published_date', 'is_published', 'active')  # noqa E501
    search_fields = ('first_name', 'last_name', 'email')
    list_filter = ('active',)

    # def get_published_date(self, obj):
    #     if obj.published_date:
    #         return obj.published_date.strftime('%d/%m/%Y')

    # get_published_date.short_description = 'Data de Publicação'

    @admin.display(description='Data de Publicação')
    def get_published_date(self, obj):
        if obj.published_date:
            return obj.published_date.strftime('%d/%m/%Y')

    @admin.display(boolean=True, ordering='-published_date', description='Publicado')
    def is_published(self, obj):
        return obj.published_date is not None

O mesmo exemplo com o modelo Article

No README do projeto das dicas, o mesmo recurso foi mostrado com o modelo Article (que tem um campo published_date). Antigamente:

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'get_published_date')

    def get_published_date(self, obj):
        if obj.published_date:
            return obj.published_date.strftime('%d/%m/%Y')

    get_published_date.short_description = 'Data de Publicação'

... e ainda funciona. A partir do Django 3.2:

@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ('__str__', 'get_published_date', 'is_published')

    # Django 3.2+
    @admin.display(description='Data de Publicação')
    def get_published_date(self, obj):
        if obj.published_date:
            return obj.published_date.strftime('%d/%m/%Y')

    @admin.display(boolean=True, ordering='-published_date', description='Publicado')
    def is_published(self, obj):
        return obj.published_date is not None

Conclusão

O @admin.display não muda o que o Admin consegue fazer; ele junta numa linha, em cima do método, o que antes ficava espalhado em atributos (short_description, boolean, admin_order_field). Há ainda o parâmetro empty_value, que define o que aparece quando o método devolve um valor vazio (o padrão é -). Os detalhes estão na documentação.

Sugerir uma correção nesta página