PROGRAMA DE CURSO APOSTILA · Views, URLs e Templates
← Início
01

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.

CamadaArquivo no projetoPergunta que respondeAnalogia
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
💡
Por que separar em três partes? Se você misturar tudo num único lugar, qualquer mudança vira um problema: mudar o visual exige mexer na lógica, e vice-versa. Com a separação do Django, você pode redesenhar a interface sem tocar na lógica de negócio, ou trocar o banco de dados sem mudar nenhum HTML.
02

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

╔══════════════════════════════════════════════════════════════════╗ ║ FLUXO COMPLETO: GET /medicos/ ║ ╚══════════════════════════════════════════════════════════════════╝ ① Usuário digita no browser: http://127.0.0.1:8000/medicos/ ↓ ② sistema/urls.py recebe a requisição path('', include('intranet.urls')) ↓ ③ intranet/urls.py encontra o padrão path('medicos/', MedicoListView.as_view(), name='medico-list') ↓ ④ MedicoListView (views.py) é executada • LoginRequiredMixin verifica sessão → OK • get_queryset() → consulta o banco ↓ ⑤ Banco de dados retorna os dados SELECT * FROM intranet_medico WHERE ativo = 1 ORDER BY nome ↓ ⑥ View monta o contexto context = { 'medicos': [<Medico: João>, <Medico: Ana>, ...] } ↓ ⑦ Template é renderizado medico/listagem.html processa {{ medicos }} e gera HTML ↓ ⑧ HTML completo é enviado ao browser <table><tr><td>João Silva</td>...</table>
✅
Resumo em uma frase A URL direciona para a View correta → a View busca dados e cria um contexto → o Template usa o contexto para gerar HTML.
03

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:

Requisição chega em: sistema/urls.py ← arquivo raiz (vê TODAS as requisições primeiro) ├── /admin/ → painel admin do Django ├── /login/ → LoginView ├── /logout/ → LogoutView └── /... → intranet/urls.py ← arquivo do app (cuida de tudo o mais) ├── / → index view ├── /home/ → home view ├── /medicos/ → MedicoListView ├── /medicos/novo/ → MedicoCreateView ├── /medicos/5/editar/ → MedicoUpdateView ├── /medicos/5/excluir/ → MedicoDeleteView └── ... (pacientes, consultas)
04

path() — Criando padrões de URL

A função path() é a peça fundamental do roteamento. Ela recebe três argumentos principais:

Assinatura da função path() python
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

intranet/urls.py — URLs fixas python
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:

intranet/urls.py — URLs com parâmetros python
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

ConversorCapturaExemplo de URLValor 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
⚠️
A ordem dos padrões importa! O Django testa os padrões de URL de cima para baixo e para na primeira que bater. Coloque padrões mais específicos antes dos mais genéricos. Por exemplo: 'medicos/novo/' deve vir ANTES de 'medicos/<int:pk>/editar/' — caso contrário, "novo" poderia ser interpretado como um pk.
05

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.

sistema/urls.py — arquivo raiz python
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/)
intranet/urls.py — arquivo do app python
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'),
]
06

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.

Três formas de usar o name de uma URL python
# ── 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/'
No template HTML — tag {% url %} markup
<!-- 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/ -->
💜
Por que usar name em vez de digitar a URL direto? Se você escrever 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.
07

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.

intranet/views.py — usando o objeto request python
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.user nos templates O Django injeta o 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.
08

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().

render() — renderiza um template e devolve HTML python
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 }}
})
redirect() — redireciona para outra URL python
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?

SEM redirect (problema): POST /medicos/novo/ → View salva → render(listagem.html) Usuário pressiona F5 → Browser pergunta "reenviar formulário?" Se confirmar → salva de novo → registro duplicado! 😱 COM redirect (correto — padrão do Django): POST /medicos/novo/ → View salva → redirect('/medicos/') Browser faz GET /medicos/ → View renderiza listagem Usuário pressiona F5 → apenas recarrega a listagem (GET) → seguro ✅
09

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.

