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
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
|