H.shokhmgar86 2 weeks ago
parent
commit
593daabe76
  1. 62
      apps/hadis/serializers/category.py
  2. 223
      apps/hadis/serializers/hadis.py
  3. 158
      apps/hadis/serializers/serializers_admin.py
  4. 3
      apps/hadis/signals.py
  5. 5
      apps/hadis/urls.py
  6. 75
      apps/hadis/views/category.py
  7. 171
      apps/hadis/views/hadis.py
  8. 19
      apps/hadis/views_admin.py

62
apps/hadis/serializers/category.py

@ -225,20 +225,15 @@ class HadisCategorySelectSerializer(serializers.ModelSerializer):
return Hadis.objects.filter(category=obj, status=True).exists()
def get_children_count(self, obj):
# """Get count of active children categories that have children or hadis"""
# children = obj.get_children().filter(sect=obj.sect)
# return len(children)
"""
Calculates the total number of Hadiths in this category
and all its descendants (sub-categories).
"""
# 1. Get all descendants of this category (including itself)
if hasattr(obj, 'total_content_count'):
return obj.total_content_count
family_tree = obj.get_descendants(include_self=True)
return Hadis.objects.filter(category__in=family_tree, status=True).count()
# 2. Count all Hadiths that belong to any category in this tree
return Hadis.objects.filter(category__in=family_tree).count()
def get_hadis_count(self,obj):
return len(Hadis.objects.filter(category=obj))
def get_hadis_count(self, obj):
if hasattr(obj, 'active_hadis_count'):
return obj.active_hadis_count
return Hadis.objects.filter(category=obj, status=True).count()
@ -274,20 +269,15 @@ class HadisCategorySelectSourceSerializer(serializers.ModelSerializer):
return Hadis.objects.filter(category=obj, status=True).exists()
def get_children_count(self, obj):
# """Get count of active children categories that have children or hadis"""
# children = obj.get_children().filter(sect=obj.sect)
# return len(children)
"""
Calculates the total number of Hadiths in this category
and all its descendants (sub-categories).
"""
# 1. Get all descendants of this category (including itself)
if hasattr(obj, 'total_content_count'):
return obj.total_content_count
family_tree = obj.get_descendants(include_self=True)
return Hadis.objects.filter(category__in=family_tree, status=True).count()
# 2. Count all Hadiths that belong to any category in this tree
return Hadis.objects.filter(category__in=family_tree).count()
def get_hadis_count(self,obj):
return len(Hadis.objects.filter(category=obj))
def get_hadis_count(self, obj):
if hasattr(obj, 'active_hadis_count'):
return obj.active_hadis_count
return Hadis.objects.filter(category=obj, status=True).count()
class CategorySerializer(serializers.ModelSerializer):
sect_id = serializers.IntegerField(source='sect.id', read_only=True)
@ -310,22 +300,18 @@ class CategorySerializer(serializers.ModelSerializer):
return obj.parent_id is not None
def get_children_count(self, obj):
# """Get count of active children categories that have children or hadis"""
# children = obj.get_children().filter(sect=obj.sect)
# return len(children)
"""
Calculates the total number of Hadiths in this category
and all its descendants (sub-categories).
"""
# 1. Get all descendants of this category (including itself)
if hasattr(obj, 'total_content_count'):
return obj.total_content_count
family_tree = obj.get_descendants(include_self=True)
return Hadis.objects.filter(category__in=family_tree, status=True).count()
def get_has_hadis(self, obj):
return Hadis.objects.filter(category=obj, status=True).exists()
# 2. Count all Hadiths that belong to any category in this tree
return Hadis.objects.filter(category__in=family_tree).count()
def get_has_hadis(self,obj):
return Hadis.objects.filter(category=obj).exists()
def get_hadis_count(self,obj):
return len(Hadis.objects.filter(category=obj))
def get_hadis_count(self, obj):
if hasattr(obj, 'active_hadis_count'):
return obj.active_hadis_count
return Hadis.objects.filter(category=obj, status=True).count()
# def get_title(self,obj):
# # ✅ Get language from request

223
apps/hadis/serializers/hadis.py

