From f56736a23c6aa7fe3ff7da9c44d6aa305a5a3272 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 9 Sep 2026 09:58:37 +0330 Subject: [PATCH] feat(corrections): apis updated for hadith corrections --- apps/hadis/serializers/hadis.py | 189 +++++++++++++++++++- apps/hadis/serializers/serializers_admin.py | 158 +++++++++++++++- apps/hadis/signals.py | 3 +- apps/hadis/urls.py | 5 +- apps/hadis/views_admin.py | 19 ++ 5 files changed, 363 insertions(+), 11 deletions(-) diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 9185ac8..d634347 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -1379,9 +1379,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 +1429,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 +1457,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) diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index a6cad2c..ff37bc1 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/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): diff --git a/apps/hadis/signals.py b/apps/hadis/signals.py index 6e9d506..6463b5a 100644 --- a/apps/hadis/signals.py +++ b/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 ] diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 81c4a3a..c1dd5d2 100644 --- a/apps/hadis/urls.py +++ b/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//', cached_view(HadisInterpretationDetailView.as_view()), name='interpretation-detail'), # Correction Detail Endpoint (تصحیح بر اساس اسلاگ) - path('corrections//', cached_view(HadisCorrectionDetailView.as_view()), name='correction-detail'), + path('corrections//', HadisCorrectionDetailView.as_view(), name='correction-detail'), # Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ) path('original-texts//', cached_view(TransmitterOriginalTextDetailView.as_view()), name='original-text-detail'), diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 34cf8f3..330e7c8 100644 --- a/apps/hadis/views_admin.py +++ b/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]