From 82ecac802fa31104d40fc65f5557e43a78de9293 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 6 Oct 2026 14:02:53 +0330 Subject: [PATCH] feat(hadis): add multi-reference endpoints and serializers for transmitter original texts with scan images and hadith_number --- ...046_originaltextreference_hadith_number.py | 18 ++ apps/hadis/models/transmitter.py | 1 + apps/hadis/serializers/serializers_admin.py | 161 ++++++++++++++++++ apps/hadis/urls.py | 3 +- apps/hadis/views_admin.py | 20 ++- 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 apps/hadis/migrations/0046_originaltextreference_hadith_number.py diff --git a/apps/hadis/migrations/0046_originaltextreference_hadith_number.py b/apps/hadis/migrations/0046_originaltextreference_hadith_number.py new file mode 100644 index 0000000..f5e4b09 --- /dev/null +++ b/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'), + ), + ] diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index dbfe554..2ff1ae8 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -582,6 +582,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 diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 64d15a8..eb62cb0 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -1229,6 +1229,159 @@ class AdminInterpretationReferenceSerializer(serializers.ModelSerializer): 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) @@ -2492,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( @@ -2523,6 +2677,7 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer): "share_link", "embedded_in", "order", + "references", "book_reference_id", "address", "delete_image_ids", @@ -2558,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 diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index ca2c283..48f120f 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -19,7 +19,7 @@ from .views_admin import ( 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 @@ -39,6 +39,7 @@ admin_router.register(r'correction-references', AdminCorrectionReferenceViewSet, 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') diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 3fe9cc0..4af1597 100644 --- a/apps/hadis/views_admin.py +++ b/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, InterpretationReference, + CorrectionReference, InterpretationReference, OriginalTextReference, ContentRelease, CategoryQuranVerse, ) @@ -25,6 +25,7 @@ from .serializers.serializers_admin import ( AdminHadisCorrectionSerializer, AdminCorrectionReferenceSerializer, AdminInterpretationReferenceSerializer, + AdminOriginalTextReferenceSerializer, AdminHadisReferenceSerializer, AdminHadisDetailSerializer, AdminHadisInterpretationSerializer, @@ -502,6 +503,23 @@ class AdminInterpretationReferenceViewSet(ModelViewSet): 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]