Descrição do Desafio
Projeto de Desenvolvimento End-to-End, ou seja, conta com um sistema de Gestão Financeira com Front-end e Back-end, arquitetura e gerenciamento de containers e observabilidade.
Contexto do Servidor
Antes de inciar este projeto, meu servidor VPS possuia apenas um sistema operacional Rocky Linux na versão 9.5.
Sendo assim foi necessário utilizar alguns comandos com o objetivo de instalar o Docker de maneira correta e segura.
[root ~] sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Comando utilizado para apontar o servidor oficial e seguir com a adicição do repositório dentro da VPS.
[root ~] sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Comando utilizado para instalar os pacotes necessários.
[root ~] sudo systemctl enable --now docker
No Rocky Linux, é necessário habilitar um módulo para que o serviço inicie de modo automático sempre que o servidor ligar.
[root ~] sudo systemctl status docker
Comando para validar o status do serviço.
O Rocky Linux vem com um Firewall, o firewalld, ativo por padrão. Então é preciso ter em mente que se o projeto for rodar na porta 8000, por exemplo, será preciso avisar o servidor.
[root ~] sudo firewall-cmd --state
Comando utilizado para validar que o Firewall está ativo. Se o retorno for running, ele está ativo.
[root ~] sudo firewall-cmd --list-all
Esse comando lista todas as regras atuais do servidor. Para validar as informações referentes as portas abertas, é só procurar pela linha ports: . Se ela estiver vazia, significa que apenas os serviços padrão como: SSH na porta 22, estão abertos.
Para este projeto vou utilizar a porta 8000 para evitar conflito com outros sites que já estão rodando.
[root ~] sudo firewall-cmd --permanent --add-port=8000/tcp
Este é o comando para abrir a porta 8000. Após rodar o comando, deve retornar success.
[root ~] sudo firewall-cmd --reload
Comando utilizado para aplicar um reload no Firewall e aplicar a nova mudança.
Nesse ponto aqui a estrutura inicial do Docker está configurada corretamente em minha VPS.
Estrutura de Pastas e Arquivos
Comecei criando o diretório do projeto dentro do servidor:
[root ~] mkdir gestao_financeira
Agora vou seguir para o VS Code para criar os arquivos iniciais.
VS Code
Para construir o ambiente dentro do Docker e manter o conrtole de versão de maneira segura e correta alguns arquivos padrão devem ser criados:
- Dockerfile
- docker-compose.yml
- .gitignore
- .env
- requirements.txt
Vou falar sobre cada um deles:
-
Dockerfile
É como uma receita de bolo, contém todas as instruções passo a passo para o Docker construir a imagem do projeto.
Começo definindo a imagem que será utilizada:
FROM python:3.11-slim
Não permite que arquivo .pyc sejam criados e habilita logs em tempo real.
ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONBUFFERED 1
Define a pasta onde o código vai morar dentro do container
WORKDIR /app
Instala as dependências do sistema necessárias para o Banco de Dados PostgreSQL.
O Rocky Linux é RHEL, mas dentro do container será utilizado o Debian (slim), pois é o padrão de mercado para imagens Python
RUN apt-get update && apt-get install -y \ libpq-dev \ gcc \ && rm -rf /var/lib/apt/lists/*
Copia o arquivo de requisitos primeiro para aproveitar o cache do Docker
COPY requirements.txt .
Instala as bibliotecas do projeto
RUN pip install --upgrade pip && \ pip install --no-cache-dir -r requirements.txt
Copia todo o conteúdo do projeto
COPY . .
Porta onde o app vai rodar
EXPOSE 8000 -
docker-compose.yml
É o maestro da infraestrutura. Ele gerencia como vários serviços convivem.
Dentro do docker-compose.yml os componentes são separados por seções:
-
Seção do Banco de Dados
Inicia a lista de todos os containers que compõem a aplicação.
services:
É o nome interno do serviço. Outros containers usarão esse nome como se fosse um "endereço web" para falar com o banco.
db:
Dá um nome fixo ao container no sistema. Sem isso, o Docker gera nomes aleatórios.
container_name: finance_db
Se a VPS cair ou o processo do Banco de Dados travar, o Docker reinicia automaticamente.
restart: always
Diz ao Compose para ler o arquivo
.env. Nele contém informações das variáveis de ambiente, como usuário e senha do Banco de Dados.env_file: - ./dotenv_files/.env
Aqui as variáveis de ambiente do arquivo
.envsão mapeadas para as variáveis que o Postgres entende. O símbolo${VAR}busca o valor dentro do arquivo.env.environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
Se o container for deletado, os dados continuam salvos no volume.
O volume aponta para uma pasta gerenciada pelo Docker no disco da VPS.
volumes: - postgres_data:/var/lib/postgresql/data
-
Seção do Banco de Dados
-
.gitignore
Se trata de um arquivo que fica na raiz do projeto e funciona como um filtro pra o Git. Neste arquivo contém uma lista de arquivos ou diretórios que devem ser ignorados pelo Git e dessa forma, não são enviados para o repositório.
Em uma aplicação que possui variáveis de ambiente, geralmente elas são condensadas em um único arquivo que normalmente é chamado de
.env.É uma boa prática adicionar este arquivo
.envao.gitignore, pois dessa forma o arquivo.envé ignorado e não é adiconado ao seu repositório do GitHub.Sem o
.gitignoreo arquivo com todas as variáveis de ambiente estaria disponibilizado publicamente.Um outro objetivo para o
.gitignoreé limpar o repositório. Por exemplo, na linguagem Python, é comum a existência de arquivos temporarios, como a pasta __pycache__, que não servem para a funcionalidade do código. Ao adicionar este diretório no.gitignoreevita que este diretório ocupe espaço e polua o histórico. -
.env
Env significa environment (meio ambiente), é o arquivo no qual é comum guardarmos todas as variáveis de ambiente utilizadas em uma aplicação, o que é uma boa prática na questão de segurança e configuração de aplicações modernas.
Ele impede que as senhas de conexão com o banco de dados, API Keys e etc fiquem expostas no código-fonte ou no GitHub.
Permite a mudança de configurações de maneira mais facilitada, visto que a chave de conexão fica concentrada em uma variável específica.
-
requirements.txt
Este arquivo garante que o ambiente de Desenvolvimento seja idêntico em todos os lugares que passar.
Ao ser utilizado em conjunto com o comando pip, permite que todos as bibliotecas e ferramentas utilizadas na aplicação sejam instaladas e configuradas na versão em que a aplicação foi desenvolvida.
Seguindo agora, vou falar um pouco sobre o Framework utilizado no Projeto.
Django Framework
Mas antes de seguirmos:
O que é um Framework?
Um Framework é um conjunto de ferramentas e bibliotecas e padrões predefinidos que funcionam como base para o desenvolvimento de um software, o que facilita a criação de aplicações de maneira mais rápida, eficiente e padronizada. O Framework elimina a necessidade de reescrever códigos comuns.
O que é Django?
É um Framework Python que peermite criar aplicações web de forma rápida e eficiente, cuidando de tarefas comuns como autenticação~, segurança e interação com banco de dados. Dessa forma, o desenvolvedor pode focar na lógica do projeto.
Ele segue o padrão MVT (Model-View-Template) e oferece ferramentas como ORM (Object-Relational-Mapper) para facilitar o trabalho com banco de dados e um painel administrativo.
Para desenvolver um Projeto utilizando o Framework Django, é necessário entrar que os Frameworks possuem uma estrutura que deve ser respeitada para que a aplicação funcione corretamente. Sendo assim, vou explicar um pouco sobre a estrutura do Django e alguns conceitos básicos que serão pontuados na etapa de Desenvolvimento.
Arquitetura MVT (Model-View-Template)
-
Model (Base de Dados):
Camada onde é definido a estrutura dos dados usando classes Python. O Django utiliza um ORM, o que significa que não é necessário escrever SQL manualmente.
No contexto do projeto são as classes
Lancamento,CategoriaeMeta. -
Template (Camada de Apresentação):
É o que o usuário vê. São arquivos HTML que utilizam
DLT (Django Template Language)para exibir dados dinâmicos dentro do HTML estático. -
View (Lógica de Negócio):
O cérebro do framework. Ele recebe uma requisição HTTP, busca os dados na Model, aplica a lógica necessária e entrega o resultado para a Template.
Conceitos
Alguns conceitos básicos e importantes que devemos conhecer para entender melhor o Desenvolvimento do sistema.
-
App
No Django, um projeto é composto por várias Apps.
Cada App deve ser responsável por uma funcionalidade específica.
-
ORM
Permite que o desenvolvedor interaja com o banco de dados usando apenas Python.
Exemplo: Ao invés de usar a
SELECT * FROM lancamentos, utilizeiLancamento.objects.all(). Isso garante que, se um dia o banco de dados passar por alterações, o código continuará funcionando. -
Sistema de autenticação
O Django já vem com um sistema de login, logout, permissões e criptografia de senhas pronto para uso. No
PoupAí, é utilizado para garantir que apenas os usuários logados vejam seus próprios dados. -
Painel administrativo
Com poucas linhas de código, o Django gera uma interface administrativa completa para que um admin possa gerenciar o banco de dados.
Estrutura de Arquivos Django
No Django a estrutura é organizada em projetos e aplicativos baseada no padrão MVT.
-
Arquivos de Configuração
São os arquivos que comandam o projeto como um todo.
Ao criar um projeto com
django-admin startproject nome_do_projeto, a seguinte estrutura é criada.-
settins.pyO coração do projeto. É onde configuramos o banco de dados, definimos onde estão os arquivos estáticos, ativamos o WhiteNoise e gerenciamos a segurança.
-
wsgi.py/asgi.pySão as portas de serviço que o NIGINX e o Gunicorn usam para fazer o site rodar no servidor.
-
manage.pyÉ o utilitário de linha de comando. Não editamos este arquivo, mas usamos para tudo: rodar o servidor, fazer migrações e criar usuários.
-
-
Arquivos do App
Os arquivos são gerados com
python manage.py startapp nome_do_appcriando a seguinte estrutura:-
urls.pyLocal centralizado onde as rotas (URLs) são configuradas e mapeadas. É essencial para o funcionamento das URLs do site.
-
views.pyÉ onde fica a lógica do Backend da aplicação, conecta a
ModelaoTemplate. Define o que acontece quando uma URL específica é acessada, processando os dados antes de exibir. -
models.pyEste arquivo define a estrutura do banco de dados usando classes Python para representar tabelas. Utiliza ORM para mapear objetos diretamente para o banco de dados, permitindo criar, manipular e consultar dados sem escrever SQL, além de gerenciar relacionamentos entre entidades.
-
admin.pyEste arquivo serve para registrar seus modelos no painel administrativo nativo do Django.
-
-
Outros diretórios importantes:
-
templates/Onde ficam os arquivos HTML. No Django, é utilizado a DLT, que permite usar a lógica dentro do HTML.
-
static/Guarda arquivos CSS, JS e de imagens. No
PoupAí, o NIGNX e o WhiteNoise trabalham juntos para entregar esses arquivos rapidamente. -
migrations/O histórico de alterações do seu banco de dados. Diz ao banco de dados como ele deve se comportar.
-
E por fim, vamos falar sobre o Banco de Dados escolhido.
PostgreSQL
O PostgreSQL é um SGBD (Sistema de Gerenciamento de Banco de Dados) Relacional de código aberto. Ele não apenas armazena informações; ele garante que as relações entre elas sejam sólidas e confiáveis.
No PoupAí, o Postgres atua como o repositório seguro para cada transação, meta e categoria criada pelos usuários.
Porque optei por utilizar o Postgres e não o SQLite que é padrão do Django?
Durante o desenvolvimento local, o Django costuma usar o SQLite. Porém, a minha ideia desde o começo era seguir com um deploy para o meu servidor VPS, então escolhi o Postgres pelos seguintes motivos:
-
Concorrência:
O Postgres lida com múltiplos usuários acessando e gravando dados ao mesmo tempo sem gravar.
-
Robustez:
Ele é feito para rodar 24/7 em servidores Linux sem corromper dados.
-
Tipagem Forte:
Ele garante que uma data seja uma data e um valor financeira seja um número decimal exato.
O Postgres é "Relacional" porque organiza os dados em tabelas que se conectam através de Chaves. No PoupAí, temos três conceitos fundamentais:
-
Tabelas:
Padrão de qualquer banco de dados relacional. O
PoupAícontém as tabelasUsers,Categorias,Lancamentos. -
Foreign Keys (Chaves Estrangeiras):
É o vínculo entre as tabelas
Exemplo: Cada
Lancamentopossui uma "Chave Estrangeira" que aponta para umaCategoria. Isso garante que o usuário não possa ter um gasto sem uma categoria válida. -
Integridade Referencial:
Se o usuário tentar excluir uma categoria que possui gastos vinculados, o Postgres (seguido pelo Django) impede a ação para evitar dados órfãos.
O Postgre segue o princípio ACID, que é vital para sistemas de gestão financeira:
-
Atomicidade:
Ou a transação financeira acontece inteira, ou não acontece nada. Se a luz cair no meio de um salvamento, o banco não registra "metade" do dado.
-
Consistência:
O banco garante que os dados sigam todas as regras.
-
Isolamento:
Uma transação não interfere em outra acontecendo ao mesmo tempo.
-
Durabilidade:
Uma vez que o Postgres diz "salvo", o dado está gravado no disco rígido de forma permanente.
Agora que você entendeu alguns conceitos básicos e as ferramentas utilizadas, posso seguir para uma parte um pouco mais complexa.
Arquitetura
[ADICIONAR IMAGEM]
A imagem acima mostra a arquitetura que criei para este projeto.
O PoupAí possui uma robustez que não se limita apenas ao código, mas na forma como os componentes se comunicam de maneira segura e eficente dentro da infraestrutura. O diamagra da arquitetura detalha 5 (cinco) camadas principais.
-
Camada de Acesso Externo - Conexão HTTP/HTTPS
À esquerda temos o Browser/Usuário. É o primeiro contato do cliente com a aplicação.
Quando o cliente busca porpoupai.suzanacavalcante.com.br, uma requisição via protocolo HTTP/HTTPS (portas 80/443) é enviada para o Servidor VPS e chega ao ponto de entrada da aplicação que, neste caso, é o NGINX que possui um Proxy Reverso configurado e atua como uma camada de segurança e eficiência que isola o servidor de aplicação (Django) do acesso direto à internet. -
VPS
Primeiramente, o que é uma VPS?
[IMAGEM VPS]
VPS significa Virtual Private Server, e basicamente é a virtualização de um servidor físico.
A virtualização divide logicamente o servidor em servidores menores e virtuais. Um servidor físico pode ter vários servidor virtualizados. Os servidores virtualizados utilizam os mesmos recursos já que fazem parte do mesmo servidor físico, porém cada VPS opera com um servidor dedicado e cada uma possui o seu próprio sistema operacional, recursos e é possível personalizar a configuração.O meu servidor VPS possui uma estrutura própria e roda outros sites.
O sistema operacional é um Rocky Linux.
Utilizo o aaPanel para gerenciar algumas coisas utilizando uma interface gráfica. Uma das coisas que gerencio por ele é o NGINX.
Como segurança de entrada utilizo o NGINX Proxy Reverso. Ele recebe o tráfego nas portas 80/443 e redireciona para o local correto, seja um diretório, um arquivo ou um Container Docker.
-
Orquestração e Rede Internet (Docker Network)
O Docker é um orquestrador de Containers.
Um container é um ambiente virtualizado e isolado dentro do seu próprio servidor. Neste ambiente isolado, tudo referente ao projeto é instalado dentro do container e isso, por sua vez, não reflete na estrutura principal da VPS.Utilizei o Docker com a intenção de criar uma rede virtual privada para guardar os containers. A comunicação entre o NGINX e o container do Django ocorre internamente através dessa rede.
O tráfego é encvaminhado para o servidor de aplicação Gunicorn, que interpreta o código Python e processa a lógica de negócio de forma isolada.Quais ferramentas instalamos inicialmente no Container?
Python/Django: A aplicação está aqui dentro, toda a lógica, processamento de URLs, Views e etc...
PostgreSQL DB: O Banco de Dados isolado, garantindo que as informações financeiras estejam salvas e seguras.
WhiteNoise: A engrenagem que mostra como o Django serve o CSS e as imagens do site (arquivos estáticos).
-
Comunicação com a Camada de Dados (SQL/ORM)
A interação entre a aplicação e o banco de dados PostgreSQL é feita via Django ORM.
- Protocolo: A conexão utiliza o protocolo nativo do PostgreSQL na porta interna 5432.
- Fluxo: As requisições de salvar ou consultar transações financeiras são traduzidas do Python para consultas SQL, garantindo integridade referencial e o cumprimento das propriedades ACID.
-
Persistência e Entrega de Estáticos (Volumes & WhiteNoise)
Diferente dos processos voláteis, a persistência de arquivos é garantida por Docker Volumes:
- Arquivos Estáticos e Mídia: O WhiteNoise gerencia a entrega de arquivos CSS e JS diretamente do container Django, assim otimizando cache.
- Mapeamento de Disco: Todos os dados do banco e uploads de usuários são mapeados do container para o disco da VPS, pois isso assegura que, mesmo em casos de reinicialização dos containers, nenhuma informação será perdida.
Nesse ponto aqui, você já deve ter entendido o que é o Django e seus conceitos principais, o motivo de eu ter escolhido utilizar o PostgreSQL e a Arquitetura da aplicação.
Agora podemos seguir para o desenvolvimento do ambiente e da aplicação
Desenvolvimento
Vou dividir esse tópico em duas partes e vou dividir essas partes em subtópicos explicando os passos:
- Desenvolvimento do Ambiente
- .gitignore
- Dockerfile
- docker-compose.yml
- .env
- .dockerignore
- scripts
- Desenvolvimento da Aplicação
- Django
- project
- asgi.py
- settings.py
- urls.py
- wsgi.py
- accounts
- migrations
- templates
- admin.py
- apps.py
- forms.py
- models.py
- urls.py
- views.py
- manage.py
- requirements.txt
Agora posso iniciar a explicação.
Desenvolvimento do Ambiente
O desenvolvimento do ambiente envolve toda a parte da criação e configuração do container. Esse passo antecede o desenevolvimento do sistema.
.gitignore
Como foi explicado anteriormente, o arquivo .gitignore fica na raiz do projeto e funciona como um filtro para o Git, ou seja, tudo o que for declarado nesse arquivo será ignorado pelo Git e não será enviado para o repositório.
Existem algumas coisas que são comumente usadas em muitos projetos, então é sim possível buscar algum template de .gitignore e você apenas adiciona o que é específico do seu projeto.
Vou explicar aqui apenas o que é específico para o projeto:
-
/djangoapp/staticNo ambiente de produção, é gerado uma cópia dos arquivos CSS e JS pelo próprio Django. Como eles são gerados a partir do código fonte, não é necessário enviar para o repositório.
-
/djangoapp/mediaAqui ficam fotos que o usuário pode fazer upload. Esses arquivos pertencem ao usuário. Se subir essa pasta para o repositório, qualquer pessoa que baixar o repositório localmente terá acesso às imagens.
-
/djangoapp/project/local_settings.pyContém informações que só serão utilizadas pelo servidor específico onde a aplicação irá rodar.
Se este arquivo subir para o repositório ele pode sobrescrever as configurações de outros desenvolvedores do projeto.
-
/data/Geralmente é utilizado por bancos de dados. São dados brutos e pesados que devem ficar guardados no servidor e não devem ser enviados para o repositório.
-
*.log,*.pot,*.pyc,__pycache__,db.sqlite3,gunicorn-error-log
*.logegunicorn-error-logSão registros de erros. Eles mudam o tempo todo e só fazem sentido dentro do servidor onde o erro ocorreu. Não há motivos para armazenar esses arquivos no repositório do projeto.
__pycache__e*.pycSão arquivos Python criados para armazenar cache e acelerar a leitura do código. Esses arquivos são gerados automaticamente pelo Python, então enviar para o repositório só o deixaria mais pesado e sujo.
db.sqlite3É o banco de dados local utilizado pelo Django. Porém este não é o banco de dados utilizado na aplicação, então não faz sentido enviar para o repositório algo que não é utilizado.
-
.envEste é um arquivo que contém o conteúdo das variáveis de ambiente, ou seja, as senhas do banco de dados, a chave secreta do Django e outras senhas utilizadas pela VPS ou pela aplicação.
São informações sensíveis que, se forem disponibilizadas em um repositório, vai facilitar o trabalho de uma pessoa mal intencionada.
Os demais arquivos que o .gitignore está ignorando podem ser vistos no repositório do projeto no GitHub.
Dockerfile
É como uma receita de bolo para criar uma Imagem Docker. Ele contém um conjunto de instruções em sequência que o Docker lê para montar um ambiente isolado, o que garante que o projeto rode exatamente da mesma forma tanto localmente quanto em meu Servidor VPS.
Vou explicar o código detalhadamente:
FROM python:3.11.3-alpine3.18
LABEL mantainer="https://suzanacavalcante.com.br"
FROM: Define a imagem pai. Aqui estou utilizando a versão Alpine, que é uma versão Linux muito leve e tem foco em segurança, o que é perfeito para a produção.
LABEL: Apenas um metadado informando quem é o mantenedor do projeto.
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
PYTHONDONTWRITEBYTECODE: Impede que o Python gere arquivos .pyc dentro do container.
PYTHONUNBUFFERED: Garante que os logs do Python apareçam em tempo real no terminal do Docker.
COPY ./djangoapp /djangoapp
COPY ./scripts /scripts
WORKDIR /djangoapp
EXPOSE 8000
COPY: Copia os arquivos informados do local de desenvolvimento para dentro do container.
WORKDIR: Define que, a partir de agora, qualquer comando será executado dentro da pasta /djangoapp.
EXPOSE: Informa que o Container terá a porta 8000.
RUN python -m venv /venv && \
/venv/bin/pip install --upgrade pip && \
/venv/bin/pip install -r /djangoapp/requirements.txt && \
adduser --disabled-password --no-create-home duser && \
mkdir -p /data/web/static && \
mkdir -p /data/web/media && \
chown -R duser:duser /data/web/static && \
chmod -R 755 /data/web/static && \
chmod -R +x /scripts
RUN python -m venv /venv && \: Cria o ambiente virtual
/venv/bin/pip install --upgrade pip && \: Atualiza o gerenciador de pacotes
/venv/bin/pip install -r /djangoapp/requirements.txt && \: Instala as bibliotecas do PoupAí
adduser --disabled-password --no-create-home duser && \: Cria um usuário do sistema
mkdir -p /data/web/static && \: Cria pastas para CSS/JS
mkdir -p /data/web/media && \: Cria pastas para uploads
chown -R duser:duser /data/web/static && \: Dá permissão para o usuário duser
chmod -R 755 /data/web/static && \: Define permissões de leitura/escrita
chmod -R +x /scripts: Torna seus scripts de inicialização executáveis
ENV PATH="/scripts:/venv/bin:$PATH"
USER duser
CMD ["commands.sh"]
ENV PATH: Informa ao Linux do Container onde buscar quando um comando for iniciado.
USER duser: Essencial para a segurança do Container. Por padrão, o Docker roda como root, então, ao mudar para duser, é garantido que, se alguèm invadir a aplicação, não terá controle total sobre o servidor.
CMD: É o comando final e só roda quando o container está ativo. Aqui estou chamando o script que inicia o servidor Django.
docker-compose.yml
É uma ferramenta de orquestração que permite definir e rodar aplicações de múltiplos containers. No caso do PoupAí, existem dois containers principais: o servidor web (Django) e o banco de dados (PostgreSQL). Este arquivo especifica como eles devem se conectar, quais portas utilizar e etc...
version: '3.9'
services:
version: Define a versão da sintaxe do Docker Compose que será utilizada.
services: Inicia a lista de todos os containers que farão parte da aplicação.
djangoapp:
restart: always
container_name: djangoapp
build:
context: .
ports:
- 8000:8000
restart: always: Se o container cair, o Docker tentará ligá-lo automaticamente.
build: context: .: Diz ao Docker para procurar o Dockerfile na pasta atual para construir a imagem deste serviço.
ports: - 8000: 8000: Faz a ponte entre a requisição de acesso à aplicação e o container.
volumes:
- ./djangoapp:/djangoapp
- ./data/web/static:/data/web/static
- ./data/web/media:/data/web/media
volumes: É um espelhamento de diretórios. O que é armazenado dentro do diretório ./djangoapp do Container refle instantaneamente na VPS. É fundamental para que os arquivos estáticos não sumam quando o container for reiniciado, desligado ou excluído.
env_file:
- .env
command: python manage.py runserver 0.0.0.0:8000
depends_on:
- db
env_file: Carrega as variáveis de ambiente do arquivo .env.
command: Sobrescreve o comando final do Dockerfile. Aqui, o comando python manage.py runserver 0.0.0.0:8000, reinicia o servidor de desenvolvimento do Django.
depends_on: Informa ao Docker que o Django só deve ser iniciado após a criação do banco de dados.
db:
image: postgres:16-alpine
container_name: finance_db
restart: always
image: Para criar o serviço db em um Container optei por utilizar uma imagem oficial e pronta do PostgreSQL 16 na versão Alpine que é mais leve.
restart: Informa ao Docker que sempre que houver alguma mudança nesse serviço o mesmo deve ser reiniciado.
environment:
- POSTGRES_DB=${POSTGRES_DB}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
environment: Define as configurações obrigatórias do Banco de Dados. O símbolo ${} indica que o Compose está buscando o conteúdo dentro do arquivo .env.
.env
Conforme expliquei algumas vezes anteiormente, o arquivo .env armazena as variáveis de ambiente que, basicamente, são variáveis que armazenam senhas importantes como senhas de banco de dados, senhas secretas para conectar com o Framework, senhas de conexão com APIs externas e etc, ou seja, são informações secretas que não devem ser compartilhadas com qualquer pessoa.
Por esse motivo, não vou mostrar o código exato deste arquivo. Mas, vou compartilhar o arquivo .env-example que é um arquivo exemplo que contém um Template do que o seu arquivo .env deve conter para você conseguir rodar o código localmente.
SECRET_KEY="CHANGE-ME"
# 0 False, 1 True
DEBUG="1"
# Comma Separated values
ALLOWED_HOSTS="127.0.0.1, localhost"
DB_ENGINE="django.db.backends.postgresql"
POSTGRES_DB="CHANGE-ME"
POSTGRES_USER="CHANGE-ME"
POSTGRES_PASSWORD="CHANGE-ME"
POSTGRES_HOST="localhost"
POSTGRES_PORT="5432"
SECRET_KEY: Entre aspas você deve colocar o código secreto de conexão com o Django. Esse código deve ser gerado especificamente para o projeto de atuação. O comando é python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'.
DEBUG: O debug é muito utilizado pelos desenvolvedores quando uma aplicação está em construção ou manutenção pois logs de erro são apresentados na tela. O número 1 indica que o modo Debug está ativo e 0 indica que está desativado.
ALLOWED_HOSTS: Informa ao Docker quais clients podem acessar o Container e consequentemente ter acesso à aplicação.
DB_ENGINE: Informa ao Docker qual engine será responsável por conectar a aplicação ao banco de dados.
POSTGRES
-
DB: Informa o nome do banco de dados dentro do serviço. -
USER: Informa o nome do administrador do banco de dados. -
PASSWORD: Informa a senha de acesso do administrador do banco de dados. -
HOST: Informa o nome do serviço do banco de dados dentro Container. -
PORT: Informa a porta de conexão que será utilizada para conectar o container da aplicação ao container do banco de dados.
.dockerignore
Assim como o .gitignore este arquivo ignora outros arquivos, porém ao invés de não enviar para o repositório, o .dockerignore não envia os arquivos declados para o Container. Isso é uma boa prática no cenário de DevOps, pois ao ignorar os arquivos declarados, o Container fica mais leve.
E também é possível utilizar um template e adicionar coisas específicas do seu projeto. Dê uma olhada no arquivo .dockerignore no repositório do projeto no GitHub.
Scripts
Fiz esse diretório especificamente para armazenar scripts bash dentro do Container.
Atualmente existe apenas um arquivo de script neste diretório e vou explicar abaixo o código.
comands.sh
# Encerra a execução do arquivo quando algum comando falhar
set -e
while ! nc -z $POSTGRES_HOST $POSTGRES_PORT; do
echo "🟡 Aguardando a Inicialização do Banco de Dados Postgres ($POSTGRES_HOST $POSTGRES_PORT)..."
sleep 0.1
done
echo "✅ O Banco de Dados Postgres foi Inicializado com Sucesso ($POSTGRES_HOST $POSTGRES_PORT)"
python manage.py collectstatic
python manage.py migrate
python manage.py runserver
#!/bin/sh: Este comando semelhante a um comentário indica ao sistema operacional que o conteúdo deste arquivo é um script automatizado.
set -e: Um comando simples que, ao encontrar algum erro durante a execução de qualquer comando, encerra a execução do script.
O laço de repetição while é responsável por informar ao usuário se o Banco de Dados está inicializando ou se já foi inicializado.
python manage.py collectstatic: Reúne todos os arquivos estáticos de todos os apps do Django e centraliza em um único diretório (que é especificado no arquivo settings.py).
Mas porque isso? Durante o desenvolvimento da aplicação, o Django serve os arquivos de cada app de maneira individual, porém é uma boa prática fazer com que o servidor web acesse todos os arquivos estáticos em apenas um lugar, pois, dessa forma, o acesso fica mais rápido.
python manage.py migrate: Este comando aplica as alterações de estruttura do banco de dados. Ele lê os arquivos na pasta migrations/ e cria ou altera as tabelas no PostgreSQL.
python manage.py runserver: Inicia o servidor de desenvolvimento do Django.
Desenvolvimento do da Aplicação
O desenvolvimento da aplicação envolve toda a parte de modelagem e criação do banco de dados, desenvolvimento das páginas, estrutura do Django e configuração das rotas de conexão.
O Django trabalha com o conceito de modularidade.
A aplicação é como se fosse um Lego, onde o projeto completo é a base onde as peças se encaixam, e cada App é um bloco de lego específico.
No Django, um App é uma subpasta dentro do projeto com o objetivo de fazer apenas uma coisa.
No caso do PoupAí existem dois Apps: project e accounts que segue o padrão de Separação de Preocupações (Separation of Concerns), ou seja, mesmo que façam parte do mesmo site, eles têm objetivos diferentes dentro da arquitetura do Django.
project
Este App possui o arquivo settings.py e funciona como o um maestro.
O objetivo principal aqui é gerenciar as configurações globais e a integração de todas as outras partes do sistema.
-
settings.pyEste arquivo é como se fosse o sistema nervoso do projeto. É aqui que o Django decide como se conectar ao banco de dados, onde salvar informações e como garantir a segurança.
Vou explicar o código detalhadamente:
BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = BASE_DIR.parent / 'data' / 'web'BASE_DIR: Localiza a pasta raiz onde o arquivomanage.pyestá.DATA_DIR: Aqui estou fazendo uma configuração personalizada. Em vez de salvar os dados dentro de uma pasta do código, estou apontando para uma pasta externa, com o objetivo de facilitar o mapeamento de Volumes do Docker.
SECRET_KEY = os.getenv('SECRET_KEY', 'CHANGE-ME') DEBUG = bool(int(os.getenv('DEBUG', 0))) ALLOWED_HOSTS = [h.strip() for h in os.getenv('ALLOWED_HOSTS', '').split(',') if h.strip()]SECRET_KEY: É a assinatura criptográfica do projeto, a chave secreta de conexão entre a aplicação e o Django. E como vimos anteriormente, o conteúdo dessa variável está armazenado na variávelSECRET_KEYno arquivo.env.DEBUG: Aqui estou desativando as páginas web de mostrarem detalhadamente erros internos, pois, quando o código for para produção, isso evita que o código fique exposto para hackers.ALLOWED_HOSTS: Define quais domínios ou endereços IP tem permissão para acessar a aplicação. Neste caso o código está informando que acessos permitidos estão armazenados no arquivo.envna variávelALLOWED_HOSTS.
INSTALLED_APPS = [ ... 'django.contrib.staticfiles', #Apps do PoupAí 'accounts', ]Aqui o Django carrega os módulos. Os primeiros são os nativos (Admin, Auth, Sessions). O detaque é o App
accounts, que adicionei manualmente para que o Django reconheça os modelos e as telas do PoupAí.
MIDDLEWARE = [ ... 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ... ]O
Middlewareé uma lista de processos que a requisição atravessa antes de chegar na View e antes de sair para o usuário.CsrfViewMiddleware: Protege contra ataques onde um site malicioso tenta enviar dados em nome do usuário.AuthenticationMiddleware: É o que informa o objeto do usuário na requisição (request.user), o que permite que a aplicação saiba quem está logado.
DATABASES = { 'default': { 'ENGINE': os.getenv('DB_ENGINE', 'CHANGE-ME'), 'NAME': os.getenv('POSTGRES_DB', 'CHANGE-ME'), ... } }Esse bloco de código informa ao Container do Django como se conectar com o Container do PostgreSQL. Aqui estou utilizando as variáveis de ambiente que estão no arquivo
.env.
LANGUAGE_CODE = 'pt-br' TIME_ZONE = 'America/Sao_Paulo'Garante que o idioma sejja português e que as informações dos usuários sejam gravados no fuso horário correto.
STATIC_URL = '/static/' STATIC_ROOT = DATA_DIR / 'static' MEDIA_URL = '/media/' MEDIA_ROOT = DATA_DIR / 'media'STATIC_ROOT: É o destino final docollectstatic. É aqui onde o servidor web vai puxar o CSS/JS.MEDIA_ROOT: É onde o Django irá salvar informações do usuário.
LOGIN_REDIRECT_URL = '/accounts/profile' LOGOUT_REDIRECT_URL = '/login'Aqui estou controlando a experiência do usuário:
- Após o login o usuário é redirecionado diretamente para o perfil.
- Após o logout o usuário é redirecionado de volta para a tela de login.
-
asgi.pyO que é ASGI?
ASGI significa Asynchronous Server Gateway Interface (Interface de Gatway de Servidor Assíncrono). Ele é o sucessor do WSGI e foi criado para permitir que o Django lide não apenas com requisições HTTP comuns, mas também com múltiplas requisições simultâneas e conexões modernas e persistentes como WebSockets e HTTP/2.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')Indica onde está o arquivo
settings.py.Ao iniciar o servidor de aplicação, o processo precisa saber onde estão os módulos, apps, bancos de dados e etc.
os.environgarante que o Django saiba que este arquivo de configurações está emproject.settings.
application = get_asgi_application()Chama uma função interna do Django que prepara toda a estrutura para receber requisições assíncronas.
A variável
applicationé o que o servidor de produção (no Docker) vai buscar. Essa variável é o ponto de entrada único.
Comentei há pouco que o ASGI é um sucessor do WSGI, mas qual a diferença entre os dois?
WSGI significa Web Server Gateway Interface (Interface de Gateway de Servidor Web) é uma especificação do Python que define como um servidor web se comunica com frameworks de aplicação (como Django ou Flask).
Diferente do ASGI, o WSGI é síncrono, ou seja, processa uma requisição por vez e bloqueia a conexão até que o resultado da requisição retorne para o usuário e, por essa limitação, pode resultar em dificuldades na escalabilidade e incapacidade de conexão com requisições WebSockets e de longa duração.O ASGI, conforme expliquei há pouco, é assíncrono, ou seja, permite que o desenvolvedor crie aplicações web que lidem com múltiplas requisições simultâneas, sem sobrecarregar a thread principal.
-
urls.pyEste arquivo armazena as rotas da aplicação, mas, afinal, o que são rotas?
No contexto web, as rotas definem os caminhos URL que direcionam as requisições HTTP para as funções específicas usando um framework web.
path('admin/', admin.site.urls),Permite o acesso ao Painel Administrativo do Django.
path('accounts/', include('accounts.urls')),Aqui entra o conceito de Apps do Django. Neste bloco de código, basicamente, estou dizendo que toda URL que começar com
accounts/deve ser entre para o arquivourls.pyque está dentro do appaccounts.
path('login/', auth_views.LoginView.as_view(template_name='accounts/login.html'), name='login'), path('logout/', auth_views.LogoutView.as_view(), name='logout'),-
Login Customizado
Aqui utilizo a classe
LoginViewque é nativa do Django, porém estou especificando que vou utilizar meu próprio template HTML, no casoaccounts/login.html. -
Names
O argumento
namepermite que, o HTML, eu utilize apenas{% url 'login' %}. Se amanhã eu decidir mudar a URL de/login/para/entrar/não vou precisar mudar em nenhum outro ponto da aplicação.
if settings.DEBUG: urlpatterns += static( settings.MEDIA_URL, document_root = settings.MEDIA_ROOT )Durante o desenvolvimento, o próprio Django se encarrega de entregar as informações que os usuários enviaram.
Com a aplicação em produção, o Django não deve servir arquivos estáticos/mídia, pois, por questões de segurança, essa é uma tarefa do Nginx/WhiteNoise. O
ifgarante que essa configuração só funciona na máquina do desenvolvedor. -
Login Customizado
accounts
Este App é funcional. O objetivo dele é isolar tudo o que diz respeito ao Usuário, cuidar do ciclo de vida da conta do usuário e segurança de acesso.
Aqui temos:
- Autenticação: Processos de Login e Logout.
- Registro: Criação de novos usuários.
- Segurança: Gerenciamento de permissões, senhas criptografadas e isolamento de informações.
-
models.pyEste arquivo define como o banco de dados será estruturado e como os dados financeiros se relacionam com os usuários.
class CategoriaEssa classe tem o objetivo de organizar as finanças do usuário.
class Categoria(models.Model): TIPO_CHOICES = [ ('receita', 'Receita (Entrada)'), ('despesa', 'Despesa (Saída)'), ] user = models.ForeignKey(User, on_delete=models.CASCADE) nome = models.CharField(max_length=100) tipo = models.CharField(max_length=10, choices=TIPO_CHOICES, default='despesa') def __str__(self): return f"{self.nome} ({self.tipo})"Essa é uma relação um-para-muitos. Um usuário pode ter várias categorias, mas uma categoria pertence a apenas um usuário.
on_delete=models.CASCADE: Se o usuário deletar a conta, o Django automaticamente limpa o banco de dados e deleta todas as categorias dele evitando dados órfãos.TIPO_CHOICES: Aqui defini um "Enum" manual para que, no banco de dados, fique salvo apenas "receita" ou "despesa" com o objetivo de economizar espaço, mas no painel do usuário aparece o texto mais bonito e estruturado.A função
__str__: Este método do Python define como o objeto aparece quando listado. Ou seja, em vez de aparecerCategoria: object, apareceráAlimentação (despesa).
class Lancamentoclass Lancamento(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) categoria = models.ForeignKey(Categoria, on_delete=models.PROTECT valor = models.DecimalField(max_digits=10, decimal_places=2) data = models.DateField() descricao = models.CharField(max_length=200) FORMA_PAGAMENTO_CHOICES = [ ('debito', 'Débito'), ('credito', 'Crédito'), ('pix', 'Pix'), ('dinheiro', 'Dinheiro'), ('outros', 'Outros'), ] forma_pagamento = models.CharField( max_length=10, choices=FORMA_PAGAMENTO_CHOICES, blank=True, null=True ) def __str__(self): return f"{self.descricao} - R$ {self.valor}"Nesta classe é onde os gastos e ganhos serão registrados.
categoria = models.ForeignKey(Categoria, on_delete=models.PROTECT): A estratégia aqui é diferente. Se o usuário tentar deletar uma categoria, mas já houver lançamentos registrados nela, o Django impedirá a exclusão. Dessa forma, os dados informados anteriormente não serão corrompidos.DecimalField: Para dinheiro, não deve ser utilizadoFloatField, pois ele gera erros de arredondamento. ODecimalFieldgarante a precisão necessária para contabilidade.blank=True, null=True: Torna o campo opcional. Se o usuário não informar o método de pagamento, o sistema aceita o registro normalmente.
class MetaFinanceiraclass MetaFinanceira(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) nome = models.CharField(max_length=100) valor_alvo = models.DecimalField(max_digits=10, decimal_places=2) valor_poupado = models.DecimalField(max_digits=10, decimal_places=2, default=0) data_limite = models.DateField(null=True, blank=True) # Função para calcular a porcentagem da barra de progresso def porcentagem(self): if self.valor_alvo > 0: # Garante que não ultrapasse 100% p = int((self.valor_poupado / self.valor_alvo) * 100) return min(p, 100) return 0 def __str__(self): return f"{self.nome} - {self.user.username}"Essa classe permite ao usuário planejar as metas do usuário.
A função
porcentagem(self)é a lógica do negócio.
Ao invés de calcular o progresso da meta no próprio HTML, o progresso é calculado no objeto.min(p, 100)Garante que se o valor guardado for maior do que a meta estabelecida, a barra de progresso irá travar em 100% e não permite que a barra de progresso quebre o layout. -
migrationsFunciona como um versionamento, as migrations registram as mudanças na estrutura das tabelas.
As migratrions são criadas a partir do arquivo models.py de cada App.
O fluxo é o seguinte:
-
Criação/Mudança
Classes são criadas ou alteradas no arquivo
models.py -
Comando
makemigrationsAo rodar o comando no terminal, o Django verifica o
models.py, (se for uma alteração, ele compara com o que já existia anteriormente) e gera um arquivo no diretóriomigrations/, exemplo0001_initial.py. -
migrate
Esse é o comando que de fato executa o SQL correspondente dentro do Container do PostgreSQL.
python manage.py makemigrationsEsse comando realiza os seguintes passos:
-
Varredura
Percorre todos os apps listados no
INSTALLED_APPS, neste caso é o app accounts. -
Comparação
Ele olha para o estado atual do arquivo
models.pye compara com o último arquivo de migration gerado no diretóriomigrations/. -
Detecção de Mudanças
Se a classe
MetaFinanceirafoi adicionada o Django percebe que essa tabela não existe no histórico. -
Geração do Plano
Escrevbe um novo arquivo Python, por exemplo
0001_initial.py, que contém instruções estruturadas comomigrations.CreateModel(...).
O arquivo gerado não é SQL, é Python e isso permite que o sistema seeja agnótico ao banco de dados. Por exemplo:
Se estiver usando o SQLite em sua máquina e PostgreSQL no Docker, o Django irá ler o mesmo arquivo de migration e irá passar para o banco de dados correspondente.
python manage.py migrateCriar a migration é como escrever uma receita; o comando
migrateé o ato de cozinhar.O Django olha para uma tabela especial no banco de dados chamada
django_migrations.Ele verifica quais arquivos do diretório
migrations/ainda não foram aplicados.Ele executa as instruções, criando as tabelas de
Categoria, Lancamento e MetaFinanceirano container Postgres. -
Criação/Mudança
-
templatesEste é um diretório onde o Django encontra o HTML, CSS e JS, o visual da aplicação.
No Django, existe uma convenção de organização que segui:
accounts/templates/accounts/. Essa repetição é chamada de namespacing e evita que o Django se confunda caso exista arquivos HTML com o mesmo nome, mas que pertençam a apps diferentes.
Estrutura de Herança
base.htmlEm projetos organizados, é comum existir um arquivo base para evitar repetição de código, se trata do princípio DRY (Don't Repeat Yourself).
Este arquivo HTML contém o cabeçaho, o rodapé e importações de CSS.
Para utilizar nas páginas é preciso definir blocos de código:
{% block content %}Os demais templates vão preencher o resto do espaço, assim mantendo o visual do site padronizado.
Template de Login
login.html
No arquivo
urls.pyapontei oLoginViewparaaccounts/login.html.Dentro do
login.htmlutilizei{{ form.as_p }}, dessa forma o Django gera os campos de usuário e senha automaticamente com base em um sistema de autenticação nativo.Uma parte muito importante aqui é a tag
{% csrf_token %}dentro do formulário (CSRF - Cross-Site Request Forgery). Sem ela, o Django bloqueia o login por segurança, impedindo ataques de falsificação de requisição.
Dinamismo com Django Template Language (DTL)
Os templates não são apenas HTMLs estáticos. Utilizie DTL para tomada de decisões:
-
Condicionais
{% if user.is_authenticated %}- permite mostrar o menu de "Sair" apenas para quem está logado. -
Loops
No caso das finanças, utilizei
{% for lancamento in lancamentos %}para desenhar as linhas da tabela de gastos. -
Filtros
No modelo de metas, utilizei filtros para formatação de moeda.
Lembrando que mais a frente vou passar pelos principais códigos das telas.
Integração com Static Files
É nos templates que o WhiteNoise começa a trabalhar.
{% load static %} link rel="stylesheet" href="{% static 'css/style.css' %}"A tag
{ % static% }traduz o caminho do arquivo para a URL final que o navegador irá buscar, garantindo que o CSS apareça corretamente mesmo depois do deploy no Docker.
Vou passar pelos principais códigos dos arquivos de template.
Formulário de Categoria
Essa é a tela onde o usuário cria e edita as categorias de lançamento.
Aqui reutilizei o template de varáveis de contexto:
{{ titulo }}.A view envia o texto "Nova Categoria" ou "Editar Categoria" e o template se adapta.
O objetivo aqui é economizar código e facilitar a manutenção.
Também utilizei a renderização granular de erros:
form.nome.errors.
Em vez utilizar apenas o{{ form.as_p }}, os campos são acessados um por um:{{ form.nome }},{{ form.tipo }}.
Isso permite um controle total sobre o layout.
Formulário de Lançamento
Este formulário é um pouco mais complexo quando comparado ao de Formulário de Categorias, pois aqui estou introduzindo iteratividade dinâmica no lado do cliente.
Este arquivo de template é um exemplo de como o Django integra o HTML, DTL, JavaScript e CSS em uma única unidade funcional.
O Ciclo de Vida do Formulário
-
Token de Segurança
{% csrf_token %}Ao renderizar essa tag, o Django salva a operação com um valor alfanumérico aleatório e então compara o token enviado pelo formulário com o token de sessão salvo pelo usuário no servidor. Se os token não coincidirem, a requisição é rejeitada, assim evitando que scripts externos enviem informações falsas para o banco de dados.
-
Injeção de Widgets
Utilizo variáveis como
{{ form.descricao }}.
Com isso, o Django consulta o arquivoforms.pye gera o HTML correspondente.
A Lógica Reativa
Esta é a parte mais dinâmica do código.
-
Gatilho
O navegador espera o HTML ser carregado antes de rodar o script, evitando erros como "elemento não encontrado".
-
Mapeamento
O script localiza o
selectde categorias edivdo pagamento através de seletores DOM, comoquerySelectoregetElementById. -
Função
verificarPagamentoEssa função checa
categoriaSelect.value. No Django, se o usuário não selecionou nada, o valor é uma string vazia que é avaliada como False. -
Evento
changeAdiciona um listener e toda a vez que o usuário troca a categoria, a função roda novamente, escondendo ou mostrando o campo em tempo real.
Se houver um ID de categoria, ela altera o CSS do container para
display: block, o que deixa o cotainer invisível. -
Execução imediata
A chamada final
verificarPagamento();é essencial para telas de edição. Se um gasto passar por uma edição, a categoria já virá preenchida do banco.
Arquitetura Visual
-
Grid Responsivo
A classe
rowcom.col-md-6cria um comportamento condicional:-
Desktop:
O espaço é dividido em 12 colunas.
col-md-6ocupa metade colocando o valor e data lado a lado. -
Mobile:
como o breakpoint é
md(medium), em telas pequenas o Bootstrap força cada coluna a ocupar 100% de largura, empilhando os campos verticalmente./p>
-
Desktop:
-
Estilização de Widgets com Django
O bloco
stylecontém uma técnica de overriding:- O Django costuma renderizar inputs com classes padrão do navegador.
-
O uso do
!importantgarante que o tema Dark seja absoluto e ignore outros estilos que o navegador tentar aplicar.
-
Resumo do Fluxo de Dados
-
Entrada
O usuário preenche os dados
-
Envio
O usuário clica em "Salvar". O formulário agrupa os dados e o
CSRF Token. -
Processamento
Os dados são enviados via
POSTpara a View do Django. -
Validação
A View valida se o valor é positivo, se a data é válida e se a categoria pertence ao usuário correto.
-
Persistência
Se tudo estiver correto, o Django salva no PostgreSQL e redireciona o usuário.
-
Entrada
Formulário de Metas
A princípio, o formulário de metas pode parecer mais simples que o de lançamentos, porém ele lida com conceitos de UX e integridade de dados.
Manipulação de Valores Monetários
Os campos
valor_alvoevalor_poupadosão os mais críticos tecnicamente.-
Backend
No arquivo
models.pydefini estes campos comoDecimalField. -
Frontend
Quando o Django renderiza
{{ form.valor_alvo }}, ele gera uminput type="number" step="0.01". O atributostep="0.01"é o que permite que o navegador aceite centavos garantindo que o usuário possa digitar sem erros de validação do lado do cliente.
O Desafio da Data Limite
O campo
{{ form.data_limite }}lida com o formato de tempo.-
Funcionamento
Para o tipo de formulário que foi utilizado no PoupAí, o Django renderiza um calendário nativo do navegador (
type="date"). -
Interação com o Banco de Dados
Ao salvar, o Django converte a string da data para um objeto do tipo
datetime.datedo Python, validando se é uma data real antes de enviar ao PostgreSQL.
Fluxo de Dados
POST: Ao salvar, os dados são enviado através do método POST.
Vinculação de Usuário: Na View, associei este formulário ao usuário logado (
request.user). Dessa forma, não é possível criar metas para outro usuário.Cálculo Automático: Uma vez salvo, a função
porcentagem()no arquivomodels.pyjá consegue ler ovalor_poupadoevalor_alvopara atualizar as barras de progresso no Dashboard automaticamente.
Categorias
A pagina de categorias é uma página híbrida. Diferente das páginas de formulário, a página de categoria combina o CRUD em uma única tela, ou seja, o usuário visualiza a lista e pode criar uma nova categoria ao mesmo tempo.
Arquitetura de Tela Dupla
Aqui utilizei o sistema de grid do Bootstrap (
col-md-4ecol-md-8).-
Lado Esquerdo - 4 colunas
Formulário rápido de criação
-
Lado Direito - 8 colunas
Tabela de visualização
O objetivo aqui é reduzir a quantidade de ações do usuário para completar uma tarefa.
Tipo de Categoria
No código utilizo
{{ cat.get_tipo_display }}.-
O que é?
No arquivo
models.pydefinichoicesno campo tipo com as opções: receita e despesa. -
Funcionamento
Se eu optasse por utilizar
{{ cat.tipo }}, o Django iria exibir o valor do banco de dados, como receita por exemplo. Ao adicionar o sufixo_display, o Django busca o rótulo que foi definido pelo usuário.
Lógica Condicional de Badges - Feedback Visual
span class="badge {% if cat.tipo == 'receita' %}bg-success{% else %}bg-danger{% endif %}"No código acima estou utilizando lógica de programação dentro de um arquivo HTML.
Aqui o Django avalia o tipo da categoria em tempo real. Se for uma entrada, aplica a classe
bg-success(verde); se for saídabg-danger(vermelho). Isso permite que o usuário bata o olho e entenda a categoria sem precisar ler.
Bloco Empty
{% empty %}O loop
{% for cat in categorias %}tenta percorrer a lista. Se a lista estiver vazia, em vez a tabela ficar branca ou quebrada, o Django executa automaticamente o código acima, assim exibindo a mensagem "Nenhuma categoria cadastrada ainda".
URs Dinâmicas com Parâmetros
{% url 'editar_categoria' cat.pk %}O Django não gera apenas o link, ele injeta a Primary Key (ID) de cada categoria na URL.
Ao clicar no ícone de lápis de qualquer categoria, o Django gera
/categorias/editar/{ID}. Isso permite que a View saiba exatamente qual objeto do banco de dados deve ser carregado para a edição.
Lançamentos
Basicamente é a central de operações principal do usuário. Assim como na tela de Categorias, mantive o padrão de CRUD em página única, porém essa tela exigiu um pouco mais de complexidade, isso porque aqui o sistema lida com relacionamentos entre tabelas e formatação de dados.
Formatação de Dados - Django Template Filter
{{ lanc.data|date:"d/m/Y" }}Observe o uso do pipe "|".
-
O banco de dados entrega a data no formato
YYYY-MM-DD -
O filtro
datedo Django formata esse dado diretamente no HTML para o padrão brasileiro.
Dados Relacionais - Deep Traversal
td class="{% if lanc.categoria.tipo == 'receita' %}text-success{% else %}text-danger{% endif %}"Aqui podemos ver o ORM do Django refletindo no Template:
-
O objeto
lancnão tem o campotipode forma direta, porém através da chave estrangeira, o código encontra a tabelacategoriae descobre se este reistro se trata de uma entrada ou saída. - Assim como na tela de Categoria, aqui também ocorre o feedback visual através das cores verde e vermelho.
Símbolos Dinâmicos
{% if lanc.categoria.tipo == 'receita' %}+{% else %}-{% endif %} R$ {{ lanc.valor }}Além do feedback visual através da cor, o código injeta matematicamente o símbolo de positivo ou negativo. Isso previne erros de interpretação já que um gasto aparece claramente com o símbolo de negativo.
namespacing de Chaves Estrangeiras no Badge
span class="badge bg-secondary">{{ lanc.categoria.nome }}Aqui estou renderizando o nome da categoria associada. No arquivo
Metasmodels.py, defini que cada lançamento obrigatoriamente precisa de uma categoria. Dessa forma, o Django garante que, ao lista os lançamentos, o nome da categoria correspondente seja buscado viaJOINno SQL.
Este arquivo é um Dashboad Reativo. O código não apenas lista os dados em uma simples tabela, mas utiliza elementos visuais, como Progress Bars e Cards, com o objetivo de entrar um feedback psicológico positivo ao usuário sobre o progresso de suas economias.
Métodos do Model
{{ meta.porcentagem }}O código acima é utilizado em três lugares diferentes: no texto do badge, na largura da barra de progresso e no atributo de acessibilidade.
-
O template não fica responsável por realizar cálculos matemáticos. Essa responsabilidade fica na função
porcentagem()presente no arquivomodels.py. Isso garante que a lógica de travar a barra de progresso em 100% ou dividir por zero seja tratada diretamente com o Python e o HTML fique responsável apenas por apresentar o resultado final.
Barras de Progresso Dinâmicas
div class="progress-bar" style="width: {{ meta.porcentagem }}%; ..."Aqui estou injetando uma variável do Django diretamente em uma propriedade CSS. Conforme o usuário poupa mais dinheiro, o Django renderiza um número maior e a barra cresce visualmente preenchendo a barra de progresso.
Cars Responsivos e Grid Interno
Nas telas anteriores utilizei tabelas, porém, aqui, optei por utilizar um sistema de Cards:
-
col-md-6 mb-3Em telas maiores, as metas aparecem em duas colunas. Em telas menores, as metas ocupam a largura total.
- As metas são objetivos individuais com várias informações. O card isola essas informações, facilitando a leitura.
Lógica de Exibição de Prazo
{% if meta.data_limite %} Prazo: {{ meta.data_limite|date:"d/m/Y" }} {% endif %}-
O usuário pode optar por não preencher o campo
data_limite, por esse motivo utilizei uma condicional. Se o usuário não definir uma data para a meta, o ícone de calendário e o texto "Prazo" simplesmente não aparecem. Isso evita que o card exiba um espaço vazio ou a palavra "None".
A seção de templates chegou ao fim. Obviamente não apresentei o código HTML inteiro pois quis me extender muito, mas você pode ler o código na íntegra acessando o repositório do projeto no GitHub.
-
Condicionais
-
apps.py
Este arquivo é informa ao Django quais Apps fazem parte do projeto.
Aqui centralizo a configuração de metadados do módulo de contas, garantindo escalabilidade a longo prazo, preparando a arquitetura para suportar grandes volumes de registros financeiros sem gargalos de indexação.
class AccountsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'accounts'Classe de Configuração - AppConfig
Quando adicionei o app "accounts" na lista em
INSTALLED_APPSno arquivosettings.py, o Django procura por este arquivo para entender como rodar este apps-
default_auto_fieldDefine o tipo de dado que será usado para as Primary Keys (ID) das tabelas de forma automática.
-
BigAutoFieldIndica ao Django que ele deve utilizar inteiros de 64 bits. Isso é importante para a escalabilidade. Com ele, o banco de dados pode ter até $9,223,372,036,854,775,807$ registros por tabela, garantindo que os IDs "nunca" esgotem.
-
-
name = 'accounts'Define o caminho completo do Python para o app. É o identificador único que o Django deverá utilizar para vincular modelos, templates e tags ao app correto.
-
-
forms.pyNo Django, o
ModelFormaé uma ferramenta muito poderosa, pois ela faz a ponte automática entre o banco de dados e o HTML.O destaque aqui está na customização do método
__init__, onde ocorre uma filtragem dinâmica de QuerySets garantindo o isolamento de dados entre usuários.from django import forms from .models import Categoria, Lancamento from .models import MetaFinanceira class CategoriaForm(forms.ModelForm): class Meta: model = Categoria fields = ['nome', 'tipo'] class LancamentoForm(forms.ModelForm): class Meta: model = Lancamento fields = ['descricao', 'valor', 'data', 'categoria', 'forma_pagamento'] widgets = { 'data': forms.DateInput(attrs={'type': 'date'}), 'valor': forms.NumberInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'step': '0.01'}), 'descricao': forms.TextInput(attrs={'class': 'form-control bg-dark text-white border-secondary', 'placeholder': 'Ex: Supermercado'}), 'categoria': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), 'forma_pagamento': forms.Select(attrs={'class': 'form-select bg-dark text-white border-secondary'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super(LancamentoForm, self).__init__(*args, **kwargs) if user: self.fields['categoria'].queryset = Categoria.objects.filter(user=user) self.fields['forma_pagamento'].label = "Forma de Pagemento (Apenas para Despesas)" class MetaForm(forms.ModelForm): class Meta: model = MetaFinanceira fields = ['nome', 'valor_alvo', 'valor_poupado', 'data_limite'] widgets = { 'data_limite': forms.DateInput(attrs={'type': 'date'}) }Uso de Widgets e Atributos HTML
No
LancamentoForme noMetaForm, faço com que o Django não decida sozinho como renderizar os campos. Para isso, é utilizado o dicionáriowidgets.-
'type': 'date'Ao definir um campo
dataedata_limite, o código forma o navegador a abrir o calendário nativo. Isso evita que o usuário digite datas em formatos incorretos. -
'step': '0.01'Permite que o campo numérico aceite casas decimais. Sem isso, o navegador poderia travar o campo apenas para números inteiros.
Injeção de Classes CSS do Lado do Servidor
attrs={'class': 'form-control bg-dark text-white border-secondary'}Em vez de caçar cada input no HTML para estilizar, optei por injetar as classes do Bootstrap diretamente no Python. Então quando o Django renderizar
{{ form.descricao }}, o HTML já nasce com o design correto para o Dark Mode da interface.
Filtro Dinâmico de Segurança
Parte mais complexa e importante deste arquivo.
def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super(LancamentoForm, self).__init__(*args, **kwargs) if user: self.fields['categoria'].queryset = Categoria.objects.filter(user=user)- Sem este bloco de código, quando o usuário fosse realizar o lançamento de um gasto, ele veria no campo "Categoria" todas as categorias de todos os usuários cadastrados no PoupAí.
-
O código sobrescreve o método de inicialização do formulário para capturar o usuário logado:
kwargs.pop('user'). -
O
querysetdo campo categoria é filtrado em tempo real. O usuário só vê as categorias que ele mesmo criou.
-
-
views.pyEste arquivo é muito extenso e, por esse motivo, ele não estará completo aqui. Mas você pode dar uma olhada na íntegra acessando o repositório do projeto.
É neste arquivo onde a lófica de negócio reside.
Aqui desenvolvi uma camada e visualização de dados compleza utilizando a API de agregação do Django e funções de banco de dados (Database Functions) como
TruncMontheExtractDay. A lógica do Dashboard é dinâmica , alternando entre granulosidade diária e mensal, grantindo performance ao processar somatórios diretamente no nível do banco de dados e assegurando o isolamento total de dados entre usuários (Milti-tenancy).Inteligência de Sinal Financeiro
A View toma decisões automáticas sobre o sinal do dinheiro.
valor_abs = abs(form.cleaned_data.get('valor')) if categoria_obj.tipo == 'despesa': lancamento.valor = -valor_abs else: lancamento.valor = valor_abs lancamento.forma_pagamento = None-
Aqui utilizo
abs()para garantir que o número seja positivo e então, baseado no tipo da categoria, o código força o sinal de negativo. -
Se for uma receita, o código anula
forma_pagamento. Isso evita inconsistências no banc de dados.
Agregações e Filtros
A função
dashboardé a mais densa do projeto. Aqui utilizo o Django Aggregation API que é mais eficiente do que somar valores usando um loop Python.-
Agregações
entradas_valor = lancamentos_mes.filter(categoria__tipo='receita').aggregate(Sum('valor'))['valor__sum'] or 0Esse bloco de código executa o comando
SUMdiretamente no SQL do banco de dados. -
O uso de
or 0é essencial para evitar erros deNoneTypequando o usuário ainda não cadastrou nenhum lançamento.
Lógica Multivisão
Aqui criei um sistema que decide o que exibir no gráfico baseado na URL
request.GET.get('mes'):-
Visão Mensal (Dia a Dia)
Utilizei
calendar.monthrangepara saber quantos dias o mês tem eExtractDaypara agrupar os gastos. -
Visão Anual
UUtilizo
TruncMonthpara agrupar tudo por mês. -
Dicionários de Mapeamento
mapa_e = {d['dia']: float(d['total_e'] or 0) for d in dados_dias}Aqui utilizo Dict Comprehension para transformar o resultado do banco em um mapa rápido para preencher os dias que não tiveram lançamentos com o valor 0, evitandoque o gráficode linhas quebre.
Segurança
Em todas as funções de edição e exclusão utilizei:
categoria = get_object_or_404(Categoria, pk=pk, user=request.user)-
Se o código tivesse apenas
pk=pk, um usuário mal-intencionado poderia mudar o ID da UR paraeditar/{ID}e tentar editar a categoria de outro usuário. -
Ao incluir
user=request.userno filtro, o Django garante que, se a categoria não pertencer ao usuário logado, ele receberá um erro 404.
Gráfico de Donut - Formas de Pagamento
dados_pagamento = lancamentos_mes.filter(...).values('forma_pagamento').annotate(total=Sum('valor'))Aqui disfarcei o uso de um
GROUP BY. O Django agrupa todas as despesas por "Cartão", "Dinheiro", etc., e soma os totais. Isso gera dados corretos para alimentar um gráfico no frontend. -
Aqui utilizo
-
dashboard.htmlEssa tela pega todo o conteúdo processado e transforma em visualização de dados.
Integração Python-JavaScript
Os dados são passados do servidor para o cliente.
labels: {{ labels_grafico|safe }}, data: {{ valores_entradas|safe }},O Filtro
|safe: Por segurança, o Django escaparia as aspas dos nomes dos meses (exemplo:['Jan']em ) -
urls.py