Programa de Curso APOSTILA · Sistema Médico Django
← Início
01

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.

CamadaResponsabilidadeNo nosso projeto
ModelDefine a estrutura do banco de dadosintranet/models.py
ViewRecebe a requisição, processa e retorna a respostaintranet/views.py
TemplateA interface HTML exibida ao usuáriointranet/templates/
💡 Como funciona uma requisição no Django? O navegador acessa uma URL → Django encontra a View correspondente nas URLs → a View consulta os Models (banco) → a View renderiza um Template com os dados → o HTML é devolvido ao navegador.
02

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_medico_django/ ← pasta raiz do projeto │ ├── sistema/ ← pacote de configurações do projeto │ ├── settings.py ← configurações globais (banco, apps, etc.) │ ├── urls.py ← roteamento global de URLs │ ├── wsgi.py ← ponto de entrada para servidores web │ └── asgi.py ← suporte a conexões assíncronas │ ├── intranet/ ← nosso app principal │ ├── migrations/ ← arquivos gerados de migração de banco │ ├── templates/ ← arquivos HTML (templates Django) │ │ └── intranet/ │ │ ├── base.html ← template base (layout comum) │ │ ├── base_logado.html ← layout para usuários autenticados │ │ ├── index.html / home.html │ │ ├── login.html │ │ ├── medico/ │ │ ├── paciente/ │ │ └── consulta/ │ ├── static/ ← arquivos estáticos (CSS, imagens) │ │ └── intranet/ │ │ ├── style.css │ │ └── assets/ │ ├── models.py ← define as tabelas do banco de dados │ ├── views.py ← lógica de cada página │ ├── forms.py ← formulários de entrada de dados │ ├── urls.py ← rotas do app │ └── admin.py ← configuração do painel admin │ ├── apostila/ ← esta apostila ├── db.sqlite3 ← banco de dados SQLite (arquivo) └── manage.py ← utilitário de linha de comando do Django
✅ Por que separar em "projeto" e "app"? O projeto (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

Terminal Bash
# 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
⚠️ Atenção ao ponto no startproject O . no final de startproject sistema . diz ao Django para criar os arquivos na pasta atual, evitando uma pasta extra desnecessária.
03

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.

intranet/models.py Python
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

CampoTipo PythonUso típico
CharFieldstrNome, telefone, CEP — textos curtos com limite definido
TextFieldstrDescrições longas, observações — sem limite definido
EmailFieldstrE-mail — valida o formato automaticamente
IntegerFieldintNúmeros inteiros
BooleanFieldboolAtivo/inativo, verdadeiro/falso
DateTimeFielddatetimeData e hora
ImageFieldstr (caminho)Upload de imagem — exige Pillow instalado
ForeignKeyobjetoRelacionamento N:1 com outra tabela
💡 ForeignKey — Chave Estrangeira Quando escrevemos 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.
04

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.

intranet/admin.py Python
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')
✅ Como criar o usuário administrador Execute no terminal: python manage.py createsuperuser. O Django pedirá nome de usuário, e-mail e senha. Depois acesse http://127.0.0.1:8000/admin/.
05

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ê.

Fluxo de migrations: Você edita Django gera Django aplica models.py → makemigrations → migrate (cria arquivo) (executa SQL)
Terminal Bash
# 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:

intranet/migrations/0001_initial.py (gerado automaticamente) Python
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)
    ]
⚠️ Nunca edite arquivos de migration manualmente Esses arquivos são gerados pelo Django e formam um histórico. Editar à mão pode corromper o banco. Se precisar alterar um model, edite o models.py e rode makemigrations novamente.
06

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.

sistema/settings.py Python
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
07

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.

Fluxo de URLs: Navegador sistema/urls.py intranet/urls.py View acessa → roteia para → encontra a rota → executa /medicos/ include(intranet) medico-list e retorna HTML

URLs globais do projeto

sistema/urls.py Python
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

intranet/urls.py Python
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'),
]
💡 Por que usar namespace? Se você tiver dois apps com uma rota chamada list, o Django não saberia qual usar. Com app_name = 'intranet', chamamos {% url 'intranet:medico-list' %} — sem ambiguidade.
08

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.

intranet/forms.py Python
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
💡 Como funciona o clean_CAMPO? Qualquer método chamado 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.
09

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

intranet/views.py Python
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

AspectoFunction-based (def)Class-based (class)
Quando usarLógica simples e personalizadaOperações padrão de CRUD
CódigoExplícito — você escreve tudoConciso — herda comportamentos
Proteção de login@login_requiredLoginRequiredMixin
Exemploindex, homeListView, CreateView…
💡 O que é reverse_lazy? 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.
10

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.

Hierarquia de Templates: base.html ← estrutura HTML completa (doctype, head, footer) │ └── {% block header %} ← header sem login (logo + botão Login) │ └── {% block content %} ← área central vazia │ ├── base_logado.html ← sobrescreve apenas o {% block header %} │ └── header com dropdown CADASTROS e nome do usuário │ ├── index.html ← extends base.html → página inicial não logada ├── login.html ← extends base.html → formulário de login ├── home.html ← extends base_logado.html → hero pós-login ├── medico/listagem.html ← extends base_logado.html → tabela de médicos ├── medico/formulario.html ← extends base_logado.html → form de médico └── consulta/formulario.html ← extends base_logado.html → form de consulta

