""" Django integration for caching strategies. Provides middleware, decorators, and template tags for easy caching. """ import json import logging from typing import Any, Dict, List, Optional, Union from django.core.cache import cache from django.http import HttpRequest, HttpResponse from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.middleware import AuthenticationMiddleware from django.utils.deprecation import MiddlewareMixin from django.template import Library from django.template.loader import render_to_string from django.db import connection from rest_framework.response import Response from rest_framework.decorators import api_view from .cache_manager import CacheManager, MalaysianDataCache, QueryCache from .strategies import ( WriteThroughCache, WriteBehindCache, ReadThroughCache, RefreshAheadCache, CacheAsidePattern, MultiLevelCache, MalaysianCacheStrategies, cache_view_response, cache_query_results ) from .config import CacheConfig logger = logging.getLogger(__name__) User = get_user_model() register = Library() class TenantCacheMiddleware(MiddlewareMixin): """Middleware for tenant-aware caching.""" def __init__(self, get_response): self.get_response = get_response self.cache_manager = CacheManager() self.malaysian_cache = MalaysianDataCache(self.cache_manager) def process_request(self, request: HttpRequest) -> Optional[HttpResponse]: """Process request with tenant-aware caching.""" # Add cache manager to request request.cache_manager = self.cache_manager request.malaysian_cache = self.malaysian_cache # Cache tenant-specific data if hasattr(request, 'tenant') and request.tenant: tenant_key = f"tenant_data_{request.tenant.id}" request.tenant_cache = self.cache_manager.get(tenant_key, {}) else: request.tenant_cache = {} return None def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: """Process response with caching.""" # Add cache headers response['X-Cache-Tenant'] = getattr(request, 'tenant', {}).get('schema_name', 'public') response['X-Cache-Status'] = 'MISS' # Will be updated by cache middleware return response class CacheMiddleware(MiddlewareMixin): """Advanced caching middleware.""" def __init__(self, get_response): self.get_response = get_response self.cache_manager = CacheManager() self.cache_aside = CacheAsidePattern(self.cache_manager) # Define cacheable paths and conditions self.cacheable_paths = getattr(settings, 'CACHEABLE_PATHS', [ '/api/products/', '/api/categories/', '/api/static-data/', ]) self.non_cacheable_paths = getattr(settings, 'NON_CACHEABLE_PATHS', [ '/api/auth/', '/api/admin/', '/api/cart/', '/api/orders/', ]) def process_request(self, request: HttpRequest) -> Optional[HttpResponse]: """Process request with caching.""" if self._should_bypass_cache(request): return None cache_key = self._generate_cache_key(request) cached_response = self.cache_manager.get(cache_key) if cached_response: response = HttpResponse(cached_response['content']) response['X-Cache-Status'] = 'HIT' response['Content-Type'] = cached_response.get('content_type', 'application/json') return response return None def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: """Process response with caching.""" if self._should_bypass_cache(request) or getattr(response, '_cache_exempt', False): response['X-Cache-Status'] = 'BYPASS' return response if self._should_cache_response(request, response): cache_key = self._generate_cache_key(request) cache_data = { 'content': response.content, 'content_type': response.get('Content-Type', 'application/json'), 'status_code': response.status_code, } timeout = self._get_cache_timeout(request) self.cache_manager.set(cache_key, cache_data, timeout) response['X-Cache-Status'] = 'MISS' return response def _should_bypass_cache(self, request: HttpRequest) -> bool: """Check if request should bypass cache.""" # Never cache authenticated user requests by default if request.user.is_authenticated: if getattr(settings, 'CACHE_AUTHENTICATED_REQUESTS', False): return False return True # Check method if request.method not in ['GET', 'HEAD']: return True # Check paths for path in self.non_cacheable_paths: if request.path.startswith(path): return True return False def _should_cache_response(self, request: HttpRequest, response: HttpResponse) -> bool: """Check if response should be cached.""" if response.status_code != 200: return False # Check cacheable paths for path in self.cacheable_paths: if request.path.startswith(path): return True return False def _generate_cache_key(self, request: HttpRequest) -> str: """Generate cache key for request.""" key_parts = [ request.path, request.method, ] if request.GET: key_parts.append(str(sorted(request.GET.items()))) # Add user info if authenticated if request.user.is_authenticated: key_parts.append(f"user_{request.user.id}") # Add tenant info if hasattr(request, 'tenant'): key_parts.append(f"tenant_{request.tenant.id}") key = "|".join(key_parts) return f"view_cache_{hash(key)}" def _get_cache_timeout(self, request: HttpRequest) -> int: """Get cache timeout for request.""" # Different timeouts for different paths if request.path.startswith('/api/static-data/'): return 3600 # 1 hour for static data elif request.path.startswith('/api/products/'): return 1800 # 30 minutes for products else: return 300 # 5 minutes default class DatabaseCacheMiddleware(MiddlewareMixin): """Middleware for database query caching.""" def __init__(self, get_response): self.get_response = get_response self.cache_manager = CacheManager() self.query_cache = QueryCache(self.cache_manager) self.queries_executed = [] self.cache_hits = 0 def process_request(self, request: HttpRequest) -> Optional[HttpResponse]: """Initialize query tracking.""" self.queries_executed = [] self.cache_hits = 0 # Add query cache to request request.query_cache = self.query_cache return None def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse: """Add query cache statistics to response.""" response['X-Cache-Queries'] = str(len(self.queries_executed)) response['X-Cache-Query-Hits'] = str(self.cache_hits) return response class MalaysianCacheMiddleware(MiddlewareMixin): """Middleware for Malaysian-specific caching.""" def __init__(self, get_response): self.get_response = get_response self.cache_manager = CacheManager() self.malaysian_cache = MalaysianDataCache(self.cache_manager) self.malaysian_strategies = MalaysianCacheStrategies(self.cache_manager) def process_request(self, request: HttpRequest) -> Optional[HttpResponse]: """Add Malaysian cache to request.""" request.malaysian_cache = self.malaysian_cache request.malaysian_strategies = self.malaysian_strategies return None # Template Tags @register.simple_tag def get_cached_postcode(postcode: str) -> str: """Get cached postcode data in template.""" cache_manager = CacheManager() malaysian_cache = MalaysianDataCache(cache_manager) data = malaysian_cache.get_cached_postcode_data(postcode) if data: return f"{data.get('city', 'Unknown')}, {data.get('state', 'Unknown')}" return "Unknown location" @register.simple_tag def get_cached_sst_rate(state: str, category: str = "standard") -> str: """Get cached SST rate in template.""" cache_manager = CacheManager() malaysian_cache = MalaysianDataCache(cache_manager) rate = malaysian_cache.get_cached_sst_rate(state, category) if rate is not None: return f"{rate * 100:.0f}%" return "Rate not available" @register.simple_tag def get_user_cache_info(user) -> str: """Get user cache information.""" if not user or not user.is_authenticated: return "Anonymous" cache_manager = CacheManager() key = f"user_profile_{user.id}" cached_data = cache_manager.get(key) if cached_data: return f"Cached user data available for {user.username}" return f"No cached data for {user.username}" # API Views @api_view(['GET']) def cache_stats(request): """Get cache statistics.""" if not request.user.is_staff: return Response({"error": "Unauthorized"}, status=403) cache_manager = CacheManager() stats = cache_manager.get_cache_stats() return Response(stats) @api_view(['POST']) def clear_cache(request): """Clear cache.""" if not request.user.is_staff: return Response({"error": "Unauthorized"}, status=403) cache_manager = CacheManager() # Clear specific cache keys cache_type = request.data.get('type', 'all') if cache_type == 'tenant': tenant_id = request.data.get('tenant_id') success = cache_manager.clear_tenant_cache(tenant_id) elif cache_type == 'malaysian': # Clear Malaysian data cache success = cache_manager.clear_tenant_cache() else: # Clear all cache success = cache_manager.clear_tenant_cache() if success: return Response({"message": f"Cache cleared successfully"}) else: return Response({"error": "Failed to clear cache"}, status=500) @api_view(['GET']) def warm_cache(request): """Warm cache with frequently accessed data.""" if not request.user.is_staff: return Response({"error": "Unauthorized"}, status=403) from .cache_manager import cache_warmer # Warm Malaysian data warmed = cache_warmer.warm_malaysian_data() # Warm user data if specified user_ids = request.GET.getlist('user_ids') if user_ids: user_ids = [int(uid) for uid in user_ids] warmed_users = cache_warmer.warm_user_data(user_ids) warmed['users'] = warmed_users return Response({ "message": "Cache warming completed", "warmed_items": warmed }) # Django Settings Integration def get_cache_config() -> Dict[str, Any]: """Get cache configuration for Django settings.""" return { 'CACHES': { 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': getattr(settings, 'REDIS_URL', 'redis://127.0.0.1:6379/1'), 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', }, 'KEY_PREFIX': 'malaysian_sme_', }, 'locmem': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-snowflake', }, }, 'CACHE_MIDDLEWARE_ALIAS': 'default', 'CACHE_MIDDLEWARE_SECONDS': 300, 'CACHE_MIDDLEWARE_KEY_PREFIX': 'malaysian_sme_', } # Signal Handlers def invalidate_user_cache(sender, instance, **kwargs): """Invalidate cache when user is updated.""" cache_manager = CacheManager() cache_key = f"user_profile_{instance.id}" cache_manager.delete(cache_key) # Clear tenant cache if user is tenant owner if hasattr(instance, 'owned_tenants'): for tenant in instance.owned_tenants.all(): cache_manager.clear_tenant_cache(tenant.id) def invalidate_model_cache(sender, instance, **kwargs): """Invalidate cache when model is updated.""" cache_manager = CacheManager() query_cache = QueryCache(cache_manager) model_name = instance.__class__.__name__.lower() query_cache.invalidate_model_cache(model_name) # Django Admin Integration class CacheAdminMixin: """Mixin for Django admin cache management.""" def save_model(self, request, obj, form, change): """Override save to invalidate cache.""" super().save_model(request, obj, form, change) # Invalidate cache cache_manager = CacheManager() model_name = obj.__class__.__name__.lower() query_cache = QueryCache(cache_manager) query_cache.invalidate_model_cache(model_name) def delete_model(self, request, obj): """Override delete to invalidate cache.""" cache_manager = CacheManager() model_name = obj.__class__.__name__.lower() query_cache = QueryCache(cache_manager) query_cache.invalidate_model_cache(model_name) super().delete_model(request, obj)