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.
883 lines
38 KiB
883 lines
38 KiB
from rest_framework.generics import ListAPIView
|
|
from rest_framework.response import Response
|
|
from django.shortcuts import get_object_or_404
|
|
from utils.pagination import NoPagination
|
|
from django.db.models import Q, Prefetch
|
|
from collections import defaultdict
|
|
from utils.pagination import StandardResultsSetPagination
|
|
from utils import absolute_https_url
|
|
from ..models import HadisSect, HadisCategory, Hadis, HadisTransmitter
|
|
from apps.bookmark.serializers.bookmark import BookmarkStatusSerializer
|
|
from ..serializers import (
|
|
HadisCategorySectListSerializer,
|
|
HadisCategoryTreeSerializer,
|
|
CategorySerializer ,
|
|
HadisCategorySelectSerializer ,
|
|
HadisCategorySelectSourceSerializer,
|
|
get_localized_text,
|
|
get_arabic_localized_text,
|
|
SimpleCategory
|
|
)
|
|
from ..docs import (
|
|
hadis_sect_list_swagger,
|
|
hadis_category_tree_swagger,
|
|
categories_list_swagger,
|
|
categories_by_sect_swagger,
|
|
categories_tree_by_sect_swagger,
|
|
categories_tree_by_sect_source_swagger,
|
|
hadis_category_xmind_swagger
|
|
)
|
|
|
|
|
|
class HadisCategorySectListView(ListAPIView):
|
|
"""
|
|
API view to list all HadisSects grouped by sect_type (shia/sunni)
|
|
"""
|
|
queryset = HadisSect.objects.filter(is_active=True).order_by('order')
|
|
serializer_class = HadisCategorySectListSerializer
|
|
pagination_class = NoPagination
|
|
|
|
@hadis_sect_list_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
|
|
class HadisCategoryTreeView(ListAPIView):
|
|
"""
|
|
API view to get all HadisCategory tree structure grouped by sect
|
|
"""
|
|
serializer_class = HadisCategoryTreeSerializer
|
|
pagination_class = NoPagination
|
|
|
|
@hadis_category_tree_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Fetch ALL active categories in a single query with annotations.
|
|
"""
|
|
from django.db.models import Count, Q
|
|
|
|
return (
|
|
HadisCategory.objects
|
|
.filter(sect__is_active=True)
|
|
.select_related('sect')
|
|
.annotate(
|
|
# Count of hadiths directly in this category
|
|
direct_hadis_count=Count('hadis', filter=Q(hadis__status=True))
|
|
)
|
|
.order_by('sect__order', 'tree_id', 'lft', 'order')
|
|
)
|
|
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
queryset = self.get_queryset()
|
|
|
|
# 1. Map Hadis IDs
|
|
hadis_ids_map = defaultdict(list)
|
|
hadis_pairs = (
|
|
Hadis.objects
|
|
.filter(status=True, category_id__in=[cat.id for cat in queryset])
|
|
.values_list('category_id', 'id')
|
|
.order_by('category_id', 'id')
|
|
)
|
|
for category_id, hadis_id in hadis_pairs:
|
|
hadis_ids_map[category_id].append(hadis_id)
|
|
|
|
# 👇 2. Map Quran Data (Bulk fetch to prevent N+1 Queries)
|
|
quran_data_map = {}
|
|
from ..models.category import CategoryQuranVerse
|
|
from ..serializers.category import CategoryQuranVerseSerializer
|
|
|
|
quran_verses = CategoryQuranVerse.objects.filter(
|
|
category__in=queryset
|
|
).prefetch_related('secondary_translations')
|
|
|
|
for verse in quran_verses:
|
|
quran_data_map[verse.category_id] = CategoryQuranVerseSerializer(
|
|
verse, context={'request': request}
|
|
).data
|
|
|
|
# 3. Build a mapping of all categories and their children
|
|
category_map = {cat.id: cat for cat in queryset}
|
|
children_map = {}
|
|
roots = []
|
|
|
|
for cat in queryset:
|
|
if cat.parent_id is None:
|
|
roots.append(cat)
|
|
else:
|
|
if cat.parent_id not in children_map:
|
|
children_map[cat.parent_id] = []
|
|
children_map[cat.parent_id].append(cat)
|
|
|
|
# 4. Pre-calculate recursive hadis counts
|
|
recursive_counts = {}
|
|
|
|
def get_recursive_count(cat_id):
|
|
if cat_id in recursive_counts:
|
|
return recursive_counts[cat_id]
|
|
|
|
cat = category_map[cat_id]
|
|
count = cat.direct_hadis_count
|
|
|
|
for child in children_map.get(cat_id, []):
|
|
count += get_recursive_count(child.id)
|
|
|
|
recursive_counts[cat_id] = count
|
|
return count
|
|
|
|
for cat in queryset:
|
|
get_recursive_count(cat.id)
|
|
|
|
# 5. Build grouped structure
|
|
grouped_data = {}
|
|
|
|
for root in roots:
|
|
sect_type = root.sect.sect_type
|
|
|
|
# Build tree using mapping (returns None if the root branch is completely empty)
|
|
category_data = self._build_tree_recursive(
|
|
root,
|
|
request,
|
|
children_map,
|
|
recursive_counts,
|
|
hadis_ids_map,
|
|
quran_data_map,
|
|
)
|
|
if category_data is None:
|
|
continue
|
|
|
|
if sect_type not in grouped_data:
|
|
grouped_data[sect_type] = {
|
|
'sects': {},
|
|
'categories': []
|
|
}
|
|
|
|
# Add sect info
|
|
sect_id = str(root.sect.id)
|
|
if sect_id not in grouped_data[sect_type]['sects']:
|
|
source_types = HadisCategory.objects.filter(
|
|
sect=root.sect
|
|
).values_list('source_type', flat=True).order_by().distinct()
|
|
|
|
grouped_data[sect_type]['sects'][sect_id] = {
|
|
'id': root.sect.id,
|
|
'sect_type': root.sect.sect_type,
|
|
'title': get_localized_text(root.sect.title, request),
|
|
'description': get_localized_text(root.sect.description, request),
|
|
'order': root.sect.order,
|
|
'source_types': list(source_types)
|
|
}
|
|
|
|
grouped_data[sect_type]['categories'].append(category_data)
|
|
|
|
total_count = len(queryset)
|
|
|
|
response_data = {
|
|
'count': total_count,
|
|
'results': grouped_data
|
|
}
|
|
|
|
return Response(response_data)
|
|
|
|
def _build_tree_recursive(self, category, request, children_map, recursive_counts, hadis_ids_map, quran_data_map):
|
|
"""
|
|
Build tree from flat mapping, pruning any node that has no active children and no active hadiths.
|
|
"""
|
|
raw_children = children_map.get(category.id, [])
|
|
built_children = []
|
|
for child in raw_children:
|
|
child_node = self._build_tree_recursive(
|
|
child,
|
|
request,
|
|
children_map,
|
|
recursive_counts,
|
|
hadis_ids_map,
|
|
quran_data_map,
|
|
)
|
|
if child_node is not None:
|
|
built_children.append(child_node)
|
|
|
|
has_hadis = category.direct_hadis_count > 0
|
|
has_quran = bool(quran_data_map.get(category.id, None)) if category.source_type == HadisCategory.SourceType.QURAN else False
|
|
|
|
# If this category has no children and no hadiths (and no quran verse), prune it
|
|
if len(built_children) == 0 and not has_hadis and not has_quran:
|
|
return None
|
|
|
|
return {
|
|
'id': category.id,
|
|
'title': get_localized_text(category.title, request),
|
|
'description': get_localized_text(category.description, request),
|
|
'slug': category.slug,
|
|
'source_type': category.source_type,
|
|
'sect_id': category.sect_id,
|
|
'sect_type': category.sect.sect_type,
|
|
'hadis_count': category.direct_hadis_count,
|
|
'children_count': recursive_counts.get(category.id, 0),
|
|
'has_hadis': has_hadis,
|
|
'has_parent': category.parent_id is not None,
|
|
'hadis_ids': hadis_ids_map.get(category.id, []) if has_hadis else [],
|
|
'quran_data': quran_data_map.get(category.id, None) if category.source_type == HadisCategory.SourceType.QURAN else None,
|
|
'order': category.order,
|
|
'thumbnail': self._get_thumbnail_url(category, request),
|
|
'xmind_file': self._get_xmind_url(category, request),
|
|
'has_xmind_file': bool(getattr(category, 'xmind_file', None)),
|
|
'children': built_children
|
|
}
|
|
|
|
def _get_thumbnail_url(self, category, request):
|
|
"""Get absolute thumbnail URL"""
|
|
if hasattr(category, 'thumbnail') and category.thumbnail:
|
|
return absolute_https_url(category.thumbnail.url, request)
|
|
return None
|
|
|
|
def _get_xmind_url(self, category, request):
|
|
"""Get absolute xmind URL"""
|
|
if getattr(category, 'xmind_file', None):
|
|
return request.build_absolute_uri(category.xmind_file.url) if request else category.xmind_file.url
|
|
return None
|
|
|
|
|
|
class HadisCategoryTreeNormalView(ListAPIView):
|
|
"""
|
|
Normal (paginated) tree view for HadisCategory.
|
|
Unlike the sync view, this simply returns the root categories (filtered to active sects)
|
|
with their nested children, and uses the project's default pagination.
|
|
"""
|
|
serializer_class = HadisCategoryTreeSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@hadis_category_tree_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
from django.db.models import Count, Q
|
|
return HadisCategory.objects.filter(
|
|
parent__isnull=True,
|
|
sect__is_active=True
|
|
).annotate(
|
|
active_hadis_count=Count('hadis', filter=Q(hadis__status=True)),
|
|
children_total_count=Count('children')
|
|
).filter(
|
|
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
|
|
).order_by('sect__order', 'order')
|
|
|
|
def apply_category_ordering_and_counts(queryset, request):
|
|
from django.db.models import Subquery, OuterRef, Count, IntegerField
|
|
from django.db.models.functions import Coalesce
|
|
from django.db.models.expressions import RawSQL
|
|
from django.utils.translation import get_language
|
|
|
|
descendant_hadis_subquery = Subquery(
|
|
Hadis.objects.filter(
|
|
category__tree_id=OuterRef('tree_id'),
|
|
category__lft__gte=OuterRef('lft'),
|
|
category__rght__lte=OuterRef('rght'),
|
|
status=True
|
|
).values('category__tree_id').annotate(cnt=Count('id')).values('cnt')[:1],
|
|
output_field=IntegerField()
|
|
)
|
|
queryset = queryset.annotate(total_content_count=Coalesce(descendant_hadis_subquery, 0))
|
|
|
|
ordering_param = (
|
|
request.query_params.get('ordering') or
|
|
request.query_params.get('sort') or
|
|
''
|
|
).strip().lower()
|
|
|
|
if ordering_param in ['content', 'most_content', '-total_content_count', '-content']:
|
|
return queryset.order_by('-total_content_count', 'order', 'id')
|
|
elif ordering_param in ['alphabetical', 'title', 'slug', 'a-z']:
|
|
lang = (
|
|
request.query_params.get('language_code') or
|
|
request.query_params.get('lang') or
|
|
getattr(request, 'LANGUAGE_CODE', None)
|
|
)
|
|
if not lang and hasattr(request, 'headers'):
|
|
lang = request.headers.get('Accept-Language')
|
|
if lang:
|
|
lang = lang.split(',')[0].split(';')[0].split('-')[0].strip().lower()
|
|
if not lang:
|
|
lang = get_language() or 'en'
|
|
|
|
queryset = queryset.annotate(
|
|
loc_title=RawSQL("""
|
|
COALESCE(
|
|
NULLIF((SELECT COALESCE(elem->>'title', elem->>'text') FROM jsonb_array_elements(CASE WHEN jsonb_typeof(hadis_hadiscategory.title::jsonb) = 'array' THEN hadis_hadiscategory.title::jsonb ELSE '[]'::jsonb END) elem WHERE elem->>'language_code' = %s LIMIT 1), ''),
|
|
NULLIF((SELECT COALESCE(elem->>'title', elem->>'text') FROM jsonb_array_elements(CASE WHEN jsonb_typeof(hadis_hadiscategory.title::jsonb) = 'array' THEN hadis_hadiscategory.title::jsonb ELSE '[]'::jsonb END) elem WHERE elem->>'language_code' = 'en' LIMIT 1), ''),
|
|
hadis_hadiscategory.slug
|
|
)
|
|
""", [lang])
|
|
)
|
|
return queryset.order_by('loc_title', 'slug', 'id')
|
|
|
|
return queryset.order_by('order', 'id')
|
|
|
|
|
|
class HadisCategorySelectBySectView(ListAPIView):
|
|
"""
|
|
Tree view for HadisCategory filtered by sect_type and category slug.
|
|
Returns the children categories of the specified category (by slug) within the sect_type.
|
|
"""
|
|
serializer_class = HadisCategorySelectSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
@categories_tree_by_sect_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
# 1. Run the standard list logic (get pagination, filter, results)
|
|
response = super().list(request, *args, **kwargs)
|
|
|
|
# 2. Find the "Parent" Category based on the URL slug
|
|
category_slug = self.kwargs.get('slug')
|
|
category_obj = get_object_or_404(HadisCategory, slug=category_slug)
|
|
|
|
# 3. Serialize this single category for the Hero section
|
|
# You might need a simple serializer just for titles/descriptions
|
|
category_data = SimpleCategory(category_obj).data
|
|
|
|
# 4. Inject it into the response data
|
|
# Note: We access response.data because we are using DRF's Response object
|
|
if isinstance(response.data, dict):
|
|
# Reorder the response to place current_category before results
|
|
ordered_data = {}
|
|
for key in ['count', 'next', 'previous']:
|
|
if key in response.data:
|
|
ordered_data[key] = response.data[key]
|
|
ordered_data['current_category'] = category_data
|
|
if 'results' in response.data:
|
|
ordered_data['results'] = response.data['results']
|
|
response.data = ordered_data
|
|
|
|
return response
|
|
|
|
|
|
def get_queryset(self):
|
|
sect_type = self.kwargs.get('sect_type')
|
|
slug = self.kwargs.get('slug')
|
|
|
|
# Find the parent category by slug and sect_type
|
|
try:
|
|
parent_category = HadisCategory.objects.get(
|
|
slug=slug,
|
|
sect__sect_type=sect_type,
|
|
sect__is_active=True
|
|
)
|
|
except HadisCategory.DoesNotExist:
|
|
return HadisCategory.objects.none()
|
|
|
|
from django.db.models import Count, Q
|
|
# Return children of this category that have either active hadiths, children, or are quran
|
|
queryset = HadisCategory.objects.filter(
|
|
parent=parent_category,
|
|
sect__sect_type=sect_type,
|
|
sect__is_active=True
|
|
).annotate(
|
|
active_hadis_count=Count('hadis', filter=Q(hadis__status=True)),
|
|
children_total_count=Count('children')
|
|
).filter(
|
|
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
|
|
)
|
|
return apply_category_ordering_and_counts(queryset, self.request)
|
|
|
|
|
|
class HadisCategorySelectBySectSourceView(ListAPIView):
|
|
"""
|
|
Tree view for HadisCategory filtered by sect_type, category slug and source_type.
|
|
Returns the children categories of the specified category (by slug) within the sect_type, filtered by source_type.
|
|
"""
|
|
serializer_class = HadisCategorySelectSourceSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
@categories_tree_by_sect_source_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
# 1. Run the standard list logic (get pagination, filter, results)
|
|
response = super().list(request, *args, **kwargs)
|
|
|
|
# 2. Find the "Parent" Category based on the URL slug
|
|
category_slug = self.kwargs.get('slug')
|
|
category_obj = get_object_or_404(HadisCategory, slug=category_slug)
|
|
|
|
# 3. Serialize this single category for the Hero section
|
|
# You might need a simple serializer just for titles/descriptions
|
|
category_data = SimpleCategory(category_obj).data
|
|
|
|
# 4. Inject it into the response data
|
|
# Note: We access response.data because we are using DRF's Response object
|
|
if isinstance(response.data, dict):
|
|
# Reorder the response to place current_category before results
|
|
ordered_data = {}
|
|
for key in ['count', 'next', 'previous']:
|
|
if key in response.data:
|
|
ordered_data[key] = response.data[key]
|
|
ordered_data['current_category'] = category_data
|
|
if 'results' in response.data:
|
|
ordered_data['results'] = response.data['results']
|
|
response.data = ordered_data
|
|
|
|
return response
|
|
|
|
|
|
def get_queryset(self):
|
|
sect_type = self.kwargs.get('sect_type')
|
|
slug = self.kwargs.get('slug')
|
|
source_type = self.kwargs.get('source_type')
|
|
|
|
# Find the parent category by slug and sect_type
|
|
try:
|
|
parent_category = HadisCategory.objects.get(
|
|
slug=slug,
|
|
sect__sect_type=sect_type,
|
|
sect__is_active=True
|
|
)
|
|
except HadisCategory.DoesNotExist:
|
|
return HadisCategory.objects.none()
|
|
|
|
from django.db.models import Count, Q
|
|
# Return children of this category, filtered by source_type and excluding empty leaves
|
|
queryset = HadisCategory.objects.filter(
|
|
parent=parent_category,
|
|
sect__sect_type=sect_type,
|
|
sect__is_active=True,
|
|
source_type=source_type
|
|
).annotate(
|
|
active_hadis_count=Count('hadis', filter=Q(hadis__status=True)),
|
|
children_total_count=Count('children')
|
|
).filter(
|
|
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
|
|
)
|
|
return apply_category_ordering_and_counts(queryset, self.request)
|
|
|
|
class CategoriesView(ListAPIView):
|
|
"""
|
|
API view to list all HadisCategories (excluding empty leaf categories)
|
|
"""
|
|
serializer_class = CategorySerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
@categories_list_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
from django.db.models import Count, Q
|
|
queryset = HadisCategory.objects.filter(
|
|
sect__is_active=True
|
|
).annotate(
|
|
active_hadis_count=Count('hadis', filter=Q(hadis__status=True)),
|
|
children_total_count=Count('children')
|
|
).filter(
|
|
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
|
|
)
|
|
is_root = self.request.query_params.get('is_root', '').lower()
|
|
if is_root in ['true', '1']:
|
|
queryset = queryset.filter(parent__isnull=True)
|
|
elif is_root in ['false', '0']:
|
|
queryset = queryset.filter(parent__isnull=False)
|
|
|
|
return apply_category_ordering_and_counts(queryset, self.request)
|
|
|
|
|
|
|
|
class CategoriesBySectView(ListAPIView):
|
|
"""
|
|
API view to list HadisCategories filtered by sect_type (excluding empty leaf categories)
|
|
"""
|
|
serializer_class = CategorySerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
sect_type = self.kwargs.get('sect_type')
|
|
from django.db.models import Count, Q
|
|
queryset = HadisCategory.objects.filter(
|
|
sect__sect_type=sect_type,
|
|
sect__is_active=True
|
|
).annotate(
|
|
active_hadis_count=Count('hadis', filter=Q(hadis__status=True)),
|
|
children_total_count=Count('children')
|
|
).filter(
|
|
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
|
|
)
|
|
is_root = self.request.query_params.get('is_root', 'true').lower()
|
|
if is_root in ['true', '1']:
|
|
queryset = queryset.filter(parent__isnull=True)
|
|
elif is_root in ['false', '0']:
|
|
queryset = queryset.filter(parent__isnull=False)
|
|
|
|
return apply_category_ordering_and_counts(queryset, self.request)
|
|
|
|
@categories_by_sect_swagger
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
|
|
from rest_framework.decorators import api_view, permission_classes, authentication_classes
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
|
|
@api_view(['GET', 'POST'])
|
|
@permission_classes([AllowAny]) # Let anyone access this
|
|
@authentication_classes([]) # Disable auth so we don't get 403
|
|
def test_deploy(request):
|
|
# This filters all headers Django receives and returns them as JSON
|
|
headers = {
|
|
k: v for k, v in request.META.items()
|
|
if k.startswith('HTTP_') or k == 'CONTENT_TYPE'
|
|
}
|
|
|
|
# Also check if Authentication settings are actually active
|
|
from django.conf import settings
|
|
auth_settings = settings.REST_FRAMEWORK.get('DEFAULT_AUTHENTICATION_CLASSES', 'NOT SET')
|
|
|
|
return Response({
|
|
"received_headers": headers,
|
|
"active_auth_settings": auth_settings
|
|
})
|
|
|
|
from django.http import JsonResponse
|
|
from django.conf import settings
|
|
def debug_headers(request):
|
|
# # Security: strictly limitation to prevent leaking sensitive info to public
|
|
# # Only allow if a specific secret key is passed in the URL
|
|
# if request.GET.get('secret_debug_key') != 'super_secret_123':
|
|
# return JsonResponse({'error': 'Unauthorized'}, status=403)
|
|
|
|
# Return all HTTP headers Django received from Nginx
|
|
headers = {
|
|
k: v for k, v in request.META.items()
|
|
if k.startswith('HTTP_') or k in ['CONTENT_TYPE', 'CONTENT_LENGTH']
|
|
}
|
|
|
|
# Also return the scheme Django thinks it is using
|
|
scheme_debug = {
|
|
'scheme': request.scheme,
|
|
'is_secure': request.is_secure(),
|
|
'SECURE_PROXY_SSL_HEADER_SETTING': getattr(settings, 'SECURE_PROXY_SSL_HEADER', None),
|
|
}
|
|
|
|
return JsonResponse({'headers': headers, 'debug': scheme_debug})
|
|
|
|
|
|
|
|
from rest_framework.views import APIView
|
|
|
|
class HadisCategoryXMindView(APIView):
|
|
"""
|
|
Returns a mind-map JSON structure for a specific category and its transmission chains ending in hadiths.
|
|
Root -> Category
|
|
Level 1+ -> Narrators in transmission chains (merged prefixes)
|
|
Leaves -> Hadiths
|
|
"""
|
|
|
|
def get_localized(self, json_field, lang='en'):
|
|
return get_localized_text(json_field, language_code=lang) or ""
|
|
|
|
def get_arabic(self, json_field):
|
|
return get_arabic_localized_text(json_field) or ""
|
|
|
|
def _get_thumbnail_url(self, thumbnail, request):
|
|
if not thumbnail:
|
|
return None
|
|
try:
|
|
if request:
|
|
return request.build_absolute_uri(thumbnail.url)
|
|
except Exception:
|
|
pass
|
|
return thumbnail.url
|
|
|
|
@hadis_category_xmind_swagger
|
|
def get(self, request, category_slug):
|
|
lang = request.query_params.get('lang') or request.query_params.get('language_code') or 'en'
|
|
category = get_object_or_404(HadisCategory, slug=category_slug)
|
|
root_title = self.get_localized(category.title, lang) or category.slug
|
|
|
|
user = request.user if request and hasattr(request, 'user') else None
|
|
if user and user.is_anonymous:
|
|
user = None
|
|
|
|
# Fetch active hadiths with their transmitters and corrections
|
|
transmitter_qs = HadisTransmitter.objects.select_related(
|
|
'transmitter__reliability',
|
|
'status',
|
|
'narrator_layer'
|
|
).prefetch_related(
|
|
'uncertain_transmitters__reliability'
|
|
).order_by('order')
|
|
|
|
hadiths = Hadis.objects.filter(
|
|
category=category,
|
|
status=True
|
|
).order_by('number').prefetch_related(
|
|
Prefetch('transmitters', queryset=transmitter_qs),
|
|
'narrator_layers',
|
|
'hadiscorrection_set'
|
|
)
|
|
|
|
# Build prefix tree (Trie-like structure)
|
|
# Root: Category
|
|
# Branches: Narrator nodes along transmission chains
|
|
# Leaves: Hadiths
|
|
root_tree = {
|
|
"id": f"cat-{category.id}",
|
|
"slug": category.slug,
|
|
"node_type": "category",
|
|
"title": root_title,
|
|
"children_map": {}, # key: (node_type, id) -> subtree dict
|
|
"leaf_hadiths": set()
|
|
}
|
|
|
|
for hadis in hadiths:
|
|
hadis_title = self.get_localized(hadis.title, lang)
|
|
hadis_title_narrator = self.get_localized(hadis.title_narrator, lang)
|
|
hadis_translation = self.get_localized(hadis.translation, lang)
|
|
|
|
bookmark_info = BookmarkStatusSerializer.get_bookmark_info(
|
|
obj=hadis,
|
|
user=user,
|
|
service='hadith'
|
|
)
|
|
is_bookmarked = bookmark_info.get('is_bookmarked', False)
|
|
|
|
corrections_data = [
|
|
{
|
|
"id": c.id,
|
|
"slug": c.slug,
|
|
"title": self.get_localized(c.title, lang),
|
|
"narrator": c.narrator,
|
|
"text": c.text,
|
|
"translation": self.get_localized(c.translation, lang),
|
|
"share_link": c.share_link,
|
|
}
|
|
for c in hadis.hadiscorrection_set.all()
|
|
]
|
|
|
|
hadis_node_data = {
|
|
"id": f"hadis-{hadis.id}",
|
|
"hadis_id": hadis.id,
|
|
"node_type": "hadith",
|
|
"slug": hadis.slug,
|
|
"number": hadis.number,
|
|
"title": hadis_title,
|
|
"title_narrator": hadis_title_narrator,
|
|
"text": hadis.text,
|
|
"translation": hadis_translation,
|
|
"share_link": hadis.share_link,
|
|
"is_bookmarked": is_bookmarked,
|
|
"corrections": corrections_data,
|
|
}
|
|
|
|
# Group transmitters by narrative layer (NarratorLayer)
|
|
# Each narrative layer forms its own transmission branch
|
|
layer_groups = defaultdict(list)
|
|
for ht in hadis.transmitters.all():
|
|
if ht.transmitter:
|
|
layer_key = ht.narrator_layer_id if ht.narrator_layer_id is not None else f"chain-{ht.chain_index}"
|
|
layer_groups[layer_key].append(ht)
|
|
|
|
if not layer_groups:
|
|
# No transmitters: directly attach hadith to category root
|
|
hadis_key = ("hadith", hadis.id)
|
|
if hadis_key not in root_tree["children_map"]:
|
|
root_tree["children_map"][hadis_key] = {
|
|
"data": hadis_node_data,
|
|
"children_map": {},
|
|
"leaf_hadiths": set()
|
|
}
|
|
root_tree["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id)
|
|
root_tree["leaf_hadiths"].add(hadis.id)
|
|
else:
|
|
# Sort layers by layer number if available
|
|
sorted_layer_items = sorted(
|
|
layer_groups.items(),
|
|
key=lambda item: (item[1][0].narrator_layer.number if item[1] and item[1][0].narrator_layer else 0)
|
|
)
|
|
|
|
for layer_key, ht_list in sorted_layer_items:
|
|
# Sort transmitters in reverse order so the chain goes from first narrator to last narrator reaching the hadith
|
|
ht_list.sort(key=lambda x: x.order, reverse=True)
|
|
|
|
curr = root_tree
|
|
curr["leaf_hadiths"].add(hadis.id)
|
|
|
|
for ht in ht_list:
|
|
tr = ht.transmitter
|
|
is_uncertain = getattr(ht, 'is_uncertain', False)
|
|
cand_objs = list(ht.uncertain_transmitters.all()) if is_uncertain else []
|
|
is_node_uncertain = bool(is_uncertain and len(cand_objs) > 0)
|
|
|
|
if is_node_uncertain:
|
|
cand_ids = tuple(sorted([ut.id for ut in cand_objs]))
|
|
tr_key = ("uncertain", cand_ids)
|
|
else:
|
|
cand_ids = ()
|
|
tr_key = ("narrator", tr.id if tr else ht.id)
|
|
|
|
if tr_key not in curr["children_map"]:
|
|
rel_obj = (tr.reliability if tr else None) or ht.status
|
|
rel_data = None
|
|
if rel_obj:
|
|
rel_data = {
|
|
"id": rel_obj.id,
|
|
"title": self.get_localized(rel_obj.title, lang),
|
|
"color": getattr(rel_obj, 'color', None),
|
|
"main_color_code": getattr(rel_obj, 'main_color_code', None),
|
|
"slug": getattr(rel_obj, 'slug', None),
|
|
}
|
|
|
|
layer_obj = ht.narrator_layer
|
|
layer_data = None
|
|
if layer_obj:
|
|
layer_data = {
|
|
"id": layer_obj.id,
|
|
"number": layer_obj.number,
|
|
"slug": layer_obj.slug,
|
|
"name": self.get_localized(layer_obj.name, lang),
|
|
"description": self.get_localized(layer_obj.description, lang),
|
|
}
|
|
|
|
uncertain_candidates = []
|
|
if is_node_uncertain:
|
|
for ut in cand_objs:
|
|
ut_rel = ut.reliability
|
|
ut_rel_data = None
|
|
if ut_rel:
|
|
ut_rel_data = {
|
|
"id": ut_rel.id,
|
|
"title": self.get_localized(ut_rel.title, lang),
|
|
"color": getattr(ut_rel, 'color', None),
|
|
"main_color_code": getattr(ut_rel, 'main_color_code', None),
|
|
"slug": getattr(ut_rel, 'slug', None),
|
|
}
|
|
uncertain_candidates.append({
|
|
"id": ut.id,
|
|
"transmitter_id": ut.id,
|
|
"slug": ut.slug,
|
|
"name": self.get_localized(ut.full_name, lang) or ut.slug,
|
|
"title": self.get_localized(ut.full_name, lang) or ut.slug,
|
|
"full_name": self.get_localized(ut.full_name, lang),
|
|
"arabic_full_name": self.get_arabic(ut.full_name),
|
|
"kunya": self.get_localized(ut.kunya, lang),
|
|
"arabic_kunya": self.get_arabic(ut.kunya),
|
|
"known_as": self.get_localized(ut.known_as, lang),
|
|
"arabic_known_as": self.get_arabic(ut.known_as),
|
|
"nickname": self.get_localized(ut.nickname, lang),
|
|
"arabic_nickname": self.get_arabic(ut.nickname),
|
|
"birth_year_hijri": ut.birth_year_hijri,
|
|
"death_year_hijri": ut.death_year_hijri,
|
|
"reliability": ut_rel_data,
|
|
"thumbnail": self._get_thumbnail_url(ut.thumbnail, request),
|
|
"share_link": ut.share_link,
|
|
})
|
|
|
|
node_type_str = "uncertain" if is_node_uncertain else "narrator"
|
|
node_id_str = f"uncertain-{'-'.join(map(str, cand_ids))}" if is_node_uncertain else f"narrator-{tr.id if tr else ht.id}"
|
|
|
|
tr_node_data = {
|
|
"id": node_id_str,
|
|
"transmitter_id": tr.id if tr else (uncertain_candidates[0]["id"] if uncertain_candidates else None),
|
|
"node_type": node_type_str,
|
|
"tag": node_type_str,
|
|
"slug": tr.slug if tr else (uncertain_candidates[0]["slug"] if uncertain_candidates else None),
|
|
"title": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) or tr.slug if tr else "Narrator"),
|
|
"full_name": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) if tr else "Narrator"),
|
|
"arabic_full_name": self.get_arabic(tr.full_name) if tr else None,
|
|
"kunya": self.get_localized(tr.kunya, lang) if tr else None,
|
|
"arabic_kunya": self.get_arabic(tr.kunya) if tr else None,
|
|
"known_as": self.get_localized(tr.known_as, lang) if tr else None,
|
|
"arabic_known_as": self.get_arabic(tr.known_as) if tr else None,
|
|
"nickname": self.get_localized(tr.nickname, lang) if tr else None,
|
|
"arabic_nickname": self.get_arabic(tr.nickname) if tr else None,
|
|
"birth_year_hijri": tr.birth_year_hijri if tr else None,
|
|
"death_year_hijri": tr.death_year_hijri if tr else None,
|
|
"age_at_death": tr.age_at_death if tr else None,
|
|
"generation": tr.generation if tr else None,
|
|
"tadlis": tr.tadlis if tr else None,
|
|
"ikhtilat": tr.ikhtilat if tr else None,
|
|
"companion_type": tr.companion_type if tr else None,
|
|
"madhhab": tr.madhhab if tr else None,
|
|
"in_sahih_muslim": tr.in_sahih_muslim if tr else None,
|
|
"in_sahih_bukhari": tr.in_sahih_bukhari if tr else None,
|
|
"reliability": rel_data,
|
|
"is_uncertain": is_node_uncertain,
|
|
"uncertain_transmitters": uncertain_candidates,
|
|
"transmitter": {
|
|
"id": tr.id,
|
|
"name": self.get_localized(tr.full_name, lang) or tr.slug,
|
|
"full_name": self.get_localized(tr.full_name, lang),
|
|
"slug": tr.slug,
|
|
"reliability": rel_data,
|
|
} if tr else None,
|
|
"layer": layer_data,
|
|
"layer_number": layer_obj.number if layer_obj else None,
|
|
"layer_name": self.get_localized(layer_obj.name, lang) if layer_obj else None,
|
|
"thumbnail": self._get_thumbnail_url(tr.thumbnail, request) if tr else None,
|
|
"share_link": tr.share_link if tr else None,
|
|
}
|
|
curr["children_map"][tr_key] = {
|
|
"data": tr_node_data,
|
|
"children_map": {},
|
|
"leaf_hadiths": set()
|
|
}
|
|
curr = curr["children_map"][tr_key]
|
|
curr["leaf_hadiths"].add(hadis.id)
|
|
|
|
# Attach hadith leaf under the last narrator of this chain
|
|
hadis_key = ("hadith", hadis.id)
|
|
if hadis_key not in curr["children_map"]:
|
|
curr["children_map"][hadis_key] = {
|
|
"data": hadis_node_data,
|
|
"children_map": {},
|
|
"leaf_hadiths": set()
|
|
}
|
|
curr["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id)
|
|
|
|
# Format prefix tree into recursive XMind JSON structure
|
|
def format_tree_node(node_dict, path_prefix=""):
|
|
node_data = dict(node_dict["data"])
|
|
node_type = node_data.get("node_type", "unknown")
|
|
raw_id = node_data.get("id", "0")
|
|
unique_node_id = f"{path_prefix}_{node_type}_{raw_id}"
|
|
node_data["node_id"] = unique_node_id
|
|
|
|
attached = []
|
|
for (child_type, child_id), child_subtree in node_dict["children_map"].items():
|
|
formatted_child = format_tree_node(child_subtree, path_prefix=unique_node_id)
|
|
attached.append(formatted_child)
|
|
|
|
node_data["count"] = len(attached)
|
|
node_data["leaf_count"] = len(node_dict.get("leaf_hadiths", set()))
|
|
node_data["children"] = {
|
|
"attached": attached
|
|
}
|
|
return node_data
|
|
|
|
root_attached = []
|
|
for (child_type, child_id), child_subtree in root_tree["children_map"].items():
|
|
root_attached.append(format_tree_node(child_subtree, path_prefix="root"))
|
|
|
|
data = {
|
|
"rootTopic": {
|
|
"id": root_tree["id"],
|
|
"node_type": "category",
|
|
"slug": category.slug,
|
|
"title": root_title,
|
|
"count": len(root_attached),
|
|
"leaf_count": len(root_tree["leaf_hadiths"]),
|
|
"structureClass": "org.xmind.ui.map.unbalanced",
|
|
"children": {
|
|
"attached": root_attached
|
|
}
|
|
}
|
|
}
|
|
|
|
response = Response(data)
|
|
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
|
response['Pragma'] = 'no-cache'
|
|
response['Expires'] = '0'
|
|
return response
|