@ -49,11 +49,12 @@ class PinnedHadisCollectionSerializer(serializers.ModelSerializer):
title = LocalizedField()
summary = LocalizedField()
thumbnail = serializers.SerializerMethodField()
hadis_ids = serializers.SerializerMethodField()
class Meta:
model = HadisCollection
# فیلدهایی که برای نمایش در بخش پین شده (مثل اسلایدر یا لیست بالا) نیاز هست
fields = ['id', 'title', 'slug', 'summary', 'thumbnail', 'order', 'created_at']
fields = ['id', 'title', 'slug', 'summary', 'thumbnail', 'order', 'created_at', 'hadis_ids']
def get_thumbnail(self, obj):
if obj.thumbnail:
@ -63,14 +64,29 @@ class PinnedHadisCollectionSerializer(serializers.ModelSerializer):
return absolute_https_url(obj.thumbnail.url)
return None
def get_hadis_ids(self, obj):
# دریافت شناسه احادیث فعال متعلق به این کالکشن به ترتیب
if hasattr(obj, '_prefetched_objects_cache') and 'hadis_items' in obj._prefetched_objects_cache:
items = [
item.hadis_id for item in obj.hadis_items.all()
if getattr(getattr(item, 'hadis', None), 'status', True)
]
return items
return list(
obj.hadis_items.filter(hadis__status=True)
.order_by('order')
.values_list('hadis_id', flat=True)
)
class HadisCollectionListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
title = LocalizedField()
summary = LocalizedField()
hadis_ids = serializers.SerializerMethodField()
class Meta:
model = HadisCollection
fields = ['id', 'title', 'summary','slug', 'thumbnail']
fields = ['id', 'title', 'summary', 'slug', 'thumbnail', 'hadis_ids']
def get_thumbnail(self, obj):
if obj.thumbnail:
@ -80,6 +96,20 @@ class HadisCollectionListSerializer(serializers.ModelSerializer):
return absolute_https_url(obj.thumbnail.url)
return None
def get_hadis_ids(self, obj):
# دریافت شناسه احادیث فعال متعلق به این کالکشن به ترتیب
if hasattr(obj, '_prefetched_objects_cache') and 'hadis_items' in obj._prefetched_objects_cache:
items = [
item.hadis_id for item in obj.hadis_items.all()
if getattr(getattr(item, 'hadis', None), 'status', True)
]
return items
return list(
obj.hadis_items.filter(hadis__status=True)
.order_by('order')
.values_list('hadis_id', flat=True)
)
class HadisSyncSerializer(serializers.ModelSerializer):
"""Serializer for syncing all hadis data (grouped fields)"""
@ -1379,9 +1409,21 @@ class NarratorRelationSerializer(serializers.ModelSerializer):
# --- سریالایزرهای کمکی تصاویر رفرنس ---
class CorrectionReferenceImageSerializer(serializers.ModelSerializer):
image = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
class Meta:
model = CorrectionReferenceImage
fields = ['id', 'image', 'priority']
fields = ['id', 'image', 'thumbnail', 'priority']
def get_image(self, obj):
if obj.image:
request = self.context.get('request')
return absolute_https_url(obj.image.url, request) if request else absolute_https_url(obj.image.url)
return None
def get_thumbnail(self, obj):
return self.get_image(obj)
class InterpretationReferenceImageSerializer(serializers.ModelSerializer):
class Meta:
@ -1417,11 +1459,22 @@ class BaseDetailedReferenceSerializer(serializers.ModelSerializer):
return []
def get_edition_info(self, obj):
if obj.edition:
edition = obj.edition
if not edition and getattr(obj, "book_volume", None) and obj.book_volume.edition:
edition = obj.book_volume.edition
if not edition and getattr(obj, "book_reference", None) and hasattr(obj.book_reference, "editions"):
edition = obj.book_reference.editions.first()
if edition:
request = self.context.get("request")
pub = edition.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
return {
"id": obj.edition.id,
"edition_number": obj.edition.edition_number,
"publisher": obj.edition.publisher
"id": edition.id,
"edition_number": edition.edition_number,
"publisher": pub or "",
"year_of_publication": edition.year_of_publication or "",
"number_of_volumes": edition.number_of_volumes,
}
return None
@ -1434,10 +1487,164 @@ class BaseDetailedReferenceSerializer(serializers.ModelSerializer):
return None
class DetailedCorrectionReferenceSerializer(BaseDetailedReferenceSerializer):
book = serializers.SerializerMethodField()
author = serializers.SerializerMethodField()
edition = serializers.SerializerMethodField()
open_book_url = serializers.SerializerMethodField()
citation_summary = serializers.SerializerMethodField()
images = CorrectionReferenceImageSerializer(many=True, read_only=True)
images_count = serializers.SerializerMethodField()
class Meta:
model = CorrectionReference
fields = ['id', 'book_title', 'book_authors', 'edition_info', 'volume_info', 'hadith_number', 'volume', 'pages', 'address', 'url', 'images']
fields = [
'id',
'book',
'author',
'edition',
'volume_info',
'hadith_number',
'volume',
'pages',
'address',
'url',
'open_book_url',
'citation_summary',
'images',
'images_count',
'book_title',
'book_authors',
'edition_info',
]
def get_book(self, obj):
if obj.book_reference:
request = self.context.get('request')
first_ed = obj.edition
if not first_ed and getattr(obj, "book_volume", None) and obj.book_volume.edition:
first_ed = obj.book_volume.edition
if not first_ed and hasattr(obj.book_reference, "editions"):
first_ed = obj.book_reference.editions.first()
pub = getattr(obj.book_reference, "publisher", None)
if pub is None and first_ed:
pub = first_ed.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
year = getattr(obj.book_reference, "year_of_publication", None)
if not year and first_ed:
year = first_ed.year_of_publication
num_vols = getattr(obj.book_reference, "number_of_volumes", None)
if num_vols is None and first_ed:
num_vols = first_ed.number_of_volumes
if num_vols is None and hasattr(obj.book_reference, "volumes"):
num_vols = obj.book_reference.volumes.count() or None
return {
'id': obj.book_reference.id,
'slug': obj.book_reference.slug,
'title': get_localized_text(obj.book_reference.title, request),
'publisher': pub or '',
'year_of_publication': year or '',
'number_of_volumes': num_vols,
}
return None
def get_author(self, obj):
if obj.book_reference and obj.book_reference.author:
author = obj.book_reference.author
request = self.context.get('request')
return {
'id': author.id,
'slug': author.slug,
'name': get_localized_text(author.name, request)
}
return None
def get_edition(self, obj):
edition = obj.edition
if not edition and getattr(obj, "book_volume", None) and obj.book_volume.edition:
edition = obj.book_volume.edition
if not edition and getattr(obj, "book_reference", None) and hasattr(obj.book_reference, "editions"):
edition = obj.book_reference.editions.first()
if edition:
request = self.context.get('request')
pub = edition.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
return {
'id': edition.id,
'edition_number': edition.edition_number,
'publisher': pub or '',
'year_of_publication': edition.year_of_publication or '',
'number_of_volumes': edition.number_of_volumes,
}
return None
def get_open_book_url(self, obj):
request = self.context.get('request')
if obj.url:
return obj.url
if obj.book_volume and obj.book_volume.file:
return absolute_https_url(obj.book_volume.file.url, request) if request else obj.book_volume.file.url
if obj.edition and obj.edition.source_url:
return obj.edition.source_url
if obj.book_reference and obj.book_reference.slug:
return f"/arguments/sources/{obj.book_reference.slug}"
return None
volume = serializers.SerializerMethodField()
pages = serializers.SerializerMethodField()
hadith_number = serializers.SerializerMethodField()
def get_volume(self, obj):
if obj.volume and str(obj.volume).strip().lower() != "none":
return obj.volume
if obj.book_volume and obj.book_volume.title:
return obj.book_volume.title
return None
def get_pages(self, obj):
if obj.pages and str(obj.pages).strip().lower() != "none":
return obj.pages
return None
def get_hadith_number(self, obj):
if obj.hadith_number and str(obj.hadith_number).strip().lower() != "none":
return obj.hadith_number
return None
def get_citation_summary(self, obj):
parts = []
vol = self.get_volume(obj)
pg = self.get_pages(obj)
num = self.get_hadith_number(obj)
if vol:
vol_str = str(vol).strip()
if vol_str.startswith("جلد"):
parts.append(vol_str)
else:
parts.append(f"جلد {vol_str}")
if pg:
pg_str = str(pg).strip()
if pg_str.startswith("ص") or pg_str.startswith("صفحه"):
parts.append(pg_str)
else:
parts.append(f"صفحه {pg_str}")
if num:
num_str = str(num).strip()
if num_str.startswith("ح") or num_str.startswith("حدیث") or num_str.startswith("شماره"):
parts.append(num_str)
else:
parts.append(f"شماره حدیث {num_str}")
return "".join(parts) if parts else ""
def get_images_count(self, obj):
if hasattr(obj, '_prefetched_objects_cache') and 'images' in obj._prefetched_objects_cache:
return len(obj.images.all())
return obj.images.count()
class DetailedInterpretationReferenceSerializer(BaseDetailedReferenceSerializer):
images = InterpretationReferenceImageSerializer(many=True, read_only=True)

