Views, URLs e Templates no Django
Entenda o fluxo completo de uma requisição — do endereço no browser até o HTML na tela — com exemplos reais do projeto Consulta.me.
O Trio MVT — Como o Django divide responsabilidades
O Django organiza o código em três camadas que trabalham juntas. Cada uma tem uma única responsabilidade — isso facilita manutenção, teste e reutilização do código.
| Camada | Arquivo no projeto | Pergunta que responde | Analogia |
|---|---|---|---|
| URL | urls.py |
"Qual view executa este endereço?" | Recepcionista: direciona a chamada para a pessoa certa |
| View | views.py |
"O que fazer com esta requisição?" | Cozinheiro: recebe o pedido, prepara e entrega |
| Template | templates/*.html |
"Como exibir os dados ao usuário?" | Prato: apresentação final do que foi preparado |
Fluxo de uma Requisição — O mapa completo
Antes de entrar nos detalhes, veja o percurso completo de uma requisição no nosso projeto. Usaremos o exemplo de acessar a listagem de médicos (/medicos/):
URLs — O Sistema de Roteamento
O arquivo urls.py é a primeira coisa que o Django verifica quando recebe uma requisição. Ele compara o endereço acessado com uma lista de padrões (patterns) e encaminha para a view correspondente.
No nosso projeto temos dois arquivos de URLs trabalhando juntos:
path() — Criando padrões de URL
A função path() é a peça fundamental do roteamento. Ela recebe três argumentos principais:
path( rota, view, name=... )
# ↑ ↑ ↑
# | | └── apelido para referenciar esta URL pelo nome (opcional mas recomendado)
# | └───────── a view que será executada quando a rota bater
# └───────────────── o padrão da URL (string ou converter)
URLs estáticas — sem parâmetros
path('', views.index, name='index')
# '' = URL raiz: http://site.com/
# index = a função/classe view a executar
# 'index' = nome desta rota
path('home/', views.home, name='home')
# 'home/' = URL: http://site.com/home/
path('medicos/', views.MedicoListView.as_view(), name='medico-list')
# 'medicos/' = URL: http://site.com/medicos/
# .as_view() = obrigatório ao usar class-based views
URLs dinâmicas — com parâmetros
Usamos conversores entre < > para capturar partes variáveis da URL e passá-las automaticamente para a view:
path('medicos/<int:pk>/editar/', views.MedicoUpdateView.as_view(), name='medico-update')
# ↑────────↑
# conversor:nome_do_parametro
#
# <int:pk> captura um número inteiro da URL e o passa para a view como argumento "pk"
# Exemplos de URLs que batem neste padrão:
# /medicos/1/editar/ → pk=1
# /medicos/42/editar/ → pk=42
# /medicos/abc/editar/ → NÃO bate (abc não é int)
path('medicos/<int:pk>/excluir/', views.MedicoDeleteView.as_view(), name='medico-delete')
path('pacientes/<int:pk>/editar/', views.PacienteUpdateView.as_view(), name='paciente-update')
Tipos de conversores disponíveis
| Conversor | Captura | Exemplo de URL | Valor passado à view |
|---|---|---|---|
<int:pk> | Número inteiro | /medicos/5/editar/ | pk=5 (int) |
<str:nome> | Texto sem barra | /busca/joao/ | nome="joao" (str) |
<slug:slug> | Letras, números, hífen | /artigo/meu-titulo/ | slug="meu-titulo" |
<uuid:id> | UUID formatado | /item/550e8400.../ | objeto UUID |
'medicos/novo/' deve vir ANTES de 'medicos/<int:pk>/editar/' — caso contrário, "novo" poderia ser interpretado como um pk.
include() — Separando URLs por app
Em vez de colocar todas as rotas em um único arquivo enorme, o Django permite delegar grupos de URLs para outros arquivos com o include(). O arquivo raiz (sistema/urls.py) fica limpo e apenas distribui para os apps.
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path, include # include() importado aqui
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# ↑ padrão do Django — painel admin
path('login/', auth_views.LoginView.as_view(
template_name='intranet/login.html'), name='login'),
# ↑ view de login pronta do Django usando nosso template
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
# ↑ view de logout pronta — redireciona para LOGOUT_REDIRECT_URL do settings
path('', include('intranet.urls')),
# ↑ include(): delega TODAS as demais URLs para o arquivo intranet/urls.py
# '' significa: sem prefixo — as URLs do app ficam na raiz do site
# Se fosse path('app/', include('intranet.urls')), ficariam em /app/medicos/, /app/home/ etc.
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# static(): em modo DEBUG, o Django mesmo serve os arquivos de upload (pasta /media/)
from django.urls import path
from . import views # importa views.py da mesma pasta (intranet/)
# app_name define o "namespace" (espaço de nomes) para este app
# É como um "sobrenome" das URLs: intranet:medico-list, intranet:home, etc.
app_name = 'intranet'
urlpatterns = [
path('', views.index, name='index'), # /
path('home/', views.home, name='home'), # /home/
# — Médicos —
path('medicos/', views.MedicoListView.as_view(), name='medico-list'),
path('medicos/novo/', views.MedicoCreateView.as_view(), name='medico-create'),
path('medicos/<int:pk>/editar/', views.MedicoUpdateView.as_view(), name='medico-update'),
path('medicos/<int:pk>/excluir/', views.MedicoDeleteView.as_view(), name='medico-delete'),
# — Pacientes —
path('pacientes/', views.PacienteListView.as_view(), name='paciente-list'),
path('pacientes/novo/', views.PacienteCreateView.as_view(), name='paciente-create'),
path('pacientes/<int:pk>/editar/', views.PacienteUpdateView.as_view(), name='paciente-update'),
path('pacientes/<int:pk>/excluir/', views.PacienteDeleteView.as_view(), name='paciente-delete'),
# — Consultas —
path('consultas/', views.ConsultaListView.as_view(), name='consulta-list'),
path('consultas/nova/', views.ConsultaCreateView.as_view(), name='consulta-create'),
path('consultas/<int:pk>/editar/', views.ConsultaUpdateView.as_view(), name='consulta-update'),
path('consultas/<int:pk>/excluir/', views.ConsultaDeleteView.as_view(), name='consulta-delete'),
]
name e reverse — Referenciando URLs pelo apelido
Cada URL tem um name (apelido). Em vez de escrever a URL como string fixa (o que quebraria se você mudasse o padrão), você sempre referencia pelo nome.
# ── 1. No Python (views.py) ──────────────────────────────────────────────────
from django.urls import reverse, reverse_lazy
# reverse(): converte o name em caminho real AGORA (avaliação imediata)
url = reverse('intranet:medico-list') # retorna '/medicos/'
# reverse_lazy(): converte o name QUANDO necessário (avaliação tardia)
# Obrigatório em class-based views porque as URLs ainda não estão prontas
# quando a classe é definida no início do arquivo
success_url = reverse_lazy('intranet:medico-list') # '/medicos/'
# Com parâmetro dinâmico (pk):
url_editar = reverse('intranet:medico-update', kwargs={'pk': 5})
# retorna '/medicos/5/editar/'
<!-- Sem parâmetro -->
<a href="{% url 'intranet:medico-list' %}">Ver Médicos</a>
<!-- gera: href="/medicos/" -->
<!-- Com parâmetro pk (capturado da URL atual ou do loop) -->
<a href="{% url 'intranet:medico-update' medico.pk %}">Editar</a>
<!-- se medico.pk = 3, gera: href="/medicos/3/editar/" -->
<a href="{% url 'intranet:medico-delete' medico.pk %}">Excluir</a>
<!-- se medico.pk = 3, gera: href="/medicos/3/excluir/" -->
<!-- Auth (sem namespace porque está em sistema/urls.py direto) -->
<a href="{% url 'login' %}">Login</a> <!-- /login/ -->
<form action="{% url 'logout' %}">...</form> <!-- /logout/ -->
href="/medicos/" em 10 templates e um dia precisar mudar para /lista-medicos/, terá que alterar 10 arquivos. Com {% url 'intranet:medico-list' %}, você muda apenas o padrão em urls.py e todos os links atualizam automaticamente.
O Objeto request — O que a View recebe
Toda view recebe como primeiro argumento o objeto request. Ele contém tudo sobre a requisição que chegou: quem enviou, qual método HTTP, quais dados foram mandados, etc.
def exemplo_view(request):
# ── Método HTTP ───────────────────────────────────────────────────────────
request.method # 'GET' ou 'POST' (ou 'PUT', 'DELETE', etc.)
# ── Usuário autenticado ───────────────────────────────────────────────────
request.user # objeto User do Django (ou AnonymousUser se não logado)
request.user.username # 'william'
request.user.is_authenticated # True/False — se está logado
request.user.is_staff # True/False — se é administrador
# ── Dados enviados pelo formulário (POST) ─────────────────────────────────
request.POST # dicionário com os dados do formulário
request.POST.get('nome') # valor do campo "nome" (ou None se não existir)
request.POST.get('email', '') # valor do campo "email" (ou '' se não existir)
# ── Parâmetros da URL (GET) ───────────────────────────────────────────────
request.GET # dicionário com parâmetros da query string
# ex: /medicos/?busca=joao → request.GET.get('busca') == 'joao'
# ── Arquivos enviados (upload) ────────────────────────────────────────────
request.FILES # dicionário com arquivos enviados
# ── Sessão ───────────────────────────────────────────────────────────────
request.session # dicionário persistente entre requisições (mantém login)
# ── Caminho acessado ─────────────────────────────────────────────────────
request.path # '/medicos/novo/'
# ── Como usamos no projeto ────────────────────────────────────────────────────
def index(request):
if request.user.is_authenticated: # ← usa request.user
return redirect('intranet:home')
return render(request, 'intranet/index.html')
request em todos os templates automaticamente (graças ao context_processors.request no settings). Por isso podemos usar {{ request.user.username }} diretamente no HTML sem precisar passar pelo contexto da view.
render() e redirect() — O que a View retorna
Uma view sempre precisa retornar uma resposta HTTP. As duas funções mais usadas são render() e redirect().
from django.shortcuts import render
def home(request):
# ↓ request ↓ caminho do template ↓ contexto (opcional)
return render(request, 'intranet/home.html', context)
# render() faz três coisas em uma linha:
# 1. Carrega o arquivo intranet/templates/intranet/home.html
# 2. Processa as tags {{ }} e {% %} usando o contexto
# 3. Devolve o HTML gerado como resposta HTTP 200
# Com contexto:
medicos = Medico.objects.filter(ativo=True)
return render(request, 'intranet/medico/listagem.html', {
'medicos': medicos, # disponível no template como {{ medicos }}
'titulo': 'Lista' # disponível no template como {{ titulo }}
})
from django.shortcuts import redirect
# Formas de usar redirect():
# 1. Pelo name da URL (mais comum e recomendado)
return redirect('intranet:home') # vai para /home/
return redirect('intranet:medico-list') # vai para /medicos/
# 2. Pelo caminho direto
return redirect('/medicos/')
# 3. Com parâmetro (para editar registro específico)
return redirect('intranet:medico-update', pk=medico.id)
# redirect() retorna HTTP 302 (Found) — o browser faz nova requisição para a nova URL
# Isso é o padrão "Post/Redirect/Get" — evita reenvio do formulário ao recarregar
Post/Redirect/Get — Por que redirecionar após o POST?
Function-based Views — Funções simples
Function-based views (FBV) são simples funções Python. Você tem controle total sobre o código — ideal para lógicas que não seguem o padrão CRUD.
def index(request):
"""
Página inicial (/).
Lógica: se já está logado → vai para home, senão → mostra landing page.
"""
if request.user.is_authenticated:
# is_authenticated: True se existe sessão de login ativa
return redirect('intranet:home') # redireciona: HTTP 302 para /home/
# Caso contrário, renderiza a landing page (sem menu de logado)
return render(request, 'intranet/index.html')
# Não passa contexto porque index.html não precisa de dados do banco
def home(request):
"""
Página inicial pós-login (/home/).
Simples: só renderiza o template.
A proteção é feita pelo decorator @login_required.
"""
return render(request, 'intranet/home.html')
# @login_required: se não estiver logado, redireciona para LOGIN_URL (/login/?next=/home/)
# O decorator "envolve" a função — é executado antes dela
Class-based Views — O CRUD pronto
O Django oferece views genéricas que implementam operações comuns. Em vez de escrever o fluxo inteiro, você apenas herda a classe e define o que é específico do seu caso.
| Classe | HTTP | O que faz no projeto | URL exemplo |
|---|---|---|---|
ListView | GET | Lista todos os médicos ativos | /medicos/ |
CreateView | GET + POST | Exibe form vazio / salva novo médico | /medicos/novo/ |
UpdateView | GET + POST | Exibe form preenchido / salva edição | /medicos/5/editar/ |
DeleteView | GET + POST | Confirma / executa exclusão | /medicos/5/excluir/ |
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy
class MedicoListView(LoginRequiredMixin, ListView):
# ↑──────────────────↑ ↑──────↑
# Mixin de login View genérica de listagem
#
# Herança múltipla: MedicoListView tem o comportamento do ListView
# + a proteção de login do LoginRequiredMixin
model = Medico # qual tabela do banco consultar
template_name = 'intranet/medico/listagem.html' # template a renderizar
context_object_name = 'medicos'
# context_object_name: nome da variável no template
# sem isso, o padrão seria 'object_list' — muito genérico
# com isso, no template usamos {{ medicos }} em vez de {{ object_list }}
def get_queryset(self):
# Sobrescreve a query padrão (que retornaria TODOS os médicos)
# filter(ativo=True) → só médicos ativos
# order_by('nome') → ordenados por nome A-Z
return Medico.objects.filter(ativo=True).order_by('nome')
# O ListView chama get_queryset() automaticamente e passa o resultado
# para o template no contexto como 'medicos'
class MedicoCreateView(LoginRequiredMixin, CreateView):
model = Medico
form_class = MedicoForm # qual formulário usar (de forms.py)
template_name = 'intranet/medico/formulario.html'
success_url = reverse_lazy('intranet:medico-list')
# success_url: para onde redirecionar APÓS salvar com sucesso
# reverse_lazy() porque a URL é resolvida em tempo de execução
# O CreateView faz isso automaticamente:
# GET → cria form vazio, renderiza template com o form
# POST → valida form, salva, redireciona para success_url
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
# super()... chama o get_context_data original → garante que 'form' está no ctx
ctx['titulo'] = 'Cadastro de Médico'
# adiciona 'titulo' ao contexto → {{ titulo }} no template = "Cadastro de Médico"
return ctx
class MedicoUpdateView(LoginRequiredMixin, UpdateView):
model = Medico
form_class = MedicoForm
template_name = 'intranet/medico/formulario.html' # mesmo template do Create!
success_url = reverse_lazy('intranet:medico-list')
# O UpdateView faz isso automaticamente:
# GET → busca o objeto pelo pk da URL, cria form preenchido, renderiza
# POST → valida form, atualiza registro, redireciona para success_url
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['titulo'] = 'Editar Médico'
# Mesmo template, título diferente — o CreateView recebe "Cadastro",
# o UpdateView recebe "Editar"
return ctx
class MedicoDeleteView(LoginRequiredMixin, DeleteView):
model = Medico
template_name = 'intranet/medico/confirmar_exclusao.html'
success_url = reverse_lazy('intranet:medico-list')
# O DeleteView faz isso automaticamente:
# GET → busca objeto pelo pk, renderiza template de confirmação
# POST → deleta o objeto, redireciona para success_url
# Exige POST para deletar — protege contra exclusão acidental por link/bot
pk da URL (/medicos/5/editar/), busca o objeto Medico.objects.get(pk=5) e preenche o formulário com os dados existentes. Se o pk não existir, retorna HTTP 404 automaticamente.
get_context_data — Passando dados para o Template
O contexto é o dicionário que a view monta e entrega ao template. Cada chave do dicionário vira uma variável disponível no HTML.
class ConsultaCreateView(LoginRequiredMixin, CreateView):
model = Consulta
form_class = ConsultaForm
template_name = 'intranet/consulta/formulario.html'
success_url = reverse_lazy('intranet:consulta-list')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
# Após super(), ctx já contém:
# 'form' → instância do ConsultaForm
# 'object' → None (CreateView não tem objeto existente)
# 'view' → self (a própria view)
ctx['titulo'] = 'Agendamento de Consulta'
# {{ titulo }} no template
ctx['medicos_json'] = _medicos_json()
# _medicos_json() retorna string JSON com todos os médicos ativos
# Usado pelo JavaScript no template para filtrar médicos por especialidade
# {{ medicos_json|escapejs }} no template
ctx['especialidades'] = ESPECIALIDADES
# Lista de tuplas de especialidades
# Disponível no template mas não usada diretamente (o form já tem o select)
return ctx
# ── A função auxiliar usada acima ──────────────────────────────────────────────
import json
def _medicos_json():
# .values() retorna dicionários Python (em vez de objetos Model)
# Mais leve e diretamente serializável para JSON
medicos = list(
Medico.objects.filter(ativo=True)
.order_by('nome')
.values('id', 'nome', 'sobrenome', 'especialidade')
)
return json.dumps(medicos, ensure_ascii=False)
# ensure_ascii=False → mantém acentos: "Cardiologia" não vira é...
Proteção de Login — Quem pode acessar cada view
No nosso projeto, apenas usuários autenticados podem acessar as telas internas. O Django oferece dois mecanismos equivalentes — um para funções, outro para classes:
from django.contrib.auth.decorators import login_required # para funções
from django.contrib.auth.mixins import LoginRequiredMixin # para classes
# ── Para Function-based views: decorator ──────────────────────────────────────
@login_required
# ↑ Decorator: executa ANTES da função
# Se o usuário não estiver logado:
# → redireciona para LOGIN_URL (settings.py: '/login/')
# → adiciona ?next=/home/ na URL para retornar após o login
def home(request):
return render(request, 'intranet/home.html')
# ── Para Class-based views: Mixin ─────────────────────────────────────────────
class MedicoListView(LoginRequiredMixin, ListView):
# ↑────────────────↑
# Mixin de login — DEVE ser o PRIMEIRO na herança
# Se vier depois do ListView, pode não funcionar
model = Medico
# ...
# ── O que acontece quando acessa sem login ────────────────────────────────────
# Usuário tenta acessar /medicos/ sem estar logado:
# → LoginRequiredMixin detecta: request.user.is_authenticated == False
# → Redireciona para: /login/?next=/medicos/
# → Após login: Django lê o parâmetro "next" e redireciona para /medicos/
Sintaxe dos Templates — Variáveis e Tags
Os templates Django misturam HTML puro com marcadores especiais. Existem dois tipos de marcadores:
| Marcador | Tipo | Função | Exemplo |
|---|---|---|---|
{{ ... }} | Variável | Exibe um valor do contexto | {{ medico.nome }} |
{% ... %} | Tag | Executa lógica (if, for, url…) | {% if user.is_authenticated %} |
{# ... #} | Comentário | Ignorado na saída HTML | {# TODO: adicionar paginação #} |
<!-- Variável simples -->
{{ titulo }}
<!-- se contexto tem titulo='Listagem de Médicos', renderiza: Listagem de Médicos -->
<!-- Atributo de objeto -->
{{ medico.nome }}
{{ medico.especialidade }}
{{ medico.criacao_data }}
<!-- Acesso aninhado — ForeignKey -->
{{ consulta.paciente_id.nome }}
<!-- consulta → paciente_id (objeto Paciente) → .nome -->
{{ consulta.medico_id.especialidade }}
<!-- consulta → medico_id (objeto Medico) → .especialidade -->
<!-- Método do modelo (sem parênteses nos templates!) -->
{{ consulta.get_status_display }}
<!-- chama consulta.get_status_display() → retorna "Agendada" em vez de "A" -->
<!-- Variável do usuário (disponível globalmente) -->
{{ request.user.username }}
{{ request.user.get_full_name }}
Herança de Templates — extends e block
A herança de templates evita repetição de código. O template pai define a estrutura completa com blocos vazios; os filhos apenas preenchem os blocos que precisam.
{% load static %}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<title>
{% block title %}Consulta.me{% endblock %}
<!-- ↑ bloco com valor padrão: se o filho não preencher, usa "Consulta.me" -->
</title>
<link rel="stylesheet" href="{% static 'intranet/style.css' %}">
{% block extra_head %}{% endblock %} <!-- bloco vazio: filhos podem inserir CSS extra -->
</head>
<body>
{% block header %}
<!-- Header padrão: logo + botão Login -->
<header class="header">...</header>
{% endblock %}
<!-- ↑ base_logado.html substitui este bloco inteiro -->
{% block content %}{% endblock %}
<!-- ↑ bloco vazio — cada página filha coloca seu conteúdo aqui -->
<footer class="footer">...</footer>
{% block extra_scripts %}{% endblock %}
<!-- ↑ bloco vazio para scripts JS extras (ex: consulta/formulario.html) -->
</body></html>
{% extends 'intranet/base_logado.html' %}
<!-- ↑ DEVE ser a primeira linha — declara de qual pai este template herda -->
<!-- Herda: base_logado.html → que por sua vez herda base.html -->
<!-- Resultado: tem o layout completo (head, header logado, footer) -->
{% load static %}
<!-- ↑ mesmo herdando, precisa carregar as tags aqui -->
{% block title %}Listagem de Médicos{% endblock %}
<!-- substitui o bloco title de base.html: <title>Listagem de Médicos</title> -->
{% block content %}
<!-- tudo aqui vai para dentro do bloco content de base.html -->
<main class="container">
<section class="card">
<h2 class="title">Listagem de médicos</h2>
<!-- ... conteúdo da página ... -->
</section>
</main>
{% endblock %}
<!-- Não precisa definir extra_scripts pois não usa JS extra nesta página -->
{{ block.super }}. Exemplo: o filho pode ter {% block title %}{{ block.super }} — Médicos{% endblock %} que geraria "Consulta.me — Médicos".
Tags de Controle — if, for, url, static, csrf_token
<!-- ── {% if %} — Condicional ──────────────────────────────────────────── -->
{% if medicos %}
<p>Há {{ medicos|length }} médicos cadastrados.</p>
{% elif request.user.is_staff %}
<p>Nenhum médico. Você pode cadastrar um.</p>
{% else %}
<p>Nenhum médico encontrado.</p>
{% endif %}
<!-- Operadores disponíveis: ==, !=, <, >, in, not in, is, is not -->
<!-- ── {% for %} — Loop ─────────────────────────────────────────────────── -->
{% for medico in medicos %}
<tr>
<td>{{ medico.nome }}</td>
<td>{{ forloop.counter }}</td>
<!-- forloop.counter: número da iteração atual (começa em 1) -->
<!-- forloop.counter0: começa em 0 -->
<!-- forloop.first: True na primeira iteração -->
<!-- forloop.last: True na última iteração -->
</tr>
{% empty %}
<!-- {% empty %}: exibido apenas se a lista estiver vazia -->
<tr><td colspan="5">Nenhum médico cadastrado.</td></tr>
{% endfor %}
<!-- ── {% url %} — Gera URLs a partir do name ───────────────────────────── -->
<a href="{% url 'intranet:medico-list' %}">Médicos</a>
<a href="{% url 'intranet:medico-update' medico.pk %}">Editar</a>
<a href="{% url 'login' %}">Login</a>
<form action="{% url 'logout' %}"></form>
<!-- ── {% static %} — Resolve caminho de arquivos estáticos ─────────────── -->
{% load static %}
<!-- OBRIGATÓRIO antes de usar {% static %} -->
<link rel="stylesheet" href="{% static 'intranet/style.css' %}">
<img src="{% static 'intranet/assets/logo.svg' %}">
<img src="{% static 'intranet/assets/edit.svg' %}">
<!-- ── {% csrf_token %} — Token de segurança para formulários POST ────────── -->
<form method="post">
{% csrf_token %}
<!-- Gera um campo hidden com um token único:
<input type="hidden" name="csrfmiddlewaretoken" value="abc123xyz...">
O Django verifica este token ao receber o POST.
Se estiver ausente ou inválido → erro 403 Forbidden.
SEMPRE necessário em formulários POST! -->
...
</form>
<!-- ── {% load %} — Carrega bibliotecas de tags ────────────────────────── -->
{% load static %} <!-- carrega tags: {% static %} -->
<!-- Deve ser chamado em cada template que usa as tags, mesmo nos filhos -->
Filtros — Transformando variáveis no Template
Filtros são modificadores aplicados a variáveis com o símbolo |. Eles transformam o valor antes de exibir — sem precisar de lógica na view.
<!-- Sintaxe: {{ variavel|filtro }} ou {{ variavel|filtro:argumento }} -->
<!-- ── Filtros usados no projeto ──────────────────────────────────────────── -->
{{ consulta.horario|date:"d/m/Y H:i" }}
<!-- date: formata um objeto datetime -->
<!-- "d/m/Y H:i" → ex: 20/05/2026 14:30 -->
<!-- Outros formatos: "d/m/Y" → 20/05/2026 | "H:i" → 14:30 | "d \d\e F" → 20 de Maio -->
{{ request.user.get_full_name|default:request.user.username }}
<!-- default: usa o valor após | se o valor anterior for vazio/falsy -->
<!-- Se get_full_name = "" → usa username; se = "João Silva" → usa "João Silva" -->
{{ medicos_json|escapejs }}
<!-- escapejs: escapa o valor para uso seguro dentro de strings JavaScript -->
<!-- ex: aspas simples viram \', quebras de linha viram \n etc. -->
<!-- ── Outros filtros úteis ────────────────────────────────────────────────── -->
{{ texto|upper }} <!-- MAIÚSCULAS -->
{{ texto|lower }} <!-- minúsculas -->
{{ texto|title }} <!-- Primeira Letra De Cada Palavra Em Maiúscula -->
{{ texto|capfirst }} <!-- Primeira letra maiúscula -->
{{ lista|length }} <!-- conta itens: {{ medicos|length }} → 5 -->
{{ lista|first }} <!-- primeiro item da lista -->
{{ lista|last }} <!-- último item -->
{{ numero|floatformat:2 }} <!-- 3.14159 → "3.14" -->
{{ texto|truncatewords:10 }} <!-- trunca após 10 palavras e adiciona "..." -->
{{ texto|linebreaks }} <!-- converte \n em <p> e <br> HTML -->
{{ html|safe }} <!-- renderiza HTML sem escapar (use com cuidado!) -->
{{ valor|yesno:"Sim,Não" }} <!-- True → "Sim", False → "Não" -->
{{ data|timesince }} <!-- "3 dias atrás", "2 horas atrás" etc. -->
{{ conteudo|safe }} desativa o escape automático do Django e renderiza HTML bruto. Nunca use em dados vindos do usuário sem sanitização — isso pode causar ataques XSS (injeção de scripts maliciosos).
Formulários no Template — Renderizando o Form
Quando uma view passa um formulário (form) no contexto, o template pode renderizá-lo de várias formas. No nosso projeto renderizamos campo por campo para controlar o layout com CSS.
<form method="post">
{% csrf_token %}
<!-- Campo por campo: dá controle total sobre o HTML gerado -->
<div class="form-group">
<label for="id_nome">Nome:</label>
{{ form.nome }}
<!-- ↑ renderiza o <input type="text" name="nome" id="id_nome" ...> -->
<!-- O Django gera id="id_NOME" automaticamente → bate com o for="id_nome" do label -->
{% if form.nome.errors %}
<p style="color:red;">{{ form.nome.errors }}</p>
<!-- form.nome.errors: lista de erros deste campo específico -->
<!-- ex: ["Este campo é obrigatório."] -->
{% endif %}
</div>
<div class="form-group">
<label for="id_especialidade">Especialidade:</label>
{{ form.especialidade }}
<!-- Para ChoiceField renderiza <select><option>s</select> -->
</div>
<button type="submit" class="btn btn-primary">Salvar</button>
</form>
<form method="post">
{% csrf_token %}
<!-- Forma 1: renderização automática como parágrafos (mais rápida, menos controle) -->
{{ form.as_p }}
<!-- gera: <p><label>Nome:</label><input ...></p> para cada campo -->
<!-- Forma 2: como tabela -->
{{ form.as_table }}
<!-- Forma 3: como lista -->
{{ form.as_ul }}
<!-- Forma 4: renderização manual COMPLETA (usada no projeto) -->
{% for field in form %}
<div class="form-group">
{{ field.label_tag }} <!-- <label for="id_nome">Nome:</label> -->
{{ field }} <!-- o <input> ou <select> -->
{{ field.errors }} <!-- erros de validação -->
</div>
{% endfor %}
<button type="submit">Salvar</button>
</form>
Fluxo GET — Listagem de Médicos
Veja o percurso completo quando o usuário clica em "Médicos" no menu. É o fluxo mais simples: apenas leitura do banco.
Browser envia: GET /medicos/ com o cookie de sessão
sistema/urls.py → include('intranet.urls') delega para o app
intranet/urls.py → path('medicos/', MedicoListView.as_view(), name='medico-list') corresponde
LoginRequiredMixin verifica o cookie de sessão → usuário está logado → continua
get_queryset() executa: Medico.objects.filter(ativo=True).order_by('nome') → retorna lista de objetos Medico
ListView monta contexto: {'medicos': [Medico#1, Medico#2, ...]}
medico/listagem.html é renderizado: {% for medico in medicos %} gera as linhas da tabela
HTML completo retorna ao browser com código HTTP 200
# Isso é o que o ListView faz "por baixo dos panos":
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
# chama get_queryset() → retorna os médicos filtrados e ordenados
context = self.get_context_data()
# monta {'medicos': [...], 'is_paginated': False, ...}
return render(request, self.template_name, context)
# renderiza o template com o contexto
Fluxo GET — Abrindo o Formulário de Edição
Quando o usuário clica em "Editar" ao lado de um médico, o browser acessa /medicos/5/editar/. Veja o que acontece:
Browser envia: GET /medicos/5/editar/
intranet/urls.py → path('medicos/<int:pk>/editar/', MedicoUpdateView...) captura pk=5
MedicoUpdateView recebe kwargs={'pk': 5}
get_object() executa: Medico.objects.get(pk=5) → retorna o médico (ou HTTP 404 se não existir)
UpdateView pré-preenche o MedicoForm com os dados do objeto encontrado
get_context_data() → {'form': <form preenchido>, 'object': <Medico#5>, 'titulo': 'Editar Médico'}
medico/formulario.html renderiza: {{ form.nome }} já vem com value="João Silva"
<!-- Além de 'form' e 'titulo', o UpdateView também passa 'object' -->
<!-- object = a instância do Model sendo editada -->
<!-- Exemplo de uso em confirmar_exclusao.html: -->
<p>
Excluir o médico
<strong>{{ object.nome }} {{ object.sobrenome }}</strong>?
<!-- object.nome = "João", object.sobrenome = "Silva" -->
</p>
<!-- O formulário de edição não precisa — o Django preenche os valores
automaticamente via o form pré-populado -->
Fluxo POST — Salvando um Formulário
Este é o fluxo mais complexo: o usuário preenche o formulário e clica em "Salvar". O browser envia os dados e a view precisa validá-los antes de salvar no banco.
Browser envia: POST /medicos/novo/ com corpo: nome=João&email=joao@email.com&crm=12345&especialidade=ORTOPEDIA&csrfmiddlewaretoken=abc...
Django verifica o CSRF token → válido → continua. Se inválido → retorna HTTP 403 Forbidden
MedicoCreateView.post() é chamado → instancia MedicoForm(request.POST)
form.is_valid() → executa todas as validações: campos obrigatórios, formato de e-mail, tamanho máximo, choices válidos
Se inválido: volta a renderizar o template com o form cheio de erros. O usuário vê as mensagens de erro em vermelho
Se válido: form.save() → executa INSERT INTO intranet_medico (...) no banco
Redirect → return redirect(success_url) → HTTP 302 para /medicos/
Browser faz novo GET /medicos/ → listagem aparece com o novo médico incluído
def post(self, request, *args, **kwargs):
# Instancia o form com os dados enviados pelo browser
form = self.form_class(request.POST)
if form.is_valid():
# Todos os campos passaram na validação
self.object = form.save() # salva no banco → INSERT INTO
return redirect(self.success_url) # redireciona para /medicos/
else:
# Algum campo falhou na validação
# Re-renderiza o template COM OS ERROS — form.campo.errors fica preenchido
return render(request, self.template_name, {'form': form, ...})
Fluxo especial — Consulta com busca de paciente por CPF
Mapa Completo do Projeto — URL → View → Template
Referência rápida de todo o sistema: cada URL, a view que executa, e o template que renderiza.
| URL | View | Template | Protegido? |
|---|---|---|---|
/ |
index() |
intranet/index.html |
Não |
/login/ |
LoginView |
intranet/login.html |
Não |
/logout/ |
LogoutView |
— (só redirect) | Não |
/home/ |
home() |
intranet/home.html |
✅ Login |
/medicos/ |
MedicoListView |
medico/listagem.html |
✅ Login |
/medicos/novo/ |
MedicoCreateView |
medico/formulario.html |
✅ Login |
/medicos/<pk>/editar/ |
MedicoUpdateView |
medico/formulario.html |
✅ Login |
/medicos/<pk>/excluir/ |
MedicoDeleteView |
medico/confirmar_exclusao.html |
✅ Login |
/pacientes/ |
PacienteListView |
paciente/listagem.html |
✅ Login |
/pacientes/novo/ |
PacienteCreateView |
paciente/formulario.html |
✅ Login |
/pacientes/<pk>/editar/ |
PacienteUpdateView |
paciente/formulario.html |
✅ Login |
/pacientes/<pk>/excluir/ |
PacienteDeleteView |
paciente/confirmar_exclusao.html |
✅ Login |
/consultas/ |
ConsultaListView |
consulta/listagem.html |
✅ Login |
/consultas/nova/ |
ConsultaCreateView |
consulta/formulario.html |
✅ Login |
/consultas/<pk>/editar/ |
ConsultaUpdateView |
consulta/formulario.html |
✅ Login |
/consultas/<pk>/excluir/ |
ConsultaDeleteView |
consulta/confirmar_exclusao.html |
✅ Login |
Apostila — Views, URLs e Templates no Django | Consulta.me
2025 © Desenvolvido por Aluno | Projeto fictício sem fins comerciais.