10 Commits

  1. 29
      apps/hadis/migrations/0045_hadisreference_address_hadisreference_book_volume_and_more.py
  2. 18
      apps/hadis/migrations/0046_originaltextreference_hadith_number.py
  3. 27
      apps/hadis/models/hadis.py
  4. 11
      apps/hadis/models/transmitter.py
  5. 13
      apps/hadis/serializers/category.py
  6. 276
      apps/hadis/serializers/hadis.py
  7. 631
      apps/hadis/serializers/serializers_admin.py
  8. 14
      apps/hadis/urls.py
  9. 22
      apps/hadis/views/category.py
  10. 180
      apps/hadis/views/hadis.py
  11. 21
      apps/hadis/views/transmitter.py
  12. 56
      apps/hadis/views_admin.py

29
apps/hadis/migrations/0045_hadisreference_address_hadisreference_book_volume_and_more.py

@ -0,0 +1,29 @@
# Generated by Django 4.2.30 on 2026-10-06 10:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('hadis', '0044_alter_hadiscategory_description_and_more'),
]
operations = [
migrations.AddField(
model_name='hadisreference',
name='address',
field=models.TextField(blank=True, null=True, verbose_name='Address'),
),
migrations.AddField(
model_name='hadisreference',
name='book_volume',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hadis_references', to='hadis.bookvolume', verbose_name='Volume Record'),
),
migrations.AddField(
model_name='hadisreference',
name='edition',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='hadis_references', to='hadis.bookedition', verbose_name='Edition'),
),
]

18
apps/hadis/migrations/0046_originaltextreference_hadith_number.py

@ -0,0 +1,18 @@
# Generated by Django 4.2.30 on 2026-10-06 13:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0045_hadisreference_address_hadisreference_book_volume_and_more'),
]
operations = [
migrations.AddField(
model_name='originaltextreference',
name='hadith_number',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='Hadith / Entry Number'),
),
]

27
apps/hadis/models/hadis.py

@ -184,7 +184,15 @@ class Hadis(LowercaseSlugMixin, models.Model):
embedded_in = models.JSONField(default=list, blank=True)
def __str__(self):
title = self.title[0]['text'] if self.title else f"Hadis {self.number}"
title = None
if self.title:
first_item = self.title[0] if isinstance(self.title, list) and self.title else self.title
if isinstance(first_item, dict):
title = first_item.get('text') or first_item.get('value') or first_item.get('title')
elif isinstance(first_item, str):
title = first_item
if not title:
title = f"Hadis {self.number}"
return f"{self.number} - {title}"
def clean(self):
@ -307,8 +315,25 @@ class HadisReference(models.Model):
verbose_name=_('book reference'),
related_name='hadis_references'
)
edition = models.ForeignKey(
'hadis.BookEdition',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Edition'),
related_name='hadis_references'
)
book_volume = models.ForeignKey(
'hadis.BookVolume',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Volume Record'),
related_name='hadis_references'
)
volume = models.CharField(max_length=100, null=True, blank=True)
pages = models.CharField(max_length=100, null=True, blank=True)
address = models.TextField(null=True, blank=True, verbose_name=_('Address'))
url = models.URLField(max_length=1000, null=True, blank=True)
hadith_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Hadith Number in Book'))

11
apps/hadis/models/transmitter.py