158
apps/hadis/serializers/serializers_admin.py

@ -13,7 +13,7 @@ from ..models import (
OriginalTextReference, OriginalTextReferenceImage,
ContentRelease
)
from .category import LocalizedField
from .category import LocalizedField, get_localized_text
def safe_copy_data(data):
@ -489,11 +489,163 @@ def save_relation_reference_data(instance, validated_data, request=None, is_corr
InterpretationReferenceImage.objects.create(reference=ref, image=img)
class AdminCorrectionReferenceSerializer(serializers.ModelSerializer):
book_reference_detail = serializers.SerializerMethodField(read_only=True)
edition_detail = serializers.SerializerMethodField(read_only=True)
book_volume_detail = serializers.SerializerMethodField(read_only=True)
images = serializers.SerializerMethodField(read_only=True)
delete_image_ids = serializers.ListField(
child=serializers.IntegerField(),
required=False,
write_only=True
)
class Meta:
model = CorrectionReference
fields = [
"id",
"correction",
"book_reference",
"book_reference_detail",
"edition",
"edition_detail",
"book_volume",
"book_volume_detail",
"hadith_number",
"volume",
"pages",
"address",
"url",
"images",
"delete_image_ids",
]
def get_book_reference_detail(self, obj):
if obj.book_reference:
request = self.context.get("request")
author = obj.book_reference.author
first_ed = obj.book_reference.editions.first() if hasattr(obj.book_reference, "editions") else None
pub = getattr(obj.book_reference, "publisher", None)
if pub is None and first_ed:
pub = first_ed.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
year = getattr(obj.book_reference, "year_of_publication", None)
if not year and first_ed:
year = first_ed.year_of_publication
num_vols = getattr(obj.book_reference, "number_of_volumes", None)
if num_vols is None and first_ed:
num_vols = first_ed.number_of_volumes
return {
"id": obj.book_reference.id,
"title": get_localized_text(obj.book_reference.title, request),
"slug": obj.book_reference.slug,
"publisher": pub or "",
"year_of_publication": year or "",
"number_of_volumes": num_vols,
"has_editions": getattr(obj.book_reference, "has_editions", False),
"author": {
"id": author.id,
"name": get_localized_text(author.name, request),
"slug": author.slug
} if author else None
}
return None
def get_edition_detail(self, obj):
if obj.edition:
request = self.context.get("request")
pub = obj.edition.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
return {
"id": obj.edition.id,
"edition_number": obj.edition.edition_number,
"publisher": pub,
"year_of_publication": obj.edition.year_of_publication,
"number_of_volumes": obj.edition.number_of_volumes,
}
return None
def get_book_volume_detail(self, obj):
if obj.book_volume:
return {
"id": obj.book_volume.id,
"title": obj.book_volume.title,
}
return None
def get_images(self, obj):
request = self.context.get("request")
return [
{
"id": img.id,
"image": absolute_https_url(img.image.url, request) if request and img.image else (img.image.url if img.image else None),
"thumbnail": absolute_https_url(img.image.url, request) if request and img.image else (img.image.url if img.image else None),
"priority": img.priority,
}
for img in obj.images.all()
]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
book_ref = data.get("book_reference")
if book_ref in ("", "none", "null"):
data["book_reference"] = None
edition = data.get("edition")
if edition in ("", "none", "null"):
data["edition"] = None
book_vol = data.get("book_volume")
if book_vol in ("", "none", "null"):
data["book_volume"] = None
delete_ids = data.get("delete_image_ids")
if delete_ids:
if isinstance(delete_ids, str):
try:
data["delete_image_ids"] = json.loads(delete_ids)
except ValueError:
data["delete_image_ids"] = [int(x) for x in delete_ids.split(",") if x.strip().isdigit()]
elif isinstance(delete_ids, list):
parsed = []
for x in delete_ids:
if isinstance(x, str) and x.strip().isdigit():
parsed.append(int(x))
elif isinstance(x, int):
parsed.append(x)
data["delete_image_ids"] = parsed
return super().to_internal_value(data)
def create(self, validated_data):
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().create(validated_data)
self._handle_images(instance, delete_image_ids)
return instance
def update(self, instance, validated_data):
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().update(instance, validated_data)
self._handle_images(instance, delete_image_ids)
return instance
def _handle_images(self, instance, delete_image_ids):
request = self.context.get("request")
if delete_image_ids:
instance.images.filter(id__in=delete_image_ids).delete()
if request:
new_images = request.FILES.getlist("uploaded_images") or request.FILES.getlist("uploaded_images[]") or request.FILES.getlist("images")
for img in new_images:
CorrectionReferenceImage.objects.create(reference=instance, image=img)
class AdminHadisCorrectionSerializer(serializers.ModelSerializer):
hadis_detail = AdminHadisBriefSerializer(source="hadis", read_only=True)
bookmark_count = serializers.IntegerField(source="annotated_bookmark_count", read_only=True)
references = AdminCorrectionReferenceSerializer(many=True, read_only=True)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
@ -529,10 +681,11 @@ class AdminHadisCorrectionSerializer(serializers.ModelSerializer):
"book_reference_id",
"address",
"delete_image_ids",
"references",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "bookmark_count", "created_at", "updated_at"]
read_only_fields = ["id", "slug", "bookmark_count", "references", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
@ -586,6 +739,7 @@ class AdminHadisCorrectionSerializer(serializers.ModelSerializer):
}
for img in images
]
ret["references"] = AdminCorrectionReferenceSerializer(instance.references.all(), many=True, context=self.context).data
return ret
def create(self, validated_data):

