You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

301 lines
11 KiB

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