@ -510,7 +510,15 @@ class TransmitterOriginalText(models.Model):
ordering = ('order', 'id')
def __str__(self):
title = self.title[0]['text'] if self.title else "Untitled"
title = None
if self.title:
first_item = self.title[0] if isinstance(self.title, list) and self.title else self.title
if isinstance(first_item, dict):
title = first_item.get('text') or first_item.get('value') or first_item.get('title')
elif isinstance(first_item, str):
title = first_item
if not title:
title = "Untitled"
return f"{title} by {self.transmitter}"
def save(self, *args, **kwargs):
@ -582,6 +590,7 @@ class OriginalTextReference(models.Model):
edition = models.ForeignKey('hadis.BookEdition', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Edition'))
book_volume = models.ForeignKey('hadis.BookVolume', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Volume Record'))
hadith_number = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Hadith / Entry Number'))
# فیلدهای متنی قدیمی برای سادگی یا مواردی که هنوز ولوم ساخته نشده است
volume = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Volume Text'))
pages = models.CharField(max_length=100, null=True, blank=True) # 👈 تغییر page به pages

13
apps/hadis/serializers/category.py

@ -36,7 +36,18 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod
# Get target language if not provided
if not language_code:
language_code = getattr(request, "LANGUAGE_CODE", None) if request else None
if request:
query_params = getattr(request, "query_params", None) or getattr(request, "GET", None)
if query_params:
language_code = query_params.get("language_code") or query_params.get("lang")
if not language_code:
language_code = getattr(request, "LANGUAGE_CODE", None)
if not language_code and hasattr(request, "headers"):
language_code = request.headers.get("Accept-Language")
elif not language_code and hasattr(request, "META"):
language_code = request.META.get("HTTP_ACCEPT_LANGUAGE")
if language_code:
language_code = language_code.split(",")[0].split(";")[0].split("-")[0].strip().lower()
if not language_code:
language_code = get_language() or fallback_lang

276
apps/hadis/serializers/hadis.py

@ -304,16 +304,27 @@ class HadisSyncSerializer(serializers.ModelSerializer):
).data
return []
class HadisTagSerializer(serializers.ModelSerializer):
"""Serializer for HadisTag"""
title = LocalizedField()
class Meta:
model = HadisTag
fields = ['id', 'title']
class HadisListSerializer(serializers.ModelSerializer):
"""Serializer for Hadis list"""
category = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
bookmark = serializers.SerializerMethodField()
tags = HadisTagSerializer(many=True, read_only=True)
translation = LocalizedField()
title = LocalizedField()
title_narrator = LocalizedField()
address = serializers.SerializerMethodField()
links = serializers.SerializerMethodField()
references = serializers.SerializerMethodField()
reference_images = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
@ -321,7 +332,11 @@ class HadisListSerializer(serializers.ModelSerializer):
model = Hadis
fields = ['id', 'number', 'slug', 'title', 'title_narrator', 'text',
'translation', 'category', 'status', 'bookmark', 'share_link',
'address', 'links', 'reference_images', 'images']
'address', 'links', 'references', 'reference_images', 'images', 'tags']
def get_references(self, obj):
refs = obj.references.all()
return DetailedHadisReferenceSerializer(refs, many=True, context=self.context).data
def get_address(self, obj):
formatted = format_address_output(obj.address)
@ -420,15 +435,6 @@ class HadisStatusSerializer(serializers.ModelSerializer):
fields = ['id', 'title', 'color', 'main_color_code', 'description']
class HadisTagSerializer(serializers.ModelSerializer):
"""Serializer for HadisTag"""
title = LocalizedField()
class Meta:
model = HadisTag
fields = ['id', 'title']
class TransmitterSerializer(serializers.ModelSerializer):
"""Serializer for Transmitters"""
full_name = LocalizedField()
@ -1035,33 +1041,181 @@ class HadisTransmitterListSerializer(serializers.ModelSerializer):
class ReferenceImageSerializer(serializers.ModelSerializer):
"""Serializer for ReferenceImage"""
thumbnail = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
class Meta:
model = ReferenceImage
fields = ['id', 'thumbnail', 'priority']
fields = ['id', 'thumbnail', 'image', 'priority']
def get_thumbnail(self, obj):
"""Get thumbnail URL"""
if obj.thumbnail:
img = getattr(obj, 'thumbnail', None) or getattr(obj, 'image', None)
if img:
request = self.context.get('request')
if request:
return absolute_https_url(obj.thumbnail.url, request)
return absolute_https_url(obj.thumbnail.url)
return absolute_https_url(img.url, request)
return absolute_https_url(img.url)
return None
def get_image(self, obj):
return self.get_thumbnail(obj)
class HadisReferenceSerializer(serializers.ModelSerializer):
"""Serializer for HadisReference with book and images"""
"""Serializer for HadisReference with book, author, edition, volume, and images"""
book_title = serializers.SerializerMethodField()
book_authors = serializers.SerializerMethodField()
book_description = serializers.SerializerMethodField()
share_link = serializers.CharField(source='book_reference.share_link', read_only=True)
edition = serializers.SerializerMethodField()
edition_info = serializers.SerializerMethodField()
volume_info = serializers.SerializerMethodField()
volume = serializers.SerializerMethodField()
pages = serializers.SerializerMethodField()
hadith_number = serializers.SerializerMethodField()
citation_summary = serializers.SerializerMethodField()
open_book_url = serializers.SerializerMethodField()
book = serializers.SerializerMethodField()
author = serializers.SerializerMethodField()
images = ReferenceImageSerializer(many=True, read_only=True)
images_count = serializers.SerializerMethodField()
class Meta:
model = HadisReference
fields = [
'id', 'book_title', 'book_description','book_authors','share_link'
'id', 'book', 'author', 'book_title', 'book_description', 'book_authors', 'share_link',
'edition', 'edition_info', 'volume_info', 'volume', 'pages', 'hadith_number',
'address', 'url', 'open_book_url', 'citation_summary', 'images', 'images_count'
]
def get_edition_info(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_volume_info(self, obj):
if obj.book_volume:
return {
"id": obj.book_volume.id,
"title": obj.book_volume.title
}
return None
def get_edition(self, obj):
return self.get_edition_info(obj)
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_open_book_url(self, obj):
request = self.context.get('request')
if obj.book_volume and obj.book_volume.file:
return absolute_https_url(obj.book_volume.file.url, request) if request else absolute_https_url(obj.book_volume.file.url)
return obj.url or None
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
return {
'id': obj.book_reference.id,
'slug': obj.book_reference.slug,
'title': get_localized_text(obj.book_reference.title, request) if isinstance(obj.book_reference.title, list) else obj.book_reference.title,
'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) if isinstance(author.name, list) else author.name
}
return None
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()
# def get_type_name(self, obj):
# """Get localized type name from book_reference.type"""
# if obj.book_reference and obj.book_reference.type:
@ -1634,17 +1788,81 @@ class DetailedCorrectionReferenceSerializer(BaseDetailedReferenceSerializer):
return len(obj.images.all())
return obj.images.count()
class DetailedInterpretationReferenceSerializer(BaseDetailedReferenceSerializer):
class DetailedInterpretationReferenceSerializer(DetailedCorrectionReferenceSerializer):
images = InterpretationReferenceImageSerializer(many=True, read_only=True)
class Meta:
model = InterpretationReference
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',
]
class DetailedOriginalTextReferenceSerializer(BaseDetailedReferenceSerializer):
class DetailedOriginalTextReferenceSerializer(DetailedCorrectionReferenceSerializer):
images = OriginalTextReferenceImageSerializer(many=True, read_only=True)
class Meta:
model = OriginalTextReference
fields = ['id', 'book_title', 'book_authors', 'edition_info', 'volume_info', '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',
]
class DetailedHadisReferenceSerializer(DetailedCorrectionReferenceSerializer):
images = ReferenceImageSerializer(many=True, read_only=True)
class Meta:
model = HadisReference
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',
]
# --- سریالایزرهای اصلی جزئیات (Detail Serializers) ---
@ -1778,7 +1996,7 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
class HadisSourceDetailsSerializer(serializers.ModelSerializer):
"""
Lightweight serializer for Hadis Source Details (Address, Links, Images).
Lightweight serializer for Hadis Source Details (Address, Links, Images, Reference).
Used on-demand when opening source detail modals in web and mobile clients.
"""
title = LocalizedField()
@ -1787,12 +2005,16 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
address = serializers.SerializerMethodField()
links = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
reference = serializers.SerializerMethodField()
references = serializers.SerializerMethodField()
tags = HadisTagSerializer(many=True, read_only=True)
class Meta:
model = Hadis
fields = [
'id', 'slug', 'title', 'narrator', 'source_type',
'address', 'links', 'images', 'share_link'
'address', 'links', 'images', 'share_link',
'reference', 'references', 'tags'
]
def get_source_type(self, obj):
@ -1834,3 +2056,13 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
})
return images_list
def get_reference(self, obj):
ref = obj.references.first()
if ref:
return DetailedHadisReferenceSerializer(ref, context=self.context).data
return None
def get_references(self, obj):
refs = obj.references.all()
return DetailedHadisReferenceSerializer(refs, many=True, context=self.context).data

