Dica 20 - Templates
Versões usadas no vídeo: Django 4.1.3, Python 3.10, django-widget-tweaks 1.4.12 e Tailwind CSS com o template Windster (Flowbite).
Importante: nas versões antigas desta página as tags de template apareciam com uma \ no meio ({%). Se copiar código de lá, remova a \.
Até aqui o projeto Dicas de Django tem login e um dashboard, mas o dashboard é só um template: nada nele retorna dados reais. Nesta dica vamos fazer o item Usuários do menu funcionar e, com isso, criar um padrão de templates que será repetido em todas as apps do projeto:
- uma subpasta em
templatescom o mesmo nome da app; <model>_list.html(lista),<model>_detail.html(detalhes) e<model>_form.html(formulário de adicionar e de editar);- uma pasta
includescom os pedaços reaproveitáveis (breadcrumb, busca, botões de navegação e modal de exclusão); - as urls e views
list,create,detaileupdate, sempre na mesma ordem.
Neste vídeo o formulário ainda não salva; isso fica para as próximas dicas.
Pré-requisitos
- O projeto das dicas anteriores, na branch
maindo repositório rg3915/dicas-de-django. - O
docker-composedo projeto com PostgreSQL, pgAdmin e MailHog. O banco fica no container (porta 5431), mas o Django roda na máquina local (localhost:8000). - O usuário customizado com login por e-mail (dica 14), o
Profilena appaccounts(dica 19.7) e oCustomUserForm(dica 17). django-widget-tweaksinstalado e com'widget_tweaks'emINSTALLED_APPS.
Suba os containers, rode as migrations e crie os usuários:
docker-compose up -d
python manage.py migrate
python manage.py createsuperuser # admin@email.com
python manage.py createsuperuser # regis@email.com
python manage.py runserver
A estrutura de pastas
O Django sugere que, dentro de templates, exista uma subpasta com o mesmo nome da app. Assim o template é referenciado como accounts/user_list.html e não colide com templates de mesmo nome de outras apps.
A app accounts já tinha templates/email e templates/registration. Vamos criar templates/accounts e, dentro dela, a pasta includes:
mkdir -p backend/accounts/templates/accounts/includes
No vídeo a pasta foi criada primeiro como include, no singular, e depois renomeada com mv backend/accounts/templates/accounts/include/ backend/accounts/templates/accounts/includes.
Agora os arquivos. Um truque para criar cada arquivo já com um comentário dizendo o nome dele é usar echo:
echo '<!-- user_list.html -->' > backend/accounts/templates/accounts/user_list.html
echo '<!-- user_detail.html -->' > backend/accounts/templates/accounts/user_detail.html
echo '<!-- user_form.html -->' > backend/accounts/templates/accounts/user_form.html
echo '<!-- breadcrumb.html -->' > backend/accounts/templates/accounts/includes/breadcrumb.html
echo '<!-- delete_modal.html -->' > backend/accounts/templates/accounts/includes/delete_modal.html
echo '<!-- navigation_buttons.html -->' > backend/accounts/templates/accounts/includes/navigation_buttons.html
echo '<!-- search.html -->' > backend/accounts/templates/accounts/includes/search.html
Com tree backend/accounts a parte de templates fica assim:
templates
├── accounts
│ ├── includes
│ │ ├── breadcrumb.html
│ │ ├── delete_modal.html
│ │ ├── navigation_buttons.html
│ │ └── search.html
│ ├── user_detail.html
│ ├── user_form.html
│ └── user_list.html
├── email
│ └── account_activation_email.html
└── registration
├── login.html
├── password_reset_complete.html
├── password_reset_confirm.html
├── password_reset_done.html
├── password_reset_email.html
├── password_reset_form.html
└── registration_form.html
As urls
As rotas de usuário ficam numa lista própria, user_patterns, incluída em users/:
# accounts/urls.py
from django.contrib.auth.views import LoginView, LogoutView
from django.urls import include, path
from backend.accounts import views as v
# A ordem das urls é importante por causa do slug, quando existir.
user_patterns = [
path('', v.user_list, name='user_list'), # noqa E501
path('create/', v.user_create, name='user_create'), # noqa E501
path('<int:pk>/', v.user_detail, name='user_detail'), # noqa E501
path('<int:pk>/update/', v.user_update, name='user_update'), # noqa E501
]
urlpatterns = [
path('login/', LoginView.as_view(), name='login'), # noqa E501
path('logout/', LogoutView.as_view(), name='logout'), # noqa E501
...
path('users/', include(user_patterns)),
]
- O
# noqa E501serve só para o linter não reclamar de linha longa (e o autopep8 não quebrar a linha); fica mais fácil de ler. - Termine sempre a rota com
/, para manter o padrão. - A ordem é
list,create,detail,update. Ela importa quando a rota usa slug:create/precisa vir antes de<slug:slug>/, senão a palavra "create" seria interpretada como um slug.
Como a app accounts é incluída em accounts/, as rotas ficam /accounts/users/, /accounts/users/create/, /accounts/users/1/ e /accounts/users/1/update/.
A lista de usuários
Primeiro, o mínimo
Todo template começa com extends e block content. Para testar a rota, comece com uma tabela fixa:
<!-- user_list.html -->
{% extends "base.html" %}
{% block content %}
<h1>Lista</h1>
<table>
<tbody>
<tr>
<td>Usuário 1</td>
</tr>
<tr>
<td>Usuário 1</td>
</tr>
<tr>
<td>Usuário 1</td>
</tr>
</tbody>
</table>
{% endblock content %}
E a view:
# accounts/views.py
from django.shortcuts import get_object_or_404, redirect, render
from .forms import CustomUserForm
from .models import User
def user_list(request):
template_name = 'accounts/user_list.html'
object_list = User.objects.all()
context = {'object_list': object_list}
return render(request, template_name, context)
O padrão de nomes é sempre o mesmo: template_name, object_list para listas, object para um item e context. Repare que o User é importado de .models: estamos usando o usuário customizado do projeto, e não o User padrão do Django.
No menu lateral (core/templates/includes/aside.html), ligue o item Usuários à rota nova (as urls de accounts não têm namespace):
<!-- core/templates/includes/aside.html -->
<a href="{% url 'user_list' %}" class="text-base text-gray-900 font-normal rounded-lg hover:bg-gray-100 flex items-center p-2 group ">
...
<span class="ml-3 flex-1 whitespace-nowrap">Usuários</span>
</a>
Escondendo o admin
A lista mostra todos os usuários, inclusive o admin. Para mostrar todo mundo menos o admin, troque all() por exclude():
# accounts/views.py
def user_list(request):
template_name = 'accounts/user_list.html'
object_list = User.objects.exclude(email='admin@email.com')
context = {'object_list': object_list}
return render(request, template_name, context)
O template completo
Agora troque o conteúdo de user_list.html pelo template no estilo do dashboard (Tailwind, template Windster). Ele usa três includes, um link para criar usuário, a tabela com os dados do usuário e do perfil, e os botões Editar e Deletar de cada linha:
<!-- user_list.html -->
{% extends "base.html" %}
{% block content %}
<!-- ... -->
{% include "./includes/breadcrumb.html" %}
<h1 class="text-xl sm:text-2xl font-semibold text-gray-900">Usuários</h1>
<!-- ... -->
{% include "./includes/search.html" %}
<!-- ... (botões Adicionar e Exportar e o início da tabela) -->
{% for object in object_list %}
<tr class="hover:bg-gray-100">
<!-- ... (checkbox e foto) -->
<div class="text-sm font-normal text-gray-500">
<div class="text-base font-semibold text-gray-900">
<a href="{{ object.get_absolute_url }}" class="text-sm font-medium text-cyan-600 hover:bg-gray-100 rounded-lg">{{ object.get_full_name }}</a>
</div>
<div class="text-sm font-normal text-gray-500">{{ object.email }}</div>
</div>
</td>
<td class="p-4 whitespace-nowrap text-base font-medium text-gray-900">{{ object.profile.linkedin|default:"---" }}</td>
<td class="p-4 whitespace-nowrap text-base font-medium text-gray-900">{{ object.profile.birthday|date:"d/m/Y"|default:"---" }}</td>
<!-- ... (status: bolinha verde Ativo ou vermelha Inativo, conforme object.is_active) -->
<td class="p-4 whitespace-nowrap space-x-2">
<!-- ... (botão Editar) -->
<button type="button" data-modal-toggle="delete-user-modal" onclick="openModal()" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm inline-flex items-center px-3 py-2 text-center">
Deletar
</button>
</td>
</tr>
{% endfor %}
<!-- ... (rodapé da tabela: veja o arquivo completo no GitHub) -->
{% include "./includes/delete_modal.html" %}
{% endblock content %}
{% block js %}
<script>
const targetEl = document.getElementById('delete-user-modal')
const modal = new Modal(targetEl)
openModal = () => {
modal.show()
}
closeModal = () => {
modal.hide()
}
</script>
{% endblock js %}
Código completo: backend/accounts/templates/accounts/user_list.html
Destaques:
{% include "./includes/..." %}: o./faz o caminho ser relativo ao template atual, ou seja,accounts/includes/....- O nome é um link para
{{ object.get_absolute_url }}, que vamos criar no model a seguir. object.profile.linkedineobject.profile.birthdayvêm doProfile(OneToOne com o usuário); o filtrodefault:"---"mostra---quando o valor está vazio.- O status usa
is_active: bolinha verde "Ativo" ou vermelha "Inativo". - A paginação ("Mostrando 1-20 de 2290") e a busca ainda são fixas; serão feitas em dicas futuras.
O bloco js e o modal de exclusão
O template original abria o modal de exclusão com o atributo data-modal-toggle do Flowbite, mas no vídeo isso não funcionou de jeito nenhum. A solução foi abrir e fechar o modal com JavaScript, usando a classe Modal do Flowbite (que vem no app.bundle.js do template). Para isso o base.html ganha um bloco js no final do body, depois do script do template:
<!-- core/templates/base.html -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
<script src="{% static 'js/app.bundle.js' %}"></script>
{% block js %}{% endblock js %}
</body>
</html>
No user_list.html, o bloco js (visto acima) pega o elemento delete-user-modal, cria um new Modal(targetEl) e define openModal() e closeModal(). O botão Deletar chama openModal() e os botões de fechar do modal chamam closeModal(). A exclusão de fato fica para uma próxima aula.
Os includes
includes/breadcrumb.html
<!-- breadcrumb.html -->
<nav class="flex mb-5" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-2">
<li class="inline-flex items-center">
<a href="#" class="text-gray-700 hover:text-gray-900 inline-flex items-center">
<svg class="w-5 h-5 mr-2.5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path></svg>
Home
</a>
</li>
<li>
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
<a href="#" class="text-gray-700 hover:text-gray-900 ml-1 md:ml-2 text-sm font-medium">Usuários</a>
</div>
</li>
<li>
<div class="flex items-center">
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
<span class="text-gray-400 ml-1 md:ml-2 text-sm font-medium" aria-current="page">Lista</span>
</div>
</li>
</ol>
</nav>
includes/search.html
<!-- search.html -->
<div class="hidden sm:flex items-center sm:divide-x sm:divide-gray-100 mb-3 sm:mb-0">
<form class="lg:pr-3" action="#" method="GET">
<label for="users-search" class="sr-only">Busca</label>
<div class="mt-1 relative lg:w-64 xl:w-96">
<input id="id_search" name="search" type="text" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-cyan-600 focus:border-cyan-600 block w-full p-2.5" placeholder="Busca...">
</div>
</form>
<div class="flex space-x-1 pl-0 sm:pl-2 mt-3 sm:mt-0">
<a href="#" class="text-gray-500 hover:text-gray-900 cursor-pointer p-1 hover:bg-gray-100 rounded inline-flex justify-center">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path></svg>
</a>
<a href="#" class="text-gray-500 hover:text-gray-900 cursor-pointer p-1 hover:bg-gray-100 rounded inline-flex justify-center">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
</a>
<a href="#" class="text-gray-500 hover:text-gray-900 cursor-pointer p-1 hover:bg-gray-100 rounded inline-flex justify-center">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
</a>
<a href="#" class="text-gray-500 hover:text-gray-900 cursor-pointer p-1 hover:bg-gray-100 rounded inline-flex justify-center">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z"></path></svg>
</a>
</div>
</div>
includes/navigation_buttons.html
<!-- navigation_buttons.html -->
<div class="flex items-center space-x-3">
<a href="" class="flex-1 text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium inline-flex items-center justify-center rounded-lg text-sm px-3 py-2 text-center">
<svg class="-ml-1 mr-1 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
Anterior
</a>
<a href="" class="flex-1 text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium inline-flex items-center justify-center rounded-lg text-sm px-3 py-2 text-center">
Próximo
<svg class="-mr-1 ml-1 h-5 w-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path>
</svg>
</a>
</div>
includes/delete_modal.html
O id="delete-user-modal" é o elemento que o JavaScript do user_list.html procura.
<!-- delete_modal.html -->
<!-- Delete User Modal -->
<div class="hidden overflow-x-hidden overflow-y-auto fixed top-4 left-0 right-0 md:inset-0 z-50 justify-center items-center h-modal sm:h-full" id="delete-user-modal">
<div class="relative w-full max-w-md px-4 h-full md:h-auto">
<!-- Modal content -->
<div class="bg-white rounded-lg shadow relative">
<!-- Modal header -->
<div class="flex justify-end p-2">
<button type="button" onclick="closeModal()" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" data-modal-toggle="delete-user-modal">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
</button>
</div>
<!-- Modal body -->
<div class="p-6 pt-0 text-center">
<svg class="w-20 h-20 text-red-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<h3 class="text-xl font-normal text-gray-500 mt-5 mb-6">Tem certeza que você quer deletar este registro?</h3>
<a href="#" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-base inline-flex items-center px-3 py-2.5 text-center mr-2">
Sim, deletar
</a>
<a href="#" onclick="closeModal()" class="text-gray-900 bg-white hover:bg-gray-100 focus:ring-4 focus:ring-cyan-200 border border-gray-200 font-medium inline-flex items-center rounded-lg text-base px-3 py-2.5 text-center" data-modal-toggle="delete-user-modal">
Não, cancelar
</a>
</div>
</div>
</div>
</div>
Detalhes de um usuário
A view recebe o pk da url, busca o usuário com get_object_or_404 (que devolve 404 se não existir) e manda para o template com o nome object:
# accounts/views.py
def user_detail(request, pk):
template_name = 'accounts/user_detail.html'
instance = get_object_or_404(User, pk=pk)
context = {'object': instance}
return render(request, template_name, context)
No model User, crie o get_absolute_url, que devolve a url de detalhes do próprio objeto. É ele que a lista usa no link do nome:
# accounts/models.py
from django.urls import reverse_lazy
...
class User(AbstractBaseUser, PermissionsMixin):
...
def get_absolute_url(self):
return reverse_lazy('user_detail', kwargs={'pk': self.pk})
O template de detalhes:
<!-- user_detail.html -->
{% extends "base.html" %}
{% block content %}
<div class="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8 ">
<div class="mb-4">
{% include "./includes/breadcrumb.html" %}
<h1 class="text-xl sm:text-2xl font-semibold text-gray-900">Detalhes: {{ object.get_full_name }}</h1>
</div>
<div class="mb-5">
<a href="javascript: history.go(-1)" class="text-base font-medium text-cyan-600 hover:bg-gray-100 rounded-lg">Voltar</a>
</div>
<!-- ... (foto, nome e e-mail) -->
<!-- Nascimento -->
<div class="block w-full overflow-x-auto">
<div class="mt-2">
<div class="text-sm font-normal text-gray-500">Nascimento</div>
{% if object.profile.birthday %}
<div class="text-base font-semibold text-gray-900">{{ object.profile.birthday|date:"l" }}, {{ object.profile.birthday }}</div>
{% else %}
<div class="text-base font-semibold text-gray-900">---</div>
{% endif %}
</div>
</div>
<!-- ... (LinkedIn, CPF e RG) -->
<!-- Inscrito -->
<div class="block w-full overflow-x-auto">
<div class="mt-2">
<div class="text-sm font-normal text-gray-500">Inscrito</div>
<div class="text-base font-semibold text-gray-900">{{ object.date_joined|date:"d/m/Y H:i" }}</div>
</div>
</div>
<!-- ... (Ativo e Admin, com bolinha verde ou vermelha: veja o arquivo completo no GitHub) -->
</div>
{% endblock content %}
Código completo: backend/accounts/templates/accounts/user_detail.html
Alguns filtros de data:
{{ object.profile.birthday|date:"l" }}mostra o dia da semana por extenso. Com o idioma em português, uma data de nascimento cadastrada no perfil pelo admin aparece como "sexta-feira, 28 de Dezembro de 1979".{{ object.date_joined|date:"d/m/Y H:i" }}mostra dia/mês/ano hora:minuto.- Para os booleanos (
is_activeeis_admin) usamos{% if %}, com bolinha verde ou vermelha. Se você desmarcar "ativo" de um usuário no admin, ele aparece como Inativo.
O formulário de adicionar
Primeiro o template, o básico que você vai usar em qualquer formulário:
<form action="." method="POST">;enctype="multipart/form-data", obrigatório quando o formulário faz upload de arquivos;{% csrf_token %};- um
foremform.visible_fields, renderizando cada campo com orender_fielddodjango-widget-tweaks(que permite passar as classes CSS do Tailwind direto no template); - um botão do tipo
submit.
<!-- user_form.html -->
{% extends "base.html" %}
{% load widget_tweaks %}
{% block content %}
<div class="mx-auto md:h-screen flex flex-col px-6 pt-8 pt:mt-0">
<!-- Card -->
<div class="bg-white shadow rounded-lg md:mt-0 w-full sm:max-w-screen-sm xl:p-0">
<div class="p-6 sm:p-8 lg:p-16 space-y-8">
<h2 class="text-2xl lg:text-3xl font-bold text-gray-900">
{% if object.pk %}
Editar
{% else %}
Adicionar
{% endif %}
Usuário
</h2>
<form class="mt-8 space-y-6" action="." method="POST" enctype="multipart/form-data">
{% csrf_token %}
{% for field in form.visible_fields %}
<div>
<label class="text-sm font-medium text-gray-900 block mb-2">{{ field.label }}</label>
{% render_field field class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-cyan-600 focus:border-cyan-600 block w-full p-2.5" %}
</div>
<span class="text-sm font-medium text-gray-500">{{ field.help_text }}</span>
{% for error in field.errors %}
<span class="text-red-500">{{ error }}</span> <br>
{% endfor %}
{% endfor %}
<div class="flex flex-col sm:flex-row">
<button type="submit" class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">Salvar</button>
<a href="{% url 'user_list' %}" class="text-gray-900 bg-white border border-gray-300 hover:bg-gray-100 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-sm px-5 py-3 mt-2 sm:mt-0 sm:ml-2 w-full sm:w-auto text-center">
Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
{% endblock content %}
O Cancelar volta para a lista de usuários. O título mostra Editar quando existe object.pk (edição) e Adicionar quando não existe.
A view usa o CustomUserForm, criado anteriormente em accounts/forms.py:
# accounts/forms.py
from django import forms
from backend.accounts.models import User
class CustomUserForm(forms.ModelForm):
first_name = forms.CharField(
label='Nome',
max_length=150,
)
last_name = forms.CharField(
label='Sobrenome',
max_length=150,
)
email = forms.EmailField(
label='E-mail',
)
class Meta:
model = User
fields = (
'first_name',
'last_name',
'email',
)
# accounts/views.py
def user_create(request):
template_name = 'accounts/user_form.html'
form = CustomUserForm(request.POST or None)
context = {'form': form}
return render(request, template_name, context)
request.POST or None: num GET o formulário vem vazio; num POST ele recebe os dados enviados. Por enquanto a view só exibe o formulário; salvar fica para a próxima aula.
Repare que o padrão é sempre o mesmo nome de template, user_form.html, tanto para adicionar quanto para editar. Se a view apontar para outro nome (no vídeo, um user_create.html digitado por engano), o Django dá TemplateDoesNotExist.
O formulário de editar
A edição precisa saber qual usuário editar, por isso a rota tem o pk. A view é uma mistura da user_detail com a user_create:
# accounts/views.py
def user_update(request, pk):
template_name = 'accounts/user_form.html'
instance = get_object_or_404(User, pk=pk)
form = CustomUserForm(request.POST or None, instance=instance)
context = {
'object': instance,
'form': form,
}
return render(request, template_name, context)
instance=instancefaz o formulário vir preenchido com os dados do usuário.'object': instanceno contexto faz o título do template mostrar Editar.
O botão Editar da lista já aponta para {% url 'user_update' object.pk %}.
Testando
Cadastre mais um usuário pelo admin (no vídeo, "Donald") e volte para Usuários: ele aparece na lista. Clicando no nome, vemos os detalhes; em Editar, o formulário vem preenchido; em Adicionar, o formulário vem vazio; em Deletar, o modal abre e fecha.
Resumo do padrão
| Ação | url | name | view | template |
|---|---|---|---|---|
| Lista | users/ |
user_list |
user_list |
accounts/user_list.html |
| Adicionar | users/create/ |
user_create |
user_create |
accounts/user_form.html |
| Detalhes | users/<int:pk>/ |
user_detail |
user_detail |
accounts/user_detail.html |
| Editar | users/<int:pk>/update/ |
user_update |
user_update |
accounts/user_form.html |
Nas próximas dicas, salvar o formulário e validar os dados (Dica 22) e repetir o mesmo padrão para os produtos (Dica 23).
Observação: o serviço de imagens via.placeholder.com, usado nos templates para a foto do usuário, não funciona mais; troque por outra imagem se for rodar hoje.