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:
17
models/entities/base.py
Normal file
17
models/entities/base.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric, Date, Enum, create_engine, text
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Configurar caminho do banco de dados
|
||||
db_dir = Path.home() / '.local' / 'share' / 'controles'
|
||||
db_dir.mkdir(parents=True, exist_ok=True)
|
||||
db_path = db_dir / 'database.db'
|
||||
|
||||
DATABASE_URL = f"sqlite:///{db_path}"
|
||||
engine = create_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
# Base SQLAlchemy
|
||||
Base = declarative_base()
|
||||
14
models/entities/email_militante.py
Normal file
14
models/entities/email_militante.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class EmailMilitante(Base):
|
||||
__tablename__ = 'emails_militantes'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
endereco_email = Column(String(100))
|
||||
|
||||
# Relacionamentos
|
||||
militante = relationship("Militante", back_populates="emails")
|
||||
19
models/entities/endereco.py
Normal file
19
models/entities/endereco.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class Endereco(Base):
|
||||
__tablename__ = 'enderecos'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
estado = Column(String(2))
|
||||
cidade = Column(String(50))
|
||||
bairro = Column(String(50))
|
||||
rua = Column(String(100))
|
||||
numero = Column(String(10))
|
||||
complemento = Column(String(50))
|
||||
cep = Column(String(9))
|
||||
|
||||
# Relacionamentos
|
||||
militantes = relationship("Militante", back_populates="endereco")
|
||||
153
models/entities/militante.py
Normal file
153
models/entities/militante.py
Normal 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()
|
||||
15
models/entities/rede_social.py
Normal file
15
models/entities/rede_social.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from models.entities.base import Base
|
||||
|
||||
class RedeSocial(Base):
|
||||
__tablename__ = 'redes_sociais'
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
tipo = Column(String(20)) # Instagram, TikTok, Discord, etc.
|
||||
identificador = Column(String(100))
|
||||
|
||||
# Relacionamentos
|
||||
militante = relationship("Militante", back_populates="redes_sociais")
|
||||
126
models/entities/usuario.py
Normal file
126
models/entities/usuario.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime, timedelta
|
||||
from flask_login import UserMixin
|
||||
import pyotp
|
||||
|
||||
from models.entities.base import Base
|
||||
from models.entities.militante import Militante
|
||||
|
||||
class TipoUsuario(enum.Enum):
|
||||
ADMIN = "admin"
|
||||
CR_RESPONSAVEL = "cr_responsavel"
|
||||
SETOR_RESPONSAVEL = "setor_responsavel"
|
||||
USUARIO = "usuario"
|
||||
|
||||
class Usuario(Base, UserMixin):
|
||||
__tablename__ = 'usuarios'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
username = Column(String(50), unique=True, nullable=False)
|
||||
password_hash = Column(String(255), nullable=False)
|
||||
email = Column(String(100), unique=True, nullable=False)
|
||||
nome = Column(String(100)) # Nome completo do usuário
|
||||
otp_secret = Column(String(32))
|
||||
role_id = Column(Integer, ForeignKey('roles.id'))
|
||||
setor_id = Column(Integer, ForeignKey('setores.id'))
|
||||
ativo = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
ultimo_login = Column(DateTime)
|
||||
ultimo_logout = Column(DateTime)
|
||||
motivo_logout = Column(String(100))
|
||||
cr_id = Column(Integer, ForeignKey('comites_regionais.id'))
|
||||
celula_id = Column(Integer, ForeignKey('celulas.id'))
|
||||
session_timeout = Column(Integer, default=30)
|
||||
tipo = Column(String(17), nullable=False)
|
||||
ultima_atividade = Column(DateTime, default=datetime.utcnow)
|
||||
# Relacionamento com militante
|
||||
militante_id = Column(Integer, ForeignKey('militantes.id'))
|
||||
|
||||
# Relacionamentos
|
||||
roles = relationship("Role", secondary="user_roles", back_populates="users")
|
||||
setor = relationship('Setor', back_populates='usuarios')
|
||||
cr = relationship('ComiteRegional', back_populates='usuarios')
|
||||
celula = relationship('Celula', back_populates='usuarios')
|
||||
militante = relationship("Militante", backref=backref("usuario", uselist=False))
|
||||
|
||||
def __init__(self, username, email=None, is_admin=False, nome=None):
|
||||
self.username = username
|
||||
self.email = email
|
||||
self.is_admin = is_admin
|
||||
self.nome = nome
|
||||
self.ativo = True
|
||||
self.session_timeout = 30
|
||||
self.tipo = "USUARIO"
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def set_password(self, password):
|
||||
self.password_hash = generate_password_hash(password)
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def update_last_activity(self):
|
||||
self.ultima_atividade = datetime.utcnow()
|
||||
|
||||
def is_session_expired(self):
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def check_session_timeout(self):
|
||||
"""Verifica se a sessão do usuário expirou"""
|
||||
if not self.ultima_atividade:
|
||||
return True
|
||||
time_diff = datetime.utcnow() - self.ultima_atividade
|
||||
return time_diff.total_seconds() > (self.session_timeout * 60)
|
||||
|
||||
def has_permission(self, permission_name):
|
||||
"""Verifica se o usuário tem uma permissão específica"""
|
||||
if self.is_admin: # Se for admin, tem todas as permissões
|
||||
return True
|
||||
|
||||
# Verifica se o usuário tem a permissão através de suas roles
|
||||
for role in self.roles:
|
||||
for permission in role.permissions:
|
||||
if permission.nome == permission_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_role(self, role_nivel):
|
||||
"""Verifica se o usuário tem um determinado nível de role"""
|
||||
for role in self.roles:
|
||||
if role.nivel == role_nivel:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_otp_uri(self):
|
||||
"""Gera a URI para autenticação em duas etapas"""
|
||||
if not self.otp_secret:
|
||||
self.otp_secret = pyotp.random_base32()
|
||||
return pyotp.totp.TOTP(self.otp_secret).provisioning_uri(
|
||||
self.username,
|
||||
issuer_name="Sistema de Controles"
|
||||
)
|
||||
|
||||
def verify_otp(self, code):
|
||||
"""Verifica se um código OTP é válido"""
|
||||
if not self.otp_secret:
|
||||
print(f"Erro: OTP secret não configurado para o usuário {self.username}")
|
||||
return False
|
||||
|
||||
totp = pyotp.totp.TOTP(self.otp_secret)
|
||||
is_valid = totp.verify(code)
|
||||
return is_valid
|
||||
|
||||
def logout(self):
|
||||
"""Registra o logout do usuário"""
|
||||
self.ultimo_logout = datetime.utcnow()
|
||||
self.motivo_logout = "Logout manual"
|
||||
self.ultima_atividade = None
|
||||
|
||||
def is_admin_user(self):
|
||||
"""Verifica se o usuário é admin"""
|
||||
return self.is_admin or any(role.nome == "admin" for role in self.roles)
|
||||
Reference in New Issue
Block a user