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
268 lines
8.5 KiB
Python
268 lines
8.5 KiB
Python
import redis
|
|
import json
|
|
import pickle
|
|
from typing import Any, Optional, Union, Dict, List
|
|
from datetime import timedelta
|
|
import os
|
|
import logging
|
|
from functools import wraps
|
|
import hashlib
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class CacheService:
|
|
"""Service for Redis caching operations"""
|
|
|
|
def __init__(self, redis_url: str = None):
|
|
"""Initialize Redis connection"""
|
|
self.redis_url = redis_url or os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
|
self.redis = None
|
|
self._connect()
|
|
|
|
def _connect(self):
|
|
"""Establish Redis connection"""
|
|
try:
|
|
self.redis = redis.from_url(self.redis_url, decode_responses=False)
|
|
# Test connection
|
|
self.redis.ping()
|
|
logger.info("Redis connection established successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to connect to Redis: {e}")
|
|
self.redis = None
|
|
|
|
def _is_connected(self) -> bool:
|
|
"""Check if Redis is connected"""
|
|
if not self.redis:
|
|
return False
|
|
try:
|
|
self.redis.ping()
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get value from cache"""
|
|
if not self._is_connected():
|
|
return default
|
|
|
|
try:
|
|
value = self.redis.get(key)
|
|
if value is None:
|
|
return default
|
|
return pickle.loads(value)
|
|
except Exception as e:
|
|
logger.error(f"Error getting cache key {key}: {e}")
|
|
return default
|
|
|
|
def set(self, key: str, value: Any, expire: int = 3600) -> bool:
|
|
"""Set value in cache with expiration"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
serialized_value = pickle.dumps(value)
|
|
return self.redis.setex(key, expire, serialized_value)
|
|
except Exception as e:
|
|
logger.error(f"Error setting cache key {key}: {e}")
|
|
return False
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""Delete key from cache"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.redis.delete(key))
|
|
except Exception as e:
|
|
logger.error(f"Error deleting cache key {key}: {e}")
|
|
return False
|
|
|
|
def exists(self, key: str) -> bool:
|
|
"""Check if key exists in cache"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.redis.exists(key))
|
|
except Exception as e:
|
|
logger.error(f"Error checking cache key {key}: {e}")
|
|
return False
|
|
|
|
def expire(self, key: str, seconds: int) -> bool:
|
|
"""Set expiration for key"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
return bool(self.redis.expire(key, seconds))
|
|
except Exception as e:
|
|
logger.error(f"Error setting expiration for cache key {key}: {e}")
|
|
return False
|
|
|
|
def ttl(self, key: str) -> int:
|
|
"""Get time to live for key"""
|
|
if not self._is_connected():
|
|
return -1
|
|
|
|
try:
|
|
return self.redis.ttl(key)
|
|
except Exception as e:
|
|
logger.error(f"Error getting TTL for cache key {key}: {e}")
|
|
return -1
|
|
|
|
def clear_pattern(self, pattern: str) -> int:
|
|
"""Clear all keys matching pattern"""
|
|
if not self._is_connected():
|
|
return 0
|
|
|
|
try:
|
|
keys = self.redis.keys(pattern)
|
|
if keys:
|
|
return self.redis.delete(*keys)
|
|
return 0
|
|
except Exception as e:
|
|
logger.error(f"Error clearing cache pattern {pattern}: {e}")
|
|
return 0
|
|
|
|
def clear_all(self) -> bool:
|
|
"""Clear all cache"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
self.redis.flushdb()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error clearing all cache: {e}")
|
|
return False
|
|
|
|
def get_many(self, keys: List[str]) -> Dict[str, Any]:
|
|
"""Get multiple values from cache"""
|
|
if not self._is_connected():
|
|
return {}
|
|
|
|
try:
|
|
values = self.redis.mget(keys)
|
|
result = {}
|
|
for key, value in zip(keys, values):
|
|
if value is not None:
|
|
result[key] = pickle.loads(value)
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error getting multiple cache keys: {e}")
|
|
return {}
|
|
|
|
def set_many(self, data: Dict[str, Any], expire: int = 3600) -> bool:
|
|
"""Set multiple values in cache"""
|
|
if not self._is_connected():
|
|
return False
|
|
|
|
try:
|
|
pipeline = self.redis.pipeline()
|
|
for key, value in data.items():
|
|
serialized_value = pickle.dumps(value)
|
|
pipeline.setex(key, expire, serialized_value)
|
|
pipeline.execute()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error setting multiple cache keys: {e}")
|
|
return False
|
|
|
|
def increment(self, key: str, amount: int = 1) -> Optional[int]:
|
|
"""Increment counter in cache"""
|
|
if not self._is_connected():
|
|
return None
|
|
|
|
try:
|
|
return self.redis.incr(key, amount)
|
|
except Exception as e:
|
|
logger.error(f"Error incrementing cache key {key}: {e}")
|
|
return None
|
|
|
|
def decrement(self, key: str, amount: int = 1) -> Optional[int]:
|
|
"""Decrement counter in cache"""
|
|
if not self._is_connected():
|
|
return None
|
|
|
|
try:
|
|
return self.redis.decr(key, amount)
|
|
except Exception as e:
|
|
logger.error(f"Error decrementing cache key {key}: {e}")
|
|
return None
|
|
|
|
# Global cache instance
|
|
cache_service = CacheService()
|
|
|
|
def cache_key_generator(*args, **kwargs) -> str:
|
|
"""Generate cache key from function arguments"""
|
|
# Create a hash of the arguments
|
|
key_data = str(args) + str(sorted(kwargs.items()))
|
|
return hashlib.md5(key_data.encode()).hexdigest()
|
|
|
|
def cached(expire: int = 3600, key_prefix: str = ""):
|
|
"""Decorator for caching function results"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
# Generate cache key
|
|
func_key = f"{key_prefix}:{func.__name__}:{cache_key_generator(*args, **kwargs)}"
|
|
|
|
# Try to get from cache
|
|
cached_result = cache_service.get(func_key)
|
|
if cached_result is not None:
|
|
logger.debug(f"Cache hit for {func_key}")
|
|
return cached_result
|
|
|
|
# Execute function and cache result
|
|
result = func(*args, **kwargs)
|
|
cache_service.set(func_key, result, expire)
|
|
logger.debug(f"Cache miss for {func_key}, stored result")
|
|
|
|
return result
|
|
return wrapper
|
|
return decorator
|
|
|
|
def invalidate_cache_pattern(pattern: str):
|
|
"""Decorator to invalidate cache after function execution"""
|
|
def decorator(func):
|
|
@wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
result = func(*args, **kwargs)
|
|
cache_service.clear_pattern(pattern)
|
|
logger.debug(f"Invalidated cache pattern: {pattern}")
|
|
return result
|
|
return wrapper
|
|
return decorator
|
|
|
|
# Cache key constants
|
|
class CacheKeys:
|
|
"""Constants for cache keys"""
|
|
MILITANTE_LIST = "militantes:list"
|
|
MILITANTE_DETAIL = "militante:detail:{}"
|
|
PAGAMENTO_LIST = "pagamentos:list"
|
|
PAGAMENTO_DETAIL = "pagamento:detail:{}"
|
|
COTA_LIST = "cotas:list"
|
|
COTA_DETAIL = "cota:detail:{}"
|
|
DASHBOARD_STATS = "dashboard:stats"
|
|
USER_SESSION = "user:session:{}"
|
|
API_RESPONSE = "api:response:{}"
|
|
|
|
@staticmethod
|
|
def militante_detail(militante_id: int) -> str:
|
|
return CacheKeys.MILITANTE_DETAIL.format(militante_id)
|
|
|
|
@staticmethod
|
|
def pagamento_detail(pagamento_id: int) -> str:
|
|
return CacheKeys.PAGAMENTO_DETAIL.format(pagamento_id)
|
|
|
|
@staticmethod
|
|
def cota_detail(cota_id: int) -> str:
|
|
return CacheKeys.COTA_DETAIL.format(cota_id)
|
|
|
|
@staticmethod
|
|
def user_session(user_id: int) -> str:
|
|
return CacheKeys.USER_SESSION.format(user_id)
|
|
|
|
@staticmethod
|
|
def api_response(endpoint: str) -> str:
|
|
return CacheKeys.API_RESPONSE.format(endpoint) |