intranet/views.py — as duas FBVs do projeto python
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
💡
Quando usar Function-based vs Class-based? Use FBV quando a lógica é específica, não encaixa em CRUD, ou você precisa de controle fino sobre o fluxo. Use CBV quando é uma operação padrão (listar, criar, editar, deletar) — o Django já entrega esse comportamento pronto.
10

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.

ClasseHTTPO que faz no projetoURL exemplo
ListViewGETLista todos os médicos ativos/medicos/
CreateViewGET + POSTExibe form vazio / salva novo médico/medicos/novo/
UpdateViewGET + POSTExibe form preenchido / salva edição/medicos/5/editar/
DeleteViewGET + POSTConfirma / executa exclusão/medicos/5/excluir/
intranet/views.py — CBVs do Médico com tudo explicado python
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
✅
O que o UpdateView faz automaticamente que o Create não faz O UpdateView recebe o 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.
11

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.

A View monta o contexto: views.py context = { 'medicos': [Medico#1, Medico#2, ...], ← vem do get_queryset() 'titulo': 'Listagem de Médicos', ← adicionado no get_context_data() 'form': <MedicoForm instance>, ← adicionado pelo CreateView/UpdateView 'request': <HttpRequest object>, ← adicionado automaticamente pelo Django 'user': <User: william>, ← adicionado automaticamente } ↓ template recebe o contexto e substitui as variáveis: {{ medicos }} → [Medico#1, Medico#2, ...] {{ titulo }} → 'Listagem de Médicos' {{ form }} → renderiza os <input>s do formulário
Exemplo real — ConsultaCreateView (o caso mais complexo) python
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 é...
12

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:

intranet/views.py — os dois jeitos de proteger python
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/
Fluxo de proteção de login: Acessa /medicos/ sem login ↓ LoginRequiredMixin: is_authenticated? → NÃO ↓ Redireciona para /login/?next=/medicos/ ↓ Usuário preenche login → Django autentica → cria sessão ↓ Lê parâmetro "next=/medicos/" → redireciona de volta para /medicos/ ↓ LoginRequiredMixin: is_authenticated? → SIM → executa a view
13

Sintaxe dos Templates — Variáveis e Tags

Os templates Django misturam HTML puro com marcadores especiais. Existem dois tipos de marcadores:

MarcadorTipoFunçãoExemplo
{{ ... }}VariávelExibe um valor do contexto{{ medico.nome }}
{% ... %}TagExecuta lógica (if, for, url…){% if user.is_authenticated %}
{# ... #}ComentárioIgnorado na saída HTML{# TODO: adicionar paginação #}
Sintaxe de variáveis — acessando dados do contexto markup
<!-- 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 }}
14

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.

Árvore de herança do projeto: base.html ← pai de todos │ define: DOCTYPE, head, header sem login, footer │ blocos: {% block title %}, {% block header %}, │ {% block content %}, {% block extra_scripts %} │ ├── index.html ← landing page (não logado) │ extends base.html │ preenche: {% block content %} com a seção hero │ ├── login.html ← página de login │ extends base.html │ preenche: {% block content %} com o formulário │ └── base_logado.html ← pai das páginas autenticadas extends base.html substitui: {% block header %} com nav + dropdown CADASTROS │ ├── home.html ← home pós-login ├── medico/listagem.html ├── medico/formulario.html ├── medico/confirmar_exclusao.html ├── paciente/listagem.html ├── paciente/formulario.html ├── consulta/listagem.html └── consulta/formulario.html
base.html — o pai que define os blocos markup
{% 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>
medico/listagem.html — filho que usa base_logado.html markup
{% 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 }} — herdando o conteúdo do bloco pai Se quiser manter o conteúdo do bloco pai E adicionar mais, use {{ block.super }}. Exemplo: o filho pode ter {% block title %}{{ block.super }} — Médicos{% endblock %} que geraria "Consulta.me — Médicos".
15

Tags de Controle — if, for, url, static, csrf_token

Tags usadas no projeto — explicadas uma a uma markup
<!-- ── {% 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 -->
16

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.

Filtros usados no projeto e outros úteis markup
<!-- 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. -->
⚠️
Cuidado com o filtro |safe {{ 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).
17

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.

medico/formulario.html — renderização manual (usada no projeto) markup
<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>
Outras formas de renderizar (para referência) markup
<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>

18

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.

1

Browser envia: GET /medicos/ com o cookie de sessão

2

sistema/urls.py → include('intranet.urls') delega para o app

3

intranet/urls.py → path('medicos/', MedicoListView.as_view(), name='medico-list') corresponde

4

LoginRequiredMixin verifica o cookie de sessão → usuário está logado → continua

5

get_queryset() executa: Medico.objects.filter(ativo=True).order_by('nome') → retorna lista de objetos Medico

6

ListView monta contexto: {'medicos': [Medico#1, Medico#2, ...]}

7

medico/listagem.html é renderizado: {% for medico in medicos %} gera as linhas da tabela

8

HTML completo retorna ao browser com código HTTP 200

O que o MedicoListView faz internamente (simplificado) python
# 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
19

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:

1

Browser envia: GET /medicos/5/editar/

2

intranet/urls.py → path('medicos/<int:pk>/editar/', MedicoUpdateView...) captura pk=5

3

MedicoUpdateView recebe kwargs={'pk': 5}

4

get_object() executa: Medico.objects.get(pk=5) → retorna o médico (ou HTTP 404 se não existir)

5

UpdateView pré-preenche o MedicoForm com os dados do objeto encontrado

6

get_context_data() → {'form': <form preenchido>, 'object': <Medico#5>, 'titulo': 'Editar Médico'}

7

medico/formulario.html renderiza: {{ form.nome }} já vem com value="João Silva"

No template — usando a variável object (disponível no UpdateView) markup
<!-- 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 -->
20

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.

1

Browser envia: POST /medicos/novo/ com corpo: nome=João&email=joao@email.com&crm=12345&especialidade=ORTOPEDIA&csrfmiddlewaretoken=abc...

2

Django verifica o CSRF token → válido → continua. Se inválido → retorna HTTP 403 Forbidden

3

MedicoCreateView.post() é chamado → instancia MedicoForm(request.POST)

4

form.is_valid() → executa todas as validações: campos obrigatórios, formato de e-mail, tamanho máximo, choices válidos

5a

Se inválido: volta a renderizar o template com o form cheio de erros. O usuário vê as mensagens de erro em vermelho

5b

Se válido: form.save() → executa INSERT INTO intranet_medico (...) no banco

6

Redirect → return redirect(success_url) → HTTP 302 para /medicos/

7

Browser faz novo GET /medicos/ → listagem aparece com o novo médico incluído

O que o CreateView faz internamente no POST (simplificado) python
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

POST /consultas/nova/ com cpf_paciente=12345678901 ConsultaCreateView.post() ↓ ConsultaForm(request.POST) ↓ form.is_valid() chama automaticamente: → clean_cpf_paciente() ← método especial do ConsultaForm cpf = '123.456.789-01' cpf_limpo = '12345678901' (remove . e -) paciente = Paciente.objects.get(cpf=cpf_limpo, ativo=True) → se encontrar: retorna objeto Paciente → form.cleaned_data['cpf_paciente'] = Paciente#3 → se não encontrar: raise ValidationError('Paciente não encontrado') ↓ form.save() chama: → ConsultaForm.save() ← método sobrescrito instance = super().save(commit=False) (cria objeto sem salvar) instance.paciente_id = cleaned_data['cpf_paciente'] (Paciente#3) instance.save() (salva com o paciente correto) ↓ redirect('/consultas/') ✅
21

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
✅
Padrão que se repete em todos os CRUDs Cada entidade (Médico, Paciente, Consulta) segue exatamente o mesmo padrão: 4 URLs, 4 Views, 3 templates (formulario.html é compartilhado entre Create e Update). Aprendeu o padrão com médico → sabe como funciona paciente e consulta também.

Apostila — Views, URLs e Templates no Django  |  Consulta.me

2025 © Desenvolvido por Aluno | Projeto fictício sem fins comerciais.