631
apps/hadis/serializers/serializers_admin.py

@ -245,6 +245,174 @@ class AdminHadisCollectionSerializer(serializers.ModelSerializer):
return super().update(instance, validated_data)
class AdminHadisReferenceSerializer(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 = HadisReference
fields = [
"id",
"hadis",
"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) if isinstance(obj.book_reference.title, list) else obj.book_reference.title,
"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) if isinstance(author.name, list) else author.name,
"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 or "",
"year_of_publication": obj.edition.year_of_publication or "",
"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,
"reference_id": obj.id,
"hadis_id": obj.hadis_id,
"thumbnail": absolute_https_url(img.thumbnail.url, request) if request and img.thumbnail else (img.thumbnail.url if img.thumbnail else None),
"image": absolute_https_url(img.thumbnail.url, request) if request and img.thumbnail else (img.thumbnail.url if img.thumbnail else None),
"priority": img.priority,
}
for img in obj.images.all().order_by("priority", "id")
]
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")
or request.FILES.getlist("images[]")
)
single_img = request.FILES.get("image") or request.FILES.get("thumbnail")
if single_img and not new_images:
new_images = [single_img]
last_img = instance.images.order_by("-priority").first()
current_priority = (last_img.priority + 1) if last_img else 1
for img in new_images:
ReferenceImage.objects.create(reference=instance, thumbnail=img, priority=current_priority)
current_priority += 1
class AdminHadisDetailSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategorySerializer(source="category", read_only=True)
hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True)
@ -279,6 +447,36 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
write_only=True,
allow_null=True,
)
edition_id = serializers.PrimaryKeyRelatedField(
queryset=BookEdition.objects.all(),
required=False,
write_only=True,
allow_null=True,
)
book_volume_id = serializers.PrimaryKeyRelatedField(
queryset=BookVolume.objects.all(),
required=False,
write_only=True,
allow_null=True,
)
volume = serializers.CharField(
required=False,
write_only=True,
allow_null=True,
allow_blank=True,
)
pages = serializers.CharField(
required=False,
write_only=True,
allow_null=True,
allow_blank=True,
)
hadith_number = serializers.CharField(
required=False,
write_only=True,
allow_null=True,
allow_blank=True,
)
bookmark_count = serializers.SerializerMethodField()
@ -325,6 +523,11 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
"collections",
"collections_detail",
"book_reference_id",
"edition_id",
"book_volume_id",
"volume",
"pages",
"hadith_number",
"share_link",
"explanation",
"bookmark_count",
@ -336,22 +539,75 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
ret = super().to_representation(instance)
request = self.context.get("request")
refs = list(instance.references.all())
ref = refs[0] if refs else None
ret["book_reference"] = ref.book_reference_id if ref else None
ret["edition"] = ref.edition_id if ref else None
ret["book_volume"] = ref.book_volume_id if ref else None
ret["volume"] = ref.volume if ref else None
ret["pages"] = ref.pages if ref else None
ret["hadith_number"] = ref.hadith_number if ref else None
if ref and ref.book_reference:
author = ref.book_reference.author
first_ed = ref.book_reference.editions.first() if hasattr(ref.book_reference, "editions") else None
pub = getattr(ref.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(ref.book_reference, "year_of_publication", None)
if not year and first_ed:
year = first_ed.year_of_publication
num_vols = getattr(ref.book_reference, "number_of_volumes", None)
if num_vols is None and first_ed:
num_vols = first_ed.number_of_volumes
ret["book_reference_detail"] = {
"id": ref.book_reference.id,
"title": ref.book_reference.title,
"title": get_localized_text(ref.book_reference.title, request) if isinstance(ref.book_reference.title, list) else ref.book_reference.title,
"slug": ref.book_reference.slug,
"publisher": pub or "",
"year_of_publication": year or "",
"number_of_volumes": num_vols,
"has_editions": getattr(ref.book_reference, "has_editions", False),
"author": {
"id": author.id,
"name": get_localized_text(author.name, request) if isinstance(author.name, list) else author.name,
"slug": author.slug
} if author else None
}
else:
ret["book_reference_detail"] = None
if ref and ref.edition:
ed = ref.edition
pub = ed.publisher
if isinstance(pub, list):
pub = get_localized_text(pub, request)
ret["edition_detail"] = {
"id": ed.id,
"edition_number": ed.edition_number,
"publisher": pub or "",
"year_of_publication": ed.year_of_publication or "",
"number_of_volumes": ed.number_of_volumes,
}
else:
ret["edition_detail"] = None
if ref and ref.book_volume:
ret["book_volume_detail"] = {
"id": ref.book_volume.id,
"title": ref.book_volume.title,
}
else:
ret["book_volume_detail"] = None
ref_images = []
for r in refs:
for img in r.images.all().order_by("priority", "id"):
url = absolute_https_url(img.thumbnail.url, self.context.get("request")) if img.thumbnail else None
url = absolute_https_url(img.thumbnail.url, request) if img.thumbnail else None
ref_images.append({
"id": img.id,
"reference_id": r.id,
@ -361,6 +617,7 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
"priority": img.priority,
})
ret["reference_images"] = ref_images
ret["references"] = AdminHadisReferenceSerializer(instance.references.all().order_by("id"), many=True, context=self.context).data
return ret
def get_bookmark_count(self, obj):
@ -380,6 +637,11 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
def create(self, validated_data):
book_reference = validated_data.pop("book_reference_id", None)
edition = validated_data.pop("edition_id", None)
book_volume = validated_data.pop("book_volume_id", None)
volume = validated_data.pop("volume", None)
pages = validated_data.pop("pages", None)
hadith_number = validated_data.pop("hadith_number", None)
tags = validated_data.pop("tags", [])
collections = validated_data.pop("collection_items", [])
hadis = super().create(validated_data)
@ -388,13 +650,32 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
if collections:
for idx, col in enumerate(collections):
HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx)
if book_reference:
HadisReference.objects.create(hadis=hadis, book_reference=book_reference)
if book_reference or edition or book_volume or volume or pages or hadith_number:
HadisReference.objects.create(
hadis=hadis,
book_reference=book_reference,
edition=edition,
book_volume=book_volume,
volume=volume or "",
pages=pages or "",
hadith_number=hadith_number or ""
)
return hadis
def update(self, instance, validated_data):
book_reference_provided = "book_reference_id" in validated_data
book_reference = validated_data.pop("book_reference_id", None)
edition_provided = "edition_id" in validated_data
edition = validated_data.pop("edition_id", None)
book_volume_provided = "book_volume_id" in validated_data
book_volume = validated_data.pop("book_volume_id", None)
volume_provided = "volume" in validated_data
volume = validated_data.pop("volume", None)
pages_provided = "pages" in validated_data
pages = validated_data.pop("pages", None)
hadith_number_provided = "hadith_number" in validated_data
hadith_number = validated_data.pop("hadith_number", None)
tags = validated_data.pop("tags", None)
collections = validated_data.pop("collection_items", None)
hadis = super().update(instance, validated_data)
@ -404,13 +685,30 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer):
instance.collection_items.all().delete()
for idx, col in enumerate(collections):
HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx)
if book_reference_provided:
any_ref_provided = (
book_reference_provided or edition_provided or book_volume_provided or
volume_provided or pages_provided or hadith_number_provided
)
if any_ref_provided:
ref = instance.references.first()
if not ref:
if book_reference or edition or book_volume or volume or pages or hadith_number:
ref = HadisReference.objects.create(hadis=hadis)
if ref:
if book_reference_provided:
ref.book_reference = book_reference
if edition_provided:
ref.edition = edition
if book_volume_provided:
ref.book_volume = book_volume
if volume_provided:
ref.volume = volume or ""
if pages_provided:
ref.pages = pages or ""
if hadith_number_provided:
ref.hadith_number = hadith_number or ""
ref.save()
elif book_reference:
HadisReference.objects.create(hadis=hadis, book_reference=book_reference)
return hadis
@ -778,8 +1076,315 @@ class AdminHadisCorrectionSerializer(serializers.ModelSerializer):
return instance
class AdminInterpretationReferenceSerializer(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 = InterpretationReference
fields = [
"id",
"interpretation",
"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:
InterpretationReferenceImage.objects.create(reference=instance, image=img)
class AdminOriginalTextReferenceSerializer(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 = OriginalTextReference
fields = [
"id",
"original_text",
"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:
OriginalTextReferenceImage.objects.create(reference=instance, image=img)
class AdminHadisInterpretationSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategoryBriefSerializer(source="category", read_only=True)
references = AdminInterpretationReferenceSerializer(many=True, read_only=True)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
@ -813,10 +1418,11 @@ class AdminHadisInterpretationSerializer(serializers.ModelSerializer):
"book_reference_id",
"address",
"delete_image_ids",
"references",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
read_only_fields = ["id", "references", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
@ -870,6 +1476,7 @@ class AdminHadisInterpretationSerializer(serializers.ModelSerializer):
}
for img in images
]
ret["references"] = AdminInterpretationReferenceSerializer(instance.references.all(), many=True, context=self.context).data
return ret
def create(self, validated_data):
@ -2038,6 +2645,7 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer):
title = serializers.JSONField(required=False, default=list)
text = serializers.JSONField(required=False, default=list)
translation = serializers.JSONField(required=False, default=list)
references = AdminOriginalTextReferenceSerializer(many=True, read_only=True)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
@ -2069,6 +2677,7 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer):
"share_link",
"embedded_in",
"order",
"references",
"book_reference_id",
"address",
"delete_image_ids",
@ -2104,6 +2713,12 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
import json
ret = super().to_representation(instance)
ret["references"] = AdminOriginalTextReferenceSerializer(
instance.references.all().order_by("id"),
many=True,
context=self.context
).data
ref = instance.references.first()
ret["book_reference"] = ref.book_reference_id if ref else None

14
apps/hadis/urls.py

@ -3,7 +3,7 @@ from rest_framework.routers import SimpleRouter
from .views.category import HadisCategorySectListView, HadisCategoryTreeView, CategoriesView, CategoriesBySectView, HadisCategorySelectBySectView, HadisCategorySelectBySectSourceView , HadisCategoryTreeNormalView ,test_deploy,debug_headers,HadisCategoryXMindView
from .views.hadis import (
HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView, CategoryHadisCorrectionsView, HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView,
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView, HadisTagListView
)
from .views.transmitter import (
TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView,
@ -14,12 +14,12 @@ from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncVi
from .views.version import ContentReleaseSyncView, HadisDatabaseVersionView
from .views.info import HadisInfoView
from .views_admin import (
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminCorrectionReferenceViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminCorrectionReferenceViewSet, AdminHadisReferenceViewSet, AdminInterpretationReferenceViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet,
AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookTagViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminHadisReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet,
AdminTransmitterViewSet, AdminTransmitterReliabilityViewSet, DovodiBookSubjectAreaViewSet, DovodiBookTypeViewSet,
DovodiNarratorLayerViewSet, DovodiTransmitterReliabilityViewSet, DovodiOpinionStatusViewSet,
DovodiHadisSectViewSet, DovodiHadisCategoryViewSet, AdminCategoryQuranVerseViewSet, AdminHadisTransmitterViewSet,
AdminTransmitterOpinionViewSet, AdminTransmitterOriginalTextViewSet, AdminBookEditionViewSet, AdminBookVolumeViewSet,
AdminTransmitterOpinionViewSet, AdminTransmitterOriginalTextViewSet, AdminOriginalTextReferenceViewSet, AdminBookEditionViewSet, AdminBookVolumeViewSet,
AdminContentReleaseViewSet
)
from .views.reference_v2 import BookAuthorListView, BookAuthorDetailView, AuthorReferencesListView, BookAuthorSyncView
@ -36,7 +36,10 @@ admin_router.register(r'dovodi-sects', DovodiHadisSectViewSet, basename='dovodi-
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'hadis-references', AdminHadisReferenceViewSet, basename='admin-hadis-references')
admin_router.register(r'interpretations', AdminHadisInterpretationViewSet, basename='admin-hadis-interpretations')
admin_router.register(r'interpretation-references', AdminInterpretationReferenceViewSet, basename='admin-interpretation-references')
admin_router.register(r'original-text-references', AdminOriginalTextReferenceViewSet, basename='admin-original-text-references')
admin_router.register(r'statuses', AdminHadisStatusViewSet, basename='admin-hadis-statuses')
admin_router.register(r'tags', AdminHadisTagViewSet, basename='admin-hadis-tags')
admin_router.register(r'collections', AdminHadisCollectionViewSet, basename='admin-hadis-collections')
@ -91,6 +94,7 @@ urlpatterns = [
path('sync/database/', HadisDatabaseVersionView.as_view(), name='hadis-database-sync'),
path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'),
path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'),
path('tags/', cached_view(HadisTagListView.as_view()), name='hadis-tag-list'),
# Category paths (more specific first)
path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'),
@ -112,7 +116,7 @@ urlpatterns = [
path('narrators/<str:narrator_slug>/students/', cached_view(NarratorStudentsView.as_view()), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments/', cached_view(NarratorArgumentsListView.as_view()), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions/', cached_view(TransmitterOpinionView.as_view()), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts/', cached_view(TransmitterOriginalTextView.as_view()), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/original_texts/', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/', cached_view(TransmitterDetailView.as_view()), name='narrator-detail'),
path('narrators/', cached_view(TransmitterView.as_view()), name='narrators'),
@ -132,7 +136,7 @@ urlpatterns = [
path('<str:hadis_slug>/transmitters/', cached_view(HadisTransmittersView.as_view()), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', cached_view(HadisLayersView.as_view()), name='hadis-layers'),
path('<str:hadis_slug>/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'),
path('<str:hadis_slug>/interprets/', cached_view(HadisInterpretsListView.as_view()), name='hadis-interprets'),
path('<str:hadis_slug>/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'),
path('<str:hadis_slug>/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST
# path('test/test-deploy',test_deploy , name='test'),

22
apps/hadis/views/category.py

@ -475,6 +475,14 @@ class CategoriesView(ListAPIView):
).filter(
Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN)
)
search_query = (self.request.query_params.get('search') or '').strip()
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(slug__icontains=search_query) |
Q(description__icontains=search_query)
)
is_root = self.request.query_params.get('is_root', '').lower()
if is_root in ['true', '1']:
queryset = queryset.filter(parent__isnull=True)
@ -504,11 +512,23 @@ class CategoriesBySectView(ListAPIView):
).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()
search_query = (self.request.query_params.get('search') or '').strip()
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(slug__icontains=search_query) |
Q(description__icontains=search_query)
)
is_root_param = self.request.query_params.get('is_root', None)
if is_root_param is not None:
is_root = is_root_param.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)
elif not search_query:
queryset = queryset.filter(parent__isnull=True)
return apply_category_ordering_and_counts(queryset, self.request)

180
apps/hadis/views/hadis.py

@ -10,15 +10,25 @@ 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, CorrectionReference, HadisInterpretation, InterpretationReference
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer
from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage, CorrectionReference, HadisInterpretation, InterpretationReference, HadisTag
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer, HadisTagSerializer
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
from ..serializers.category import get_localized_text
from rest_framework import status
from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin
class HadisTagListView(ListAPIView):
"""
API view to list active Hadis Tags
"""
serializer_class = HadisTagSerializer
pagination_class = NoPagination
permission_classes = [AllowAny]
def get_queryset(self):
return HadisTag.objects.filter(status=True).order_by('id')
class PinnedHadisCollectionListView(ListAPIView):
"""
API view to list pinned hadis collections with metadata info
@ -293,6 +303,14 @@ class HadisListView(ListAPIView):
for status in HadisStatus.objects.all().order_by('order')
]
tags_list = [
{
"id": tag.id,
"title": get_localized_text(tag.title, request=request, language_code=lang) or "",
}
for tag in HadisTag.objects.filter(status=True).order_by('id')
]
# 6. Inject it into the response data
if isinstance(response.data, dict):
ordered_data = {}
@ -303,8 +321,9 @@ class HadisListView(ListAPIView):
ordered_data['current_category'] = category_data
ordered_data['quran_data'] = quran_data
# 👇 تزریق لیست استاتوس‌ها قبل از نتایج
# 👇 تزریق لیست استاتوس‌ها و تگ‌ها قبل از نتایج
ordered_data['statuses'] = statuses_list
ordered_data['tags'] = tags_list
if 'results' in response.data:
ordered_data['results'] = response.data['results']
@ -323,10 +342,18 @@ class HadisListView(ListAPIView):
).annotate(
layer_count=Count('transmitters__narrator_layer', distinct=True)
).select_related('category').prefetch_related(
'tags',
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority'))
queryset=HadisReference.objects.select_related(
'book_reference',
'book_reference__author',
'edition',
'book_volume',
'book_volume__edition'
).prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')),
'book_reference__editions'
)
)
)
@ -338,6 +365,10 @@ class HadisListView(ListAPIView):
search_conditions |= Q(title__icontains=search_query)
search_conditions |= Q(title_narrator__icontains=search_query)
search_conditions |= Q(translation__icontains=search_query)
search_conditions |= Q(tags__title__icontains=search_query)
search_conditions |= Q(references__book_reference__title__icontains=search_query)
search_conditions |= Q(transmitters__transmitter__full_name__icontains=search_query)
search_conditions |= Q(transmitters__transmitter__known_as__icontains=search_query)
queryset = queryset.filter(search_conditions)
# 👇 2. Apply Status Filter (Supports multiple comma-separated slugs)
@ -368,6 +399,20 @@ class HadisListView(ListAPIView):
author_q |= Q(references__book_reference__author_id__in=numeric_ids)
queryset = queryset.filter(author_q)
# 👇 6. Apply Tag Filter (Supports comma-separated IDs or titles)
tag_filter = self.request.query_params.get('tag', None) or self.request.query_params.get('tags', None)
if tag_filter:
tag_items = [t.strip() for t in tag_filter.split(',') if t.strip()]
numeric_tag_ids = [int(t) for t in tag_items if t.isdigit()]
non_numeric_tags = [t for t in tag_items if not t.isdigit()]
tag_q = Q()
if numeric_tag_ids:
tag_q |= Q(tags__id__in=numeric_tag_ids)
for t_val in non_numeric_tags:
tag_q |= Q(tags__title__icontains=t_val)
if tag_q:
queryset = queryset.filter(tag_q)
# Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true' and self.request.user.is_authenticated:
@ -406,7 +451,7 @@ class HadisListView(ListAPIView):
class HadisMainListView(ListAPIView):
"""
API view to list Hadis by category_id
API view to list Hadis globally with comprehensive filtering
"""
serializer_class = HadisListSerializer
authentication_classes = [TokenAuthentication]
@ -418,31 +463,94 @@ class HadisMainListView(ListAPIView):
return self.list(request, *args, **kwargs)
def get_queryset(self):
queryset = Hadis.objects.select_related('category__sect', 'hadis_status').prefetch_related(
queryset = Hadis.objects.filter(status=True).select_related(
'category__sect', 'hadis_status'
).annotate(
layer_count=Count('transmitters__narrator_layer', distinct=True)
).prefetch_related(
'tags',
Prefetch(
'references',
queryset=HadisReference.objects.select_related('book_reference__author').prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority'))
queryset=HadisReference.objects.select_related(
'book_reference',
'book_reference__author',
'edition',
'book_volume',
'book_volume__edition'
).prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')),
'book_reference__editions'
)
)
)
# Get search parameters
search_query = self.request.query_params.get('search', None)
sect_filter = self.request.query_params.get('sect', None) or self.request.query_params.get('sect_type', None)
source_type_filter = self.request.query_params.get('source_type', None)
status_filter = self.request.query_params.get('status', None)
category_filter = self.request.query_params.get('category', None)
author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None)
source_filter = self.request.query_params.get('source', None)
category_filter = self.request.query_params.get('category', None) or self.request.query_params.get('category_slug', None)
tag_filter = self.request.query_params.get('tag', None) or self.request.query_params.get('tags', None)
# Apply search filter
if search_query:
queryset = self.apply_search_filter(queryset, search_query)
# Apply status filter
# Apply sect filter (e.g. shia, sunni)
if sect_filter:
sects = [s.strip().lower() for s in sect_filter.split(',') if s.strip()]
if sects:
queryset = queryset.filter(category__sect__sect_type__in=sects)
# Apply source_type filter (e.g. hadith, quran, history, fatwa, quote)
if source_type_filter:
source_types = [st.strip().lower() for st in source_type_filter.split(',') if st.strip()]
if source_types:
queryset = queryset.filter(category__source_type__in=source_types)
# Apply status filter (by slug or title)
if status_filter:
queryset = queryset.filter(hadis_status__title__icontains=status_filter)
status_items = [s.strip() for s in status_filter.split(',') if s.strip()]
queryset = queryset.filter(
Q(hadis_status__slug__in=status_items) | Q(hadis_status__title__icontains=status_filter)
)
# Apply author filter
if author_filter:
author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()]
author_q = Q(references__book_reference__author__slug__in=author_slugs)
numeric_ids = [int(a) for a in author_slugs if a.isdigit()]
if numeric_ids:
author_q |= Q(references__book_reference__author_id__in=numeric_ids)
queryset = queryset.filter(author_q)
# Apply source filter (reference books)
if source_filter:
source_slugs = [s.strip() for s in source_filter.split(',') if s.strip()]
if source_slugs:
queryset = queryset.filter(references__book_reference__slug__in=source_slugs)
# Apply category filter
if category_filter:
queryset = queryset.filter(category__title__icontains=category_filter)
cat_items = [c.strip() for c in category_filter.split(',') if c.strip()]
queryset = queryset.filter(
Q(category__slug__in=cat_items) | Q(category__title__icontains=category_filter)
)
# Apply tag filter
if tag_filter:
tag_items = [t.strip() for t in tag_filter.split(',') if t.strip()]
numeric_tag_ids = [int(t) for t in tag_items if t.isdigit()]
non_numeric_tags = [t for t in tag_items if not t.isdigit()]
tag_q = Q()
if numeric_tag_ids:
tag_q |= Q(tags__id__in=numeric_tag_ids)
for t_val in non_numeric_tags:
tag_q |= Q(tags__title__icontains=t_val)
if tag_q:
queryset = queryset.filter(tag_q)
# Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
@ -455,7 +563,23 @@ class HadisMainListView(ListAPIView):
).values_list('content_id', flat=True)
queryset = queryset.filter(id__in=bookmarked_ids)
return queryset
return queryset.distinct()
def get_serializer_context(self):
"""Add user bookmarks to serializer context to avoid caching issues"""
context = super().get_serializer_context()
user = self.request.user
if user.is_authenticated:
from apps.bookmark.models.bookmark import Bookmark
user_bookmarks = Bookmark.objects.filter(
user=user,
service=Bookmark.ServiceChoices.HADITH,
status=True
).values_list('content_id', flat=True)
context['user_bookmarked_hadis_ids'] = set(user_bookmarks)
else:
context['user_bookmarked_hadis_ids'] = set()
return context
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
@ -465,19 +589,7 @@ class HadisMainListView(ListAPIView):
if page is not None:
serializer = self.get_serializer(page, many=True)
paginated_response = self.get_paginated_response(serializer.data)
# Get category titles
category_titles = self.get_category_titles(request)
# Get status titles
status_titles = self.get_status_titles(request)
# Modify the paginated response to include our custom data
response_data = paginated_response.data
# response_data['category_titles'] = self.get_cached_category_titles(request)
# response_data['status_titles'] = self.get_cached_status_titles(request)
return Response(response_data)
return Response(paginated_response.data)
# Fallback for when pagination is disabled
serializer = self.get_serializer(queryset, many=True)
@ -486,8 +598,6 @@ class HadisMainListView(ListAPIView):
'results': serializer.data
})
return Response(response_data)
def get_category_titles(self,request):
"""Get list of category titles based on language"""
from ..models import HadisCategory
@ -516,7 +626,7 @@ class HadisMainListView(ListAPIView):
def apply_search_filter(self, queryset, search_query):
"""
Apply search filter across multiple fields including JSONFields.
Searches in: title, title_narrator, text, translation
Searches in: title, title_narrator, text, translation, tags, book references, transmitters
"""
from django.db.models import Q
@ -528,6 +638,10 @@ class HadisMainListView(ListAPIView):
search_conditions |= Q(title__icontains=search_query)
search_conditions |= Q(title_narrator__icontains=search_query)
search_conditions |= Q(translation__icontains=search_query)
search_conditions |= Q(tags__title__icontains=search_query)
search_conditions |= Q(references__book_reference__title__icontains=search_query)
search_conditions |= Q(transmitters__transmitter__full_name__icontains=search_query)
search_conditions |= Q(transmitters__transmitter__known_as__icontains=search_query)
return queryset.filter(search_conditions)
@ -1273,7 +1387,9 @@ class HadisSourceDetailsView(RetrieveAPIView):
).prefetch_related(
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
queryset=HadisReference.objects.select_related(
'book_reference', 'book_reference__author', 'edition', 'book_volume'
).prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority'))
)
)

21
apps/hadis/views/transmitter.py

@ -12,8 +12,8 @@ from drf_yasg.utils import swagger_auto_schema
from ..models import Transmitters, HadisTransmitter
from ..serializers import NarratorRelationSerializer
from utils.pagination import StandardResultsSetPagination
from ..models import Transmitters , TransmitterOpinion, TransmitterOriginalText, Hadis
from ..serializers import TransmitterSerializer , TransmitterDetailSerializer, TransmitterSyncSerializer,TransmitterOpinionSerializer, TransmitterOriginalTextSerializer, HadisListSerializer
from ..models import Transmitters, TransmitterOpinion, TransmitterOriginalText, Hadis, OriginalTextReference, OriginalTextReferenceImage
from ..serializers import TransmitterSerializer, TransmitterDetailSerializer, TransmitterSyncSerializer, TransmitterOpinionSerializer, TransmitterOriginalTextSerializer, TransmitterOriginalTextDetailSerializer, HadisListSerializer
from ..docs import transmitter_filters_swagger,transmitter_list_swagger, transmitter_detail_swagger, transmitter_sync_swagger, transmitter_opinion_swagger, transmitter_original_text_swagger
from utils.pagination import NoPagination
from .category import get_localized_text
@ -102,7 +102,7 @@ class TransmitterOpinionView(ListAPIView):
class TransmitterOriginalTextView(ListAPIView):
serializer_class = TransmitterOriginalTextSerializer
serializer_class = TransmitterOriginalTextDetailSerializer
pagination_class = StandardResultsSetPagination
@transmitter_original_text_swagger
@ -117,8 +117,19 @@ class TransmitterOriginalTextView(ListAPIView):
).select_related(
'transmitter'
).prefetch_related(
'references__images',
'references__book_reference',
Prefetch(
'references',
queryset=OriginalTextReference.objects.select_related(
'book_reference',
'book_reference__author',
'edition',
'book_volume',
'book_volume__edition'
).prefetch_related(
Prefetch('images', queryset=OriginalTextReferenceImage.objects.order_by('priority')),
'book_reference__editions'
)
)
)

56
apps/hadis/views_admin.py

@ -13,7 +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,
CorrectionReference, InterpretationReference, OriginalTextReference,
ContentRelease,
CategoryQuranVerse,
)
@ -24,6 +24,9 @@ from .serializers.serializers_admin import (
AdminHadisCategoryArchiveDetailSerializer,
AdminHadisCorrectionSerializer,
AdminCorrectionReferenceSerializer,
AdminInterpretationReferenceSerializer,
AdminOriginalTextReferenceSerializer,
AdminHadisReferenceSerializer,
AdminHadisDetailSerializer,
AdminHadisInterpretationSerializer,
AdminHadisCollectionSerializer,
@ -466,6 +469,57 @@ class AdminCorrectionReferenceViewSet(ModelViewSet):
return qs.order_by("id")
class AdminHadisReferenceViewSet(ModelViewSet):
serializer_class = AdminHadisReferenceSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
parser_classes = (MultiPartParser, FormParser, JSONParser)
pagination_class = OptionalPagination
def get_queryset(self):
qs = HadisReference.objects.all().select_related(
"book_reference__author", "edition", "book_volume"
).prefetch_related("images")
hadis_id = self.request.query_params.get("hadis_id")
if hadis_id:
qs = qs.filter(hadis_id=hadis_id)
return qs.order_by("id")
class AdminInterpretationReferenceViewSet(ModelViewSet):
serializer_class = AdminInterpretationReferenceSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
parser_classes = (MultiPartParser, FormParser, JSONParser)
pagination_class = OptionalPagination
def get_queryset(self):
qs = InterpretationReference.objects.all().select_related(
"book_reference__author", "edition", "book_volume"
).prefetch_related("images")
interpretation_id = self.request.query_params.get("interpretation_id") or self.request.query_params.get("interpretation")
if interpretation_id:
qs = qs.filter(interpretation_id=interpretation_id)
return qs.order_by("id")
class AdminOriginalTextReferenceViewSet(ModelViewSet):
serializer_class = AdminOriginalTextReferenceSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
parser_classes = (MultiPartParser, FormParser, JSONParser)
pagination_class = OptionalPagination
def get_queryset(self):
qs = OriginalTextReference.objects.all().select_related(
"book_reference__author", "edition", "book_volume"
).prefetch_related("images")
original_text_id = self.request.query_params.get("original_text_id") or self.request.query_params.get("original_text")
if original_text_id:
qs = qs.filter(original_text_id=original_text_id)
return qs.order_by("id")
class AdminHadisInterpretationViewSet(ModelViewSet):
serializer_class = AdminHadisInterpretationSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]

Loading…
Cancel
Save