refactor: Implementa arquitetura MVC limpa

- Separa modelos em entidades individuais
- Cria camada de serviços para acesso a dados
- Implementa controladores para lógica de negócio
- Organiza rotas em blueprints por funcionalidade
- Adiciona documentação de arquitetura no README
- Cria script para preparação da estrutura

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
LS
2025-04-22 16:35:08 -03:00
parent e6057cd566
commit 62aaec3fbe
22 changed files with 2083 additions and 101 deletions

194
README.md
View File

@@ -1,134 +1,116 @@
# Sistema de Controle de Militantes
# Sistema de Controles
Sistema para gerenciamento de militantes, células, setores e comitês regionais.
Sistema de gestão para controle de militantes, pagamentos, cotas e relatórios.
## Estrutura de Permissões (RBAC)
## Arquitetura MVC
O sistema utiliza um sistema de controle de acesso baseado em papéis (RBAC) com a seguinte hierarquia:
O projeto segue a arquitetura Model-View-Controller (MVC) para separação de responsabilidades:
### Níveis de Papéis
### Models
1. **Militante Básico** (Nível 1)
- Visualizar próprios dados
- Editar próprios dados
- Visualizar dados da célula
Os modelos representam as entidades do sistema e estão organizados em:
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
- **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.
3. **Membro de Setor** (Nível 3)
- Todas as permissões do Secretário de Célula
- Visualizar relatórios do setor
### Controllers
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
Os controladores contêm a lógica de negócio e manipulam os dados dos modelos:
5. **Membro de CR** (Nível 5)
- Todas as permissões do Secretário de Setor
- Visualizar relatórios do CR
- **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.
6. **Secretário de CR** (Nível 6)
- Todas as permissões do Membro de CR
- Gerenciar setores do CR
- Criar setores no CR
### Views
7. **Membro do CC** (Nível 7)
- Todas as permissões do Secretário de CR
- Visualizar relatórios nacionais
As views são os templates que exibem os dados para o usuário:
8. **Secretário Geral** (Nível 8)
- Todas as permissões do Membro do CC
- Gerenciar CRs
- Criar CRs
- Configurar sistema
- **templates/**: Templates Jinja2
- Organizados por funcionalidade (admin, militantes, cotas, etc.)
### Services
Camada adicional para encapsular a lógica de acesso a dados:
- **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.
### Routes
Rotas da aplicação organizadas em blueprints:
- **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.
## Instalação
1. Clone o repositório
2. Crie um ambiente virtual:
```bash
python -m venv venv
source venv/bin/activate # Linux/Mac
# ou
venv\Scripts\activate # Windows
1. Clone o repositório:
```
git clone [URL_DO_REPOSITORIO]
```
2. Crie e ative um ambiente virtual:
```
python -m venv myenv
source myenv/bin/activate # Linux/Mac
myenv\Scripts\activate # Windows
```
3. Instale as dependências:
```bash
```
pip install -r requirements.txt
```
4. Execute as migrações do banco de dados:
```bash
python sql/migrate_db.py
4. Inicialize o banco de dados:
```
5. Configure as variáveis de ambiente no arquivo `.env`:
```
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=sua_chave_secreta
MAIL_SERVER=seu_servidor_smtp
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USERNAME=seu_email
MAIL_PASSWORD=sua_senha
```
6. Execute o aplicativo:
```bash
flask run
python app.py --init
```
## Uso
### 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 no Código
```python
# Verificar se um usuário tem uma permissão
if user.has_permission('create_cell_member'):
# Faça algo
# Verificar se um usuário tem um papel
if user.has_role('Secretário de Célula'):
# Faça algo
# Obter o papel mais alto do usuário
highest_role = user.get_highest_role()
if highest_role and highest_role.nivel >= Role.SECRETARIO_CR:
# Faça algo
5. Execute a aplicação:
```
python app.py
```
## Estrutura do Banco de Dados
## Credenciais padrão
O sistema utiliza as seguintes tabelas para o RBAC:
- **Administrador**:
- Usuário: admin
- Senha: admin123
- `roles`: Armazena os papéis disponíveis
- `permissions`: Armazena as permissões disponíveis
- `role_permissions`: Mapeia papéis para permissões
- `user_roles`: Mapeia usuários para papéis
## Desenvolvimento
## Segurança
Para adicionar novos recursos, siga a arquitetura MVC:
- 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
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/`
## Testes
Execute os testes usando pytest:
```
python -m pytest
```
Ou use o script de teste:
```
./run_tests.sh
```

123
app.py.new Normal file
View 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'
)

View File

@@ -0,0 +1,124 @@
from flask import session, flash, redirect, url_for, request
from flask_login import login_user, logout_user, current_user
from datetime import datetime
import pyotp
import qrcode
from io import BytesIO
import base64
from models.entities.usuario import Usuario
from services.database_service import DatabaseService
class AuthController:
"""Controlador para funções de autenticação"""
@staticmethod
def login():
"""Processa o login de usuário"""
if request.method != "POST":
return False
email_or_username = request.form.get("email")
password = request.form.get("password")
otp = request.form.get("otp")
if not all([email_or_username, password]):
flash("Email/usuário e senha são obrigatórios.", "danger")
return False
db = DatabaseService.get_db_connection()
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):
flash("Email/usuário ou senha incorretos.", "danger")
return False
# Verificar OTP se o usuário tiver configurado
if user.otp_secret and not otp:
flash("Código OTP é obrigatório para sua conta.", "danger")
return False
if user.otp_secret and not user.verify_otp(otp):
flash("Código OTP inválido.", "danger")
return False
# Atualizar último login
user.ultimo_login = datetime.utcnow()
db.commit()
# Fazer login e setar sessão
login_user(user)
session['user_id'] = user.id
session['username'] = user.username
session['is_admin'] = user.is_admin
return True
finally:
db.close()
@staticmethod
def logout():
"""Processa o logout de usuário"""
db = DatabaseService.get_db_connection()
try:
user = current_user
if user.is_authenticated:
user.logout()
db.commit()
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 False
if nova_senha != confirmar_senha:
flash("As senhas não coincidem.", "error")
return False
db = DatabaseService.get_db_connection()
try:
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 False
user.set_password(nova_senha)
db.commit()
flash("Senha alterada com sucesso!", "success")
return True
finally:
db.close()
@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()
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)
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

View File

@@ -0,0 +1,80 @@
from flask import session, render_template
from datetime import datetime
from sqlalchemy.sql import func
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
class HomeController:
"""Controlador para página inicial e dashboard"""
@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()

View File

@@ -0,0 +1,270 @@
from flask import request, jsonify, flash
from datetime import datetime
from werkzeug.exceptions import NotFound
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
class MilitanteController:
"""Controlador para operações com militantes"""
@staticmethod
def listar_militantes():
"""Lista todos os militantes"""
return MilitanteService.listar_militantes()
@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 já existe militante com este CPF
if MilitanteService.buscar_por_cpf(cpf):
return jsonify({
'status': 'error',
'message': 'CPF já cadastrado'
}), 400
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')
)
# Salvar endereço para obter ID
endereco_id = MilitanteService.salvar_endereco(endereco)
# 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
# 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,
# Contato
telefone1=form_data.get('telefone1'),
telefone2=form_data.get('telefone2'),
endereco_id=endereco_id,
# Profissional
profissao=form_data.get('profissao'),
regime_trabalho=form_data.get('regime_trabalho'),
empresa=form_data.get('empresa'),
contratante=form_data.get('contratante'),
# Acadêmico
instituicao_ensino=form_data.get('instituicao_ensino'),
tipo_instituicao=form_data.get('tipo_instituicao'),
# 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',
# 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),
# Por padrão, todo novo militante é aspirante
aspirante=True,
data_inicio_aspirante=datetime.now()
)
# Salvar militante para obter ID
militante_id = MilitanteService.salvar_militante(militante)
# 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 jsonify({
'status': 'success',
'message': 'Militante criado com sucesso!',
'id': militante_id
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'Erro ao criar militante: {str(e)}'
}), 500
@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
# 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')
# 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
# Validar sequência lógica das datas
validar_sequencia_datas(
data_nascimento=data_nascimento,
data_entrada=data_entrada_oci,
data_efetivacao=data_efetivacao_oci
)
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
# 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
# Calcular idade
if data_nascimento:
militante.idade = calcular_idade(data_nascimento)
# 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',
'message': 'Militante não encontrado'
}), 404
# Função auxiliar para formatar data com validação
def formatar_data_segura(data):
try:
if not data:
return None
return data.strftime('%Y-%m-%d')
except Exception as e:
print(f"Erro ao formatar data: {str(e)}, valor: {data}")
return None
# 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': 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,
'celula_id': militante.celula_id,
'responsabilidades_valor': militante.responsabilidades,
'sindicato': militante.sindicato,
'cargo_sindical': militante.cargo_sindical,
'central_sindical': militante.central_sindical,
'dirigente_sindical': militante.dirigente_sindical
})

View File

@@ -0,0 +1,202 @@
from flask import request, jsonify, flash, session
from flask_login import current_user
from datetime import datetime
import secrets
import pyotp
from models.entities.usuario import Usuario
from services.usuario_service import UsuarioService
from services.database_service import DatabaseService
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
if UsuarioService.buscar_por_username(data['username']):
flash('Nome de usuário já existe.', 'danger')
return False
try:
# Criar usuário
usuario = Usuario(
username=data['username'],
email=data['email'],
nome=data.get('nome'),
is_admin=data.get('is_admin', False)
)
# 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:
flash(f'Erro ao cadastrar usuário: {str(e)}', 'danger')
return False
@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
# Atualizar campos
if 'email' in data:
usuario.email = data['email']
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,
'error': 'Usuário não encontrado.'
}), 404
usuario.ativo = not usuario.ativo
if UsuarioService.salvar_usuario(usuario):
return jsonify({
'success': True,
'message': f'Usuário {\'ativado\' if usuario.ativo else \'desativado\'}'
})
else:
return jsonify({
'success': False,
'error': 'Erro ao salvar alterações.'
}), 500
@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
# 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'}

17
models/entities/base.py Normal file
View 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()

View 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")

View 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")

View File

@@ -0,0 +1,153 @@
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 = relationship("VendaJornalAvulso", back_populates="militante")
assinaturas = relationship("AssinaturaAnual", back_populates="militante")
celula = relationship("Celula", back_populates="militantes", foreign_keys=[celula_id])
# 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()

View 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")

126
models/entities/usuario.py Normal file
View 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)

61
routes/auth.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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'))

26
scripts/prepare_mvc.sh Normal file
View File

@@ -0,0 +1,26 @@
#!/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
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"

View 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()

View 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()

View 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()