Sistema Médico com Django — Consulta.me
Apostila completa do projeto: do banco de dados ao frontend integrado, passo a passo com explicação de cada linha de código.
O que é Django?
Django é um framework web escrito em Python que segue o padrão arquitetural MVT (Model – View – Template). Ele resolve os problemas mais comuns no desenvolvimento web (banco de dados, autenticação, formulários, roteamento) de forma automática, para que você foque na lógica do seu projeto.
| Camada | Responsabilidade | No nosso projeto |
|---|---|---|
| Model | Define a estrutura do banco de dados | intranet/models.py |
| View | Recebe a requisição, processa e retorna a resposta | intranet/views.py |
| Template | A interface HTML exibida ao usuário | intranet/templates/ |
Estrutura do Projeto
Um projeto Django é composto por um projeto (configurações globais) e um ou mais apps (módulos de funcionalidade). No nosso caso:
sistema/) guarda configurações globais. O app (intranet/) contém a lógica real. Isso permite reutilizar apps em outros projetos e manter o código organizado.Comandos iniciais para recriar o projeto do zero
# Cria o ambiente virtual Python
python -m venv .venv
# Ativa o ambiente virtual (macOS/Linux)
source .venv/bin/activate
# Instala o Django no ambiente isolado
pip install django pillow
# Cria o projeto chamado "sistema" na pasta atual
django-admin startproject sistema .
# Cria o app chamado "intranet" dentro do projeto
python manage.py startapp intranet
# Sobe o servidor de desenvolvimento
python manage.py runserver
. no final de startproject sistema . diz ao Django para criar os arquivos na pasta atual, evitando uma pasta extra desnecessária.Models — O Banco de Dados
Um Model é uma classe Python que representa uma tabela no banco de dados. Cada atributo da classe corresponde a uma coluna da tabela. O Django converte automaticamente essas classes em SQL.
from django.utils import timezone # fornece data/hora com fuso horário correto
from django.db import models # módulo principal dos models do Django
# ─── TABELA: medico ───────────────────────────────────────────────────────────
class Medico(models.Model): # herda de Model → vira tabela no banco
nome = models.CharField(max_length=30) # texto curto, até 30 caracteres
sobrenome = models.CharField(max_length=30) # texto curto, até 30 caracteres
email = models.EmailField() # valida formato de e-mail automaticamente
criacao_data= models.DateTimeField( # data/hora de quando o registro foi criado
default=timezone.now) # valor padrão = momento atual
telefone = models.CharField(max_length=15) # string (pode ter traços e parênteses)
crm = models.CharField(max_length=6) # código de registro médico (até 6 dígitos)
especialidade = models.CharField(max_length=20) # ex: ORTOPEDIA, CARDIOLOGIA
ativo = models.BooleanField(default=True) # True/False — se o médico está ativo
mensagem = models.TextField(blank=True) # texto longo, opcional (blank=True)
imagem = models.ImageField( # campo de upload de imagem
upload_to='img/%Y/%m', # salva em media/img/ANO/MES/
blank=True) # opcional: médico pode não ter foto
def __str__(self): # define como o objeto aparece no admin e no terminal
return f'{self.nome} {self.sobrenome}' # ex: "João Silva"
# ─── TABELA: paciente ─────────────────────────────────────────────────────────
class Paciente(models.Model):
nome = models.CharField(max_length=50)
sobrenome = models.CharField(max_length=50)
email = models.EmailField()
telefone = models.CharField(max_length=15)
cpf = models.CharField(max_length=11) # 11 dígitos sem formatação
criacao_data = models.DateTimeField(default=timezone.now)
mensagem = models.TextField(blank=True)
ativo = models.BooleanField(default=True)
imagem = models.ImageField(upload_to='img/%Y/%m', blank=True)
def __str__(self):
return f'{self.nome} {self.sobrenome}'
# ─── TABELA: consulta ─────────────────────────────────────────────────────────
class Consulta(models.Model):
# ForeignKey cria um relacionamento "muitos para um" (N:1)
# on_delete=CASCADE: se o paciente for deletado, a consulta também é deletada
paciente_id = models.ForeignKey(Paciente, on_delete=models.CASCADE)
medico_id = models.ForeignKey(Medico, on_delete=models.CASCADE)
horario = models.DateTimeField(default=timezone.now) # data/hora da consulta
observacao = models.TextField(blank=True) # anotação opcional
# choices: lista de opções válidas para o campo
# formato: lista de tuplas (valor_no_banco, texto_exibido)
status = models.CharField(
default='A', # valor padrão ao criar a consulta
max_length=1, # salva apenas 1 caractere no banco (A, X, C ou R)
choices=(
('A', 'Agendada'),
('X', 'Cancelada'),
('C', 'Confirmada'),
('R', 'Realizada'),
)
)
def __str__(self):
return f'Consulta {self.get_status_display()} — {self.paciente_id}'
# get_status_display() retorna o texto legível (ex: "Agendada"), não o código ("A")
Tipos de campos mais usados
| Campo | Tipo Python | Uso típico |
|---|---|---|
CharField | str | Nome, telefone, CEP — textos curtos com limite definido |
TextField | str | Descrições longas, observações — sem limite definido |
EmailField | str | E-mail — valida o formato automaticamente |
IntegerField | int | Números inteiros |
BooleanField | bool | Ativo/inativo, verdadeiro/falso |
DateTimeField | datetime | Data e hora |
ImageField | str (caminho) | Upload de imagem — exige Pillow instalado |
ForeignKey | objeto | Relacionamento N:1 com outra tabela |
paciente_id = models.ForeignKey(Paciente, ...), estamos dizendo: "cada consulta pertence a um paciente". O Django cria automaticamente uma coluna paciente_id_id no banco que armazena o ID do paciente. No Python, consulta.paciente_id retorna o objeto Paciente completo.Admin — Painel Administrativo
O Django vem com um painel administrativo pronto em /admin/. Para que nossos models apareçam lá, precisamos registrá-los no arquivo admin.py.
from django.contrib import admin # importa o módulo admin do Django
from intranet import models # importa nossos models para registrar
# @admin.register é um decorator: conecta a classe AdminConfig ao model Medico
@admin.register(models.Medico)
class MedicoAdmin(admin.ModelAdmin):
# list_display: define quais colunas aparecem na listagem do admin
list_display = ('id', 'nome', 'email', 'telefone', 'especialidade', 'ativo')
# ↑ ID ↑ Nome ↑ E-mail ↑ Telefone ↑ Especialidade ↑ Ativo?
@admin.register(models.Paciente)
class PacienteAdmin(admin.ModelAdmin):
list_display = ('id', 'nome', 'email', 'telefone', 'ativo')
@admin.register(models.Consulta)
class ConsultaAdmin(admin.ModelAdmin):
# paciente_id e medico_id são ForeignKeys: o admin exibe o __str__ deles
list_display = ('id', 'paciente_id', 'medico_id', 'status')
python manage.py createsuperuser. O Django pedirá nome de usuário, e-mail e senha. Depois acesse http://127.0.0.1:8000/admin/.Migrations — Versionamento do Banco
As migrations são arquivos Python gerados automaticamente que descrevem as alterações no banco de dados. Em vez de escrever SQL manualmente, você edita os models e o Django gera o SQL por você.
# Analisa os models e gera o arquivo de migration (não altera o banco ainda)
python manage.py makemigrations
# Aplica todas as migrations pendentes no banco de dados
python manage.py migrate
# Para ver o SQL que será executado (útil para aprender):
python manage.py sqlmigrate intranet 0001
O Django gerou o arquivo intranet/migrations/0001_initial.py automaticamente. Veja o que ele contém:
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True # marca como a primeira migration do app
dependencies = [] # lista de migrations que precisam rodar antes desta
operations = [
# CreateModel: cria a tabela "intranet_medico" no banco
migrations.CreateModel(
name='Medico',
fields=[
# Django cria o campo "id" automaticamente (BigAutoField = inteiro longo)
('id', models.BigAutoField(auto_created=True, primary_key=True, ...)),
('nome', models.CharField(max_length=30)),
('sobrenome', models.CharField(max_length=30)),
('email', models.EmailField(max_length=254)),
('criacao_data',models.DateTimeField(default=django.utils.timezone.now)),
('telefone', models.CharField(max_length=15)),
('crm', models.CharField(max_length=6)),
('especialidade', models.CharField(max_length=20)),
('ativo', models.BooleanField(default=True)),
('mensagem', models.TextField(blank=True)),
('imagem', models.ImageField(blank=True, upload_to='img/%Y/%m')),
],
),
# ... (Paciente e Consulta seguem o mesmo padrão)
]
models.py e rode makemigrations novamente.Settings — Configurações do Projeto
O arquivo settings.py é o centro de controle do Django. Aqui definimos tudo: quais apps estão instalados, onde ficam os templates, como funciona o banco de dados, idioma, fuso horário, e muito mais.
from pathlib import Path
# BASE_DIR é o caminho absoluto da pasta raiz do projeto
# Path(__file__) → caminho deste arquivo (settings.py)
# .resolve() → converte para caminho absoluto
# .parent.parent → sobe duas pastas (settings.py → sistema/ → sistema_medico_django/)
BASE_DIR = Path(__file__).resolve().parent.parent
# ─── Segurança ────────────────────────────────────────────────────────────────
SECRET_KEY = 'django-insecure-...' # chave criptográfica — troque em produção!
DEBUG = True # True = modo desenvolvedor (exibe erros detalhados)
ALLOWED_HOSTS = [] # domínios permitidos — em produção adicione o seu domínio
# ─── Apps instalados ──────────────────────────────────────────────────────────
INSTALLED_APPS = [
'django.contrib.admin', # painel administrativo em /admin/
'django.contrib.auth', # sistema de autenticação (login/logout/usuários)
'django.contrib.contenttypes', # framework de tipos de conteúdo (necessário para permissões)
'django.contrib.sessions', # sistema de sessões (mantém usuário logado)
'django.contrib.messages', # mensagens flash (ex: "Salvo com sucesso!")
'django.contrib.staticfiles', # gerenciamento de arquivos estáticos (CSS, JS, img)
'intranet', # nosso app personalizado — OBRIGATÓRIO adicionar aqui
]
# ─── Templates ────────────────────────────────────────────────────────────────
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'], # pasta global de templates (opcional)
'APP_DIRS': True, # True: procura templates dentro de cada app/templates/
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request', # disponibiliza request nos templates
'django.contrib.auth.context_processors.auth', # disponibiliza user nos templates
'django.contrib.messages.context_processors.messages', # mensagens flash
],
},
},
]
# ─── Banco de dados ───────────────────────────────────────────────────────────
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3', # motor: SQLite (arquivo único, ideal p/ aprendizado)
'NAME': BASE_DIR / 'db.sqlite3', # caminho do arquivo de banco
}
}
# ─── Internacionalização ──────────────────────────────────────────────────────
LANGUAGE_CODE = 'pt-br' # idioma da interface do admin
TIME_ZONE = 'America/Sao_Paulo' # fuso horário do servidor
USE_I18N = True # ativa internacionalização
USE_TZ = True # armazena datas com fuso horário (recomendado)
# ─── Arquivos estáticos (CSS, JS, imagens do projeto) ─────────────────────────
STATIC_URL = 'static/' # URL pública para acessar estáticos (ex: /static/intranet/style.css)
# ─── Arquivos de mídia (uploads feitos pelo usuário) ──────────────────────────
MEDIA_URL = '/media/' # URL pública dos uploads
MEDIA_ROOT = BASE_DIR / 'media' # pasta física onde os uploads são salvos
# ─── Autenticação ─────────────────────────────────────────────────────────────
LOGIN_URL = '/login/' # para onde redirecionar se não estiver logado
LOGIN_REDIRECT_URL = '/home/' # para onde ir após login bem-sucedido
LOGOUT_REDIRECT_URL = '/' # para onde ir após logout
# ─── Chave primária padrão ────────────────────────────────────────────────────
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # tipo do campo "id" gerado automaticamente
URLs — O Sistema de Roteamento
O Django usa um sistema de roteamento baseado em expressões — cada URL é mapeada para uma View. Temos dois arquivos de URLs: o global (sistema/urls.py) que distribui as rotas para os apps, e o do app (intranet/urls.py) que define as rotas específicas.
URLs globais do projeto
from django.contrib import admin
from django.contrib.auth import views as auth_views # views prontas de login/logout do Django
from django.urls import path, include # path() cria rotas, include() delega para outro arquivo
from django.conf import settings
from django.conf.urls.static import static # serve arquivos de mídia em desenvolvimento
urlpatterns = [
# Painel administrativo — sempre disponível em /admin/
path('admin/', admin.site.urls),
# Login: usa a LoginView pronta do Django, mas com nosso template personalizado
path('login/', auth_views.LoginView.as_view(template_name='intranet/login.html'), name='login'),
# Logout: redireciona para LOGOUT_REDIRECT_URL definido no settings.py
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
# Inclui todas as rotas do app intranet (definidas em intranet/urls.py)
# Tudo que não é admin/login/logout vai para o app intranet
path('', include('intranet.urls')),
# static(): em DEBUG=True, serve os arquivos de /media/ (uploads do usuário)
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
URLs do app intranet
from django.urls import path
from . import views # importa as views do mesmo diretório (intranet/views.py)
# app_name define o namespace — evita conflito de nomes entre apps diferentes
# uso nos templates: {% url 'intranet:medico-list' %}
app_name = 'intranet'
urlpatterns = [
# ─── Páginas iniciais ─────────────────────────────────────────────────────
path('', views.index, name='index'), # / → landing page (não logado)
path('home/', views.home, name='home'), # /home/ → página inicial (logado)
# ─── CRUD de Médicos ──────────────────────────────────────────────────────
path('medicos/', views.MedicoListView.as_view(), name='medico-list'),
# /medicos/ → lista todos os médicos ativos
path('medicos/novo/', views.MedicoCreateView.as_view(), name='medico-create'),
# /medicos/novo/ → formulário para cadastrar novo médico
path('medicos/<int:pk>/editar/', views.MedicoUpdateView.as_view(), name='medico-update'),
# /medicos/5/editar/ → formulário para editar médico com id=5
# <int:pk> é um parâmetro dinâmico: captura o número e passa para a view como "pk"
path('medicos/<int:pk>/excluir/', views.MedicoDeleteView.as_view(), name='medico-delete'),
# /medicos/5/excluir/ → página de confirmação para deletar médico com id=5
# ─── CRUD de 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'),
# ─── CRUD de 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'),
]
list, o Django não saberia qual usar. Com app_name = 'intranet', chamamos {% url 'intranet:medico-list' %} — sem ambiguidade.Forms — Formulários
Os Forms do Django validam e processam os dados enviados pelo usuário. Um ModelForm é um formulário gerado automaticamente a partir de um Model — ele sabe quais campos existem, quais são obrigatórios e como validar cada tipo.
from django import forms
from .models import Medico, Paciente, Consulta # importa nossos models
# Lista de especialidades disponíveis (formato: tupla valor, texto)
# O mesmo valor é armazenado no banco E exibido no dropdown
ESPECIALIDADES = [
('CARDIOLOGIA', 'Cardiologia'),
('DERMATOLOGIA', 'Dermatologia'),
('GINECOLOGIA', 'Ginecologia'),
('NEUROLOGIA', 'Neurologia'),
('OFTALMOLOGIA', 'Oftalmologia'),
('ORTOPEDIA', 'Ortopedia'),
('PEDIATRIA', 'Pediatria'),
]
# ─── Formulário de Médico ─────────────────────────────────────────────────────
class MedicoForm(forms.ModelForm): # ModelForm = form gerado a partir de um Model
# Substituímos o CharField padrão por um ChoiceField com opções fixas
especialidade = forms.ChoiceField(
choices=[('', 'Selecione uma especialidade')] + ESPECIALIDADES,
# [('','')] + ESPECIALIDADES = adiciona opção vazia no início da lista
label='Especialidade',
)
class Meta: # classe interna que configura o ModelForm
model = Medico # qual model este form representa
fields = ['nome', 'sobrenome', 'email', 'telefone', 'crm', 'especialidade']
# fields: lista APENAS os campos que o usuário pode preencher
# campos não listados (ex: criacao_data, ativo) são preenchidos automaticamente
labels = { # textos dos rótulos exibidos no HTML
'nome': 'Nome',
'sobrenome': 'Sobrenome',
'email': 'E-mail',
'telefone': 'Telefone',
'crm': 'CRM',
}
# ─── Formulário de Paciente ───────────────────────────────────────────────────
class PacienteForm(forms.ModelForm):
class Meta:
model = Paciente
fields = ['nome', 'sobrenome', 'email', 'telefone', 'cpf']
labels = {
'nome': 'Nome',
'sobrenome': 'Sobrenome',
'email': 'E-mail',
'telefone': 'Telefone',
'cpf': 'CPF',
}
# ─── Formulário de Consulta ───────────────────────────────────────────────────
class ConsultaForm(forms.ModelForm):
# Campo extra (não existe no model): usado para filtrar médicos por especialidade
especialidade = forms.ChoiceField(
choices=[('', 'Selecione uma especialidade')] + ESPECIALIDADES,
required=False, # não obrigatório — só serve para filtrar no frontend
label='Especialidade',
)
# Campo extra: recebe o CPF digitado e busca o paciente no banco
cpf_paciente = forms.CharField(
max_length=14,
label='CPF do Paciente',
widget=forms.TextInput(attrs={'placeholder': 'Digite o CPF do paciente'}),
# widget: define o tipo de elemento HTML — TextInput vira <input type="text">
)
class Meta:
model = Consulta
fields = ['medico_id', 'horario', 'observacao']
# Não inclui paciente_id aqui — vamos preencher via CPF no clean/save
labels = {
'medico_id': 'Médico',
'horario': 'Data e Hora',
'observacao': 'Observação',
}
widgets = {
# DateTimeInput com type="datetime-local" exibe um seletor de data/hora no browser
'horario': forms.DateTimeInput(
attrs={'type': 'datetime-local'},
format='%Y-%m-%dT%H:%M', # formato esperado pelo input HTML
),
}
def __init__(self, *args, **kwargs):
# __init__ é chamado ao instanciar o form — podemos personalizar aqui
super().__init__(*args, **kwargs)
# Filtra apenas médicos ativos no dropdown
self.fields['medico_id'].queryset = Medico.objects.filter(ativo=True).order_by('nome')
self.fields['medico_id'].empty_label = 'Selecione um médico'
self.fields['observacao'].required = False # torna observação opcional
# Se está editando uma consulta existente, formata a data para o input HTML
if self.instance.pk and self.instance.horario:
self.initial['horario'] = self.instance.horario.strftime('%Y-%m-%dT%H:%M')
def clean_cpf_paciente(self):
# Método especial: clean_CAMPO — validação personalizada do campo "cpf_paciente"
# É chamado automaticamente pelo Django quando form.is_valid() é executado
cpf = self.cleaned_data.get('cpf_paciente', '')
# Remove formatação: "123.456.789-01" → "12345678901"
cpf_limpo = cpf.replace('.', '').replace('-', '').replace(' ', '')
try:
# Tenta encontrar o paciente com esse CPF que esteja ativo
return Paciente.objects.get(cpf=cpf_limpo, ativo=True)
# Se encontrar, retorna o OBJETO Paciente (não o CPF!)
except Paciente.DoesNotExist:
# Se não encontrar, lança erro de validação — o form.is_valid() retorna False
raise forms.ValidationError(
'Paciente com este CPF não encontrado. Cadastre o paciente primeiro.'
)
def save(self, commit=True):
# Sobrescreve o save padrão para associar o paciente encontrado pelo CPF
instance = super().save(commit=False) # cria o objeto sem salvar no banco ainda
# clean_cpf_paciente() retornou o objeto Paciente — atribuímos à consulta
instance.paciente_id = self.cleaned_data['cpf_paciente']
if commit:
instance.save() # agora sim salva no banco
return instance
clean_nomedocampo() no form é executado automaticamente na validação. Se lançar ValidationError, o campo fica inválido. Se retornar um valor, esse valor substitui o original em cleaned_data. Por isso, retornamos o objeto Paciente em vez do CPF.Views — A Lógica do Sistema
As Views são o coração do Django. Cada view recebe uma requisição HTTP, executa a lógica necessária (consultar banco, validar form, etc.) e retorna uma resposta HTTP (HTML, redirect, JSON…).
No nosso projeto usamos dois tipos: function-based views (funções simples) e class-based views (classes com comportamento pré-definido como ListView, CreateView, etc.).
import json
from django.shortcuts import render, redirect # funções auxiliares de resposta
from django.contrib.auth.decorators import login_required # decorator para exigir login
from django.contrib.auth.mixins import LoginRequiredMixin # mixin para class-based views
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.urls import reverse_lazy # gera URL a partir do name (avaliação tardia)
from .models import Medico, Paciente, Consulta
from .forms import MedicoForm, PacienteForm, ConsultaForm, ESPECIALIDADES
# ─── Views de função simples ──────────────────────────────────────────────────
def index(request):
# request é o objeto HTTP com todas as informações da requisição
if request.user.is_authenticated: # verifica se há sessão de login ativa
return redirect('intranet:home') # se logado, redireciona para home
return render(request, 'intranet/index.html')
# render(request, template) → carrega o template e devolve HTML como resposta
@login_required # decorator: se não estiver logado, redireciona para LOGIN_URL (/login/)
def home(request):
return render(request, 'intranet/home.html')
# ─── Class-Based Views para Médico ────────────────────────────────────────────
# LoginRequiredMixin: equivalente ao @login_required para class-based views
# Deve ser o PRIMEIRO na lista de herança para funcionar corretamente
class MedicoListView(LoginRequiredMixin, ListView):
model = Medico # qual model listar
template_name = 'intranet/medico/listagem.html'
context_object_name = 'medicos' # nome da variável disponível no template ({{ medicos }})
def get_queryset(self):
# Sobrescreve a query padrão — retorna apenas médicos ativos, ordenados por nome
return Medico.objects.filter(ativo=True).order_by('nome')
class MedicoCreateView(LoginRequiredMixin, CreateView):
model = Medico
form_class = MedicoForm # qual form usar para criar
template_name = 'intranet/medico/formulario.html'
success_url = reverse_lazy('intranet:medico-list') # para onde ir após salvar
def get_context_data(self, **kwargs):
# Adiciona variáveis extras ao contexto do template
ctx = super().get_context_data(**kwargs) # obtém o contexto padrão (form, etc.)
ctx['titulo'] = 'Cadastro de Médico' # adiciona variável {{ titulo }} ao template
return ctx
class MedicoUpdateView(LoginRequiredMixin, UpdateView):
model = Medico
form_class = MedicoForm
template_name = 'intranet/medico/formulario.html' # reutiliza o mesmo template do create
success_url = reverse_lazy('intranet:medico-list')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['titulo'] = 'Editar Médico'
return ctx
class MedicoDeleteView(LoginRequiredMixin, DeleteView):
model = Medico
template_name = 'intranet/medico/confirmar_exclusao.html'
success_url = reverse_lazy('intranet:medico-list')
# O DeleteView exige um POST para deletar (proteção contra bots)
# O template deve ter um <form method="post"> com {% csrf_token %}
# ─── Class-Based Views para Paciente ──────────────────────────────────────────
# (mesmo padrão do Médico — estrutura idêntica, apenas com Paciente)
class PacienteListView(LoginRequiredMixin, ListView):
model = Paciente
template_name = 'intranet/paciente/listagem.html'
context_object_name = 'pacientes'
def get_queryset(self):
return Paciente.objects.filter(ativo=True).order_by('nome')
class PacienteCreateView(LoginRequiredMixin, CreateView):
model = Paciente
form_class = PacienteForm
template_name = 'intranet/paciente/formulario.html'
success_url = reverse_lazy('intranet:paciente-list')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['titulo'] = 'Cadastro de Paciente'
return ctx
class PacienteUpdateView(LoginRequiredMixin, UpdateView):
model = Paciente
form_class = PacienteForm
template_name = 'intranet/paciente/formulario.html'
success_url = reverse_lazy('intranet:paciente-list')
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['titulo'] = 'Editar Paciente'
return ctx
class PacienteDeleteView(LoginRequiredMixin, DeleteView):
model = Paciente
template_name = 'intranet/paciente/confirmar_exclusao.html'
success_url = reverse_lazy('intranet:paciente-list')
# ─── Class-Based Views para Consulta ──────────────────────────────────────────
class ConsultaListView(LoginRequiredMixin, ListView):
model = Consulta
template_name = 'intranet/consulta/listagem.html'
context_object_name = 'consultas'
def get_queryset(self):
# select_related: faz JOIN no banco para buscar paciente e médico em UMA query
# sem select_related, cada linha da tabela geraria 2 queries extras (N+1 problem)
return Consulta.objects.select_related('paciente_id', 'medico_id').order_by('-horario')
# ↑ - = decrescente (mais recente primeiro)
def _medicos_json():
# Função auxiliar: retorna a lista de médicos ativos como string JSON
# Usada no template do formulário de consulta para filtrar médicos por especialidade via JS
medicos = list(
Medico.objects.filter(ativo=True)
.order_by('nome')
.values('id', 'nome', 'sobrenome', 'especialidade')
# values() retorna dicionários em vez de objetos Python (mais leve para JSON)
)
return json.dumps(medicos, ensure_ascii=False)
# ensure_ascii=False: mantém acentos no JSON (ex: "Cardiologia" não vira "é")
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)
ctx['titulo'] = 'Agendamento de Consulta'
ctx['medicos_json'] = _medicos_json() # passa JSON para o JavaScript do template
ctx['especialidades'] = ESPECIALIDADES # passa lista para o template
return ctx
class ConsultaUpdateView(LoginRequiredMixin, UpdateView):
model = Consulta
form_class = ConsultaForm
template_name = 'intranet/consulta/formulario.html'
success_url = reverse_lazy('intranet:consulta-list')
def get_initial(self):
# get_initial: pré-preenche campos do form ao carregar a página de edição
initial = super().get_initial()
obj = self.get_object() # busca a consulta pelo <int:pk> da URL
initial['cpf_paciente'] = obj.paciente_id.cpf # pré-preenche o CPF
initial['especialidade'] = obj.medico_id.especialidade # pré-seleciona especialidade
return initial
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['titulo'] = 'Editar Consulta'
ctx['medicos_json'] = _medicos_json()
ctx['especialidades'] = ESPECIALIDADES
return ctx
class ConsultaDeleteView(LoginRequiredMixin, DeleteView):
model = Consulta
template_name = 'intranet/consulta/confirmar_exclusao.html'
success_url = reverse_lazy('intranet:consulta-list')
Comparativo: Function-based vs Class-based Views
| Aspecto | Function-based (def) | Class-based (class) |
|---|---|---|
| Quando usar | Lógica simples e personalizada | Operações padrão de CRUD |
| Código | Explícito — você escreve tudo | Conciso — herda comportamentos |
| Proteção de login | @login_required | LoginRequiredMixin |
| Exemplo | index, home | ListView, CreateView… |
reverse_lazy('intranet:medico-list') converte o name da URL em seu caminho real (/medicos/). O lazy (preguiçoso) significa que a resolução acontece só quando necessário — importante porque as URLs ainda não estão carregadas no momento em que as classes são definidas.Templates — Base e Herança
Os templates Django são arquivos HTML com marcadores especiais ({{ variável }}, {% tag %}) que o Django processa antes de enviar ao navegador. A característica mais poderosa é a herança de templates: um template "filho" pode estender um "pai", sobreescrevendo apenas os blocos que precisa.
Template base.html
{% load static %}
<!-- {% load static %}: carrega a tag {% static %} que resolve caminhos de arquivos estáticos -->
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- {% block title %}: bloco que templates filhos podem sobreescrever -->
<title>{% block title %}Consulta.me{% endblock %}</title>
<!-- {% static '...' %}: resolve o caminho do arquivo estático para /static/intranet/... -->
<link rel="stylesheet" href="{% static 'intranet/style.css' %}">
{% block extra_head %}{% endblock %}
<!-- bloco vazio: templates filhos podem inserir CSS/JS extras aqui -->
</head>
<body>
{% block header %}
<!-- Este bloco é substituído pelo base_logado.html quando o usuário está logado -->
<header class="header">
<div class="container header-container">
<!-- {% url 'intranet:index' %}: gera a URL / a partir do name definido nas URLs -->
<a class="logo" href="{% url 'intranet:index' %}">
<img src="{% static 'intranet/assets/logo.svg' %}" alt="Consulta.me">
</a>
<div class="navbar-form">
<!-- {% url 'login' %}: URL da LoginView definida em sistema/urls.py -->
<a href="{% url 'login' %}" class="btn-link">Login</a>
</div>
</div>
</header>
{% endblock %}
<!-- Sistema de mensagens flash (ex: "Salvo com sucesso!") -->
{% if messages %}
<div class="container" style="margin-top:15px;">
{% for message in messages %} <!-- itera sobre as mensagens pendentes -->
<div style="padding:10px; border-radius:6px;">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% block content %}{% endblock %}
<!-- bloco principal: cada template filho preenche aqui com seu conteúdo -->
<footer class="footer">...</footer>
{% block extra_scripts %}{% endblock %}
<!-- scripts extras: o formulário de consulta insere o JavaScript de filtro aqui -->
</body>
</html>
Template base_logado.html
{% extends 'intranet/base.html' %}
<!-- extends: declara que este template herda de base.html -->
<!-- Tudo que não está em um {% block %} é ignorado — só blocos são usados -->
{% load static %}
{% block header %}
<!-- Sobrescreve o bloco header do pai — este é o cabeçalho para usuários logados -->
<header class="header">
<div class="container header-container">
<a class="logo" href="{% url 'intranet:home' %}">
<img src="{% static 'intranet/assets/logo.svg' %}" alt="">
</a>
<div class="wrapper">
<nav class="main-nav">
<!-- Dropdown CADASTROS -->
<ul class="nav-left">
<li class="dropdown"> <!-- :hover mostra .dropdown-menu via CSS -->
<a href="">CADASTROS <i class="arrow-down"></i></a>
<ul class="dropdown-menu">
<li><a href="{% url 'intranet:medico-list' %}">Médicos</a></li>
<li><a href="{% url 'intranet:paciente-list' %}">Pacientes</a></li>
<li><a href="{% url 'intranet:consulta-list' %}">Consultas</a></li>
</ul>
</li>
</ul>
<!-- Dropdown do usuário logado -->
<ul class="nav-right">
<li class="dropdown">
<a href="">
<!-- request.user: o usuário Django autenticado na sessão atual -->
<!-- get_full_name: retorna "Nome Sobrenome"; se vazio, usa o username -->
Olá, <span>{{ request.user.get_full_name|default:request.user.username }}</span>
<!-- |default:X: filtro Django — usa X se o valor anterior for vazio -->
</a>
<ul class="dropdown-menu">
<li>
<!-- Logout deve ser POST (proteção CSRF) — não pode ser link GET -->
<form method="post" action="{% url 'logout' %}">
{% csrf_token %} <!-- token de segurança contra Cross-Site Request Forgery -->
<button type="submit">Sair</button>
</form>
</li>
</ul>
</li>
</ul>
</nav>
</div>
</div>
</header>
{% endblock %}
{% csrf_token %} garante que o POST veio realmente do nosso site.Template — Página de Login
{% extends 'intranet/base.html' %} <!-- herda o layout base sem menu de logado -->
{% load static %}
{% block title %}Consulta.me — Login{% endblock %}
{% block content %}
<main class="container">
<section class="card form-card">
<h2 class="title">Login</h2>
<!-- method="post": dados enviados no corpo da requisição (não na URL) -->
<form method="post">
{% csrf_token %} <!-- obrigatório em todo formulário POST no Django -->
<!-- form.errors: dicionário com erros de validação do formulário Django -->
{% if form.errors %}
<p style="color:#c00;">Usuário ou senha incorretos. Tente novamente.</p>
{% endif %}
<div class="form-group">
<label for="id_username">Usuário:</label>
<!-- Django espera os campos "username" e "password" na LoginView -->
<input id="id_username" name="username" type="text" required autofocus>
</div>
<div class="form-group">
<label for="id_password">Senha:</label>
<input id="id_password" name="password" type="password" required>
</div>
<!-- "next": URL para onde redirecionar após login -->
<!-- ex: se tentou acessar /medicos/ sem login, next=/medicos/ e vai direto para lá -->
<input type="hidden" name="next" value="{{ next }}">
<button class="btn btn-primary" type="submit">Entrar</button>
</form>
</section>
</main>
{% endblock %}
Templates — Médico
Listagem de Médicos
{% extends 'intranet/base_logado.html' %} <!-- herda o layout com menu de logado -->
{% load static %}
{% block title %}Listagem de Médicos{% endblock %}
{% block content %}
<main class="container">
<section class="card">
<h2 class="title">Listagem de médicos</h2>
<div class="table-controls">
<!-- {% url 'intranet:medico-create' %} → /medicos/novo/ -->
<a href="{% url 'intranet:medico-create' %}" class="btn btn-tertiary">
<img src="{% static 'intranet/assets/plus.png' %}" class="btn-icon"> Novo Médico
</a>
</div>
<table class="tabela">
<thead>...</thead>
<tbody>
<!-- {% for medico in medicos %}: itera sobre a lista "medicos" enviada pela View -->
{% for medico in medicos %}
<tr>
<td>{{ medico.nome }} {{ medico.sobrenome }}</td>
<td>{{ medico.email }}</td>
<td>{{ medico.crm }}</td>
<td>{{ medico.especialidade }}</td>
<td>
<!-- medico.pk = primary key (ID) do médico — usado na URL dinâmica -->
<a href="{% url 'intranet:medico-update' medico.pk %}">
<img src="{% static 'intranet/assets/edit.svg' %}" alt="Editar">
</a>
<a href="{% url 'intranet:medico-delete' medico.pk %}">
<img src="{% static 'intranet/assets/delete.svg' %}" alt="Excluir">
</a>
</td>
</tr>
{% empty %}
<!-- {% empty %}: exibido quando a lista está vazia -->
<tr><td colspan="5">Nenhum médico cadastrado.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
</main>
{% endblock %}
Formulário de Médico (Create & Update)
{% extends 'intranet/base_logado.html' %}
{% load static %}
{% block title %}{{ titulo }}{% endblock %}
<!-- {{ titulo }} vem do get_context_data da View: "Cadastro de Médico" ou "Editar Médico" -->
{% block content %}
<main class="container">
<section class="card form-card">
<h2 class="title">{{ titulo }}</h2>
<form method="post">
{% csrf_token %}
<!-- Renderizamos campo por campo para controlar o layout com as classes CSS -->
<div class="form-group">
<label for="id_nome">Nome:</label>
{{ form.nome }}
<!-- {{ form.nome }} renderiza o <input> do campo "nome" do MedicoForm -->
{% if form.nome.errors %}<p style="color:#c00;">{{ form.nome.errors }}</p>{% endif %}
<!-- form.nome.errors: lista de erros de validação deste campo específico -->
</div>
<!-- ... campos sobrenome, email, telefone, crm seguem o mesmo padrão ... -->
<div class="form-group">
<label for="id_especialidade">Especialidade:</label>
{{ form.especialidade }}
<!-- Renderiza o <select> com as opções de ESPECIALIDADES definidas no forms.py -->
</div>
<div class="buttons">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{% url 'intranet:medico-list' %}" class="btn btn-secondary">Voltar</a>
</div>
</form>
</section>
</main>
{% endblock %}
formulario.html serve tanto para o CreateView quanto para o UpdateView. A diferença é que o UpdateView pré-preenche os campos com os dados existentes — o Django faz isso automaticamente quando a view passa o object no contexto.Templates — Paciente
Os templates de paciente seguem exatamente a mesma estrutura dos de médico. A listagem exibe Nome, E-mail, Telefone e CPF (conforme o requisito da atividade). O formulário tem os campos: Nome, Sobrenome, E-mail, Telefone e CPF.
{% for paciente in pacientes %}
<tr>
<td>{{ paciente.nome }} {{ paciente.sobrenome }}</td>
<td>{{ paciente.email }}</td>
<td>{{ paciente.telefone }}</td>
<td>{{ paciente.cpf }}</td>
<td>
<a href="{% url 'intranet:paciente-update' paciente.pk %}">editar</a>
<a href="{% url 'intranet:paciente-delete' paciente.pk %}">excluir</a>
</td>
</tr>
{% empty %}
<tr><td colspan="5">Nenhum paciente cadastrado.</td></tr>
{% endfor %}
cpf.replace('.','').replace('-',''). Se quiser exibir formatado, use um filtro de template personalizado ou formate com JavaScript.Templates — Consulta
O formulário de consulta é o mais complexo — ele filtra os médicos por especialidade usando JavaScript no cliente, e busca o paciente pelo CPF no servidor.
{% extends 'intranet/base_logado.html' %}
{% load static %}
{% block content %}
<main class="container">
<section class="card form-card">
<h2 class="title">{{ titulo }}</h2>
<form method="post">
{% csrf_token %}
<!-- 1. Especialidade: dropdown não salvo, apenas filtra os médicos via JS -->
<div class="form-group">
<label>Especialidade:</label>
{{ form.especialidade }} <!-- renderiza o <select id="id_especialidade"> -->
</div>
<!-- 2. Médico: inicialmente vazio, preenchido pelo JavaScript -->
<div class="form-group">
<label>Médico:</label>
{{ form.medico_id }} <!-- <select id="id_medico_id"> -->
</div>
<!-- 3. CPF: o servidor usa este valor para buscar o Paciente -->
<div class="form-group">
<label>CPF do Paciente:</label>
{{ form.cpf_paciente }}
{% if form.cpf_paciente.errors %}
<p style="color:#c00;">{{ form.cpf_paciente.errors }}</p>
{% endif %}
</div>
<!-- 4. Data e hora -->
<div class="form-group">
<label>Data e Hora:</label>
{{ form.horario }} <!-- <input type="datetime-local"> -->
</div>
<div class="buttons">
<button type="submit" class="btn btn-primary">Salvar</button>
<a href="{% url 'intranet:consulta-list' %}" class="btn btn-secondary">Voltar</a>
</div>
</form>
</section>
</main>
{% endblock %}
{% block extra_scripts %}
<script>
// medicos_json: variável Python injetada no template pela view (ConsultaCreateView)
// |escapejs: filtro Django que escapa aspas e caracteres especiais para uso em JS
const todosOsMedicos = JSON.parse('{{ medicos_json|escapejs }}');
// Resultado: array de objetos [{id:1, nome:"João", sobrenome:"Silva", especialidade:"ORTOPEDIA"}, ...]
function filtrarMedicos() {
const especialidade = document.getElementById('id_especialidade').value;
// Captura o valor selecionado no dropdown de especialidades
const selectMedico = document.getElementById('id_medico_id');
const valorAtual = selectMedico.value; // guarda o médico já selecionado (edição)
selectMedico.innerHTML = '<option value="">---------</option>';
// Limpa o dropdown de médicos antes de popular
// Filtra o array: se especialidade selecionada, mostra apenas médicos dela
const lista = especialidade
? todosOsMedicos.filter(m => m.especialidade === especialidade)
: todosOsMedicos; // se nenhuma especialidade, mostra todos
lista.forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = m.nome + ' ' + m.sobrenome;
if (String(m.id) === String(valorAtual)) opt.selected = true;
// Re-seleciona o médico anterior se estiver editando
selectMedico.appendChild(opt);
});
}
// Filtra ao trocar a especialidade
document.getElementById('id_especialidade').addEventListener('change', filtrarMedicos);
// Filtra ao carregar a página (importante para pré-preencher no modo edição)
window.addEventListener('DOMContentLoaded', filtrarMedicos);
</script>
{% endblock %}
Listagem de Consultas
{% for consulta in consultas %}
<tr>
<!-- consulta.paciente_id é o OBJETO Paciente (ForeignKey), não o ID -->
<td>{{ consulta.paciente_id.nome }} {{ consulta.paciente_id.sobrenome }}</td>
<td>{{ consulta.medico_id.nome }} {{ consulta.medico_id.sobrenome }}</td>
<!-- |date: filtro Django que formata datetime -->
<td>{{ consulta.horario|date:"d/m/Y H:i" }}</td>
<!-- ex: 20/05/2026 14:30 -->
<!-- get_status_display(): retorna "Agendada" em vez de "A" -->
<td>{{ consulta.get_status_display }}</td>
<td>
<a href="{% url 'intranet:consulta-update' consulta.pk %}">editar</a>
<a href="{% url 'intranet:consulta-delete' consulta.pk %}">excluir</a>
</td>
</tr>
{% endfor %}
Arquivos Estáticos — CSS e Imagens
Arquivos estáticos são CSS, JavaScript e imagens que não mudam com as requisições. O Django os serve de forma especial para garantir cache e organização.
Onde ficam os arquivos
style.css, haveria conflito. A convenção é criar uma subpasta com o nome do app: intranet/static/intranet/style.css → URL pública: /static/intranet/style.css.Como usar nos templates
<!-- 1. SEMPRE carregue a tag static no topo do template -->
{% load static %}
<!-- 2. Use {% static 'caminho' %} para referenciar o arquivo -->
<link rel="stylesheet" href="{% static 'intranet/style.css' %}">
<!-- gera: /static/intranet/style.css -->
<img src="{% static 'intranet/assets/logo.svg' %}" alt="Logo">
<!-- gera: /static/intranet/assets/logo.svg -->
<!-- NUNCA use caminhos relativos como ../style.css em templates Django -->
<!-- O Django pode servir a página de qualquer URL, tornando relativos inválidos -->
O CSS do dropdown — detalhe importante
/* position: relative é obrigatório no elemento pai do dropdown */
/* Sem isso, o menu position:absolute sai fora do lugar */
.header .dropdown {
position: relative; /* ancora o menu abaixo deste elemento */
}
/* O menu começa oculto */
.header .dropdown-menu {
display: none;
position: absolute; /* posicionado em relação ao .dropdown pai */
top: 100%; /* aparece logo abaixo do elemento pai */
left: 0;
background-color: #339cff;
z-index: 1; /* fica na frente de outros elementos */
min-width: 150px;
}
/* Ao passar o mouse no .dropdown, o menu filho aparece */
.header .dropdown:hover .dropdown-menu {
display: block;
}
Autenticação — Login e Proteção
O Django já vem com um sistema completo de autenticação. Não precisamos criar nada do zero — apenas configuramos e conectamos ao nosso layout.
Como o login funciona
Proteção nas views
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
# ── Jeito 1: Function-based view ─────────────────────────────────────────────
@login_required # decorator: aplicado antes da função
def home(request): # se não estiver logado, redireciona para LOGIN_URL
return render(request, 'intranet/home.html')
# ── Jeito 2: Class-based view ─────────────────────────────────────────────────
class MedicoListView(LoginRequiredMixin, ListView):
# LoginRequiredMixin DEVE ser o primeiro da herança
# Se não estiver logado, redireciona para LOGIN_URL definido no settings
model = Medico
template = 'intranet/medico/listagem.html'
Configurações de autenticação no settings.py
LOGIN_URL = '/login/' # URL da página de login
LOGIN_REDIRECT_URL = '/home/' # onde ir APÓS login bem-sucedido
LOGOUT_REDIRECT_URL = '/' # onde ir APÓS logout
Execute no terminal com o ambiente virtual ativado:
python manage.py createsuperuser
O Django pedirá: nome de usuário, e-mail (opcional) e senha.
Este usuário pode acessar o painel em
/admin/ E fazer login em /login/.Fluxo Completo — Como Tudo se Conecta
Veja o caminho completo de uma requisição — do clique do usuário até o HTML na tela:
Resumo das tecnologias e suas funções
| Tecnologia | Função no projeto |
|---|---|
| Python | Linguagem base de todo o backend |
| Django | Framework web: ORM, views, templates, autenticação |
| SQLite | Banco de dados relacional (arquivo único, ideal para aprendizado) |
| HTML + CSS | Interface visual do sistema (templates + static files) |
| JavaScript | Filtro dinâmico de médicos por especialidade no formulário de consulta |
| Prism.js | Syntax highlighting desta apostila |
Comandos do dia a dia
# Ativar ambiente virtual
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
# Subir o servidor de desenvolvimento
python manage.py runserver # acesse http://127.0.0.1:8000/
# Criar superusuário (primeiro acesso)
python manage.py createsuperuser
# Após alterar models.py
python manage.py makemigrations # gera o arquivo de migration
python manage.py migrate # aplica no banco
# Abrir shell Python com contexto Django (útil para testar queries)
python manage.py shell
# Exemplo de uso no shell:
# from intranet.models import Medico
# Medico.objects.all() → lista todos os médicos
# Medico.objects.filter(ativo=True) → filtra apenas ativos
# Medico.objects.get(pk=1) → busca pelo ID
Apostila — Sistema Médico com Django | Consulta.me
2026 © Desenvolvido por Aluno | Projeto fictício sem fins comerciais.
Apostila — Sistema Médico com Django | Consulta.me
2026 © Desenvolvido por Aluno | Projeto fictício sem fins comerciais.