Files
controles/services/cache_service.py
LS 7399e0000e feat: Implementar arquitetura de permissões no nível de dados
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
2025-07-01 13:42:56 -03:00

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)