from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch, Case, When, Value, IntegerField from rest_framework.pagination import PageNumberPagination from rest_framework.generics import ListAPIView from django.core.cache import cache from django.conf import settings import hashlib import json from apps.hadis.models import * from apps.hadis.serializers import * from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger class CategoryPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' max_page_size = 100 class CategoryListView(ListAPIView): serializer_class = HadisCategorySerializer permission_classes = (IsAuthenticated,) pagination_class = CategoryPagination # Cache timeout in seconds (1 hour) CACHE_TIMEOUT = 60 * 60 def get_cache_key(self, source_type=None): """ Generate a unique cache key based on the view name and filter parameters. Args: source_type: Optional source_type filter parameter Returns: A unique cache key string """ # Base key with the view name key_parts = ['category_tree'] # Add filter parameters to make the key specific if source_type: key_parts.append(f'source_type:{source_type}') # Join all parts with a separator key = ':'.join(key_parts) return key @classmethod def invalidate_cache(cls, source_type=None): """ Invalidate the category tree cache. Args: source_type: Optional source_type to invalidate specific cache. If None, invalidates all category tree caches. """ if source_type: # Invalidate specific tree cache tree_cache_key = cls().get_cache_key(source_type) cache.delete(tree_cache_key) # Invalidate all paginated caches for this source_type paginated_pattern = f'category_tree_paginated:source_type:{source_type}*' paginated_keys = cache.keys(paginated_pattern) if paginated_keys: cache.delete_many(paginated_keys) else: # Invalidate all category tree caches (both full trees and paginated results) # This uses cache key pattern matching if supported by the cache backend # For Redis, we can use wildcards all_cache_keys = cache.keys('category_tree*') if all_cache_keys: cache.delete_many(all_cache_keys) else: # Fallback: delete specific known keys for st in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]: # Delete tree cache tree_cache_key = cls().get_cache_key(st) cache.delete(tree_cache_key) # Try to delete paginated caches try: paginated_pattern = f'category_tree_paginated:source_type:{st}*' paginated_keys = cache.keys(paginated_pattern) if paginated_keys: cache.delete_many(paginated_keys) except: pass # Also delete the default keys (no source_type) cache.delete(cls().get_cache_key()) try: default_paginated_keys = cache.keys('category_tree_paginated:page:*') if default_paginated_keys: cache.delete_many(default_paginated_keys) except: pass def get_children(self, obj): return [self.to_dict(cat) for cat in obj.get_children()] def to_dict(self, c): """ Convert a category to a dictionary with proper tree structure based on level. Args: c: The HadisCategory instance Returns: Dictionary representation of the category with proper tree structure """ # Get the level of this category level = c.level_p # Determine source_type and category_type based on level source_type = None category_type = None if level == 1: # Level 1 (Root) - Has its own source_type source_type = c.source_type category_type = None elif level == 2: # Level 2 (Child) - Inherits source_type from parent, has own category_type if c.parent: source_type = c.parent.source_type else: source_type = c.source_type category_type = c.category_type elif level == 3: # Level 3 (Grandchild) - Inherits source_type from grandparent, category_type from parent if c.parent and c.parent.parent: source_type = c.parent.parent.source_type category_type = c.parent.category_type else: source_type = c.source_type category_type = c.category_type # Get direct children - use getattr to handle both model instances and cached trees if hasattr(c, 'get_children'): # For model instances children = c.get_children() else: # For cached trees children = getattr(c, 'children', []) # Create the dictionary representation return { 'id': c.id, 'name': c.name, 'hadis_count': getattr(c, 'hadis_count', 0), 'source_type': source_type, 'category_type': category_type, 'children': [] if not children else [self.to_dict(child) for child in children], } def get_pagination_cache_key(self, source_type=None, page=1, page_size=None): """ Generate a cache key for paginated results. Args: source_type: Optional source_type filter page: Page number page_size: Number of items per page Returns: A unique cache key for the paginated results """ # Base key with the view name key_parts = ['category_tree_paginated'] # Add filter parameters if source_type: key_parts.append(f'source_type:{source_type}') # Add pagination parameters key_parts.append(f'page:{page}') if page_size: key_parts.append(f'page_size:{page_size}') else: key_parts.append(f'page_size:{self.pagination_class.page_size}') # Join all parts with a separator key = ':'.join(key_parts) return key @category_list_swagger def get(self, request, *args, **kwargs): from mptt.templatetags.mptt_tags import cache_tree_children # Get source_type filter from query params source_type = request.query_params.get('source_type', None) # Get pagination parameters page = request.query_params.get('page', 1) page_size = request.query_params.get('page_size', self.pagination_class.page_size) # Try to get paginated response from cache first pagination_cache_key = self.get_pagination_cache_key(source_type, page, page_size) cached_response = cache.get(pagination_cache_key) if cached_response: return Response(cached_response) # Generate a unique cache key for the full tree tree_cache_key = self.get_cache_key(source_type) # Try to get the tree from cache first tree = cache.get(tree_cache_key) # If not in cache, build the tree if tree is None: # Build filter query filter_query = Q(is_active=True) if source_type and source_type in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]: filter_query &= Q(source_type=source_type) # Get ALL categories with hadis count - this is important to include all levels queryset = HadisCategory.objects.filter(filter_query).select_related( 'parent', 'parent__parent' # Prefetch parent relationships for efficient access ).annotate( hadis_count=Count('hadis'), ) # Use cache_tree_children to build the full tree structure # This will properly set up the parent-child relationships for the entire tree all_categories = cache_tree_children(queryset) # Filter to get only level 1 (root) categories as the starting point for our tree root_categories = [category for category in all_categories if category.parent is None] # Build the tree tree = [] for c in root_categories: # Convert to dictionary with proper tree structure based on level tdata = self.to_dict(c) # Calculate total hadis_count including all children recursively def calculate_total_hadis_count(node): total = node['hadis_count'] for child in node['children']: total += calculate_total_hadis_count(child) return total # Update the hadis_count to include all children tdata['hadis_count'] = calculate_total_hadis_count(tdata) # Add to the result tree tree.append(tdata) # Store the tree in cache cache.set(tree_cache_key, tree, self.CACHE_TIMEOUT) # Apply pagination only to the root categories (level 1) page_obj = self.paginate_queryset(tree) if page_obj is not None: # Get paginated response response = self.get_paginated_response(page_obj) # Cache the paginated response cache.set(pagination_cache_key, response.data, self.CACHE_TIMEOUT) return response # If pagination is not applied, return the full tree return Response(tree) def get_queryset(self): """ Get the base queryset for the serializer. This is used by DRF's default list() method if we don't override get(). Note: This method is not used directly in our implementation since we override get(), but it's kept for completeness and API compatibility. """ source_type = self.request.query_params.get('source_type', None) # Build filter query filter_query = Q(is_active=True) if source_type and source_type in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]: filter_query &= Q(source_type=source_type) # Get ALL categories with proper prefetching for efficiency queryset = HadisCategory.objects.filter(filter_query).select_related( 'parent', 'parent__parent' ).prefetch_related( 'children', 'children__children' # Prefetch two levels of children ).annotate( hadis_count=Count('hadis'), ) # Filter to only return root categories (level 1) queryset = queryset.filter(parent=None) return queryset