Compare commits
10 Commits
dev_corrig
...
911ead7835
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
911ead7835 | ||
|
|
91d9bef6c6 | ||
|
|
4742a888b7 | ||
|
|
6a3675b735 | ||
|
|
bb6e5c887b | ||
|
|
63ebf09fb6 | ||
|
|
f87e03640d | ||
|
|
debcbe6663 | ||
|
|
d45fefd72c | ||
|
|
62aaec3fbe |
20
.gitignore
vendored
20
.gitignore
vendored
@@ -260,8 +260,6 @@ poetry.toml
|
||||
pyrightconfig.json
|
||||
|
||||
database.db
|
||||
database.db-shm
|
||||
database.db-wal
|
||||
admin_qr.png
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python,flask
|
||||
@@ -272,21 +270,3 @@ docs/alteracoes_db_connection.md
|
||||
# QR Codes
|
||||
*_qr.png
|
||||
*_qr.txt
|
||||
|
||||
# Redis and Cache
|
||||
*.rdb
|
||||
*.aof
|
||||
dump.rdb
|
||||
appendonly.aof
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
|
||||
# Environment files
|
||||
.env.local
|
||||
.env.production
|
||||
.env.staging
|
||||
|
||||
63
Dockerfile
63
Dockerfile
@@ -1,48 +1,39 @@
|
||||
FROM alpine:latest
|
||||
|
||||
# Diretório de trabalho
|
||||
# Instalar dependências do sistema
|
||||
RUN apk update && \
|
||||
apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
make \
|
||||
git \
|
||||
gcc \
|
||||
python3-dev \
|
||||
musl-dev \
|
||||
linux-headers
|
||||
|
||||
# Criar link simbólico para python3
|
||||
RUN ln -sf python3 /usr/bin/python
|
||||
|
||||
# Definir diretório de trabalho
|
||||
WORKDIR /app
|
||||
|
||||
# UID/GID configuráveis para compatibilizar permissões em bind mounts Linux
|
||||
ARG APP_UID=1000
|
||||
ARG APP_GID=1000
|
||||
|
||||
# Instalar Python no Alpine e criar alias `python`
|
||||
RUN apk add --no-cache python3 py3-pip \
|
||||
&& ln -sf python3 /usr/bin/python
|
||||
|
||||
# Instalar dependências Python em venv usando build deps temporários
|
||||
COPY requirements.txt .
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
musl-dev \
|
||||
linux-headers \
|
||||
&& python -m venv /venv \
|
||||
&& /venv/bin/pip install --upgrade pip \
|
||||
&& /venv/bin/pip install --no-cache-dir -r requirements.txt \
|
||||
&& apk del .build-deps
|
||||
|
||||
# Copiar código da aplicação
|
||||
# Copiar arquivos do projeto
|
||||
COPY . .
|
||||
|
||||
# Criar usuário sem privilégios e diretórios de escrita necessários
|
||||
RUN addgroup -S -g "${APP_GID}" appgroup \
|
||||
&& adduser -S -D -H -u "${APP_UID}" -G appgroup appuser \
|
||||
&& mkdir -p /data /app/logs \
|
||||
&& chown -R appuser:appgroup /app /data /venv
|
||||
|
||||
# Ambiente padrão
|
||||
ENV PATH="/venv/bin:$PATH" \
|
||||
FLASK_APP=app.py \
|
||||
FLASK_ENV=production \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Rodar aplicação como usuário não-root
|
||||
USER appuser
|
||||
# Criar e ativar ambiente virtual
|
||||
RUN python -m venv /venv && \
|
||||
. /venv/bin/activate && \
|
||||
pip install --upgrade pip && \
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Expor a porta que o Flask usa
|
||||
EXPOSE 5000
|
||||
|
||||
# Definir o ambiente virtual como padrão
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_ENV=production
|
||||
|
||||
# Comando para rodar a aplicação
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
|
||||
|
||||
148
Makefile
148
Makefile
@@ -1,149 +1,23 @@
|
||||
.PHONY: install clean db-reset db-seed-fake db-seed-test-users admin-reset admin-rotate-otp \
|
||||
run run-gunicorn docker-db-reset docker-db-seed-fake docker-db-seed-test-users \
|
||||
docker-admin-reset docker-admin-rotate-otp docker-build docker-up docker-down docker-logs \
|
||||
docker-restart docker-db-reset-xplat docker-db-seed-fake-xplat docker-db-seed-test-users-xplat \
|
||||
docker-admin-reset-xplat docker-admin-rotate-otp-xplat docker-build-xplat docker-up-xplat \
|
||||
docker-down-xplat docker-logs-xplat cache-clear cache-status cache-keys dev-up dev-down \
|
||||
prod-build prod-up prod-logs cache-warmup cache-monitor
|
||||
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
|
||||
clean:
|
||||
rm -f ~/.local/share/controles/database.db*
|
||||
rm -f database.db*
|
||||
rm -f data/database.db*
|
||||
rm -rf ~/.local/share/controles/database.db*
|
||||
rm -f admin_qr.png
|
||||
rm -f data/admin_qr.png
|
||||
rm -f /tmp/admin_qr.png
|
||||
find . -type d -name "__pycache__" -prune -exec rm -rf {} +
|
||||
|
||||
db-reset: clean
|
||||
PYTHONUNBUFFERED=1 python -B scripts/manage.py db_reset
|
||||
init-db: clean
|
||||
python init_db.py
|
||||
|
||||
# Apenas seed (seed_database.py)
|
||||
db-seed-fake:
|
||||
PYTHONUNBUFFERED=1 python -B scripts/manage.py db_seed_fake
|
||||
seed: init-db
|
||||
python seed.py
|
||||
|
||||
# Apenas seed (create_test_users.py)
|
||||
db-seed-test-users:
|
||||
PYTHONUNBUFFERED=1 python -B scripts/manage.py db_seed_test_users
|
||||
init:
|
||||
python app.py --init
|
||||
|
||||
# Busca o OTP padrão
|
||||
admin-reset:
|
||||
PYTHONUNBUFFERED=1 python -B scripts/manage.py admin_reset
|
||||
|
||||
# Novo OTP
|
||||
admin-rotate-otp:
|
||||
PYTHONUNBUFFERED=1 python -B scripts/manage.py admin_rotate_otp
|
||||
|
||||
# Server padrão do python
|
||||
run:
|
||||
PYTHONUNBUFFERED=1 python -B app.py
|
||||
python app.py
|
||||
|
||||
# server padrão de produção (recomendado)
|
||||
run-gunicorn:
|
||||
PYTHONUNBUFFERED=1 python -B -m gunicorn --bind 0.0.0.0:5000 app:app
|
||||
run-with-seed: seed init run
|
||||
|
||||
# Docker commands
|
||||
docker-db-reset:
|
||||
mkdir -p data logs
|
||||
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_reset
|
||||
|
||||
docker-db-seed-fake:
|
||||
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_seed_fake
|
||||
|
||||
docker-db-seed-test-users:
|
||||
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py db_seed_test_users
|
||||
|
||||
docker-admin-reset:
|
||||
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py admin_reset
|
||||
|
||||
docker-admin-rotate-otp:
|
||||
docker-compose -f docker-compose.yml exec app python -B scripts/manage.py admin_rotate_otp
|
||||
|
||||
docker-build:
|
||||
mkdir -p data logs
|
||||
docker-compose -f docker-compose.yml build
|
||||
|
||||
docker-up:
|
||||
mkdir -p data logs
|
||||
docker-compose -f docker-compose.yml up -d
|
||||
|
||||
docker-down:
|
||||
docker-compose -f docker-compose.yml down
|
||||
|
||||
docker-logs:
|
||||
docker-compose -f docker-compose.yml logs -f
|
||||
|
||||
docker-restart:
|
||||
docker-compose -f docker-compose.yml restart
|
||||
|
||||
# Docker commands (fallback cross-platform)
|
||||
docker-db-reset-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_reset
|
||||
|
||||
docker-db-seed-fake-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_seed_fake
|
||||
|
||||
docker-db-seed-test-users-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py db_seed_test_users
|
||||
|
||||
docker-admin-reset-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py admin_reset
|
||||
|
||||
docker-admin-rotate-otp-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml exec app python -B scripts/manage.py admin_rotate_otp
|
||||
|
||||
docker-build-xplat:
|
||||
mkdir -p data logs
|
||||
docker-compose -f docker-compose.crossplatform.yml build
|
||||
|
||||
docker-up-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml up -d
|
||||
|
||||
docker-down-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml down
|
||||
|
||||
docker-logs-xplat:
|
||||
docker-compose -f docker-compose.crossplatform.yml logs -f
|
||||
|
||||
# Redis cache commands
|
||||
cache-clear:
|
||||
docker-compose -f docker-compose.yml exec redis redis-cli FLUSHDB
|
||||
|
||||
cache-status:
|
||||
docker-compose -f docker-compose.yml exec redis redis-cli INFO
|
||||
|
||||
cache-keys:
|
||||
docker-compose -f docker-compose.yml exec redis redis-cli KEYS "*"
|
||||
|
||||
# Development with Docker
|
||||
dev-up: docker-build docker-up
|
||||
@echo "Development environment started with Redis cache"
|
||||
@echo "Application: http://localhost:5000"
|
||||
|
||||
dev-down: docker-down
|
||||
@echo "Development environment stopped"
|
||||
|
||||
# Production commands
|
||||
prod-build:
|
||||
docker-compose -f docker-compose.yml build --no-cache
|
||||
|
||||
prod-up:
|
||||
docker-compose -f docker-compose.yml up -d
|
||||
|
||||
prod-logs:
|
||||
docker-compose -f docker-compose.yml logs -f app
|
||||
|
||||
# Cache management
|
||||
cache-warmup:
|
||||
@echo "Warming up cache..."
|
||||
curl -X GET http://localhost:5000/api/dashboard/stats
|
||||
curl -X GET http://localhost:5000/api/dashboard/militante-stats
|
||||
curl -X GET http://localhost:5000/api/dashboard/financial-stats
|
||||
@echo "Cache warmup completed"
|
||||
|
||||
cache-monitor:
|
||||
@echo "Monitoring Redis cache..."
|
||||
watch -n 5 'docker-compose -f docker-compose.yml exec redis redis-cli INFO memory'
|
||||
reset-admin: clean
|
||||
python create_admin.py
|
||||
|
||||
389
README.md
389
README.md
@@ -1,349 +1,116 @@
|
||||
# Sistema de Controles OCI
|
||||
# Sistema de Controles
|
||||
|
||||
Sistema web para gestão organizacional (militantes, estrutura hierárquica, cotas, pagamentos e materiais), com autenticação por senha + OTP, permissões RBAC e cache Redis.
|
||||
Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
|
||||
|
||||
## 🔧 Tecnologias
|
||||
## Arquitetura MVC
|
||||
|
||||
- **Backend**: Flask 3.0.2
|
||||
- **Frontend**: Bootstrap 5, HTML5, CSS3, JavaScript
|
||||
- **Database**: SQLite + SQLAlchemy 2.0+ (>= 2.0.36)
|
||||
- **Cache**: Redis 7.4.4 (opcional fora do Docker)
|
||||
- **Authentication**: Flask-Login + OTP (pyotp)
|
||||
- **Container**: Docker + Docker Compose
|
||||
- **Server**: Gunicorn
|
||||
O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
|
||||
|
||||
## 🚀 Status Atual
|
||||
### Models
|
||||
|
||||
- Sistema com Arquitetura de Permissões (RBAC)
|
||||
- Sistema de permissões implementado no nível de dados
|
||||
- Estrutura organizacional completa
|
||||
- Aplicação Flask rodando com Docker
|
||||
- Redis cache integrado e funcionando
|
||||
- Banco de dados SQLite inicializado
|
||||
- Usuário admin configurado com OTP
|
||||
- 30 militantes de teste criados
|
||||
- Menus sempre visíveis, controle transparente
|
||||
Os modelos representam as entidades do sistema e estão organizados em:
|
||||
|
||||
## 🏗️ Arquitetura de Permissões
|
||||
- **models/entities/**: Classes de entidades do banco de dados (SQLAlchemy)
|
||||
- `base.py`: Configuração do SQLAlchemy e classe Base
|
||||
- `usuario.py`: Modelo de usuário
|
||||
- `militante.py`: Modelo de militante
|
||||
- `cota_mensal.py`: Modelo de cota mensal
|
||||
- etc.
|
||||
|
||||
O sistema implementa uma estratégia de controle de permissões no **nível de dados**, garantindo que:
|
||||
### Controllers
|
||||
|
||||
- **Menus permanecem sempre visíveis** - Não há restrições na interface
|
||||
- **Dados são filtrados por hierarquia** - Admin → CC → CR → Setor → Célula
|
||||
- **Templates nunca quebram** - Sempre renderizam, mesmo com dados vazios
|
||||
Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
|
||||
|
||||
## ⚙️ Instalação - Pré-requisitos
|
||||
- **controllers/**: Implementação dos controladores
|
||||
- `auth_controller.py`: Controle de autenticação
|
||||
- `usuario_controller.py`: Operações com usuários
|
||||
- `militante_controller.py`: Operações com militantes
|
||||
- `home_controller.py`: Controlador da página inicial
|
||||
- etc.
|
||||
|
||||
- Docker + Docker Compose (para fluxo com containers)
|
||||
- Porta 5000 disponível para a aplicação
|
||||
- Porta 6379 disponível para Redis
|
||||
- Python 3.10+ (recomendado)
|
||||
- `pip`
|
||||
- `make`
|
||||
### Views
|
||||
|
||||
## 🐳 Primeiro Inicio com Docker (recomendado)
|
||||
As views são os templates que exibem os dados para o usuário:
|
||||
|
||||
### 0. Clone o repositorio
|
||||
- **templates/**: Templates Jinja2
|
||||
- Organizados por funcionalidade (admin, militantes, cotas, etc.)
|
||||
|
||||
```bash
|
||||
git clone git@gitea.comunatec.org:comunatec/controles.git
|
||||
cd controles
|
||||
```
|
||||
### Services
|
||||
|
||||
### 1. Resete o banco
|
||||
Camada adicional para encapsular a lógica de acesso a dados:
|
||||
|
||||
```bash
|
||||
make docker-db-reset
|
||||
```
|
||||
- **services/**: Serviços para acesso a dados
|
||||
- `database_service.py`: Gerenciamento de conexões com o banco
|
||||
- `usuario_service.py`: Acesso a dados de usuários
|
||||
- `militante_service.py`: Acesso a dados de militantes
|
||||
- etc.
|
||||
|
||||
### 2. Adicione dados fakes para testes (opcional)
|
||||
### Routes
|
||||
|
||||
```bash
|
||||
make docker-db-seed-fake
|
||||
```
|
||||
Rotas da aplicação organizadas em blueprints:
|
||||
|
||||
### 3. Subir aplicação
|
||||
- **routes/**: Módulos de rotas (Flask Blueprints)
|
||||
- `main.py`: Rotas principais
|
||||
- `auth.py`: Rotas de autenticação
|
||||
- `admin.py`: Rotas administrativas
|
||||
- `militante.py`: Rotas para gerenciamento de militantes
|
||||
- etc.
|
||||
|
||||
```bash
|
||||
make dev-up
|
||||
```
|
||||
## Instalação
|
||||
|
||||
### 4. Acompanhar logs
|
||||
|
||||
- Aplicação: `logs/controles.log`
|
||||
- Cache: `logs/cache.log`
|
||||
- Docker: `docker-compose logs`
|
||||
|
||||
```bash
|
||||
make docker-logs # Toda a aplicação
|
||||
docker-compose logs redis # Somente o redis
|
||||
make cache-status # INFO do redis
|
||||
```
|
||||
|
||||
### 5. Descer aplicação
|
||||
|
||||
```bash
|
||||
make dev-down
|
||||
```
|
||||
|
||||
## 🐍 Primeiro Inicio - Execução Local (Sem Docker)
|
||||
|
||||
### 0. Clone o repositorio
|
||||
|
||||
```bash
|
||||
git clone git@gitea.comunatec.org:comunatec/controles.git
|
||||
cd controles
|
||||
```
|
||||
|
||||
### 1. Ambiente Python
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux/Mac
|
||||
# ou
|
||||
venv\Scripts\activate # Windows
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. Crie o `.env` na raiz
|
||||
|
||||
Exemplo:
|
||||
|
||||
```env
|
||||
# Usando OTP padrão para não trocar toda hora no desenvolvimento
|
||||
ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
|
||||
|
||||
# Para usar o mesmo banco que o Docker (Linux/WSL permite bind-mount)
|
||||
DATABASE_URL=sqlite:///data/database.db
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=troque_esta_chave
|
||||
|
||||
APP_UID=1000
|
||||
APP_GID=1000
|
||||
|
||||
MAIL_SERVER=seu_servidor_smtp
|
||||
MAIL_PORT=587
|
||||
MAIL_USE_TLS=True
|
||||
MAIL_USERNAME=seu_email
|
||||
MAIL_PASSWORD=sua_senha
|
||||
```
|
||||
|
||||
Se Redis não estiver disponível localmente, a aplicação continua rodando sem cache.
|
||||
|
||||
### 3. Inicialize o banco e rode
|
||||
|
||||
```bash
|
||||
make db-reset
|
||||
make db-seed-fake # opcional
|
||||
make run
|
||||
# ou
|
||||
make run-gunicorn # server de produção
|
||||
```
|
||||
|
||||
## 🔐 Acesso ao Sistema
|
||||
|
||||
### Credenciais do Admin
|
||||
- **URL**: http://localhost:5000
|
||||
- **Usuário**: admin
|
||||
- **Senha**: admin123
|
||||
- **OTP Secret**: JBSWY3DPEHPK3PXP
|
||||
|
||||
### Configuração OTP
|
||||
|
||||
1. Instale um aplicativo autenticador (Google Authenticator, Microsoft Authenticator)
|
||||
|
||||
2. Configure manualmente:
|
||||
- Descrição da chave (Codinome): Controles-OCI-admin
|
||||
- Segredo OTP (Sua Chave): JBSWY3DPEHPK3PXP
|
||||
- Tipo: TOTP
|
||||
- Algoritmo: SHA1
|
||||
- Dígitos: 6
|
||||
- Intervalo: 30 segundos
|
||||
|
||||
**OU** use o QR Code gerado em `/tmp/admin_qr.png` ou `/data/admin_qr.png` ou `admin_qr.png`.
|
||||
|
||||
PS: Google Authenticator só tem "Codinome" e "Sua Chave" de config, e tá tudo bem.
|
||||
|
||||
## Testes Automatizados
|
||||
|
||||
```bash
|
||||
# ambiente já configurado
|
||||
pip install -r tests/requirements-test.txt
|
||||
pytest
|
||||
```
|
||||
|
||||
Também existe `run_tests.sh`, que monta um venv e executa a suíte automaticamente.
|
||||
|
||||
- TODO: Talvez trocar o nome para venv_test
|
||||
|
||||
## 📁 Estrutura de arquivos
|
||||
|
||||
O sistema busca seguir padrão MVC (Model-View-Controller), atualmente está:
|
||||
|
||||
```
|
||||
controles/
|
||||
├── controllers/ # Controladores (lógica de rotas)
|
||||
├── data/ # Banco de dados (e talvez qr_code admin)
|
||||
├── docs/ # Documentações da arquitetura
|
||||
├── functions/ # Funções utilitárias
|
||||
├── logs/ # Logs de aplicação, redis...
|
||||
├── migrations/ # Alterações de banco para não perder dados (produção)
|
||||
├── models/ # Modelos (operações de banco)
|
||||
├── routes/ # Rotas de aplicação
|
||||
├── scripts/ # Scripts de gerenciamento
|
||||
├── services/ # Serviços (lógica de negócio)
|
||||
├── sql/ # Migrate para o rbac
|
||||
├── static/ # Arquivos estáticos (icon/css/js)
|
||||
├── templates/ # Views (templates HTML)
|
||||
├── tests/ # Testes automatizados
|
||||
├── utils/ # Funções sem regra de negócio ou dependencia de domínio
|
||||
├── app.py # Ponto de entrada da aplicação
|
||||
├── docker-compose.yml # Configuração Docker
|
||||
├── Dockerfile # Imagem Docker
|
||||
└── requirements.txt # Dependências Python
|
||||
```
|
||||
|
||||
- TODO: temos duas rotas (routes e controllers)? Unificar futuramente.
|
||||
- TODO: sql/migrate_db parece utilizar outro banco.
|
||||
|
||||
## 🤝 Contribuição
|
||||
|
||||
1. Crie uma branch para sua feature
|
||||
2. Commit suas mudanças
|
||||
3. Push sua branch para o Gitea
|
||||
4. Outro camarada verifica a branch
|
||||
5. Abra um Pull Request para a branch solicitada
|
||||
|
||||
## 📄 Licença
|
||||
|
||||
Este projeto é privado para uso da OCI.
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
1. **Redis não conecta**
|
||||
|
||||
```bash
|
||||
docker-compose logs redis
|
||||
docker-compose restart redis
|
||||
1. Clone o repositório:
|
||||
```
|
||||
- Redis está indisponível localmente, mas app continua executando mesmo fora do Docker.
|
||||
|
||||
2. **Cache não funciona**
|
||||
|
||||
```bash
|
||||
make cache-status
|
||||
make cache-clear
|
||||
git clone [URL_DO_REPOSITORIO]
|
||||
```
|
||||
|
||||
3. **Aplicação não inicia**
|
||||
|
||||
```bash
|
||||
docker-compose logs app
|
||||
docker-compose down && docker-compose up -d
|
||||
2. Crie e ative um ambiente virtual:
|
||||
```
|
||||
python -m venv myenv
|
||||
source myenv/bin/activate # Linux/Mac
|
||||
myenv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
4. **Modificações no banco local não alteram o banco no Docker**
|
||||
3. Instale as dependências:
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
- Linux bind mount no grupo de usuario errado: ajuste `APP_UID`/`APP_GID` no `Dockerfile` para seu grupo de usuarios (padrão=1000).
|
||||
- Docker com engine do Windows não consegue fazer bind mount, então alterações no banco local não refletem no banco do Docker, use as operações dentro do docker com make docker-* ou no windows instale o wsl2 e instale o Docker com apenas a engine "Docker no WSL".
|
||||
4. Inicialize o banco de dados:
|
||||
```
|
||||
python app.py --init
|
||||
```
|
||||
|
||||
## Estrutura de Permissões (RBAC)
|
||||
5. Execute a aplicação:
|
||||
```
|
||||
python app.py
|
||||
```
|
||||
|
||||
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC), onde a verificação de ações são feitas com permissões (permission), e as permissões são pré-definidas com base em papeis (role). Que possuem a seguinte hierarquia:
|
||||
## Credenciais padrão
|
||||
|
||||
### Níveis de Papéis
|
||||
- **Administrador**:
|
||||
- Usuário: admin
|
||||
- Senha: admin123
|
||||
|
||||
1. **Militante Básico** (Nível 1)
|
||||
- Visualizar próprios dados
|
||||
- Editar próprios dados
|
||||
- Visualizar dados da célula
|
||||
## Desenvolvimento
|
||||
|
||||
2. **Secretário de Célula** (Nível 2)
|
||||
- Todas as permissões do Militante Básico
|
||||
- Gerenciar membros da célula
|
||||
- Criar membros na célula
|
||||
- Visualizar relatórios da célula
|
||||
Para adicionar novos recursos, siga a arquitetura MVC:
|
||||
|
||||
3. **Membro de Setor** (Nível 3)
|
||||
- Todas as permissões do Secretário de Célula
|
||||
- Visualizar relatórios do setor
|
||||
1. Crie modelos necessários em `models/entities/`
|
||||
2. Implemente serviços para acesso a dados em `services/`
|
||||
3. Crie controladores com lógica de negócio em `controllers/`
|
||||
4. Adicione rotas em módulos existentes ou crie novos em `routes/`
|
||||
5. Desenvolva templates em `templates/`
|
||||
|
||||
4. **Secretário de Setor** (Nível 4)
|
||||
- Todas as permissões do Membro de Setor
|
||||
- Gerenciar células do setor
|
||||
- Criar células no setor
|
||||
## Testes
|
||||
|
||||
5. **Membro de CR** (Nível 5)
|
||||
- Todas as permissões do Secretário de Setor
|
||||
- Visualizar relatórios do CR
|
||||
Execute os testes usando pytest:
|
||||
|
||||
6. **Secretário de CR** (Nível 6)
|
||||
- Todas as permissões do Membro de CR
|
||||
- Gerenciar setores do CR
|
||||
- Criar setores no CR
|
||||
|
||||
7. **Membro do CC** (Nível 7)
|
||||
- Todas as permissões do Secretário de CR
|
||||
- Visualizar relatórios nacionais
|
||||
|
||||
8. **Secretário Geral** (Nível 8)
|
||||
- Todas as permissões do Membro do CC
|
||||
- Gerenciar CRs
|
||||
- Criar CRs
|
||||
- Configurar sistema
|
||||
|
||||
## Uso do RBAC
|
||||
|
||||
### Decoradores de Permissão
|
||||
|
||||
O sistema fornece três decoradores para controle de acesso:
|
||||
|
||||
1. `@require_permission(permission_name)`
|
||||
- Verifica se o usuário tem uma permissão específica
|
||||
- Exemplo: `@require_permission('create_cell_member')`
|
||||
|
||||
2. `@require_role(role_name)`
|
||||
- Verifica se o usuário tem um papel específico
|
||||
- Exemplo: `@require_role('Secretário de Célula')`
|
||||
|
||||
3. `@require_minimum_role(min_level)`
|
||||
- Verifica se o usuário tem um papel com nível mínimo
|
||||
- Exemplo: `@require_minimum_role(Role.SECRETARIO_CR)`
|
||||
|
||||
### Verificando Permissões e Papéis no Código
|
||||
|
||||
```python
|
||||
# Verificar se um usuário tem uma permissão
|
||||
if user.has_permission(Permission.CREATE_CELL_MEMBER):
|
||||
# Faça algo
|
||||
|
||||
# Verificar se um usuário tem um papel
|
||||
if user.has_role(Role.SECRETARIO_CELULA):
|
||||
# Faça algo
|
||||
|
||||
# Obter o papel mais alto do usuário
|
||||
highest_role = user.get_highest_role()
|
||||
|
||||
# Verificar se o usuário tem nível secretário de célula ou superior
|
||||
if user.has_minimum_role(Role.SECRETARIO_CELULA):
|
||||
# Faça algo
|
||||
```
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
## Documentação Complementar
|
||||
Ou use o script de teste:
|
||||
|
||||
- Documentação complementar: `docs/README.md`
|
||||
- RBAC: `docs/rbac.md`
|
||||
- Estratégia de permissões: `docs/permission_strategy.md`
|
||||
- Redis e cache: `docs/redis_cache_setup.md`
|
||||
- Histórico de correções de permissões: `docs/permission_fixes_summary.md`
|
||||
|
||||
## Segurança
|
||||
|
||||
- Todas as senhas são armazenadas com hash bcrypt
|
||||
- Sessões expiram após período de inatividade
|
||||
- Controle de acesso granular baseado em papéis
|
||||
- Proteção contra CSRF
|
||||
- Validação de entrada de dados
|
||||
```
|
||||
./run_tests.sh
|
||||
```
|
||||
123
app.py.new
Normal file
123
app.py.new
Normal file
@@ -0,0 +1,123 @@
|
||||
from flask import Flask
|
||||
from flask_bootstrap import Bootstrap5
|
||||
from flask_mail import Mail
|
||||
from flask_login import LoginManager
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import secrets
|
||||
import logging
|
||||
|
||||
# Importações de configurações
|
||||
from models.entities.base import Base, engine
|
||||
from routes.main import main_bp
|
||||
from routes.admin import admin_bp
|
||||
from routes.auth import auth_bp
|
||||
from routes.militante import militante_bp
|
||||
from routes.pagamento import pagamento_bp
|
||||
from routes.relatorio import relatorio_bp
|
||||
from routes.cota import cota_bp
|
||||
|
||||
# Configuração do logger
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Carregar variáveis de ambiente
|
||||
load_dotenv()
|
||||
|
||||
def create_app():
|
||||
"""Factory para criação da aplicação Flask"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuração secreta
|
||||
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
|
||||
|
||||
# Configuração de Bootstrap
|
||||
bootstrap = Bootstrap5(app)
|
||||
|
||||
# Registrar blueprints
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(militante_bp)
|
||||
app.register_blueprint(pagamento_bp)
|
||||
app.register_blueprint(relatorio_bp)
|
||||
app.register_blueprint(cota_bp)
|
||||
|
||||
# Configurar proteção CSRF
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
app.config['WTF_CSRF_CHECK_DEFAULT'] = False
|
||||
app.config['WTF_CSRF_HEADERS'] = ['X-CSRFToken']
|
||||
|
||||
# Configurar Flask-Login
|
||||
login_manager = LoginManager()
|
||||
login_manager.init_app(app)
|
||||
login_manager.login_view = 'auth.login'
|
||||
|
||||
# Função para carregar usuário no login_manager
|
||||
from models.entities.usuario import Usuario
|
||||
from services.database_service import DatabaseService
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
"""Carrega o usuário pelo ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles)
|
||||
).get(user_id)
|
||||
return user
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# Adicionar filtros Jinja2
|
||||
@app.template_filter('bitwise_and')
|
||||
def bitwise_and(value1, value2):
|
||||
"""Filtro para operação bit a bit AND"""
|
||||
return value1 & value2
|
||||
|
||||
# Configurar Flask-Mail
|
||||
app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com')
|
||||
app.config['MAIL_PORT'] = int(os.getenv('MAIL_PORT', 587))
|
||||
app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', 'True').lower() == 'true'
|
||||
app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME')
|
||||
app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD')
|
||||
app.config['MAIL_DEFAULT_SENDER'] = os.getenv('MAIL_DEFAULT_SENDER')
|
||||
|
||||
# Inicializar Mail
|
||||
mail = Mail(app)
|
||||
|
||||
return app
|
||||
|
||||
def init_system():
|
||||
"""Inicializa o sistema com banco de dados e usuários padrão"""
|
||||
from functions.database import init_database
|
||||
|
||||
# Inicializar banco de dados
|
||||
logger.info("Inicializando banco de dados...")
|
||||
init_database()
|
||||
|
||||
# Outros procedimentos de inicialização podem ser adicionados aqui
|
||||
|
||||
def main():
|
||||
"""Inicializa e retorna a aplicação Flask"""
|
||||
return create_app()
|
||||
|
||||
# Criar a aplicação
|
||||
app = main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
|
||||
# Verificar se é para inicializar o sistema
|
||||
if '--init' in sys.argv:
|
||||
init_system()
|
||||
else:
|
||||
# Executar a aplicação
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=5000,
|
||||
debug=os.getenv('FLASK_ENV') == 'development'
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
# Controllers package
|
||||
@@ -1,33 +1,32 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, session, jsonify
|
||||
from flask import session, flash, redirect, url_for, request
|
||||
from flask_login import login_user, logout_user, current_user
|
||||
from datetime import datetime
|
||||
from functions.database import Militante, get_db_session, Usuario
|
||||
from functions.decorators import require_login
|
||||
from werkzeug.security import generate_password_hash
|
||||
from services.otp_service import generate_qr_code_base64
|
||||
import pyotp
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
import base64
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
from models.entities.usuario import Usuario
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
"""Rota de login"""
|
||||
print(f"=== LOGIN ROUTE CALLED ===")
|
||||
print(f"Method: {request.method}")
|
||||
print(f"Form data: {dict(request.form)}")
|
||||
class AuthController:
|
||||
"""Controlador para funções de autenticação"""
|
||||
|
||||
@staticmethod
|
||||
def login():
|
||||
"""Processa o login de usuário"""
|
||||
if request.method != "POST":
|
||||
return False
|
||||
|
||||
if request.method == "POST":
|
||||
email_or_username = request.form.get("email")
|
||||
password = request.form.get("password")
|
||||
otp = request.form.get("otp")
|
||||
|
||||
print(f"Tentativa de login - Email/Username: {email_or_username}, OTP: {otp}")
|
||||
|
||||
if not all([email_or_username, password]):
|
||||
print("Erro: Email/usuário e senha são obrigatórios")
|
||||
flash("Email/usuário e senha são obrigatórios.", "danger")
|
||||
return redirect(url_for("auth.login"))
|
||||
return False
|
||||
|
||||
db = get_db_session()
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
# Tenta encontrar o usuário por email ou username
|
||||
user = db.query(Usuario).filter(
|
||||
@@ -35,27 +34,18 @@ def login():
|
||||
(Usuario.username == email_or_username)
|
||||
).first()
|
||||
|
||||
print(f"Usuário encontrado: {user.username if user else 'Não encontrado'}")
|
||||
|
||||
if not user or not user.check_password(password):
|
||||
print("Erro: Email/usuário ou senha incorretos")
|
||||
flash("Email/usuário ou senha incorretos.", "danger")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
print(f"Senha válida. OTP Secret: {user.otp_secret}")
|
||||
return False
|
||||
|
||||
# Verificar OTP se o usuário tiver configurado
|
||||
if user.otp_secret and not otp:
|
||||
print("Erro: Código OTP é obrigatório")
|
||||
flash("Código OTP é obrigatório para sua conta.", "danger")
|
||||
return redirect(url_for("auth.login"))
|
||||
return False
|
||||
|
||||
if user.otp_secret and not user.verify_otp(otp):
|
||||
print(f"Erro: Código OTP inválido. Código fornecido: {otp}")
|
||||
flash("Código OTP inválido.", "danger")
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
print("OTP válido! Fazendo login...")
|
||||
return False
|
||||
|
||||
# Atualizar último login
|
||||
user.ultimo_login = datetime.utcnow()
|
||||
@@ -66,219 +56,69 @@ def login():
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['is_admin'] = user.is_admin
|
||||
print(f"Login realizado: user_id={user.id}, username={user.username}, is_admin={user.is_admin}")
|
||||
|
||||
# Redirecionar para home
|
||||
return redirect(url_for("home.index"))
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return render_template("login.html")
|
||||
|
||||
@auth_bp.route("/api/login", methods=["POST"])
|
||||
def api_login():
|
||||
"""Endpoint de login API sem CSRF para automação/testes"""
|
||||
try:
|
||||
# Verificar se é uma requisição JSON
|
||||
if request.is_json:
|
||||
data = request.get_json()
|
||||
email_or_username = data.get("email") or data.get("username")
|
||||
password = data.get("password")
|
||||
otp = data.get("otp")
|
||||
else:
|
||||
# Fallback para form data
|
||||
email_or_username = request.form.get("email") or request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
otp = request.form.get("otp")
|
||||
|
||||
print(f"=== API LOGIN CALLED ===")
|
||||
print(f"Email/Username: {email_or_username}")
|
||||
print(f"OTP: {otp}")
|
||||
|
||||
# Validações básicas
|
||||
if not email_or_username or not password:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Email/username e senha são obrigatórios'
|
||||
}), 400
|
||||
|
||||
db = get_db_session()
|
||||
@staticmethod
|
||||
def logout():
|
||||
"""Processa o logout de usuário"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
# Buscar usuário
|
||||
user = db.query(Usuario).filter(
|
||||
(Usuario.email == email_or_username) |
|
||||
(Usuario.username == email_or_username)
|
||||
).first()
|
||||
|
||||
if not user:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usuário não encontrado'
|
||||
}), 401
|
||||
|
||||
# Verificar senha
|
||||
if not user.check_password(password):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Senha incorreta'
|
||||
}), 401
|
||||
|
||||
# Verificar OTP se configurado
|
||||
if user.otp_secret:
|
||||
if not otp:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Código OTP é obrigatório para esta conta'
|
||||
}), 400
|
||||
|
||||
if not user.verify_otp(otp):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Código OTP inválido'
|
||||
}), 401
|
||||
|
||||
# Atualizar último login
|
||||
user.ultimo_login = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# Fazer login
|
||||
login_user(user)
|
||||
session['user_id'] = user.id
|
||||
session['username'] = user.username
|
||||
session['is_admin'] = user.is_admin
|
||||
|
||||
print(f"API Login realizado: user_id={user.id}, username={user.username}")
|
||||
|
||||
# Retornar resposta de sucesso
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Login realizado com sucesso',
|
||||
'user': {
|
||||
'id': user.id,
|
||||
'username': user.username,
|
||||
'email': user.email,
|
||||
'nome': user.nome,
|
||||
'is_admin': user.is_admin,
|
||||
'ultimo_login': user.ultimo_login.isoformat() if user.ultimo_login else None
|
||||
},
|
||||
'session_id': session.get('_id')
|
||||
})
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro no API login: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro interno do servidor'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route("/api/logout", methods=["POST"])
|
||||
def api_logout():
|
||||
"""Endpoint de logout API"""
|
||||
try:
|
||||
if current_user.is_authenticated:
|
||||
db = get_db_session()
|
||||
try:
|
||||
user = current_user
|
||||
user = current_user
|
||||
if user.is_authenticated:
|
||||
user.logout()
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logout_user()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Logout realizado com sucesso'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro no API logout: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro interno do servidor'
|
||||
}), 500
|
||||
|
||||
@auth_bp.route("/api/status")
|
||||
def api_status():
|
||||
"""Endpoint para verificar status da autenticação"""
|
||||
if current_user.is_authenticated:
|
||||
return jsonify({
|
||||
'authenticated': True,
|
||||
'user': {
|
||||
'id': current_user.id,
|
||||
'username': current_user.username,
|
||||
'email': current_user.email,
|
||||
'nome': current_user.nome,
|
||||
'is_admin': current_user.is_admin
|
||||
}
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'authenticated': False
|
||||
})
|
||||
|
||||
@auth_bp.route("/logout")
|
||||
@require_login
|
||||
def logout():
|
||||
db = get_db_session()
|
||||
try:
|
||||
user = current_user
|
||||
if user:
|
||||
user.logout()
|
||||
db.commit()
|
||||
logout_user()
|
||||
finally:
|
||||
db.close()
|
||||
flash('Logout realizado com sucesso!', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def alterar_senha():
|
||||
"""Rota para alterar a senha do usuário"""
|
||||
if request.method == "POST":
|
||||
senha_atual = request.form.get("senha_atual")
|
||||
nova_senha = request.form.get("nova_senha")
|
||||
confirmar_senha = request.form.get("confirmar_senha")
|
||||
logout_user()
|
||||
flash('Logout realizado com sucesso!', 'success')
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def alterar_senha(user_id, senha_atual, nova_senha, confirmar_senha):
|
||||
"""Altera a senha do usuário"""
|
||||
if not all([senha_atual, nova_senha, confirmar_senha]):
|
||||
flash("Todos os campos são obrigatórios.", "error")
|
||||
return redirect(url_for("auth.alterar_senha"))
|
||||
return False
|
||||
|
||||
if nova_senha != confirmar_senha:
|
||||
flash("As senhas não coincidem.", "error")
|
||||
return redirect(url_for("auth.alterar_senha"))
|
||||
return False
|
||||
|
||||
db = get_db_session()
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(current_user.id)
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
flash("Usuário não encontrado.", "error")
|
||||
return False
|
||||
|
||||
if not user.check_password(senha_atual):
|
||||
flash("Senha atual incorreta.", "error")
|
||||
return redirect(url_for("auth.alterar_senha"))
|
||||
return False
|
||||
|
||||
user.password_hash = generate_password_hash(nova_senha)
|
||||
user.set_password(nova_senha)
|
||||
db.commit()
|
||||
flash("Senha alterada com sucesso!", "success")
|
||||
return redirect(url_for("home.index"))
|
||||
return True
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return render_template("alterar_senha.html")
|
||||
@staticmethod
|
||||
def generate_qr_code(user):
|
||||
"""Gera um QR code para o usuário"""
|
||||
if not user.otp_secret:
|
||||
user.otp_secret = pyotp.random_base32()
|
||||
|
||||
@auth_bp.route("/qr/<token>")
|
||||
def get_qr_code(token):
|
||||
"""Gera QR code para configuração OTP"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).filter_by(temp_token=token).first()
|
||||
if not militante or militante.temp_token_expiry < datetime.now():
|
||||
flash('Token inválido ou expirado.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
totp = pyotp.TOTP(user.otp_secret)
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(totp.provisioning_uri(user.email, issuer_name="Sistema de Controles"))
|
||||
qr.make(fit=True)
|
||||
|
||||
qr_code = generate_qr_code_base64(militante)
|
||||
return render_template('mostrar_qr_code.html', qr_code=qr_code)
|
||||
finally:
|
||||
db.close()
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
qr_code = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
return qr_code
|
||||
@@ -1,134 +0,0 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from functions.database import get_db_session, CotaMensal, Militante
|
||||
from functions.decorators import require_login
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
from datetime import datetime
|
||||
from flask_login import current_user
|
||||
|
||||
cota_bp = Blueprint('cota', __name__)
|
||||
|
||||
@cota_bp.route("/cotas/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def novo():
|
||||
"""Cria uma nova cota mensal"""
|
||||
if request.method == "POST":
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
valor_antigo = float(request.form.get("valor_antigo"))
|
||||
valor_novo = float(request.form.get("valor_novo"))
|
||||
data_alteracao = converter_data(request.form.get("data_alteracao"))
|
||||
data_vencimento = converter_data(request.form.get("data_vencimento"))
|
||||
|
||||
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
|
||||
flash('Data inválida ou futura', 'danger')
|
||||
return redirect(url_for('cota.novo'))
|
||||
|
||||
cota = CotaMensal(
|
||||
militante_id=militante_id,
|
||||
valor_antigo=valor_antigo,
|
||||
valor_novo=valor_novo,
|
||||
data_alteracao=data_alteracao,
|
||||
data_vencimento=data_vencimento,
|
||||
pago=False
|
||||
)
|
||||
db.add(cota)
|
||||
db.commit()
|
||||
flash('Cota cadastrada com sucesso!', 'success')
|
||||
return redirect(url_for('cota.listar'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao cadastrar cota', 'danger')
|
||||
return redirect(url_for('cota.novo'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# GET - Renderizar formulário
|
||||
db = get_db_session()
|
||||
try:
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
return render_template("nova_cota.html", militantes=militantes)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@cota_bp.route("/cotas")
|
||||
@require_login
|
||||
def listar():
|
||||
"""Lista todas as cotas mensais com controle de permissões no nível de dados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
|
||||
cotas = []
|
||||
|
||||
# Verificar permissões para filtrar dados
|
||||
if current_user.is_admin:
|
||||
# Admin vê todas
|
||||
cotas = db.query(CotaMensal).join(Militante).order_by(CotaMensal.data_vencimento.desc()).all()
|
||||
elif hasattr(current_user, 'has_permission'):
|
||||
# Outros usuários veem baseado nas suas permissões
|
||||
# Por enquanto, deixar vazio até implementar a lógica específica
|
||||
cotas = []
|
||||
|
||||
# SEMPRE renderizar o template, independente das permissões
|
||||
return render_template("listar_cotas.html", cotas=cotas)
|
||||
except Exception as e:
|
||||
print(f"Erro no controller de cotas: {e}")
|
||||
# Em caso de erro, renderizar com dados vazios
|
||||
return render_template("listar_cotas.html", cotas=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@cota_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def editar(id):
|
||||
"""Edita uma cota mensal"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
cota = db.query(CotaMensal).get(id)
|
||||
if not cota:
|
||||
flash('Cota não encontrada.', 'danger')
|
||||
return redirect(url_for('cota.listar'))
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
cota.valor_antigo = float(request.form.get("valor_antigo"))
|
||||
cota.valor_novo = float(request.form.get("valor_novo"))
|
||||
cota.data_alteracao = converter_data(request.form.get("data_alteracao"))
|
||||
cota.data_vencimento = converter_data(request.form.get("data_vencimento"))
|
||||
cota.pago = request.form.get("pago") == "on"
|
||||
|
||||
db.commit()
|
||||
flash('Cota atualizada com sucesso!', 'success')
|
||||
return redirect(url_for('cota.listar'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao atualizar cota.', 'danger')
|
||||
print(f"Erro ao atualizar cota: {e}")
|
||||
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
return render_template("editar_cota.html", cota=cota, militantes=militantes)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@cota_bp.route("/cotas/excluir/<int:id>", methods=["POST"])
|
||||
@require_login
|
||||
def excluir(id):
|
||||
"""Exclui uma cota mensal"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
cota = db.query(CotaMensal).get(id)
|
||||
if not cota:
|
||||
flash('Cota não encontrada.', 'danger')
|
||||
return redirect(url_for('cota.listar'))
|
||||
|
||||
# Excluir a cota
|
||||
db.delete(cota)
|
||||
db.commit()
|
||||
flash('Cota excluída com sucesso!', 'success')
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao excluir cota. Por favor, tente novamente.', 'danger')
|
||||
print(f"Erro ao excluir cota: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
return redirect(url_for('cota.listar'))
|
||||
@@ -1,184 +1,80 @@
|
||||
from flask import Blueprint, render_template, flash, redirect, url_for, jsonify
|
||||
from functions.database import get_db_session, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
|
||||
from functions.decorators import require_login
|
||||
from flask import session, render_template
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
from services.dashboard_service import DashboardService
|
||||
from services.cache_service import cache_service, CacheKeys
|
||||
from flask_login import current_user
|
||||
import logging
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from models.entities.militante import Militante
|
||||
from models.entities.cota_mensal import CotaMensal
|
||||
from models.entities.material_vendido import MaterialVendido
|
||||
from models.entities.assinatura_anual import AssinaturaAnual
|
||||
from models.entities.pagamento import Pagamento
|
||||
from models.entities.tipo_pagamento import TipoPagamento
|
||||
from models.entities.usuario import Usuario
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
home_bp = Blueprint('home', __name__)
|
||||
class HomeController:
|
||||
"""Controlador para página inicial e dashboard"""
|
||||
|
||||
@home_bp.route("/")
|
||||
@require_login
|
||||
def index():
|
||||
"""Rota principal"""
|
||||
return redirect(url_for('home.dashboard'))
|
||||
|
||||
@home_bp.route("/dashboard")
|
||||
@home_bp.route("/home")
|
||||
@require_login
|
||||
def dashboard():
|
||||
"""Página inicial do sistema com dashboard"""
|
||||
try:
|
||||
# Get dashboard stats from cached service
|
||||
stats = DashboardService.get_dashboard_stats()
|
||||
|
||||
# Get tipos de pagamento for the modal
|
||||
db = get_db_session()
|
||||
@staticmethod
|
||||
def dashboard():
|
||||
"""Gera dados para o dashboard principal"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
# Buscar nome do usuário
|
||||
usuario = db.query(Usuario).get(session.get('user_id'))
|
||||
nome_usuario = usuario.username if usuario else "Usuário"
|
||||
|
||||
# Formatar data atual em português
|
||||
data_atual = datetime.now().strftime("%d de %B de %Y")
|
||||
|
||||
# Buscar dados para o dashboard
|
||||
total_militantes = db.query(Militante).count()
|
||||
total_cotas = db.query(func.sum(CotaMensal.valor_novo)).scalar() or 0
|
||||
total_materiais = db.query(MaterialVendido).count()
|
||||
total_assinaturas = db.query(AssinaturaAnual).count()
|
||||
|
||||
# Buscar últimos militantes cadastrados
|
||||
ultimos_militantes = db.query(Militante)\
|
||||
.order_by(Militante.id.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
# Buscar últimos pagamentos
|
||||
ultimos_pagamentos = db.query(Pagamento)\
|
||||
.join(Militante)\
|
||||
.order_by(Pagamento.data_pagamento.desc())\
|
||||
.limit(5)\
|
||||
.all()
|
||||
|
||||
# Buscar tipos de pagamento
|
||||
tipos_pagamento = db.query(TipoPagamento).all()
|
||||
|
||||
return {
|
||||
'nome_usuario': nome_usuario,
|
||||
'data_atual': data_atual,
|
||||
'total_militantes': total_militantes,
|
||||
'total_cotas': "{:.2f}".format(total_cotas),
|
||||
'total_materiais': total_materiais,
|
||||
'total_assinaturas': total_assinaturas,
|
||||
'ultimos_militantes': ultimos_militantes,
|
||||
'ultimos_pagamentos': ultimos_pagamentos,
|
||||
'tipos_pagamento': tipos_pagamento
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao carregar dashboard: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return {
|
||||
'nome_usuario': "Usuário",
|
||||
'data_atual': datetime.now().strftime("%d/%m/%Y"),
|
||||
'total_militantes': 0,
|
||||
'total_cotas': "0.00",
|
||||
'total_materiais': 0,
|
||||
'total_assinaturas': 0,
|
||||
'ultimos_militantes': [],
|
||||
'ultimos_pagamentos': [],
|
||||
'tipos_pagamento': []
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return render_template('home.html',
|
||||
nome_usuario=current_user.nome or current_user.username,
|
||||
data_atual=datetime.now().strftime("%d/%m/%Y"),
|
||||
total_militantes=stats.get('total_militantes', 0),
|
||||
total_cotas=stats.get('total_cotas', "0.00"),
|
||||
total_materiais=stats.get('total_materiais', 0),
|
||||
total_assinaturas=stats.get('total_assinaturas', 0),
|
||||
ultimos_militantes=stats.get('ultimos_militantes', []),
|
||||
ultimos_pagamentos=stats.get('ultimos_pagamentos', []),
|
||||
tipos_pagamento=tipos_pagamento,
|
||||
Militante=Militante,
|
||||
cache_timestamp=stats.get('cache_timestamp'))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro na página inicial: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
flash('Erro ao carregar a página inicial', 'danger')
|
||||
return render_template('home.html',
|
||||
nome_usuario="Usuário",
|
||||
data_atual=datetime.now().strftime("%d/%m/%Y"),
|
||||
total_militantes=0,
|
||||
total_cotas="0.00",
|
||||
total_materiais=0,
|
||||
total_assinaturas=0,
|
||||
ultimos_militantes=[],
|
||||
ultimos_pagamentos=[],
|
||||
Militante=Militante)
|
||||
|
||||
@home_bp.route('/check_session')
|
||||
def check_session():
|
||||
"""Verifica se a sessão ainda é válida"""
|
||||
if current_user.is_authenticated:
|
||||
if current_user.is_session_expired():
|
||||
return jsonify({'valid': False, 'message': 'Sessão expirada'})
|
||||
return jsonify({'valid': True})
|
||||
return jsonify({'valid': False, 'message': 'Usuário não autenticado'})
|
||||
|
||||
@home_bp.route('/api/dashboard/stats')
|
||||
@require_login
|
||||
def api_dashboard_stats():
|
||||
"""API endpoint for dashboard statistics"""
|
||||
try:
|
||||
stats = DashboardService.get_dashboard_stats()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter estatísticas do dashboard: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao obter estatísticas'
|
||||
}), 500
|
||||
|
||||
@home_bp.route('/api/dashboard/militante-stats')
|
||||
@require_login
|
||||
def api_militante_stats():
|
||||
"""API endpoint for militante statistics"""
|
||||
try:
|
||||
stats = DashboardService.get_militante_stats()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter estatísticas de militantes: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao obter estatísticas de militantes'
|
||||
}), 500
|
||||
|
||||
@home_bp.route('/api/dashboard/financial-stats')
|
||||
@require_login
|
||||
def api_financial_stats():
|
||||
"""API endpoint for financial statistics"""
|
||||
try:
|
||||
stats = DashboardService.get_financial_stats()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter estatísticas financeiras: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao obter estatísticas financeiras'
|
||||
}), 500
|
||||
|
||||
@home_bp.route('/api/cache/clear')
|
||||
@require_login
|
||||
def clear_cache():
|
||||
"""Clear all cache (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Acesso negado'
|
||||
}), 403
|
||||
|
||||
try:
|
||||
cache_service.clear_all()
|
||||
# Invalidate dashboard cache
|
||||
DashboardService.invalidate_dashboard_cache()
|
||||
|
||||
logger.info(f"Cache limpo por {current_user.username}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Cache limpo com sucesso'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao limpar cache: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao limpar cache'
|
||||
}), 500
|
||||
|
||||
@home_bp.route('/api/cache/status')
|
||||
@require_login
|
||||
def cache_status():
|
||||
"""Get cache status (admin only)"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Acesso negado'
|
||||
}), 403
|
||||
|
||||
try:
|
||||
# Check if Redis is connected
|
||||
is_connected = cache_service._is_connected()
|
||||
|
||||
# Get some cache statistics
|
||||
stats = {
|
||||
'connected': is_connected,
|
||||
'dashboard_stats_cached': cache_service.exists(CacheKeys.DASHBOARD_STATS),
|
||||
'dashboard_stats_ttl': cache_service.ttl(CacheKeys.DASHBOARD_STATS) if is_connected else -1
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': stats
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao obter status do cache: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao obter status do cache'
|
||||
}), 500
|
||||
@@ -1,243 +0,0 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from functions.database import get_db_session, MaterialVendido, Militante, TipoMaterial
|
||||
from functions.decorators import require_login
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
from datetime import datetime
|
||||
from flask_login import current_user
|
||||
|
||||
material_bp = Blueprint('material', __name__)
|
||||
|
||||
@material_bp.route("/materiais")
|
||||
@require_login
|
||||
def listar():
|
||||
"""Lista todos os materiais com controle de permissões no nível de dados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
|
||||
materiais = []
|
||||
|
||||
# Verificar permissões para filtrar dados
|
||||
if current_user.is_admin:
|
||||
# Admin vê todos
|
||||
materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).order_by(MaterialVendido.data_venda.desc()).all()
|
||||
elif hasattr(current_user, 'has_permission'):
|
||||
# Outros usuários veem baseado nas suas permissões
|
||||
# Por enquanto, mostrar todos - pode ser refinado depois
|
||||
materiais = db.query(MaterialVendido).join(Militante).join(TipoMaterial).order_by(MaterialVendido.data_venda.desc()).all()
|
||||
|
||||
# Buscar tipos para o template
|
||||
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
|
||||
|
||||
# SEMPRE renderizar o template, independente das permissões
|
||||
return render_template('listar_materiais.html',
|
||||
materiais=materiais,
|
||||
tipos_materiais=tipos_materiais)
|
||||
except Exception as e:
|
||||
print(f"Erro no controller de materiais: {e}")
|
||||
# Em caso de erro, renderizar com dados vazios
|
||||
return render_template('listar_materiais.html',
|
||||
materiais=[],
|
||||
tipos_materiais=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@material_bp.route("/materiais/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def novo():
|
||||
"""Cria um novo material vendido"""
|
||||
if request.method == "POST":
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
tipo_material_id = request.form.get("tipo_material_id")
|
||||
descricao = request.form.get("descricao")
|
||||
valor = float(request.form.get("valor"))
|
||||
data_venda = converter_data(request.form.get("data_venda"))
|
||||
|
||||
if not validar_data(data_venda):
|
||||
flash('Data de venda inválida ou futura', 'danger')
|
||||
return redirect(url_for('material.novo'))
|
||||
|
||||
material = MaterialVendido(
|
||||
militante_id=militante_id,
|
||||
tipo_material_id=tipo_material_id,
|
||||
descricao=descricao,
|
||||
valor=valor,
|
||||
data_venda=data_venda
|
||||
)
|
||||
db.add(material)
|
||||
db.commit()
|
||||
flash('Material cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('material.listar'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao cadastrar material', 'danger')
|
||||
return redirect(url_for('material.novo'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# GET - Renderizar formulário
|
||||
db = get_db_session()
|
||||
try:
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
|
||||
return render_template("novo_material.html", militantes=militantes, tipos_material=tipos_material)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@material_bp.route('/materiais/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def editar(id):
|
||||
"""Edita um material vendido"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
material = db.query(MaterialVendido).get(id)
|
||||
if not material:
|
||||
flash('Material não encontrado.', 'danger')
|
||||
return redirect(url_for('material.listar'))
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
material.militante_id = request.form.get("militante_id")
|
||||
material.tipo_material_id = request.form.get("tipo_material_id")
|
||||
material.descricao = request.form.get("descricao")
|
||||
material.valor = float(request.form.get("valor"))
|
||||
material.data_venda = converter_data(request.form.get("data_venda"))
|
||||
|
||||
db.commit()
|
||||
flash('Material atualizado com sucesso!', 'success')
|
||||
return redirect(url_for('material.listar'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao atualizar material.', 'danger')
|
||||
print(f"Erro ao atualizar material: {e}")
|
||||
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
tipos_material = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
|
||||
return render_template("editar_material.html", material=material, militantes=militantes, tipos_material=tipos_material)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@material_bp.route("/materiais/excluir/<int:id>", methods=["POST"])
|
||||
@require_login
|
||||
def excluir(id):
|
||||
"""Exclui um material vendido"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
material = db.query(MaterialVendido).get(id)
|
||||
if not material:
|
||||
flash('Material não encontrado.', 'danger')
|
||||
return redirect(url_for('material.listar'))
|
||||
|
||||
db.delete(material)
|
||||
db.commit()
|
||||
flash('Material excluído com sucesso!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao excluir material.', 'danger')
|
||||
print(f"Erro ao excluir material: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return redirect(url_for('material.listar'))
|
||||
|
||||
@material_bp.route("/tipos-materiais")
|
||||
@require_login
|
||||
def listar_tipos():
|
||||
"""Lista todos os tipos de materiais com controle de permissões no nível de dados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
|
||||
tipos_materiais = []
|
||||
|
||||
# Verificar permissões para filtrar dados
|
||||
if current_user.is_admin:
|
||||
# Admin vê todos
|
||||
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
|
||||
elif hasattr(current_user, 'has_permission'):
|
||||
# Outros usuários veem baseado nas suas permissões
|
||||
# Por enquanto, mostrar todos - pode ser refinado depois
|
||||
tipos_materiais = db.query(TipoMaterial).order_by(TipoMaterial.descricao).all()
|
||||
|
||||
# SEMPRE renderizar o template, independente das permissões
|
||||
return render_template('listar_tipos_materiais.html', tipos_materiais=tipos_materiais)
|
||||
except Exception as e:
|
||||
print(f"Erro no controller de tipos de materiais: {e}")
|
||||
# Em caso de erro, renderizar com dados vazios
|
||||
return render_template('listar_tipos_materiais.html', tipos_materiais=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@material_bp.route("/tipos-materiais/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def novo_tipo():
|
||||
"""Cria um novo tipo de material"""
|
||||
if request.method == "POST":
|
||||
db = get_db_session()
|
||||
try:
|
||||
descricao = request.form.get("descricao")
|
||||
|
||||
tipo = TipoMaterial(descricao=descricao)
|
||||
db.add(tipo)
|
||||
db.commit()
|
||||
flash('Tipo de material cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('material.listar_tipos'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao cadastrar tipo de material', 'danger')
|
||||
return redirect(url_for('material.novo_tipo'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return render_template("novo_tipo_material.html")
|
||||
|
||||
@material_bp.route('/tipos-materiais/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def editar_tipo(id):
|
||||
"""Edita um tipo de material"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
tipo = db.query(TipoMaterial).get(id)
|
||||
if not tipo:
|
||||
flash('Tipo de material não encontrado.', 'danger')
|
||||
return redirect(url_for('material.listar_tipos'))
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
tipo.descricao = request.form.get("descricao")
|
||||
db.commit()
|
||||
flash('Tipo de material atualizado com sucesso!', 'success')
|
||||
return redirect(url_for('material.listar_tipos'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao atualizar tipo de material.', 'danger')
|
||||
print(f"Erro ao atualizar tipo de material: {e}")
|
||||
|
||||
return render_template("editar_tipo_material.html", tipo=tipo)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@material_bp.route("/tipos-materiais/excluir/<int:id>", methods=["POST"])
|
||||
@require_login
|
||||
def excluir_tipo(id):
|
||||
"""Exclui um tipo de material"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
tipo = db.query(TipoMaterial).get(id)
|
||||
if not tipo:
|
||||
flash('Tipo de material não encontrado.', 'danger')
|
||||
return redirect(url_for('material.listar_tipos'))
|
||||
|
||||
db.delete(tipo)
|
||||
db.commit()
|
||||
flash('Tipo de material excluído com sucesso!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao excluir tipo de material.', 'danger')
|
||||
print(f"Erro ao excluir tipo de material: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return redirect(url_for('material.listar_tipos'))
|
||||
@@ -1,293 +1,233 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from functions.database import get_db_session, Militante, EmailMilitante, Endereco, Celula, Setor, ComiteRegional
|
||||
from functions.decorators import require_login
|
||||
from functions.validations import validar_cpf
|
||||
from functions.rbac import Permission
|
||||
from utils.date_utils import validar_data, converter_data, calcular_idade
|
||||
from flask import request, jsonify, flash
|
||||
from datetime import datetime
|
||||
from sqlalchemy.orm import joinedload
|
||||
from flask_login import current_user
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
militante_bp = Blueprint('militante', __name__)
|
||||
from services.militante_service import MilitanteService
|
||||
from models.entities.militante import Militante, EstadoMilitante
|
||||
from models.entities.endereco import Endereco
|
||||
from models.entities.email_militante import EmailMilitante
|
||||
from utils.date_utils import validar_data, converter_data, validar_sequencia_datas, calcular_idade
|
||||
|
||||
@militante_bp.route("/militantes/criar", methods=["POST"])
|
||||
@require_login
|
||||
def criar():
|
||||
"""Cria um novo militante"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
data = request.get_json()
|
||||
class MilitanteController:
|
||||
"""Controlador para operações com militantes"""
|
||||
|
||||
# Validações básicas
|
||||
if not data.get('nome') or not data.get('cpf'):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Nome e CPF são obrigatórios'
|
||||
}), 400
|
||||
@staticmethod
|
||||
def listar_militantes():
|
||||
"""Lista todos os militantes"""
|
||||
return MilitanteService.listar_militantes()
|
||||
|
||||
if not validar_cpf(data['cpf']):
|
||||
@staticmethod
|
||||
def buscar_militante(militante_id):
|
||||
"""Busca um militante pelo ID"""
|
||||
militante = MilitanteService.buscar_militante(militante_id)
|
||||
if not militante:
|
||||
raise NotFound(f"Militante com ID {militante_id} não encontrado")
|
||||
return militante
|
||||
|
||||
@staticmethod
|
||||
def criar_militante(form_data):
|
||||
"""Cria um novo militante"""
|
||||
# Validar CPF
|
||||
from functions.validations import validar_cpf
|
||||
cpf = form_data.get('cpf')
|
||||
if not validar_cpf(cpf):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'CPF inválido'
|
||||
}), 400
|
||||
|
||||
# Verificar se CPF já existe
|
||||
if db.query(Militante).filter_by(cpf=data['cpf']).first():
|
||||
# Verificar se já existe militante com este CPF
|
||||
if MilitanteService.buscar_por_cpf(cpf):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'CPF já cadastrado'
|
||||
}), 400
|
||||
|
||||
# Criar endereço se fornecido
|
||||
endereco_id = None
|
||||
if data.get('endereco'):
|
||||
endereco = Endereco(**data['endereco'])
|
||||
db.add(endereco)
|
||||
db.flush()
|
||||
endereco_id = endereco.id
|
||||
|
||||
# Criar militante
|
||||
militante = Militante(
|
||||
nome=data['nome'],
|
||||
cpf=data['cpf'],
|
||||
titulo_eleitoral=data.get('titulo_eleitoral'),
|
||||
data_nascimento=converter_data(data.get('data_nascimento')) if data.get('data_nascimento') else None,
|
||||
data_entrada_oci=converter_data(data.get('data_entrada_oci')) if data.get('data_entrada_oci') else None,
|
||||
data_efetivacao_oci=converter_data(data.get('data_efetivacao_oci')) if data.get('data_efetivacao_oci') else None,
|
||||
telefone1=data.get('telefone1'),
|
||||
telefone2=data.get('telefone2'),
|
||||
profissao=data.get('profissao'),
|
||||
regime_trabalho=data.get('regime_trabalho'),
|
||||
empresa=data.get('empresa'),
|
||||
contratante=data.get('contratante'),
|
||||
instituicao_ensino=data.get('instituicao_ensino'),
|
||||
tipo_instituicao=data.get('tipo_instituicao'),
|
||||
sindicato=data.get('sindicato'),
|
||||
cargo_sindical=data.get('cargo_sindical'),
|
||||
dirigente_sindical=data.get('dirigente_sindical', False),
|
||||
central_sindical=data.get('central_sindical'),
|
||||
endereco_id=endereco_id,
|
||||
celula_id=data.get('celula_id'),
|
||||
registrado_por=current_user.id
|
||||
)
|
||||
|
||||
db.add(militante)
|
||||
db.flush()
|
||||
|
||||
# Criar email se fornecido
|
||||
if data.get('email'):
|
||||
email = EmailMilitante(
|
||||
militante_id=militante.id,
|
||||
endereco_email=data['email']
|
||||
try:
|
||||
# Criar endereço
|
||||
endereco = Endereco(
|
||||
cep=form_data.get('cep'),
|
||||
estado=form_data.get('estado'),
|
||||
cidade=form_data.get('cidade'),
|
||||
bairro=form_data.get('bairro'),
|
||||
rua=form_data.get('logradouro'),
|
||||
numero=form_data.get('numero'),
|
||||
complemento=form_data.get('complemento')
|
||||
)
|
||||
db.add(email)
|
||||
|
||||
db.commit()
|
||||
# Salvar endereço para obter ID
|
||||
endereco_id = MilitanteService.salvar_endereco(endereco)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante criado com sucesso',
|
||||
'militante_id': militante.id
|
||||
})
|
||||
# Processar datas
|
||||
data_nascimento = datetime.strptime(form_data.get('data_nascimento'), '%Y-%m-%d') if form_data.get('data_nascimento') else None
|
||||
data_entrada_oci = datetime.strptime(form_data.get('data_entrada_oci'), '%Y-%m-%d') if form_data.get('data_entrada_oci') else None
|
||||
data_efetivacao_oci = datetime.strptime(form_data.get('data_efetivacao_oci'), '%Y-%m-%d') if form_data.get('data_efetivacao_oci') else None
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao criar militante: {str(e)}'
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
# Criar militante
|
||||
militante = Militante(
|
||||
# Dados Básicos
|
||||
nome=form_data.get('nome'),
|
||||
cpf=cpf,
|
||||
titulo_eleitoral=form_data.get('titulo_eleitoral'),
|
||||
data_nascimento=data_nascimento,
|
||||
data_entrada_oci=data_entrada_oci,
|
||||
data_efetivacao_oci=data_efetivacao_oci,
|
||||
|
||||
@militante_bp.route("/militantes")
|
||||
@require_login
|
||||
def listar():
|
||||
"""Lista todos os militantes com controle de permissões no nível de dados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
|
||||
militantes = []
|
||||
# Contato
|
||||
telefone1=form_data.get('telefone1'),
|
||||
telefone2=form_data.get('telefone2'),
|
||||
endereco_id=endereco_id,
|
||||
|
||||
# Verificar permissões para filtrar dados
|
||||
if current_user.is_admin:
|
||||
# Admin vê todos
|
||||
militantes = db.query(Militante).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
elif hasattr(current_user, 'has_permission'):
|
||||
if current_user.has_permission(Permission.VIEW_CC_REPORTS):
|
||||
# CC vê todos
|
||||
militantes = db.query(Militante).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
elif current_user.has_permission(Permission.VIEW_CR_REPORTS):
|
||||
# CR vê do seu CR
|
||||
if hasattr(current_user, 'cr_id') and current_user.cr_id:
|
||||
militantes = db.query(Militante).join(Celula).join(Setor).filter(
|
||||
Setor.cr_id == current_user.cr_id
|
||||
).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
elif current_user.has_permission(Permission.VIEW_SECTOR_REPORTS):
|
||||
# Setor vê do seu setor
|
||||
if hasattr(current_user, 'setor_id') and current_user.setor_id:
|
||||
militantes = db.query(Militante).join(Celula).filter(
|
||||
Celula.setor_id == current_user.setor_id
|
||||
).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
elif current_user.has_permission(Permission.VIEW_CELL_DATA):
|
||||
# Célula vê da sua célula
|
||||
if hasattr(current_user, 'celula_id') and current_user.celula_id:
|
||||
militantes = db.query(Militante).filter(
|
||||
Militante.celula_id == current_user.celula_id
|
||||
).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
# Profissional
|
||||
profissao=form_data.get('profissao'),
|
||||
regime_trabalho=form_data.get('regime_trabalho'),
|
||||
empresa=form_data.get('empresa'),
|
||||
contratante=form_data.get('contratante'),
|
||||
|
||||
# Buscar dados auxiliares para o template
|
||||
celulas = db.query(Celula).all()
|
||||
setores = db.query(Setor).all()
|
||||
# Acadêmico
|
||||
instituicao_ensino=form_data.get('instituicao_ensino'),
|
||||
tipo_instituicao=form_data.get('tipo_instituicao'),
|
||||
|
||||
# SEMPRE renderizar o template, independente das permissões
|
||||
# O controle é feito no nível dos dados, não do template
|
||||
return render_template('listar_militantes.html',
|
||||
militantes=militantes,
|
||||
Militante=Militante,
|
||||
celulas=celulas,
|
||||
setores=setores)
|
||||
except Exception as e:
|
||||
print(f"Erro no controller de militantes: {e}")
|
||||
# Em caso de erro, renderizar com dados vazios
|
||||
return render_template('listar_militantes.html',
|
||||
militantes=[],
|
||||
Militante=Militante,
|
||||
celulas=[],
|
||||
setores=[])
|
||||
finally:
|
||||
db.close()
|
||||
# Sindical
|
||||
sindicato=form_data.get('sindicato'),
|
||||
cargo_sindical=form_data.get('cargo_sindical'),
|
||||
central_sindical=form_data.get('central_sindical'),
|
||||
dirigente_sindical=form_data.get('dirigente_sindical') == 'on',
|
||||
|
||||
@militante_bp.route("/militantes/excluir/<int:id>", methods=["POST"])
|
||||
@require_login
|
||||
def excluir(id):
|
||||
"""Exclui um militante"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).get(id)
|
||||
if not militante:
|
||||
flash('Militante não encontrado.', 'danger')
|
||||
return redirect(url_for('militante.listar'))
|
||||
# Organização
|
||||
estado=EstadoMilitante(form_data.get('estado', 'ATIVO')),
|
||||
celula_id=form_data.get('celula_id', type=int),
|
||||
responsabilidades=form_data.get('responsabilidades', type=int, default=0),
|
||||
|
||||
# Verificar permissões
|
||||
if not current_user.has_permission('gerenciar_militantes'):
|
||||
flash('Você não tem permissão para excluir militantes.', 'danger')
|
||||
return redirect(url_for('militante.listar'))
|
||||
# Por padrão, todo novo militante é aspirante
|
||||
aspirante=True,
|
||||
data_inicio_aspirante=datetime.now()
|
||||
)
|
||||
|
||||
db.delete(militante)
|
||||
db.commit()
|
||||
flash('Militante excluído com sucesso!', 'success')
|
||||
# Salvar militante para obter ID
|
||||
militante_id = MilitanteService.salvar_militante(militante)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao excluir militante.', 'danger')
|
||||
print(f"Erro ao excluir militante: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
# Adicionar email principal se fornecido
|
||||
email = form_data.get('email')
|
||||
if email:
|
||||
email_militante = EmailMilitante(
|
||||
endereco_email=email,
|
||||
militante_id=militante_id
|
||||
)
|
||||
MilitanteService.salvar_email_militante(email_militante)
|
||||
|
||||
return redirect(url_for('militante.listar'))
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante criado com sucesso!',
|
||||
'id': militante_id
|
||||
})
|
||||
|
||||
@militante_bp.route('/militantes/editar/<int:militante_id>', methods=['POST'])
|
||||
@require_login
|
||||
def editar(militante_id):
|
||||
"""Edita um militante existente"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
data = request.get_json()
|
||||
militante = db.query(Militante).get(militante_id)
|
||||
|
||||
if not militante:
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}), 404
|
||||
'message': f'Erro ao criar militante: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# Atualizar dados básicos
|
||||
militante.nome = data.get('nome', militante.nome)
|
||||
militante.cpf = data.get('cpf', militante.cpf)
|
||||
militante.titulo_eleitoral = data.get('titulo_eleitoral', militante.titulo_eleitoral)
|
||||
militante.telefone1 = data.get('telefone1', militante.telefone1)
|
||||
militante.telefone2 = data.get('telefone2', militante.telefone2)
|
||||
militante.profissao = data.get('profissao', militante.profissao)
|
||||
militante.regime_trabalho = data.get('regime_trabalho', militante.regime_trabalho)
|
||||
militante.empresa = data.get('empresa', militante.empresa)
|
||||
militante.contratante = data.get('contratante', militante.contratante)
|
||||
militante.instituicao_ensino = data.get('instituicao_ensino', militante.instituicao_ensino)
|
||||
militante.tipo_instituicao = data.get('tipo_instituicao', militante.tipo_instituicao)
|
||||
militante.sindicato = data.get('sindicato', militante.sindicato)
|
||||
militante.cargo_sindical = data.get('cargo_sindical', militante.cargo_sindical)
|
||||
militante.dirigente_sindical = data.get('dirigente_sindical', militante.dirigente_sindical)
|
||||
militante.central_sindical = data.get('central_sindical', militante.central_sindical)
|
||||
@staticmethod
|
||||
def atualizar_militante(militante_id, form_data):
|
||||
"""Atualiza um militante existente"""
|
||||
try:
|
||||
militante = MilitanteService.buscar_militante(militante_id)
|
||||
if not militante:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}), 404
|
||||
|
||||
# Atualizar datas
|
||||
if data.get('data_nascimento'):
|
||||
militante.data_nascimento = converter_data(data['data_nascimento'])
|
||||
if data.get('data_entrada_oci'):
|
||||
militante.data_entrada_oci = converter_data(data['data_entrada_oci'])
|
||||
if data.get('data_efetivacao_oci'):
|
||||
militante.data_efetivacao_oci = converter_data(data['data_efetivacao_oci'])
|
||||
# Obter dados do formulário
|
||||
nome = form_data.get('nome')
|
||||
cpf = form_data.get('cpf')
|
||||
titulo_eleitoral = form_data.get('titulo_eleitoral')
|
||||
data_nascimento = form_data.get('data_nascimento')
|
||||
data_entrada_oci = form_data.get('data_entrada_oci')
|
||||
data_efetivacao_oci = form_data.get('data_efetivacao_oci')
|
||||
telefone1 = form_data.get('telefone1')
|
||||
telefone2 = form_data.get('telefone2')
|
||||
email = form_data.get('email')
|
||||
|
||||
# Atualizar endereço
|
||||
if data.get('endereco') and militante.endereco:
|
||||
endereco = militante.endereco
|
||||
endereco.cep = data['endereco'].get('cep', endereco.cep)
|
||||
endereco.estado = data['endereco'].get('estado', endereco.estado)
|
||||
endereco.cidade = data['endereco'].get('cidade', endereco.cidade)
|
||||
endereco.bairro = data['endereco'].get('bairro', endereco.bairro)
|
||||
endereco.rua = data['endereco'].get('rua', endereco.rua)
|
||||
endereco.numero = data['endereco'].get('numero', endereco.numero)
|
||||
endereco.complemento = data['endereco'].get('complemento', endereco.complemento)
|
||||
# Validar e converter datas
|
||||
try:
|
||||
data_nascimento = converter_data(data_nascimento) if data_nascimento else None
|
||||
data_entrada_oci = converter_data(data_entrada_oci) if data_entrada_oci else None
|
||||
data_efetivacao_oci = converter_data(data_efetivacao_oci) if data_efetivacao_oci else None
|
||||
|
||||
# Atualizar email
|
||||
if data.get('email') and militante.emails:
|
||||
militante.emails[0].endereco_email = data['email']
|
||||
# Validar sequência lógica das datas
|
||||
validar_sequencia_datas(
|
||||
data_nascimento=data_nascimento,
|
||||
data_entrada=data_entrada_oci,
|
||||
data_efetivacao=data_efetivacao_oci
|
||||
)
|
||||
|
||||
db.commit()
|
||||
except ValueError as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante atualizado com sucesso'
|
||||
})
|
||||
# Atualizar dados básicos
|
||||
if nome: militante.nome = nome
|
||||
if cpf: militante.cpf = cpf
|
||||
if titulo_eleitoral: militante.titulo_eleitoral = titulo_eleitoral
|
||||
militante.data_nascimento = data_nascimento
|
||||
militante.data_entrada_oci = data_entrada_oci
|
||||
militante.data_efetivacao_oci = data_efetivacao_oci
|
||||
militante.telefone1 = telefone1
|
||||
militante.telefone2 = telefone2
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao atualizar militante: {str(e)}'
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
# Calcular idade
|
||||
if data_nascimento:
|
||||
militante.idade = calcular_idade(data_nascimento)
|
||||
|
||||
@militante_bp.route("/militantes/dados/<int:militante_id>")
|
||||
@require_login
|
||||
def buscar_dados(militante_id):
|
||||
"""Busca os dados de um militante específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco)
|
||||
).get(militante_id)
|
||||
# Atualizar ou criar email
|
||||
if email:
|
||||
MilitanteService.atualizar_email_militante(militante_id, email)
|
||||
|
||||
# Salvar alterações
|
||||
MilitanteService.salvar_militante(militante)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Militante atualizado com sucesso',
|
||||
'data': {
|
||||
'nome': militante.nome,
|
||||
'cpf': militante.cpf,
|
||||
'idade': militante.idade if hasattr(militante, 'idade') else None,
|
||||
'emails': [e.endereco_email for e in militante.emails],
|
||||
'telefone1': militante.telefone1,
|
||||
'celula_id': str(militante.celula_id) if militante.celula_id else None,
|
||||
'responsabilidades_valor': militante.responsabilidades
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao atualizar militante: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@staticmethod
|
||||
def excluir_militante(militante_id):
|
||||
"""Exclui um militante"""
|
||||
try:
|
||||
if MilitanteService.excluir_militante(militante_id):
|
||||
flash('Militante excluído com sucesso!', 'success')
|
||||
return True
|
||||
else:
|
||||
flash('Militante não encontrado', 'danger')
|
||||
return False
|
||||
except Exception as e:
|
||||
flash(f'Erro ao excluir militante: {str(e)}', 'danger')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def buscar_dados_militante(militante_id):
|
||||
"""Busca os dados de um militante específico"""
|
||||
militante = MilitanteService.buscar_militante(militante_id)
|
||||
if not militante:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
@@ -304,62 +244,27 @@ def buscar_dados(militante_id):
|
||||
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
|
||||
return None
|
||||
|
||||
# Preparar dados para retorno
|
||||
dados = {
|
||||
# Formatar datas com validação
|
||||
data_nascimento = formatar_data_segura(militante.data_nascimento)
|
||||
data_entrada_oci = formatar_data_segura(militante.data_entrada_oci)
|
||||
data_efetivacao_oci = formatar_data_segura(militante.data_efetivacao_oci)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'id': militante.id,
|
||||
'nome': militante.nome,
|
||||
'cpf': militante.cpf,
|
||||
'titulo_eleitoral': militante.titulo_eleitoral,
|
||||
'data_nascimento': formatar_data_segura(militante.data_nascimento),
|
||||
'data_entrada_oci': formatar_data_segura(militante.data_entrada_oci),
|
||||
'data_efetivacao_oci': formatar_data_segura(militante.data_efetivacao_oci),
|
||||
'data_nascimento': data_nascimento,
|
||||
'data_entrada_oci': data_entrada_oci,
|
||||
'data_efetivacao_oci': data_efetivacao_oci,
|
||||
'emails': [email.endereco_email for email in militante.emails] if militante.emails else [],
|
||||
'telefone1': militante.telefone1,
|
||||
'telefone2': militante.telefone2,
|
||||
'profissao': militante.profissao,
|
||||
'regime_trabalho': militante.regime_trabalho,
|
||||
'empresa': militante.empresa,
|
||||
'contratante': militante.contratante,
|
||||
'instituicao_ensino': militante.instituicao_ensino,
|
||||
'tipo_instituicao': militante.tipo_instituicao,
|
||||
'celula_id': militante.celula_id,
|
||||
'responsabilidades_valor': militante.responsabilidades,
|
||||
'sindicato': militante.sindicato,
|
||||
'cargo_sindical': militante.cargo_sindical,
|
||||
'dirigente_sindical': militante.dirigente_sindical,
|
||||
'central_sindical': militante.central_sindical,
|
||||
'responsabilidades': militante.responsabilidades,
|
||||
'estado': militante.estado.value if militante.estado else None,
|
||||
'celula_id': militante.celula_id,
|
||||
'email': militante.emails[0].endereco_email if militante.emails else None,
|
||||
'endereco': {
|
||||
'cep': militante.endereco.cep if militante.endereco else None,
|
||||
'estado': militante.endereco.estado if militante.endereco else None,
|
||||
'cidade': militante.endereco.cidade if militante.endereco else None,
|
||||
'bairro': militante.endereco.bairro if militante.endereco else None,
|
||||
'rua': militante.endereco.rua if militante.endereco else None,
|
||||
'numero': militante.endereco.numero if militante.endereco else None,
|
||||
'complemento': militante.endereco.complemento if militante.endereco else None
|
||||
} if militante.endereco else None
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': dados
|
||||
'dirigente_sindical': militante.dirigente_sindical
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'Erro ao buscar dados: {str(e)}'
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@militante_bp.route("/api/setores/<int:cr_id>")
|
||||
@require_login
|
||||
def get_setores(cr_id):
|
||||
"""Retorna setores de um CR específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
setores = db.query(Setor).filter_by(cr_id=cr_id).all()
|
||||
return jsonify([{'id': s.id, 'nome': s.nome} for s in setores])
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,209 +0,0 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from functions.database import get_db_session, Pagamento, Militante, TipoPagamento
|
||||
from functions.decorators import require_login
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
from datetime import datetime
|
||||
from flask_login import current_user
|
||||
|
||||
pagamento_bp = Blueprint('pagamento', __name__)
|
||||
|
||||
@pagamento_bp.route("/pagamentos/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def novo():
|
||||
"""Cria um novo pagamento"""
|
||||
if request.method == "POST":
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
|
||||
valor = float(request.form.get("valor"))
|
||||
data_pagamento = converter_data(request.form.get("data_pagamento"))
|
||||
|
||||
if not validar_data(data_pagamento):
|
||||
flash('Data de pagamento inválida ou futura', 'danger')
|
||||
return redirect(url_for('pagamento.novo'))
|
||||
|
||||
pagamento = Pagamento(
|
||||
militante_id=militante_id,
|
||||
tipo_pagamento_id=tipo_pagamento_id,
|
||||
valor=valor,
|
||||
data_pagamento=data_pagamento
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
flash('Pagamento cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao cadastrar pagamento', 'danger')
|
||||
return redirect(url_for('pagamento.novo'))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
# GET - Renderizar formulário
|
||||
db = get_db_session()
|
||||
try:
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
tipos_pagamento = db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
|
||||
return render_template("novo_pagamento.html", militantes=militantes, tipos_pagamento=tipos_pagamento)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pagamento_bp.route("/pagamentos")
|
||||
@require_login
|
||||
def listar():
|
||||
"""Lista todos os pagamentos com controle de permissões no nível de dados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# SEMPRE renderizar o template, mas filtrar os dados baseado nas permissões
|
||||
pagamentos = []
|
||||
|
||||
# Verificar permissões para filtrar dados
|
||||
if current_user.is_admin:
|
||||
# Admin vê todos
|
||||
pagamentos = db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all()
|
||||
elif hasattr(current_user, 'has_permission'):
|
||||
# Outros usuários veem baseado nas suas permissões
|
||||
# Por enquanto, deixar vazio até implementar a lógica específica
|
||||
pagamentos = []
|
||||
|
||||
# Buscar dados auxiliares para o template
|
||||
militantes = db.query(Militante).order_by(Militante.nome).all()
|
||||
|
||||
# SEMPRE renderizar o template, independente das permissões
|
||||
return render_template("listar_pagamentos.html",
|
||||
pagamentos=pagamentos,
|
||||
militantes=militantes)
|
||||
except Exception as e:
|
||||
print(f"Erro no controller de pagamentos: {e}")
|
||||
# Em caso de erro, renderizar com dados vazios
|
||||
return render_template("listar_pagamentos.html",
|
||||
pagamentos=[],
|
||||
militantes=[])
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pagamento_bp.route("/pagamentos/adicionar", methods=["POST"])
|
||||
@require_login
|
||||
def adicionar():
|
||||
"""Adiciona um novo pagamento"""
|
||||
if request.method == "POST":
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante_id = request.form.get("militante_id")
|
||||
tipo_pagamento = request.form.get("tipo_pagamento")
|
||||
valor = float(request.form.get("valor"))
|
||||
data_pagamento = converter_data(request.form.get("data_pagamento"))
|
||||
|
||||
pagamento = Pagamento(
|
||||
militante_id=militante_id,
|
||||
tipo_pagamento=tipo_pagamento,
|
||||
valor=valor,
|
||||
data_pagamento=data_pagamento
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
flash('Pagamento adicionado com sucesso!', 'success')
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash(f'Erro ao adicionar pagamento: {str(e)}', 'danger')
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
|
||||
@pagamento_bp.route('/celulas/<int:celula_id>/pagamentos')
|
||||
@require_login
|
||||
def list_pagamentos_celula(celula_id):
|
||||
"""Lista pagamentos de uma célula específica"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamentos = db.query(Pagamento).filter_by(celula_id=celula_id).all()
|
||||
return render_template('pagamentos/list.html', pagamentos=pagamentos)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pagamento_bp.route('/setores/<int:setor_id>/pagamentos')
|
||||
@require_login
|
||||
def list_pagamentos_setor(setor_id):
|
||||
"""Lista pagamentos de um setor específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
|
||||
return render_template('pagamentos/list.html', pagamentos=pagamentos)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pagamento_bp.route('/crs/<int:cr_id>/pagamentos')
|
||||
@require_login
|
||||
def list_pagamentos_cr(cr_id):
|
||||
"""Lista pagamentos de um CR específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamentos = db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
|
||||
return render_template('pagamentos/list.html', pagamentos=pagamentos)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@pagamento_bp.route('/celulas/<int:celula_id>/pagamentos/novo', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def novo_pagamento_celula(celula_id):
|
||||
"""Cria novo pagamento para uma célula"""
|
||||
if request.method == 'POST':
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = Pagamento(
|
||||
valor=request.form['valor'],
|
||||
data=request.form['data'],
|
||||
militante_id=request.form['militante_id'],
|
||||
celula_id=celula_id
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
flash('Pagamento registrado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.list_pagamentos_celula', celula_id=celula_id))
|
||||
finally:
|
||||
db.close()
|
||||
return render_template('pagamentos/form.html')
|
||||
|
||||
@pagamento_bp.route('/setores/<int:setor_id>/pagamentos/novo', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def novo_pagamento_setor(setor_id):
|
||||
"""Cria novo pagamento para um setor"""
|
||||
if request.method == 'POST':
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = Pagamento(
|
||||
valor=request.form['valor'],
|
||||
data=request.form['data'],
|
||||
militante_id=request.form['militante_id'],
|
||||
setor_id=setor_id
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
flash('Pagamento registrado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.list_pagamentos_setor', setor_id=setor_id))
|
||||
finally:
|
||||
db.close()
|
||||
return render_template('pagamentos/form.html')
|
||||
|
||||
@pagamento_bp.route('/crs/<int:cr_id>/pagamentos/novo', methods=['GET', 'POST'])
|
||||
@require_login
|
||||
def novo_pagamento_cr(cr_id):
|
||||
"""Cria novo pagamento para um CR"""
|
||||
if request.method == 'POST':
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = Pagamento(
|
||||
valor=request.form['valor'],
|
||||
data=request.form['data'],
|
||||
militante_id=request.form['militante_id'],
|
||||
cr_id=cr_id
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
flash('Pagamento registrado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.list_pagamentos_cr', cr_id=cr_id))
|
||||
finally:
|
||||
db.close()
|
||||
return render_template('pagamentos/form.html')
|
||||
@@ -1,71 +1,124 @@
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash, jsonify
|
||||
from functions.database import get_db_session, Usuario, Role, Setor
|
||||
from functions.decorators import require_login
|
||||
from flask import request, jsonify, flash, session
|
||||
from flask_login import current_user
|
||||
from datetime import datetime
|
||||
import secrets
|
||||
import pyotp
|
||||
|
||||
usuario_bp = Blueprint('usuario', __name__)
|
||||
from models.entities.usuario import Usuario
|
||||
from services.usuario_service import UsuarioService
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
@usuario_bp.route("/usuarios/novo", methods=["GET", "POST"])
|
||||
@require_login
|
||||
def novo():
|
||||
"""Cria um novo usuário"""
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
email = request.form.get("email")
|
||||
role_id = request.form.get("role_id")
|
||||
setor_id = request.form.get("setor_id")
|
||||
class UsuarioController:
|
||||
"""Controlador para operações com usuários"""
|
||||
|
||||
@staticmethod
|
||||
def listar_usuarios():
|
||||
"""Lista todos os usuários do sistema"""
|
||||
return UsuarioService.listar_usuarios()
|
||||
|
||||
@staticmethod
|
||||
def buscar_usuario(user_id):
|
||||
"""Busca um usuário pelo ID"""
|
||||
return UsuarioService.buscar_usuario(user_id)
|
||||
|
||||
@staticmethod
|
||||
def criar_usuario(data):
|
||||
"""Cria um novo usuário"""
|
||||
# Verificar campos obrigatórios
|
||||
required_fields = ['username', 'password', 'email']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
flash(f'Campo {field} é obrigatório.', 'danger')
|
||||
return False
|
||||
|
||||
# Verificar se usuário já existe
|
||||
db = get_db_session()
|
||||
if UsuarioService.buscar_por_username(data['username']):
|
||||
flash('Nome de usuário já existe.', 'danger')
|
||||
return False
|
||||
|
||||
try:
|
||||
if db.query(Usuario).filter_by(username=username).first():
|
||||
flash('Nome de usuário já existe.', 'danger')
|
||||
return render_template("novo_usuario.html")
|
||||
|
||||
novo_usuario = Usuario(
|
||||
username=username,
|
||||
email=email,
|
||||
role_id=role_id,
|
||||
setor_id=setor_id
|
||||
# Criar usuário
|
||||
usuario = Usuario(
|
||||
username=data['username'],
|
||||
email=data['email'],
|
||||
nome=data.get('nome'),
|
||||
is_admin=data.get('is_admin', False)
|
||||
)
|
||||
novo_usuario.set_password(password)
|
||||
novo_usuario.otp_secret = pyotp.random_base32()
|
||||
|
||||
db.add(novo_usuario)
|
||||
db.commit()
|
||||
flash('Usuário cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('usuario.listar'))
|
||||
# Definir senha
|
||||
usuario.set_password(data['password'])
|
||||
|
||||
# Gerar OTP secret
|
||||
usuario.otp_secret = pyotp.random_base32()
|
||||
|
||||
# Definir outros campos
|
||||
if 'role_id' in data:
|
||||
usuario.role_id = data['role_id']
|
||||
|
||||
if 'setor_id' in data:
|
||||
usuario.setor_id = data['setor_id']
|
||||
|
||||
# Salvar no banco
|
||||
result = UsuarioService.salvar_usuario(usuario)
|
||||
|
||||
if result:
|
||||
flash('Usuário cadastrado com sucesso!', 'success')
|
||||
return True
|
||||
else:
|
||||
flash('Erro ao cadastrar usuário.', 'danger')
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao cadastrar usuário: {e}")
|
||||
flash('Erro ao cadastrar usuário', 'danger')
|
||||
return render_template("novo_usuario.html")
|
||||
finally:
|
||||
db.close()
|
||||
flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
|
||||
return False
|
||||
|
||||
db = get_db_session()
|
||||
try:
|
||||
roles = db.query(Role).order_by(Role.nome).all()
|
||||
setores = db.query(Setor).order_by(Setor.nome).all()
|
||||
return render_template("novo_usuario.html", roles=roles, setores=setores)
|
||||
finally:
|
||||
db.close()
|
||||
@staticmethod
|
||||
def atualizar_usuario(user_id, data):
|
||||
"""Atualiza um usuário existente"""
|
||||
usuario = UsuarioService.buscar_usuario(user_id)
|
||||
if not usuario:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return False
|
||||
|
||||
@usuario_bp.route('/usuarios/<int:user_id>/toggle_status', methods=['POST'])
|
||||
@require_login
|
||||
def toggle_status(user_id):
|
||||
"""Ativa/desativa um usuário"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Você não tem permissão para alterar o status de usuários.'
|
||||
}), 403
|
||||
# Atualizar campos
|
||||
if 'email' in data:
|
||||
usuario.email = data['email']
|
||||
|
||||
db = get_db_session()
|
||||
try:
|
||||
usuario = db.query(Usuario).get(user_id)
|
||||
if 'nome' in data:
|
||||
usuario.nome = data['nome']
|
||||
|
||||
if 'role_id' in data:
|
||||
usuario.role_id = data['role_id']
|
||||
|
||||
if 'setor_id' in data:
|
||||
usuario.setor_id = data['setor_id']
|
||||
|
||||
if 'is_admin' in data:
|
||||
usuario.is_admin = data['is_admin']
|
||||
|
||||
if 'password' in data and data['password']:
|
||||
usuario.set_password(data['password'])
|
||||
|
||||
# Salvar alterações
|
||||
result = UsuarioService.salvar_usuario(usuario)
|
||||
|
||||
if result:
|
||||
flash('Usuário atualizado com sucesso!', 'success')
|
||||
return True
|
||||
else:
|
||||
flash('Erro ao atualizar usuário.', 'danger')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def toggle_status(user_id):
|
||||
"""Ativa/desativa um usuário"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Você não tem permissão para alterar o status de usuários.'
|
||||
}), 403
|
||||
|
||||
usuario = UsuarioService.buscar_usuario(user_id)
|
||||
if not usuario:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
@@ -73,112 +126,77 @@ def toggle_status(user_id):
|
||||
}), 404
|
||||
|
||||
usuario.ativo = not usuario.ativo
|
||||
db.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Usuário {"ativado" if usuario.ativo else "desativado"} com sucesso!'
|
||||
})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@usuario_bp.route('/usuarios/<int:user_id>/alterar_nivel', methods=['POST'])
|
||||
@require_login
|
||||
def alterar_nivel(user_id):
|
||||
"""Altera o nível de acesso de um usuário"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Você não tem permissão para alterar níveis de usuários.'
|
||||
}), 403
|
||||
|
||||
novo_nivel = request.form.get('novo_nivel')
|
||||
if not novo_nivel:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Novo nível não especificado.'
|
||||
}), 400
|
||||
|
||||
db = get_db_session()
|
||||
try:
|
||||
usuario = db.query(Usuario).get(user_id)
|
||||
if not usuario:
|
||||
if UsuarioService.salvar_usuario(usuario):
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usuário não encontrado.'
|
||||
}), 404
|
||||
|
||||
# Buscar role pelo nível
|
||||
role = db.query(Role).filter_by(nivel=int(novo_nivel)).first()
|
||||
if not role:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Nível de acesso inválido.'
|
||||
}), 400
|
||||
|
||||
# Limpar roles existentes e adicionar nova
|
||||
usuario.roles.clear()
|
||||
usuario.roles.append(role)
|
||||
db.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Nível de acesso alterado para {role.nome} com sucesso!'
|
||||
})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@usuario_bp.route('/usuarios/<int:user_id>/toggle_quadro_orientador', methods=['POST'])
|
||||
@require_login
|
||||
def toggle_quadro_orientador(user_id):
|
||||
"""Ativa/desativa quadro orientador para um usuário"""
|
||||
if not current_user.is_admin:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Você não tem permissão para alterar responsabilidades de usuários.'
|
||||
}), 403
|
||||
|
||||
db = get_db_session()
|
||||
try:
|
||||
usuario = db.query(Usuario).get(user_id)
|
||||
if not usuario:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Usuário não encontrado.'
|
||||
}), 404
|
||||
|
||||
# Toggle quadro orientador
|
||||
if usuario.quadro_orientador:
|
||||
usuario.quadro_orientador = False
|
||||
message = 'Quadro Orientador desativado com sucesso!'
|
||||
'success': True,
|
||||
'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
|
||||
})
|
||||
else:
|
||||
usuario.quadro_orientador = True
|
||||
message = 'Quadro Orientador ativado com sucesso!'
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Erro ao salvar alterações.'
|
||||
}), 500
|
||||
|
||||
db.commit()
|
||||
@staticmethod
|
||||
def reset_password(user_id):
|
||||
"""Reseta a senha de um usuário"""
|
||||
usuario = UsuarioService.buscar_usuario(user_id)
|
||||
if not usuario:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return False, None
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': message,
|
||||
'quadro_orientador': usuario.quadro_orientador
|
||||
})
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
}), 500
|
||||
finally:
|
||||
db.close()
|
||||
# Gerar nova senha
|
||||
new_password = secrets.token_urlsafe(8)
|
||||
usuario.set_password(new_password)
|
||||
|
||||
# Salvar alterações
|
||||
if UsuarioService.salvar_usuario(usuario):
|
||||
return True, new_password
|
||||
else:
|
||||
flash('Erro ao resetar senha.', 'danger')
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def reset_otp(user_id):
|
||||
"""Reseta o OTP de um usuário"""
|
||||
usuario = UsuarioService.buscar_usuario(user_id)
|
||||
if not usuario:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return False
|
||||
|
||||
# Gerar novo OTP secret
|
||||
usuario.otp_secret = pyotp.random_base32()
|
||||
|
||||
# Salvar alterações
|
||||
if UsuarioService.salvar_usuario(usuario):
|
||||
flash(f'OTP resetado com sucesso para {usuario.email}.', 'success')
|
||||
return True
|
||||
else:
|
||||
flash('Erro ao resetar OTP.', 'danger')
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def check_session():
|
||||
"""Verifica status da sessão"""
|
||||
if 'user_id' not in session:
|
||||
return {'status': 'expired'}
|
||||
|
||||
if 'last_activity' in session:
|
||||
last_activity = datetime.fromtimestamp(session['last_activity'])
|
||||
now = datetime.now()
|
||||
|
||||
if now - last_activity > timedelta(hours=2):
|
||||
# Registrar o logout por timeout
|
||||
try:
|
||||
user = UsuarioService.buscar_usuario(session.get('user_id'))
|
||||
if user:
|
||||
user.ultimo_logout = datetime.now()
|
||||
user.motivo_logout = "Timeout de sessão"
|
||||
UsuarioService.salvar_usuario(user)
|
||||
except Exception as e:
|
||||
print(f"Erro ao registrar logout por timeout: {e}")
|
||||
|
||||
session.clear()
|
||||
return {'status': 'expired'}
|
||||
|
||||
return {'status': 'active'}
|
||||
|
||||
151
create_admin.py
Normal file
151
create_admin.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from functions.database import init_database, Usuario, Role, get_db_connection
|
||||
import qrcode
|
||||
import os
|
||||
from pathlib import Path
|
||||
import pyotp
|
||||
|
||||
def generate_qr_code(user):
|
||||
"""
|
||||
Gera o QR code para um usuário específico
|
||||
|
||||
Args:
|
||||
user: Instância do modelo Usuario
|
||||
|
||||
Returns:
|
||||
Path: Caminho do arquivo QR code gerado
|
||||
"""
|
||||
# Gerar QR Code apenas na raiz do projeto
|
||||
qr_path = Path('admin_qr.png')
|
||||
|
||||
# Remover arquivo antigo se existir
|
||||
if qr_path.exists():
|
||||
os.remove(str(qr_path))
|
||||
|
||||
# Gerar e salvar QR Code
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
|
||||
# Gerar URI do OTP
|
||||
totp = pyotp.TOTP(user.otp_secret)
|
||||
otp_uri = totp.provisioning_uri(
|
||||
name=user.username,
|
||||
issuer_name="Sistema de Controles"
|
||||
)
|
||||
|
||||
qr.add_data(otp_uri)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img.save(str(qr_path))
|
||||
|
||||
print(f"\nQR Code gerado em: {os.path.abspath(qr_path)}")
|
||||
|
||||
return qr_path, otp_uri
|
||||
|
||||
def create_admin_user():
|
||||
"""Cria ou atualiza o usuário admin"""
|
||||
try:
|
||||
# Inicializar banco de dados
|
||||
init_database()
|
||||
|
||||
# Criar sessão
|
||||
db = get_db_connection()
|
||||
|
||||
try:
|
||||
# Verificar se já existe um usuário admin
|
||||
admin = db.query(Usuario).filter_by(username="admin").first()
|
||||
|
||||
if admin:
|
||||
print("\n=== Usuário Admin Encontrado ===")
|
||||
if not admin.otp_secret:
|
||||
print("Gerando novo segredo OTP...")
|
||||
admin.generate_otp_secret()
|
||||
db.commit()
|
||||
else:
|
||||
print("\n=== Criando Novo Usuário Admin ===")
|
||||
# Criar novo usuário admin
|
||||
admin = Usuario(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
is_admin=True
|
||||
)
|
||||
admin.set_password("admin123")
|
||||
admin.generate_otp_secret()
|
||||
|
||||
# Buscar ou criar role de admin
|
||||
admin_role = db.query(Role).filter_by(nome="admin").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="admin", nivel=0) # Nível 0 é o mais alto
|
||||
db.add(admin_role)
|
||||
|
||||
# Adicionar role ao usuário
|
||||
admin.roles.append(admin_role)
|
||||
|
||||
# Adicionar e fazer commit
|
||||
db.add(admin)
|
||||
db.commit()
|
||||
|
||||
# Gerar QR code apenas se solicitado ou se for novo usuário
|
||||
if not os.path.exists('admin_qr.png'):
|
||||
qr_path, otp_uri = generate_qr_code(admin)
|
||||
print("\n=== QR Code Gerado ===")
|
||||
print(f"QR Code salvo em: {qr_path}")
|
||||
print(f"URI do OTP: {otp_uri}")
|
||||
else:
|
||||
print("\n=== QR Code Existente ===")
|
||||
print("Usando QR Code existente em: admin_qr.png")
|
||||
qr_path = 'admin_qr.png'
|
||||
|
||||
# Mostrar informações
|
||||
print("\n=== Informações do Admin ===")
|
||||
print(f"Username: {admin.username}")
|
||||
print(f"Email: {admin.email}")
|
||||
print(f"Senha: admin123")
|
||||
print(f"Segredo OTP: {admin.otp_secret}")
|
||||
|
||||
# Gerar código atual para verificação
|
||||
totp = pyotp.TOTP(admin.otp_secret)
|
||||
current_code = totp.now()
|
||||
print("\n=== Verificação do OTP ===")
|
||||
print(f"Código OTP atual: {current_code}")
|
||||
print(f"Verificação do código: {totp.verify(current_code)}")
|
||||
|
||||
print("\n=== Instruções para Configuração ===")
|
||||
print("1. Instale um aplicativo autenticador no seu celular")
|
||||
print(" (Google Authenticator, Microsoft Authenticator, etc)")
|
||||
print("2. Abra o aplicativo")
|
||||
print("3. Selecione a opção para adicionar uma nova conta")
|
||||
print("4. Escaneie o QR Code salvo em:", qr_path)
|
||||
print("\nOU configure manualmente:")
|
||||
print(f"- Nome da conta: {admin.username}")
|
||||
print(f"- Segredo: {admin.otp_secret}")
|
||||
print("- Tipo: Baseado em tempo (TOTP)")
|
||||
print("- Algoritmo: SHA1")
|
||||
print("- Dígitos: 6")
|
||||
print("- Intervalo: 30 segundos")
|
||||
|
||||
# Verificação final
|
||||
print("\n=== Teste de Verificação ===")
|
||||
test_code = totp.now()
|
||||
print(f"Código de teste: {test_code}")
|
||||
is_valid = admin.verify_otp(test_code)
|
||||
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
|
||||
|
||||
if not is_valid:
|
||||
print("\nALERTA: Verificação do OTP falhou!")
|
||||
print("Por favor, verifique se o segredo OTP está correto.")
|
||||
|
||||
# Fazer commit final para garantir que tudo foi salvo
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
except Exception as e:
|
||||
print(f"\nErro durante a execução: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_admin_user()
|
||||
@@ -1,9 +1,9 @@
|
||||
from functions.database import get_db_session, Usuario, Role
|
||||
from functions.database import get_db_connection, Usuario, Role
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
def create_test_users():
|
||||
"""Cria usuários de teste"""
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Lista de usuários de teste
|
||||
test_users = [
|
||||
@@ -1,54 +0,0 @@
|
||||
services:
|
||||
# Redis Cache Service
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: controles_redis
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- controles_network
|
||||
|
||||
# Flask Application
|
||||
app:
|
||||
build: .
|
||||
container_name: controles_app
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_ENV=production
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- DATABASE_URL=sqlite:////data/database.db
|
||||
|
||||
# DEV apenas para facilitar testes, não deve ser usado em produção
|
||||
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
|
||||
|
||||
# Produção definir em .env
|
||||
#- ADMIN_OTP_SECRET=${ADMIN_OTP_SECRET}
|
||||
volumes:
|
||||
- app_data:/data
|
||||
- app_logs:/app/logs
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- controles_network
|
||||
|
||||
networks:
|
||||
controles_network:
|
||||
driver: bridge
|
||||
@@ -1,62 +1,14 @@
|
||||
services:
|
||||
# Redis Cache Service
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: controles_redis
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
networks:
|
||||
- controles_network
|
||||
version: '3.8'
|
||||
|
||||
# Flask Application
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
APP_UID: ${APP_UID:-1000}
|
||||
APP_GID: ${APP_GID:-1000}
|
||||
container_name: controles_app
|
||||
build: .
|
||||
ports:
|
||||
- "5000:5000"
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_ENV=production
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- DATABASE_URL=sqlite:////data/database.db
|
||||
|
||||
# DEV apenas para facilitar testes, não deve ser usado em produção
|
||||
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP
|
||||
|
||||
# Produção definir em .env
|
||||
#- ADMIN_OTP_SECRET=${ADMIN_OTP_SECRET}
|
||||
volumes:
|
||||
- ./data:/data
|
||||
- ./logs:/app/logs
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
- ALL
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
- .:/app
|
||||
- ~/.local/share/controles:/root/.local/share/controles
|
||||
environment:
|
||||
- FLASK_ENV=development
|
||||
- FLASK_APP=app.py
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- controles_network
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
controles_network:
|
||||
driver: bridge
|
||||
|
||||
243
docs/README.md
243
docs/README.md
@@ -1,162 +1,165 @@
|
||||
# Sistema de Controles OCI
|
||||
# Sistema de Controle OCI
|
||||
|
||||
Sistema de gerenciamento para a Organização Comunista Internacionalista (OCI) com controle de militantes, cotas, pagamentos e materiais.
|
||||
## Hierarquia e Permissões
|
||||
|
||||
## Trilha Recomendada de Leitura
|
||||
### Níveis de Acesso
|
||||
|
||||
1. `docs/architecture_summary.md`
|
||||
2. `docs/rbac.md`
|
||||
3. `docs/permission_strategy.md`
|
||||
4. `docs/redis_cache_setup.md`
|
||||
5. `docs/permission_fixes_summary.md`
|
||||
1. **Militante Básico**
|
||||
- Pode ver apenas os membros da sua própria célula
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
## Índice por Tema
|
||||
2. **Secretário de Célula**
|
||||
- Pode ver e gerenciar apenas os membros da sua célula
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
### Arquitetura
|
||||
3. **Membro de Setor**
|
||||
- Pode ver apenas os dados do setor ao qual pertence
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
- `docs/architecture_summary.md`: visão geral do estado da arquitetura.
|
||||
- `docs/mvc_refactoring.md`: detalhes da refatoração MVC.
|
||||
4. **Secretário de Setor**
|
||||
- Pode ver e gerenciar todos os dados do seu setor
|
||||
- Pode alterar níveis de militantes do setor, transformando-os em secretários
|
||||
- Não pode alterar níveis de membros de outros setores
|
||||
|
||||
### Permissões e Segurança de Acesso
|
||||
5. **Membro de CR**
|
||||
- Pode ver apenas os dados do CR ao qual pertence
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
- `docs/rbac.md`: níveis de papel e herança de permissões.
|
||||
- `docs/permission_strategy.md`: estratégia de filtragem de dados e uso em templates.
|
||||
- `docs/permission_fixes_summary.md`: resumo das correções aplicadas em permissões.
|
||||
6. **Secretário de CR**
|
||||
- Pode ver e gerenciar todos os dados do seu CR
|
||||
- Pode alterar níveis de membros do CR
|
||||
- Não pode alterar níveis de membros de outros CRs
|
||||
|
||||
### Infra e Performance
|
||||
7. **Membro do CC**
|
||||
- Pode ver todos os dados do sistema
|
||||
- Não pode alterar níveis de outros usuários
|
||||
|
||||
- `docs/redis_cache_setup.md`: configuração e uso de cache Redis.
|
||||
8. **Secretário Geral e Secretário de Organização**
|
||||
- Pode ver todos os dados do sistema
|
||||
- Pode alterar níveis de qualquer usuário em qualquer instância
|
||||
|
||||
### Histórico Técnico
|
||||
### Regras de Visualização
|
||||
|
||||
- `docs/alteracoes_db_connection.md`: alterações no gerenciamento de conexão/sessão de banco.
|
||||
- Cada militante só pode ver os membros da sua própria célula
|
||||
- Membros de setor só veem dados do setor ao qual pertencem
|
||||
- Membros de CR só veem informações do CR ao qual pertencem
|
||||
- Membros do CC podem ver todas as informações do sistema
|
||||
|
||||
## Como Manter Esta Pasta Organizada
|
||||
### Regras de Edição
|
||||
|
||||
- Preferir um arquivo por assunto (evitar documentos muito amplos).
|
||||
- Começar cada documento com contexto, problema e decisão.
|
||||
- Registrar trade-offs e impactos de manutenção.
|
||||
- Atualizar este índice sempre que um novo documento for criado.
|
||||
- Apenas o Secretário Geral e o Secretário de Organização podem alterar níveis em todas as instâncias
|
||||
- Secretários de CR podem alterar níveis apenas dentro do seu CR
|
||||
- Secretários de Setor podem alterar níveis apenas dentro do seu setor, transformando militantes em secretários
|
||||
- Outros níveis não podem alterar níveis de outros usuários
|
||||
|
||||
### Diagrama da Arquitetura
|
||||
## Responsabilidades
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Request] --> B[Controller Layer]
|
||||
B --> C{Permission Check}
|
||||
C -->|Admin| D[All Data]
|
||||
C -->|CC| E[All Data]
|
||||
C -->|CR| F[CR Data Only]
|
||||
C -->|Setor| G[Setor Data Only]
|
||||
C -->|Célula| H[Célula Data Only]
|
||||
C -->|No Permission| I[Empty Data]
|
||||
O sistema suporta as seguintes responsabilidades para militantes:
|
||||
|
||||
D --> J[Template Rendering]
|
||||
E --> J
|
||||
F --> J
|
||||
G --> J
|
||||
H --> J
|
||||
I --> J
|
||||
- Militante Básico (1)
|
||||
- Secretário de Célula (2)
|
||||
- Secretário de Setor (4)
|
||||
- Secretário de CR (8)
|
||||
- Secretário de CC (16)
|
||||
- Secretário Geral (32)
|
||||
- Quadro-Orientador (64)
|
||||
- Responsável de Finanças (256)
|
||||
- Responsável de Imprensa (512)
|
||||
|
||||
J --> K[Always Renders Successfully]
|
||||
```
|
||||
### Status de Aspirante
|
||||
|
||||
## 📊 Funcionalidades
|
||||
Todo novo militante começa como Aspirante. Este status tem as seguintes características:
|
||||
|
||||
### Gestão de Militantes
|
||||
- Cadastro completo com dados pessoais e profissionais
|
||||
- Endereços e contatos
|
||||
- Responsabilidades organizacionais
|
||||
- Estados (Ativo, Desligado, Suspenso, Afastado)
|
||||
1. **Duração Mínima**: O status de Aspirante deve ser mantido por pelo menos 3 meses após a integração do militante.
|
||||
|
||||
### Gestão Financeira
|
||||
- Cotas mensais
|
||||
- Pagamentos diversos
|
||||
- Vendas de materiais
|
||||
- Assinaturas anuais
|
||||
2. **Avaliação Obrigatória**: Para remover o status de Aspirante, é necessário:
|
||||
- Ter passado o período mínimo de 3 meses
|
||||
- Registrar uma avaliação detalhada da atuação do militante durante este período
|
||||
|
||||
### Estrutura Organizacional
|
||||
- Comitês Centrais
|
||||
- Comitês Regionais
|
||||
- Setores
|
||||
- Células
|
||||
3. **Quem pode Avaliar**: A avaliação e remoção do status de Aspirante pode ser feita por:
|
||||
- Secretário Geral
|
||||
- Secretário de Organização
|
||||
- Secretários de CR (para militantes de seu CR)
|
||||
- Secretários de Setor (para militantes de seu setor)
|
||||
|
||||
### Relatórios
|
||||
- Relatórios de cotas
|
||||
- Relatórios de vendas
|
||||
- Relatórios de pagamentos
|
||||
4. **Registro da Avaliação**: A avaliação deve incluir:
|
||||
- Análise da participação do militante nas atividades
|
||||
- Desenvolvimento político e organizativo
|
||||
- Pontos fortes e aspectos a melhorar
|
||||
- Recomendações para o desenvolvimento futuro
|
||||
|
||||
## 📈 Performance
|
||||
5. **Histórico**: O sistema mantém registro de:
|
||||
- Data de início do período como Aspirante
|
||||
- Data da avaliação
|
||||
- Texto completo da avaliação
|
||||
|
||||
### Cache Redis
|
||||
- Dashboard statistics: 5 minutos
|
||||
- Militante data: 30 minutos
|
||||
- Pagamento data: 30 minutos
|
||||
- API responses: Variável
|
||||
O Quadro-Orientador é uma responsabilidade especial que pode ser atribuída a militantes em qualquer nível hierárquico, incluindo membros de CR e CC. Esta responsabilidade indica que o militante tem a função de orientar e apoiar outros militantes em sua formação política e organizativa.
|
||||
|
||||
### Monitoramento
|
||||
A atribuição da responsabilidade de Quadro-Orientador pode ser feita por:
|
||||
- Secretário Geral
|
||||
- Secretário de Organização
|
||||
- Secretários de CR (para militantes de seu CR)
|
||||
- Secretários de Setor (para militantes de seu setor)
|
||||
|
||||
```bash
|
||||
# Status do cache
|
||||
make cache-status
|
||||
### Responsáveis de Finanças e Imprensa
|
||||
|
||||
# Logs da aplicação
|
||||
make docker-logs
|
||||
Cada instância (Célula, Setor, CR e CC) possui três responsáveis:
|
||||
|
||||
# Logs do Redis
|
||||
docker-compose logs redis
|
||||
```
|
||||
1. **Responsável Geral**: Obrigatório para todas as instâncias. É o principal responsável pela instância.
|
||||
|
||||
## 📋 Recommended Next Steps
|
||||
2. **Responsável de Finanças**: Opcional. Responsável por:
|
||||
- Controle financeiro da instância
|
||||
- Arrecadação de contribuições
|
||||
- Prestação de contas
|
||||
- Planejamento financeiro
|
||||
|
||||
### High Priority
|
||||
1. **Add Unit Tests**: Create comprehensive test coverage for models and services
|
||||
2. **API Documentation**: Add OpenAPI/Swagger documentation
|
||||
3. **Logging**: Implement structured logging throughout the application
|
||||
4. **Configuration Management**: Centralize configuration management
|
||||
3. **Responsável de Imprensa**: Opcional. Responsável por:
|
||||
- Comunicação externa da instância
|
||||
- Produção de materiais de divulgação
|
||||
- Gestão de redes sociais
|
||||
- Relacionamento com a mídia
|
||||
|
||||
### Medium Priority
|
||||
1. **Repository Pattern**: Implement for better data access abstraction
|
||||
2. **Caching**: Add Redis caching for frequently accessed data
|
||||
3. **Background Jobs**: Implement Celery for background task processing
|
||||
4. **Monitoring**: Add application monitoring and health checks
|
||||
Os responsáveis de finanças e imprensa são designados pelo responsável geral da instância, com aprovação da instância superior.
|
||||
|
||||
### Low Priority
|
||||
1. **Event System**: Implement for decoupled component communication
|
||||
2. **API Versioning**: Add support for multiple API versions
|
||||
3. **GraphQL**: Consider GraphQL for more flexible data querying
|
||||
4. **Microservices**: Evaluate splitting into microservices if needed
|
||||
## Hierarquia de Instâncias
|
||||
|
||||
## 🔧 Correções de Permissões Recentes
|
||||
1. **Comitê Central (CC)**
|
||||
- Instância máxima da organização
|
||||
- Possui responsável geral, de finanças e de imprensa
|
||||
- Coordena todos os CRs
|
||||
|
||||
### Problema Identificado
|
||||
Durante a implementação inicial, foi descoberto que aplicar restrições no nível de template estava causando o desaparecimento dos menus administrativos.
|
||||
2. **Comitê Regional (CR)**
|
||||
- Subordinado ao CC
|
||||
- Possui responsável geral, de finanças e de imprensa
|
||||
- Coordena os setores da sua região
|
||||
|
||||
### Solução Implementada
|
||||
- **Controle movido para o nível de dados**: Filtragem acontece nos controllers
|
||||
- **Templates simplificados**: `user_can()` sempre retorna `True`
|
||||
- **Menus sempre visíveis**: Nenhuma restrição na interface
|
||||
- **Degradação graceful**: Erros retornam dados vazios, nunca quebram
|
||||
3. **Setor**
|
||||
- Subordinado ao CR
|
||||
- Possui responsável geral, de finanças e de imprensa
|
||||
- Coordena as células do seu setor
|
||||
|
||||
### Controllers Atualizados
|
||||
- ✅ `militante_controller.py` - Filtragem hierárquica implementada
|
||||
- ✅ `cota_controller.py` - Controle baseado em permissões
|
||||
- ✅ `material_controller.py` - Acesso flexível por nível
|
||||
- ✅ `pagamento_controller.py` - Filtragem organizacional
|
||||
4. **Célula**
|
||||
- Subordinada ao Setor
|
||||
- Possui responsável geral, de finanças e de imprensa
|
||||
- Unidade básica de organização
|
||||
|
||||
### Templates Corrigidos
|
||||
- ✅ `listar_cotas.html` - URLs e referências corrigidas
|
||||
- ✅ `listar_tipos_materiais.html` - Variáveis e campos ajustados
|
||||
- ✅ `base.html` - Menus sempre visíveis
|
||||
## Permissões
|
||||
|
||||
### Status dos Testes
|
||||
**Funcionais:** `/`, `/dashboard`, `/pagamentos`, `/materiais`
|
||||
**Com problemas:** `/militantes`, `/cotas`, `/tipos-materiais`, `/admin/dashboard`
|
||||
As permissões no sistema são baseadas nas responsabilidades do militante e na hierarquia das instâncias:
|
||||
|
||||
Para detalhes completos, consulte: [docs/permission_fixes_summary.md](docs/permission_fixes_summary.md)
|
||||
1. **Visualização**
|
||||
- Militantes básicos veem apenas sua célula
|
||||
- Secretários de célula veem sua célula
|
||||
- Secretários de setor veem seu setor e células
|
||||
- Secretários de CR veem seu CR, setores e células
|
||||
- Secretários de CC veem todos os dados
|
||||
|
||||
---
|
||||
2. **Edição**
|
||||
- Cada nível pode gerenciar apenas os níveis abaixo
|
||||
- Responsáveis de finanças e imprensa podem editar apenas suas áreas
|
||||
- Quadros-Orientadores podem avaliar militantes
|
||||
|
||||
**Última atualização**: Julho 2025
|
||||
**Versão**: 1.0.0
|
||||
**Status**: ✅ Produção
|
||||
3. **Responsabilidades**
|
||||
- Apenas o nível superior pode atribuir responsabilidades
|
||||
- Responsáveis de finanças e imprensa são designados pelo responsável geral
|
||||
- O status de Quadro-Orientador segue regras específicas
|
||||
@@ -1,191 +0,0 @@
|
||||
# Architecture Summary - Current State
|
||||
|
||||
## ✅ Completed MVC Refactoring
|
||||
|
||||
Your Flask application has been successfully refactored to follow the MVC (Model-View-Controller) pattern. Here's the current state:
|
||||
|
||||
### Current Architecture
|
||||
|
||||
```
|
||||
📁 controles/
|
||||
├── 🎯 app.py (80 lines) - Minimal application entry point
|
||||
├── 🎮 controllers/ - Route handlers and request logic
|
||||
│ ├── auth_controller.py (143 lines)
|
||||
│ ├── home_controller.py (80 lines)
|
||||
│ ├── militante_controller.py (308 lines)
|
||||
│ ├── pagamento_controller.py (191 lines)
|
||||
│ ├── cota_controller.py (120 lines)
|
||||
│ └── usuario_controller.py (184 lines)
|
||||
├── 📊 models/ - Database operations and data manipulation
|
||||
│ ├── militante_model.py (252 lines)
|
||||
│ ├── pagamento_model.py (184 lines)
|
||||
│ └── entities/
|
||||
├── 🔧 services/ - Business logic and external integrations
|
||||
│ ├── auth_service.py (157 lines)
|
||||
│ ├── dashboard_service.py (72 lines)
|
||||
│ └── celula_service.py (78 lines)
|
||||
├── 🎨 templates/ - Views (HTML templates)
|
||||
├── 📦 static/ - Static assets
|
||||
└── 🛠️ functions/ - Utility functions
|
||||
```
|
||||
|
||||
### Key Achievements
|
||||
|
||||
✅ **Separation of Concerns**: Each component has a single responsibility
|
||||
✅ **Modularity**: Features are organized into logical modules
|
||||
✅ **Maintainability**: Code is easier to locate and modify
|
||||
✅ **Testability**: Components can be tested independently
|
||||
✅ **Scalability**: New features can be added as new controllers
|
||||
✅ **Blueprint Pattern**: Modular route organization
|
||||
✅ **Type Hints**: Better code documentation and IDE support
|
||||
✅ **Error Handling**: Consistent patterns across layers
|
||||
|
||||
### File Size Reduction
|
||||
|
||||
| Component | Before | After | Improvement |
|
||||
|-----------|--------|-------|-------------|
|
||||
| `app.py` | 120+ lines | 80 lines | 33% reduction |
|
||||
| Controllers | N/A | 80-308 lines each | Focused responsibilities |
|
||||
| Models | N/A | 200+ lines each | Data operations |
|
||||
| Services | N/A | 70-150 lines each | Business logic |
|
||||
|
||||
## 🎯 Current Strengths
|
||||
|
||||
1. **Clean Architecture**: Proper separation between presentation, business logic, and data access
|
||||
2. **Consistent Patterns**: Similar structure across all controllers and models
|
||||
3. **Database Management**: Proper connection handling with try/finally blocks
|
||||
4. **Authentication**: Well-structured auth service with OTP support
|
||||
5. **Error Handling**: Consistent error response patterns
|
||||
6. **Documentation**: Good use of docstrings and type hints
|
||||
|
||||
## 🔄 Potential Improvements
|
||||
|
||||
### 1. Repository Pattern
|
||||
Consider implementing a repository pattern for further data access abstraction:
|
||||
|
||||
```python
|
||||
# Example: repositories/militante_repository.py
|
||||
class MilitanteRepository:
|
||||
def __init__(self, db_session):
|
||||
self.db = db_session
|
||||
|
||||
def find_by_id(self, id: int) -> Optional[Militante]:
|
||||
return self.db.query(Militante).get(id)
|
||||
|
||||
def save(self, militante: Militante) -> Militante:
|
||||
self.db.add(militante)
|
||||
self.db.commit()
|
||||
return militante
|
||||
```
|
||||
|
||||
### 2. Dependency Injection
|
||||
Implement a dependency injection container for better service management:
|
||||
|
||||
```python
|
||||
# Example: container.py
|
||||
class Container:
|
||||
def __init__(self):
|
||||
self.services = {}
|
||||
|
||||
def register(self, name, service):
|
||||
self.services[name] = service
|
||||
|
||||
def get(self, name):
|
||||
return self.services[name]
|
||||
```
|
||||
|
||||
### 3. API Versioning
|
||||
Add support for API versioning:
|
||||
|
||||
```python
|
||||
# Example: api/v1/routes.py
|
||||
from flask import Blueprint
|
||||
|
||||
api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1')
|
||||
|
||||
@api_v1.route('/militantes', methods=['GET'])
|
||||
def list_militantes():
|
||||
# API endpoint logic
|
||||
pass
|
||||
```
|
||||
|
||||
### 4. Caching Layer
|
||||
Implement Redis caching for performance:
|
||||
|
||||
```python
|
||||
# Example: services/cache_service.py
|
||||
import redis
|
||||
|
||||
class CacheService:
|
||||
def __init__(self):
|
||||
self.redis = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
def get(self, key):
|
||||
return self.redis.get(key)
|
||||
|
||||
def set(self, key, value, expire=3600):
|
||||
self.redis.setex(key, expire, value)
|
||||
```
|
||||
|
||||
### 5. Event System
|
||||
Implement an event system for decoupled communication:
|
||||
|
||||
```python
|
||||
# Example: events/event_bus.py
|
||||
class EventBus:
|
||||
def __init__(self):
|
||||
self.listeners = {}
|
||||
|
||||
def subscribe(self, event_type, listener):
|
||||
if event_type not in self.listeners:
|
||||
self.listeners[event_type] = []
|
||||
self.listeners[event_type].append(listener)
|
||||
|
||||
def publish(self, event_type, data):
|
||||
if event_type in self.listeners:
|
||||
for listener in self.listeners[event_type]:
|
||||
listener(data)
|
||||
```
|
||||
|
||||
## 📋 Recommended Next Steps
|
||||
|
||||
### High Priority
|
||||
1. **Add Unit Tests**: Create comprehensive test coverage for models and services
|
||||
2. **API Documentation**: Add OpenAPI/Swagger documentation
|
||||
3. **Logging**: Implement structured logging throughout the application
|
||||
4. **Configuration Management**: Centralize configuration management
|
||||
|
||||
### Medium Priority
|
||||
1. **Repository Pattern**: Implement for better data access abstraction
|
||||
2. **Caching**: Add Redis caching for frequently accessed data
|
||||
3. **Background Jobs**: Implement Celery for background task processing
|
||||
4. **Monitoring**: Add application monitoring and health checks
|
||||
|
||||
### Low Priority
|
||||
1. **Event System**: Implement for decoupled component communication
|
||||
2. **API Versioning**: Add support for multiple API versions
|
||||
3. **GraphQL**: Consider GraphQL for more flexible data querying
|
||||
4. **Microservices**: Evaluate splitting into microservices if needed
|
||||
|
||||
## 🏆 Best Practices Already Implemented
|
||||
|
||||
✅ **Single Responsibility Principle**: Each class has one reason to change
|
||||
✅ **Dependency Inversion**: Controllers depend on abstractions (services)
|
||||
✅ **Open/Closed Principle**: Easy to extend without modifying existing code
|
||||
✅ **Interface Segregation**: Services have focused interfaces
|
||||
✅ **DRY Principle**: Code reuse through models and services
|
||||
✅ **SOLID Principles**: Overall adherence to SOLID principles
|
||||
|
||||
## 📊 Code Quality Metrics
|
||||
|
||||
- **Cyclomatic Complexity**: Low (simple, focused functions)
|
||||
- **Code Duplication**: Minimal (good reuse through services)
|
||||
- **Test Coverage**: Needs improvement (recommend adding tests)
|
||||
- **Documentation**: Good (docstrings and type hints)
|
||||
- **Error Handling**: Consistent and comprehensive
|
||||
|
||||
## 🎉 Conclusion
|
||||
|
||||
Your Flask application has been successfully transformed into a well-architected, maintainable, and scalable system. The MVC refactoring provides a solid foundation for future development and makes the codebase much more professional and enterprise-ready.
|
||||
|
||||
The current architecture follows industry best practices and provides excellent separation of concerns while maintaining all existing functionality. The modular structure will make it easy to add new features and maintain the application as it grows.
|
||||
@@ -1,211 +0,0 @@
|
||||
# MVC Refactoring Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the MVC (Model-View-Controller) refactoring that has been implemented in the Flask application to improve code organization, maintainability, and separation of concerns.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The application has been refactored from a monolithic `app.py` file to a proper MVC architecture with the following structure:
|
||||
|
||||
```
|
||||
controles/
|
||||
├── app.py # Main application entry point (minimal)
|
||||
├── controllers/ # Controllers (handling routes and request logic)
|
||||
│ ├── auth_controller.py
|
||||
│ ├── home_controller.py
|
||||
│ ├── militante_controller.py
|
||||
│ ├── pagamento_controller.py
|
||||
│ ├── cota_controller.py
|
||||
│ └── usuario_controller.py
|
||||
├── models/ # Models (database operations and business logic)
|
||||
│ ├── militante_model.py
|
||||
│ ├── pagamento_model.py
|
||||
│ └── entities/
|
||||
├── services/ # Services (business logic and external integrations)
|
||||
│ ├── auth_service.py
|
||||
│ ├── dashboard_service.py
|
||||
│ └── celula_service.py
|
||||
├── templates/ # Views (HTML templates)
|
||||
├── static/ # Static assets (CSS, JS, images)
|
||||
└── functions/ # Utility functions and helpers
|
||||
```
|
||||
|
||||
## Key Improvements
|
||||
|
||||
### 1. Separation of Concerns
|
||||
|
||||
**Before Refactoring:**
|
||||
- All routes, business logic, and database operations were in a single `app.py` file
|
||||
- Mixed responsibilities made the code difficult to maintain
|
||||
- Large file size (120+ lines) with complex logic
|
||||
|
||||
**After Refactoring:**
|
||||
- **Controllers**: Handle HTTP requests, route definitions, and request/response logic
|
||||
- **Models**: Encapsulate database operations and data manipulation
|
||||
- **Services**: Contain business logic and external service integrations
|
||||
- **Views**: HTML templates remain in the templates directory
|
||||
|
||||
### 2. Modularity
|
||||
|
||||
Each major feature now has its own controller:
|
||||
- `auth_controller.py` - Authentication and user management
|
||||
- `home_controller.py` - Dashboard and home page
|
||||
- `militante_controller.py` - Member management
|
||||
- `pagamento_controller.py` - Payment management
|
||||
- `cota_controller.py` - Quota management
|
||||
- `usuario_controller.py` - User administration
|
||||
|
||||
### 3. Code Reusability
|
||||
|
||||
- **Models**: Provide reusable database operations
|
||||
- **Services**: Encapsulate business logic that can be used across controllers
|
||||
- **Blueprints**: Enable modular route registration
|
||||
|
||||
## Detailed Architecture
|
||||
|
||||
### Controllers Layer
|
||||
|
||||
Controllers handle HTTP requests and coordinate between models and services:
|
||||
|
||||
```python
|
||||
# Example: auth_controller.py
|
||||
from flask import Blueprint, request, render_template, redirect, url_for, flash
|
||||
from services.auth_service import AuthService
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if request.method == "POST":
|
||||
result = AuthService.autenticar_usuario(
|
||||
request.form.get("email"),
|
||||
request.form.get("password"),
|
||||
request.form.get("otp")
|
||||
)
|
||||
# Handle result and response
|
||||
```
|
||||
|
||||
### Models Layer
|
||||
|
||||
Models encapsulate database operations and data manipulation:
|
||||
|
||||
```python
|
||||
# Example: militante_model.py
|
||||
class MilitanteModel:
|
||||
@staticmethod
|
||||
def criar_militante(data: Dict) -> Dict:
|
||||
"""Cria um novo militante"""
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Database operations
|
||||
return {'status': 'success', 'message': 'Militante criado'}
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
finally:
|
||||
db.close()
|
||||
```
|
||||
|
||||
### Services Layer
|
||||
|
||||
Services contain business logic and external integrations:
|
||||
|
||||
```python
|
||||
# Example: auth_service.py
|
||||
class AuthService:
|
||||
@staticmethod
|
||||
def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict:
|
||||
"""Autentica um usuário"""
|
||||
# Business logic for authentication
|
||||
return {'status': 'success', 'user': user}
|
||||
```
|
||||
|
||||
### Main Application
|
||||
|
||||
The main `app.py` file is now minimal and focused on configuration:
|
||||
|
||||
```python
|
||||
def create_app():
|
||||
"""Cria e configura a aplicação Flask"""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Configuration
|
||||
app.secret_key = os.getenv('SECRET_KEY', secrets.token_hex(16))
|
||||
bootstrap = Bootstrap5(app)
|
||||
csrf = CSRFProtect()
|
||||
csrf.init_app(app)
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(home_bp)
|
||||
app.register_blueprint(militante_bp)
|
||||
app.register_blueprint(pagamento_bp)
|
||||
app.register_blueprint(cota_bp)
|
||||
app.register_blueprint(usuario_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
## Benefits Achieved
|
||||
|
||||
### 1. Maintainability
|
||||
- Each component has a single responsibility
|
||||
- Easier to locate and modify specific functionality
|
||||
- Reduced coupling between components
|
||||
|
||||
### 2. Testability
|
||||
- Controllers can be tested independently
|
||||
- Models can be unit tested without HTTP context
|
||||
- Services can be mocked for testing
|
||||
|
||||
### 3. Scalability
|
||||
- New features can be added as new controllers
|
||||
- Existing functionality can be extended without affecting other parts
|
||||
- Blueprint structure supports modular development
|
||||
|
||||
### 4. Code Organization
|
||||
- Clear separation between presentation, business logic, and data access
|
||||
- Consistent patterns across the application
|
||||
- Easier onboarding for new developers
|
||||
|
||||
## File Size Reduction
|
||||
|
||||
**Before Refactoring:**
|
||||
- `app.py`: 120+ lines with mixed responsibilities
|
||||
|
||||
**After Refactoring:**
|
||||
- `app.py`: ~80 lines (configuration only)
|
||||
- Controllers: 80-300 lines each (focused responsibilities)
|
||||
- Models: 200+ lines each (data operations)
|
||||
- Services: 70-150 lines each (business logic)
|
||||
|
||||
## Best Practices Implemented
|
||||
|
||||
1. **Single Responsibility Principle**: Each class/module has one reason to change
|
||||
2. **Dependency Injection**: Services are injected into controllers
|
||||
3. **Error Handling**: Consistent error handling patterns across layers
|
||||
4. **Type Hints**: Used throughout for better code documentation
|
||||
5. **Static Methods**: Used in models and services for stateless operations
|
||||
6. **Blueprint Pattern**: Modular route organization
|
||||
7. **Database Connection Management**: Proper connection handling with try/finally blocks
|
||||
|
||||
## Migration Notes
|
||||
|
||||
The refactoring maintains backward compatibility:
|
||||
- All existing routes continue to work
|
||||
- Database models remain unchanged
|
||||
- Template structure is preserved
|
||||
- Configuration and environment variables are maintained
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Repository Pattern**: Further abstraction of data access layer
|
||||
2. **Dependency Injection Container**: For better service management
|
||||
3. **API Versioning**: Support for multiple API versions
|
||||
4. **Caching Layer**: Redis integration for performance
|
||||
5. **Event System**: Decoupled event handling between components
|
||||
|
||||
## Conclusion
|
||||
|
||||
The MVC refactoring has successfully transformed the application from a monolithic structure to a well-organized, maintainable, and scalable architecture. The separation of concerns makes the codebase easier to understand, test, and extend while maintaining all existing functionality.
|
||||
@@ -1,261 +0,0 @@
|
||||
# Correções de Permissões e Arquitetura Final
|
||||
|
||||
## Diagrama da Arquitetura de Permissões
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Request] --> B[Controller Layer]
|
||||
B --> C{Permission Check}
|
||||
C -->|Admin| D[All Data]
|
||||
C -->|CC| E[All Data]
|
||||
C -->|CR| F[CR Data Only]
|
||||
C -->|Setor| G[Setor Data Only]
|
||||
C -->|Célula| H[Célula Data Only]
|
||||
C -->|No Permission| I[Empty Data]
|
||||
|
||||
D --> J[Template Rendering]
|
||||
E --> J
|
||||
F --> J
|
||||
G --> J
|
||||
H --> J
|
||||
I --> J
|
||||
|
||||
J --> K[Always Renders Successfully]
|
||||
|
||||
subgraph DataLevel["Data Level Control"]
|
||||
L[Militante Controller]
|
||||
M[Cota Controller]
|
||||
N[Material Controller]
|
||||
O[Pagamento Controller]
|
||||
end
|
||||
|
||||
subgraph TemplateLevel["Template Level"]
|
||||
P[Base Template]
|
||||
Q[Listar Templates]
|
||||
R[Modal Templates]
|
||||
end
|
||||
|
||||
subgraph PermissionStrategy["Permission Strategy"]
|
||||
S["user_can always returns True"]
|
||||
T[Data Filtering in Controllers]
|
||||
U[Graceful Error Handling]
|
||||
end
|
||||
|
||||
B --> L
|
||||
B --> M
|
||||
B --> N
|
||||
B --> O
|
||||
|
||||
L --> Q
|
||||
M --> Q
|
||||
N --> Q
|
||||
O --> Q
|
||||
|
||||
P --> S
|
||||
Q --> T
|
||||
R --> U
|
||||
```
|
||||
|
||||
## Problema Identificado
|
||||
|
||||
Durante a implementação inicial do sistema de permissões, foi descoberto que aplicar restrições no nível de template (menus) estava causando o desaparecimento de todos os menus administrativos. O usuário corretamente identificou que o controle deveria ser no **nível de dados**, não no nível de interface.
|
||||
|
||||
## Estratégia Final Implementada
|
||||
|
||||
### 1. Princípios Fundamentais
|
||||
|
||||
- **Menus sempre visíveis**: Nenhuma restrição no nível de template/menu
|
||||
- **Controle no nível de dados**: Filtragem acontece nos controllers
|
||||
- **Degradação graceful**: Erros retornam dados vazios, nunca quebram templates
|
||||
- **Acesso hierárquico**: Baseado no nível organizacional do usuário
|
||||
|
||||
### 2. Arquitetura de Permissões
|
||||
|
||||
```
|
||||
USER REQUEST → CONTROLLER (Data Filter) → TEMPLATE (Always Renders)
|
||||
↓
|
||||
PERMISSION CHECK
|
||||
├─ Admin: All Data
|
||||
├─ CC: All Data
|
||||
├─ CR: By CR
|
||||
├─ Setor: By Sector
|
||||
└─ Célula: By Cell
|
||||
```
|
||||
|
||||
### 3. Implementação por Camadas
|
||||
|
||||
#### Template Helpers Simplificados
|
||||
```python
|
||||
def permission_context_processor():
|
||||
"""Context processor simples que disponibiliza informações básicas do usuário"""
|
||||
context = {
|
||||
'user_can': lambda permission: True, # Sempre True - controle é no nível de dados
|
||||
'user_has_role': lambda role: True, # Sempre True - controle é no nível de dados
|
||||
'is_admin': False,
|
||||
'current_user_data': None
|
||||
}
|
||||
|
||||
if current_user.is_authenticated:
|
||||
context.update({
|
||||
'is_admin': getattr(current_user, 'is_admin', False),
|
||||
'current_user_data': current_user
|
||||
})
|
||||
|
||||
return context
|
||||
```
|
||||
|
||||
#### Controle de Dados nos Controllers
|
||||
|
||||
**Militante Controller:**
|
||||
```python
|
||||
def listar():
|
||||
try:
|
||||
if current_user.is_admin:
|
||||
militantes = query.all()
|
||||
elif hasattr(current_user, 'militante') and current_user.militante:
|
||||
if current_user.militante.responsabilidades & Militante.TESOUREIRO:
|
||||
# Tesoureiro pode fazer tudo que secretário pode
|
||||
militantes = query.filter(Militante.celula_id == current_user.militante.celula_id).all()
|
||||
else:
|
||||
militantes = query.filter(Militante.celula_id == current_user.militante.celula_id).all()
|
||||
else:
|
||||
militantes = []
|
||||
|
||||
return render_template('listar_militantes.html', militantes=militantes)
|
||||
except Exception as e:
|
||||
print(f"Erro: {e}")
|
||||
return render_template('listar_militantes.html', militantes=[])
|
||||
```
|
||||
|
||||
**Padrão de Erro Robusto:**
|
||||
```python
|
||||
try:
|
||||
# Lógica de negócio com dados filtrados por permissão
|
||||
return render_template('template.html', data=filtered_data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
# SEMPRE renderizar template com dados vazios em vez de falhar
|
||||
return render_template('template.html', data=[])
|
||||
```
|
||||
|
||||
## Alterações Implementadas
|
||||
|
||||
### 1. Template Helpers (`functions/template_helpers.py`)
|
||||
|
||||
**Antes:**
|
||||
- Sistema complexo de verificação de permissões
|
||||
- `user_can()` retornava verificações reais
|
||||
- Controle no nível de template
|
||||
|
||||
**Depois:**
|
||||
- Sistema simplificado
|
||||
- `user_can()` sempre retorna `True`
|
||||
- Controle movido para o nível de dados
|
||||
|
||||
### 2. Controllers Atualizados
|
||||
|
||||
#### Militante Controller (`controllers/militante_controller.py`)
|
||||
- ✅ Filtragem hierárquica de dados
|
||||
- ✅ Tratamento robusto de erros
|
||||
- ✅ Regra especial para tesoureiros
|
||||
- ✅ Remoção de decoradores problemáticos
|
||||
|
||||
#### Cota Controller (`controllers/cota_controller.py`)
|
||||
- ✅ Filtragem baseada em permissões
|
||||
- ✅ Admin vê todas as cotas
|
||||
- ✅ Outros usuários veem apenas suas cotas
|
||||
- ✅ Tratamento de erros graceful
|
||||
|
||||
#### Material Controller (`controllers/material_controller.py`)
|
||||
- ✅ Controle de acesso flexível
|
||||
- ✅ Filtragem por permissões
|
||||
- ✅ Tratamento de erros robusto
|
||||
|
||||
#### Pagamento Controller (`controllers/pagamento_controller.py`)
|
||||
- ✅ Filtragem hierárquica similar aos militantes
|
||||
- ✅ Controle baseado no nível organizacional
|
||||
- ✅ Tratamento de erros consistente
|
||||
|
||||
### 3. Templates Corrigidos
|
||||
|
||||
#### Base Template (`templates/base.html`)
|
||||
- ✅ Menus sempre visíveis
|
||||
- ✅ Remoção de verificações de permissão nos menus
|
||||
- ✅ Manutenção da estrutura de navegação
|
||||
|
||||
#### Listar Cotas (`templates/listar_cotas.html`)
|
||||
- ✅ Correção de URLs: `nova_cota` → `cota.nova`
|
||||
- ✅ Remoção de referências a campos inexistentes
|
||||
- ✅ Tratamento adequado de dados vazios
|
||||
|
||||
#### Listar Tipos Materiais (`templates/listar_tipos_materiais.html`)
|
||||
- ✅ Correção de variável: `tipos` → `tipos_materiais`
|
||||
- ✅ Remoção de referências a campo `preco` inexistente
|
||||
- ✅ Estrutura de template consistente
|
||||
|
||||
### 4. Regras de Permissão Especiais
|
||||
|
||||
#### Tesoureiros
|
||||
- **Regra**: Tesoureiro pode fazer tudo que o secretário da instância pode fazer
|
||||
- **Implementação**: Verificação especial nos controllers
|
||||
- **Resultado**: Tesoureiros têm acesso completo aos dados de sua instância
|
||||
|
||||
#### Hierarquia Organizacional
|
||||
```
|
||||
Admin → Acesso total
|
||||
CC → Acesso total
|
||||
CR → Dados do CR
|
||||
Setor → Dados do setor
|
||||
Célula → Dados da célula
|
||||
```
|
||||
|
||||
## Status Final dos Testes
|
||||
|
||||
### Rotas Funcionais ✅
|
||||
- `/` - Home (HTTP 200)
|
||||
- `/dashboard` - Dashboard (HTTP 200)
|
||||
- `/pagamentos` - Payments (HTTP 200)
|
||||
- `/materiais` - Materials (HTTP 200)
|
||||
|
||||
### Rotas com Problemas ❌
|
||||
- `/militantes` - HTTP 500 (referências a `Militante` indefinido)
|
||||
- `/cotas` - HTTP 500 (URLs corrigidas mas ainda com problemas)
|
||||
- `/tipos-materiais` - HTTP 500 (referências a campos inexistentes)
|
||||
- `/admin/dashboard` - HTTP 404 (problema de roteamento)
|
||||
|
||||
## Próximos Passos Recomendados
|
||||
|
||||
### Alta Prioridade
|
||||
1. **Corrigir referências a `Militante` nos templates**
|
||||
- Passar classe `Militante` no contexto dos templates
|
||||
- Ou remover referências diretas à classe
|
||||
|
||||
2. **Resolver problemas de campos inexistentes**
|
||||
- Verificar modelo `TipoMaterial` para campo `preco`
|
||||
- Ajustar templates conforme modelo real
|
||||
|
||||
3. **Corrigir roteamento admin**
|
||||
- Verificar registro do blueprint admin
|
||||
- Confirmar rota `/admin/dashboard`
|
||||
|
||||
### Média Prioridade
|
||||
1. **Implementar testes automatizados**
|
||||
2. **Adicionar logging estruturado**
|
||||
3. **Melhorar tratamento de erros**
|
||||
|
||||
### Baixa Prioridade
|
||||
1. **Otimizações de performance**
|
||||
2. **Melhorias na interface**
|
||||
3. **Documentação adicional**
|
||||
|
||||
## Conclusões
|
||||
|
||||
A estratégia final implementada resolve o problema fundamental identificado pelo usuário:
|
||||
|
||||
- ✅ **Menus não desaparecem**: Sempre visíveis independente de permissões
|
||||
- ✅ **Controle adequado**: No nível de dados, não de interface
|
||||
- ✅ **Robustez**: Templates nunca quebram, sempre renderizam
|
||||
- ✅ **Hierarquia respeitada**: Dados filtrados por nível organizacional
|
||||
- ✅ **Tesoureiros empoderados**: Acesso completo conforme solicitado
|
||||
|
||||
A arquitetura agora segue o padrão correto onde a interface permanece consistente e o controle de acesso acontece de forma transparente no backend, proporcionando uma experiência de usuário fluida e segura.
|
||||
@@ -1,365 +0,0 @@
|
||||
# Estratégia de Controle de Permissões Granular
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Esta documentação descreve a estratégia implementada para controle de permissões granular no sistema, permitindo que usuários vejam apenas dados e elementos para os quais têm autorização, sem quebrar templates ou causar erros.
|
||||
|
||||
## Arquitetura da Solução
|
||||
|
||||
### 1. Context Processors
|
||||
|
||||
**Arquivo**: `functions/template_helpers.py`
|
||||
|
||||
Os context processors disponibilizam automaticamente as permissões do usuário em todos os templates:
|
||||
|
||||
```python
|
||||
# Disponível em todos os templates
|
||||
user_can('permission_name') # Verifica permissão específica
|
||||
user_has_role('role_name') # Verifica role específica
|
||||
is_admin # Booleano se é admin
|
||||
current_user_data # Dados completos do usuário
|
||||
```
|
||||
|
||||
### 2. Template Filters
|
||||
|
||||
Filtros Jinja2 para uso direto nos templates:
|
||||
|
||||
```jinja2
|
||||
{{ 'view_cell_data' | has_permission }}
|
||||
{{ militantes | safe_data('view_cell_data', []) }}
|
||||
{{ 'militante' | can_manage }}
|
||||
```
|
||||
|
||||
### 3. Safe Data Controllers
|
||||
|
||||
Decorators que retornam dados vazios em caso de falta de permissão:
|
||||
|
||||
```python
|
||||
@safe_data_controller(Permission.VIEW_CELL_DATA, empty_data={'militantes': []})
|
||||
def listar():
|
||||
# Lógica normal do controller
|
||||
return render_template('template.html', militantes=militantes)
|
||||
```
|
||||
|
||||
### 4. Template Macros
|
||||
|
||||
Componentes reutilizáveis para elementos condicionais:
|
||||
|
||||
```jinja2
|
||||
{% from 'components/permission_wrapper.html' import permission_button %}
|
||||
{{ permission_button('create_cell_member', url_for('militante.novo'), 'Novo Militante') }}
|
||||
```
|
||||
|
||||
## Implementação por Camadas
|
||||
|
||||
### Camada 1: Controllers (Backend)
|
||||
|
||||
```python
|
||||
# Filtragem de dados baseada em permissões
|
||||
if current_user.is_admin:
|
||||
# Admin vê todos os dados
|
||||
militantes = query.all()
|
||||
elif current_user.has_permission(Permission.VIEW_CR_REPORTS):
|
||||
# CR vê apenas do seu CR
|
||||
militantes = query.filter(cr_id=current_user.cr_id).all()
|
||||
else:
|
||||
# Sem permissão - lista vazia
|
||||
militantes = []
|
||||
```
|
||||
|
||||
### Camada 2: Templates (Frontend)
|
||||
|
||||
```jinja2
|
||||
<!-- Menu só aparece se tiver permissão -->
|
||||
{% if user_can('view_cell_data') %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('militante.listar') }}">Militantes</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Dados condicionais -->
|
||||
{% if user_can('view_cell_data') %}
|
||||
{% if militantes %}
|
||||
<!-- Exibir tabela -->
|
||||
{% else %}
|
||||
<div class="alert alert-info">Nenhum dado disponível</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">Sem permissão para visualizar</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Camada 3: JavaScript (Interação)
|
||||
|
||||
```javascript
|
||||
// Verificações no frontend
|
||||
if (userPermissions.includes('manage_cell_members')) {
|
||||
// Habilitar funcionalidades de edição
|
||||
enableEditFeatures();
|
||||
}
|
||||
```
|
||||
|
||||
## Níveis de Permissão
|
||||
|
||||
### 1. Visualização de Dados
|
||||
|
||||
- `view_own_data`: Apenas próprios dados
|
||||
- `view_cell_data`: Dados da célula
|
||||
- `view_sector_reports`: Dados do setor
|
||||
- `view_cr_reports`: Dados do CR
|
||||
- `view_cc_reports`: Dados nacionais
|
||||
|
||||
### 2. Gerenciamento
|
||||
|
||||
- `manage_cell_members`: Gerenciar membros da célula
|
||||
- `manage_sector_cells`: Gerenciar células do setor
|
||||
- `create_cell_member`: Criar novos membros
|
||||
- `register_cell_payment`: Registrar pagamentos
|
||||
|
||||
### 3. Administração
|
||||
|
||||
- `system_config`: Configurações do sistema
|
||||
- `manage_cc_crs`: Gerenciar CRs
|
||||
- `create_cc_cr`: Criar novos CRs
|
||||
|
||||
## Regras Especiais de Permissão
|
||||
|
||||
### 1. Administrador (Admin)
|
||||
- **Acesso Total**: Tem todas as permissões do sistema
|
||||
- **Bypass de Verificações**: Sempre retorna `true` para qualquer verificação de permissão
|
||||
- **Acesso a Configurações**: Pode configurar o sistema e gerenciar usuários
|
||||
|
||||
### 2. Tesoureiro
|
||||
- **Regra Especial**: Tesoureiro pode fazer tudo que o secretário da instância pode fazer
|
||||
- **Permissões Automáticas**: Quando um militante tem responsabilidade de `TESOUREIRO`, automaticamente recebe:
|
||||
- `view_cell_data`: Visualizar dados da célula
|
||||
- `manage_cell_members`: Gerenciar membros da célula
|
||||
- `create_cell_member`: Criar novos membros
|
||||
- `view_cell_reports`: Visualizar relatórios da célula
|
||||
- `manage_cell_reports`: Gerenciar relatórios da célula
|
||||
- `register_cell_payment`: Registrar pagamentos da célula
|
||||
|
||||
### 3. Hierarquia de Instâncias
|
||||
- **Célula** → **Setor** → **CR** → **CC**
|
||||
- Usuários de níveis superiores têm acesso aos dados dos níveis inferiores
|
||||
- Secretários podem gerenciar todas as instâncias de seu nível e abaixo
|
||||
|
||||
## Padrões de Uso
|
||||
|
||||
### 1. Menus Condicionais
|
||||
|
||||
```jinja2
|
||||
{% if user_can('view_cell_data') %}
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" data-bs-toggle="dropdown">
|
||||
Militantes
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('militante.listar') }}">Listar</a></li>
|
||||
{% if user_can('create_cell_member') %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('militante.novo') }}">Novo</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 2. Botões Condicionais
|
||||
|
||||
```jinja2
|
||||
{% if user_can('create_cell_member') %}
|
||||
<a href="{{ url_for('militante.novo') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus me-2"></i>Novo Militante
|
||||
</a>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 3. Dados Filtrados
|
||||
|
||||
```jinja2
|
||||
{% if user_can('view_cell_data') %}
|
||||
{% if militantes %}
|
||||
<table class="table">
|
||||
<!-- Tabela com dados -->
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info">Nenhum militante encontrado</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
Você não tem permissão para visualizar estes dados
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 4. Formulários Condicionais
|
||||
|
||||
```jinja2
|
||||
{% if user_can('create_cell_member') %}
|
||||
<form method="POST" action="{{ url_for('militante.criar') }}">
|
||||
<!-- Campos do formulário -->
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
Você não tem permissão para criar militantes
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## Tratamento de Erros
|
||||
|
||||
### 1. Dados Não Encontrados
|
||||
|
||||
```jinja2
|
||||
{% if user_can('view_cell_data') %}
|
||||
{% if data %}
|
||||
<!-- Exibir dados -->
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Nenhum registro encontrado
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 2. Permissão Negada
|
||||
|
||||
```jinja2
|
||||
{% if not user_can('required_permission') %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
Você não tem permissão para acessar esta funcionalidade
|
||||
</div>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 3. Fallbacks Graceful
|
||||
|
||||
```python
|
||||
# Controller com fallback
|
||||
@safe_data_controller('view_cell_data', empty_data={'militantes': []})
|
||||
def listar():
|
||||
# Se não tiver permissão, retorna lista vazia
|
||||
# Template não quebra, apenas não mostra dados
|
||||
pass
|
||||
```
|
||||
|
||||
## Vantagens da Estratégia
|
||||
|
||||
### 1. **Segurança por Camadas**
|
||||
- Verificação no backend (controllers)
|
||||
- Verificação no frontend (templates)
|
||||
- Verificação no JavaScript (interação)
|
||||
|
||||
### 2. **Graceful Degradation**
|
||||
- Templates nunca quebram
|
||||
- Dados vazios em vez de erros
|
||||
- Mensagens informativas para usuário
|
||||
|
||||
### 3. **Flexibilidade**
|
||||
- Permissões granulares
|
||||
- Fácil de estender
|
||||
- Reutilização de componentes
|
||||
|
||||
### 4. **Manutenibilidade**
|
||||
- Lógica centralizada
|
||||
- Padrões consistentes
|
||||
- Fácil debugging
|
||||
|
||||
### 5. **UX Melhorada**
|
||||
- Interface adapta-se às permissões
|
||||
- Sem elementos inacessíveis visíveis
|
||||
- Feedback claro sobre limitações
|
||||
|
||||
## Exemplo Completo de Implementação
|
||||
|
||||
### Controller
|
||||
|
||||
```python
|
||||
@militante_bp.route("/militantes")
|
||||
@require_login
|
||||
@safe_data_controller(Permission.VIEW_CELL_DATA, empty_data={'militantes': []})
|
||||
def listar():
|
||||
# Filtragem baseada em permissões
|
||||
if current_user.is_admin:
|
||||
militantes = query.all()
|
||||
elif current_user.has_permission(Permission.VIEW_CELL_DATA):
|
||||
militantes = query.filter(celula_id=current_user.celula_id).all()
|
||||
else:
|
||||
militantes = []
|
||||
|
||||
return render_template('militantes.html', militantes=militantes)
|
||||
```
|
||||
|
||||
### Template
|
||||
|
||||
```jinja2
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Militantes</h2>
|
||||
|
||||
{% if user_can('create_cell_member') %}
|
||||
<a href="{{ url_for('militante.novo') }}" class="btn btn-success">
|
||||
<i class="fas fa-plus me-2"></i>Novo Militante
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user_can('view_cell_data') %}
|
||||
{% if militantes %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Email</th>
|
||||
{% if user_can('manage_cell_members') %}
|
||||
<th>Ações</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for militante in militantes %}
|
||||
<tr>
|
||||
<td>{{ militante.nome }}</td>
|
||||
<td>{{ militante.email }}</td>
|
||||
{% if user_can('manage_cell_members') %}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary">Editar</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Nenhum militante encontrado
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-lock me-2"></i>
|
||||
Você não tem permissão para visualizar militantes
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
## Conclusão
|
||||
|
||||
Esta estratégia garante que:
|
||||
|
||||
1. **Nunca há erros de template** - dados sempre estão disponíveis (mesmo que vazios)
|
||||
2. **Segurança é mantida** - usuários só veem o que podem
|
||||
3. **UX é preservada** - interface clara sobre limitações
|
||||
4. **Código é limpo** - padrões reutilizáveis e consistentes
|
||||
5. **Manutenção é fácil** - lógica centralizada e bem documentada
|
||||
6. **Tesoureiros têm poder adequado** - podem fazer tudo que secretários fazem
|
||||
|
||||
A implementação permite desenvolvimento ágil sem comprometer segurança ou experiência do usuário.
|
||||
26
docs/rbac.md
26
docs/rbac.md
@@ -109,22 +109,26 @@ CREATE TABLE user_roles (
|
||||
- `manage_cell_members`: Gerenciar membros da célula
|
||||
- `create_cell_member`: Criar novos membros na célula
|
||||
- `view_cell_reports`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
### Permissões de Setor
|
||||
- `manage_sector_cells`: Gerenciar células do setor
|
||||
- `create_sector_cell`: Criar novas células no setor
|
||||
- `view_sector_reports`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
### Permissões de CR
|
||||
- `manage_cr_sectors`: Gerenciar setores do CR
|
||||
- `create_cr_sector`: Criar novos setores no CR
|
||||
- `view_cr_reports`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
### Permissões de CC
|
||||
- `manage_cc_crs`: Gerenciar CRs
|
||||
- `create_cc_cr`: Criar novos CRs
|
||||
- `view_cc_reports`: Visualizar relatórios nacionais
|
||||
- `system_config`: Configurar o sistema
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
|
||||
## Uso no Código
|
||||
|
||||
@@ -166,12 +170,12 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
||||
- `MANAGE_CELL_MEMBERS`: Gerenciar membros da célula
|
||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CELL_DATA`: Visualizar dados da célula
|
||||
- `VIEW_CELL_REPORTS`: Visualizar relatórios da célula
|
||||
- `REGISTER_CELL_PAYMENT`: Registrar pagamentos da célula
|
||||
- `REGISTER_CELL_RECEIPT`: Registrar comprovantes da célula
|
||||
|
||||
- **Militante**:
|
||||
- `VIEW_OWN_DATA`: Visualizar apenas seus próprios dados
|
||||
@@ -180,32 +184,32 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_SECTOR_CELLS`: Gerenciar células do setor
|
||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_SECTOR_REPORTS`: Visualizar relatórios do setor
|
||||
- `REGISTER_SECTOR_PAYMENT`: Registrar pagamentos do setor
|
||||
- `REGISTER_SECTOR_RECEIPT`: Registrar comprovantes do setor
|
||||
|
||||
### CR
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_CR_SECTORS`: Gerenciar setores do CR
|
||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CR_REPORTS`: Visualizar relatórios do CR
|
||||
- `REGISTER_CR_PAYMENT`: Registrar pagamentos do CR
|
||||
- `REGISTER_CR_RECEIPT`: Registrar comprovantes do CR
|
||||
|
||||
### CC
|
||||
- **Secretário(a)**:
|
||||
- `MANAGE_CC_CRS`: Gerenciar CRs
|
||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
- `SYSTEM_CONFIG`: Configurar o sistema
|
||||
|
||||
- **Tesoureiro(a)**:
|
||||
- `VIEW_CC_REPORTS`: Visualizar relatórios do CC
|
||||
- `REGISTER_CC_PAYMENT`: Registrar pagamentos do CC
|
||||
- `REGISTER_CC_RECEIPT`: Registrar comprovantes do CC
|
||||
|
||||
## Regras de Acesso a Dados
|
||||
|
||||
@@ -214,10 +218,10 @@ O sistema possui uma estrutura hierárquica com os seguintes níveis:
|
||||
- Secretários e tesoureiros podem ver dados de sua instância
|
||||
- O CC tem acesso a todos os dados
|
||||
|
||||
2. **Registro de Pagamentos**:
|
||||
- Apenas tesoureiros e secretários podem registrar pagamentos
|
||||
2. **Registro de Comprovantes**:
|
||||
- Apenas tesoureiros e secretários podem registrar comprovantes
|
||||
- O registro é restrito à instância do usuário
|
||||
- O CC pode registrar pagamentos em qualquer nível
|
||||
- O CC pode registrar comprovantes em qualquer nível
|
||||
|
||||
## Implementação Técnica
|
||||
|
||||
|
||||
@@ -1,321 +0,0 @@
|
||||
# Redis Cache Setup and Usage
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the Redis cache implementation for the Flask application, including setup, configuration, and usage patterns.
|
||||
|
||||
## Architecture
|
||||
|
||||
The application now uses Redis for caching to improve performance and reduce database load. The cache layer is implemented with the following components:
|
||||
|
||||
- **Redis Server**: Running in Docker container
|
||||
- **Cache Service**: Python service for Redis operations
|
||||
- **Cached Decorators**: For automatic function result caching
|
||||
- **Cache Invalidation**: Automatic cache clearing on data changes
|
||||
|
||||
## Docker Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker and Docker Compose installed
|
||||
- Port 6379 available for Redis
|
||||
- Port 5000 available for Flask application
|
||||
|
||||
### Quick Start
|
||||
|
||||
1. **Start the entire stack:**
|
||||
```bash
|
||||
make dev-up
|
||||
```
|
||||
|
||||
2. **Check status:**
|
||||
```bash
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
3. **View logs:**
|
||||
```bash
|
||||
make docker-logs
|
||||
```
|
||||
|
||||
4. **Check cache status:**
|
||||
```bash
|
||||
make cache-status
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- ADMIN_OTP_SECRET=JBSWY3DPEHPK3PXP # Valid base32 format
|
||||
```
|
||||
|
||||
### Redis Configuration
|
||||
|
||||
```yaml
|
||||
# Redis service configuration
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
## Cache Service Implementation
|
||||
|
||||
### Service Structure
|
||||
|
||||
```python
|
||||
# services/cache_service.py
|
||||
class CacheService:
|
||||
def __init__(self):
|
||||
self.redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
||||
self.redis = None
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Establish Redis connection with retry logic"""
|
||||
max_retries = 5
|
||||
retry_delay = 2
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
self.redis = redis.from_url(self.redis_url)
|
||||
self.redis.ping()
|
||||
return True
|
||||
except Exception as e:
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(retry_delay)
|
||||
retry_delay *= 2
|
||||
return False
|
||||
```
|
||||
|
||||
### Cache Keys
|
||||
|
||||
```python
|
||||
# Cache key patterns
|
||||
class CacheKeys:
|
||||
DASHBOARD_STATS = "dashboard:stats"
|
||||
MILITANTE_STATS = "dashboard:militante_stats"
|
||||
FINANCIAL_STATS = "dashboard:financial_stats"
|
||||
MILITANTES_LIST = "militantes:list"
|
||||
PAGAMENTOS_LIST = "pagamentos:list"
|
||||
```
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Caching Decorators
|
||||
|
||||
```python
|
||||
# Example: Caching dashboard statistics
|
||||
@cached(expire=300, key_prefix="dashboard") # 5 minutes
|
||||
def get_dashboard_stats():
|
||||
# Expensive database query
|
||||
return stats
|
||||
|
||||
# Example: Cache invalidation
|
||||
@invalidate_cache_pattern("dashboard:*")
|
||||
def update_dashboard_data():
|
||||
# Update data and invalidate cache
|
||||
pass
|
||||
```
|
||||
|
||||
### Manual Cache Operations
|
||||
|
||||
```python
|
||||
# Get cached data
|
||||
stats = cache_service.get(CacheKeys.DASHBOARD_STATS)
|
||||
|
||||
# Set cached data
|
||||
cache_service.set(CacheKeys.DASHBOARD_STATS, data, expire=300)
|
||||
|
||||
# Delete cached data
|
||||
cache_service.delete(CacheKeys.DASHBOARD_STATS)
|
||||
|
||||
# Clear all cache
|
||||
cache_service.clear_all()
|
||||
```
|
||||
|
||||
## Performance Benefits
|
||||
|
||||
### Before Redis Cache
|
||||
- Dashboard queries: 500-800ms
|
||||
- Militante list: 200-400ms
|
||||
- Database load: High
|
||||
|
||||
### After Redis Cache
|
||||
- Dashboard queries: 50-100ms (80% improvement)
|
||||
- Militante list: 20-50ms (85% improvement)
|
||||
- Database load: Reduced by 70%
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check Redis health
|
||||
make cache-status
|
||||
|
||||
# Monitor Redis memory usage
|
||||
docker-compose exec redis redis-cli INFO memory
|
||||
|
||||
# View cache keys
|
||||
make cache-keys
|
||||
```
|
||||
|
||||
### Cache Management
|
||||
|
||||
```bash
|
||||
# Clear all cache
|
||||
make cache-clear
|
||||
|
||||
# Warm up cache
|
||||
make cache-warmup
|
||||
|
||||
# Monitor cache performance
|
||||
make cache-monitor
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Redis Connection Failed**
|
||||
```bash
|
||||
# Check Redis logs
|
||||
docker-compose logs redis
|
||||
|
||||
# Restart Redis
|
||||
docker-compose restart redis
|
||||
```
|
||||
|
||||
2. **Cache Not Working**
|
||||
```bash
|
||||
# Check cache status
|
||||
make cache-status
|
||||
|
||||
# Clear and warm up cache
|
||||
make cache-clear
|
||||
make cache-warmup
|
||||
```
|
||||
|
||||
3. **Memory Issues**
|
||||
```bash
|
||||
# Check memory usage
|
||||
docker-compose exec redis redis-cli INFO memory
|
||||
|
||||
# Clear cache
|
||||
make cache-clear
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
- **Application logs**: `logs/controles.log`
|
||||
- **Cache logs**: `logs/cache.log`
|
||||
- **Redis logs**: `docker-compose logs redis`
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Cache Key Design
|
||||
- Use descriptive, hierarchical keys
|
||||
- Include version numbers for cache invalidation
|
||||
- Use consistent naming conventions
|
||||
|
||||
### TTL (Time To Live)
|
||||
- Dashboard data: 5 minutes
|
||||
- User data: 30 minutes
|
||||
- Static data: 1 hour
|
||||
- Configuration: 24 hours
|
||||
|
||||
### Cache Invalidation
|
||||
- Invalidate on data changes
|
||||
- Use pattern-based invalidation
|
||||
- Consider cache warming strategies
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Redis Security
|
||||
- Redis is only accessible within Docker network
|
||||
- No external access by default
|
||||
- Consider Redis password for production
|
||||
|
||||
### Data Privacy
|
||||
- Cache contains sensitive user data
|
||||
- Implement proper cache expiration
|
||||
- Clear cache on logout
|
||||
|
||||
## Production Considerations
|
||||
|
||||
### Scaling
|
||||
- Consider Redis Cluster for high availability
|
||||
- Implement cache sharding for large datasets
|
||||
- Monitor cache hit rates
|
||||
|
||||
### Backup
|
||||
- Redis AOF (Append Only File) enabled
|
||||
- Consider Redis RDB snapshots
|
||||
- Implement cache backup strategies
|
||||
|
||||
## Recent Fixes Applied
|
||||
|
||||
### ✅ OTP Secret Format
|
||||
- **Problem**: Invalid base32 format causing authentication errors
|
||||
- **Solution**: Changed to `JBSWY3DPEHPK3PXP` (valid base32)
|
||||
- **Impact**: Fixed login authentication
|
||||
|
||||
### ✅ Redis Connection Retry
|
||||
- **Problem**: Connection failures during startup
|
||||
- **Solution**: Implemented exponential backoff retry logic
|
||||
- **Impact**: Improved startup reliability
|
||||
|
||||
### ✅ QR Code Permissions
|
||||
- **Problem**: Permission denied when saving QR codes
|
||||
- **Solution**: Multiple fallback paths, save to `/tmp/`
|
||||
- **Impact**: Admin QR code generation works correctly
|
||||
|
||||
### ✅ Docker Network Configuration
|
||||
- **Problem**: Services couldn't communicate
|
||||
- **Solution**: Explicit network configuration
|
||||
- **Impact**: Redis and app can communicate properly
|
||||
|
||||
## Current Status
|
||||
|
||||
✅ **Fully Operational**
|
||||
- Redis cache running and healthy
|
||||
- Application connecting successfully
|
||||
- Cache performance improvements active
|
||||
- All authentication issues resolved
|
||||
- QR code generation working
|
||||
- 30 test users created successfully
|
||||
|
||||
## Commands Reference
|
||||
|
||||
```bash
|
||||
# Development
|
||||
make dev-up # Start development environment
|
||||
make dev-down # Stop development environment
|
||||
make docker-logs # View application logs
|
||||
|
||||
# Cache Management
|
||||
make cache-status # Check Redis status
|
||||
make cache-clear # Clear all cache
|
||||
make cache-keys # List cache keys
|
||||
make cache-warmup # Warm up cache
|
||||
make cache-monitor # Monitor cache performance
|
||||
|
||||
# Docker Operations
|
||||
make docker-build # Rebuild containers
|
||||
make docker-restart # Restart services
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: June 2025
|
||||
**Status**: ✅ Production Ready
|
||||
@@ -1,17 +1,17 @@
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Configurar caminho do banco de dados
|
||||
db_path = Path(__file__).resolve().parents[1] / 'data' / 'database.db'
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
db_fallback = f'sqlite:///{db_path}'
|
||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||
db_dir.mkdir(parents=True, exist_ok=True)
|
||||
db_path = db_dir / 'database.db'
|
||||
|
||||
# Configurar SQLite com opções para melhor concorrência
|
||||
engine = create_engine(
|
||||
os.environ.get('DATABASE_URL', db_fallback),
|
||||
f'sqlite:///{db_path}',
|
||||
connect_args={
|
||||
'timeout': 30, # Tempo de espera em segundos
|
||||
'check_same_thread': False # Permite acesso de múltiplas threads
|
||||
@@ -23,15 +23,11 @@ engine = create_engine(
|
||||
Session = sessionmaker(bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db_session():
|
||||
"""Retorna uma nova sessão do banco de dados com PRAGMAs configuradas"""
|
||||
db_session = Session()
|
||||
def get_db_connection():
|
||||
"""Retorna uma nova sessão do banco de dados"""
|
||||
session = Session()
|
||||
try:
|
||||
# Configurar SQLite para melhor tratamento de concorrência
|
||||
db_session.execute(text("PRAGMA journal_mode=WAL"))
|
||||
db_session.execute(text("PRAGMA busy_timeout=5000"))
|
||||
return db_session
|
||||
return session
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
db_session.close()
|
||||
session.rollback()
|
||||
raise e
|
||||
@@ -1,10 +1,10 @@
|
||||
from datetime import datetime, UTC
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from functions.database import get_db_session, Controle as ControleModel
|
||||
from functions.database import get_db_connection, Controle as ControleModel
|
||||
|
||||
class Controle:
|
||||
def __init__(self):
|
||||
self.db = get_db_session()
|
||||
self.db = get_db_connection()
|
||||
|
||||
def registrar_controle(self, militante_id: int, tipo: str, valor: float, observacao: str = None) -> bool:
|
||||
"""
|
||||
|
||||
@@ -1,31 +1,57 @@
|
||||
from datetime import datetime, timedelta, UTC
|
||||
from datetime import datetime, timedelta
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text, Float
|
||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||
import os
|
||||
import pyotp
|
||||
from pathlib import Path
|
||||
from sqlalchemy.pool import NullPool
|
||||
import secrets
|
||||
from flask_mail import Message
|
||||
from flask import url_for
|
||||
import enum
|
||||
from flask_login import UserMixin
|
||||
from .rbac import Role
|
||||
from .base import Base, get_db_session
|
||||
from .rbac import Role, Permission, role_permissions, user_roles
|
||||
from .base import Base, engine, Session
|
||||
import logging
|
||||
|
||||
# Configurar caminho do banco de dados
|
||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||
db_dir.mkdir(parents=True, exist_ok=True)
|
||||
db_path = db_dir / 'database.db'
|
||||
|
||||
DATABASE_URL = f"sqlite:///{db_path}"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
def get_db_connection():
|
||||
"""Retorna uma nova sessão do banco de dados"""
|
||||
Session = sessionmaker(bind=engine)
|
||||
db = Session()
|
||||
|
||||
try:
|
||||
# Configurar SQLite para melhor tratamento de concorrência
|
||||
db.execute(text("PRAGMA journal_mode=WAL"))
|
||||
db.execute(text("PRAGMA busy_timeout=5000"))
|
||||
return db
|
||||
except:
|
||||
db.close()
|
||||
raise
|
||||
|
||||
def execute_query(query, params=None):
|
||||
"""
|
||||
Executa uma query usando SQLAlchemy
|
||||
"""
|
||||
db = get_db_session()
|
||||
session = get_db_connection()
|
||||
try:
|
||||
result = db.execute(query, params)
|
||||
db.commit()
|
||||
result = session.execute(query, params)
|
||||
session.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
session.close()
|
||||
|
||||
class EstadoMilitante(enum.Enum):
|
||||
ATIVO = 'ativo'
|
||||
@@ -148,7 +174,7 @@ class Militante(Base):
|
||||
quadro_orientador = Column(Boolean, default=False)
|
||||
# Campos para Aspirante
|
||||
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
|
||||
data_inicio_aspirante = Column(DateTime, default=datetime.now(UTC))
|
||||
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
||||
avaliacao_aspirante = Column(Text)
|
||||
data_avaliacao_aspirante = Column(DateTime)
|
||||
|
||||
@@ -161,9 +187,11 @@ class Militante(Base):
|
||||
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
||||
pagamentos = relationship("Pagamento", back_populates="militante")
|
||||
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
|
||||
vendas_jornais = relationship("VendaJornalAvulso", back_populates="militante")
|
||||
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
|
||||
vendas_jornais = relationship("VendaJornal", back_populates="militante")
|
||||
assinaturas = relationship("AssinaturaJornal", back_populates="militante")
|
||||
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
|
||||
comprovantes = relationship("Comprovante", back_populates="militante")
|
||||
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
|
||||
|
||||
# Constantes para responsabilidades
|
||||
SECRETARIO = 1
|
||||
@@ -251,7 +279,7 @@ class Militante(Base):
|
||||
def generate_username(self):
|
||||
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
|
||||
from sqlalchemy import func
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Pega o primeiro nome
|
||||
primeiro_nome = self.nome.split()[0].lower()
|
||||
@@ -335,7 +363,7 @@ class VendaJornalAvulso(Base):
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_venda = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="vendas_jornais")
|
||||
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")
|
||||
|
||||
class AssinaturaAnual(Base):
|
||||
__tablename__ = 'assinaturas_anuais'
|
||||
@@ -428,7 +456,7 @@ class Usuario(Base, UserMixin):
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||
session_timeout = Column(Integer, default=30)
|
||||
tipo = Column(String(17), nullable=False)
|
||||
ultima_atividade = Column(DateTime, default=datetime.now(UTC))
|
||||
ultima_atividade = Column(DateTime, default=datetime.utcnow)
|
||||
# Relacionamento com militante
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
||||
@@ -447,7 +475,7 @@ class Usuario(Base, UserMixin):
|
||||
self.ativo = True
|
||||
self.session_timeout = 30
|
||||
self.tipo = "USUARIO"
|
||||
self.ultima_atividade = datetime.now(UTC)
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
@@ -456,24 +484,23 @@ class Usuario(Base, UserMixin):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def update_last_activity(self):
|
||||
self.ultima_atividade = datetime.now(UTC)
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def is_session_expired(self):
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.now(UTC) - self.ultima_atividade
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def check_session_timeout(self):
|
||||
"""Verifica se a sessão do usuário expirou"""
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.now(UTC) - self.ultima_atividade
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def has_permission(self, permission_name):
|
||||
"""Verifica se o usuário tem uma permissão específica"""
|
||||
# TODO: (talvez) remover, confirmar admin por RBAC
|
||||
if self.is_admin: # Se for admin, tem todas as permissões
|
||||
return True
|
||||
|
||||
@@ -485,66 +512,49 @@ class Usuario(Base, UserMixin):
|
||||
return False
|
||||
|
||||
def has_role(self, role_nivel):
|
||||
"""Verifica se o usuário tem um nível de role específico."""
|
||||
"""Verifica se o usuário tem um determinado nível de role"""
|
||||
for role in self.roles:
|
||||
if role.nivel == role_nivel:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_highest_role(self):
|
||||
"""Retorna a role de maior nível do usuário."""
|
||||
if not self.roles:
|
||||
return None
|
||||
return max(self.roles, key=lambda role: role.nivel)
|
||||
|
||||
def has_minimum_role(self, min_level):
|
||||
"""Verifica se o usuário possui ao menos o nível informado."""
|
||||
highest_role = self.get_highest_role()
|
||||
return bool(highest_role and highest_role.nivel >= min_level)
|
||||
|
||||
def generate_otp_secret(self):
|
||||
"""Gera um novo segredo OTP para o usuário"""
|
||||
self.otp_secret = pyotp.random_base32()
|
||||
return self.otp_secret
|
||||
|
||||
def get_otp_uri(self):
|
||||
"""Gera a URI para autenticação em duas etapas"""
|
||||
if not self.otp_secret:
|
||||
raise ValueError(f"OTP não configurado para {self.username}")
|
||||
|
||||
totp = pyotp.TOTP(self.otp_secret)
|
||||
return totp.provisioning_uri(
|
||||
name=self.username,
|
||||
self.otp_secret = pyotp.random_base32()
|
||||
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
|
||||
self.username,
|
||||
issuer_name="Sistema de Controles"
|
||||
)
|
||||
|
||||
def verify_otp(self, code):
|
||||
"""Verifica se um código OTP é válido"""
|
||||
if not self.otp_secret:
|
||||
raise ValueError(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
||||
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
||||
return False
|
||||
|
||||
print(f"Verificando OTP para usuário {self.username}")
|
||||
print(f"OTP Secret: {self.otp_secret}")
|
||||
print(f"Código fornecido: {code}")
|
||||
|
||||
totp = pyotp.TOTP(self.otp_secret)
|
||||
totp = pyotp.totp.TOTP(self.otp_secret)
|
||||
is_valid = totp.verify(code)
|
||||
|
||||
print(f"Resultado da verificação: {'Válido' if is_valid else 'Inválido'}")
|
||||
print(f"Tempo atual: {datetime.now(UTC)}")
|
||||
print(f"Período atual: {totp.timecode(datetime.now(UTC))}")
|
||||
print(f"Tempo atual: {datetime.utcnow()}")
|
||||
print(f"Período atual: {totp.timecode(datetime.utcnow())}")
|
||||
|
||||
return is_valid
|
||||
|
||||
def logout(self):
|
||||
"""Registra o logout do usuário"""
|
||||
self.ultimo_logout = datetime.now(UTC)
|
||||
self.ultimo_logout = datetime.utcnow()
|
||||
self.motivo_logout = "Logout manual"
|
||||
self.ultima_atividade = None
|
||||
|
||||
def is_admin_user(self):
|
||||
"""Verifica se o usuário é admin"""
|
||||
return self.is_admin or any(role.nivel == Role.SECRETARIO_GERAL for role in self.roles)
|
||||
return self.is_admin or any(role.nome == "admin" for role in self.roles)
|
||||
|
||||
class PagamentoCelula(Base):
|
||||
__tablename__ = 'pagamentos_celula'
|
||||
@@ -615,5 +625,142 @@ class TransacaoPIX(Base):
|
||||
status = Column(String(20)) # Pendente, Pago, Expirado
|
||||
qr_code = Column(Text)
|
||||
pagamento_id = Column(Integer, ForeignKey('pagamentos.id'))
|
||||
comprovante_id = Column(Integer, ForeignKey('comprovantes.id'))
|
||||
|
||||
pagamento = relationship("Pagamento", back_populates="transacoes_pix")
|
||||
comprovante = relationship("Comprovante", back_populates="transacoes_pix")
|
||||
|
||||
class TipoComprovante(Base):
|
||||
__tablename__ = 'tipos_comprovante'
|
||||
id = Column(Integer, primary_key=True)
|
||||
descricao = Column(String(50), nullable=False)
|
||||
valor = Column(Float, nullable=False)
|
||||
|
||||
class Comprovante(Base):
|
||||
__tablename__ = 'comprovantes'
|
||||
id = Column(Integer, primary_key=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
|
||||
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
|
||||
data_comprovante = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="comprovantes")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
|
||||
|
||||
class VendaJornal(Base):
|
||||
__tablename__ = 'vendas_jornais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
quantidade = Column(Integer, nullable=False)
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_venda = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="vendas_jornais")
|
||||
|
||||
class AssinaturaJornal(Base):
|
||||
__tablename__ = 'assinaturas_jornais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
|
||||
quantidade = Column(Integer, nullable=False)
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_inicio = Column(Date, nullable=False)
|
||||
data_fim = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="assinaturas")
|
||||
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
|
||||
|
||||
def init_database():
|
||||
"""Inicializa o banco de dados com dados básicos"""
|
||||
print("Inicializando banco de dados...")
|
||||
|
||||
session = get_db_connection()
|
||||
try:
|
||||
# Criar todas as tabelas
|
||||
Base.metadata.drop_all(engine) # Remover todas as tabelas existentes
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Criar roles padrão
|
||||
roles = [
|
||||
("Administrador", Role.SECRETARIO_GERAL),
|
||||
("Secretário", Role.SECRETARIO_CELULA),
|
||||
("Militante", Role.MILITANTE_BASICO)
|
||||
]
|
||||
|
||||
for nome, nivel in roles:
|
||||
if not session.query(Role).filter_by(nome=nome).first():
|
||||
role = Role(nome=nome, nivel=nivel)
|
||||
session.add(role)
|
||||
session.commit()
|
||||
|
||||
# Criar setores padrão
|
||||
setores = ["Setor 1", "Setor 2", "Setor 3"]
|
||||
for nome in setores:
|
||||
if not session.query(Setor).filter_by(nome=nome).first():
|
||||
setor = Setor(nome=nome)
|
||||
session.add(setor)
|
||||
session.commit()
|
||||
|
||||
# Criar comitês padrão
|
||||
comites = ["Comitê 1", "Comitê 2", "Comitê 3"]
|
||||
for nome in comites:
|
||||
if not session.query(ComiteCentral).filter_by(nome=nome).first():
|
||||
comite = ComiteCentral(nome=nome)
|
||||
session.add(comite)
|
||||
session.commit()
|
||||
|
||||
# Gerar OTP para admin
|
||||
admin_otp_secret = pyotp.random_base32()
|
||||
print(f"Novo OTP gerado: {admin_otp_secret}")
|
||||
|
||||
# Criar usuário admin
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
setor = session.query(Setor).first()
|
||||
|
||||
admin = Usuario(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
is_admin=True
|
||||
)
|
||||
admin.set_password("admin123")
|
||||
admin.tipo = "ADMIN"
|
||||
admin.otp_secret = admin_otp_secret
|
||||
admin.roles.append(admin_role)
|
||||
admin.setor = setor
|
||||
session.add(admin)
|
||||
session.commit()
|
||||
|
||||
# Gerar QR code
|
||||
totp = pyotp.totp.TOTP(admin_otp_secret)
|
||||
provisioning_uri = totp.provisioning_uri("admin", issuer_name="Sistema de Controles")
|
||||
|
||||
import qrcode
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(provisioning_uri)
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
img.save('admin_qr.png')
|
||||
|
||||
print("=== Usuário Admin Criado ===")
|
||||
print(f"Username: admin")
|
||||
print(f"Senha: admin123")
|
||||
print(f"Email: {admin.email}")
|
||||
print(f"OTP Secret: {admin_otp_secret}")
|
||||
print(f"QR Code: admin_qr.png")
|
||||
|
||||
# Importar e executar o seed após criar todas as dependências
|
||||
from seed_data import seed_database
|
||||
print("\nPopulando banco de dados com dados de teste...")
|
||||
seed_database()
|
||||
print("Dados de teste criados com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro na inicialização do banco: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_database()
|
||||
@@ -2,7 +2,7 @@ from functools import wraps
|
||||
from flask import session, redirect, url_for, flash
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy.orm import joinedload
|
||||
from .database import get_db_session, Usuario, Role
|
||||
from .database import get_db_connection, Usuario, Role
|
||||
from .rbac import Permission
|
||||
|
||||
def require_login(f):
|
||||
@@ -11,10 +11,38 @@ def require_login(f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'danger')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Executar a função diretamente sem try/catch
|
||||
return f(*args, **kwargs)
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Carregar o usuário com suas roles e permissões
|
||||
user = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles).joinedload(Role.permissions),
|
||||
joinedload(Usuario.militante),
|
||||
joinedload(Usuario.cr),
|
||||
joinedload(Usuario.setor),
|
||||
joinedload(Usuario.celula)
|
||||
).get(current_user.id)
|
||||
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'danger')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Atualiza timestamp da última atividade
|
||||
user.update_last_activity()
|
||||
db.commit()
|
||||
|
||||
# Substituir o current_user pelo usuário carregado
|
||||
setattr(current_user, '_get_current_object', lambda: user)
|
||||
|
||||
# Executar a função com o usuário carregado
|
||||
return f(*args, **kwargs)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
flash('Erro ao carregar dados do usuário.', 'danger')
|
||||
return redirect(url_for('login'))
|
||||
finally:
|
||||
db.close()
|
||||
return decorated_function
|
||||
|
||||
def require_permission(permission_name):
|
||||
@@ -24,9 +52,9 @@ def require_permission(permission_name):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
# Carregar o usuário com suas roles e permissões
|
||||
user = db.query(Usuario).options(
|
||||
@@ -39,7 +67,7 @@ def require_permission(permission_name):
|
||||
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not user.has_permission(permission_name):
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
@@ -58,22 +86,19 @@ def require_permission(permission_name):
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
def require_role(role_level):
|
||||
def require_role(role_name):
|
||||
"""Decorador para verificar se o usuário tem um papel específico"""
|
||||
if not isinstance(role_level, int):
|
||||
raise TypeError("require_role espera um nível numérico (int), use a classe Role.")
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(current_user.id)
|
||||
if not user or not user.has_role(role_level):
|
||||
if not user or not user.has_role(role_name):
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@@ -89,24 +114,22 @@ def require_role(role_level):
|
||||
|
||||
def require_minimum_role(min_level):
|
||||
"""Decorador para verificar se o usuário tem um papel com nível mínimo"""
|
||||
if not isinstance(min_level, int):
|
||||
raise TypeError("require_minimum_role espera um nível numérico de role (int).")
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Você precisa estar logado para acessar esta página.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(current_user.id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not user.has_minimum_role(min_level):
|
||||
highest_role = user.get_highest_role()
|
||||
if not highest_role or highest_role.nivel < min_level:
|
||||
flash('Você não tem permissão para acessar esta página.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@@ -127,7 +150,7 @@ def require_instance_permission(permission_name, instance_param):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
# Obtém o ID da instância dos argumentos da função
|
||||
instance_id = kwargs.get(instance_param)
|
||||
@@ -150,43 +173,32 @@ def require_instance_access(instance_type, instance_id):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_authenticated:
|
||||
flash('Por favor, faça login para acessar esta página.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return redirect(url_for('login'))
|
||||
|
||||
db = get_db_session()
|
||||
try:
|
||||
user = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles).joinedload(Role.permissions)
|
||||
).get(current_user.id)
|
||||
if not user:
|
||||
flash('Usuário não encontrado.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
# Verificar acesso baseado na instância do usuário
|
||||
if instance_type == 'celula':
|
||||
if not (current_user.celula_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a esta célula.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'setor':
|
||||
if not (current_user.setor_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este setor.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'cr':
|
||||
if not (current_user.cr_id == instance_id or
|
||||
current_user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este CR.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Verificar acesso baseado na instância do usuário
|
||||
if instance_type == 'celula':
|
||||
if not (user.celula_id == instance_id or
|
||||
user.has_permission(Permission.VIEW_SECTOR_REPORTS) or
|
||||
user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a esta célula.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'setor':
|
||||
if not (user.setor_id == instance_id or
|
||||
user.has_permission(Permission.VIEW_CR_REPORTS) or
|
||||
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este setor.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
elif instance_type == 'cr':
|
||||
if not (user.cr_id == instance_id or
|
||||
user.has_permission(Permission.VIEW_CC_REPORTS)):
|
||||
flash('Você não tem acesso a este CR.', 'error')
|
||||
return redirect(url_for('index'))
|
||||
# Atualiza timestamp da última atividade
|
||||
current_user.update_last_activity()
|
||||
db_session.commit()
|
||||
|
||||
# Atualiza timestamp da última atividade
|
||||
user.update_last_activity()
|
||||
db.commit()
|
||||
|
||||
return f(*args, **kwargs)
|
||||
finally:
|
||||
db.close()
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
@@ -133,183 +133,183 @@ class Permission(Base):
|
||||
|
||||
def init_rbac():
|
||||
"""Inicializa o sistema RBAC com roles e permissões básicas"""
|
||||
from .database import Usuario, get_db_session
|
||||
db = get_db_session()
|
||||
from .database import Usuario, get_db_connection
|
||||
session = get_db_connection()
|
||||
|
||||
try:
|
||||
# Criar role de administrador primeiro
|
||||
admin_role = db.query(Role).filter_by(nome="Administrador").first()
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||
db.add(admin_role)
|
||||
db.commit()
|
||||
session.add(admin_role)
|
||||
session.commit()
|
||||
|
||||
# Criar outras roles
|
||||
for nivel, nome in Role.get_roles_list():
|
||||
if nome != "Administrador": # Pular Administrador pois já foi criado
|
||||
role = db.query(Role).filter_by(nivel=nivel).first()
|
||||
role = session.query(Role).filter_by(nivel=nivel).first()
|
||||
if not role:
|
||||
role = Role(nome=nome, nivel=nivel)
|
||||
db.add(role)
|
||||
session.add(role)
|
||||
|
||||
# Criar permissões
|
||||
for nome, descricao in Permission.get_permissions_list():
|
||||
permission = db.query(Permission).filter_by(nome=nome).first()
|
||||
permission = session.query(Permission).filter_by(nome=nome).first()
|
||||
if not permission:
|
||||
permission = Permission(nome=nome, descricao=descricao)
|
||||
db.add(permission)
|
||||
session.add(permission)
|
||||
|
||||
db.commit()
|
||||
session.commit()
|
||||
|
||||
# Dar todas as permissões para o admin
|
||||
all_permissions = db.query(Permission).all()
|
||||
all_permissions = session.query(Permission).all()
|
||||
admin_role.permissions = all_permissions
|
||||
db.commit()
|
||||
session.commit()
|
||||
|
||||
# Buscar usuário admin e atribuir role de administrador
|
||||
admin_user = db.query(Usuario).filter_by(username="admin").first()
|
||||
admin_user = session.query(Usuario).filter_by(username="admin").first()
|
||||
if admin_user:
|
||||
if admin_role not in admin_user.roles:
|
||||
admin_user.roles = [admin_role] # Substituir roles existentes
|
||||
db.commit()
|
||||
session.commit()
|
||||
|
||||
# Mapear permissões para outros roles
|
||||
for role in db.query(Role).filter(Role.nome != "Administrador").all():
|
||||
for role in session.query(Role).filter(Role.nome != "Administrador").all():
|
||||
# Militante Básico
|
||||
if role.nivel == Role.MILITANTE_BASICO:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first()
|
||||
]
|
||||
|
||||
# Secretário de Célula
|
||||
elif role.nivel == Role.SECRETARIO_CELULA:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CELL_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro de Setor
|
||||
elif role.nivel == Role.MEMBRO_SETOR:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário de Setor
|
||||
elif role.nivel == Role.SECRETARIO_SETOR:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_SECTOR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro de CR
|
||||
elif role.nivel == Role.MEMBRO_CR:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário de CR
|
||||
elif role.nivel == Role.SECRETARIO_CR:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CR_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Membro do CC
|
||||
elif role.nivel == Role.MEMBRO_CC:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first()
|
||||
]
|
||||
|
||||
# Secretário Geral
|
||||
elif role.nivel == Role.SECRETARIO_GERAL:
|
||||
role.permissions = [
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||
db.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.EDIT_OWN_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_DATA).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_MEMBERS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CELL_MEMBER).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CELL_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_SECTOR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_SECTOR_CELLS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_SECTOR_CELL).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CR_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CR_SECTORS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CR_SECTOR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.VIEW_CC_REPORTS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.MANAGE_CC_CRS).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.CREATE_CC_CR).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.REGISTER_CC_PAYMENT).first(),
|
||||
session.query(Permission).filter_by(nome=Permission.SYSTEM_CONFIG).first()
|
||||
]
|
||||
|
||||
db.commit()
|
||||
session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao inicializar RBAC: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
session.close()
|
||||
@@ -1,53 +0,0 @@
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
def permission_context_processor():
|
||||
"""Context processor simples que disponibiliza informações básicas do usuário"""
|
||||
context = {
|
||||
'user_can': lambda permission: True, # Sempre True - controle é no nível de dados
|
||||
'user_has_role': lambda role: True, # Sempre True - controle é no nível de dados
|
||||
'is_admin': False,
|
||||
'current_user_data': None
|
||||
}
|
||||
|
||||
if current_user.is_authenticated:
|
||||
context.update({
|
||||
'is_admin': getattr(current_user, 'is_admin', False),
|
||||
'current_user_data': current_user
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def safe_render_helper():
|
||||
"""Helper que fornece dados seguros para templates"""
|
||||
return {
|
||||
'safe_data': lambda data, default=None: data if data is not None else (default or [])
|
||||
}
|
||||
|
||||
def init_template_filters(app):
|
||||
"""Inicializa filtros de template personalizados"""
|
||||
|
||||
@app.template_filter('safe_list')
|
||||
def safe_list_filter(value):
|
||||
"""Garante que o valor seja sempre uma lista"""
|
||||
if value is None:
|
||||
return []
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
@app.template_filter('safe_dict')
|
||||
def safe_dict_filter(value):
|
||||
"""Garante que o valor seja sempre um dicionário"""
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
return {}
|
||||
|
||||
@app.template_filter('safe_str')
|
||||
def safe_str_filter(value):
|
||||
"""Garante que o valor seja sempre uma string"""
|
||||
if value is None:
|
||||
return ""
|
||||
return str(value)
|
||||
23
functions/usuario.py
Normal file
23
functions/usuario.py
Normal file
@@ -0,0 +1,23 @@
|
||||
def get_permissoes_por_cargo(cargo_id):
|
||||
permissoes = {
|
||||
1: [ # Secretário Geral
|
||||
'gerenciar_relatorios_celula',
|
||||
'visualizar_relatorios_celula',
|
||||
'gerenciar_militantes',
|
||||
'gerenciar_tipos_comprovante'
|
||||
],
|
||||
2: [ # Admin
|
||||
'gerenciar_relatorios_celula',
|
||||
'visualizar_relatorios_celula',
|
||||
'gerenciar_militantes',
|
||||
'gerenciar_tipos_comprovante'
|
||||
],
|
||||
3: [ # Secretário Financeiro do Comitê Central
|
||||
'gerenciar_relatorios_celula',
|
||||
'visualizar_relatorios_celula',
|
||||
'gerenciar_militantes',
|
||||
'gerenciar_tipos_comprovante'
|
||||
],
|
||||
# ... existing code ...
|
||||
}
|
||||
return permissoes.get(cargo_id, [])
|
||||
19
init_db.py
Normal file
19
init_db.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from functions.database import init_database
|
||||
from functions.rbac import init_rbac
|
||||
from create_admin import create_admin_user
|
||||
from create_test_users import create_test_users
|
||||
|
||||
def init_system():
|
||||
print("Inicializando banco de dados...")
|
||||
init_database()
|
||||
|
||||
print("Inicializando sistema RBAC...")
|
||||
init_rbac()
|
||||
|
||||
print("Criando usuários iniciais...")
|
||||
create_admin_user()
|
||||
create_test_users()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_system()
|
||||
print("Sistema inicializado com sucesso!")
|
||||
58
init_system.py
Normal file
58
init_system.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from create_admin import create_admin
|
||||
from create_test_users import create_test_users
|
||||
from functions.database import get_db_connection, Usuario
|
||||
from functions.rbac import Role
|
||||
|
||||
def init_system():
|
||||
print("=== Inicializando Sistema ===")
|
||||
|
||||
# Criar admin
|
||||
print("\nCriando usuário admin...")
|
||||
create_admin()
|
||||
|
||||
# Criar usuários de teste
|
||||
print("\nCriando usuários de teste...")
|
||||
create_test_users()
|
||||
|
||||
# Verificar configuração
|
||||
print("\n=== Verificando Configuração ===")
|
||||
session = get_db_connection()
|
||||
try:
|
||||
# Verificar admin
|
||||
admin = session.query(Usuario).filter_by(username='admin').first()
|
||||
if admin:
|
||||
print("Admin: OK")
|
||||
print(f"OTP configurado: {'Sim' if admin.otp_secret else 'Não'}")
|
||||
else:
|
||||
print("Admin: FALHOU")
|
||||
|
||||
# Verificar usuários de teste
|
||||
test_users = ['aligner', 'tester', 'deployer']
|
||||
for username in test_users:
|
||||
user = session.query(Usuario).filter_by(username=username).first()
|
||||
if user:
|
||||
print(f"{username}: OK")
|
||||
print(f"OTP configurado: {'Sim' if user.otp_secret else 'Não'}")
|
||||
else:
|
||||
print(f"{username}: FALHOU")
|
||||
|
||||
print("\n=== Instruções ===")
|
||||
print("1. Use o aplicativo autenticador para configurar o OTP de cada usuário")
|
||||
print("2. Faça login com cada usuário para testar")
|
||||
print("3. Altere a senha no primeiro login")
|
||||
print("\nCredenciais:")
|
||||
print("Admin:")
|
||||
print(" Usuário: admin")
|
||||
print(" Senha: admin123")
|
||||
print("\nUsuários de teste:")
|
||||
print(" Usuário: aligner, tester, deployer")
|
||||
print(" Senha: Test123!@#")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao verificar configuração: {str(e)}")
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_system()
|
||||
@@ -1 +0,0 @@
|
||||
# Models package
|
||||
@@ -1,4 +0,0 @@
|
||||
from models.entities.assinatura_jornal import AssinaturaJornal as AssinaturaAnual
|
||||
|
||||
# This file is a compatibility layer for code that uses AssinaturaAnual
|
||||
# The class has been renamed to AssinaturaJornal
|
||||
18
models/entities/assinatura_jornal.py
Normal file
18
models/entities/assinatura_jornal.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class AssinaturaJornal(Base):
|
||||
__tablename__ = 'assinaturas_jornais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
|
||||
quantidade = Column(Integer, nullable=False)
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_inicio = Column(Date, nullable=False)
|
||||
data_fim = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="assinaturas", foreign_keys=[militante_id])
|
||||
tipo_material = relationship("TipoMaterial", back_populates="assinaturas")
|
||||
17
models/entities/base.py
Normal file
17
models/entities/base.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Configurar caminho do banco de dados
|
||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||
db_dir.mkdir(parents=True, exist_ok=True)
|
||||
db_path = db_dir / 'database.db'
|
||||
|
||||
DATABASE_URL = f"sqlite:///{db_path}"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Base SQLAlchemy
|
||||
Base = declarative_base()
|
||||
@@ -1,24 +0,0 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class Celula(Base):
|
||||
__tablename__ = 'celulas'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(100), nullable=False)
|
||||
setor_id = Column(Integer, ForeignKey('setores.id', use_alter=True, name='fk_celula_setor'))
|
||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id', use_alter=True, name='fk_celula_cr'))
|
||||
secretario = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_secretario'))
|
||||
responsavel_financas = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_celula_responsavel_financas'))
|
||||
quadro_orientador = Column(String(255))
|
||||
|
||||
# Relacionamentos
|
||||
setor = relationship("Setor", back_populates="celulas")
|
||||
cr = relationship("ComiteRegional", back_populates="celulas")
|
||||
militantes = relationship("Militante", back_populates="celula", foreign_keys="[Militante.celula_id]")
|
||||
secretario_rel = relationship("Militante", foreign_keys=[secretario])
|
||||
responsavel_financas_rel = relationship("Militante", foreign_keys=[responsavel_financas])
|
||||
pagamentos = relationship("PagamentoCelula", back_populates="celula")
|
||||
usuarios = relationship("Usuario", back_populates="celula")
|
||||
15
models/entities/comprovante.py
Normal file
15
models/entities/comprovante.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class Comprovante(Base):
|
||||
__tablename__ = 'comprovantes'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'), nullable=False)
|
||||
tipo_comprovante = Column(String(50)) # Cota, Jornal, Assinatura, etc.
|
||||
data_comprovante = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="comprovantes")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="comprovante")
|
||||
17
models/entities/cota_mensal.py
Normal file
17
models/entities/cota_mensal.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class CotaMensal(Base):
|
||||
__tablename__ = 'cotas_mensais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
valor_antigo = Column(Numeric(10, 2), nullable=False)
|
||||
valor_novo = Column(Numeric(10, 2), nullable=False)
|
||||
data_alteracao = Column(Date, nullable=False)
|
||||
data_vencimento = Column(Date, nullable=False)
|
||||
pago = Column(Boolean, default=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="cotas_mensais")
|
||||
14
models/entities/email_militante.py
Normal file
14
models/entities/email_militante.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class EmailMilitante(Base):
|
||||
__tablename__ = 'emails_militantes'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
endereco_email = Column(String(100))
|
||||
|
||||
# Relacionamentos
|
||||
militante = relationship("Militante", back_populates="emails")
|
||||
19
models/entities/endereco.py
Normal file
19
models/entities/endereco.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class Endereco(Base):
|
||||
__tablename__ = 'enderecos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
estado = Column(String(2))
|
||||
cidade = Column(String(50))
|
||||
bairro = Column(String(50))
|
||||
rua = Column(String(100))
|
||||
numero = Column(String(10))
|
||||
complemento = Column(String(50))
|
||||
cep = Column(String(9))
|
||||
|
||||
# Relacionamentos
|
||||
militantes = relationship("Militante", back_populates="endereco")
|
||||
17
models/entities/material_vendido.py
Normal file
17
models/entities/material_vendido.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class MaterialVendido(Base):
|
||||
__tablename__ = 'materiais_vendidos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo_material_id = Column(Integer, ForeignKey('tipos_materiais.id'))
|
||||
descricao = Column(String(255), nullable=False)
|
||||
valor = Column(Numeric(10, 2), nullable=False)
|
||||
data_venda = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="materiais_vendidos")
|
||||
tipo_material = relationship("TipoMaterial", back_populates="materiais_vendidos")
|
||||
155
models/entities/militante.py
Normal file
155
models/entities/militante.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Date, Text, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
import secrets
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class EstadoMilitante(enum.Enum):
|
||||
ATIVO = 'ativo'
|
||||
DESLIGADO = 'desligado'
|
||||
SUSPENSO = 'suspenso'
|
||||
AFASTADO = 'afastado'
|
||||
|
||||
class Militante(Base):
|
||||
__tablename__ = 'militantes'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
nome = Column(String(100), nullable=False)
|
||||
cpf = Column(String(14), unique=True)
|
||||
# Novos campos básicos
|
||||
titulo_eleitoral = Column(String(20))
|
||||
data_nascimento = Column(Date)
|
||||
data_entrada_oci = Column(Date)
|
||||
data_efetivacao_oci = Column(Date)
|
||||
# Campos de contato
|
||||
telefone1 = Column(String(15))
|
||||
telefone2 = Column(String(15))
|
||||
# Relacionamento para múltiplos emails
|
||||
emails = relationship("EmailMilitante", back_populates="militante")
|
||||
# Endereço
|
||||
endereco_id = Column(Integer, ForeignKey('enderecos.id', use_alter=True, name='fk_militante_endereco'))
|
||||
endereco = relationship("Endereco", back_populates="militantes")
|
||||
# Redes sociais
|
||||
redes_sociais = relationship("RedeSocial", back_populates="militante")
|
||||
# Campos profissionais
|
||||
profissao = Column(String(100))
|
||||
regime_trabalho = Column(String(50)) # CLT, Estatutário, etc.
|
||||
empresa = Column(String(100))
|
||||
contratante = Column(String(100)) # Para terceirizados
|
||||
# Campos acadêmicos
|
||||
instituicao_ensino = Column(String(100))
|
||||
tipo_instituicao = Column(String(20)) # Federal, Estadual, etc.
|
||||
# Campos sindicais
|
||||
sindicato = Column(String(100))
|
||||
cargo_sindical = Column(String(50))
|
||||
dirigente_sindical = Column(Boolean)
|
||||
central_sindical = Column(String(100))
|
||||
# Responsável pelo cadastro
|
||||
registrado_por = Column(Integer, ForeignKey('militantes.id', use_alter=True, name='fk_militante_registrado_por'))
|
||||
# Campos existentes
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id', use_alter=True, name='fk_militante_celula'))
|
||||
responsabilidades = Column(Integer, default=0)
|
||||
otp_secret = Column(String(32))
|
||||
temp_token = Column(String(64))
|
||||
temp_token_expiry = Column(DateTime)
|
||||
# Novo campo para Quadro-Orientador
|
||||
quadro_orientador = Column(Boolean, default=False)
|
||||
# Campos para Aspirante
|
||||
aspirante = Column(Boolean, default=True) # Por padrão, todo novo militante é aspirante
|
||||
data_inicio_aspirante = Column(DateTime, default=datetime.utcnow)
|
||||
avaliacao_aspirante = Column(Text)
|
||||
data_avaliacao_aspirante = Column(DateTime)
|
||||
|
||||
# Campos para estado do militante
|
||||
estado = Column(Enum(EstadoMilitante), default=EstadoMilitante.ATIVO)
|
||||
data_desligamento = Column(DateTime)
|
||||
motivo_desligamento = Column(Text)
|
||||
|
||||
# Relacionamentos existentes
|
||||
cotas_mensais = relationship("CotaMensal", back_populates="militante")
|
||||
pagamentos = relationship("Pagamento", back_populates="militante")
|
||||
materiais_vendidos = relationship("MaterialVendido", back_populates="militante")
|
||||
vendas_jornais_avulsos = relationship("VendaJornalAvulso", back_populates="militante")
|
||||
vendas_jornais = relationship("VendaJornal", back_populates="militante", foreign_keys="[VendaJornal.militante_id]")
|
||||
assinaturas = relationship("AssinaturaJornal", back_populates="militante", foreign_keys="[AssinaturaJornal.militante_id]")
|
||||
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
|
||||
comprovantes = relationship("Comprovante", back_populates="militante")
|
||||
|
||||
# Constantes para responsabilidades
|
||||
SECRETARIO = 1
|
||||
TESOUREIRO = 2
|
||||
IMPRENSA = 4
|
||||
MNS = 8
|
||||
MPS = 16
|
||||
JUVENTUDE = 32
|
||||
QUADRO_ORIENTADOR = 64
|
||||
ASPIRANTE = 128
|
||||
RESPONSAVEL_FINANCAS = 256
|
||||
RESPONSAVEL_IMPRENSA = 512
|
||||
|
||||
@staticmethod
|
||||
def get_responsabilidades_list():
|
||||
return [
|
||||
(Militante.SECRETARIO, "Secretário"),
|
||||
(Militante.TESOUREIRO, "Tesoureiro"),
|
||||
(Militante.IMPRENSA, "Imprensa"),
|
||||
(Militante.MNS, "MNS"),
|
||||
(Militante.MPS, "MPS"),
|
||||
(Militante.JUVENTUDE, "Juventude"),
|
||||
(Militante.QUADRO_ORIENTADOR, "Quadro-Orientador"),
|
||||
(Militante.ASPIRANTE, "Aspirante"),
|
||||
(Militante.RESPONSAVEL_FINANCAS, "Responsável de Finanças"),
|
||||
(Militante.RESPONSAVEL_IMPRENSA, "Responsável de Imprensa")
|
||||
]
|
||||
|
||||
def set_responsabilidades(self, resp_list):
|
||||
"""
|
||||
Define as responsabilidades do militante
|
||||
resp_list: lista de inteiros representando as responsabilidades
|
||||
"""
|
||||
self.responsabilidades = sum(resp_list)
|
||||
|
||||
def get_responsabilidades(self):
|
||||
"""
|
||||
Retorna lista de responsabilidades ativas
|
||||
"""
|
||||
resp = []
|
||||
for valor, nome in self.get_responsabilidades_list():
|
||||
if self.responsabilidades & valor:
|
||||
resp.append(nome)
|
||||
return resp
|
||||
|
||||
def generate_temp_token(self):
|
||||
"""
|
||||
Gera um token temporário para acesso ao QR code
|
||||
"""
|
||||
self.temp_token = secrets.token_urlsafe(32)
|
||||
self.temp_token_expiry = datetime.now() + timedelta(hours=48)
|
||||
return self.temp_token
|
||||
|
||||
def generate_username(self):
|
||||
"""Gera um nome de usuário único baseado no primeiro nome e um código"""
|
||||
from sqlalchemy import func
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
# Pega o primeiro nome
|
||||
primeiro_nome = self.nome.split()[0].lower()
|
||||
|
||||
# Importação local para evitar dependência circular
|
||||
from models.entities.usuario import Usuario
|
||||
|
||||
# Conta quantos usuários já existem com esse prefixo
|
||||
count = db.query(func.count(Usuario.id)).filter(
|
||||
Usuario.username.like(f"{primeiro_nome}%")
|
||||
).scalar()
|
||||
|
||||
# Gera o código (número sequencial)
|
||||
codigo = str(count + 1).zfill(3)
|
||||
|
||||
return f"{primeiro_nome}{codigo}"
|
||||
finally:
|
||||
db.close()
|
||||
21
models/entities/pagamento.py
Normal file
21
models/entities/pagamento.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class Pagamento(Base):
|
||||
__tablename__ = 'pagamentos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo_pagamento = Column(String(50)) # Cota, Jornal, Assinatura, etc.
|
||||
mes_referencia = Column(Date)
|
||||
numero_jornal = Column(String(20))
|
||||
numero_inicial_assinatura = Column(String(20))
|
||||
numero_final_assinatura = Column(String(20))
|
||||
campanha_financeira = Column(String(50))
|
||||
valor = Column(Numeric(10, 2), nullable=False)
|
||||
data_pagamento = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="pagamentos")
|
||||
transacoes_pix = relationship("TransacaoPIX", back_populates="pagamento")
|
||||
15
models/entities/rede_social.py
Normal file
15
models/entities/rede_social.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class RedeSocial(Base):
|
||||
__tablename__ = 'redes_sociais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
|
||||
identificador = Column(String(100))
|
||||
|
||||
# Relacionamentos
|
||||
militante = relationship("Militante", back_populates="redes_sociais")
|
||||
13
models/entities/tipo_material.py
Normal file
13
models/entities/tipo_material.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class TipoMaterial(Base):
|
||||
__tablename__ = 'tipos_materiais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
descricao = Column(String(100), nullable=False)
|
||||
|
||||
materiais_vendidos = relationship("MaterialVendido", back_populates="tipo_material")
|
||||
assinaturas = relationship("AssinaturaJornal", back_populates="tipo_material")
|
||||
126
models/entities/usuario.py
Normal file
126
models/entities/usuario.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
from flask_login import UserMixin
|
||||
import pyotp
|
||||
|
||||
from models.entities.base import Base
|
||||
from models.entities.militante import Militante
|
||||
|
||||
class TipoUsuario(enum.Enum):
|
||||
ADMIN = "admin"
|
||||
CR_RESPONSAVEL = "cr_responsavel"
|
||||
SETOR_RESPONSAVEL = "setor_responsavel"
|
||||
USUARIO = "usuario"
|
||||
|
||||
class Usuario(Base, UserMixin):
|
||||
__tablename__ = 'usuarios'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(50), unique=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
email = Column(String(100), unique=True, nullable=False)
|
||||
nome = Column(String(100)) # Nome completo do usuário
|
||||
otp_secret = Column(String(32))
|
||||
role_id = Column(Integer, ForeignKey('roles.id'))
|
||||
setor_id = Column(Integer, ForeignKey('setores.id'))
|
||||
ativo = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
ultimo_login = Column(DateTime)
|
||||
ultimo_logout = Column(DateTime)
|
||||
motivo_logout = Column(String(100))
|
||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||
session_timeout = Column(Integer, default=30)
|
||||
tipo = Column(String(17), nullable=False)
|
||||
ultima_atividade = Column(DateTime, default=datetime.utcnow)
|
||||
# Relacionamento com militante
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
|
||||
# Relacionamentos
|
||||
roles = relationship("Role", secondary="user_roles", back_populates="users")
|
||||
setor = relationship('Setor', back_populates='usuarios')
|
||||
cr = relationship('ComiteRegional', back_populates='usuarios')
|
||||
celula = relationship('Celula', back_populates='usuarios')
|
||||
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
||||
|
||||
def __init__(self, username, email=None, is_admin=False, nome=None):
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.is_admin = is_admin
|
||||
self.nome = nome
|
||||
self.ativo = True
|
||||
self.session_timeout = 30
|
||||
self.tipo = "USUARIO"
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def update_last_activity(self):
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def is_session_expired(self):
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def check_session_timeout(self):
|
||||
"""Verifica se a sessão do usuário expirou"""
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def has_permission(self, permission_name):
|
||||
"""Verifica se o usuário tem uma permissão específica"""
|
||||
if self.is_admin: # Se for admin, tem todas as permissões
|
||||
return True
|
||||
|
||||
# Verifica se o usuário tem a permissão através de suas roles
|
||||
for role in self.roles:
|
||||
for permission in role.permissions:
|
||||
if permission.nome == permission_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_role(self, role_nivel):
|
||||
"""Verifica se o usuário tem um determinado nível de role"""
|
||||
for role in self.roles:
|
||||
if role.nivel == role_nivel:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_otp_uri(self):
|
||||
"""Gera a URI para autenticação em duas etapas"""
|
||||
if not self.otp_secret:
|
||||
self.otp_secret = pyotp.random_base32()
|
||||
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
|
||||
self.username,
|
||||
issuer_name="Sistema de Controles"
|
||||
)
|
||||
|
||||
def verify_otp(self, code):
|
||||
"""Verifica se um código OTP é válido"""
|
||||
if not self.otp_secret:
|
||||
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
||||
return False
|
||||
|
||||
totp = pyotp.totp.TOTP(self.otp_secret)
|
||||
is_valid = totp.verify(code)
|
||||
return is_valid
|
||||
|
||||
def logout(self):
|
||||
"""Registra o logout do usuário"""
|
||||
self.ultimo_logout = datetime.utcnow()
|
||||
self.motivo_logout = "Logout manual"
|
||||
self.ultima_atividade = None
|
||||
|
||||
def is_admin_user(self):
|
||||
"""Verifica se o usuário é admin"""
|
||||
return self.is_admin or any(role.nome == "admin" for role in self.roles)
|
||||
15
models/entities/venda_jornal.py
Normal file
15
models/entities/venda_jornal.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class VendaJornal(Base):
|
||||
__tablename__ = 'vendas_jornais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
quantidade = Column(Integer, nullable=False)
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_venda = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="vendas_jornais", foreign_keys=[militante_id])
|
||||
15
models/entities/venda_jornal_avulso.py
Normal file
15
models/entities/venda_jornal_avulso.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Numeric, Date
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class VendaJornalAvulso(Base):
|
||||
__tablename__ = 'vendas_jornais_avulsos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
quantidade = Column(Integer, nullable=False)
|
||||
valor_total = Column(Numeric(10, 2), nullable=False)
|
||||
data_venda = Column(Date, nullable=False)
|
||||
|
||||
militante = relationship("Militante", back_populates="vendas_jornais_avulsos")
|
||||
@@ -1,328 +0,0 @@
|
||||
from functions.database import get_db_session, Militante, EmailMilitante, Endereco
|
||||
from sqlalchemy.orm import joinedload
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from services.cache_service import cache_service, cached, CacheKeys, invalidate_cache_pattern
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MilitanteModel:
|
||||
"""Model para operações com militantes"""
|
||||
|
||||
@staticmethod
|
||||
@invalidate_cache_pattern("militantes:*")
|
||||
def criar_militante(data: Dict) -> Dict:
|
||||
"""Cria um novo militante"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Criar endereço se fornecido
|
||||
endereco_id = None
|
||||
if data.get('endereco'):
|
||||
endereco = Endereco(**data['endereco'])
|
||||
db.add(endereco)
|
||||
db.flush()
|
||||
endereco_id = endereco.id
|
||||
|
||||
# Criar militante
|
||||
militante = Militante(
|
||||
nome=data['nome'],
|
||||
cpf=data['cpf'],
|
||||
titulo_eleitoral=data.get('titulo_eleitoral'),
|
||||
data_nascimento=data.get('data_nascimento'),
|
||||
data_entrada_oci=data.get('data_entrada_oci'),
|
||||
data_efetivacao_oci=data.get('data_efetivacao_oci'),
|
||||
telefone1=data.get('telefone1'),
|
||||
telefone2=data.get('telefone2'),
|
||||
profissao=data.get('profissao'),
|
||||
regime_trabalho=data.get('regime_trabalho'),
|
||||
empresa=data.get('empresa'),
|
||||
contratante=data.get('contratante'),
|
||||
instituicao_ensino=data.get('instituicao_ensino'),
|
||||
tipo_instituicao=data.get('tipo_instituicao'),
|
||||
sindicato=data.get('sindicato'),
|
||||
cargo_sindical=data.get('cargo_sindical'),
|
||||
dirigente_sindical=data.get('dirigente_sindical', False),
|
||||
central_sindical=data.get('central_sindical'),
|
||||
endereco_id=endereco_id,
|
||||
celula_id=data.get('celula_id'),
|
||||
registrado_por=data.get('registrado_por')
|
||||
)
|
||||
|
||||
db.add(militante)
|
||||
db.flush()
|
||||
|
||||
# Criar email se fornecido
|
||||
if data.get('email'):
|
||||
email = EmailMilitante(
|
||||
militante_id=militante.id,
|
||||
endereco_email=data['email']
|
||||
)
|
||||
db.add(email)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Cache the new militante
|
||||
cache_key = CacheKeys.militante_detail(militante.id)
|
||||
militante_data = MilitanteModel.formatar_dados_militante(militante)
|
||||
cache_service.set(cache_key, militante_data, 1800) # 30 minutes
|
||||
|
||||
logger.info(f"Militante {militante.id} criado e cacheado")
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Militante criado com sucesso',
|
||||
'militante_id': militante.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao criar militante: {e}")
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao criar militante: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
@cached(expire=1800, key_prefix="militantes") # Cache for 30 minutes
|
||||
def listar_militantes() -> List[Militante]:
|
||||
"""Lista todos os militantes"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militantes = db.query(Militante).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.celula)
|
||||
).order_by(Militante.nome).all()
|
||||
|
||||
# Cache individual militantes
|
||||
for militante in militantes:
|
||||
cache_key = CacheKeys.militante_detail(militante.id)
|
||||
militante_data = MilitanteModel.formatar_dados_militante(militante)
|
||||
cache_service.set(cache_key, militante_data, 1800)
|
||||
|
||||
logger.debug(f"Listados {len(militantes)} militantes e cacheados")
|
||||
return militantes
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao listar militantes: {e}")
|
||||
return []
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_por_id(militante_id: int) -> Optional[Militante]:
|
||||
"""Busca um militante por ID"""
|
||||
# Try cache first
|
||||
cache_key = CacheKeys.militante_detail(militante_id)
|
||||
cached_militante = cache_service.get(cache_key)
|
||||
|
||||
if cached_militante:
|
||||
logger.debug(f"Cache hit para militante {militante_id}")
|
||||
# Convert cached data back to Militante object if needed
|
||||
return cached_militante
|
||||
|
||||
# Cache miss, get from database
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).options(
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco)
|
||||
).get(militante_id)
|
||||
|
||||
if militante:
|
||||
# Cache the militante
|
||||
militante_data = MilitanteModel.formatar_dados_militante(militante)
|
||||
cache_service.set(cache_key, militante_data, 1800)
|
||||
logger.debug(f"Cache miss para militante {militante_id}, cacheado")
|
||||
|
||||
return militante
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao buscar militante {militante_id}: {e}")
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
@invalidate_cache_pattern("militantes:*")
|
||||
def atualizar_militante(militante_id: int, data: Dict) -> Dict:
|
||||
"""Atualiza um militante existente"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).get(militante_id)
|
||||
|
||||
if not militante:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}
|
||||
|
||||
# Atualizar dados básicos
|
||||
militante.nome = data.get('nome', militante.nome)
|
||||
militante.cpf = data.get('cpf', militante.cpf)
|
||||
militante.titulo_eleitoral = data.get('titulo_eleitoral', militante.titulo_eleitoral)
|
||||
militante.telefone1 = data.get('telefone1', militante.telefone1)
|
||||
militante.telefone2 = data.get('telefone2', militante.telefone2)
|
||||
militante.profissao = data.get('profissao', militante.profissao)
|
||||
militante.regime_trabalho = data.get('regime_trabalho', militante.regime_trabalho)
|
||||
militante.empresa = data.get('empresa', militante.empresa)
|
||||
militante.contratante = data.get('contratante', militante.contratante)
|
||||
militante.instituicao_ensino = data.get('instituicao_ensino', militante.instituicao_ensino)
|
||||
militante.tipo_instituicao = data.get('tipo_instituicao', militante.tipo_instituicao)
|
||||
militante.sindicato = data.get('sindicato', militante.sindicato)
|
||||
militante.cargo_sindical = data.get('cargo_sindical', militante.cargo_sindical)
|
||||
militante.dirigente_sindical = data.get('dirigente_sindical', militante.dirigente_sindical)
|
||||
militante.central_sindical = data.get('central_sindical', militante.central_sindical)
|
||||
|
||||
# Atualizar datas
|
||||
if data.get('data_nascimento'):
|
||||
militante.data_nascimento = data['data_nascimento']
|
||||
if data.get('data_entrada_oci'):
|
||||
militante.data_entrada_oci = data['data_entrada_oci']
|
||||
if data.get('data_efetivacao_oci'):
|
||||
militante.data_efetivacao_oci = data['data_efetivacao_oci']
|
||||
|
||||
# Atualizar endereço
|
||||
if data.get('endereco') and militante.endereco:
|
||||
endereco = militante.endereco
|
||||
endereco.cep = data['endereco'].get('cep', endereco.cep)
|
||||
endereco.estado = data['endereco'].get('estado', endereco.estado)
|
||||
endereco.cidade = data['endereco'].get('cidade', endereco.cidade)
|
||||
endereco.bairro = data['endereco'].get('bairro', endereco.bairro)
|
||||
endereco.rua = data['endereco'].get('rua', endereco.rua)
|
||||
endereco.numero = data['endereco'].get('numero', endereco.numero)
|
||||
endereco.complemento = data['endereco'].get('complemento', endereco.complemento)
|
||||
|
||||
# Atualizar email
|
||||
if data.get('email') and militante.emails:
|
||||
militante.emails[0].endereco_email = data['email']
|
||||
|
||||
db.commit()
|
||||
|
||||
# Update cache
|
||||
cache_key = CacheKeys.militante_detail(militante_id)
|
||||
militante_data = MilitanteModel.formatar_dados_militante(militante)
|
||||
cache_service.set(cache_key, militante_data, 1800)
|
||||
|
||||
logger.info(f"Militante {militante_id} atualizado e cache atualizado")
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Militante atualizado com sucesso'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao atualizar militante {militante_id}: {e}")
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao atualizar militante: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
@invalidate_cache_pattern("militantes:*")
|
||||
def excluir_militante(militante_id: int) -> Dict:
|
||||
"""Exclui um militante"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).get(militante_id)
|
||||
if not militante:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Militante não encontrado'
|
||||
}
|
||||
|
||||
db.delete(militante)
|
||||
db.commit()
|
||||
|
||||
# Remove from cache
|
||||
cache_key = CacheKeys.militante_detail(militante_id)
|
||||
cache_service.delete(cache_key)
|
||||
|
||||
logger.info(f"Militante {militante_id} excluído e removido do cache")
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Militante excluído com sucesso'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Erro ao excluir militante {militante_id}: {e}")
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao excluir militante: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
@cached(expire=1800, key_prefix="militantes")
|
||||
def buscar_por_cpf(cpf: str) -> Optional[Militante]:
|
||||
"""Busca um militante por CPF"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
militante = db.query(Militante).filter_by(cpf=cpf).first()
|
||||
if militante:
|
||||
# Cache the militante
|
||||
cache_key = CacheKeys.militante_detail(militante.id)
|
||||
militante_data = MilitanteModel.formatar_dados_militante(militante)
|
||||
cache_service.set(cache_key, militante_data, 1800)
|
||||
return militante
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao buscar militante por CPF {cpf}: {e}")
|
||||
return None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def formatar_dados_militante(militante: Militante) -> Dict:
|
||||
"""Formata os dados de um militante para retorno JSON"""
|
||||
def formatar_data_segura(data):
|
||||
try:
|
||||
if not data:
|
||||
return None
|
||||
return data.strftime('%Y-%m-%d')
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao formatar data: {str(e)}, valor: {data}")
|
||||
return None
|
||||
|
||||
return {
|
||||
'id': militante.id,
|
||||
'nome': militante.nome,
|
||||
'cpf': militante.cpf,
|
||||
'titulo_eleitoral': militante.titulo_eleitoral,
|
||||
'data_nascimento': formatar_data_segura(militante.data_nascimento),
|
||||
'data_entrada_oci': formatar_data_segura(militante.data_entrada_oci),
|
||||
'data_efetivacao_oci': formatar_data_segura(militante.data_efetivacao_oci),
|
||||
'telefone1': militante.telefone1,
|
||||
'telefone2': militante.telefone2,
|
||||
'profissao': militante.profissao,
|
||||
'regime_trabalho': militante.regime_trabalho,
|
||||
'empresa': militante.empresa,
|
||||
'contratante': militante.contratante,
|
||||
'instituicao_ensino': militante.instituicao_ensino,
|
||||
'tipo_instituicao': militante.tipo_instituicao,
|
||||
'sindicato': militante.sindicato,
|
||||
'cargo_sindical': militante.cargo_sindical,
|
||||
'dirigente_sindical': militante.dirigente_sindical,
|
||||
'central_sindical': militante.central_sindical,
|
||||
'responsabilidades': militante.responsabilidades,
|
||||
'estado': militante.estado.value if militante.estado else None,
|
||||
'celula_id': militante.celula_id,
|
||||
'email': militante.emails[0].endereco_email if militante.emails else None,
|
||||
'endereco': {
|
||||
'cep': militante.endereco.cep if militante.endereco else None,
|
||||
'estado': militante.endereco.estado if militante.endereco else None,
|
||||
'cidade': militante.endereco.cidade if militante.endereco else None,
|
||||
'bairro': militante.endereco.bairro if militante.endereco else None,
|
||||
'rua': militante.endereco.rua if militante.endereco else None,
|
||||
'numero': militante.endereco.numero if militante.endereco else None,
|
||||
'complemento': militante.endereco.complemento if militante.endereco else None
|
||||
} if militante.endereco else None
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
from functions.database import get_db_session, Pagamento, Militante, TipoPagamento
|
||||
from sqlalchemy.orm import joinedload
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
class PagamentoModel:
|
||||
"""Model para operações com pagamentos"""
|
||||
|
||||
@staticmethod
|
||||
def criar_pagamento(data: Dict) -> Dict:
|
||||
"""Cria um novo pagamento"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = Pagamento(
|
||||
militante_id=data['militante_id'],
|
||||
tipo_pagamento_id=data.get('tipo_pagamento_id'),
|
||||
valor=data['valor'],
|
||||
data_pagamento=data['data_pagamento']
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Pagamento criado com sucesso',
|
||||
'pagamento_id': pagamento.id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao criar pagamento: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def listar_pagamentos() -> List[Pagamento]:
|
||||
"""Lista todos os pagamentos"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_por_id(pagamento_id: int) -> Optional[Pagamento]:
|
||||
"""Busca um pagamento por ID"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).get(pagamento_id)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def atualizar_pagamento(pagamento_id: int, data: Dict) -> Dict:
|
||||
"""Atualiza um pagamento existente"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = db.query(Pagamento).get(pagamento_id)
|
||||
|
||||
if not pagamento:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Pagamento não encontrado'
|
||||
}
|
||||
|
||||
pagamento.militante_id = data.get('militante_id', pagamento.militante_id)
|
||||
pagamento.tipo_pagamento_id = data.get('tipo_pagamento_id', pagamento.tipo_pagamento_id)
|
||||
pagamento.valor = data.get('valor', pagamento.valor)
|
||||
pagamento.data_pagamento = data.get('data_pagamento', pagamento.data_pagamento)
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Pagamento atualizado com sucesso'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao atualizar pagamento: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def excluir_pagamento(pagamento_id: int) -> Dict:
|
||||
"""Exclui um pagamento"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
pagamento = db.query(Pagamento).get(pagamento_id)
|
||||
if not pagamento:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Pagamento não encontrado'
|
||||
}
|
||||
|
||||
db.delete(pagamento)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Pagamento excluído com sucesso'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao excluir pagamento: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def listar_por_celula(celula_id: int) -> List[Pagamento]:
|
||||
"""Lista pagamentos de uma célula específica"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).filter_by(celula_id=celula_id).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def listar_por_setor(setor_id: int) -> List[Pagamento]:
|
||||
"""Lista pagamentos de um setor específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).join(Usuario).filter(Usuario.setor_id == setor_id).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def listar_por_cr(cr_id: int) -> List[Pagamento]:
|
||||
"""Lista pagamentos de um CR específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).join(Usuario).filter(Usuario.cr_id == cr_id).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def listar_por_militante(militante_id: int) -> List[Pagamento]:
|
||||
"""Lista pagamentos de um militante específico"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).filter_by(militante_id=militante_id).order_by(Pagamento.data_pagamento.desc()).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_tipos_pagamento() -> List[TipoPagamento]:
|
||||
"""Obtém todos os tipos de pagamento"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(TipoPagamento).order_by(TipoPagamento.descricao).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_militantes() -> List[Militante]:
|
||||
"""Obtém todos os militantes"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Militante).order_by(Militante.nome).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def formatar_dados_pagamento(pagamento: Pagamento) -> Dict:
|
||||
"""Formata os dados de um pagamento para retorno JSON"""
|
||||
return {
|
||||
'id': pagamento.id,
|
||||
'militante_id': pagamento.militante_id,
|
||||
'tipo_pagamento_id': pagamento.tipo_pagamento_id,
|
||||
'valor': float(pagamento.valor) if pagamento.valor else 0.0,
|
||||
'data_pagamento': pagamento.data_pagamento.strftime('%Y-%m-%d') if pagamento.data_pagamento else None,
|
||||
'militante_nome': pagamento.militante.nome if pagamento.militante else None,
|
||||
'tipo_pagamento_nome': pagamento.tipo_pagamento.descricao if pagamento.tipo_pagamento else None
|
||||
}
|
||||
@@ -3,17 +3,17 @@ Flask-SQLAlchemy==3.1.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-WTF==1.2.1
|
||||
Flask-Mail==0.9.1
|
||||
SQLAlchemy>=2.0.36
|
||||
SQLAlchemy==2.0.27
|
||||
Werkzeug==3.0.1
|
||||
python-dotenv==1.0.1
|
||||
pyotp==2.9.0
|
||||
qrcode==7.4.2
|
||||
Pillow>=10.4.0
|
||||
email-validator==2.3.0
|
||||
Pillow==9.5.0
|
||||
email-validator==2.1.0.post1
|
||||
cryptography==42.0.2
|
||||
bcrypt==4.1.2
|
||||
Bootstrap-Flask==2.3.3
|
||||
flask-bootstrap5==0.1.dev1
|
||||
PyJWT==2.8.0
|
||||
gunicorn==21.2.0
|
||||
Faker==19.13.0
|
||||
redis==5.0.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask import Blueprint, render_template, flash, redirect, url_for, request, jsonify
|
||||
from functions.database import Usuario, get_db_session
|
||||
from functions.decorators import require_login
|
||||
from functions.database import Usuario, get_db_connection
|
||||
from functions.decorators import require_permission, require_role, require_minimum_role
|
||||
from flask_login import login_required, current_user
|
||||
from sqlalchemy.orm import joinedload
|
||||
import pyotp
|
||||
@@ -9,7 +9,10 @@ import secrets
|
||||
from functools import wraps
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import logging
|
||||
<<<<<<< HEAD
|
||||
from datetime import datetime
|
||||
=======
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,7 +23,7 @@ def admin_required(f):
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not current_user.is_admin:
|
||||
flash('Acesso não autorizado.', 'danger')
|
||||
return redirect(url_for('home.index'))
|
||||
return redirect(url_for('main.index'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
@@ -29,7 +32,7 @@ def admin_required(f):
|
||||
@admin_required
|
||||
def dashboard():
|
||||
"""Dashboard principal da área administrativa com lista de usuários"""
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
now = datetime.now()
|
||||
|
||||
@@ -49,8 +52,12 @@ def dashboard():
|
||||
total_users=total_users,
|
||||
active_users=active_users,
|
||||
inactive_users=inactive_users,
|
||||
<<<<<<< HEAD
|
||||
users=users,
|
||||
now=now
|
||||
=======
|
||||
users=users
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
)
|
||||
except SQLAlchemyError as e:
|
||||
logger.error(f"Erro ao buscar dados do dashboard: {str(e)}")
|
||||
@@ -65,10 +72,10 @@ def dashboard():
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/reset-otp', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@require_role('ADMIN')
|
||||
def reset_user_otp(user_id):
|
||||
"""Reseta o OTP de um usuário"""
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
@@ -86,10 +93,10 @@ def reset_user_otp(user_id):
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/reset-password', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@require_role('ADMIN')
|
||||
def reset_user_password(user_id):
|
||||
"""Reseta a senha de um usuário"""
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
@@ -108,10 +115,10 @@ def reset_user_password(user_id):
|
||||
|
||||
@admin_bp.route('/users/<int:user_id>/toggle-status', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
@require_role('ADMIN')
|
||||
def toggle_user_status(user_id):
|
||||
"""Ativa/desativa um usuário"""
|
||||
db = get_db_session()
|
||||
db = get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
|
||||
61
routes/auth.py
Normal file
61
routes/auth.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from controllers.auth_controller import AuthController
|
||||
from services.database_service import DatabaseService
|
||||
from models.entities.usuario import Usuario
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
"""Rota de login"""
|
||||
if request.method == "POST":
|
||||
# Processar o login através do controlador
|
||||
if AuthController.login():
|
||||
# Redirecionar para home em caso de sucesso
|
||||
return redirect(url_for("home"))
|
||||
|
||||
# GET ou falha no login, renderizar template
|
||||
return render_template("login.html")
|
||||
|
||||
@auth_bp.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
"""Rota de logout"""
|
||||
AuthController.logout()
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
@auth_bp.route("/alterar_senha", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def alterar_senha():
|
||||
"""Rota para alterar a senha do usuário"""
|
||||
if request.method == "POST":
|
||||
senha_atual = request.form.get("senha_atual")
|
||||
nova_senha = request.form.get("nova_senha")
|
||||
confirmar_senha = request.form.get("confirmar_senha")
|
||||
|
||||
if AuthController.alterar_senha(current_user.id, senha_atual, nova_senha, confirmar_senha):
|
||||
return redirect(url_for("home"))
|
||||
|
||||
return render_template("alterar_senha.html")
|
||||
|
||||
@auth_bp.route("/qr/<token>")
|
||||
def get_qr_code(token):
|
||||
"""Rota para exibir QR code para configuração 2FA"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
user = db.query(Usuario).filter_by(username='admin').first()
|
||||
if not user:
|
||||
flash('Usuário não encontrado', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
qr_uri = user.get_otp_uri()
|
||||
return render_template('mostrar_qr_code.html', qr_uri=qr_uri)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@auth_bp.route('/check_session')
|
||||
def check_session():
|
||||
"""Rota para verificar status da sessão via AJAX"""
|
||||
return jsonify(AuthController.check_session())
|
||||
123
routes/cota.py
Normal file
123
routes/cota.py
Normal file
@@ -0,0 +1,123 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
|
||||
from flask_login import login_required
|
||||
|
||||
from models.entities.cota_mensal import CotaMensal
|
||||
from services.cota_service import CotaService
|
||||
from services.militante_service import MilitanteService
|
||||
from functions.decorators import require_permission
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
|
||||
cota_bp = Blueprint('cota', __name__, url_prefix='/cotas')
|
||||
|
||||
@cota_bp.route("/")
|
||||
@login_required
|
||||
@require_permission('gerenciar_cotas')
|
||||
def listar():
|
||||
"""Lista todas as cotas mensais"""
|
||||
cotas = CotaService.listar_cotas()
|
||||
# Calcular status de cada cota
|
||||
for cota in cotas:
|
||||
if cota.pago:
|
||||
cota.status = "paga"
|
||||
elif cota.data_vencimento < datetime.now().date():
|
||||
cota.status = "atrasada"
|
||||
else:
|
||||
cota.status = "pendente"
|
||||
|
||||
militantes = MilitanteService.listar_militantes()
|
||||
return render_template("listar_cotas.html", cotas=cotas, militantes=militantes)
|
||||
|
||||
@cota_bp.route("/novo", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_cotas')
|
||||
def nova():
|
||||
"""Cria uma nova cota"""
|
||||
if request.method == "POST":
|
||||
militante_id = request.form.get("militante_id")
|
||||
valor_antigo = float(request.form.get("valor_antigo"))
|
||||
valor_novo = float(request.form.get("valor_novo"))
|
||||
data_alteracao = converter_data(request.form.get("data_alteracao"))
|
||||
data_vencimento = converter_data(request.form.get("data_vencimento"))
|
||||
|
||||
# Validar datas
|
||||
if not validar_data(data_alteracao) or not validar_data(data_vencimento):
|
||||
flash('Datas inválidas', 'danger')
|
||||
return redirect(url_for('cota.nova'))
|
||||
|
||||
result = CotaService.criar_cota(militante_id, valor_antigo, valor_novo,
|
||||
data_alteracao, data_vencimento)
|
||||
|
||||
if result:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Cota cadastrada com sucesso!'
|
||||
})
|
||||
|
||||
flash('Cota cadastrada com sucesso!', 'success')
|
||||
return redirect(url_for('cota.listar'))
|
||||
else:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Erro ao cadastrar cota. Verifique os dados e tente novamente.'
|
||||
}), 400
|
||||
|
||||
flash('Erro ao cadastrar cota', 'danger')
|
||||
return redirect(url_for('cota.nova'))
|
||||
|
||||
# GET
|
||||
militantes = MilitanteService.listar_militantes()
|
||||
return render_template("nova_cota.html", militantes=militantes)
|
||||
|
||||
@cota_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@require_permission('gerenciar_cotas')
|
||||
def editar(id):
|
||||
"""Edita uma cota existente"""
|
||||
cota = CotaService.buscar_cota(id)
|
||||
if not cota:
|
||||
flash('Cota não encontrada', 'danger')
|
||||
return redirect(url_for('cota.listar'))
|
||||
|
||||
if request.method == 'POST':
|
||||
militante_id = int(request.form['militante_id'])
|
||||
valor_antigo = float(request.form['valor_antigo'])
|
||||
valor_novo = float(request.form['valor_novo'])
|
||||
data_alteracao = converter_data(request.form['data_alteracao'])
|
||||
data_vencimento = converter_data(request.form['data_vencimento'])
|
||||
pago = request.form.get('pago', '').lower() == 'true'
|
||||
|
||||
if CotaService.atualizar_cota(id, militante_id, valor_antigo, valor_novo,
|
||||
data_alteracao, data_vencimento, pago):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Cota atualizada com sucesso!'
|
||||
})
|
||||
|
||||
flash('Cota atualizada com sucesso!', 'success')
|
||||
return redirect(url_for('cota.listar'))
|
||||
else:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Erro ao atualizar cota'
|
||||
}), 400
|
||||
|
||||
flash('Erro ao atualizar cota', 'danger')
|
||||
return redirect(url_for('cota.editar', id=id))
|
||||
|
||||
return render_template('editar_cota.html', cota=cota)
|
||||
|
||||
@cota_bp.route('/excluir/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@require_permission('gerenciar_cotas')
|
||||
def excluir(id):
|
||||
"""Exclui uma cota existente"""
|
||||
if CotaService.excluir_cota(id):
|
||||
flash('Cota excluída com sucesso!', 'success')
|
||||
else:
|
||||
flash('Erro ao excluir cota', 'danger')
|
||||
|
||||
return redirect(url_for('cota.listar'))
|
||||
41
routes/main.py
Normal file
41
routes/main.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from functions.decorators import require_login
|
||||
from controllers.home_controller import HomeController
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@main_bp.route("/")
|
||||
@require_login
|
||||
def index():
|
||||
"""Rota principal - redireciona para home se autenticado"""
|
||||
return redirect(url_for('main.home'))
|
||||
|
||||
@main_bp.route("/home")
|
||||
@require_login
|
||||
def home():
|
||||
"""Página inicial do sistema com dashboard"""
|
||||
dashboard_data = HomeController.dashboard()
|
||||
return render_template('home.html',
|
||||
nome_usuario=dashboard_data['nome_usuario'],
|
||||
data_atual=dashboard_data['data_atual'],
|
||||
total_militantes=dashboard_data['total_militantes'],
|
||||
total_cotas=dashboard_data['total_cotas'],
|
||||
total_materiais=dashboard_data['total_materiais'],
|
||||
total_assinaturas=dashboard_data['total_assinaturas'],
|
||||
ultimos_militantes=dashboard_data['ultimos_militantes'],
|
||||
ultimos_pagamentos=dashboard_data['ultimos_pagamentos'],
|
||||
tipos_pagamento=dashboard_data['tipos_pagamento'],
|
||||
Militante=None) # Militante class for constants
|
||||
|
||||
@main_bp.route("/api/setores/<int:cr_id>")
|
||||
@require_login
|
||||
def get_setores(cr_id):
|
||||
"""API para listar setores por comitê regional"""
|
||||
from services.setor_service import SetorService
|
||||
|
||||
setores = SetorService.listar_setores_por_cr(cr_id)
|
||||
return jsonify({
|
||||
'setores': [{'id': s.id, 'nome': s.nome} for s in setores]
|
||||
})
|
||||
50
routes/militante.py
Normal file
50
routes/militante.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
|
||||
from flask_login import login_required
|
||||
|
||||
from controllers.militante_controller import MilitanteController
|
||||
from services.celula_service import CelulaService
|
||||
from functions.decorators import require_permission, require_role
|
||||
|
||||
militante_bp = Blueprint('militante', __name__, url_prefix='/militantes')
|
||||
|
||||
@militante_bp.route("/")
|
||||
@login_required
|
||||
@require_permission('gerenciar_militantes')
|
||||
def listar():
|
||||
"""Lista todos os militantes"""
|
||||
militantes = MilitanteController.listar_militantes()
|
||||
celulas = CelulaService.listar_celulas()
|
||||
return render_template('listar_militantes.html',
|
||||
militantes=militantes,
|
||||
celulas=celulas,
|
||||
Militante=None) # Militante class for constants
|
||||
|
||||
@militante_bp.route("/criar", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_militantes')
|
||||
def criar():
|
||||
"""Cria um novo militante"""
|
||||
return MilitanteController.criar_militante(request.form)
|
||||
|
||||
@militante_bp.route("/editar/<int:militante_id>", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_militantes')
|
||||
def editar(militante_id):
|
||||
"""Edita um militante existente"""
|
||||
return MilitanteController.atualizar_militante(militante_id, request.form)
|
||||
|
||||
@militante_bp.route("/excluir/<int:militante_id>", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_militantes')
|
||||
def excluir(militante_id):
|
||||
"""Exclui um militante"""
|
||||
if MilitanteController.excluir_militante(militante_id):
|
||||
return redirect(url_for('militante.listar'))
|
||||
return redirect(url_for('militante.listar'))
|
||||
|
||||
@militante_bp.route("/dados/<int:militante_id>")
|
||||
@login_required
|
||||
@require_permission('gerenciar_militantes')
|
||||
def dados(militante_id):
|
||||
"""Busca os dados de um militante específico"""
|
||||
return MilitanteController.buscar_dados_militante(militante_id)
|
||||
116
routes/pagamento.py
Normal file
116
routes/pagamento.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
|
||||
from flask_login import login_required
|
||||
|
||||
from services.pagamento_service import PagamentoService
|
||||
from services.militante_service import MilitanteService
|
||||
from services.tipo_pagamento_service import TipoPagamentoService
|
||||
from functions.decorators import require_permission
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
|
||||
pagamento_bp = Blueprint('pagamento', __name__, url_prefix='/pagamentos')
|
||||
|
||||
@pagamento_bp.route("/")
|
||||
@login_required
|
||||
@require_permission('gerenciar_pagamentos')
|
||||
def listar():
|
||||
"""Lista todos os pagamentos"""
|
||||
pagamentos = PagamentoService.listar_pagamentos()
|
||||
militantes = MilitanteService.listar_militantes()
|
||||
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
|
||||
|
||||
return render_template("listar_pagamentos.html",
|
||||
pagamentos=pagamentos,
|
||||
militantes=militantes,
|
||||
tipos_pagamento=tipos_pagamento)
|
||||
|
||||
@pagamento_bp.route("/novo", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_pagamentos')
|
||||
def novo():
|
||||
"""Cria um novo pagamento"""
|
||||
if request.method == "POST":
|
||||
militante_id = request.form.get("militante_id")
|
||||
tipo_pagamento_id = request.form.get("tipo_pagamento_id")
|
||||
valor = float(request.form.get("valor"))
|
||||
data_pagamento = converter_data(request.form.get("data_pagamento"))
|
||||
|
||||
if not validar_data(data_pagamento):
|
||||
flash('Data de pagamento inválida ou futura', 'danger')
|
||||
return redirect(url_for('pagamento.novo'))
|
||||
|
||||
if PagamentoService.criar_pagamento(militante_id, tipo_pagamento_id, valor, data_pagamento):
|
||||
flash('Pagamento cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
else:
|
||||
flash('Erro ao cadastrar pagamento', 'danger')
|
||||
return redirect(url_for('pagamento.novo'))
|
||||
|
||||
# GET
|
||||
militantes = MilitanteService.listar_militantes()
|
||||
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
|
||||
return render_template("novo_pagamento.html",
|
||||
militantes=militantes,
|
||||
tipos_pagamento=tipos_pagamento)
|
||||
|
||||
@pagamento_bp.route("/adicionar", methods=["POST"])
|
||||
@login_required
|
||||
@require_permission('gerenciar_pagamentos')
|
||||
def adicionar():
|
||||
"""Adiciona um novo pagamento (via AJAX)"""
|
||||
militante_id = request.form.get("militante_id")
|
||||
tipo_pagamento = request.form.get("tipo_pagamento")
|
||||
valor = float(request.form.get("valor"))
|
||||
data_pagamento = converter_data(request.form.get("data_pagamento"))
|
||||
|
||||
if PagamentoService.criar_pagamento_simples(militante_id, tipo_pagamento, valor, data_pagamento):
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': 'Pagamento adicionado com sucesso!'
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'Erro ao adicionar pagamento'
|
||||
}), 400
|
||||
|
||||
@pagamento_bp.route('/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@require_permission('gerenciar_pagamentos')
|
||||
def editar(id):
|
||||
"""Edita um pagamento existente"""
|
||||
pagamento = PagamentoService.buscar_pagamento(id)
|
||||
if not pagamento:
|
||||
flash('Pagamento não encontrado', 'danger')
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
|
||||
if request.method == 'POST':
|
||||
militante_id = int(request.form['militante_id'])
|
||||
tipo_pagamento_id = int(request.form['tipo_pagamento_id'])
|
||||
valor = float(request.form['valor'])
|
||||
data_pagamento = converter_data(request.form['data_pagamento'])
|
||||
|
||||
if PagamentoService.atualizar_pagamento(id, militante_id, tipo_pagamento_id, valor, data_pagamento):
|
||||
flash('Pagamento atualizado com sucesso!', 'success')
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
else:
|
||||
flash('Erro ao atualizar pagamento', 'danger')
|
||||
return redirect(url_for('pagamento.editar', id=id))
|
||||
|
||||
militantes = MilitanteService.listar_militantes()
|
||||
tipos_pagamento = TipoPagamentoService.listar_tipos_pagamento()
|
||||
return render_template('editar_pagamento.html',
|
||||
pagamento=pagamento,
|
||||
militantes=militantes,
|
||||
tipos_pagamento=tipos_pagamento)
|
||||
|
||||
@pagamento_bp.route('/excluir/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@require_permission('gerenciar_pagamentos')
|
||||
def excluir(id):
|
||||
"""Exclui um pagamento existente"""
|
||||
if PagamentoService.excluir_pagamento(id):
|
||||
flash('Pagamento excluído com sucesso!', 'success')
|
||||
else:
|
||||
flash('Erro ao excluir pagamento', 'danger')
|
||||
|
||||
return redirect(url_for('pagamento.listar'))
|
||||
149
routes/relatorio.py
Normal file
149
routes/relatorio.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, flash
|
||||
from flask_login import login_required
|
||||
from datetime import date
|
||||
|
||||
from services.relatorio_service import RelatorioService
|
||||
from services.setor_service import SetorService
|
||||
from services.comite_service import ComiteService
|
||||
from functions.decorators import require_permission
|
||||
from utils.date_utils import validar_data, converter_data
|
||||
|
||||
relatorio_bp = Blueprint('relatorio', __name__, url_prefix='/relatorios')
|
||||
|
||||
# Rotas para relatórios de cotas
|
||||
@relatorio_bp.route("/cotas")
|
||||
@login_required
|
||||
@require_permission('visualizar_relatorios')
|
||||
def listar_cotas():
|
||||
"""Lista todos os relatórios de cotas"""
|
||||
relatorios = RelatorioService.listar_relatorios_cotas()
|
||||
return render_template("listar_relatorios_cotas.html", relatorios=relatorios)
|
||||
|
||||
@relatorio_bp.route("/cotas/novo", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@require_permission('gerar_relatorios')
|
||||
def novo_relatorio_cotas():
|
||||
"""Cria um novo relatório de cotas"""
|
||||
if request.method == "POST":
|
||||
setor_id = request.form.get("setor_id")
|
||||
comite_id = request.form.get("comite_id")
|
||||
total_cotas = float(request.form.get("total_cotas"))
|
||||
data_relatorio = request.form.get("data_relatorio")
|
||||
|
||||
# Validar data
|
||||
if not validar_data(data_relatorio):
|
||||
flash('Data do relatório inválida', 'danger')
|
||||
return render_template("novo_relatorio_cotas.html")
|
||||
|
||||
# Converter data
|
||||
data_relatorio = converter_data(data_relatorio)
|
||||
|
||||
# Validar data futura
|
||||
if data_relatorio > date.today():
|
||||
flash('A data do relatório não pode ser futura', 'danger')
|
||||
return render_template("novo_relatorio_cotas.html")
|
||||
|
||||
if RelatorioService.criar_relatorio_cotas(setor_id, comite_id, total_cotas, data_relatorio):
|
||||
flash('Relatório de cotas cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('relatorio.listar_cotas'))
|
||||
else:
|
||||
flash('Erro ao cadastrar relatório de cotas', 'danger')
|
||||
return render_template("novo_relatorio_cotas.html")
|
||||
|
||||
# GET
|
||||
setores = SetorService.listar_setores()
|
||||
comites = ComiteService.listar_comites()
|
||||
return render_template("novo_relatorio_cotas.html",
|
||||
setores=setores,
|
||||
comites=comites,
|
||||
hoje=date.today().strftime('%Y-%m-%d'))
|
||||
|
||||
@relatorio_bp.route('/cotas/editar/<int:id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@require_permission('gerar_relatorios')
|
||||
def editar_relatorio_cotas(id):
|
||||
"""Edita um relatório de cotas existente"""
|
||||
relatorio = RelatorioService.buscar_relatorio_cotas(id)
|
||||
if not relatorio:
|
||||
flash('Relatório não encontrado', 'danger')
|
||||
return redirect(url_for('relatorio.listar_cotas'))
|
||||
|
||||
if request.method == 'POST':
|
||||
setor_id = int(request.form['setor_id']) if request.form['setor_id'] else None
|
||||
comite_id = int(request.form['comite_id']) if request.form['comite_id'] else None
|
||||
total_cotas = float(request.form['total_cotas'])
|
||||
data_relatorio = converter_data(request.form['data_relatorio'])
|
||||
|
||||
if RelatorioService.atualizar_relatorio_cotas(id, setor_id, comite_id, total_cotas, data_relatorio):
|
||||
flash('Relatório atualizado com sucesso!', 'success')
|
||||
return redirect(url_for('relatorio.listar_cotas'))
|
||||
else:
|
||||
flash('Erro ao atualizar relatório', 'danger')
|
||||
return redirect(url_for('relatorio.editar_relatorio_cotas', id=id))
|
||||
|
||||
setores = SetorService.listar_setores()
|
||||
comites = ComiteService.listar_comites()
|
||||
return render_template('editar_relatorio_cotas.html',
|
||||
relatorio=relatorio,
|
||||
setores=setores,
|
||||
comites=comites)
|
||||
|
||||
@relatorio_bp.route('/cotas/excluir/<int:id>', methods=['POST'])
|
||||
@login_required
|
||||
@require_permission('gerar_relatorios')
|
||||
def excluir_relatorio_cotas(id):
|
||||
"""Exclui um relatório de cotas existente"""
|
||||
if RelatorioService.excluir_relatorio_cotas(id):
|
||||
flash('Relatório excluído com sucesso!', 'success')
|
||||
else:
|
||||
flash('Erro ao excluir relatório', 'danger')
|
||||
|
||||
return redirect(url_for('relatorio.listar_cotas'))
|
||||
|
||||
# Rotas para relatórios de vendas
|
||||
@relatorio_bp.route("/vendas")
|
||||
@login_required
|
||||
@require_permission('visualizar_relatorios')
|
||||
def listar_vendas():
|
||||
"""Lista todos os relatórios de vendas"""
|
||||
relatorios = RelatorioService.listar_relatorios_vendas()
|
||||
return render_template("listar_relatorios_vendas.html", relatorios=relatorios)
|
||||
|
||||
@relatorio_bp.route("/vendas/novo", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@require_permission('gerar_relatorios')
|
||||
def novo_relatorio_vendas():
|
||||
"""Cria um novo relatório de vendas"""
|
||||
if request.method == "POST":
|
||||
setor_id = request.form.get("setor_id")
|
||||
comite_id = request.form.get("comite_id")
|
||||
total_vendas = float(request.form.get("total_vendas"))
|
||||
data_relatorio = request.form.get("data_relatorio")
|
||||
|
||||
# Validar data
|
||||
if not validar_data(data_relatorio):
|
||||
flash('Data do relatório inválida', 'danger')
|
||||
return render_template("novo_relatorio_vendas.html")
|
||||
|
||||
# Converter data
|
||||
data_relatorio = converter_data(data_relatorio)
|
||||
|
||||
# Validar data futura
|
||||
if data_relatorio > date.today():
|
||||
flash('A data do relatório não pode ser futura', 'danger')
|
||||
return render_template("novo_relatorio_vendas.html")
|
||||
|
||||
if RelatorioService.criar_relatorio_vendas(setor_id, comite_id, total_vendas, data_relatorio):
|
||||
flash('Relatório de vendas cadastrado com sucesso!', 'success')
|
||||
return redirect(url_for('relatorio.listar_vendas'))
|
||||
else:
|
||||
flash('Erro ao cadastrar relatório de vendas', 'danger')
|
||||
return render_template("novo_relatorio_vendas.html")
|
||||
|
||||
# GET
|
||||
setores = SetorService.listar_setores()
|
||||
comites = ComiteService.listar_comites()
|
||||
return render_template("novo_relatorio_vendas.html",
|
||||
setores=setores,
|
||||
comites=comites,
|
||||
hoje=date.today().strftime('%Y-%m-%d'))
|
||||
@@ -1,198 +0,0 @@
|
||||
import os
|
||||
import pyotp
|
||||
from pathlib import Path
|
||||
from functions.database import Usuario, Role, get_db_session
|
||||
from services.otp_service import generate_qr_code
|
||||
|
||||
ADMIN_USERNAME = "admin"
|
||||
ADMIN_PASSWORD = "admin123"
|
||||
ADMIN_ROLE = Role.SECRETARIO_GERAL
|
||||
|
||||
|
||||
def salvar_qr_code(user):
|
||||
"""
|
||||
Gera o QR code para um usuário específico
|
||||
|
||||
Args:
|
||||
user: Instância do modelo Usuario
|
||||
|
||||
Returns:
|
||||
tuple: (caminho do arquivo, URI do OTP)
|
||||
"""
|
||||
# Tentar diferentes caminhos para salvar o QR code
|
||||
qr_paths = [
|
||||
Path('/tmp/admin_qr.png'), # Diretório temporário do sistema
|
||||
Path('/data/admin_qr.png'), # Diretório de dados do container
|
||||
Path('admin_qr.png') # Diretório atual (fallback fora do container)
|
||||
]
|
||||
|
||||
# Tentar salvar em diferentes locais
|
||||
qr_saved = False
|
||||
saved_path = None
|
||||
|
||||
img = generate_qr_code(user) # Gera o QR code para o usuário
|
||||
|
||||
for qr_path in qr_paths:
|
||||
try:
|
||||
# Tentar salvar o arquivo
|
||||
img.save(qr_path)
|
||||
qr_saved = True
|
||||
saved_path = qr_path
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Não foi possível salvar o QR code em {qr_path}: {e}")
|
||||
continue
|
||||
|
||||
if not qr_saved:
|
||||
print("AVISO: Não foi possível salvar o QR code em nenhum local")
|
||||
print("O QR code pode ser gerado manualmente usando o URI OTP")
|
||||
saved_path = None
|
||||
|
||||
return saved_path
|
||||
|
||||
|
||||
def _ensure_admin_role(db, admin_user, role):
|
||||
admin_role = db.query(Role).filter_by(nome=role).first()
|
||||
if admin_role is None:
|
||||
admin_role = Role(nome=role, nivel=Role.SECRETARIO_GERAL)
|
||||
db.add(admin_role)
|
||||
db.flush()
|
||||
|
||||
if admin_role not in admin_user.roles:
|
||||
admin_user.roles.append(admin_role)
|
||||
db.flush()
|
||||
|
||||
|
||||
def _ensure_admin_otp(db, admin_user):
|
||||
if admin_user.otp_secret:
|
||||
return False
|
||||
secret = (os.environ.get('ADMIN_OTP_SECRET') or "").strip()
|
||||
admin_user.otp_secret = secret or admin_user.generate_otp_secret()
|
||||
db.flush()
|
||||
return True
|
||||
|
||||
|
||||
def create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE, save_qr=True):
|
||||
"""Limpa e cria o usuário admin"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Verificar se já existe um usuário admin
|
||||
admin_user = db.query(Usuario).filter_by(username=username).first()
|
||||
if admin_user is not None:
|
||||
db.delete(admin_user)
|
||||
db.flush()
|
||||
|
||||
print("\n=== Criando Novo Usuário Admin ===")
|
||||
admin_user = Usuario(
|
||||
username=username,
|
||||
email="admin@example.com",
|
||||
is_admin=True
|
||||
)
|
||||
admin_user.set_password(password)
|
||||
db.add(admin_user)
|
||||
_ensure_admin_otp(db, admin_user)
|
||||
_ensure_admin_role(db, admin_user, role)
|
||||
|
||||
db.commit()
|
||||
|
||||
qr_path = salvar_qr_code(admin_user)
|
||||
|
||||
# Mostrar informações
|
||||
print("\n=== Informações do Admin ===")
|
||||
print(f"Username: {admin_user.username}")
|
||||
print(f"Email: {admin_user.email}")
|
||||
print(f"Senha: {password}")
|
||||
print(f"Segredo OTP: {admin_user.otp_secret}")
|
||||
print(f"URI do OTP: {admin_user.get_otp_uri()}")
|
||||
if qr_path:
|
||||
print(f"QR Code salvo em: {qr_path}")
|
||||
else:
|
||||
print("QR Code não foi salvo. Use o URI do OTP ou o Segredo OTP para configuração manual.")
|
||||
|
||||
print("\n=== Instruções para Configuração ===")
|
||||
print("1. Instale um aplicativo autenticador no seu celular")
|
||||
print(" (Google Authenticator, Microsoft Authenticator, etc)")
|
||||
print("2. Abra o aplicativo")
|
||||
print("3. Selecione a opção para adicionar uma nova conta")
|
||||
if qr_path:
|
||||
print("4. Escaneie o QR Code salvo em:", qr_path)
|
||||
print("\nOU configure manualmente:")
|
||||
print(f"- Nome da conta: {admin_user.username}")
|
||||
print(f"- Segredo OTP: {admin_user.otp_secret}")
|
||||
print("- Tipo: Baseado em tempo (TOTP)")
|
||||
print("- Algoritmo: SHA1")
|
||||
print("- Dígitos: 6")
|
||||
print("- Intervalo: 30 segundos")
|
||||
|
||||
# Gerar código atual para verificação
|
||||
totp = pyotp.TOTP(admin_user.otp_secret)
|
||||
current_code = totp.now()
|
||||
print("\n=== Verificação do OTP ===")
|
||||
print(f"Código OTP atual: {current_code}")
|
||||
is_valid = admin_user.verify_otp(current_code)
|
||||
print(f"Verificação do código: {'Sucesso' if is_valid else 'Falha'}")
|
||||
|
||||
if not is_valid:
|
||||
print("\nALERTA: Verificação do OTP falhou!")
|
||||
print("Por favor, verifique se o segredo OTP está correto.")
|
||||
|
||||
# Fazer commit final para garantir que tudo foi salvo
|
||||
db.commit()
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def verify_admin(username=ADMIN_USERNAME, role=ADMIN_ROLE, save_qr=False):
|
||||
"""Verifica se o usuário admin existe e tem OTP configurado"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
admin_user = db.query(Usuario).filter_by(username=username).first()
|
||||
if admin_user is not None:
|
||||
print("\n=== Usuário Admin Encontrado ===")
|
||||
_ensure_admin_otp(db, admin_user)
|
||||
_ensure_admin_role(db, admin_user, role)
|
||||
return True
|
||||
else:
|
||||
print("\n=== Usuário Admin NÃO Encontrado ===")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao verificar o usuário admin: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def rotate_admin_otp(username=ADMIN_USERNAME, save_qr=False):
|
||||
db = get_db_session()
|
||||
try:
|
||||
admin_user = db.query(Usuario).filter_by(username=username).first()
|
||||
if admin_user is None:
|
||||
print("Usuário admin não encontrado")
|
||||
return False
|
||||
|
||||
admin_user.generate_otp_secret()
|
||||
db.commit()
|
||||
print(f"OTP do usuário '{username}' foi rotacionado.")
|
||||
print(f"Novo segredo OTP: {admin_user.otp_secret}")
|
||||
|
||||
if save_qr:
|
||||
qr_path = salvar_qr_code(admin_user)
|
||||
if qr_path:
|
||||
print(f"Novo QR code salvo em: {qr_path}")
|
||||
else:
|
||||
print("Não foi possível salvar o QR code automaticamente.")
|
||||
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_admin()
|
||||
27
scripts/init_db.py
Normal file
27
scripts/init_db.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from functions.database import Role, Permissao, RolePermissao, Base, engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
with Session(engine) as session:
|
||||
# Criar roles
|
||||
admin = Role(nome='Administrador', nivel=1)
|
||||
coord = Role(nome='Coordenador', nivel=2)
|
||||
milit = Role(nome='Militante', nivel=3)
|
||||
|
||||
# Criar permissões
|
||||
perm_admin = Permissao(nome='admin', descricao='Acesso total')
|
||||
perm_militantes = Permissao(nome='ver_militantes', descricao='Ver militantes')
|
||||
# ... outras permissões ...
|
||||
|
||||
session.add_all([admin, coord, milit, perm_admin, perm_militantes])
|
||||
session.commit()
|
||||
|
||||
# Associar permissões aos roles
|
||||
session.add(RolePermissao(role=admin, permissao=perm_admin))
|
||||
session.add(RolePermissao(role=coord, permissao=perm_militantes))
|
||||
session.commit()
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_db()
|
||||
@@ -1,92 +0,0 @@
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
# Carregar .env antes de importar módulos
|
||||
load_dotenv(ROOT_DIR / ".env")
|
||||
|
||||
from functions.base import Base, engine, get_db_session
|
||||
from functions.rbac import Role, init_rbac
|
||||
from scripts.create_admin import create_admin, rotate_admin_otp
|
||||
from scripts.create_test_users import create_test_users
|
||||
from scripts.seed_database import seed_database
|
||||
|
||||
|
||||
ADMIN_USERNAME = "admin"
|
||||
ADMIN_PASSWORD = "admin123"
|
||||
ADMIN_ROLE = Role.SECRETARIO_GERAL
|
||||
|
||||
def reset_db(args):
|
||||
"""Inicializando banco de dados e criando tabelas"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Criar todas as tabelas
|
||||
Base.metadata.drop_all(engine)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro na drop ou create all da Base: {e}")
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
print("Inicializando sistema RBAC...")
|
||||
init_rbac()
|
||||
|
||||
print("Cria usuário admin...")
|
||||
create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE)
|
||||
|
||||
print("Banco inicializado com sucesso.")
|
||||
return 0
|
||||
|
||||
def seed_db_with_fakes(args):
|
||||
"""Função para popular o banco com dados fake para desenvolvimento"""
|
||||
seed_database()
|
||||
|
||||
def seed_db_test_users(args):
|
||||
"""Função para popular o banco com dados fake para desenvolvimento"""
|
||||
create_test_users()
|
||||
|
||||
def reset_admin(args):
|
||||
create_admin(username=ADMIN_USERNAME, password=ADMIN_PASSWORD, role=ADMIN_ROLE)
|
||||
|
||||
def rotate_admin_otp_cmd(args):
|
||||
rotate_admin_otp(username=ADMIN_USERNAME, save_qr=True)
|
||||
|
||||
|
||||
def build_parser():
|
||||
parser = argparse.ArgumentParser(description="Gerenciador de comandos do sistema Controles")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
db_reset_parser = subparsers.add_parser("db_reset", help="Reseta o banco e recria tabelas, RBAC e admin")
|
||||
db_reset_parser.set_defaults(func=reset_db)
|
||||
|
||||
db_seed_fake_parser = subparsers.add_parser("db_seed_fake", help="Adiciona dados falsos para desenvolvimento")
|
||||
db_seed_fake_parser.set_defaults(func=seed_db_with_fakes)
|
||||
|
||||
db_seed_test_users_parser = subparsers.add_parser("db_seed_test_users", help="Adiciona usuários de teste para desenvolvimento")
|
||||
db_seed_test_users_parser.set_defaults(func=seed_db_test_users)
|
||||
|
||||
admin_reset_parser = subparsers.add_parser("admin_reset", help="Reseta o usuário admin (padrão: admin123)")
|
||||
admin_reset_parser.set_defaults(func=reset_admin)
|
||||
|
||||
admin_rotate_otp_parser = subparsers.add_parser("admin_rotate_otp", help="Rotaciona o OTP do usuário admin - se não definido em .env")
|
||||
admin_rotate_otp_parser.set_defaults(func=rotate_admin_otp_cmd)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main():
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
44
scripts/prepare_mvc.sh
Executable file
44
scripts/prepare_mvc.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script para preparar a estrutura MVC
|
||||
|
||||
echo "Preparando a estrutura MVC para o Sistema de Controles..."
|
||||
|
||||
# Criar estrutura de diretórios
|
||||
echo "Criando estrutura de diretórios..."
|
||||
mkdir -p models/entities controllers services
|
||||
|
||||
# Mover arquivos refatorados
|
||||
echo "Movendo arquivos refatorados..."
|
||||
cp app.py.new app.py
|
||||
|
||||
# Criar arquivo __init__.py nos diretórios Python
|
||||
echo "Criando arquivos de inicialização..."
|
||||
touch models/__init__.py
|
||||
touch models/entities/__init__.py
|
||||
touch controllers/__init__.py
|
||||
touch services/__init__.py
|
||||
|
||||
# Criar arquivo __init__.py com importações para models/entities
|
||||
cat > models/entities/__init__.py << EOF
|
||||
from models.entities.base import Base
|
||||
from models.entities.usuario import Usuario, TipoUsuario
|
||||
from models.entities.militante import Militante, EstadoMilitante
|
||||
from models.entities.endereco import Endereco
|
||||
from models.entities.email_militante import EmailMilitante
|
||||
from models.entities.rede_social import RedeSocial
|
||||
from models.entities.cota_mensal import CotaMensal
|
||||
from models.entities.pagamento import Pagamento
|
||||
from models.entities.tipo_material import TipoMaterial
|
||||
from models.entities.material_vendido import MaterialVendido
|
||||
from models.entities.venda_jornal import VendaJornal
|
||||
from models.entities.venda_jornal_avulso import VendaJornalAvulso
|
||||
from models.entities.assinatura_jornal import AssinaturaJornal
|
||||
from models.entities.comprovante import Comprovante
|
||||
EOF
|
||||
|
||||
echo "Todos os arquivos criados com sucesso!"
|
||||
echo "Para usar a nova estrutura MVC, execute:"
|
||||
echo "1. chmod +x scripts/prepare_mvc.sh"
|
||||
echo "2. ./scripts/prepare_mvc.sh"
|
||||
echo "3. python app.py"
|
||||
@@ -1,32 +1,34 @@
|
||||
from datetime import datetime, timedelta
|
||||
from functions.database import (
|
||||
Base, Militante, CotaMensal, TipoPagamento, Pagamento,
|
||||
Base, Militante, CotaMensal, TipoComprovante, Comprovante,
|
||||
MaterialVendido, TipoMaterial, VendaJornalAvulso, AssinaturaAnual,
|
||||
RelatorioCotasMensais, RelatorioVendasMateriais,
|
||||
RelatorioCotasMensais, RelatorioVendasMateriais, engine, SessionLocal,
|
||||
Setor, ComiteCentral, Usuario, Role, EmailMilitante, Endereco,
|
||||
ComiteRegional, Celula, EstadoMilitante, get_db_session
|
||||
ComiteRegional, Celula, EstadoMilitante, get_db_connection,
|
||||
init_database
|
||||
)
|
||||
import random
|
||||
from faker import Faker
|
||||
import time
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
fake = Faker('pt_BR')
|
||||
|
||||
def criar_estrutura_organizacional(db):
|
||||
def criar_estrutura_organizacional(session):
|
||||
"""Cria a estrutura organizacional básica"""
|
||||
print("\nCriando estrutura organizacional...")
|
||||
|
||||
# Criar Comitê Central
|
||||
cc = ComiteCentral(nome="Comitê Central SP")
|
||||
db.add(cc)
|
||||
db.flush()
|
||||
session.add(cc)
|
||||
session.flush()
|
||||
|
||||
# Criar Comitês Regionais
|
||||
crs = []
|
||||
for nome in ["CR São Paulo", "CR ABC", "CR Campinas"]:
|
||||
cr = ComiteRegional(nome=nome)
|
||||
db.add(cr)
|
||||
db.flush()
|
||||
session.add(cr)
|
||||
session.flush()
|
||||
crs.append(cr)
|
||||
|
||||
# Criar Setores para cada CR
|
||||
@@ -37,8 +39,8 @@ def criar_estrutura_organizacional(db):
|
||||
nome=f"Setor {i+1} - {cr.nome}",
|
||||
cr_id=cr.id
|
||||
)
|
||||
db.add(setor)
|
||||
db.flush()
|
||||
session.add(setor)
|
||||
session.flush()
|
||||
setores.append(setor)
|
||||
|
||||
# Criar Células para cada Setor
|
||||
@@ -48,27 +50,35 @@ def criar_estrutura_organizacional(db):
|
||||
nome=f"Célula {i+1} - {setor.nome}",
|
||||
setor_id=setor.id
|
||||
)
|
||||
db.add(celula)
|
||||
session.add(celula)
|
||||
|
||||
db.commit()
|
||||
session.commit()
|
||||
return crs, setores
|
||||
|
||||
def criar_tipos_pagamento(db):
|
||||
"""Cria tipos de pagamento padrão"""
|
||||
print("\nCriando tipos de pagamento...")
|
||||
def criar_tipos_comprovante(session):
|
||||
"""Cria tipos de comprovante padrão"""
|
||||
print("\nCriando tipos de comprovante...")
|
||||
tipos = [
|
||||
"Dinheiro",
|
||||
"PIX",
|
||||
"Cartão de Crédito",
|
||||
"Cartão de Débito",
|
||||
"Transferência Bancária"
|
||||
"Comprovante Padrão",
|
||||
"Comprovante Especial",
|
||||
"Comprovante Extraordinário",
|
||||
"Jornal Avulso",
|
||||
"Assinatura de Jornal",
|
||||
"Campanha Financeira"
|
||||
]
|
||||
for tipo in tipos:
|
||||
if not db.query(TipoPagamento).filter_by(descricao=tipo).first():
|
||||
db.add(TipoPagamento(descricao=tipo))
|
||||
db.commit()
|
||||
|
||||
def criar_tipos_material(db):
|
||||
for tipo in tipos:
|
||||
if not session.query(TipoComprovante).filter_by(descricao=tipo).first():
|
||||
session.add(TipoComprovante(descricao=tipo))
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
print("Tipos de comprovante criados com sucesso!")
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
print(f"Erro ao criar tipos de comprovante: {e}")
|
||||
|
||||
def criar_tipos_material(session):
|
||||
"""Cria tipos de material padrão"""
|
||||
print("\nCriando tipos de material...")
|
||||
tipos = [
|
||||
@@ -79,11 +89,11 @@ def criar_tipos_material(db):
|
||||
"Cartilha"
|
||||
]
|
||||
for tipo in tipos:
|
||||
if not db.query(TipoMaterial).filter_by(descricao=tipo).first():
|
||||
db.add(TipoMaterial(descricao=tipo))
|
||||
db.commit()
|
||||
if not session.query(TipoMaterial).filter_by(descricao=tipo).first():
|
||||
session.add(TipoMaterial(descricao=tipo))
|
||||
session.commit()
|
||||
|
||||
def criar_militantes(db, num_militantes, setores):
|
||||
def criar_militantes(session, num_militantes, setores):
|
||||
"""Cria militantes com todos os dados necessários"""
|
||||
print(f"\nCriando {num_militantes} militantes...")
|
||||
militantes = []
|
||||
@@ -95,6 +105,13 @@ def criar_militantes(db, num_militantes, setores):
|
||||
nome = fake.name()
|
||||
cpf = fake.cpf()
|
||||
|
||||
# Email único
|
||||
while True:
|
||||
email = fake.email()
|
||||
if email not in emails_usados:
|
||||
emails_usados.add(email)
|
||||
break
|
||||
|
||||
# Criar endereço
|
||||
endereco = Endereco(
|
||||
cep=fake.postcode(),
|
||||
@@ -105,12 +122,12 @@ def criar_militantes(db, num_militantes, setores):
|
||||
numero=str(random.randint(1, 999)),
|
||||
complemento=f"Bloco {random.randint(1, 10)}, Apto {random.randint(1, 999)}" if random.random() < 0.3 else None
|
||||
)
|
||||
db.add(endereco)
|
||||
db.flush()
|
||||
session.add(endereco)
|
||||
session.flush()
|
||||
|
||||
# Selecionar setor e célula aleatórios
|
||||
setor = random.choice(setores)
|
||||
celula = random.choice(db.query(Celula).filter_by(setor_id=setor.id).all())
|
||||
celula = random.choice(session.query(Celula).filter_by(setor_id=setor.id).all())
|
||||
|
||||
# Definir responsabilidades
|
||||
responsabilidades = 0
|
||||
@@ -160,34 +177,27 @@ def criar_militantes(db, num_militantes, setores):
|
||||
responsabilidades=responsabilidades,
|
||||
estado=random.choice(list(EstadoMilitante))
|
||||
)
|
||||
db.add(militante)
|
||||
db.flush()
|
||||
|
||||
# Email único
|
||||
while True:
|
||||
email = fake.email()
|
||||
if email not in emails_usados:
|
||||
emails_usados.add(email)
|
||||
break
|
||||
session.add(militante)
|
||||
session.flush()
|
||||
|
||||
# Criar email do militante
|
||||
email_militante = EmailMilitante(
|
||||
militante_id=militante.id,
|
||||
endereco_email=email
|
||||
)
|
||||
db.add(email_militante)
|
||||
session.add(email_militante)
|
||||
|
||||
militantes.append(militante)
|
||||
db.commit()
|
||||
session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar militante {i+1}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
continue
|
||||
|
||||
return militantes
|
||||
|
||||
def criar_cotas(db, militantes):
|
||||
def criar_cotas(session, militantes):
|
||||
"""Cria cotas mensais para os militantes"""
|
||||
print("\nCriando cotas mensais...")
|
||||
for militante in militantes:
|
||||
@@ -204,38 +214,38 @@ def criar_cotas(db, militantes):
|
||||
data_vencimento=data_base + timedelta(days=30),
|
||||
pago=random.choice([True, False])
|
||||
)
|
||||
db.add(cota)
|
||||
db.commit()
|
||||
session.add(cota)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar cotas para militante {militante.nome}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
|
||||
def criar_pagamentos(db, militantes):
|
||||
"""Cria pagamentos para os militantes"""
|
||||
print("\nCriando pagamentos...")
|
||||
tipos_pagamento = db.query(TipoPagamento).all()
|
||||
def criar_comprovantes(session, militantes):
|
||||
"""Cria comprovantes para os militantes"""
|
||||
print("\nCriando comprovantes...")
|
||||
tipos_comprovante = session.query(TipoComprovante).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
# Criar entre 3 e 8 pagamentos por militante
|
||||
# Criar entre 3 e 8 comprovantes por militante
|
||||
for _ in range(random.randint(3, 8)):
|
||||
tipo = random.choice(tipos_pagamento)
|
||||
pagamento = Pagamento(
|
||||
tipo = random.choice(tipos_comprovante)
|
||||
comprovante = Comprovante(
|
||||
militante_id=militante.id,
|
||||
tipo_pagamento=tipo.descricao, # Usando a descrição do tipo
|
||||
valor=random.uniform(50, 500),
|
||||
data_pagamento=fake.date_between(start_date='-1y', end_date='today')
|
||||
tipo_comprovante=tipo.descricao, # Usando a descrição do tipo
|
||||
valor=random.uniform(10, 1000),
|
||||
data_comprovante=fake.date_between(start_date='-1y', end_date='today')
|
||||
)
|
||||
db.add(pagamento)
|
||||
db.commit()
|
||||
session.add(comprovante)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar pagamentos para militante {militante.nome}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
print(f"Erro ao criar comprovantes para militante {militante.nome}: {e}")
|
||||
|
||||
def criar_materiais_vendidos(db, militantes):
|
||||
def criar_materiais_vendidos(session, militantes):
|
||||
"""Cria registros de materiais vendidos"""
|
||||
print("\nCriando materiais vendidos...")
|
||||
tipos_material = db.query(TipoMaterial).all()
|
||||
tipos_material = session.query(TipoMaterial).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
@@ -248,13 +258,13 @@ def criar_materiais_vendidos(db, militantes):
|
||||
valor=random.uniform(20, 100),
|
||||
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||
)
|
||||
db.add(material)
|
||||
db.commit()
|
||||
session.add(material)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar materiais vendidos para militante {militante.nome}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
|
||||
def criar_vendas_jornal(db, militantes):
|
||||
def criar_vendas_jornal(session, militantes):
|
||||
"""Cria vendas de jornal avulso"""
|
||||
print("\nCriando vendas de jornal...")
|
||||
for militante in militantes:
|
||||
@@ -269,16 +279,16 @@ def criar_vendas_jornal(db, militantes):
|
||||
valor_total=quantidade * valor_unitario,
|
||||
data_venda=fake.date_time_between(start_date='-1y', end_date='now')
|
||||
)
|
||||
db.add(venda)
|
||||
db.commit()
|
||||
session.add(venda)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar vendas de jornal para militante {militante.nome}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
|
||||
def criar_assinaturas(db, militantes):
|
||||
def criar_assinaturas(session, militantes):
|
||||
"""Cria assinaturas anuais"""
|
||||
print("\nCriando assinaturas anuais...")
|
||||
tipos_material = db.query(TipoMaterial).all()
|
||||
tipos_material = session.query(TipoMaterial).all()
|
||||
|
||||
for militante in militantes:
|
||||
try:
|
||||
@@ -293,45 +303,42 @@ def criar_assinaturas(db, militantes):
|
||||
data_inicio=data_inicio,
|
||||
data_fim=data_inicio + timedelta(days=365)
|
||||
)
|
||||
db.add(assinatura)
|
||||
db.commit()
|
||||
session.add(assinatura)
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
print(f"Erro ao criar assinatura para militante {militante.nome}: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
|
||||
def seed_database():
|
||||
"""Função principal para popular o banco de dados"""
|
||||
db = get_db_session()
|
||||
session = get_db_connection()
|
||||
try:
|
||||
print("Iniciando população do banco de dados...")
|
||||
|
||||
# Criar tipos básicos
|
||||
criar_tipos_pagamento(db)
|
||||
criar_tipos_material(db)
|
||||
|
||||
# Criar estrutura organizacional
|
||||
crs, setores = criar_estrutura_organizacional(db)
|
||||
crs, setores = criar_estrutura_organizacional(session)
|
||||
|
||||
# Criar tipos básicos
|
||||
criar_tipos_comprovante(session)
|
||||
criar_tipos_material(session)
|
||||
|
||||
# Criar militantes (30 militantes para teste)
|
||||
militantes = criar_militantes(db, 30, setores)
|
||||
militantes = criar_militantes(session, 30, setores)
|
||||
|
||||
# Criar dados financeiros e materiais
|
||||
criar_cotas(db, militantes)
|
||||
criar_pagamentos(db, militantes)
|
||||
criar_materiais_vendidos(db, militantes)
|
||||
criar_vendas_jornal(db, militantes)
|
||||
criar_assinaturas(db, militantes)
|
||||
criar_cotas(session, militantes)
|
||||
criar_comprovantes(session, militantes)
|
||||
criar_materiais_vendidos(session, militantes)
|
||||
criar_vendas_jornal(session, militantes)
|
||||
criar_assinaturas(session, militantes)
|
||||
|
||||
print("\nBanco de dados populado com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Erro durante a população do banco: {e}")
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
seed_database()
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Services package
|
||||
@@ -1,141 +0,0 @@
|
||||
from functions.database import get_db_session, Usuario
|
||||
from flask_login import login_user, logout_user
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from services.otp_service import generate_qr_code_base64
|
||||
|
||||
class AuthService:
|
||||
"""Service para operações de autenticação"""
|
||||
|
||||
@staticmethod
|
||||
def autenticar_usuario(email_or_username: str, password: str, otp: str = None) -> Dict:
|
||||
"""Autentica um usuário"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Tenta encontrar o usuário por email ou username
|
||||
user = db.query(Usuario).filter(
|
||||
(Usuario.email == email_or_username) |
|
||||
(Usuario.username == email_or_username)
|
||||
).first()
|
||||
|
||||
if not user or not user.check_password(password):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Email/usuário ou senha incorretos.'
|
||||
}
|
||||
|
||||
# Verificar OTP se o usuário tiver configurado
|
||||
if user.otp_secret and not otp:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Código OTP é obrigatório para sua conta.'
|
||||
}
|
||||
|
||||
if user.otp_secret and not user.verify_otp(otp):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Código OTP inválido.'
|
||||
}
|
||||
|
||||
# Atualizar último login
|
||||
user.ultimo_login = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
# Fazer login
|
||||
login_user(user)
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'user': user
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro na autenticação: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def desautenticar_usuario(user) -> Dict:
|
||||
"""Desautentica um usuário"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
if user:
|
||||
user.logout()
|
||||
db.commit()
|
||||
logout_user()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Logout realizado com sucesso!'
|
||||
}
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro no logout: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def alterar_senha(user_id: int, senha_atual: str, nova_senha: str) -> Dict:
|
||||
"""Altera a senha de um usuário"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
user = db.query(Usuario).get(user_id)
|
||||
if not user:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Usuário não encontrado.'
|
||||
}
|
||||
|
||||
if not user.check_password(senha_atual):
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Senha atual incorreta.'
|
||||
}
|
||||
|
||||
user.set_password(nova_senha)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'message': 'Senha alterada com sucesso!'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'Erro ao alterar senha: {str(e)}'
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def gerar_qr_code(user) -> str:
|
||||
"""Gera um QR code para o usuário"""
|
||||
return generate_qr_code_base64(user)
|
||||
|
||||
@staticmethod
|
||||
def verificar_sessao(user) -> Dict:
|
||||
"""Verifica se a sessão do usuário ainda é válida"""
|
||||
if not user.is_authenticated:
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Usuário não autenticado'
|
||||
}
|
||||
|
||||
if user.is_session_expired():
|
||||
return {
|
||||
'valid': False,
|
||||
'message': 'Sessão expirada'
|
||||
}
|
||||
|
||||
return {
|
||||
'valid': True
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import redis
|
||||
import json
|
||||
import pickle
|
||||
from typing import Any, Optional, Union, Dict, List
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import logging
|
||||
from functools import wraps
|
||||
import hashlib
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CacheService:
|
||||
"""Service for Redis caching operations"""
|
||||
|
||||
def __init__(self, redis_url: str = None):
|
||||
"""Initialize Redis connection"""
|
||||
self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
||||
self.redis = None
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Establish Redis connection"""
|
||||
try:
|
||||
self.redis = redis.from_url(self.redis_url, decode_responses=False)
|
||||
# Test connection
|
||||
self.redis.ping()
|
||||
logger.info("Redis connection established successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
self.redis = None
|
||||
|
||||
def _is_connected(self) -> bool:
|
||||
"""Check if Redis is connected"""
|
||||
if not self.redis:
|
||||
return False
|
||||
try:
|
||||
self.redis.ping()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get value from cache"""
|
||||
if not self._is_connected():
|
||||
return default
|
||||
|
||||
try:
|
||||
value = self.redis.get(key)
|
||||
if value is None:
|
||||
return default
|
||||
return pickle.loads(value)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cache key {key}: {e}")
|
||||
return default
|
||||
|
||||
def set(self, key: str, value: Any, expire: int = 3600) -> bool:
|
||||
"""Set value in cache with expiration"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
serialized_value = pickle.dumps(value)
|
||||
return self.redis.setex(key, expire, serialized_value)
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting cache key {key}: {e}")
|
||||
return False
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""Delete key from cache"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
return bool(self.redis.delete(key))
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting cache key {key}: {e}")
|
||||
return False
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
"""Check if key exists in cache"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
return bool(self.redis.exists(key))
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking cache key {key}: {e}")
|
||||
return False
|
||||
|
||||
def expire(self, key: str, seconds: int) -> bool:
|
||||
"""Set expiration for key"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
return bool(self.redis.expire(key, seconds))
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting expiration for cache key {key}: {e}")
|
||||
return False
|
||||
|
||||
def ttl(self, key: str) -> int:
|
||||
"""Get time to live for key"""
|
||||
if not self._is_connected():
|
||||
return -1
|
||||
|
||||
try:
|
||||
return self.redis.ttl(key)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting TTL for cache key {key}: {e}")
|
||||
return -1
|
||||
|
||||
def clear_pattern(self, pattern: str) -> int:
|
||||
"""Clear all keys matching pattern"""
|
||||
if not self._is_connected():
|
||||
return 0
|
||||
|
||||
try:
|
||||
keys = self.redis.keys(pattern)
|
||||
if keys:
|
||||
return self.redis.delete(*keys)
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing cache pattern {pattern}: {e}")
|
||||
return 0
|
||||
|
||||
def clear_all(self) -> bool:
|
||||
"""Clear all cache"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.redis.flushdb()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing all cache: {e}")
|
||||
return False
|
||||
|
||||
def get_many(self, keys: List[str]) -> Dict[str, Any]:
|
||||
"""Get multiple values from cache"""
|
||||
if not self._is_connected():
|
||||
return {}
|
||||
|
||||
try:
|
||||
values = self.redis.mget(keys)
|
||||
result = {}
|
||||
for key, value in zip(keys, values):
|
||||
if value is not None:
|
||||
result[key] = pickle.loads(value)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting multiple cache keys: {e}")
|
||||
return {}
|
||||
|
||||
def set_many(self, data: Dict[str, Any], expire: int = 3600) -> bool:
|
||||
"""Set multiple values in cache"""
|
||||
if not self._is_connected():
|
||||
return False
|
||||
|
||||
try:
|
||||
pipeline = self.redis.pipeline()
|
||||
for key, value in data.items():
|
||||
serialized_value = pickle.dumps(value)
|
||||
pipeline.setex(key, expire, serialized_value)
|
||||
pipeline.execute()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error setting multiple cache keys: {e}")
|
||||
return False
|
||||
|
||||
def increment(self, key: str, amount: int = 1) -> Optional[int]:
|
||||
"""Increment counter in cache"""
|
||||
if not self._is_connected():
|
||||
return None
|
||||
|
||||
try:
|
||||
return self.redis.incr(key, amount)
|
||||
except Exception as e:
|
||||
logger.error(f"Error incrementing cache key {key}: {e}")
|
||||
return None
|
||||
|
||||
def decrement(self, key: str, amount: int = 1) -> Optional[int]:
|
||||
"""Decrement counter in cache"""
|
||||
if not self._is_connected():
|
||||
return None
|
||||
|
||||
try:
|
||||
return self.redis.decr(key, amount)
|
||||
except Exception as e:
|
||||
logger.error(f"Error decrementing cache key {key}: {e}")
|
||||
return None
|
||||
|
||||
# Global cache instance
|
||||
cache_service = CacheService()
|
||||
|
||||
def cache_key_generator(*args, **kwargs) -> str:
|
||||
"""Generate cache key from function arguments"""
|
||||
# Create a hash of the arguments
|
||||
key_data = str(args) + str(sorted(kwargs.items()))
|
||||
return hashlib.md5(key_data.encode()).hexdigest()
|
||||
|
||||
def cached(expire: int = 3600, key_prefix: str = ""):
|
||||
"""Decorator for caching function results"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Generate cache key
|
||||
func_key = f"{key_prefix}:{func.__name__}:{cache_key_generator(*args, **kwargs)}"
|
||||
|
||||
# Try to get from cache
|
||||
cached_result = cache_service.get(func_key)
|
||||
if cached_result is not None:
|
||||
logger.debug(f"Cache hit for {func_key}")
|
||||
return cached_result
|
||||
|
||||
# Execute function and cache result
|
||||
result = func(*args, **kwargs)
|
||||
cache_service.set(func_key, result, expire)
|
||||
logger.debug(f"Cache miss for {func_key}, stored result")
|
||||
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def invalidate_cache_pattern(pattern: str):
|
||||
"""Decorator to invalidate cache after function execution"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
cache_service.clear_pattern(pattern)
|
||||
logger.debug(f"Invalidated cache pattern: {pattern}")
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# Cache key constants
|
||||
class CacheKeys:
|
||||
"""Constants for cache keys"""
|
||||
MILITANTE_LIST = "militantes:list"
|
||||
MILITANTE_DETAIL = "militante:detail:{}"
|
||||
PAGAMENTO_LIST = "pagamentos:list"
|
||||
PAGAMENTO_DETAIL = "pagamento:detail:{}"
|
||||
COTA_LIST = "cotas:list"
|
||||
COTA_DETAIL = "cota:detail:{}"
|
||||
DASHBOARD_STATS = "dashboard:stats"
|
||||
USER_SESSION = "user:session:{}"
|
||||
API_RESPONSE = "api:response:{}"
|
||||
|
||||
@staticmethod
|
||||
def militante_detail(militante_id: int) -> str:
|
||||
return CacheKeys.MILITANTE_DETAIL.format(militante_id)
|
||||
|
||||
@staticmethod
|
||||
def pagamento_detail(pagamento_id: int) -> str:
|
||||
return CacheKeys.PAGAMENTO_DETAIL.format(pagamento_id)
|
||||
|
||||
@staticmethod
|
||||
def cota_detail(cota_id: int) -> str:
|
||||
return CacheKeys.COTA_DETAIL.format(cota_id)
|
||||
|
||||
@staticmethod
|
||||
def user_session(user_id: int) -> str:
|
||||
return CacheKeys.USER_SESSION.format(user_id)
|
||||
|
||||
@staticmethod
|
||||
def api_response(endpoint: str) -> str:
|
||||
return CacheKeys.API_RESPONSE.format(endpoint)
|
||||
@@ -1,78 +0,0 @@
|
||||
from services.database_service import DatabaseService
|
||||
from models.entities.celula import Celula
|
||||
|
||||
class CelulaService:
|
||||
"""Service for Celula operations"""
|
||||
|
||||
@staticmethod
|
||||
def get_all_celulas():
|
||||
"""Get all celulas from the database"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
celulas = db.query(Celula).all()
|
||||
return celulas
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def get_celula_by_id(celula_id):
|
||||
"""Get a celula by its ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
celula = db.query(Celula).get(celula_id)
|
||||
return celula
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def create_celula(data):
|
||||
"""Create a new celula"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
celula = Celula(**data)
|
||||
db.add(celula)
|
||||
db.commit()
|
||||
return celula
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def update_celula(celula_id, data):
|
||||
"""Update an existing celula"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
celula = db.query(Celula).get(celula_id)
|
||||
if not celula:
|
||||
return None
|
||||
|
||||
for key, value in data.items():
|
||||
setattr(celula, key, value)
|
||||
|
||||
db.commit()
|
||||
return celula
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def delete_celula(celula_id):
|
||||
"""Delete a celula"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
celula = db.query(Celula).get(celula_id)
|
||||
if not celula:
|
||||
return False
|
||||
|
||||
db.delete(celula)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,254 +0,0 @@
|
||||
from functions.database import get_db_session, Militante, Pagamento, CotaMensal, MaterialVendido, AssinaturaAnual, TipoPagamento
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import joinedload
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any
|
||||
from services.cache_service import cache_service, cached, CacheKeys, invalidate_cache_pattern
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DashboardService:
|
||||
"""Service for dashboard data aggregation with caching"""
|
||||
|
||||
@staticmethod
|
||||
@cached(expire=300, key_prefix="dashboard") # Cache for 5 minutes
|
||||
def get_dashboard_stats() -> Dict[str, Any]:
|
||||
"""Get dashboard statistics with caching"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Get cached stats first
|
||||
cache_key = CacheKeys.DASHBOARD_STATS
|
||||
cached_stats = cache_service.get(cache_key)
|
||||
if cached_stats:
|
||||
logger.debug("Using cached dashboard stats")
|
||||
return cached_stats
|
||||
|
||||
# Calculate fresh stats
|
||||
stats = DashboardService._calculate_stats(db)
|
||||
|
||||
# Cache the results
|
||||
cache_service.set(cache_key, stats, 300) # 5 minutes
|
||||
logger.debug("Cached fresh dashboard stats")
|
||||
|
||||
return stats
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting dashboard stats: {e}")
|
||||
return DashboardService._get_default_stats()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def _calculate_stats(db) -> Dict[str, Any]:
|
||||
"""Calculate dashboard statistics"""
|
||||
try:
|
||||
# Total militantes
|
||||
total_militantes = db.query(func.count(Militante.id)).scalar()
|
||||
|
||||
# Total cotas (soma dos valores)
|
||||
total_cotas_result = db.query(func.sum(CotaMensal.valor_novo)).scalar()
|
||||
total_cotas = f"{total_cotas_result:.2f}" if total_cotas_result else "0.00"
|
||||
|
||||
# Total de materiais vendidos
|
||||
total_materiais = db.query(func.count(MaterialVendido.id)).scalar()
|
||||
|
||||
# Total de assinaturas ativas
|
||||
total_assinaturas = db.query(func.count(AssinaturaAnual.id)).scalar()
|
||||
|
||||
# Últimos militantes cadastrados (limit 5) - eager load emails
|
||||
militantes_query = db.query(Militante).options(
|
||||
joinedload(Militante.emails)
|
||||
).order_by(Militante.id.desc()).limit(5).all()
|
||||
|
||||
# Convert militantes to dictionaries to avoid lazy loading issues
|
||||
ultimos_militantes = []
|
||||
for militante in militantes_query:
|
||||
militante_dict = {
|
||||
'id': militante.id,
|
||||
'nome': militante.nome,
|
||||
'emails': [{'endereco_email': email.endereco_email} for email in militante.emails]
|
||||
}
|
||||
ultimos_militantes.append(militante_dict)
|
||||
|
||||
# Últimos pagamentos (limit 5) - eager load militante
|
||||
pagamentos_query = db.query(Pagamento).options(
|
||||
joinedload(Pagamento.militante)
|
||||
).order_by(Pagamento.data_pagamento.desc()).limit(5).all()
|
||||
|
||||
# Convert pagamentos to dictionaries to avoid lazy loading issues
|
||||
ultimos_pagamentos = []
|
||||
for pagamento in pagamentos_query:
|
||||
pagamento_dict = {
|
||||
'id': pagamento.id,
|
||||
'valor': pagamento.valor,
|
||||
'data_pagamento': pagamento.data_pagamento,
|
||||
'militante': {
|
||||
'id': pagamento.militante.id,
|
||||
'nome': pagamento.militante.nome
|
||||
}
|
||||
}
|
||||
ultimos_pagamentos.append(pagamento_dict)
|
||||
|
||||
# Estatísticas por período
|
||||
hoje = datetime.now().date()
|
||||
inicio_mes = hoje.replace(day=1)
|
||||
|
||||
# Militantes cadastrados este mês
|
||||
militantes_mes = db.query(func.count(Militante.id)).filter(
|
||||
Militante.id >= 1 # Assuming ID is auto-increment
|
||||
).scalar()
|
||||
|
||||
# Pagamentos este mês
|
||||
pagamentos_mes = db.query(func.sum(Pagamento.valor)).filter(
|
||||
Pagamento.data_pagamento >= inicio_mes
|
||||
).scalar()
|
||||
total_pagamentos_mes = f"{pagamentos_mes:.2f}" if pagamentos_mes else "0.00"
|
||||
|
||||
return {
|
||||
'total_militantes': total_militantes,
|
||||
'total_cotas': total_cotas,
|
||||
'total_materiais': total_materiais,
|
||||
'total_assinaturas': total_assinaturas,
|
||||
'ultimos_militantes': ultimos_militantes,
|
||||
'ultimos_pagamentos': ultimos_pagamentos,
|
||||
'militantes_mes': militantes_mes,
|
||||
'pagamentos_mes': total_pagamentos_mes,
|
||||
'cache_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating dashboard stats: {e}")
|
||||
return DashboardService._get_default_stats()
|
||||
|
||||
@staticmethod
|
||||
def _get_default_stats() -> Dict[str, Any]:
|
||||
"""Get default statistics when calculation fails"""
|
||||
return {
|
||||
'total_militantes': 0,
|
||||
'total_cotas': "0.00",
|
||||
'total_materiais': 0,
|
||||
'total_assinaturas': 0,
|
||||
'ultimos_militantes': [],
|
||||
'ultimos_pagamentos': [],
|
||||
'militantes_mes': 0,
|
||||
'pagamentos_mes': "0.00",
|
||||
'cache_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@invalidate_cache_pattern("dashboard:*")
|
||||
def invalidate_dashboard_cache():
|
||||
"""Invalidate dashboard cache when data changes"""
|
||||
logger.info("Dashboard cache invalidated")
|
||||
|
||||
@staticmethod
|
||||
@cached(expire=600, key_prefix="dashboard") # Cache for 10 minutes
|
||||
def get_militante_stats() -> Dict[str, Any]:
|
||||
"""Get militante-specific statistics"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Militantes por estado
|
||||
estados = db.query(Militante.estado, func.count(Militante.id)).group_by(Militante.estado).all()
|
||||
|
||||
# Militantes por responsabilidade
|
||||
responsabilidades = {}
|
||||
militantes = db.query(Militante).all()
|
||||
|
||||
for militante in militantes:
|
||||
for resp in militante.get_responsabilidades():
|
||||
responsabilidades[resp] = responsabilidades.get(resp, 0) + 1
|
||||
|
||||
return {
|
||||
'estados': dict(estados),
|
||||
'responsabilidades': responsabilidades,
|
||||
'cache_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting militante stats: {e}")
|
||||
return {'estados': {}, 'responsabilidades': {}, 'cache_timestamp': datetime.now().isoformat()}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
@cached(expire=300, key_prefix="dashboard")
|
||||
def get_financial_stats() -> Dict[str, Any]:
|
||||
"""Get financial statistics"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
# Total de pagamentos
|
||||
total_pagamentos = db.query(func.sum(Pagamento.valor)).scalar()
|
||||
|
||||
# Pagamentos por mês (últimos 6 meses)
|
||||
hoje = datetime.now().date()
|
||||
stats_mensais = []
|
||||
|
||||
for i in range(6):
|
||||
inicio_mes = hoje.replace(day=1) - timedelta(days=30*i)
|
||||
fim_mes = inicio_mes.replace(day=28) + timedelta(days=4)
|
||||
fim_mes = fim_mes.replace(day=1) - timedelta(days=1)
|
||||
|
||||
valor_mes = db.query(func.sum(Pagamento.valor)).filter(
|
||||
Pagamento.data_pagamento >= inicio_mes,
|
||||
Pagamento.data_pagamento <= fim_mes
|
||||
).scalar()
|
||||
|
||||
stats_mensais.append({
|
||||
'mes': inicio_mes.strftime('%Y-%m'),
|
||||
'valor': float(valor_mes) if valor_mes else 0.0
|
||||
})
|
||||
|
||||
return {
|
||||
'total_pagamentos': float(total_pagamentos) if total_pagamentos else 0.0,
|
||||
'stats_mensais': stats_mensais,
|
||||
'cache_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting financial stats: {e}")
|
||||
return {
|
||||
'total_pagamentos': 0.0,
|
||||
'stats_mensais': [],
|
||||
'cache_timestamp': datetime.now().isoformat()
|
||||
}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_ultimos_militantes(limite: int = 5) -> List[Militante]:
|
||||
"""Obtém os últimos militantes cadastrados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Militante).order_by(Militante.id.desc()).limit(limite).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_ultimos_pagamentos(limite: int = 5) -> List[Pagamento]:
|
||||
"""Obtém os últimos pagamentos realizados"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(Pagamento).join(Militante).order_by(Pagamento.data_pagamento.desc()).limit(limite).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_tipos_pagamento() -> List[TipoPagamento]:
|
||||
"""Obtém todos os tipos de pagamento"""
|
||||
db = get_db_session()
|
||||
try:
|
||||
return db.query(TipoPagamento).all()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def obter_dados_dashboard() -> Dict:
|
||||
"""Obtém todos os dados necessários para o dashboard"""
|
||||
return {
|
||||
'estatisticas': DashboardService.get_dashboard_stats(),
|
||||
'ultimos_militantes': DashboardService.obter_ultimos_militantes(),
|
||||
'ultimos_pagamentos': DashboardService.obter_ultimos_pagamentos(),
|
||||
'tipos_pagamento': DashboardService.obter_tipos_pagamento(),
|
||||
'data_atual': datetime.now().strftime("%d/%m/%Y")
|
||||
}
|
||||
35
services/database_service.py
Normal file
35
services/database_service.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from sqlalchemy import text
|
||||
from models.entities.base import engine, SessionLocal
|
||||
|
||||
class DatabaseService:
|
||||
"""Serviço para gerenciar conexões com o banco de dados"""
|
||||
|
||||
@staticmethod
|
||||
def get_db_connection():
|
||||
"""Retorna uma nova sessão do banco de dados"""
|
||||
db = SessionLocal()
|
||||
|
||||
try:
|
||||
# Configurar SQLite para melhor tratamento de concorrência
|
||||
db.execute(text("PRAGMA journal_mode=WAL"))
|
||||
db.execute(text("PRAGMA busy_timeout=5000"))
|
||||
return db
|
||||
except:
|
||||
db.close()
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def execute_query(query, params=None):
|
||||
"""
|
||||
Executa uma query usando SQLAlchemy
|
||||
"""
|
||||
session = DatabaseService.get_db_connection()
|
||||
try:
|
||||
result = session.execute(query, params)
|
||||
session.commit()
|
||||
return result
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
raise e
|
||||
finally:
|
||||
session.close()
|
||||
161
services/militante_service.py
Normal file
161
services/militante_service.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from sqlalchemy.orm import joinedload
|
||||
from datetime import datetime
|
||||
|
||||
from models.entities.militante import Militante
|
||||
from models.entities.email_militante import EmailMilitante
|
||||
from models.entities.endereco import Endereco
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
class MilitanteService:
|
||||
"""Serviço para operações com militantes"""
|
||||
|
||||
@staticmethod
|
||||
def listar_militantes():
|
||||
"""Lista todos os militantes"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
militantes = db.query(Militante)\
|
||||
.options(
|
||||
joinedload(Militante.celula),
|
||||
joinedload(Militante.emails)
|
||||
)\
|
||||
.order_by(Militante.nome)\
|
||||
.all()
|
||||
return militantes
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_militante(militante_id):
|
||||
"""Busca um militante pelo ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
militante = db.query(Militante)\
|
||||
.options(
|
||||
joinedload(Militante.celula),
|
||||
joinedload(Militante.emails),
|
||||
joinedload(Militante.endereco),
|
||||
joinedload(Militante.redes_sociais)
|
||||
)\
|
||||
.get(militante_id)
|
||||
return militante
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_por_cpf(cpf):
|
||||
"""Busca um militante pelo CPF"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
militante = db.query(Militante).filter(Militante.cpf == cpf).first()
|
||||
return militante
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def salvar_militante(militante):
|
||||
"""Salva um militante no banco de dados"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
if militante.id is None: # Novo militante
|
||||
db.add(militante)
|
||||
db.flush() # Para obter o ID gerado
|
||||
militante_id = militante.id
|
||||
else: # Militante existente
|
||||
db.merge(militante)
|
||||
militante_id = militante.id
|
||||
|
||||
db.commit()
|
||||
return militante_id
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao salvar militante: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def salvar_endereco(endereco):
|
||||
"""Salva um endereço no banco de dados"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
db.add(endereco)
|
||||
db.flush() # Para obter o ID gerado
|
||||
endereco_id = endereco.id
|
||||
db.commit()
|
||||
return endereco_id
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao salvar endereço: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def salvar_email_militante(email_militante):
|
||||
"""Salva um email de militante no banco de dados"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
db.add(email_militante)
|
||||
db.commit()
|
||||
return email_militante.id
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao salvar email: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def atualizar_email_militante(militante_id, email):
|
||||
"""Atualiza ou cria o email principal de um militante"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
# Verificar se já existe email
|
||||
email_existente = db.query(EmailMilitante)\
|
||||
.filter_by(militante_id=militante_id)\
|
||||
.first()
|
||||
|
||||
if email_existente:
|
||||
email_existente.endereco_email = email
|
||||
db.commit()
|
||||
else:
|
||||
novo_email = EmailMilitante(
|
||||
endereco_email=email,
|
||||
militante_id=militante_id
|
||||
)
|
||||
db.add(novo_email)
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao atualizar email: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def excluir_militante(militante_id):
|
||||
"""Exclui um militante pelo ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
militante = db.query(Militante).get(militante_id)
|
||||
if not militante:
|
||||
return False
|
||||
|
||||
# Excluir emails associados
|
||||
db.query(EmailMilitante)\
|
||||
.filter_by(militante_id=militante_id)\
|
||||
.delete()
|
||||
|
||||
# Excluir o militante
|
||||
db.delete(militante)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao excluir militante: {e}")
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
@@ -1,19 +0,0 @@
|
||||
import base64
|
||||
import pyotp
|
||||
import qrcode
|
||||
from io import BytesIO
|
||||
|
||||
def generate_qr_code(user):
|
||||
"""Gera imagem PIL do QR code OTP para o usuário."""
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(user.get_otp_uri())
|
||||
qr.make(fit=True)
|
||||
return qr.make_image(fill_color="black", back_color="white")
|
||||
|
||||
|
||||
def generate_qr_code_base64(user):
|
||||
"""Gera QR code OTP codificado em base64 (PNG)."""
|
||||
img = generate_qr_code(user)
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
return base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||
95
services/usuario_service.py
Normal file
95
services/usuario_service.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models.entities.usuario import Usuario
|
||||
from services.database_service import DatabaseService
|
||||
|
||||
class UsuarioService:
|
||||
"""Serviço para operações com usuários"""
|
||||
|
||||
@staticmethod
|
||||
def listar_usuarios():
|
||||
"""Lista todos os usuários"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
usuarios = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles),
|
||||
joinedload(Usuario.militante),
|
||||
joinedload(Usuario.setor),
|
||||
joinedload(Usuario.cr),
|
||||
joinedload(Usuario.celula)
|
||||
).all()
|
||||
return usuarios
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_usuario(user_id):
|
||||
"""Busca um usuário pelo ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
usuario = db.query(Usuario).options(
|
||||
joinedload(Usuario.roles),
|
||||
joinedload(Usuario.militante),
|
||||
joinedload(Usuario.setor),
|
||||
joinedload(Usuario.cr),
|
||||
joinedload(Usuario.celula)
|
||||
).get(user_id)
|
||||
return usuario
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_por_username(username):
|
||||
"""Busca um usuário pelo nome de usuário"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
usuario = db.query(Usuario).filter(Usuario.username == username).first()
|
||||
return usuario
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def buscar_por_email(email):
|
||||
"""Busca um usuário pelo email"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
usuario = db.query(Usuario).filter(Usuario.email == email).first()
|
||||
return usuario
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def salvar_usuario(usuario):
|
||||
"""Salva um usuário no banco de dados"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
if usuario.id is None: # Novo usuário
|
||||
db.add(usuario)
|
||||
else: # Usuário existente
|
||||
db.merge(usuario)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao salvar usuário: {e}")
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@staticmethod
|
||||
def excluir_usuario(user_id):
|
||||
"""Exclui um usuário pelo ID"""
|
||||
db = DatabaseService.get_db_connection()
|
||||
try:
|
||||
usuario = db.query(Usuario).get(user_id)
|
||||
if usuario:
|
||||
db.delete(usuario)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Erro ao excluir usuário: {e}")
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
@@ -2,15 +2,10 @@ import os
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Adiciona o diretório raiz ao PYTHONPATH
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
# Carregar .env antes de importar módulos
|
||||
load_dotenv(ROOT_DIR / ".env")
|
||||
root_dir = str(Path(__file__).parent.parent)
|
||||
sys.path.append(root_dir)
|
||||
|
||||
from functions.base import Base, engine
|
||||
from functions.database import init_database
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
from functions.database import get_db_session, Usuario
|
||||
from functions.database import get_db_connection, Usuario
|
||||
from functions.rbac import Role, Permission
|
||||
|
||||
def migrate_existing_users():
|
||||
"""Migra os usuários existentes para o novo sistema RBAC"""
|
||||
db = get_db_session()
|
||||
session = get_db_connection()
|
||||
|
||||
try:
|
||||
# Buscar todos os usuários
|
||||
usuarios = db.query(Usuario).all()
|
||||
usuarios = session.query(Usuario).all()
|
||||
|
||||
# Buscar ou criar role de administrador
|
||||
admin_role = db.query(Role).filter_by(nome="Administrador").first()
|
||||
admin_role = session.query(Role).filter_by(nome="Administrador").first()
|
||||
if not admin_role:
|
||||
admin_role = Role(nome="Administrador", nivel=Role.SECRETARIO_GERAL)
|
||||
db.add(admin_role)
|
||||
session.add(admin_role)
|
||||
|
||||
# Buscar ou criar role de militante básico
|
||||
militante_role = db.query(Role).filter_by(nome="Militante Básico").first()
|
||||
militante_role = session.query(Role).filter_by(nome="Militante Básico").first()
|
||||
if not militante_role:
|
||||
militante_role = Role(nome="Militante Básico", nivel=Role.MILITANTE_BASICO)
|
||||
db.add(militante_role)
|
||||
session.add(militante_role)
|
||||
|
||||
# Atualizar usuários
|
||||
for usuario in usuarios:
|
||||
@@ -33,15 +33,15 @@ def migrate_existing_users():
|
||||
else:
|
||||
usuario.roles.append(militante_role)
|
||||
|
||||
db.commit()
|
||||
session.commit()
|
||||
print("Migração de usuários concluída com sucesso!")
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
session.rollback()
|
||||
print(f"Erro durante a migração de usuários: {str(e)}")
|
||||
raise e
|
||||
finally:
|
||||
db.close()
|
||||
session.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
migrate_existing_users()
|
||||
51
static/js/comprovantes.js
Normal file
51
static/js/comprovantes.js
Normal file
@@ -0,0 +1,51 @@
|
||||
$(document).ready(function() {
|
||||
// Inicialização da tabela
|
||||
$('#tabelaComprovantes').DataTable({
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||
}
|
||||
});
|
||||
|
||||
// Modal de edição
|
||||
$('#modalEditarComprovante').on('show.bs.modal', function(event) {
|
||||
var button = $(event.relatedTarget);
|
||||
var comprovanteId = button.data('comprovante-id');
|
||||
var militanteId = button.data('militante-id');
|
||||
var militanteNome = button.data('militante-nome');
|
||||
var tipoComprovante = button.data('tipo-comprovante');
|
||||
var valor = button.data('valor');
|
||||
var dataComprovante = button.data('data-comprovante');
|
||||
|
||||
var modal = $(this);
|
||||
modal.find('#editMilitante').val(militanteId);
|
||||
modal.find('#editMilitanteNome').val(militanteNome);
|
||||
modal.find('#editTipoComprovante').val(tipoComprovante);
|
||||
modal.find('#editValor').val(valor);
|
||||
modal.find('#editDataComprovante').val(dataComprovante);
|
||||
|
||||
modal.find('form').attr('action', '/comprovantes/editar/' + comprovanteId);
|
||||
});
|
||||
|
||||
// Modal de exclusão
|
||||
$('#modalExcluirComprovante').on('show.bs.modal', function(event) {
|
||||
var button = $(event.relatedTarget);
|
||||
var comprovanteId = button.data('comprovante-id');
|
||||
var comprovanteInfo = button.data('comprovante-info');
|
||||
|
||||
var modal = $(this);
|
||||
modal.find('#comprovanteInfo').text(comprovanteInfo);
|
||||
modal.find('form').attr('action', '/comprovantes/excluir/' + comprovanteId);
|
||||
});
|
||||
|
||||
// Formatação de valores monetários
|
||||
$('.money').mask('000.000.000.000.000,00', {reverse: true});
|
||||
|
||||
// Validação de formulários
|
||||
$('form').on('submit', function(e) {
|
||||
if (!this.checkValidity()) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
$(this).addClass('was-validated');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Configurar clique nos itens da lista de pagamentos
|
||||
document.querySelectorAll('.list-group-item[onclick*="carregarDadosPagamento"]').forEach(item => {
|
||||
// Configurar clique nos itens da lista de comprovantes
|
||||
document.querySelectorAll('.list-group-item[onclick*="carregarDadosComprovante"]').forEach(item => {
|
||||
item.addEventListener('click', function(e) {
|
||||
const pagamentoId = this.getAttribute('data-pagamento-id');
|
||||
if (pagamentoId) {
|
||||
carregarDadosPagamento(pagamentoId);
|
||||
const comprovanteId = this.getAttribute('data-comprovante-id');
|
||||
if (comprovanteId) {
|
||||
carregarDadosComprovante(comprovanteId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Carregando script pagamentos.js...');
|
||||
|
||||
// Inicializar DataTable
|
||||
const table = $('#tabelaPagamentos').DataTable({
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||
},
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 3, // Coluna de data
|
||||
type: 'date-br',
|
||||
render: function(data, type, row) {
|
||||
if (type === 'sort') {
|
||||
return data.split('/').reverse().join('');
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{
|
||||
targets: 2, // Coluna de valor
|
||||
type: 'numeric',
|
||||
render: function(data, type, row) {
|
||||
if (type === 'sort') {
|
||||
return parseFloat(data.replace('R$ ', '').replace(',', '.'));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
{ targets: -1, orderable: false } // Coluna de ações
|
||||
],
|
||||
order: [[3, 'desc']] // Ordenar por data decrescente por padrão
|
||||
});
|
||||
|
||||
// Configuração do modal de edição
|
||||
const modalEditarPagamento = document.getElementById('modalEditarPagamento');
|
||||
if (modalEditarPagamento) {
|
||||
modalEditarPagamento.addEventListener('show.bs.modal', function(event) {
|
||||
console.log('Modal de edição sendo exibido');
|
||||
const button = event.relatedTarget;
|
||||
|
||||
if (!button) {
|
||||
console.error('Botão não encontrado!');
|
||||
return;
|
||||
}
|
||||
|
||||
const pagamentoId = button.getAttribute('data-pagamento-id');
|
||||
console.log('ID do pagamento:', pagamentoId);
|
||||
|
||||
// Dados do pagamento
|
||||
const dados = {
|
||||
militanteId: button.getAttribute('data-militante-id'),
|
||||
militanteNome: button.closest('tr').querySelector('td').textContent.trim(),
|
||||
tipoPagamento: button.getAttribute('data-tipo-pagamento'),
|
||||
valor: button.getAttribute('data-valor'),
|
||||
dataPagamento: button.getAttribute('data-data-pagamento')
|
||||
};
|
||||
console.log('Dados do pagamento:', dados);
|
||||
|
||||
// Preencher campos
|
||||
document.getElementById('editMilitante').value = dados.militanteId;
|
||||
document.getElementById('editMilitanteNome').value = dados.militanteNome;
|
||||
document.getElementById('editTipoPagamento').value = dados.tipoPagamento;
|
||||
document.getElementById('editValor').value = dados.valor;
|
||||
document.getElementById('editDataPagamento').value = dados.dataPagamento;
|
||||
|
||||
// Configurar formulário
|
||||
const form = document.getElementById('formEditarPagamento');
|
||||
if (form) {
|
||||
form.action = `/pagamentos/editar/${pagamentoId}`;
|
||||
console.log('Action do formulário:', form.action);
|
||||
|
||||
// Remover listeners antigos para evitar duplicação
|
||||
const newForm = form.cloneNode(true);
|
||||
form.parentNode.replaceChild(newForm, form);
|
||||
|
||||
// Adicionar listener para o submit do formulário
|
||||
newForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Formulário submetido');
|
||||
|
||||
// Criar FormData com os dados do formulário
|
||||
const formData = new FormData(this);
|
||||
|
||||
// Log dos dados sendo enviados
|
||||
console.log('Dados do formulário:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(key + ': ' + value);
|
||||
}
|
||||
|
||||
// Enviar requisição
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Status da resposta:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Resposta:', data);
|
||||
if (data.status === 'success') {
|
||||
// Fechar modal
|
||||
const modal = bootstrap.Modal.getInstance(modalEditarPagamento);
|
||||
modal.hide();
|
||||
|
||||
// Recarregar página
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Erro ao atualizar pagamento: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro:', error);
|
||||
alert('Erro ao atualizar pagamento. Por favor, tente novamente.');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Configuração do modal de exclusão
|
||||
const modalExcluirPagamento = document.getElementById('modalExcluirPagamento');
|
||||
if (modalExcluirPagamento) {
|
||||
modalExcluirPagamento.addEventListener('show.bs.modal', function(event) {
|
||||
console.log('Modal de exclusão sendo exibido');
|
||||
const button = event.relatedTarget;
|
||||
|
||||
if (!button) {
|
||||
console.error('Botão não encontrado!');
|
||||
return;
|
||||
}
|
||||
|
||||
const pagamentoId = button.getAttribute('data-pagamento-id');
|
||||
const pagamentoInfo = button.getAttribute('data-pagamento-info');
|
||||
console.log('ID do pagamento:', pagamentoId);
|
||||
|
||||
// Atualizar informações no modal
|
||||
document.getElementById('pagamentoInfo').textContent = pagamentoInfo;
|
||||
|
||||
// Configurar formulário
|
||||
const form = document.getElementById('formExcluirPagamento');
|
||||
if (form) {
|
||||
form.action = `/pagamentos/excluir/${pagamentoId}`;
|
||||
console.log('Action do formulário:', form.action);
|
||||
|
||||
// Remover listeners antigos para evitar duplicação
|
||||
const newForm = form.cloneNode(true);
|
||||
form.parentNode.replaceChild(newForm, form);
|
||||
|
||||
// Adicionar listener para o submit do formulário
|
||||
newForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Formulário submetido');
|
||||
|
||||
// Enviar requisição
|
||||
fetch(this.action, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Status da resposta:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Resposta:', data);
|
||||
if (data.status === 'success') {
|
||||
// Fechar modal
|
||||
const modal = bootstrap.Modal.getInstance(modalExcluirPagamento);
|
||||
modal.hide();
|
||||
|
||||
// Recarregar página
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Erro ao excluir pagamento: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro:', error);
|
||||
alert('Erro ao excluir pagamento. Por favor, tente novamente.');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Configuração do formulário de novo pagamento
|
||||
const formNovoPagamento = document.getElementById('formNovoPagamento');
|
||||
if (formNovoPagamento) {
|
||||
formNovoPagamento.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
console.log('Formulário de novo pagamento submetido');
|
||||
|
||||
// Criar FormData com os dados do formulário
|
||||
const formData = new FormData(this);
|
||||
|
||||
// Log dos dados sendo enviados
|
||||
console.log('Dados do formulário:');
|
||||
for (let [key, value] of formData.entries()) {
|
||||
console.log(key + ': ' + value);
|
||||
}
|
||||
|
||||
// Enviar requisição
|
||||
fetch(this.action, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
console.log('Status da resposta:', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Resposta:', data);
|
||||
if (data.status === 'success') {
|
||||
// Fechar modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('modalNovoPagamento'));
|
||||
modal.hide();
|
||||
|
||||
// Recarregar página
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Erro ao adicionar pagamento: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erro:', error);
|
||||
alert('Erro ao adicionar pagamento. Por favor, tente novamente.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Configuração do botão de exportar
|
||||
const btnExportar = document.getElementById('btnExportar');
|
||||
if (btnExportar) {
|
||||
btnExportar.addEventListener('click', function() {
|
||||
console.log('Exportando dados...');
|
||||
|
||||
// Coletar dados da tabela
|
||||
const dados = [];
|
||||
table.rows().every(function() {
|
||||
const row = this.data();
|
||||
dados.push({
|
||||
militante: row[0],
|
||||
tipo_pagamento: row[1],
|
||||
valor: row[2].replace('R$ ', ''),
|
||||
data_pagamento: row[3]
|
||||
});
|
||||
});
|
||||
|
||||
// Converter para CSV
|
||||
const csv = [
|
||||
['Militante', 'Tipo de Pagamento', 'Valor', 'Data do Pagamento'],
|
||||
...dados.map(row => [
|
||||
row.militante,
|
||||
row.tipo_pagamento,
|
||||
row.valor,
|
||||
row.data_pagamento
|
||||
])
|
||||
]
|
||||
.map(row => row.join(','))
|
||||
.join('\n');
|
||||
|
||||
// Criar blob e fazer download
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
if (link.download !== undefined) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', 'pagamentos.csv');
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Funções de validação e formatação de datas
|
||||
function validarData(data) {
|
||||
if (!data) return false;
|
||||
|
||||
const dataObj = new Date(data);
|
||||
if (isNaN(dataObj.getTime())) return false;
|
||||
|
||||
const hoje = new Date();
|
||||
hoje.setHours(0, 0, 0, 0);
|
||||
|
||||
return dataObj <= hoje;
|
||||
}
|
||||
|
||||
function formatarData(data) {
|
||||
if (!data) return '';
|
||||
|
||||
const dataObj = new Date(data);
|
||||
if (isNaN(dataObj.getTime())) return '';
|
||||
|
||||
return dataObj.toLocaleDateString('pt-BR');
|
||||
}
|
||||
|
||||
// Configurar campos de data
|
||||
const camposData = document.querySelectorAll('input[type="date"]');
|
||||
camposData.forEach(campo => {
|
||||
// Definir data máxima como hoje
|
||||
const hoje = new Date().toISOString().split('T')[0];
|
||||
campo.setAttribute('max', hoje);
|
||||
|
||||
campo.addEventListener('change', function() {
|
||||
if (!validarData(this.value)) {
|
||||
this.setCustomValidity('Data inválida ou futura');
|
||||
this.classList.add('is-invalid');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,7 +56,7 @@ function configurarOrdenacaoTabela(tabelaId) {
|
||||
if (column === 'data' ||
|
||||
column === 'data_vencimento' ||
|
||||
column === 'data_alteracao' ||
|
||||
column === 'data_pagamento' ||
|
||||
column === 'data_comprovante' ||
|
||||
column === 'data_venda' ||
|
||||
column === 'data_relatorio') {
|
||||
const aDate = converterDataParaComparacao(aValue);
|
||||
@@ -112,7 +112,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
'materiaisTable',
|
||||
'vendasTable',
|
||||
'cotasTable',
|
||||
'pagamentosTable'
|
||||
'comprovantesTable'
|
||||
];
|
||||
|
||||
tabelas.forEach(tabelaId => {
|
||||
@@ -198,3 +198,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function sortTable(table, column, type = 'text') {
|
||||
// ... existing code ...
|
||||
if (column === 'data_comprovante') {
|
||||
// ... existing code ...
|
||||
}
|
||||
// ... existing code ...
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB |
@@ -2,6 +2,7 @@
|
||||
|
||||
{% block title %}Dashboard Administrativo{% endblock %}
|
||||
|
||||
<<<<<<< HEAD
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.card {
|
||||
@@ -126,10 +127,21 @@
|
||||
<h2 class="display-4 mb-0">{{ total_users }}</h2>
|
||||
<i class="fas fa-users fa-3x opacity-50"></i>
|
||||
</div>
|
||||
=======
|
||||
{% block content %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total de Usuários</h5>
|
||||
<p class="card-text display-4">{{ total_users }}</p>
|
||||
<i class="fas fa-users fa-2x text-primary"></i>
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<<<<<<< HEAD
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-uppercase">Usuários Ativos</h5>
|
||||
@@ -137,10 +149,18 @@
|
||||
<h2 class="display-4 mb-0">{{ active_users }}</h2>
|
||||
<i class="fas fa-user-check fa-3x opacity-50"></i>
|
||||
</div>
|
||||
=======
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Usuários Ativos</h5>
|
||||
<p class="card-text display-4">{{ active_users }}</p>
|
||||
<i class="fas fa-user-check fa-2x text-success"></i>
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<<<<<<< HEAD
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-uppercase">Usuários Inativos</h5>
|
||||
@@ -148,11 +168,19 @@
|
||||
<h2 class="display-4 mb-0">{{ inactive_users }}</h2>
|
||||
<i class="fas fa-user-times fa-3x opacity-50"></i>
|
||||
</div>
|
||||
=======
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Usuários Inativos</h5>
|
||||
<p class="card-text display-4">{{ inactive_users }}</p>
|
||||
<i class="fas fa-user-times fa-2x text-danger"></i>
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<<<<<<< HEAD
|
||||
<div class="card lista-usuarios">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
@@ -185,16 +213,54 @@
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-warning btn-sm" title="Reset OTP" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
|
||||
<i class="fas fa-key"></i>
|
||||
=======
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Gerenciamento de Usuários</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="users-table" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Nome</th>
|
||||
<th>Status</th>
|
||||
<th>Último Login</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>
|
||||
<span class="badge {% if user.is_active %}bg-success{% else %}bg-danger{% endif %}">
|
||||
{{ "Ativo" if user.is_active else "Inativo" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user.last_login.strftime('%d/%m/%Y %H:%M') if user.last_login else 'Nunca' }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('admin.reset_user_otp', user_id=user.id) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-warning btn-sm" onclick="return confirm('Confirma o reset do OTP deste usuário?')">
|
||||
<i class="fas fa-key"></i> Reset OTP
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('admin.reset_user_password', user_id=user.id) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<<<<<<< HEAD
|
||||
<button type="submit" class="btn btn-info btn-sm" title="Reset Senha" onclick="return confirm('Confirma o reset da senha deste usuário?')">
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('admin.toggle_user_status', user_id=user.id) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-{{ 'danger' if user.is_active else 'success' }} btn-sm" title="{{ 'Desativar' if user.is_active else 'Ativar' }} Usuário">
|
||||
<i class="fas fa-{{ 'user-times' if user.is_active else 'user-check' }}"></i>
|
||||
</button>
|
||||
@@ -205,19 +271,58 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
=======
|
||||
<button type="submit" class="btn btn-info btn-sm" onclick="return confirm('Confirma o reset da senha deste usuário?')">
|
||||
<i class="fas fa-lock"></i> Reset Senha
|
||||
</button>
|
||||
</form>
|
||||
<button onclick="toggleUserStatus({{ user.id }})" class="btn btn-{% if user.is_active %}danger{% else %}success{% endif %} btn-sm">
|
||||
<i class="fas fa-{% if user.is_active %}user-times{% else %}user-check{% endif %}"></i>
|
||||
{{ "Desativar" if user.is_active else "Ativar" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
function toggleUserStatus(userId) {
|
||||
if (confirm('Deseja alterar o status deste usuário?')) {
|
||||
fetch(`/admin/users/${userId}/toggle-status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
$(document).ready(function() {
|
||||
$('#users-table').DataTable({
|
||||
language: {
|
||||
url: '//cdn.datatables.net/plug-ins/1.13.7/i18n/pt-BR.json'
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25
|
||||
=======
|
||||
order: [[1, 'asc']]
|
||||
>>>>>>> a22b0e4 (refactor(#11): Integra listagem de usuários no dashboard)
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() if csrf_token is defined else '' }}">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='img/favicon.ico') }}">
|
||||
<title>{% block title %}{% endblock %} - Controles OCI</title>
|
||||
|
||||
@@ -508,7 +508,7 @@
|
||||
{% block navbar %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('home.index') }}">
|
||||
<a class="navbar-brand" href="{{ url_for('home') }}">
|
||||
<img src="{{ url_for('static', filename='img/logo002-alpha.png') }}" alt="Logo OCI">
|
||||
Controles OCI
|
||||
</a>
|
||||
@@ -524,7 +524,7 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('militante.listar') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_militantes') }}">
|
||||
<i class="fas fa-list"></i>Listar Militantes
|
||||
</a>
|
||||
</li>
|
||||
@@ -536,12 +536,12 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('cota.listar') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_cotas') }}">
|
||||
<i class="fas fa-money-bill-wave"></i>Cotas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('pagamento.listar') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_pagamentos') }}">
|
||||
<i class="fas fa-receipt"></i>Pagamentos
|
||||
</a>
|
||||
</li>
|
||||
@@ -553,23 +553,18 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('material.listar') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_materiais') }}">
|
||||
<i class="fas fa-box"></i>Listar Materiais
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('material.listar_tipos') }}">
|
||||
<i class="fas fa-tags"></i>Tipos de Materiais
|
||||
<a class="dropdown-item" href="{{ url_for('listar_vendas_jornal') }}">
|
||||
<i class="fas fa-newspaper"></i>Vendas de Jornais
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('material.novo') }}">
|
||||
<i class="fas fa-plus"></i>Novo Material
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('material.novo_tipo') }}">
|
||||
<i class="fas fa-plus"></i>Novo Tipo
|
||||
<a class="dropdown-item" href="{{ url_for('listar_assinaturas') }}">
|
||||
<i class="fas fa-file-signature"></i>Assinaturas
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -580,12 +575,12 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('home.dashboard') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_relatorios_cotas') }}">
|
||||
<i class="fas fa-file-invoice-dollar"></i>Relatórios de Cotas
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('home.dashboard') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('listar_relatorios_vendas') }}">
|
||||
<i class="fas fa-file-alt"></i>Relatórios de Vendas
|
||||
</a>
|
||||
</li>
|
||||
@@ -598,9 +593,9 @@
|
||||
<i class="fas fa-user me-1"></i>{{ session.get('username', 'Usuário') }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% if is_admin %}
|
||||
{% if session.get('is_admin') %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('usuario.novo') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('novo_usuario') }}">
|
||||
<i class="fas fa-user-plus"></i>Novo Usuário
|
||||
</a>
|
||||
</li>
|
||||
@@ -612,7 +607,7 @@
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
|
||||
<a class="dropdown-item" href="{{ url_for('logout') }}">
|
||||
<i class="fas fa-sign-out-alt"></i>Sair
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
<!-- Componente para wrapping de elementos baseado em permissões -->
|
||||
<!-- Uso: {% include 'components/permission_wrapper.html' with context %} -->
|
||||
|
||||
<!-- Macro para verificar permissões e renderizar conteúdo condicionalmente -->
|
||||
{% macro render_if_permission(permission_name, content='', fallback='', show_fallback=false) %}
|
||||
{% if user_can(permission_name) %}
|
||||
{{ content | safe }}
|
||||
{% elif show_fallback %}
|
||||
{{ fallback | safe }}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para botões com permissão -->
|
||||
{% macro permission_button(permission_name, url, text, icon='', btn_class='btn-primary', title='') %}
|
||||
{% if user_can(permission_name) %}
|
||||
<a href="{{ url }}" class="btn {{ btn_class }}" title="{{ title }}">
|
||||
{% if icon %}<i class="{{ icon }} me-2"></i>{% endif %}{{ text }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para links de menu com permissão -->
|
||||
{% macro permission_menu_item(permission_name, url, text, icon='') %}
|
||||
{% if user_can(permission_name) %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url }}">
|
||||
{% if icon %}<i class="{{ icon }}"></i>{% endif %}{{ text }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para seções de dados com permissão -->
|
||||
{% macro permission_data_section(permission_name, data, template_content='', empty_message='Nenhum dado disponível') %}
|
||||
{% if user_can(permission_name) %}
|
||||
{% if data %}
|
||||
{{ template_content | safe }}
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>{{ empty_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-lock me-2"></i>Você não tem permissão para visualizar estes dados.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para tabelas com dados filtrados por permissão -->
|
||||
{% macro permission_table(permission_name, data, headers, row_template='', empty_message='Nenhum registro encontrado') %}
|
||||
{% if user_can(permission_name) %}
|
||||
{% if data %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
{% for header in headers %}
|
||||
<th>{{ header }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ row_template | safe }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center">
|
||||
<i class="fas fa-table me-2"></i>{{ empty_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning text-center">
|
||||
<i class="fas fa-lock me-2"></i>Você não tem permissão para visualizar estes dados.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para cards de estatísticas com permissão -->
|
||||
{% macro permission_stats_card(permission_name, title, value, icon, color='primary', url='#') %}
|
||||
{% if user_can(permission_name) %}
|
||||
<div class="col-md-3 mb-4">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-{{ color }} mb-3">
|
||||
<i class="{{ icon }} fa-3x"></i>
|
||||
</div>
|
||||
<h5 class="card-title text-muted">{{ title }}</h5>
|
||||
<h2 class="card-text text-{{ color }}">{{ value }}</h2>
|
||||
{% if url != '#' %}
|
||||
<a href="{{ url }}" class="btn btn-outline-{{ color }} btn-sm">
|
||||
Ver detalhes <i class="fas fa-arrow-right ms-1"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para formulários com permissão -->
|
||||
{% macro permission_form(permission_name, form_content='', action='', method='POST') %}
|
||||
{% if user_can(permission_name) %}
|
||||
<form action="{{ action }}" method="{{ method }}" class="needs-validation" novalidate>
|
||||
{{ form_content | safe }}
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-lock me-2"></i>Você não tem permissão para realizar esta ação.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<!-- Macro para modais com permissão -->
|
||||
{% macro permission_modal(permission_name, modal_id, title, content='', show_button=true, button_text='Abrir', button_class='btn-primary') %}
|
||||
{% if user_can(permission_name) %}
|
||||
{% if show_button %}
|
||||
<button type="button" class="btn {{ button_class }}" data-bs-toggle="modal" data-bs-target="#{{ modal_id }}">
|
||||
{{ button_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ title }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -17,7 +17,7 @@
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" class="needs-validation" novalidate>
|
||||
<!-- CSRF token removido temporariamente -->
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="nome" class="form-label">Nome</label>
|
||||
|
||||
@@ -6,7 +6,14 @@
|
||||
<div class="container mt-4">
|
||||
<h2 class="mb-4"><i class="fas fa-users-cog"></i> Administração de Usuários</h2>
|
||||
<div class="card">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<h3 class="mb-0"><i class="fas fa-users-cog"></i> Administração de Usuários</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="fas fa-info-circle"></i> Aqui você pode gerenciar todos os usuários do sistema. Use os controles abaixo para ativar/desativar contas ou alterar níveis de acesso.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-light">
|
||||
@@ -29,6 +36,7 @@
|
||||
<td>{{ usuario.last_login }}</td>
|
||||
<td>
|
||||
<span class="badge {% if usuario.ativo %}bg-success{% else %}bg-danger{% endif %}">
|
||||
<span class="badge {% if usuario.ativo %}badge-success{% else %}badge-danger{% endif %}">
|
||||
{{ "Ativo" if usuario.ativo else "Inativo" }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
43
templates/editar_comprovante.html
Normal file
43
templates/editar_comprovante.html
Normal file
@@ -0,0 +1,43 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar Comprovante{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-money-bill-wave me-2"></i>Editar Comprovante
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label for="tipo_comprovante_id">Tipo de Comprovante</label>
|
||||
<select class="form-control" id="tipo_comprovante_id" name="tipo_comprovante_id" required>
|
||||
<option value="1" {% if comprovante.tipo_comprovante_id == 1 %}selected{% endif %}>1 - Comprovante Padrão</option>
|
||||
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||
<option value="2" {% if comprovante.tipo_comprovante_id == 2 %}selected{% endif %}>2 - Comprovante Especial</option>
|
||||
<option value="3" {% if comprovante.tipo_comprovante_id == 3 %}selected{% endif %}>3 - Comprovante Extraordinário</option>
|
||||
<option value="4" {% if comprovante.tipo_comprovante_id == 4 %}selected{% endif %}>4 - Jornal Avulso</option>
|
||||
<option value="5" {% if comprovante.tipo_comprovante_id == 5 %}selected{% endif %}>5 - Assinatura de Jornal</option>
|
||||
<option value="6" {% if comprovante.tipo_comprovante_id == 6 %}selected{% endif %}>6 - Campanha Financeira</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="data_comprovante" class="form-label">Data do Comprovante:</label>
|
||||
<input type="date" class="form-control" id="data_comprovante" name="data_comprovante"
|
||||
required max="{{ hoje }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -23,7 +23,7 @@
|
||||
<label class="form-check-label" for="pago">Pago</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('cota.listar') }}" class="btn btn-secondary">Cancelar</a>
|
||||
<a href="{{ url_for('listar_cotas') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -64,7 +64,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-success">Salvar</button>
|
||||
<a href="{{ url_for('material.listar') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||
<a href="{{ url_for('listar_materiais') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% endwith %}
|
||||
|
||||
<form id="formEditarMilitante" method="POST" class="needs-validation" novalidate>
|
||||
<!-- CSRF token removido temporariamente -->
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="militante_id" value="{{ militante.id }}">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Editar Relatório de Pagamentos{% endblock %}
|
||||
{% block title %}Editar Relatório de Comprovantes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="mb-4">Editar Relatório de Pagamentos</h1>
|
||||
<h1 class="mb-4">Editar Relatório de Comprovantes</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@@ -44,10 +44,10 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="total_pagamentos" class="form-label">Total de Pagamentos</label>
|
||||
<input type="number" class="form-control" id="total_pagamentos" name="total_pagamentos" step="0.01" value="{{ relatorio.total_pagamentos }}" required>
|
||||
<label for="total_comprovantes" class="form-label">Total de Comprovantes</label>
|
||||
<input type="number" class="form-control" id="total_comprovantes" name="total_comprovantes" step="0.01" value="{{ relatorio.total_comprovantes }}" required>
|
||||
<div class="invalid-feedback">
|
||||
Por favor, insira o total de pagamentos.
|
||||
Por favor, insira o total de comprovantes.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-success">Salvar</button>
|
||||
<a href="{{ url_for('listar_relatorios_pagamentos') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||
<a href="{{ url_for('listar_relatorios_comprovantes') }}" class="btn btn-outline-secondary">Voltar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<button type="submit" class="btn btn-primary">Salvar</button>
|
||||
<a href="{{ url_for('material.listar_tipos') }}" class="btn btn-secondary">Cancelar</a>
|
||||
<a href="{{ url_for('listar_tipos_materiais') }}" class="btn btn-secondary">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="stats-card blue">
|
||||
<div class="title">Total de Militantes</div>
|
||||
<div class="value">{{ total_militantes }}</div>
|
||||
<a href="{{ url_for('militante.listar') }}" class="link">
|
||||
<a href="{{ url_for('listar_militantes') }}" class="link">
|
||||
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="icon">
|
||||
@@ -31,7 +31,7 @@
|
||||
<div class="stats-card green">
|
||||
<div class="title">Total de Cotas</div>
|
||||
<div class="value">R$ {{ total_cotas }}</div>
|
||||
<a href="{{ url_for('cota.listar') }}" class="link">
|
||||
<a href="{{ url_for('listar_cotas') }}" class="link">
|
||||
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="icon">
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="stats-card cyan">
|
||||
<div class="title">Materiais Vendidos</div>
|
||||
<div class="value">{{ total_materiais }}</div>
|
||||
<a href="{{ url_for('militante.listar') }}" class="link">
|
||||
<a href="{{ url_for('listar_materiais') }}" class="link">
|
||||
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="icon">
|
||||
@@ -57,7 +57,7 @@
|
||||
<div class="stats-card yellow">
|
||||
<div class="title">Assinaturas Ativas</div>
|
||||
<div class="value">{{ total_assinaturas }}</div>
|
||||
<a href="{{ url_for('militante.listar') }}" class="link">
|
||||
<a href="{{ url_for('listar_assinaturas') }}" class="link">
|
||||
Ver detalhes <i class="fas fa-arrow-right"></i>
|
||||
</a>
|
||||
<div class="icon">
|
||||
@@ -115,7 +115,7 @@
|
||||
<div class="list-group-item" style="cursor: pointer" onclick="carregarDadosPagamento({{ pagamento.id }})">
|
||||
<div class="militante-info">
|
||||
<h6 class="mb-1">{{ pagamento.militante.nome }}</h6>
|
||||
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') if pagamento.data_pagamento else '' }}</small>
|
||||
<small>{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</small>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-success">R$ {{ "%.2f"|format(pagamento.valor) }}</span>
|
||||
|
||||
49
templates/lista_comprovantes.html
Normal file
49
templates/lista_comprovantes.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}Lista de Comprovantes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-list me-2"></i>Lista de Comprovantes
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Data</th>
|
||||
<th>Valor</th>
|
||||
<th>Tipo</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for comprovante in comprovantes %}
|
||||
<tr>
|
||||
<td>{{ comprovante.id }}</td>
|
||||
<td>{{ comprovante.data.strftime('%d/%m/%Y') }}</td>
|
||||
<td>R$ {{ "%.2f"|format(comprovante.valor) }}</td>
|
||||
<td>
|
||||
{% if comprovante.tipo_comprovante_id == 1 %}
|
||||
1 - Comprovante Padrão
|
||||
{% elif current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||
{% if comprovante.tipo_comprovante_id == 2 %}
|
||||
2 - Comprovante Especial
|
||||
<td>{{ comprovante.tipo }}</td>
|
||||
<td>{{ comprovante.data }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,14 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Pagamentos{% endblock %}
|
||||
{% block title %}Comprovantes{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid mt-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2><i class="fas fa-money-bill-wave"></i> Pagamentos</h2>
|
||||
<h2><i class="fas fa-money-bill-wave"></i> Comprovantes</h2>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoPagamento">
|
||||
<i class="fas fa-plus"></i> Novo Pagamento
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalNovoComprovante">
|
||||
<i class="fas fa-plus"></i> Novo Comprovante
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnExportar">
|
||||
<i class="fas fa-file-export"></i> Exportar
|
||||
@@ -19,56 +19,62 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover" id="tabelaPagamentos">
|
||||
<table class="table table-striped table-hover" id="tabelaComprovantes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Militante</th>
|
||||
<th>Tipo de Pagamento</th>
|
||||
<th>Tipo de Comprovante</th>
|
||||
<th>Valor</th>
|
||||
<th>Data do Pagamento</th>
|
||||
<th>Data do Comprovante</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pagamento in pagamentos %}
|
||||
{% for comprovante in comprovantes %}
|
||||
<tr>
|
||||
<td data-militante="{{ pagamento.militante_id }}">{{ pagamento.militante.nome if pagamento.militante else 'N/A' }}</td>
|
||||
<td data-tipo="{{ pagamento.tipo_pagamento }}">
|
||||
{% if pagamento.tipo_pagamento == 1 %}
|
||||
Mensalidade
|
||||
{% elif pagamento.tipo_pagamento == 2 %}
|
||||
<td data-militante="{{ comprovante.militante_id }}">{{ comprovante.militante.nome if comprovante.militante else 'N/A' }}</td>
|
||||
<td data-tipo="{{ comprovante.tipo_comprovante }}">
|
||||
{% if comprovante.tipo_comprovante == 1 %}
|
||||
Cota
|
||||
{% elif comprovante.tipo_comprovante == 2 %}
|
||||
Contribuição Extra
|
||||
{% elif pagamento.tipo_pagamento == 3 %}
|
||||
{% elif comprovante.tipo_comprovante == 3 %}
|
||||
Doação
|
||||
{% elif pagamento.tipo_pagamento == 4 %}
|
||||
{% elif comprovante.tipo_comprovante == 4 %}
|
||||
Taxa de Evento
|
||||
{% elif pagamento.tipo_pagamento == 5 %}
|
||||
{% elif comprovante.tipo_comprovante == 5 %}
|
||||
Jornal Avulso
|
||||
{% elif comprovante.tipo_comprovante == 6 %}
|
||||
Assinatura de Jornal
|
||||
{% elif comprovante.tipo_comprovante == 7 %}
|
||||
Campanha Financeira
|
||||
{% elif comprovante.tipo_comprovante == 8 %}
|
||||
Outros
|
||||
{% else %}
|
||||
Não Definido
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-valor="{{ pagamento.valor }}">R$ {{ "%.2f"|format(pagamento.valor) }}</td>
|
||||
<td data-data="{{ pagamento.data_pagamento }}">{{ pagamento.data_pagamento.strftime('%d/%m/%Y') }}</td>
|
||||
<td data-valor="{{ comprovante.valor }}">R$ {{ "%.2f"|format(comprovante.valor) }}</td>
|
||||
<td data-data="{{ comprovante.data_comprovante }}">{{ comprovante.data_comprovante.strftime('%d/%m/%Y') }}</td>
|
||||
<td>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalEditarPagamento"
|
||||
data-pagamento-id="{{ pagamento.id }}"
|
||||
data-militante-id="{{ pagamento.militante_id }}"
|
||||
data-tipo-pagamento="{{ pagamento.tipo_pagamento }}"
|
||||
data-valor="{{ pagamento.valor }}"
|
||||
data-data-pagamento="{{ pagamento.data_pagamento.strftime('%Y-%m-%d') }}"
|
||||
data-bs-target="#modalEditarComprovante"
|
||||
data-comprovante-id="{{ comprovante.id }}"
|
||||
data-militante-id="{{ comprovante.militante_id }}"
|
||||
data-tipo-comprovante="{{ comprovante.tipo_comprovante }}"
|
||||
data-valor="{{ comprovante.valor }}"
|
||||
data-data-comprovante="{{ comprovante.data_comprovante.strftime('%Y-%m-%d') }}"
|
||||
title="Editar">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalExcluirPagamento"
|
||||
data-pagamento-id="{{ pagamento.id }}"
|
||||
data-pagamento-info="Pagamento de {{ pagamento.militante.nome if pagamento.militante else 'N/A' }} - R$ {{ "%.2f"|format(pagamento.valor) }}"
|
||||
data-bs-target="#modalExcluirComprovante"
|
||||
data-comprovante-id="{{ comprovante.id }}"
|
||||
data-comprovante-info="Comprovante de {{ comprovante.militante.nome if comprovante.militante else 'N/A' }} - R$ {{ "%.2f"|format(comprovante.valor) }}"
|
||||
title="Excluir">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
@@ -82,16 +88,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Novo Pagamento -->
|
||||
<div class="modal fade" id="modalNovoPagamento" tabindex="-1">
|
||||
<!-- Modal Novo Comprovante -->
|
||||
<div class="modal fade" id="modalNovoComprovante" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Pagamento</h5>
|
||||
<h5 class="modal-title"><i class="fas fa-plus"></i> Novo Comprovante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formNovoPagamento" method="post" action="{{ url_for('pagamento.novo') }}">
|
||||
<form id="formNovoComprovante" method="post" action="{{ url_for('adicionar_comprovante') }}">
|
||||
<div class="mb-3">
|
||||
<label for="militante" class="form-label">Militante:</label>
|
||||
<select class="form-select" id="militante" name="militante_id" required>
|
||||
@@ -102,14 +108,19 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="tipoPagamento" class="form-label">Tipo de Pagamento:</label>
|
||||
<select class="form-select" id="tipoPagamento" name="tipo_pagamento" required>
|
||||
<label for="tipoComprovante" class="form-label">Tipo de Comprovante:</label>
|
||||
<select class="form-select" id="tipoComprovante" name="tipo_comprovante" required>
|
||||
<option value="">Selecione o tipo</option>
|
||||
<option value="1">Mensalidade</option>
|
||||
<option value="1">Cota</option>
|
||||
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||
<option value="2">Contribuição Extra</option>
|
||||
<option value="3">Doação</option>
|
||||
<option value="4">Taxa de Evento</option>
|
||||
<option value="5">Outros</option>
|
||||
<option value="5">Jornal Avulso</option>
|
||||
<option value="6">Assinatura de Jornal</option>
|
||||
<option value="7">Campanha Financeira</option>
|
||||
<option value="8">Outros</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -117,8 +128,8 @@
|
||||
<input type="number" step="0.01" class="form-control" id="valor" name="valor" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dataPagamento" class="form-label">Data do Pagamento:</label>
|
||||
<input type="date" class="form-control" id="dataPagamento" name="data_pagamento" required>
|
||||
<label for="dataComprovante" class="form-label">Data do Comprovante:</label>
|
||||
<input type="date" class="form-control" id="dataComprovante" name="data_comprovante" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
@@ -130,30 +141,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Editar Pagamento -->
|
||||
<div class="modal fade" id="modalEditarPagamento" tabindex="-1">
|
||||
<!-- Modal Editar Comprovante -->
|
||||
<div class="modal fade" id="modalEditarComprovante" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Pagamento</h5>
|
||||
<h5 class="modal-title"><i class="fas fa-edit"></i> Editar Comprovante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formEditarPagamento" method="post">
|
||||
<form id="formEditarComprovante" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="editMilitante" class="form-label">Militante:</label>
|
||||
<input type="text" class="form-control bg-light" id="editMilitanteNome" readonly>
|
||||
<input type="hidden" id="editMilitante" name="militante_id">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editTipoPagamento" class="form-label">Tipo de Pagamento:</label>
|
||||
<select class="form-select" id="editTipoPagamento" name="tipo_pagamento" required>
|
||||
<label for="editTipoComprovante" class="form-label">Tipo de Comprovante:</label>
|
||||
<select class="form-select" id="editTipoComprovante" name="tipo_comprovante" required>
|
||||
<option value="">Selecione o tipo</option>
|
||||
<option value="1">Mensalidade</option>
|
||||
<option value="1">Cota</option>
|
||||
{% if current_user.has_permission('gerenciar_tipos_comprovante') %}
|
||||
<option value="2">Contribuição Extra</option>
|
||||
<option value="3">Doação</option>
|
||||
<option value="4">Taxa de Evento</option>
|
||||
<option value="5">Outros</option>
|
||||
<option value="5">Jornal Avulso</option>
|
||||
<option value="6">Assinatura de Jornal</option>
|
||||
<option value="7">Campanha Financeira</option>
|
||||
<option value="8">Outros</option>
|
||||
{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@@ -161,8 +177,8 @@
|
||||
<input type="number" step="0.01" class="form-control" id="editValor" name="valor" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editDataPagamento" class="form-label">Data do Pagamento:</label>
|
||||
<input type="date" class="form-control" id="editDataPagamento" name="data_pagamento" required>
|
||||
<label for="editDataComprovante" class="form-label">Data do Comprovante:</label>
|
||||
<input type="date" class="form-control" id="editDataComprovante" name="data_comprovante" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
@@ -174,20 +190,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Excluir Pagamento -->
|
||||
<div class="modal fade" id="modalExcluirPagamento" tabindex="-1">
|
||||
<!-- Modal Excluir Comprovante -->
|
||||
<div class="modal fade" id="modalExcluirComprovante" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Pagamento</h5>
|
||||
<h5 class="modal-title"><i class="fas fa-trash"></i> Excluir Comprovante</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Tem certeza que deseja excluir este pagamento?</p>
|
||||
<p id="pagamentoInfo" class="text-muted"></p>
|
||||
<p>Tem certeza que deseja excluir este comprovante?</p>
|
||||
<p id="comprovanteInfo" class="text-muted"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<form id="formExcluirPagamento" method="post">
|
||||
<form id="formExcluirComprovante" method="post">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="submit" class="btn btn-danger">Excluir</button>
|
||||
</form>
|
||||
@@ -198,6 +214,6 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/pagamentos.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/comprovantes.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formNovaCota" method="post" action="{{ url_for('cota.novo') }}">
|
||||
<form id="formNovaCota" method="post" action="{{ url_for('nova_cota') }}">
|
||||
<div class="mb-3">
|
||||
<label for="militante_id" class="form-label">Militante:</label>
|
||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="formNovoMaterial" method="post" action="{{ url_for('material.novo') }}">
|
||||
<form id="formNovoMaterial" method="post" action="{{ url_for('novo_material') }}">
|
||||
<div class="mb-3">
|
||||
<label for="militante_id" class="form-label">Militante:</label>
|
||||
<select class="form-select" id="militante_id" name="militante_id" required>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user