Admin Panel¶
Painel administrativo estilo Django com tipagem genérica para autocomplete no PyCharm.
Fundamentos¶
Habilitação¶
Admin é habilitado por padrão. Acesse em /admin/.
# src/settings.py
class AppSettings(Settings):
admin_enabled: bool = True # Default
admin_url_prefix: str = "/admin"
admin_site_title: str = "Minha Empresa"
admin_site_header: str = "Painel Administrativo"
admin_primary_color: str = "#3B82F6" # blue-500
Registrar Models¶
# src/apps/posts/admin.py
from strider.admin import admin, ModelAdmin
from .models import Post
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]): # Tipagem genérica para autocomplete
display_name = "Post"
display_name_plural = "Posts"
icon = "file-text" # Lucide icon
list_display = ("id", "title", "published", "created_at")
list_filter = ("published",)
search_fields = ("title", "content")
ordering = ("-created_at",)
Tipagem Genérica¶
Use ModelAdmin[Model] para autocomplete no PyCharm:
from strider.admin import ModelAdmin, WidgetConfig, IconType
@admin.register(Domain)
class DomainAdmin(ModelAdmin[Domain]):
# PyCharm sugere ícones válidos
icon: IconType = "globe"
# Campos do model Domain
list_display = ("id", "domain", "is_verified")
# TypedDict para widgets
widgets: dict[str, WidgetConfig] = {
"hostname_status": {
"widget": "choices",
"label": "Status do Hostname",
},
}
Opções Completas¶
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]):
# ══════════════════════════════════════════════════════════════════
# Metadados
# ══════════════════════════════════════════════════════════════════
display_name = "Post"
display_name_plural = "Posts"
icon = "file-text" # Lucide icon
# ══════════════════════════════════════════════════════════════════
# List View
# ══════════════════════════════════════════════════════════════════
list_display = ("id", "title", "author_id", "published", "created_at")
list_display_links = ("id", "title") # Campos clicáveis
list_filter = ("published", "author_id")
search_fields = ("title", "content")
ordering = ("-created_at",)
list_per_page = 25
list_max_show_all = 200
# ══════════════════════════════════════════════════════════════════
# Detail/Edit View
# ══════════════════════════════════════════════════════════════════
fields = ("title", "content", "published") # Campos no form
exclude = ("deleted_at",) # Campos a excluir
readonly_fields = ("id", "created_at", "updated_at")
# ══════════════════════════════════════════════════════════════════
# Fieldsets (agrupamento)
# ══════════════════════════════════════════════════════════════════
fieldsets = [
("Conteúdo", {"fields": ("title", "content")}),
("Status", {"fields": ("published",)}),
("Metadados", {"fields": ("created_at", "updated_at")}),
]
# ══════════════════════════════════════════════════════════════════
# Widgets e Help Texts
# ══════════════════════════════════════════════════════════════════
widgets = {
"content": {"widget": "text", "label": "Conteúdo do Post"},
}
help_texts = {
"title": "Título que aparece na listagem",
"content": "Conteúdo em markdown",
}
# ══════════════════════════════════════════════════════════════════
# Permissões
# ══════════════════════════════════════════════════════════════════
permissions = ("view", "add", "change", "delete")
exclude_actions = () # Actions a desabilitar
Interface e widgets¶
Widgets Disponíveis¶
| Widget | Descrição |
|---|---|
default |
Input padrão baseado no tipo |
string |
Input de texto |
text |
Textarea |
integer |
Input numérico |
float |
Input decimal |
boolean |
Checkbox |
datetime |
Date/time picker |
date |
Date picker |
time |
Time picker |
json |
Editor JSON |
uuid |
Input UUID |
password |
Input de senha |
password_hash |
Campo de hash (oculto) |
virtual_password |
Senha virtual (usa set_password) |
secret |
Campo secreto (nunca exibe) |
email |
Input de email |
url |
Input de URL |
slug |
Input de slug (auto-gera) |
color |
Color picker |
ip |
Input de IP |
choices |
Select dropdown (enum) |
fk |
Foreign key (autocomplete) |
m2m_select |
Many-to-many select |
file_upload |
Upload de arquivo (storage local ou GCS); drag-and-drop, preview |
Campos de arquivo (Storage) e exclusão¶
Quando o model tem campos que armazenam path ou URL de arquivo (ex.: image, avatar, file_path, attachment_url), o admin detecta automaticamente e exibe o widget file_upload:
- Detecção automática: nomes como
image,avatar,photo,file_path,attachment,*_url(para imagem/arquivo) viramfile_upload. - Widget no formulário: área de drag-and-drop, preview (imagem ou ícone), link para o arquivo atual e botão para remover.
- Upload: o arquivo é enviado para o backend configurado em Settings (
storage_backend: local ou GCS). O valor retornado (path ou URL) é salvo no campo.
Configure o storage em src/settings.py (veja Settings — Storage). Documentação completa da API e fluxo: Storage (37-storage.md).
Exclusão e arquivos físicos¶
Ao deletar um registro (botão "Delete" na tela de edição):
- Abre um modal de confirmação.
- Se o model tiver campos file_upload com valor, aparece a opção: "Also delete X file(s) from storage".
- Se marcar, o backend remove o(s) arquivo(s) do disco ou do bucket GCS ao deletar o registro.
Assim você evita arquivo órfão no storage. Em bulk delete (listagem), o body da requisição pode incluir "delete_physical_files": true para o mesmo efeito.
Forçar widget file_upload em um campo¶
Se o nome da coluna não for detectado automaticamente, use widgets:
@admin.register(Profile)
class ProfileAdmin(ModelAdmin[Profile]):
widgets = {
"cover_image": {"widget": "file_upload", "label": "Cover image"},
}
Ícones (Lucide)¶
# Ícones mais comuns
icon = "file" # Arquivo
icon = "folder" # Pasta
icon = "user" # Usuário
icon = "users" # Usuários
icon = "settings" # Configurações
icon = "database" # Banco de dados
icon = "globe" # Domínio/Web
icon = "mail" # Email
icon = "lock" # Segurança
icon = "key" # Chave
icon = "shield" # Proteção
icon = "activity" # Atividade
icon = "calendar" # Calendário
icon = "clock" # Tempo
icon = "tag" # Tag
icon = "bookmark" # Favorito
icon = "star" # Estrela
icon = "heart" # Coração
icon = "bell" # Notificação
icon = "search" # Busca
icon = "filter" # Filtro
icon = "edit" # Editar
icon = "trash" # Lixeira
icon = "plus" # Adicionar
icon = "refresh-cw" # Atualizar
icon = "download" # Download
icon = "upload" # Upload
icon = "link" # Link
icon = "eye" # Visualizar
icon = "copy" # Copiar
icon = "archive" # Arquivar
icon = "box" # Caixa
icon = "package" # Pacote
icon = "layers" # Camadas
icon = "grid" # Grade
icon = "list" # Lista
icon = "table" # Tabela
icon = "pie-chart" # Gráfico pizza
icon = "bar-chart" # Gráfico barras
icon = "trending-up" # Tendência alta
icon = "dollar-sign" # Dinheiro
icon = "credit-card" # Cartão
icon = "shopping-cart" # Carrinho
icon = "truck" # Entrega
icon = "map-pin" # Localização
icon = "zap" # Raio/Rápido
icon = "cpu" # Processador
icon = "server" # Servidor
icon = "hard-drive" # Disco
icon = "wifi" # WiFi
icon = "monitor" # Monitor
icon = "smartphone" # Celular
icon = "code" # Código
icon = "terminal" # Terminal
icon = "git-branch" # Git
Lógica do ModelAdmin¶
Actions Customizadas¶
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]):
list_display = ("id", "title", "published")
actions = ["delete_selected", "publish", "unpublish"]
@admin.action(description="Publicar selecionados")
async def publish(self, db, queryset):
for post in queryset:
post.published = True
await post.save(db)
@admin.action(description="Despublicar selecionados")
async def unpublish(self, db, queryset):
for post in queryset:
post.published = False
await post.save(db)
Hooks de Ciclo de Vida¶
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]):
async def before_save(self, db, obj, is_new: bool) -> None:
"""Executado antes de salvar (create ou update)."""
if is_new:
obj.slug = slugify(obj.title)
async def after_save(self, db, obj, is_new: bool) -> None:
"""Executado após salvar."""
if is_new:
await send_notification(f"Novo post: {obj.title}")
async def before_delete(self, db, obj) -> None:
"""Executado antes de deletar."""
await archive_post(obj)
async def after_delete(self, db, obj) -> None:
"""Executado após deletar."""
await clear_cache(f"post:{obj.id}")
Queryset Customizado¶
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]):
def get_queryset(self, db):
"""Filtra queryset base."""
# Exemplo: só mostrar posts do workspace do usuário
return Post.objects.using(db).filter(workspace_id=self.request.user.workspace_id)
Password Virtual¶
Para models com set_password(), o admin detecta automaticamente e cria um campo virtual de senha:
@admin.register(User)
class UserAdmin(ModelAdmin[User]):
# password_field é auto-detectado se model tem set_password()
# e uma coluna password_hash/hashed_password
# Para customizar:
password_field = "password_hash" # Coluna do hash
widgets = {
"password": {
"help_text": "Deixe vazio para manter a senha atual",
"required_on_create": True,
"required_on_edit": False,
},
}
Many-to-Many¶
Relacionamentos M2M são detectados automaticamente:
@admin.register(User)
class UserAdmin(ModelAdmin[User]):
# groups e user_permissions são detectados automaticamente
# se o model tem esses relacionamentos M2M
fieldsets = [
("Conta", {"fields": ("email", "password")}),
("Permissões", {"fields": ("is_active", "is_staff", "groups", "user_permissions")}),
]
Permissões¶
Acesso ao admin requer is_staff=True.
@admin.register(Post)
class PostAdmin(ModelAdmin[Post]):
# Permissões disponíveis
permissions = ("view", "add", "change", "delete")
# Desabilitar ações específicas
exclude_actions = ("delete_selected",)
Painel, ops e acesso¶
Configurações do Admin¶
# src/settings.py
class AppSettings(Settings):
# Habilitar/desabilitar
admin_enabled: bool = True
# URL
admin_url_prefix: str = "/admin" # ou "/backoffice", "/ops-secret"
# Branding
admin_site_title: str = "Minha Empresa" # Título na aba
admin_site_header: str = "Painel Admin" # Header no sidebar
admin_logo_url: str = "/static/logo.png" # Logo custom
# Tema
admin_theme: str = "default" # ou "dark"
admin_primary_color: str = "#3B82F6" # Cor primária (hex)
admin_custom_css: str = "./static/admin-custom.css" # CSS extra
# Segurança
admin_cookie_secure: bool = None # None = auto-detect HTTPS
Operations Center¶
O admin inclui um centro de operações para monitorar:
class AppSettings(Settings):
# Habilitar Operations Center
ops_enabled: bool = True
# Tasks
ops_task_persist: bool = True # Persistir execuções
ops_task_retention_days: int = 30 # Dias para reter
# Workers
ops_worker_heartbeat_interval: int = 30 # Heartbeat (segundos)
ops_worker_offline_ttl: int = 24 # Horas para manter offline
# Logs
ops_log_buffer_size: int = 5000 # Tamanho do buffer
ops_log_stream_enabled: bool = True # Streaming SSE
# Infraestrutura
ops_infrastructure_poll_interval: int = 60 # Métricas (segundos)
Login¶
Admin usa autenticação por sessão (separada do JWT da API).
# Criar superusuário
core createsuperuser
Login padrão: /admin/login
Auto-Discovery¶
Módulos admin são descobertos automaticamente de arquivos admin.py:
src/apps/
├── posts/
│ ├── models.py
│ └── admin.py # Auto-descoberto
└── users/
├── models.py
└── admin.py # Auto-descoberto
Tipos para Autocomplete¶
from strider.admin import (
ModelAdmin,
WidgetConfig, # TypedDict para widgets
FieldsetConfig, # Tipo para fieldsets
IconType, # Literal com ícones válidos
PermissionType, # Literal para permissões
WidgetType, # Literal com widgets válidos
)
Referência¶
Próximos Passos¶
- CLI — Comandos disponíveis
- Permissions — Controle de acesso
- Settings — Todas as configurações