BREAKING CHANGES: - Sistema de permissões movido do nível de template para nível de dados - Menus sempre visíveis, controle transparente no backend - Templates nunca quebram, sempre renderizam com dados filtrados Features: - ✅ Arquitetura MVC completa implementada - ✅ Controllers com filtragem hierárquica de dados - ✅ Template helpers simplificados (user_can sempre True) - ✅ Controle de acesso baseado na hierarquia organizacional - ✅ Regra especial para tesoureiros (acesso completo) - ✅ Tratamento robusto de erros em todos os controllers Controllers implementados: - militante_controller.py - Filtragem por célula/setor/CR/CC - cota_controller.py - Controle baseado em permissões - material_controller.py - Acesso flexível por nível - pagamento_controller.py - Filtragem organizacional - auth_controller.py - Autenticação com OTP - home_controller.py - Dashboard com estatísticas - usuario_controller.py - Gestão de usuários Templates corrigidos: - listar_cotas.html - URLs corrigidas (nova_cota → cota.nova) - listar_tipos_materiais.html - Variáveis ajustadas (tipos → tipos_materiais) - base.html - Menus sempre visíveis - Diversos templates com correções de URLs e referências Services implementados: - auth_service.py - Lógica de autenticação - dashboard_service.py - Estatísticas do dashboard - cache_service.py - Integração com Redis - celula_service.py - Operações de células Models implementados: - militante_model.py - Operações de militantes - pagamento_model.py - Operações de pagamentos Documentação: - docs/permission_fixes_summary.md - Resumo completo das correções - docs/architecture_summary.md - Arquitetura MVC - docs/mvc_refactoring.md - Detalhes da refatoração - docs/permission_strategy.md - Estratégia de permissões - docs/redis_cache_setup.md - Setup do cache Redis - README.md atualizado com nova arquitetura Testes: - test_menu_navigation.py - Testes unitários de navegação - test_integration_menu.py - Testes de integração com Selenium Status dos testes: ✅ Funcionais: /, /dashboard, /pagamentos, /materiais ❌ Com problemas: /militantes, /cotas, /tipos-materiais, /admin/dashboard Hierarquia de permissões implementada: Admin → Acesso total CC → Acesso total CR → Dados do CR Setor → Dados do setor Célula → Dados da célula Próximos passos identificados: - Corrigir referências a Militante indefinido nos templates - Resolver problemas de campos inexistentes - Corrigir roteamento admin
180 lines
5.3 KiB
Python
180 lines
5.3 KiB
Python
from datetime import datetime, date
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def validar_data(data_input):
|
|
"""
|
|
Valida se uma data é válida e não é futura
|
|
"""
|
|
if not data_input:
|
|
return False
|
|
|
|
try:
|
|
if isinstance(data_input, str):
|
|
data_obj = datetime.strptime(data_input, '%Y-%m-%d').date()
|
|
elif isinstance(data_input, datetime):
|
|
data_obj = data_input.date()
|
|
elif isinstance(data_input, date):
|
|
data_obj = data_input
|
|
else:
|
|
return False
|
|
|
|
# Verificar se não é futura
|
|
return data_obj <= date.today()
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
def converter_data(data_input):
|
|
"""
|
|
Converte string de data para objeto datetime
|
|
"""
|
|
if not data_input:
|
|
return None
|
|
|
|
try:
|
|
if isinstance(data_input, str):
|
|
return datetime.strptime(data_input, '%Y-%m-%d').date()
|
|
elif isinstance(data_input, datetime):
|
|
return data_input.date()
|
|
elif isinstance(data_input, date):
|
|
return data_input
|
|
else:
|
|
return None
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def formatar_data(data_obj):
|
|
"""
|
|
Formata data para exibição
|
|
"""
|
|
if not data_obj:
|
|
return ""
|
|
|
|
try:
|
|
if isinstance(data_obj, str):
|
|
data_obj = datetime.strptime(data_obj, '%Y-%m-%d').date()
|
|
elif isinstance(data_obj, datetime):
|
|
data_obj = data_obj.date()
|
|
|
|
return data_obj.strftime('%d/%m/%Y')
|
|
except (ValueError, TypeError):
|
|
return ""
|
|
|
|
def validar_sequencia_datas(data_nascimento: date = None,
|
|
data_entrada: date = None,
|
|
data_efetivacao: date = None) -> None:
|
|
"""
|
|
Valida a sequência lógica entre datas.
|
|
|
|
Args:
|
|
data_nascimento: Data de nascimento
|
|
data_entrada: Data de entrada na OCI
|
|
data_efetivacao: Data de efetivação na OCI
|
|
|
|
Raises:
|
|
ValueError: Se houver inconsistência entre as datas
|
|
"""
|
|
hoje = date.today()
|
|
|
|
# Validar datas futuras
|
|
for nome, data in [
|
|
("Data de nascimento", data_nascimento),
|
|
("Data de entrada", data_entrada),
|
|
("Data de efetivação", data_efetivacao)
|
|
]:
|
|
if data and data > hoje:
|
|
logger.warning(f"{nome} no futuro: {data}")
|
|
raise ValueError(f"{nome} não pode ser no futuro")
|
|
|
|
# Validar sequência
|
|
if data_nascimento and data_entrada and data_nascimento > data_entrada:
|
|
logger.warning(f"Data de entrada ({data_entrada}) anterior à data de nascimento ({data_nascimento})")
|
|
raise ValueError("Data de entrada na OCI não pode ser anterior à data de nascimento")
|
|
|
|
if data_entrada and data_efetivacao and data_entrada > data_efetivacao:
|
|
logger.warning(f"Data de efetivação ({data_efetivacao}) anterior à data de entrada ({data_entrada})")
|
|
raise ValueError("Data de efetivação não pode ser anterior à data de entrada")
|
|
|
|
def calcular_idade(data_nascimento: date) -> int:
|
|
"""
|
|
Calcula a idade com base na data de nascimento.
|
|
|
|
Args:
|
|
data_nascimento: Data de nascimento
|
|
|
|
Returns:
|
|
int: Idade em anos
|
|
"""
|
|
if not data_nascimento:
|
|
return None
|
|
|
|
hoje = date.today()
|
|
idade = hoje.year - data_nascimento.year
|
|
|
|
# Ajustar se ainda não fez aniversário este ano
|
|
if hoje.month < data_nascimento.month or \
|
|
(hoje.month == data_nascimento.month and hoje.day < data_nascimento.day):
|
|
idade -= 1
|
|
|
|
return idade
|
|
|
|
def converter_data_br(data_str):
|
|
"""Converte string de data no formato DD/MM/YYYY para objeto date"""
|
|
if not data_str:
|
|
return None
|
|
try:
|
|
dia, mes, ano = map(int, data_str.split('/'))
|
|
return date(ano, mes, dia)
|
|
except (ValueError, TypeError) as e:
|
|
return None
|
|
|
|
def converter_data_iso(data_str):
|
|
"""Converte string de data no formato YYYY-MM-DD para objeto date"""
|
|
if not data_str:
|
|
return None
|
|
try:
|
|
return datetime.strptime(data_str, '%Y-%m-%d').date()
|
|
except (ValueError, TypeError) as e:
|
|
return None
|
|
|
|
def formatar_data_br(data):
|
|
"""Formata objeto date para string no formato DD/MM/YYYY"""
|
|
if not data:
|
|
return ''
|
|
if isinstance(data, str):
|
|
data = converter_data_iso(data) or converter_data_br(data)
|
|
if not data:
|
|
return ''
|
|
return data.strftime('%d/%m/%Y')
|
|
|
|
def formatar_data_iso(data):
|
|
"""Formata objeto date para string no formato YYYY-MM-DD"""
|
|
if not data:
|
|
return ''
|
|
if isinstance(data, str):
|
|
data = converter_data_br(data) or converter_data_iso(data)
|
|
if not data:
|
|
return ''
|
|
return data.strftime('%Y-%m-%d')
|
|
|
|
def validar_data(data, data_maxima=None, data_minima=None):
|
|
"""Valida se a data está dentro do intervalo permitido"""
|
|
if not data:
|
|
return True
|
|
|
|
if isinstance(data, str):
|
|
data = converter_data_br(data) or converter_data_iso(data)
|
|
if not data:
|
|
return False
|
|
|
|
hoje = date.today()
|
|
|
|
if data_maxima and data > data_maxima:
|
|
return False
|
|
if data_minima and data < data_minima:
|
|
return False
|
|
if data > hoje:
|
|
return False
|
|
|
|
return True |