Template base.html

intranet/templates/intranet/base.html 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

intranet/templates/intranet/base_logado.html 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 %}
⚠️ Por que o logout é um formulário POST? Se o logout fosse um simples link GET, qualquer imagem ou link malicioso numa página externa poderia deslogar o usuário sem ele querer. Por isso o Django exige método POST para ações que alteram estado, e o {% csrf_token %} garante que o POST veio realmente do nosso site.
11

Template — Página de Login

intranet/templates/intranet/login.html HTML
{% 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 %}
12

Templates — Médico

Listagem de Médicos

intranet/templates/intranet/medico/listagem.html HTML
{% 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)

intranet/templates/intranet/medico/formulario.html HTML
{% 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 %}
✅ Um formulário para criar E editar O mesmo template 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.
13

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.

intranet/templates/intranet/paciente/listagem.html (estrutura) HTML
{% 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 no banco sem formatação O model guarda o CPF como texto puro (11 dígitos, sem pontos e traço). O formulário de consulta limpa a formatação antes de buscar: cpf.replace('.','').replace('-',''). Se quiser exibir formatado, use um filtro de template personalizado ou formate com JavaScript.
14

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.

intranet/templates/intranet/consulta/formulario.html HTML
{% 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

consulta/listagem.html (trecho da tabela) HTML
{% 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 %}
15

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

intranet/static/intranet/ ├── style.css ← toda a estilização do projeto └── assets/ ├── logo.svg ← logotipo do sistema ├── medico.png ← ilustração do médico (hero) ├── medica.png ← ilustração da médica (hero) ├── plus.png ← ícone de adicionar (+) ├── edit.svg ← ícone de editar (lápis) ├── delete.svg ← ícone de excluir (lixeira) ├── back.png ← ícone de voltar (seta) └── logo-alura.png ← logo do rodapé
💡 Por que a pasta se chama intranet/static/intranet/? Django unifica os estáticos de todos os apps em uma pasta só. Se dois apps tiverem um arquivo 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

Usando arquivos estáticos em qualquer template HTML
<!-- 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

intranet/static/intranet/style.css (trecho do dropdown) CSS
/* 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;
}
16

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

Fluxo de Login: 1. Usuário acessa /medicos/ sem estar logado 2. LoginRequiredMixin detecta que não há sessão ativa 3. Redireciona para /login/?next=/medicos/ 4. Usuário preenche usuário/senha 5. Django verifica no banco (tabela auth_user) 6. Se correto: cria sessão, salva cookie no browser 7. Redireciona para /medicos/ (valor de "next") 8. Nas próximas requisições, Django lê o cookie e identifica o usuário

Proteção nas views

Dois jeitos de proteger uma view Python
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

sistema/settings.py (parte de autenticação) Python
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
✅ Como criar o primeiro usuário administrador
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/.
17

Fluxo Completo — Como Tudo se Conecta

Veja o caminho completo de uma requisição — do clique do usuário até o HTML na tela:

Exemplo: usuário clica em "Novo Médico" BROWSER GET /medicos/novo/ │ ▼ sistema/urls.py path('', include('intranet.urls')) → delega para intranet │ ▼ intranet/urls.py path('medicos/novo/', MedicoCreateView.as_view(), name='medico-create') │ ▼ intranet/views.py — MedicoCreateView 1. LoginRequiredMixin verifica sessão → OK, usuário está logado 2. get() é chamado → instancia um MedicoForm vazio 3. get_context_data() → {'form': <form vazio>, 'titulo': 'Cadastro de Médico'} 4. render(template, context) → processa o template com os dados │ ▼ intranet/medico/formulario.html 1. {% extends base_logado.html %} → carrega o layout 2. {{ titulo }} → "Cadastro de Médico" 3. {{ form.nome }}, {{ form.email }}... → <input>s HTML renderizados 4. HTML completo gerado │ ▼ BROWSER recebe o HTML → exibe o formulário ao usuário Exemplo: usuário preenche e clica em "Salvar" BROWSER POST /medicos/novo/ (dados: nome=João, email=joao@..., crm=12345...) │ ▼ intranet/views.py — MedicoCreateView.post() 1. MedicoForm(request.POST) → instancia o form com os dados enviados 2. form.is_valid() → valida todos os campos - CharField não pode estar vazio - EmailField valida formato de e-mail - ChoiceField valida se a especialidade existe na lista 3. Se inválido → renderiza o template novamente com os erros 4. Se válido → form.save() → INSERT INTO intranet_medico (...) │ ▼ Redirect para /medicos/ │ ▼ BROWSER exibe a listagem com o novo médico incluído

Resumo das tecnologias e suas funções

TecnologiaFunção no projeto
PythonLinguagem base de todo o backend
DjangoFramework web: ORM, views, templates, autenticação
SQLiteBanco de dados relacional (arquivo único, ideal para aprendizado)
HTML + CSSInterface visual do sistema (templates + static files)
JavaScriptFiltro dinâmico de médicos por especialidade no formulário de consulta
Prism.jsSyntax highlighting desta apostila

Comandos do dia a dia

Terminal — Comandos úteis Bash
# 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.