From 62aaec3fbef3a00c90ee49fce00a35bfdefe3f73 Mon Sep 17 00:00:00 2001 From: LS Date: Tue, 22 Apr 2025 16:35:08 -0300 Subject: [PATCH] refactor: Implementa arquitetura MVC limpa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 184 +++++++++---------- app.py.new | 123 +++++++++++++ controllers/auth_controller.py | 124 +++++++++++++ controllers/home_controller.py | 80 +++++++++ controllers/militante_controller.py | 270 ++++++++++++++++++++++++++++ controllers/usuario_controller.py | 202 +++++++++++++++++++++ models/entities/base.py | 17 ++ models/entities/email_militante.py | 14 ++ models/entities/endereco.py | 19 ++ models/entities/militante.py | 153 ++++++++++++++++ models/entities/rede_social.py | 15 ++ models/entities/usuario.py | 126 +++++++++++++ routes/auth.py | 61 +++++++ routes/cota.py | 123 +++++++++++++ routes/main.py | 41 +++++ routes/militante.py | 50 ++++++ routes/pagamento.py | 116 ++++++++++++ routes/relatorio.py | 149 +++++++++++++++ scripts/prepare_mvc.sh | 26 +++ services/database_service.py | 35 ++++ services/militante_service.py | 161 +++++++++++++++++ services/usuario_service.py | 95 ++++++++++ 22 files changed, 2083 insertions(+), 101 deletions(-) create mode 100644 app.py.new create mode 100644 controllers/auth_controller.py create mode 100644 controllers/home_controller.py create mode 100644 controllers/militante_controller.py create mode 100644 controllers/usuario_controller.py create mode 100644 models/entities/base.py create mode 100644 models/entities/email_militante.py create mode 100644 models/entities/endereco.py create mode 100644 models/entities/militante.py create mode 100644 models/entities/rede_social.py create mode 100644 models/entities/usuario.py create mode 100644 routes/auth.py create mode 100644 routes/cota.py create mode 100644 routes/main.py create mode 100644 routes/militante.py create mode 100644 routes/pagamento.py create mode 100644 routes/relatorio.py create mode 100644 scripts/prepare_mvc.sh create mode 100644 services/database_service.py create mode 100644 services/militante_service.py create mode 100644 services/usuario_service.py diff --git a/README.md b/README.md index 11713c4..cd45cd7 100644 --- a/README.md +++ b/README.md @@ -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 +5. Execute a aplicação: + ``` + python app.py + ``` -### Decoradores de Permissão +## Credenciais padrão -O sistema fornece três decoradores para controle de acesso: +- **Administrador**: + - Usuário: admin + - Senha: admin123 -1. `@require_permission(permission_name)` - - Verifica se o usuário tem uma permissão específica - - Exemplo: `@require_permission('create_cell_member')` +## Desenvolvimento -2. `@require_role(role_name)` - - Verifica se o usuário tem um papel específico - - Exemplo: `@require_role('Secretário de Célula')` +Para adicionar novos recursos, siga a arquitetura MVC: -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)` +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/` -### Verificando Permissões no Código +## Testes -```python -# Verificar se um usuário tem uma permissão -if user.has_permission('create_cell_member'): - # Faça algo +Execute os testes usando pytest: -# 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 +``` +python -m pytest ``` -## Estrutura do Banco de Dados +Ou use o script de teste: -O sistema utiliza as seguintes tabelas para o RBAC: - -- `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 - -## 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 +``` \ No newline at end of file diff --git a/app.py.new b/app.py.new new file mode 100644 index 0000000..2f7852e --- /dev/null +++ b/app.py.new @@ -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' + ) \ No newline at end of file diff --git a/controllers/auth_controller.py b/controllers/auth_controller.py new file mode 100644 index 0000000..1cca5da --- /dev/null +++ b/controllers/auth_controller.py @@ -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 \ No newline at end of file diff --git a/controllers/home_controller.py b/controllers/home_controller.py new file mode 100644 index 0000000..956c910 --- /dev/null +++ b/controllers/home_controller.py @@ -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() \ No newline at end of file diff --git a/controllers/militante_controller.py b/controllers/militante_controller.py new file mode 100644 index 0000000..c312d1c --- /dev/null +++ b/controllers/militante_controller.py @@ -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 + }) \ No newline at end of file diff --git a/controllers/usuario_controller.py b/controllers/usuario_controller.py new file mode 100644 index 0000000..ad16d26 --- /dev/null +++ b/controllers/usuario_controller.py @@ -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'} diff --git a/models/entities/base.py b/models/entities/base.py new file mode 100644 index 0000000..3a0b9ed --- /dev/null +++ b/models/entities/base.py @@ -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() diff --git a/models/entities/email_militante.py b/models/entities/email_militante.py new file mode 100644 index 0000000..03b1ff0 --- /dev/null +++ b/models/entities/email_militante.py @@ -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") diff --git a/models/entities/endereco.py b/models/entities/endereco.py new file mode 100644 index 0000000..b8d0e85 --- /dev/null +++ b/models/entities/endereco.py @@ -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") diff --git a/models/entities/militante.py b/models/entities/militante.py new file mode 100644 index 0000000..a53064f --- /dev/null +++ b/models/entities/militante.py @@ -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() \ No newline at end of file diff --git a/models/entities/rede_social.py b/models/entities/rede_social.py new file mode 100644 index 0000000..6c2380c --- /dev/null +++ b/models/entities/rede_social.py @@ -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") diff --git a/models/entities/usuario.py b/models/entities/usuario.py new file mode 100644 index 0000000..a033879 --- /dev/null +++ b/models/entities/usuario.py @@ -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) diff --git a/routes/auth.py b/routes/auth.py new file mode 100644 index 0000000..a2c28d0 --- /dev/null +++ b/routes/auth.py @@ -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/") +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()) \ No newline at end of file diff --git a/routes/cota.py b/routes/cota.py new file mode 100644 index 0000000..a1da0a4 --- /dev/null +++ b/routes/cota.py @@ -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/', 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/', 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')) diff --git a/routes/main.py b/routes/main.py new file mode 100644 index 0000000..1800a94 --- /dev/null +++ b/routes/main.py @@ -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/") +@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] + }) \ No newline at end of file diff --git a/routes/militante.py b/routes/militante.py new file mode 100644 index 0000000..30f4faf --- /dev/null +++ b/routes/militante.py @@ -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/", 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/", 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/") +@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) \ No newline at end of file diff --git a/routes/pagamento.py b/routes/pagamento.py new file mode 100644 index 0000000..9a2c784 --- /dev/null +++ b/routes/pagamento.py @@ -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/', 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/', 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')) diff --git a/routes/relatorio.py b/routes/relatorio.py new file mode 100644 index 0000000..5663957 --- /dev/null +++ b/routes/relatorio.py @@ -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/', 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/', 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')) \ No newline at end of file diff --git a/scripts/prepare_mvc.sh b/scripts/prepare_mvc.sh new file mode 100644 index 0000000..c4f2bf4 --- /dev/null +++ b/scripts/prepare_mvc.sh @@ -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" \ No newline at end of file diff --git a/services/database_service.py b/services/database_service.py new file mode 100644 index 0000000..17bcb10 --- /dev/null +++ b/services/database_service.py @@ -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() \ No newline at end of file diff --git a/services/militante_service.py b/services/militante_service.py new file mode 100644 index 0000000..b05cfdb --- /dev/null +++ b/services/militante_service.py @@ -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() diff --git a/services/usuario_service.py b/services/usuario_service.py new file mode 100644 index 0000000..32bf428 --- /dev/null +++ b/services/usuario_service.py @@ -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() \ No newline at end of file