Documento de referência do projeto. Mantido manualmente a cada nova feature. Última atualização: Julho/2026
- Visão geral do projeto
- Stack e dependências
- Estrutura de pastas
- Arquitetura MTV — como o projeto se organiza
- Models
- URLs
- Views
- Templates
- CSS — classes do tema
- Admin
- Features implementadas
- Features pendentes / próximos passos
- Como rodar o projeto
- Decisões de arquitetura registradas
- Erros conhecidos e soluções
DevBlog Django é um blog de artigos técnicos sobre tecnologia, dados e Machine Learning. Os artigos são criados e gerenciados pelo Django Admin com editor de texto rico (TinyMCE). O site é público e exibe os artigos publicados em uma listagem com cards visuais, busca por texto, e filtros por eixo de tecnologia.
| Item | Valor |
|---|---|
| Nome do projeto Django | cursoDjango |
| Nome do app principal | motorartigos |
| Banco de dados | MariaDB 10.5+ (via XAMPP) |
| Nome do banco | djangoartigos |
Django==4.2.x
django-tinymce
beautifulsoup4
mysqlclient
O projeto usa Django 4.2.x (série LTS) por compatibilidade com MariaDB 10.5. Django 5.x exige MariaDB 10.5+ e causava erros de versão no ambiente de desenvolvimento.
Instalar todas as dependências:
pip install -r requirements.txtcursoDjango/ ← raiz do projeto
├── manage.py ← ponto de entrada de todos os comandos Django
├── requirements.txt ← dependências do projeto
├── .env.example ← modelo de variáveis de ambiente (sem valores reais)
├── .gitignore ← arquivos ignorados pelo Git
│
├── cursoDjango/ ← configurações do projeto Django
│ ├── __init__.py
│ ├── settings.py ← configurações globais (banco, apps, static, etc.)
│ └── urls.py ← URLconf raiz (inclui motorartigos.urls)
│
└── motorartigos/ ← app principal do blog
├── __init__.py
├── admin.py ← configuração da interface administrativa
├── models.py ← Models: Autor, EixoTecnologia, Artigo
├── views.py ← Views: index, busca, artigo
├── urls.py ← Rotas do app
│
├── templates/
│ └── motorartigos/
│ ├── base.html ← layout base (head, nav, footer)
│ ├── index.html ← página inicial (Mais recentes + Todos os artigos)
│ ├── busca.html ← página de resultados de busca
│ ├── artigo.html ← detalhe de artigo (ainda hardcoded)
│ └── partials/ ← fragmentos reutilizáveis de interface
│ ├── _header.html ← navbar + texto intro + busca + filtros de eixo
│ ├── _footer.html ← rodapé
│ ├── _busca.html ← formulário de busca (reutilizável)
│ └── _card_artigo.html ← card de artigo (reutilizável)
│
└── static/
└── css/
└── styles.css ← estilos do tema escuro
Django usa o padrão MTV (Model – Template – View), que é uma variação do MVC com nomenclatura própria:
| Camada | Arquivo | Responsabilidade |
|---|---|---|
| Model | models.py |
Define as tabelas do banco e as regras dos dados |
| Template | templates/**/*.html |
Define como os dados são exibidos (HTML) |
| View | views.py |
Recebe a requisição, consulta o Model, passa dados ao Template |
| URLconf | urls.py |
Mapeia URLs para Views |
Fluxo de uma requisição:
Navegador
↓ GET /busca/?q=python
URLconf (urls.py)
↓ chama views.busca()
View (views.py)
↓ lê request.GET.get('q')
↓ filtra Artigo.objects.filter(Q(...))
↓ monta contexto {'artigos': ..., 'q': ..., 'total': ...}
Template (busca.html)
↓ renderiza HTML com os dados do contexto
Navegador
← recebe HTML finalizado
Arquivo: motorartigos/models.py
| Campo | Tipo | Descrição |
|---|---|---|
id |
AutoField (PK) | Gerado automaticamente |
nome |
CharField | Nome completo do autor |
email |
EmailField | E-mail do autor |
biografia |
TextField | Texto livre de apresentação |
| Campo | Tipo | Descrição |
|---|---|---|
id |
AutoField (PK) | Gerado automaticamente |
nome |
CharField | Nome do eixo (ex: "Inteligência Artificial") |
| Campo | Tipo | Descrição |
|---|---|---|
id |
AutoField (PK) | Gerado automaticamente |
titulo |
CharField | Título do artigo |
texto |
TextField | Corpo do artigo em HTML (editado via TinyMCE) |
id_fk_autor |
ForeignKey → Autor | Autor do artigo |
id_fk_eixo |
ForeignKey → EixoTecnologia | Categoria/eixo do artigo |
nivel |
CharField (choices) | 'B' = Básico, 'I' = Intermédio, 'A' = Avançado |
data_publicacao |
DateField | Data de publicação |
publicada |
BooleanField | True = visível no site, False = rascunho |
foto |
ImageField | Imagem de capa do artigo (opcional) |
get_nivel_display()— método automático do Django para campos comchoices. Retorna o texto legível ("Básico") a partir do valor interno ("B").
path('', include('motorartigos.urls')),
path('tinymce/', include('tinymce.urls')),urlpatterns = [
path('', index, name='index'), # página inicial
path('artigo/', artigo, name='artigo'), # detalhe do artigo (hardcoded)
path('busca/', busca, name='busca'), # resultados de busca
]Uso nos templates:
{% url 'index' %},{% url 'busca' %},{% url 'artigo' %}
Arquivo: motorartigos/views.py
URL: /
Template: motorartigos/index.html
Consulta dois QuerySets a partir da mesma base filtrada e ordenada:
artigos_recentes→ 4 artigos publicados mais recentes ([:4]= LIMIT 4)artigos→ todos os artigos publicados, ordenados por data decrescente
artigos_publicados = Artigo.objects.filter(publicada=True).order_by('-data_publicacao')
artigos_recentes = artigos_publicados[:4]
artigos = artigos_publicadosContexto passado ao template:
{'artigos_recentes': artigos_recentes, 'artigos': artigos}URL: /busca/?q=termo
Template: motorartigos/busca.html
Lê o parâmetro q da URL (request.GET.get('q', '')).
Filtra artigos usando Q() com OR em 4 campos simultâneos:
Q(titulo__icontains=q) | # busca no título
Q(id_fk_autor__nome__icontains=q) | # busca pelo nome do autor (atravessa FK)
Q(id_fk_eixo__nome__icontains=q) | # busca pelo nome do eixo (atravessa FK)
Q(texto__icontains=q) # busca no corpo do artigo.distinct() evita duplicatas quando o termo aparece em mais de um campo do mesmo artigo.
Contexto passado ao template:
{'artigos': artigos, 'q': q, 'total': artigos.count()}URL: /artigo/
Template: motorartigos/artigo.html
⚠️ Ainda hardcoded — exibe sempre o mesmo artigo estático. Feature pendente: receberidvia URL (/artigo/<int:id>/) e buscar no banco.
base.html
├── {% include '_header.html' %} ← em todas as páginas
│ └── {% include '_busca.html' %} ← formulário de busca
├── {% block content %}
│ ├── index.html extends base.html
│ ├── busca.html extends base.html
│ └── artigo.html extends base.html
└── {% include '_footer.html' %} ← em todas as páginas
Layout base. Define <html>, <head>, imports de Bootstrap e CSS,
inclui header e footer, e declara {% block content %} para as páginas filhas preencherem.
Incluído em todas as páginas via base.html. Contém:
- Navbar com logo "DevBlog Django" (link para
/) - Texto introdutório do blog
{% include '_busca.html' %}— formulário de busca- Filtros de eixo (ainda hardcoded — feature futura: dinâmico via banco)
Formulário de busca reutilizável. Pode ser incluído em qualquer página:
{% include 'motorartigos/partials/_busca.html' %}method="GET"→ termo vai para a URL como?q=action="{% url 'busca' %}"→ aponta para a view de resultadosname="q"→ nome do parâmetro lido pela viewvalue="{{ q|default:'' }}"→ mantém o campo preenchido na página de resultados
Card de artigo reutilizável. Deve sempre ser incluído com with:
{% include 'motorartigos/partials/_card_artigo.html' with artigo=artigo %}Usado em:
index.html— seção "Mais recentes"index.html— seção "Todos os Artigos"busca.html— grid de resultados
Lógica do thumbnail:
- Se
artigo.fotoexiste → exibe<img>comclass="card-thumb" - Se não existe → exibe
<div>colorido baseado no nível:- Avançado →
thumb-gradient(rosa/roxo/azul) - Intermédio →
thumb-mockup(cinza escuro) - Básico →
thumb-dashboard(verde escuro)
- Avançado →
Lógica do badge de nível:
- Avançado →
bg-danger-custom(vermelho) - Intermédio →
bg-warning-custom text-dark(amarelo) - Básico →
bg-success-custom(verde)
Estende base.html. Duas seções:
"Mais recentes" — itera sobre artigos_recentes (4 itens):
{% for artigo in artigos_recentes %}
{% include '.../_card_artigo.html' with artigo=artigo %}
{% endfor %}Layout: flex-nowrap (linha horizontal — visual de carrossel).
"Todos os Artigos" — itera sobre artigos (todos publicados):
{% for artigo in artigos %}
{% include '.../_card_artigo.html' with artigo=artigo %}
{% endfor %}Layout: row g-4 (grade que quebra em múltiplas linhas).
Estende base.html. Exibe:
- Cabeçalho com contagem:
"X artigos encontrados para 'termo'" - Badge com o termo buscado (classe
.badge-termo) - Estado vazio com sugestões quando nenhum resultado é encontrado
- Grid de cards usando
_card_artigo.html
Estende base.html.
⚠️ Conteúdo ainda completamente hardcoded. Feature pendente.
Arquivo: motorartigos/static/css/styles.css
Bootstrap 5.3.2 via CDN + classes customizadas para o tema escuro.
| Variável visual | Hex | Uso |
|---|---|---|
| Fundo principal | #0b0f19 |
body, bg-dark-custom |
| Fundo cards | #111c30 |
.card-custom |
| Fundo inputs | #111827 |
.search-box, .btn-filter |
| Bordas | #1f2937 |
.search-box, .btn-filter |
| Azul destaque | #3b82f6 |
.btn-filter.active, .btn-busca |
| Texto secundário | #9ca3af |
.text-secondary |
| Classe | Descrição |
|---|---|
.card-custom |
Card com fundo #111c30, border-radius 12px e hover com translateY |
.card-thumb |
Padroniza altura (180px) e object-fit: cover dos thumbnails |
.thumb-gradient |
Thumbnail colorido — gradiente rosa/roxo/azul (Avançado) |
.thumb-mockup |
Thumbnail colorido — gradiente cinza escuro (Intermédio) |
.thumb-dashboard |
Thumbnail colorido — gradiente verde escuro (Básico) |
.btn-filter |
Botão de filtro de eixo (pill arredondado, tema escuro) |
.btn-filter.active |
Filtro selecionado (fundo azul) |
.btn-busca |
Botão submit do formulário de busca |
.btn-nav |
Botões ‹ › do carrossel (circular, sem preenchimento) |
.search-box |
Container do campo de busca (fundo escuro, borda sutil) |
.badge-status |
Badge da navbar ("Mapeamento de Modelos Django Ativo") |
.badge-termo |
Badge que exibe o termo buscado na página de resultados |
.estado-vazio |
Container de mensagem quando busca não retorna resultados |
.avatar |
Círculo com inicial do autor (24×24px, roxo #6366f1) |
.bg-danger-custom |
Badge vermelho para nível Avançado |
.bg-warning-custom |
Badge amarelo para nível Intermédio |
.bg-success-custom |
Badge verde para nível Básico |
.text-primary-custom |
Azul claro #60a5fa para "Ler mais →" |
.rodape |
Estilo do <footer> |
Arquivo: motorartigos/admin.py
list_display = ('id', 'nome', 'email')
search_fields = ('nome', 'email')list_display = ('id', 'nome')
search_fields = ('nome',)list_display = ('id', 'titulo', 'id_fk_autor', 'id_fk_eixo',
'nivel', 'resumo_texto', 'data_publicacao')
search_fields = ('titulo', 'id_fk_autor__nome', 'texto')
list_filter = ('id_fk_eixo', 'nivel', 'data_publicacao')
list_display_links = ('id', 'titulo')
formfield_overrides = {
db_models.TextField: {'widget': TinyMCE()},
}resumo_texto() — método customizado de exibição.
Usa BeautifulSoup para extrair texto puro do HTML do campo texto,
exibe os primeiros 80 caracteres seguidos de ... se o texto for mais longo.
TinyMCE — substitui todos os campos TextField no formulário de edição
por um editor visual WYSIWYG (rich text), gerando HTML salvo no banco.
- App
motorartigoscriado e registrado emINSTALLED_APPS base.htmlcom sistema de herança de templates ({% extends %},{% block %})- Partials separados:
_header.html,_footer.html - Bootstrap 5.3.2 via CDN
- CSS tema escuro (
styles.css)
- Models
Autor,EixoTecnologia,Artigocriados e com migrations aplicadas - Admin configurado com
list_display,search_fields,list_filter - TinyMCE integrado como widget para campos
TextFieldno admin BeautifulSoupusado no admin para preview de texto limpo (sem tags HTML)
- Seção "Mais recentes" — 4 artigos mais recentes do banco (antes hardcoded)
- Seção "Todos os Artigos" — todos os artigos com
publicada=True - Ambas as seções usam o partial
_card_artigo.html
- Partial reutilizável incluído com
{% include ... with artigo=artigo %} - Thumbnail com altura fixa de
180pxvia.card-thumb - Se artigo tem foto → exibe a imagem real com
object-fit: cover - Se não tem foto → exibe div colorido pelo nível (Avançado/Intermédio/Básico)
- Badge de nível com cor dinâmica (
bg-danger-custom,bg-warning-custom,bg-success-custom) - Avatar com inicial do nome do autor (
|slice:":1"|upper)
Formulário (_busca.html):
<form method="GET" action="{% url 'busca' %}"><input name="q">comvalue="{{ q|default:'' }}"para manter o termo após buscatype="search"com botão submit.btn-busca
View (busca):
- Lê
request.GET.get('q', '').strip() - Filtra com
Q()em 4 campos:titulo,autor__nome,eixo__nome,texto .filter(publicada=True).distinct()garante só artigos publicados, sem duplicatas- Passa
q,artigosetotalpara o template
Página de resultados (busca.html):
- Exibe contagem: "X artigos encontrados"
- Badge com o termo buscado (
.badge-termo) - Estado vazio com sugestões quando não encontra nada
- Grid de cards usando o mesmo
_card_artigo.html
| Feature | Descrição | Complexidade |
|---|---|---|
| 🔲 Detalhe do artigo | artigo.html dinâmico recebendo id via URL |
Baixa |
| 🔲 Filtros de eixo dinâmicos | Eixos no _header.html vindo do banco |
Baixa |
| 🔲 Iniciais duplas no avatar | Custom template tag para "AC" de "Ana Costa" | Baixa |
| 🔲 Carrossel funcional | JavaScript nos botões ‹ › da seção "Mais recentes" | Média |
| 🔲 Busca em tempo real | Live search com HTMX ou JavaScript | Média |
| 🔲 Paginação | Dividir "Todos os Artigos" em páginas | Média |
| 🔲 Filtro combinado | Eixo + busca ao mesmo tempo na mesma URL | Média |
| 🔲 Página de autor | Listar artigos de um autor específico | Média |
- Python 3.10+
- XAMPP com MariaDB 10.5+ rodando
- Banco
djangoartigoscriado no MySQL
# 1. Clonar o repositório
git clone https://github.com/SEU_USUARIO/cursoDjango.git
cd cursoDjango
# 2. Criar e ativar o ambiente virtual
python -m venv .venv
# Windows PowerShell:
.venv\Scripts\Activate.ps1
# Linux/macOS:
source .venv/bin/activate
# 3. Instalar dependências
pip install -r requirements.txt
# 4. Configurar variáveis de ambiente
# Copie .env.example para .env e preencha os valores:
# SECRET_KEY=sua-chave-aqui
# DB_NAME=djangoartigos
# DB_USER=root
# DB_PASSWORD=
# 5. Aplicar migrations
python manage.py migrate
# 6. Criar superusuário para o admin
python manage.py createsuperuser
# 7. Rodar o servidor
python manage.py runserverAcesse:
- Site: http://127.0.0.1:8000
- Admin: http://127.0.0.1:8000/admin
O ambiente de desenvolvimento usa XAMPP com MariaDB. A versão disponível na máquina era 10.4.x. Django 5.x exige MariaDB 10.5+. Após atualizar o XAMPP para MariaDB 10.6, o Django 5.x passou a funcionar — mas mantivemos o 4.2.x por ser a versão LTS e mais estável para o curso.
Com GET, o termo buscado vai para a URL (/busca/?q=python).
Isso permite: compartilhar o link dos resultados, usar o botão
"voltar" do navegador, fazer bookmark da busca.
Com POST, a URL não muda e o usuário perde o resultado ao recarregar a página.
Princípio DRY (Don't Repeat Yourself). O card é usado em 3 lugares: "Mais recentes", "Todos os Artigos" e "Busca". Com um partial único, qualquer mudança visual no card é feita em um arquivo e reflete nos três lugares automaticamente.
Dentro de um {% for artigo in artigos %}, a variável artigo
existe no contexto da página mãe. Mas {% include %} cria um
contexto próprio para o partial. Sem with artigo=artigo, o partial
não recebe a variável e renderiza vazio. O with é a forma explícita
e segura de passar dados para partials.
Se um artigo contém o termo em mais de um campo (ex: a palavra "python"
aparece no título E no texto), o filter(Q(...) | Q(...)) pode retornar
o mesmo artigo mais de uma vez. .distinct() garante unicidade.
Causa: beautifulsoup4 não instalado no venv.
Solução: pip install beautifulsoup4
Causa: Django 5.x/4.2.x com MariaDB 10.4.x no XAMPP. Solução: Atualizar o XAMPP para versão com MariaDB 10.5+.
Causa: A pasta data/ do MariaDB foi de uma versão para outra
sem shutdown limpo. O redo log estava corrompido.
Solução: Recriar o banco do zero — fazer backup da pasta
C:\xampp\mysql\data, usar a pasta backup/ limpa do XAMPP,
recriar o banco djangoartigos e rodar python manage.py migrate.
Causa: O banco existia nos metadados mas os arquivos .ibd do
InnoDB estavam ausentes ou corrompidos após reinstalação do MariaDB.
Solução: Dropar e recriar o banco:
DROP DATABASE djangoartigos;
CREATE DATABASE djangoartigos CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Depois: python manage.py migrate
Causa: O <input> estava solto no HTML, sem <form>, sem name,
sem action. O formulário não enviava dados para nenhuma view.
Solução: Envolver em <form method="GET" action="{% url 'busca' %}">,
adicionar name="q" ao input e criar a view + URL de busca.