3
apps/hadis/signals.py

@ -12,7 +12,8 @@ TARGET_MODELS = [
Hadis, HadisCategory, HadisCollection, HadisCorrection, HadisInCollection, HadisReference,
HadisSect, HadisStatus, HadisTag, HadisTransmitter, Transmitters, TransmitterOpinion, TransmitterReliability,
TransmitterOriginalText, ReferenceImage, BookReference, BookAttribute, BookAuthor, BookReferenceImage,
NarratorLayer, OpinionStatus, ContentRelease
NarratorLayer, OpinionStatus, ContentRelease,
CorrectionReference, BookEdition, BookVolume, CorrectionReferenceImage
]

5
apps/hadis/urls.py

@ -14,7 +14,7 @@ from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncVi
from .views.version import ContentReleaseSyncView
from .views.info import HadisInfoView
from .views_admin import (
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminCorrectionReferenceViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookTagViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminHadisReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet,
AdminTransmitterViewSet, AdminTransmitterReliabilityViewSet, DovodiBookSubjectAreaViewSet, DovodiBookTypeViewSet,
DovodiNarratorLayerViewSet, DovodiTransmitterReliabilityViewSet, DovodiOpinionStatusViewSet,
@ -37,6 +37,7 @@ admin_router.register(r'quran-verses', AdminCategoryQuranVerseViewSet, basename=
admin_router.register(r'dovodi-sects', DovodiHadisSectViewSet, basename='dovodi-hadis-sects')
admin_router.register(r'dovodi-categories', DovodiHadisCategoryViewSet, basename='dovodi-hadis-categories')
admin_router.register(r'corrections', AdminHadisCorrectionViewSet, basename='admin-hadis-corrections')
admin_router.register(r'correction-references', AdminCorrectionReferenceViewSet, basename='admin-correction-references')
admin_router.register(r'interpretations', AdminHadisInterpretationViewSet, basename='admin-hadis-interpretations')
admin_router.register(r'statuses', AdminHadisStatusViewSet, basename='admin-hadis-statuses')
admin_router.register(r'tags', AdminHadisTagViewSet, basename='admin-hadis-tags')
@ -141,7 +142,7 @@ urlpatterns = [
# Interpretation Detail Endpoint (تفسیر بر اساس آی‌دی)
path('interpretations/<str:interpretation_slug>/', cached_view(HadisInterpretationDetailView.as_view()), name='interpretation-detail'),
# Correction Detail Endpoint (تصحیح بر اساس اسلاگ)
path('corrections/<str:correction_slug>/', cached_view(HadisCorrectionDetailView.as_view()), name='correction-detail'),
path('corrections/<str:correction_slug>/', HadisCorrectionDetailView.as_view(), name='correction-detail'),
# Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ)
path('original-texts/<str:original_text_slug>/', cached_view(TransmitterOriginalTextDetailView.as_view()), name='original-text-detail'),

75
apps/hadis/views/category.py

@ -266,6 +266,57 @@ class HadisCategoryTreeNormalView(ListAPIView):
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):
"""
@ -274,7 +325,6 @@ class HadisCategorySelectBySectView(ListAPIView):
"""
serializer_class = HadisCategorySelectSerializer
pagination_class = StandardResultsSetPagination
@categories_tree_by_sect_swagger
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
@ -323,7 +373,7 @@ class HadisCategorySelectBySectView(ListAPIView):
from django.db.models import Count, Q
# Return children of this category that have either active hadiths, children, or are quran
return HadisCategory.objects.filter(
queryset = HadisCategory.objects.filter(
parent=parent_category,
sect__sect_type=sect_type,
sect__is_active=True
@ -332,7 +382,8 @@ class HadisCategorySelectBySectView(ListAPIView):
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('order')
)
return apply_category_ordering_and_counts(queryset, self.request)
class HadisCategorySelectBySectSourceView(ListAPIView):
@ -391,7 +442,7 @@ class HadisCategorySelectBySectSourceView(ListAPIView):
from django.db.models import Count, Q
# Return children of this category, filtered by source_type and excluding empty leaves
return HadisCategory.objects.filter(
queryset = HadisCategory.objects.filter(
parent=parent_category,
sect__sect_type=sect_type,
sect__is_active=True,
@ -401,7 +452,8 @@ class HadisCategorySelectBySectSourceView(ListAPIView):
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('order')
)
return apply_category_ordering_and_counts(queryset, self.request)
class CategoriesView(ListAPIView):
"""
@ -415,14 +467,21 @@ class CategoriesView(ListAPIView):
def get_queryset(self):
from django.db.models import Count, Q
return HadisCategory.objects.filter(
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)
).order_by('order', 'id')
)
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)
@ -451,7 +510,7 @@ class CategoriesBySectView(ListAPIView):
elif is_root in ['false', '0']:
queryset = queryset.filter(parent__isnull=False)
return queryset.order_by('order', 'id')
return apply_category_ordering_and_counts(queryset, self.request)
@categories_by_sect_swagger
def get(self, request, *args, **kwargs):

171
apps/hadis/views/hadis.py

@ -10,7 +10,7 @@ from rest_framework.response import Response
from django.db.models import Count
from django.db.models import Prefetch
from ..serializers.category import get_localized_text
from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage
from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage, CorrectionReference, HadisInterpretation, InterpretationReference
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer
from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger
from django.db.models import Q
@ -33,10 +33,17 @@ class PinnedHadisCollectionListView(ListAPIView):
return self.list(request, *args, **kwargs)
def get_queryset(self):
from django.db.models import Prefetch
from apps.hadis.models import HadisInCollection
return HadisCollection.objects.filter(
status=True,
display_position=HadisCollection.DisplayPosition.PINNED,
hadis_items__hadis__status=True
).prefetch_related(
Prefetch(
'hadis_items',
queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order')
)
).distinct().order_by('order', '-created_at')
def list(self, request, *args, **kwargs):
@ -100,9 +107,16 @@ class HadisCollectionListView(ListAPIView):
return self.list(request, *args, **kwargs)
def get_queryset(self):
from django.db.models import Prefetch
from apps.hadis.models import HadisInCollection
return HadisCollection.objects.filter(
status=True,
display_position=HadisCollection.DisplayPosition.MIDDLE
).prefetch_related(
Prefetch(
'hadis_items',
queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order')
)
).order_by('order', '-created_at')
@ -143,24 +157,44 @@ class HadisSyncView(ListAPIView):
)
)
),
# 👇 اضافه شدن واکشی عمیق برای تصحیحات
# 👇 اضافه شدن واکشی عمیق برای تصحیحات (بدون N+1)
Prefetch(
'hadiscorrection_set',
queryset=HadisCorrection.objects.select_related('hadis').prefetch_related(
'references__book_reference__author',
'references__edition',
'references__book_volume',
'references__images'
Prefetch(
'references',
queryset=CorrectionReference.objects.select_related(
'book_reference',
'book_reference__author',
'edition',
'book_volume',
'book_volume__edition'
).prefetch_related(
'images',
'book_reference__editions',
'book_reference__volumes'
)
)
)
),
# 👇 اضافه شدن واکشی عمیق برای تفاسیر
# 👇 اضافه شدن واکشی عمیق برای تفاسیر (بدون N+1)
Prefetch(
'category__interpretations',
queryset=HadisInterpretation.objects.select_related('category').prefetch_related(
'references__book_reference__author',
'references__edition',
'references__book_volume',
'references__images'
Prefetch(
'references',
queryset=InterpretationReference.objects.select_related(
'book_reference',
'book_reference__author',
'edition',
'book_volume',
'book_volume__edition'
).prefetch_related(
'images',
'book_reference__editions',
'book_reference__volumes'
)
)
)
),
)
@ -670,11 +704,13 @@ class CategoryHadisCorrectionsView(ListAPIView):
@swagger_auto_schema(
operation_summary="Get Category Hadis Corrections",
operation_description="Returns all text corrections across all hadiths belonging to a specific category, including hadis_info for each correction.",
operation_description="Returns all text corrections across all hadiths belonging to a specific category, including hadis_info for each correction. Supports filtering by multiple hadiths and/or categories.",
tags=['Dobodbi - Hadis (V2)'],
manual_parameters=[
openapi.Parameter('search', openapi.IN_QUERY, description="Search in text or narrator", type=openapi.TYPE_STRING),
openapi.Parameter('hadis_slug', openapi.IN_QUERY, description="Filter corrections by specific hadith slug", type=openapi.TYPE_STRING),
openapi.Parameter('hadis_slug', openapi.IN_QUERY, description="Filter corrections by specific hadith slug (comma-separated for multiple)", type=openapi.TYPE_STRING),
openapi.Parameter('hadis_slugs', openapi.IN_QUERY, description="Filter corrections by multiple hadith slugs (comma-separated)", type=openapi.TYPE_STRING),
openapi.Parameter('category_slugs', openapi.IN_QUERY, description="Filter corrections by multiple category slugs (comma-separated)", type=openapi.TYPE_STRING),
openapi.Parameter('is_bookmark', openapi.IN_QUERY, description="Filter only bookmarked corrections (requires auth)", type=openapi.TYPE_BOOLEAN),
]
)
@ -684,12 +720,52 @@ class CategoryHadisCorrectionsView(ListAPIView):
def get_queryset(self):
category_slug = self.kwargs.get('category_slug')
try:
category_ids = []
if category_slug:
category = HadisCategory.objects.get(slug=category_slug)
category_ids = category.get_descendants(include_self=True).values_list('id', flat=True)
hadis_ids = Hadis.objects.filter(
category_id__in=category_ids,
status=True
).values_list('id', flat=True)
category_ids = list(category.get_descendants(include_self=True).values_list('id', flat=True))
# Optional filter by multiple categories (selected subcategories or category slugs/IDs)
cat_param_keys = ['category_slugs', 'categories', 'category_slug']
cat_slug_list = []
for key in cat_param_keys:
vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else []
if not vals and key in self.request.query_params:
val = self.request.query_params.get(key)
if val:
vals = [val] if isinstance(val, str) else list(val)
for v in vals:
if isinstance(v, str):
cat_slug_list.extend([s.strip() for s in v.split(',') if s.strip()])
elif isinstance(v, (list, tuple)):
cat_slug_list.extend(v)
elif v is not None:
cat_slug_list.append(str(v))
if cat_slug_list:
cat_filter = Q(slug__in=cat_slug_list)
id_list = [int(s) for s in cat_slug_list if str(s).isdigit()]
if id_list:
cat_filter |= Q(id__in=id_list)
selected_categories = HadisCategory.objects.filter(cat_filter)
selected_cat_ids = set()
for sc in selected_categories:
selected_cat_ids.update(sc.get_descendants(include_self=True).values_list('id', flat=True))
if selected_cat_ids:
if category_ids:
category_ids = list(set(category_ids).intersection(selected_cat_ids))
else:
category_ids = list(selected_cat_ids)
else:
return HadisCorrection.objects.none()
hadis_filter_kwargs = {'status': True}
if category_ids:
hadis_filter_kwargs['category_id__in'] = category_ids
hadis_ids = Hadis.objects.filter(**hadis_filter_kwargs).values_list('id', flat=True)
queryset = HadisCorrection.objects.filter(
hadis_id__in=hadis_ids
@ -711,10 +787,30 @@ class CategoryHadisCorrectionsView(ListAPIView):
)
queryset = queryset.filter(search_conditions)
# Optional filter by specific hadith slug within this category
hadis_slug = self.request.query_params.get('hadis_slug', None)
if hadis_slug:
queryset = queryset.filter(hadis__slug=hadis_slug)
# Optional filter by specific hadith slug(s) or ID(s)
hadis_param_keys = ['hadis_slug', 'hadis_slugs', 'hadiths']
hadis_slug_list = []
for key in hadis_param_keys:
vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else []
if not vals and key in self.request.query_params:
val = self.request.query_params.get(key)
if val:
vals = [val] if isinstance(val, str) else list(val)
for v in vals:
if isinstance(v, str):
hadis_slug_list.extend([s.strip() for s in v.split(',') if s.strip()])
elif isinstance(v, (list, tuple)):
hadis_slug_list.extend(v)
elif v is not None:
hadis_slug_list.append(str(v))
if hadis_slug_list:
hadis_query = Q(hadis__slug__in=hadis_slug_list)
id_list = [int(s) for s in hadis_slug_list if str(s).isdigit()]
if id_list:
hadis_query |= Q(hadis__id__in=id_list)
queryset = queryset.filter(hadis_query)
# Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
@ -734,15 +830,44 @@ class CategoryHadisCorrectionsView(ListAPIView):
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
category_slug = self.kwargs.get('category_slug')
category_obj = HadisCategory.objects.filter(slug=category_slug).first()
category_obj = HadisCategory.objects.filter(slug=category_slug).first() if category_slug else None
category_data = SimpleCategory(category_obj, context={'request': request}).data if category_obj else None
available_hadiths = []
available_categories = []
if category_obj:
cat_ids = category_obj.get_descendants(include_self=True).values_list('id', flat=True)
hadiths_in_cat = Hadis.objects.filter(
category_id__in=cat_ids,
status=True
).order_by('number', 'id')
for h in hadiths_in_cat:
h_title = get_localized_text(h.title, request) if hasattr(h, 'title') and h.title else f"Hadith {h.number}"
available_hadiths.append({
"id": h.id,
"number": h.number,
"slug": h.slug,
"title": h_title,
})
subcats = category_obj.get_children() if hasattr(category_obj, 'get_children') else category_obj.children.all()
for sc in subcats:
sc_title = get_localized_text(sc.title, request) if hasattr(sc, 'title') and sc.title else sc.slug
available_categories.append({
"id": sc.id,
"slug": sc.slug,
"title": sc_title,
})
if isinstance(response.data, dict):
ordered_data = {
'count': response.data.get('count', 0),
'next': response.data.get('next'),
'previous': response.data.get('previous'),
'current_category': category_data,
'available_hadiths': available_hadiths,
'available_categories': available_categories,
'results': response.data.get('results', []),
}
response.data = ordered_data

19
apps/hadis/views_admin.py

@ -13,6 +13,7 @@ from .models import (
Hadis, HadisCategory, HadisCollection, HadisInCollection, HadisCorrection, HadisInterpretation, HadisStatus, HadisTag, HadisSect, HadisTransmitter, HadisReference, ReferenceImage,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText,
CorrectionReference,
ContentRelease
)
from .serializers.serializers_admin import (
@ -21,6 +22,7 @@ from .serializers.serializers_admin import (
AdminHadisCategoryArchiveSerializer,
AdminHadisCategoryArchiveDetailSerializer,
AdminHadisCorrectionSerializer,
AdminCorrectionReferenceSerializer,
AdminHadisDetailSerializer,
AdminHadisInterpretationSerializer,
AdminHadisCollectionSerializer,
@ -425,6 +427,23 @@ class AdminHadisCorrectionViewSet(ModelViewSet):
return Response({"status": "success"})
class AdminCorrectionReferenceViewSet(ModelViewSet):
serializer_class = AdminCorrectionReferenceSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
parser_classes = (MultiPartParser, FormParser, JSONParser)
pagination_class = OptionalPagination
def get_queryset(self):
qs = CorrectionReference.objects.all().select_related(
"book_reference__author", "edition", "book_volume"
).prefetch_related("images")
correction_id = self.request.query_params.get("correction_id")
if correction_id:
qs = qs.filter(correction_id=correction_id)
return qs.order_by("id")
class AdminHadisInterpretationViewSet(ModelViewSet):
serializer_class = AdminHadisInterpretationSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]

Loading…
Cancel
Save