Browse Source

feat: Implement Hadis data serializers for collections, detailed sync, lists, status, and tags.

master
Mohsen Taba 2 months ago
parent
commit
9dd4860a82
  1. 6
      apps/hadis/models/hadis.py
  2. 20
      apps/hadis/serializers/hadis.py
  3. 3
      apps/hadis/serializers/reference.py
  4. 12
      apps/hadis/urls.py
  5. 137
      apps/hadis/views/category.py

6
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 = [

20
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):

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

12
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'),

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

Loading…
Cancel
Save