diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index 11ab341..0824db6 100644 --- a/apps/hadis/models/hadis.py +++ b/apps/hadis/models/hadis.py @@ -388,8 +388,10 @@ class Hadis(models.Model): return self._get_json_field("explanations" , lang) def get_address_details(self, lang): - """Get address details (returns full list, not localized)""" - return self.address_details if self.address_details else [] + """Get address details safety check""" + if hasattr(self, 'address_details'): + return self.address_details if self.address_details else [] + return [] class Meta: indexes = [ diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index f22222c..a7f672a 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -102,7 +102,7 @@ class HadisSyncSerializer(serializers.ModelSerializer): # Process address_details address_details_list = [] - if obj.address_details and isinstance(obj.address_details, list): + if hasattr(obj, 'address_details') and obj.address_details and isinstance(obj.address_details, list): address_details_list = sorted( obj.address_details, key=lambda x: x.get('priority', 0) @@ -128,10 +128,23 @@ class HadisSyncSerializer(serializers.ModelSerializer): for transmitter_rel in obj.transmitters.all(): t = transmitter_rel.transmitter layer = transmitter_rel.narrator_layer + + reliability_block = None + if t.reliability: + reliability_block = { + 'id': t.reliability.id, + 'title': get_localized_text(t.reliability.title, request), + 'color': t.reliability.color, + 'main_color_code': t.reliability.main_color_code, + } + transmitters_data.append({ 'id': t.id, 'name': get_localized_text(t.full_name, request), - 'reliability': get_localized_text(t.reliability,request), + 'slug': t.slug, + 'known_as': get_localized_text(t.known_as, request), + 'nickname': get_localized_text(t.nickname, request), + 'reliability': reliability_block, 'layer_level': layer.number if layer else None, 'layer_name': get_localized_text(layer.name, request) if layer else None, 'is_gap': transmitter_rel.is_gap, @@ -150,7 +163,7 @@ class HadisSyncSerializer(serializers.ModelSerializer): request = self.context.get('request') # Return structured explanations with title and description explanations_list = [] - if obj.explanations and isinstance(obj.explanations, list): + if hasattr(obj, 'explanations') and obj.explanations and isinstance(obj.explanations, list): for item in obj.explanations: if isinstance(item, dict): lang = item.get('language_code') @@ -419,6 +432,7 @@ class TransmitterSyncSerializer(serializers.ModelSerializer): 'in_sahih_bukhari': obj.in_sahih_bukhari, 'description': get_localized_text(obj.description, request), 'thumbnail': obj.thumbnail.url if obj.thumbnail else None, + 'share_link': obj.share_link, } def get_scholars_opinions(self, obj): diff --git a/apps/hadis/serializers/reference.py b/apps/hadis/serializers/reference.py index 6832488..ed3c0c3 100644 --- a/apps/hadis/serializers/reference.py +++ b/apps/hadis/serializers/reference.py @@ -148,7 +148,7 @@ class BookReferenceSyncSerializer(serializers.ModelSerializer): class Meta: model = BookReference fields = [ - 'id', 'title', 'rate', 'author', 'detail', 'image', 'attribute', 'hadises' + 'id', 'title', 'slug', 'share_link', 'rate', 'author', 'detail', 'image', 'attribute', 'hadises' ] def get_detail(self, obj): @@ -157,6 +157,7 @@ class BookReferenceSyncSerializer(serializers.ModelSerializer): return { 'description': get_localized_text(obj.description, request), + 'publisher': get_localized_text(obj.publisher, request), 'volume': obj.volume, 'language': get_localized_text(obj.language, request), 'isbn': obj.isbn, diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index ee724d5..75dae65 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -15,12 +15,12 @@ def cached_view(view_func): urlpatterns = [ # Most specific first (with parameters) - path('collections/', cached_view(HadisCollectionListView.as_view()), name='hadis-collection-list'), - path('sync/sects/', cached_view(HadisCategorySectListView.as_view()), name='hadis-sect-list'), - path('sync/categories/tree/', cached_view(HadisCategoryTreeView.as_view()), name='hadis-category-tree'), - path('sync/hadis/', cached_view(HadisSyncView.as_view()), name='hadis-sync'), - path('sync/narrators/', cached_view(TransmitterSyncView.as_view()), name='transmitter-sync'), - path('sync/references/', cached_view(BookReferenceSyncView.as_view()), name='reference-sync'), + path('collections/', HadisCollectionListView.as_view(), name='hadis-collection-list'), + path('sync/sects/', HadisCategorySectListView.as_view(), name='hadis-sect-list'), + path('sync/categories/tree/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'), + path('sync/hadis/', HadisSyncView.as_view(), name='hadis-sync'), + path('sync/narrators/', TransmitterSyncView.as_view(), name='transmitter-sync'), + path('sync/references/', BookReferenceSyncView.as_view(), name='reference-sync'), path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'), path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'), diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index 0876ab8..b6b4015 100644 --- a/apps/hadis/views/category.py +++ b/apps/hadis/views/category.py @@ -52,47 +52,63 @@ class HadisCategoryTreeView(ListAPIView): def get_queryset(self): """ - Prefetch ALL data at once to avoid N+1 + Fetch ALL active categories in a single query with annotations. """ - from django.db.models import Prefetch, Count, Q - - # Create a queryset for children that also has annotations - children_queryset = ( - HadisCategory.objects - .select_related('sect') - .prefetch_related('children') - .annotate( - hadis_count=Count('hadis', filter=Q(hadis__status=True)), - children_count=Count('children', distinct=True) - ) - .order_by('order') - ) + from django.db.models import Count, Q return ( HadisCategory.objects - .filter(parent__isnull=True, sect__is_active=True) + .filter(sect__is_active=True) .select_related('sect') - .prefetch_related( - Prefetch( - 'children', - queryset=children_queryset - ), - ) .annotate( - hadis_count=Count('hadis', filter=Q(hadis__status=True)), - children_count=Count('children', distinct=True) + # Count of hadiths directly in this category + direct_hadis_count=Count('hadis', filter=Q(hadis__status=True)) ) - .order_by('sect__order', 'order') + .order_by('sect__order', 'tree_id', 'lft', 'order') ) def list(self, request, *args, **kwargs): queryset = self.get_queryset() + + # 1. 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) + + # 2. Pre-calculate recursive hadis counts (total hadiths in sub-tree) + # This matches CategorySerializer.children_count logic + 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) + + # 3. Build grouped structure grouped_data = {} - # Single pass through prefetched data (NO QUERIES) - for category in queryset: - sect_type = category.sect.sect_type + for root in roots: + sect_type = root.sect.sect_type if sect_type not in grouped_data: grouped_data[sect_type] = { @@ -101,26 +117,29 @@ class HadisCategoryTreeView(ListAPIView): } # Add sect info - sect_id = str(category.sect.id) + sect_id = str(root.sect.id) if sect_id not in grouped_data[sect_type]['sects']: + # Optimistic source_types fetch - though still a query per sect + # but better than before source_types = HadisCategory.objects.filter( - sect=category.sect - ).values_list('source_type', flat=True) + sect=root.sect + ).values_list('source_type', flat=True).order_by().distinct() + grouped_data[sect_type]['sects'][sect_id] = { - 'id': category.sect.id, - 'sect_type': category.sect.sect_type, - 'title': get_localized_text(category.sect.title, request), - 'description': get_localized_text(category.sect.description,request), - 'order': category.sect.order, - 'source_types':list(set(source_types)) + '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) } - # Build tree using prefetched data - category_data = self._build_tree(category, request) + # Build tree using mapping + category_data = self._build_tree_recursive(root, request, children_map, recursive_counts) grouped_data[sect_type]['categories'].append(category_data) - # Count total categories (simple loop, no queries) - total_count = self._count_tree_items(grouped_data) + # Count total categories + total_count = len(queryset) response_data = { 'count': total_count, @@ -129,48 +148,34 @@ class HadisCategoryTreeView(ListAPIView): return Response(response_data) - def _build_tree(self, category, request): + def _build_tree_recursive(self, category, request, children_map, recursive_counts): """ - Build tree from ALREADY PREFETCHED data - No database queries here! + Build tree from flat mapping """ + children = children_map.get(category.id, []) + + # Align fields with CategorySerializer 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, - 'hadis_count': category.hadis_count, # ← Use annotated value - 'children_count': category.children_count, # ← Use annotated value - 'has_hadis': category.hadis_count > 0, # ← Simple calculation + 'sect_id': category.sect_id, + 'sect_type': category.sect.sect_type, + 'hadis_count': category.direct_hadis_count, # Direct count + 'children_count': recursive_counts.get(category.id, 0), # Total in sub-tree (matches normal endpoint) + 'has_hadis': category.direct_hadis_count > 0, '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': [ - self._build_tree(child, request) - for child in category.children.all() # Already prefetched! + self._build_tree_recursive(child, request, children_map, recursive_counts) + for child in children ] } - def _count_tree_items(self, grouped_data): - """Count total items in tree""" - total = 0 - for sect_type_data in grouped_data.values(): - for item in sect_type_data['categories']: - total += 1 - total += self._count_children(item.get('children', [])) - return total - - def _count_children(self, children_list): - """Recursively count children""" - count = 0 - for item in children_list: - count += 1 - if item.get('children'): - count += self._count_children(item['children']) - return count - def _get_thumbnail_url(self, category, request): """Get absolute thumbnail URL""" if hasattr(category, 'thumbnail') and category.thumbnail: