diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 81ba50d..438b8f7 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -1,3960 +1,3960 @@ -from rest_framework import serializers -from django.core.files.uploadedfile import SimpleUploadedFile -from django.utils.translation import gettext_lazy as _ -from django.db.models import Exists, OuterRef, Q -from django.core.exceptions import ValidationError as DjangoValidationError - -from utils.image_compression import maybe_compress_uploaded_file -from utils import absolute_https_url -from ..models import ( - Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage, - BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookPublisher, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, - Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, ScholarlyOpinion, TransmitterOpinion, TransmitterOriginalText, - City, Country, Kunya, TransmitterFeature, TransmitterProfession, SahabiStatus, Madhhab, RelationType, YearValue, NarratorDate, - CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, - OriginalTextReference, OriginalTextReferenceImage, - ContentRelease -) -from .category import LocalizedField, get_localized_text - - -def safe_copy_data(data): - if hasattr(data, "lists"): - new_data = {} - for key, values in data.lists(): - if key in ["subject_area", "authors", "book_references", "delete_image_ids", "uploaded_images"] or key.endswith("[]"): - new_data[key] = values - elif len(values) > 1: - new_data[key] = values - else: - new_data[key] = values[0] if values else None - return new_data - elif hasattr(data, "copy"): - return data.copy() - else: - return dict(data) - - -class AbsoluteImageField(serializers.ImageField): - def to_internal_value(self, data): - uploaded = super().to_internal_value(data) - if not uploaded: - return uploaded - - compressed_bytes = maybe_compress_uploaded_file(uploaded) - if compressed_bytes is None: - return uploaded - - return SimpleUploadedFile( - name=getattr(uploaded, "name", "image"), - content=compressed_bytes, - content_type=getattr(uploaded, "content_type", None), - ) - - def to_representation(self, value): - if not value: - return None - request = self.context.get("request") - url = value.url if hasattr(value, "url") else str(value) - return absolute_https_url(url, request) - - -class AdminHadisCategorySerializer(serializers.ModelSerializer): - sect_type = serializers.CharField(source='sect.sect_type', read_only=True) - title = LocalizedField() - description = LocalizedField() - has_children = serializers.SerializerMethodField() - is_leaf = serializers.SerializerMethodField() - has_hadith = serializers.SerializerMethodField() - has_quran_verse = serializers.SerializerMethodField() - ancestors = serializers.SerializerMethodField() - - def get_has_children(self, obj): - if hasattr(obj, "annotated_has_children"): - return bool(obj.annotated_has_children) - return obj.children.exists() - - def get_is_leaf(self, obj): - if hasattr(obj, "annotated_has_children"): - return not bool(obj.annotated_has_children) - return not obj.children.exists() - - def get_has_hadith(self, obj): - if hasattr(obj, "annotated_has_hadith"): - return bool(obj.annotated_has_hadith) - return obj.hadis_set.exists() or (hasattr(obj, "hadis_items") and obj.hadis_items.exists()) - - def get_has_quran_verse(self, obj): - if hasattr(obj, "annotated_has_quran_verse"): - return bool(obj.annotated_has_quran_verse) - return hasattr(obj, "quran_verse") and obj.quran_verse is not None - - def get_ancestors(self, obj): - if not obj: - return [] - ancestors_qs = obj.get_ancestors(include_self=True).select_related('sect') - return [ - { - "id": a.id, - "title": a.title, - "slug": a.slug, - "is_topic": a.is_topic, - "source_type": a.source_type, - "sect_type": a.sect.sect_type if a.sect else None, - "order": a.order, - "is_root": a.parent_id is None, - "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, - } - for a in ancestors_qs - ] - - class Meta: - model = HadisCategory - fields = [ - "id", - "parent", - "title", - "slug", - "is_topic", - "source_type", - "order", - "sect", - "sect_type", - "description", - "has_children", - "is_leaf", - "has_hadith", - "has_quran_verse", - "ancestors", - ] - - -class AdminHadisStatusSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField() - - class Meta: - model = HadisStatus - fields = ["id", "title", "slug", "color", "main_color_code", "order", "description"] - - -class AdminHadisTagSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = HadisTag - fields = ["id", "title", "status"] - - -class AdminHadisCategoryListSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = HadisCategory - fields = ["id", "title", "slug", "source_type", "order"] - - -class AdminHadisListSerializer(serializers.ModelSerializer): - category_detail = AdminHadisCategoryListSerializer(source="category", read_only=True) - categories_detail = AdminHadisCategoryListSerializer(source="categories", many=True, read_only=True) - hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True) - collections_detail = serializers.SerializerMethodField() - - class Meta: - model = Hadis - fields = [ - "id", - "number", - "slug", - "title", - "title_narrator", - "text", - "translation", - "status", - "category", - "category_detail", - "categories", - "categories_detail", - "hadis_status", - "hadis_status_detail", - "collections_detail", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - def get_collections_detail(self, obj): - return [ - {"id": item.collection_id, "title": item.collection.title, "slug": item.collection.slug} - for item in obj.collection_items.all() - ] - - -class HadisCollectionsField(serializers.PrimaryKeyRelatedField): - def to_representation(self, value): - return value.collection_id - - -class AdminHadisCollectionSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - summary = serializers.JSONField() - thumbnail = AbsoluteImageField(required=False, allow_null=True) - remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) - - class Meta: - model = HadisCollection - fields = [ - "id", - "title", - "slug", - "summary", - "status", - "display_position", - "order", - "thumbnail", - "remove_thumbnail", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "created_at", "updated_at"] - - def to_internal_value(self, data): - import json - mutable_data = safe_copy_data(data) - for json_field in ["title", "summary"]: - val = mutable_data.get(json_field) - if isinstance(val, str): - try: - mutable_data[json_field] = json.loads(val) - except ValueError: - pass - thumbnail = mutable_data.get("thumbnail") if hasattr(mutable_data, "get") else None - if thumbnail and hasattr(thumbnail, "read"): - compressed_bytes = maybe_compress_uploaded_file(thumbnail) - if compressed_bytes is not None: - mutable_data = safe_copy_data(mutable_data) - mutable_data["thumbnail"] = SimpleUploadedFile( - name=getattr(thumbnail, "name", "image"), - content=compressed_bytes, - content_type=getattr(thumbnail, "content_type", None), - ) - return super().to_internal_value(mutable_data) - - def create(self, validated_data): - validated_data.pop("remove_thumbnail", False) - return super().create(validated_data) - - def update(self, instance, validated_data): - remove_thumbnail = validated_data.pop("remove_thumbnail", False) - if remove_thumbnail and instance.thumbnail: - instance.thumbnail.delete(save=False) - instance.thumbnail = None - 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) - categories_detail = AdminHadisCategorySerializer(source="categories", many=True, read_only=True) - hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True) - tags_detail = AdminHadisTagSerializer(source="tags", many=True, read_only=True) - - category = serializers.PrimaryKeyRelatedField( - queryset=HadisCategory.objects.all(), - required=False, - allow_null=True, - ) - categories = serializers.PrimaryKeyRelatedField( - queryset=HadisCategory.objects.all(), - many=True, - required=False, - ) - hadis_status = serializers.PrimaryKeyRelatedField( - queryset=HadisStatus.objects.all(), - required=False, - allow_null=True, - ) - tags = serializers.PrimaryKeyRelatedField( - queryset=HadisTag.objects.all(), - many=True, - required=False, - ) - collections = HadisCollectionsField( - source="collection_items", - queryset=HadisCollection.objects.all(), - many=True, - required=False, - ) - collections_detail = serializers.SerializerMethodField() - category_path = serializers.SerializerMethodField() - categories_paths = serializers.SerializerMethodField() - book_reference_id = serializers.PrimaryKeyRelatedField( - queryset=BookReference.objects.all(), - required=False, - 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() - - def get_category_path(self, obj): - cat = obj.category - if not cat and hasattr(obj, "categories"): - cat = obj.categories.first() - if not cat: - return [] - ancestors_qs = cat.get_ancestors(include_self=True).select_related('sect') - return [ - { - "id": a.id, - "title": a.title, - "slug": a.slug, - "source_type": a.source_type, - "sect_type": a.sect.sect_type if a.sect else None, - "order": a.order, - "is_root": a.parent_id is None, - "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, - } - for a in ancestors_qs - ] - - def get_categories_paths(self, obj): - cats = list(obj.categories.all()) if hasattr(obj, "categories") else [] - if not cats and obj.category: - cats = [obj.category] - all_paths = [] - for cat in cats: - ancestors_qs = cat.get_ancestors(include_self=True).select_related('sect') - all_paths.append([ - { - "id": a.id, - "title": a.title, - "slug": a.slug, - "source_type": a.source_type, - "sect_type": a.sect.sect_type if a.sect else None, - "order": a.order, - "is_root": a.parent_id is None, - "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, - } - for a in ancestors_qs - ]) - return all_paths - - class Meta: - model = Hadis - fields = [ - "id", - "number", - "slug", - "category", - "category_detail", - "category_path", - "categories", - "categories_detail", - "categories_paths", - "title_narrator", - "title", - "description", - "text", - "translation", - "status", - "hadis_status", - "hadis_status_detail", - "hadis_status_text", - "address", - "links", - "tags", - "tags_detail", - "collections", - "collections_detail", - "book_reference_id", - "edition_id", - "book_volume_id", - "volume", - "pages", - "hadith_number", - "share_link", - "explanation", - "bookmark_count", - "created_at", - "updated_at", - "embedded_in", - ] - read_only_fields = ["id", "slug", "share_link", "bookmark_count", "created_at", "updated_at"] - - 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": 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, request) if img.thumbnail else None - ref_images.append({ - "id": img.id, - "reference_id": r.id, - "hadis_id": instance.id, - "thumbnail": url, - "image": url, - "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): - from apps.bookmark.models.bookmark import Bookmark - return Bookmark.objects.filter(service=Bookmark.ServiceChoices.HADITH, content_id=obj.id, status=True).count() - - def validate_category(self, value): - if value and value.children.exists(): - raise serializers.ValidationError( - _("Cannot assign a hadith/item to a category that has subcategories. Only leaf categories can contain hadiths.") - ) - return value - - def validate_categories(self, values): - for val in values: - if val and val.children.exists(): - raise serializers.ValidationError( - _("Cannot assign a hadith/item to a category that has subcategories. Only leaf categories can contain hadiths.") - ) - return values - - def get_collections_detail(self, obj): - collections = [item.collection for item in obj.collection_items.all()] - return AdminHadisCollectionSerializer(collections, many=True).data - - 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", []) - categories = validated_data.pop("categories", None) - hadis = super().create(validated_data) - if categories is not None: - hadis.categories.set(categories) - if not hadis.category and categories: - hadis.category = categories[0] - hadis.save(update_fields=['category']) - elif hadis.category: - hadis.categories.set([hadis.category]) - if tags: - hadis.tags.set(tags) - if collections: - for idx, col in enumerate(collections): - HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx) - 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 "" - ) - if not hadis.title_narrator or (isinstance(hadis.title_narrator, list) and not any(isinstance(item, dict) and item.get('text', '').strip() for item in hadis.title_narrator)): - if hadis.auto_populate_title_narrator(): - hadis.save(update_fields=['title_narrator', 'normalized_text']) - 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) - categories = validated_data.pop("categories", None) - hadis = super().update(instance, validated_data) - if categories is not None: - hadis.categories.set(categories) - if not hadis.category and categories: - hadis.category = categories[0] - hadis.save(update_fields=['category']) - elif "category" in validated_data and validated_data.get("category"): - if not hadis.categories.exists(): - hadis.categories.set([validated_data["category"]]) - - if tags is not None: - hadis.tags.set(tags) - if collections is not None: - instance.collection_items.all().delete() - for idx, col in enumerate(collections): - HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx) - - 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() - if not hadis.title_narrator or (isinstance(hadis.title_narrator, list) and not any(isinstance(item, dict) and item.get('text', '').strip() for item in hadis.title_narrator)): - if hadis.auto_populate_title_narrator(): - hadis.save(update_fields=['title_narrator', 'normalized_text']) - return hadis - - -class AdminHadisBriefSerializer(serializers.ModelSerializer): - class Meta: - model = Hadis - fields = ["id", "number", "slug", "title", "title_narrator", "status"] - - - -class AdminHadisCategoryBriefSerializer(serializers.ModelSerializer): - title = LocalizedField() - - class Meta: - model = HadisCategory - fields = ["id", "title"] - - -def save_relation_reference_data(instance, validated_data, request=None, is_correction=True, ref_type=None): - book_reference = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - new_images = [] - if request: - files_obj = getattr(request, "FILES", None) - if not files_obj and hasattr(request, "_request"): - files_obj = getattr(request._request, "FILES", None) - - if files_obj: - if hasattr(files_obj, "getlist"): - new_images = files_obj.getlist("uploaded_images") or files_obj.getlist("uploaded_images[]") - if not new_images: - val = files_obj.get("uploaded_images") or files_obj.get("uploaded_images[]") - if val: - new_images = [val] if not isinstance(val, list) else val - - if not new_images and hasattr(request, "data"): - data_obj = request.data - if hasattr(data_obj, "getlist"): - new_images = data_obj.getlist("uploaded_images") or data_obj.getlist("uploaded_images[]") - if not new_images and hasattr(data_obj, "get"): - val = data_obj.get("uploaded_images") or data_obj.get("uploaded_images[]") - if val: - new_images = val if isinstance(val, list) else [val] - - actual_images = [img for img in new_images if img and not isinstance(img, (str, int, float, bool, dict))] - - ref = instance.references.first() - if not ref: - if book_reference or address or actual_images: - if ref_type == "original_text": - ref = OriginalTextReference.objects.create(original_text=instance) - elif is_correction: - ref = CorrectionReference.objects.create(correction=instance) - else: - ref = InterpretationReference.objects.create(interpretation=instance) - else: - return - - if book_reference is not None: - ref.book_reference = book_reference - if address is not None: - if isinstance(address, (list, dict)): - import json - ref.address = json.dumps(address, ensure_ascii=False) - else: - ref.address = str(address) - ref.save() - - if delete_image_ids: - ref.images.filter(id__in=delete_image_ids).delete() - - for img in actual_images: - if ref_type == "original_text": - OriginalTextReferenceImage.objects.create(reference=ref, image=img) - elif is_correction: - CorrectionReferenceImage.objects.create(reference=ref, image=img) - else: - 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( - queryset=BookReference.objects.all(), - required=False, - write_only=True, - allow_null=True - ) - address = serializers.JSONField( - required=False, - write_only=True, - allow_null=True - ) - delete_image_ids = serializers.ListField( - child=serializers.IntegerField(), - required=False, - write_only=True - ) - - class Meta: - model = HadisCorrection - fields = [ - "id", - "hadis", - "hadis_detail", - "title", - "slug", - "text", - "translation", - "priority", - "bookmark_count", - "links", - "book_reference_id", - "address", - "delete_image_ids", - "references", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "slug", "bookmark_count", "references", "created_at", "updated_at"] - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - for json_field in ["title", "translation", "links", "address"]: - val = data.get(json_field) - if isinstance(val, str): - try: - data[json_field] = json.loads(val) - except ValueError: - pass - 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 to_representation(self, instance): - import json - ret = super().to_representation(instance) - refs = list(instance.references.all()) - ref = refs[0] if refs else None - ret["book_reference"] = ref.book_reference_id if ref else None - - addr_val = ref.address if ref and ref.address else "" - if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): - try: - ret["address"] = json.loads(addr_val) - except (ValueError, TypeError): - ret["address"] = addr_val - else: - ret["address"] = addr_val - - request = self.context.get("request") - images = list(ref.images.all()) if ref else [] - ret["images"] = [ - { - "id": img.id, - "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) - } - for img in images - ] - ret["references"] = AdminCorrectionReferenceSerializer(instance.references.all(), many=True, context=self.context).data - return ret - - def create(self, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - instance = super().create(validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, is_correction=True) - return instance - - def update(self, instance, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - instance = super().update(instance, validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, is_correction=True) - 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( - queryset=BookReference.objects.all(), - required=False, - write_only=True, - allow_null=True - ) - address = serializers.JSONField( - required=False, - write_only=True, - allow_null=True - ) - delete_image_ids = serializers.ListField( - child=serializers.IntegerField(), - required=False, - write_only=True - ) - - class Meta: - model = HadisInterpretation - fields = [ - "id", - "category", - "category_detail", - "title", - "text", - "translation", - "priority", - "links", - "book_reference_id", - "address", - "delete_image_ids", - "references", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "references", "created_at", "updated_at"] - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - for json_field in ["title", "translation", "links", "address"]: - val = data.get(json_field) - if isinstance(val, str): - try: - data[json_field] = json.loads(val) - except ValueError: - pass - 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 to_representation(self, instance): - import json - ret = super().to_representation(instance) - refs = list(instance.references.all()) - ref = refs[0] if refs else None - ret["book_reference"] = ref.book_reference_id if ref else None - - addr_val = ref.address if ref and ref.address else "" - if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): - try: - ret["address"] = json.loads(addr_val) - except (ValueError, TypeError): - ret["address"] = addr_val - else: - ret["address"] = addr_val - - request = self.context.get("request") - images = list(ref.images.all()) if ref else [] - ret["images"] = [ - { - "id": img.id, - "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) - } - for img in images - ] - ret["references"] = AdminInterpretationReferenceSerializer(instance.references.all(), many=True, context=self.context).data - return ret - - def create(self, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - instance = super().create(validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, is_correction=False) - return instance - - def update(self, instance, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - instance = super().update(instance, validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, is_correction=False) - return instance - -class AbsoluteFileField(serializers.FileField): - def to_representation(self, value): - if not value: - return None - request = self.context.get("request") - url = value.url if hasattr(value, "url") else str(value) - if request: - return request.build_absolute_uri(url) - return url - - -class AdminHadisSectArchiveSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField() - - class Meta: - model = HadisSect - fields = ["id", "sect_type", "title", "description", "is_active", "order"] - - -class AdminHadisCategoryArchiveParentSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = HadisCategory - fields = ["id", "title", "slug"] - - -class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer): - parent_detail = AdminHadisCategoryArchiveParentSerializer(source="parent", read_only=True) - sect_detail = AdminHadisSectArchiveSerializer(source="sect", read_only=True) - title = serializers.JSONField() - description = serializers.JSONField() - xmind_file = AbsoluteFileField(required=False, allow_null=True) - remove_xmind_file = serializers.BooleanField(write_only=True, required=False, default=False) - has_hadith = serializers.SerializerMethodField() - has_children = serializers.SerializerMethodField() - is_leaf = serializers.SerializerMethodField() - - parent = serializers.PrimaryKeyRelatedField( - queryset=HadisCategory.objects.all(), - required=False, - allow_null=True, - ) - sect = serializers.PrimaryKeyRelatedField( - queryset=HadisSect.objects.all(), - required=True, - ) - - def get_has_hadith(self, obj): - if hasattr(obj, "annotated_has_hadith"): - return bool(obj.annotated_has_hadith) - return obj.hadis_set.exists() - - def get_has_children(self, obj): - if hasattr(obj, "annotated_has_children"): - return bool(obj.annotated_has_children) - return obj.children.exists() - - def get_is_leaf(self, obj): - if hasattr(obj, "annotated_has_children"): - return not bool(obj.annotated_has_children) - return not obj.children.exists() - - def validate_parent(self, value): - if value and value.hadis_set.exists(): - raise serializers.ValidationError( - _("Cannot set parent to a category that already contains hadiths/content items.") - ) - return value - - class Meta: - model = HadisCategory - fields = [ - "id", - "parent", - "parent_detail", - "sect", - "sect_detail", - "is_topic", - "source_type", - "title", - "description", - "order", - "xmind_file", - "remove_xmind_file", - "slug", - "share_link", - "xmind_share_link", - "has_hadith", - "has_children", - "is_leaf", - ] - read_only_fields = ["id", "share_link", "xmind_share_link"] - extra_kwargs = { - "description": {"required": False}, - "source_type": {"required": False, "allow_null": True, "allow_blank": True}, - } - - def validate(self, attrs): - is_topic = attrs.get('is_topic', getattr(self.instance, 'is_topic', False)) - source_type = attrs.get('source_type', getattr(self.instance, 'source_type', None)) - if is_topic: - if not source_type: - raise serializers.ValidationError({"source_type": _("A topic must have a source type.")}) - else: - attrs['source_type'] = None - - parent = attrs.get('parent', getattr(self.instance, 'parent', None)) - if parent and getattr(parent, 'is_topic', False): - raise serializers.ValidationError({"parent": _("Cannot set parent to a topic. Parent must be a category.")}) - - return super().validate(attrs) - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - for json_field in ["title", "description"]: - val = data.get(json_field) - if isinstance(val, str): - try: - data[json_field] = json.loads(val) - except ValueError: - pass - return super().to_internal_value(data) - - @staticmethod - def _raise_drf_validation_error(exc): - if hasattr(exc, "message_dict"): - detail = { - ("non_field_errors" if key == "__all__" else key): value - for key, value in exc.message_dict.items() - } - else: - detail = {"non_field_errors": exc.messages} - raise serializers.ValidationError(detail) - - def create(self, validated_data): - validated_data.pop("remove_xmind_file", False) - try: - return super().create(validated_data) - except DjangoValidationError as exc: - self._raise_drf_validation_error(exc) - - -class AdminHadisCategoryArchiveDetailSerializer(AdminHadisCategoryArchiveSerializer): - ancestors = serializers.SerializerMethodField() - children_type = serializers.SerializerMethodField() - child_categories = serializers.SerializerMethodField() - child_arguments = serializers.SerializerMethodField() - child_count = serializers.SerializerMethodField() - - def get_ancestors(self, obj): - if not obj or not obj.pk: - return [] - ancestors_qs = obj.get_ancestors(include_self=True).select_related("sect") - return [ - { - "id": a.id, - "title": a.title, - "slug": a.slug, - "is_topic": a.is_topic, - "source_type": a.source_type, - "sect_type": a.sect.sect_type if a.sect else None, - "order": a.order, - "is_root": a.parent_id is None, - "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, "_mptt_meta") else 0, - } - for a in ancestors_qs - ] - - def get_children_type(self, obj): - if not obj or not obj.pk: - return "none" - has_children = bool(getattr(obj, "annotated_has_children", obj.children.exists())) - if has_children: - return "categories" - has_hadith = bool( - getattr( - obj, - "annotated_has_hadith", - obj.hadis_set.exists() or (hasattr(obj, "hadis_items") and obj.hadis_items.exists()) - ) - ) - if has_hadith: - return "arguments" - return "none" - - def get_child_categories(self, obj): - if not obj or not obj.pk: - return [] - child_hadis_subquery = Hadis.objects.filter(Q(category=OuterRef("pk")) | Q(categories=OuterRef("pk"))) - child_children_subquery = HadisCategory.objects.filter(parent=OuterRef("pk")) - children = obj.children.all().annotate( - annotated_has_hadith=Exists(child_hadis_subquery), - annotated_has_children=Exists(child_children_subquery), - ).order_by("order", "id") - return [ - { - "id": c.id, - "title": c.title, - "slug": c.slug, - "order": c.order, - "is_topic": c.is_topic, - "source_type": c.source_type, - "has_children": bool(c.annotated_has_children), - "has_hadith": bool(c.annotated_has_hadith), - } - for c in children - ] - - def get_child_arguments(self, obj): - if not obj or not obj.pk: - return [] - hadis_qs = Hadis.objects.filter( - Q(category=obj) | Q(categories=obj) - ).distinct().select_related("hadis_status").order_by("number", "-id")[:100] - return [ - { - "id": h.id, - "number": h.number, - "title": h.title, - "title_narrator": h.title_narrator, - "slug": h.slug, - "status": h.status, - "status_detail": { - "id": h.hadis_status.id, - "title": h.hadis_status.title, - "color": h.hadis_status.color, - "main_color_code": getattr(h.hadis_status, "main_color_code", None), - } if h.hadis_status else None, - } - for h in hadis_qs - ] - - def get_child_count(self, obj): - if not obj or not obj.pk: - return 0 - cat_count = obj.children.count() - if cat_count > 0: - return cat_count - return Hadis.objects.filter(Q(category=obj) | Q(categories=obj)).distinct().count() - - class Meta(AdminHadisCategoryArchiveSerializer.Meta): - fields = AdminHadisCategoryArchiveSerializer.Meta.fields + [ - "ancestors", - "children_type", - "child_categories", - "child_arguments", - "child_count", - ] - - def update(self, instance, validated_data): - remove_xmind_file = validated_data.pop("remove_xmind_file", False) - if remove_xmind_file and instance.xmind_file: - instance.xmind_file.delete(save=False) - instance.xmind_file = None - try: - return super().update(instance, validated_data) - except DjangoValidationError as exc: - self._raise_drf_validation_error(exc) - -class AdminTransmitterReliabilitySerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = TransmitterReliability - fields = ["id", "title", "slug", "color", "main_color_code", "description"] - - -class AdminNarratorLayerSerializer(serializers.ModelSerializer): - name = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - hadis = serializers.PrimaryKeyRelatedField( - queryset=Hadis.objects.all(), - required=True, - ) - hadis_detail = serializers.SerializerMethodField() - - class Meta: - model = NarratorLayer - fields = [ - "id", - "hadis", - "hadis_detail", - "name", - "number", - "description", - "slug", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "hadis_detail", "created_at", "updated_at"] - - def get_hadis_detail(self, obj): - if not obj.hadis: - return None - return { - "id": obj.hadis.id, - "number": obj.hadis.number, - "slug": obj.hadis.slug, - "title": obj.hadis.title, - } - - -class AdminOpinionStatusSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = OpinionStatus - fields = ["id", "title", "slug", "color", "main_color_code", "description"] - - -class AdminYearValueSerializer(serializers.ModelSerializer): - translations = serializers.JSONField(required=False, default=list) - values_list = serializers.JSONField(required=False, default=list) - narrators_count = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = YearValue - fields = [ - "id", - "category", - "raw_value", - "parse_type", - "value", - "value_min", - "value_max", - "values_list", - "translations", - "narrators_count", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "narrators_count", "created_at", "updated_at"] - - def get_narrators_count(self, obj): - if hasattr(obj, "narrators_count_val"): - return obj.narrators_count_val - if obj.parse_type == "context" and obj.raw_value.startswith("#"): - return NarratorDate.objects.filter(raw_value=obj.raw_value).values("transmitter_id").distinct().count() - return obj.narrator_dates.count() - - def to_representation(self, instance): - rep = super().to_representation(instance) - rep["canonical"] = rep.get("raw_value") - rep["date_type"] = rep.get("category") - rep["date_mode"] = rep.get("parse_type") - return rep - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - if not data.get("raw_value") and data.get("canonical"): - data["raw_value"] = data["canonical"] - if not data.get("category") and data.get("date_type"): - data["category"] = data["date_type"] - if not data.get("parse_type") and data.get("date_mode"): - data["parse_type"] = data["date_mode"] - for jf in ["translations", "values_list"]: - val = data.get(jf) - if isinstance(val, str): - try: - data[jf] = json.loads(val) - except ValueError: - pass - for nf in ["value", "value_min", "value_max"]: - if nf in data and data[nf] in ("", "null", "undefined", None): - data[nf] = None - if not data.get("category"): - data["category"] = "death" - return super().to_internal_value(data) - - def validate(self, attrs): - from ..utils.date_engine import parse_canonical_date, build_translations_for_parsed - category = attrs.get("category", getattr(self.instance, "category", "death")) - raw_value = attrs.get("raw_value", getattr(self.instance, "raw_value", "")) - if raw_value is not None: - raw_str = str(raw_value).strip() - parsed = parse_canonical_date(raw_str, category=category) - if not parsed.is_valid: - raise serializers.ValidationError({"raw_value": parsed.error}) - attrs["raw_value"] = parsed.raw_value - attrs["parse_type"] = parsed.parse_type - if parsed.parse_type != "context": - attrs["value"] = parsed.value - attrs["value_min"] = parsed.value_min - attrs["value_max"] = parsed.value_max - attrs["values_list"] = parsed.values_list - if not attrs.get("translations"): - attrs["translations"] = build_translations_for_parsed(parsed, category=category) - return attrs - - def create(self, validated_data): - raw_value = validated_data.get("raw_value", "") - category = validated_data.get("category", "death") - existing = YearValue.objects.filter(category=category, raw_value=raw_value).first() - if existing: - for k, v in validated_data.items(): - setattr(existing, k, v) - existing.save() - instance = existing - else: - instance = super().create(validated_data) - - # Sync context tag across birth and death so #tag works seamlessly for both - if instance.parse_type == "context" and instance.raw_value.startswith("#"): - other_cat = "birth" if instance.category == "death" else "death" - YearValue.objects.update_or_create( - category=other_cat, - raw_value=instance.raw_value, - defaults={ - "parse_type": "context", - "value": instance.value, - "value_min": instance.value_min, - "value_max": instance.value_max, - "values_list": instance.values_list, - "translations": instance.translations, - } - ) - return instance - - def update(self, instance, validated_data): - instance = super().update(instance, validated_data) - if instance.parse_type == "context" and instance.raw_value.startswith("#"): - YearValue.objects.filter(raw_value=instance.raw_value, parse_type="context").exclude(id=instance.id).update( - value=instance.value, - value_min=instance.value_min, - value_max=instance.value_max, - values_list=instance.values_list, - translations=instance.translations, - ) - return instance - - -class AdminCountrySerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = Country - fields = ["id", "title", "slug", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - -class AdminCitySerializer(serializers.ModelSerializer): - title = serializers.JSONField() - country_detail = AdminCountrySerializer(source="country", read_only=True) - - class Meta: - model = City - fields = ["id", "title", "slug", "country", "country_detail", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "country_detail", "created_at", "updated_at"] - - -class AdminBookPublisherSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - city_detail = AdminCitySerializer(source="city", read_only=True) - country_detail = AdminCountrySerializer(source="country", read_only=True) - - class Meta: - model = BookPublisher - fields = [ - "id", - "title", - "slug", - "city", - "city_detail", - "country", - "country_detail", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "slug", "city_detail", "country_detail", "created_at", "updated_at"] - - -class AdminKunyaSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = Kunya - fields = ["id", "title", "slug", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - - - -class AdminTransmitterFeatureSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = TransmitterFeature - fields = ["id", "title", "slug", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - -class AdminTransmitterProfessionSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - - class Meta: - model = TransmitterProfession - fields = ["id", "title", "slug", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - - - -class AdminSahabiStatusSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = SahabiStatus - fields = ["id", "title", "slug", "description", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - - - - -class AdminRelationTypeSerializer(serializers.ModelSerializer): - code = serializers.CharField(required=False, allow_blank=True) - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = RelationType - fields = ["id", "code", "title", "slug", "description", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - -class AdminMadhhabSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = Madhhab - fields = ["id", "title", "slug", "description", "created_at", "updated_at"] - read_only_fields = ["id", "slug", "created_at", "updated_at"] - - -def _build_admin_date_preview(obj, date_type: str, raw_val: str): - if not raw_val or not str(raw_val).strip(): - return None - return { - "ru": obj.get_formatted_date(date_type, lang="ru"), - "en": obj.get_formatted_date(date_type, lang="en"), - "ar": obj.get_formatted_date(date_type, lang="ar"), - } - - -class AdminTransmitterListSerializer(serializers.ModelSerializer): - reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) - kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True) - features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) - professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True) - sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True) - madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True) - full_name = serializers.JSONField() - known_as = serializers.JSONField() - nickname = serializers.JSONField() - thumbnail = AbsoluteImageField(required=False, allow_null=True) - share_link = serializers.CharField(read_only=True) - birth_year_preview = serializers.SerializerMethodField(read_only=True) - death_year_preview = serializers.SerializerMethodField(read_only=True) - age_at_death_preview = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = Transmitters - fields = [ - "id", - "full_name", - "slug", - "gender", - "birth_year_hijri", - "death_year_hijri", - "age_at_death", - "birth_year_preview", - "death_year_preview", - "age_at_death_preview", - "known_as", - "nickname", - "kunyas_detail", - "features_detail", - "professions_detail", - "sahabi_status", - "sahabi_status_detail", - "reliability", - "reliability_detail", - "madhhab", - "madhhabs_detail", - "generation", - "thumbnail", - "share_link", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at"] - - def get_birth_year_preview(self, obj): - return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) - - def get_death_year_preview(self, obj): - return _build_admin_date_preview(obj, "death", obj.death_year_hijri) - - def get_age_at_death_preview(self, obj): - return _build_admin_date_preview(obj, "age", obj.age_at_death) - - -class AdminTransmitterDetailSerializer(serializers.ModelSerializer): - reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) - full_name = serializers.JSONField() - kunya = serializers.JSONField(required=False, default=list) - kunyas = serializers.PrimaryKeyRelatedField( - queryset=Kunya.objects.all(), - many=True, - required=False, - ) - kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True) - features = serializers.JSONField(required=False, default=list) - features_list = serializers.PrimaryKeyRelatedField( - queryset=TransmitterFeature.objects.all(), - many=True, - required=False, - ) - features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) - profession = serializers.JSONField(required=False, default=list) - professions_list = serializers.PrimaryKeyRelatedField( - queryset=TransmitterProfession.objects.all(), - many=True, - required=False, - ) - professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True) - sahabi_status = serializers.PrimaryKeyRelatedField( - queryset=SahabiStatus.objects.all(), - required=False, - allow_null=True, - ) - sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True) - madhhabs = serializers.PrimaryKeyRelatedField( - queryset=Madhhab.objects.all(), - many=True, - required=False, - ) - madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True) - companion_type = serializers.JSONField(required=False, default=list) - known_as = serializers.JSONField() - nickname = serializers.JSONField() - origin = serializers.JSONField(required=False, allow_null=True) - lived_in = serializers.JSONField(required=False, allow_null=True) - died_in = serializers.JSONField(required=False, allow_null=True) - origin_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - origin_city_detail = AdminCitySerializer(source="origin_city", read_only=True) - lived_in_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - lived_in_city_detail = AdminCitySerializer(source="lived_in_city", read_only=True) - died_in_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - died_in_city_detail = AdminCitySerializer(source="died_in_city", read_only=True) - description = serializers.JSONField() - thumbnail = AbsoluteImageField(required=False, allow_null=True) - remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) - reliability = serializers.PrimaryKeyRelatedField( - queryset=TransmitterReliability.objects.all(), - required=False, - ) - father = serializers.PrimaryKeyRelatedField( - queryset=Transmitters.objects.all(), - required=False, - allow_null=True, - ) - father_detail = serializers.SerializerMethodField(read_only=True) - sons = serializers.SerializerMethodField(read_only=True) - sons_ids = serializers.ListField( - child=serializers.IntegerField(), - required=False, - write_only=True, - ) - share_link = serializers.CharField(read_only=True) - birth_year_preview = serializers.SerializerMethodField(read_only=True) - death_year_preview = serializers.SerializerMethodField(read_only=True) - age_at_death_preview = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = Transmitters - fields = [ - "id", - "full_name", - "kunya", - "kunyas", - "kunyas_detail", - "features", - "features_list", - "features_detail", - "profession", - "professions_list", - "professions_detail", "sahabi_status_detail", "madhhabs_detail", - "sahabi_status", - "sahabi_status_detail", - "companion_type", - "sahabi_status", - "known_as", - "nickname", - "gender", - "slug", - "legacy_number", - "legacy_id", - "birth_year_hijri", - "death_year_hijri", - "age_at_death", - "birth_year_preview", - "death_year_preview", - "age_at_death_preview", - "generation", - "reliability", - "reliability_detail", - "madhhab", - "madhhabs", - "madhhabs_detail", - "in_sahih_muslim", - "in_sahih_bukhari", - "origin", - "origin_city", - "origin_city_detail", - "lived_in", - "lived_in_city", - "lived_in_city_detail", - "died_in", - "died_in_city", - "died_in_city_detail", - "description", - "thumbnail", - "remove_thumbnail", - "father", - "father_detail", - "sons", - "sons_ids", - "share_link", - "created_at", - "updated_at", - ] - read_only_fields = [ - "id", "slug", "share_link", "created_at", "updated_at", - "reliability_detail", "father_detail", "sons", - "birth_year_preview", "death_year_preview", "age_at_death_preview", - "origin_city_detail", "lived_in_city_detail", "died_in_city_detail", - "kunyas_detail", "features_detail", "professions_detail", - ] - - def get_birth_year_preview(self, obj): - return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) - - def get_death_year_preview(self, obj): - return _build_admin_date_preview(obj, "death", obj.death_year_hijri) - - def get_age_at_death_preview(self, obj): - return _build_admin_date_preview(obj, "age", obj.age_at_death) - - def get_father_detail(self, obj): - if obj.father: - name = obj.father.get_name('en') if hasattr(obj.father, 'get_name') else None - if not name and obj.father.full_name and isinstance(obj.father.full_name, list) and len(obj.father.full_name) > 0: - first = obj.father.full_name[0] - if isinstance(first, dict): - name = first.get('text', '—') - return { - "id": obj.father.id, - "full_name": obj.father.full_name, - "name": name or '—', - "slug": obj.father.slug, - } - return None - - def get_sons(self, obj): - result = [] - for son in obj.children_as_father.all(): - name = son.get_name('en') if hasattr(son, 'get_name') else None - if not name and son.full_name and isinstance(son.full_name, list) and len(son.full_name) > 0: - first = son.full_name[0] - if isinstance(first, dict): - name = first.get('text', '—') - result.append({ - "id": son.id, - "full_name": son.full_name, - "name": name or '—', - "slug": son.slug, - }) - return result - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - for json_field in [ - "full_name", - "kunya", - "features", - "profession", - "known_as", - "nickname", - "origin", - "lived_in", - "died_in", - "description", - ]: - val = data.get(json_field) - if isinstance(val, str): - try: - data[json_field] = json.loads(val) - except ValueError: - pass - - # Handle empty/null values for optional fields (prevent unique constraint violations on empty strings) - for field in [ - "legacy_id", - "legacy_number", - "age_at_death", - "generation", - "birth_year_hijri", - "death_year_hijri", - "companion_type", - "father", - "origin_city", - "lived_in_city", - "died_in_city", - ]: - if field in data and (data[field] == "" or data[field] is None or data[field] == "null" or data[field] == "undefined" or (isinstance(data[field], str) and not data[field].strip())): - data[field] = None - elif field in ("birth_year_hijri", "death_year_hijri", "age_at_death") and field in data and data[field] is not None: - data[field] = str(data[field]).strip() - - if "sons_ids" in data: - val = data["sons_ids"] - if isinstance(val, str): - try: - data["sons_ids"] = json.loads(val) - except ValueError: - pass - - if "kunyas" in data: - val = data["kunyas"] - if isinstance(val, str): - try: - data["kunyas"] = json.loads(val) - except ValueError: - pass - - if "features_list" in data: - val = data["features_list"] - if isinstance(val, str): - try: - data["features_list"] = json.loads(val) - except ValueError: - pass - - if "professions_list" in data: - val = data["professions_list"] - if isinstance(val, str): - try: - data["professions_list"] = json.loads(val) - except ValueError: - pass - - if "madhhabs" in data: - val = data["madhhabs"] - if isinstance(val, str): - try: - data["madhhabs"] = json.loads(val) - except ValueError: - pass - - return super().to_internal_value(data) - - def validate(self, attrs): - from ..utils.date_engine import parse_canonical_date - date_checks = [ - ("birth_year_hijri", "birth"), - ("death_year_hijri", "death"), - ("age_at_death", "age"), - ] - errors = {} - for field_name, category in date_checks: - if field_name in attrs and attrs[field_name] is not None: - raw_val = str(attrs[field_name]).strip() - if raw_val: - parsed = parse_canonical_date(raw_val, category=category) - if not parsed.is_valid: - errors[field_name] = parsed.error - else: - attrs[field_name] = parsed.raw_value - if errors: - raise serializers.ValidationError(errors) - return super().validate(attrs) - - def create(self, validated_data): - validated_data.pop("remove_thumbnail", False) - sons_ids = validated_data.pop("sons_ids", None) - kunyas_objs = validated_data.pop("kunyas", None) - features_objs = validated_data.pop("features_list", None) - professions_objs = validated_data.pop("professions_list", None) - madhhabs_objs = validated_data.pop("madhhabs", None) - instance = super().create(validated_data) - if sons_ids is not None: - Transmitters.objects.filter(id__in=sons_ids).update(father=instance) - if kunyas_objs is not None: - instance.kunyas.set(kunyas_objs) - if kunyas_objs: - first_kunya = kunyas_objs[0] - instance.kunya = first_kunya.title or [] - instance.save(update_fields=['kunya']) - else: - instance.kunya = [] - instance.save(update_fields=['kunya']) - if features_objs is not None: - instance.features_list.set(features_objs) - if features_objs: - first_feat = features_objs[0] - instance.features = first_feat.title or [] - instance.save(update_fields=['features']) - else: - instance.features = [] - instance.save(update_fields=['features']) - if professions_objs is not None: - instance.professions_list.set(professions_objs) - if professions_objs: - first_prof = professions_objs[0] - instance.profession = first_prof.title or [] - instance.save(update_fields=['profession']) - else: - instance.profession = [] - instance.save(update_fields=['profession']) - if "sahabi_status" in validated_data: - st = validated_data.get("sahabi_status") - instance.companion_type = st.title if st else [] - instance.save(update_fields=['companion_type']) - if madhhabs_objs is not None: - instance.madhhabs.set(madhhabs_objs) - for m in madhhabs_objs: - if m.slug == "sunni": - instance.madhhab = "sunni" - instance.save(update_fields=["madhhab"]) - break - elif m.slug == "shia": - instance.madhhab = "shia" - instance.save(update_fields=["madhhab"]) - break - return instance - - def update(self, instance, validated_data): - remove_thumbnail = validated_data.pop("remove_thumbnail", False) - sons_ids = validated_data.pop("sons_ids", None) - kunyas_objs = validated_data.pop("kunyas", None) - features_objs = validated_data.pop("features_list", None) - professions_objs = validated_data.pop("professions_list", None) - madhhabs_objs = validated_data.pop("madhhabs", None) - if remove_thumbnail and instance.thumbnail: - instance.thumbnail.delete(save=False) - instance.thumbnail = None - instance = super().update(instance, validated_data) - if sons_ids is not None: - instance.children_as_father.exclude(id__in=sons_ids).update(father=None) - Transmitters.objects.filter(id__in=sons_ids).update(father=instance) - if kunyas_objs is not None: - instance.kunyas.set(kunyas_objs) - if kunyas_objs: - first_kunya = kunyas_objs[0] - instance.kunya = first_kunya.title or [] - instance.save(update_fields=['kunya']) - else: - instance.kunya = [] - instance.save(update_fields=['kunya']) - if features_objs is not None: - instance.features_list.set(features_objs) - if features_objs: - first_feat = features_objs[0] - instance.features = first_feat.title or [] - instance.save(update_fields=['features']) - else: - instance.features = [] - instance.save(update_fields=['features']) - if professions_objs is not None: - instance.professions_list.set(professions_objs) - if professions_objs: - first_prof = professions_objs[0] - instance.profession = first_prof.title or [] - instance.save(update_fields=['profession']) - else: - instance.profession = [] - instance.save(update_fields=['profession']) - if "sahabi_status" in validated_data: - st = validated_data.get("sahabi_status") - instance.companion_type = st.title if st else [] - instance.save(update_fields=['companion_type']) - if madhhabs_objs is not None: - instance.madhhabs.set(madhhabs_objs) - for m in madhhabs_objs: - if m.slug == "sunni": - instance.madhhab = "sunni" - instance.save(update_fields=["madhhab"]) - break - elif m.slug == "shia": - instance.madhhab = "shia" - instance.save(update_fields=["madhhab"]) - break - return instance - - -class AdminBookSubjectAreaSerializer(serializers.ModelSerializer): - class Meta: - model = BookSubjectArea - fields = ["id", "title", "created_at", "updated_at"] - read_only_fields = ["id", "created_at", "updated_at"] - - -class AdminBookTypeSerializer(serializers.ModelSerializer): - class Meta: - model = BookType - fields = ["id", "title", "sect", "created_at", "updated_at"] - read_only_fields = ["id", "created_at", "updated_at"] - - -class AdminBookAuthorRutbaSerializer(serializers.ModelSerializer): - title = serializers.JSONField() - description = serializers.JSONField(required=False, default=list) - - class Meta: - model = BookAuthorRutba - fields = ["id", "title", "slug", "color", "main_color_code", "description"] - - -class AdminBookAuthorSerializer(serializers.ModelSerializer): - image = AbsoluteImageField(required=False, allow_null=True) - remove_image = serializers.BooleanField(write_only=True, required=False, default=False) - rutba_status = serializers.PrimaryKeyRelatedField( - queryset=BookAuthorRutba.objects.all(), - required=False, - allow_null=True, - ) - rutba_detail = AdminBookAuthorRutbaSerializer(source="rutba_status", read_only=True) - features_list = serializers.PrimaryKeyRelatedField( - queryset=TransmitterFeature.objects.all(), - many=True, - required=False, - ) - features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) - book_references = serializers.PrimaryKeyRelatedField( - many=True, - required=False, - queryset=BookReference.objects.all(), - ) - birth_year_preview = serializers.SerializerMethodField(read_only=True) - death_year_preview = serializers.SerializerMethodField(read_only=True) - age_at_death_preview = serializers.SerializerMethodField(read_only=True) - origin = serializers.JSONField(required=False, allow_null=True) - lived_in = serializers.JSONField(required=False, allow_null=True) - died_in = serializers.JSONField(required=False, allow_null=True) - origin_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - origin_city_detail = AdminCitySerializer(source="origin_city", read_only=True) - lived_in_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - lived_in_city_detail = AdminCitySerializer(source="lived_in_city", read_only=True) - died_in_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - died_in_city_detail = AdminCitySerializer(source="died_in_city", read_only=True) - - class Meta: - model = BookAuthor - fields = [ - "id", - "name", - "slug", - "image", - "remove_image", - "birth_year_hijri", - "death_year_hijri", - "age_at_death", - "birth_year_preview", - "death_year_preview", - "age_at_death_preview", - "birth_year_miladi", - "death_year_miladi", - "related_narrator", - "known_as", - "scholarity", - "tabaqa", - "rutba", - "rutba_status", - "rutba_detail", - "features", - "features_list", - "features_detail", - "origin", - "origin_city", - "origin_city_detail", - "lived_in", - "lived_in_city", - "lived_in_city_detail", - "died_in", - "died_in_city", - "died_in_city_detail", - "book_references", - ] - read_only_fields = [ - "id", - "slug", - "birth_year_preview", - "death_year_preview", - "age_at_death_preview", - "origin_city_detail", - "lived_in_city_detail", - "died_in_city_detail", - "features_detail", - ] - - def get_birth_year_preview(self, obj): - return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) - - def get_death_year_preview(self, obj): - return _build_admin_date_preview(obj, "death", obj.death_year_hijri) - - def get_age_at_death_preview(self, obj): - return _build_admin_date_preview(obj, "age", getattr(obj, "age_at_death", None)) - - def to_internal_value(self, data): - import json - mutable_data = safe_copy_data(data) - for json_field in ["name", "known_as", "scholarity", "tabaqa", "rutba", "features", "origin", "lived_in", "died_in", "book_references"]: - if json_field in mutable_data: - val = mutable_data.get(json_field) - if isinstance(val, str): - try: - mutable_data[json_field] = json.loads(val) - except ValueError: - pass - for field in [ - "birth_year_hijri", - "death_year_hijri", - "age_at_death", - "birth_year_miladi", - "death_year_miladi", - "related_narrator", - "rutba_status", - "origin_city", - "lived_in_city", - "died_in_city", - ]: - if field in mutable_data: - val = mutable_data.get(field) - if val == "" or val is None or val == "null" or val == "undefined" or val == "none" or (isinstance(val, str) and not val.strip()): - mutable_data[field] = None - elif field in ("birth_year_hijri", "death_year_hijri", "age_at_death") and val is not None: - mutable_data[field] = str(val).strip() - if "features_list" in mutable_data: - val = mutable_data["features_list"] - if isinstance(val, str): - try: - mutable_data["features_list"] = json.loads(val) - except ValueError: - pass - return super().to_internal_value(mutable_data) - - def validate(self, attrs): - from ..utils.date_engine import parse_canonical_date - date_checks = [ - ("birth_year_hijri", "birth"), - ("death_year_hijri", "death"), - ("age_at_death", "age"), - ] - errors = {} - for field_name, category in date_checks: - if field_name in attrs and attrs[field_name] is not None: - raw_val = str(attrs[field_name]).strip() - if raw_val: - parsed = parse_canonical_date(raw_val, category=category) - if not parsed.is_valid: - errors[field_name] = parsed.error - else: - attrs[field_name] = parsed.raw_value - if errors: - raise serializers.ValidationError(errors) - return super().validate(attrs) - - def create(self, validated_data): - validated_data.pop("remove_image", False) - book_references = validated_data.pop("book_references", None) - features_objs = validated_data.pop("features_list", None) - author = super().create(validated_data) - if book_references: - for book in book_references: - book.author = author - book.save(update_fields=["author"]) - if features_objs is not None: - author.features_list.set(features_objs) - if features_objs: - first_feat = features_objs[0] - author.features = first_feat.title or [] - author.save(update_fields=['features']) - else: - author.features = [] - author.save(update_fields=['features']) - return author - - def update(self, instance, validated_data): - remove_image = validated_data.pop("remove_image", False) - book_references = validated_data.pop("book_references", None) - features_objs = validated_data.pop("features_list", None) - if remove_image and instance.image: - instance.image.delete(save=False) - instance.image = None - author = super().update(instance, validated_data) - if book_references is not None: - current_ids = set(author.book_references.values_list('id', flat=True)) - new_ids = {b.id for b in book_references} - to_remove = current_ids - new_ids - if to_remove: - BookReference.objects.filter(id__in=to_remove, author=author).update(author=None) - to_add = new_ids - current_ids - if to_add: - BookReference.objects.filter(id__in=to_add).update(author=author) - if features_objs is not None: - author.features_list.set(features_objs) - if features_objs: - first_feat = features_objs[0] - author.features = first_feat.title or [] - author.save(update_fields=['features']) - else: - author.features = [] - author.save(update_fields=['features']) - return author - - -class AdminBookAttributeSerializer(serializers.ModelSerializer): - class Meta: - model = BookAttribute - fields = ["id", "book_reference", "title", "value"] - - -class AdminBookReferenceImageSerializer(serializers.ModelSerializer): - image = AbsoluteImageField(required=True) - volume = serializers.SerializerMethodField() - - class Meta: - model = BookReferenceImage - fields = ["id", "book_reference", "image", "volume", "order", "description"] - - def get_volume(self, obj): - return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" - - -class AdminHadisReferenceImageSerializer(serializers.ModelSerializer): - thumbnail = AbsoluteImageField(required=True) - hadis_id = serializers.IntegerField(source="reference.hadis_id", read_only=True) - - class Meta: - model = ReferenceImage - fields = ["id", "reference", "hadis_id", "thumbnail", "priority"] - - -class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer): - file = AbsoluteFileField(required=True) - volume = serializers.SerializerMethodField() - - class Meta: - model = BookReferenceDocument - fields = ["id", "book_reference", "file", "volume", "title", "order"] - - def get_volume(self, obj): - return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" - - -class AdminBookTagSerializer(serializers.ModelSerializer): - title = serializers.JSONField(default=list) - - class Meta: - model = BookTag - fields = ["id", "title", "created_at", "updated_at"] - - -class AdminBookReferenceListSerializer(serializers.ModelSerializer): - author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True) - author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True) - tag_id = serializers.IntegerField(source="tag.id", read_only=True, allow_null=True) - tag_detail = AdminBookTagSerializer(source="tag", read_only=True) - volume = serializers.SerializerMethodField() - publisher = serializers.SerializerMethodField() - year_of_publication = serializers.SerializerMethodField() - hadis_count = serializers.SerializerMethodField() - thumbnail = serializers.SerializerMethodField() - image = serializers.SerializerMethodField() - - class Meta: - model = BookReference - fields = [ - "id", - "title", - "slug", - "has_editions", - "priority", - "author_id", - "author_name", - "tag_id", - "tag_detail", - "volume", - "publisher", - "year_of_publication", - "rate", - "hadis_count", - "thumbnail", - "image", - "created_at", - ] - - def get_hadis_count(self, obj): - if hasattr(obj, "annotated_hadis_count"): - return obj.annotated_hadis_count - if not obj or not obj.pk: - return 0 - return Hadis.objects.filter(references__book_reference=obj).distinct().count() - - def get_thumbnail(self, obj): - first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first() - if first_img and first_img.image: - request = self.context.get("request") - if request: - return absolute_https_url(first_img.image.url, request) - return absolute_https_url(first_img.image.url) - first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first() - if first_vol and first_vol.image: - request = self.context.get("request") - if request: - return absolute_https_url(first_vol.image.url, request) - return absolute_https_url(first_vol.image.url) - return None - - def get_image(self, obj): - return self.get_thumbnail(obj) - - def get_volume(self, obj): - vols = list(obj.volumes.all()) - return vols[0].title if vols else "" - - def get_publisher(self, obj): - editions = list(obj.editions.all()) - return editions[0].publisher if editions else [] - - def get_year_of_publication(self, obj): - editions = list(obj.editions.all()) - return editions[0].year_of_publication if editions else "" - - -class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): - subject_area_detail = AdminBookSubjectAreaSerializer(source="subject_area", many=True, read_only=True) - type_detail = AdminBookTypeSerializer(source="type", read_only=True) - author_detail = AdminBookAuthorSerializer(source="author", read_only=True) - tag_detail = AdminBookTagSerializer(source="tag", read_only=True) - - subject_area = serializers.PrimaryKeyRelatedField( - queryset=BookSubjectArea.objects.all(), - many=True, - required=False, - ) - type = serializers.PrimaryKeyRelatedField( - queryset=BookType.objects.all(), - required=False, - allow_null=True, - ) - author = serializers.PrimaryKeyRelatedField( - queryset=BookAuthor.objects.all(), - required=False, - allow_null=True, - ) - tag = serializers.PrimaryKeyRelatedField( - queryset=BookTag.objects.all(), - required=False, - allow_null=True, - ) - - source_url = serializers.SerializerMethodField() - isbn = serializers.SerializerMethodField() - volume = serializers.SerializerMethodField() - number_of_volumes = serializers.SerializerMethodField() - year_of_publication = serializers.SerializerMethodField() - publisher = serializers.SerializerMethodField() - city_of_publication = serializers.SerializerMethodField() - city_of_publication_city = serializers.SerializerMethodField() - city_of_publication_city_detail = serializers.SerializerMethodField() - country_of_publication = serializers.SerializerMethodField() - edition_number = serializers.SerializerMethodField() - hadis_count = serializers.SerializerMethodField() - child_arguments = serializers.SerializerMethodField() - - thumbnail = serializers.SerializerMethodField() - image = serializers.SerializerMethodField() - remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) - remove_image = serializers.BooleanField(write_only=True, required=False, default=False) - - def get_thumbnail(self, obj): - first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first() - if first_img and first_img.image: - request = self.context.get("request") - if request: - return absolute_https_url(first_img.image.url, request) - return absolute_https_url(first_img.image.url) - first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first() - if first_vol and first_vol.image: - request = self.context.get("request") - if request: - return absolute_https_url(first_vol.image.url, request) - return absolute_https_url(first_vol.image.url) - return None - - def get_image(self, obj): - return self.get_thumbnail(obj) - - def get_source_url(self, obj): - ed = obj.editions.first() - return ed.source_url if ed else "" - - def get_isbn(self, obj): - ed = obj.editions.first() - return ed.isbn if ed else "" - - def get_volume(self, obj): - vol = obj.volumes.first() - return vol.title if vol else "" - - def get_number_of_volumes(self, obj): - ed = obj.editions.first() - return ed.number_of_volumes if ed else None - - def get_year_of_publication(self, obj): - ed = obj.editions.first() - return ed.year_of_publication if ed else "" - - def get_publisher(self, obj): - ed = obj.editions.first() - return ed.publisher if ed else [] - - def get_city_of_publication(self, obj): - ed = obj.editions.first() - return ed.city_of_publication if ed else [] - - def get_city_of_publication_city(self, obj): - ed = obj.editions.first() - return ed.city_of_publication_city_id if ed else None - - def get_city_of_publication_city_detail(self, obj): - ed = obj.editions.first() - if ed and ed.city_of_publication_city: - return AdminCitySerializer(ed.city_of_publication_city).data - return None - - def get_country_of_publication(self, obj): - ed = obj.editions.first() - return ed.country_of_publication if ed else [] - - def get_edition_number(self, obj): - ed = obj.editions.first() - return ed.edition_number if ed else "" - - def get_hadis_count(self, obj): - if hasattr(obj, "annotated_hadis_count"): - return obj.annotated_hadis_count - if not obj or not obj.pk: - return 0 - return Hadis.objects.filter(references__book_reference=obj).distinct().count() - - def get_child_arguments(self, obj): - if not obj or not obj.pk: - return [] - hadises = ( - Hadis.objects.filter(references__book_reference=obj) - .select_related("hadis_status", "category") - .distinct() - .order_by("number", "id") - ) - return [ - { - "id": h.id, - "number": h.number, - "title": h.title, - "title_narrator": h.title_narrator, - "slug": h.slug, - "status": h.status, - "source_type": h.category.source_type if h.category else "hadith", - "status_detail": { - "id": h.hadis_status.id, - "title": h.hadis_status.title, - "color": h.hadis_status.color, - "main_color_code": getattr(h.hadis_status, "main_color_code", None), - } if h.hadis_status else None, - } - for h in hadises - ] - - class Meta: - model = BookReference - fields = [ - "id", - "legacy_id", - "source_url", - "title", - "description", - "language", - "has_editions", - "isbn", - "volume", - "number_of_volumes", - "year_of_publication", - "number_page", - "slug", - "publisher", - "subject_area", - "subject_area_detail", - "type", - "type_detail", - "tag", - "tag_detail", - "order", - "priority", - "city_of_publication", - "city_of_publication_city", - "city_of_publication_city_detail", - "country_of_publication", - "edition_number", - "rate", - "author", - "author_detail", - "hadis_count", - "child_arguments", - "thumbnail", - "image", - "remove_thumbnail", - "remove_image", - "created_at", - "updated_at", - "share_link", - ] - read_only_fields = ["id", "slug", "hadis_count", "child_arguments", "created_at", "updated_at", "share_link", "city_of_publication_city", "city_of_publication_city_detail"] - - def to_internal_value(self, data): - import json - mutable_data = safe_copy_data(data) - for json_field in ["title", "description", "language", "publisher", "city_of_publication", "country_of_publication"]: - if json_field in mutable_data: - val = mutable_data.get(json_field) - if isinstance(val, str): - try: - mutable_data[json_field] = json.loads(val) - except ValueError: - pass - - for list_field in ["subject_area", "authors"]: - if list_field in mutable_data: - val = mutable_data.get(list_field) - if isinstance(val, str): - try: - parsed = json.loads(val) - mutable_data[list_field] = parsed if isinstance(parsed, list) else [parsed] - except ValueError: - if val.isdigit(): - mutable_data[list_field] = [int(val)] - elif val.strip(): - mutable_data[list_field] = [int(v.strip()) for v in val.split(",") if v.strip().isdigit()] - else: - mutable_data[list_field] = [] - elif isinstance(val, int): - mutable_data[list_field] = [val] - elif isinstance(val, list): - cleaned_list = [] - for item in val: - if isinstance(item, str): - try: - item_parsed = json.loads(item) - if isinstance(item_parsed, list): - cleaned_list.extend(item_parsed) - else: - cleaned_list.append(item_parsed) - except ValueError: - cleaned_list.append(int(item) if item.isdigit() else item) - else: - cleaned_list.append(item) - mutable_data[list_field] = cleaned_list - - if "authors" in mutable_data and ("author" not in mutable_data or mutable_data.get("author") is None): - auth_val = mutable_data.get("authors") - if isinstance(auth_val, list): - mutable_data["author"] = auth_val[0] if len(auth_val) > 0 else None - elif auth_val and str(auth_val).isdigit(): - mutable_data["author"] = int(auth_val) - - for int_field in ["number_page", "order", "type", "tag", "author"]: - if int_field in mutable_data: - val = mutable_data.get(int_field) - if val == "" or val == "null" or val == "undefined": - mutable_data[int_field] = None - - if "rate" in mutable_data: - val = mutable_data.get("rate") - if val == "" or val == "null" or val == "undefined": - mutable_data["rate"] = None - - if "has_editions" in mutable_data: - val = mutable_data.get("has_editions") - if isinstance(val, str): - mutable_data["has_editions"] = val.lower() in ["true", "1"] - - return super().to_internal_value(mutable_data) - - def to_representation(self, instance): - ret = super().to_representation(instance) - if instance.author_id: - ret["authors"] = [instance.author_id] - if ret.get("author_detail"): - ret["authors_detail"] = [ret["author_detail"]] - else: - ret["authors"] = [] - ret["authors_detail"] = [] - return ret - - def create(self, validated_data): - subject_area = validated_data.pop("subject_area", []) - validated_data.pop("remove_thumbnail", False) - validated_data.pop("remove_image", False) - - request = self.context.get("request") - thumbnail = None - if request and hasattr(request, "FILES"): - thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image") - - book = super().create(validated_data) - if subject_area: - book.subject_area.set(subject_area) - - if thumbnail: - BookReferenceImage.objects.create( - book_reference=book, - book_volume=None, - image=thumbnail, - order=0, - ) - return book - - def update(self, instance, validated_data): - subject_area = validated_data.pop("subject_area", None) - remove_thumbnail = validated_data.pop("remove_thumbnail", False) or validated_data.pop("remove_image", False) - - request = self.context.get("request") - thumbnail = None - if request and hasattr(request, "FILES"): - thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image") - - book = super().update(instance, validated_data) - if subject_area is not None: - book.subject_area.set(subject_area) - - if remove_thumbnail: - instance.images.filter(book_volume__isnull=True, order=0).delete() - - if thumbnail: - main_img = instance.images.filter(book_volume__isnull=True, order=0).first() - if main_img: - main_img.image = thumbnail - main_img.save() - else: - BookReferenceImage.objects.create( - book_reference=instance, - book_volume=None, - image=thumbnail, - order=0, - ) - return book - - - -# Category Quran Verse Admin Serializers -from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation - -class AdminQuranSecondaryTranslationSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(required=False) - language_code = serializers.CharField(max_length=10, default="ru") - translation_text = serializers.CharField(allow_blank=True, default="") - translator = serializers.CharField(max_length=255, allow_blank=True, default="") - - class Meta: - model = QuranSecondaryTranslation - fields = ["id", "language_code", "translation_text", "translator"] - -class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer): - category_detail = AdminHadisCategorySerializer(source="category", read_only=True) - surah_name = serializers.JSONField(default=list) - translation = serializers.JSONField(default=list) - translator = serializers.JSONField(default=list) - secondary_translations = AdminQuranSecondaryTranslationSerializer(many=True, required=False) - - class Meta: - model = CategoryQuranVerse - fields = [ - "id", - "category", - "category_detail", - "surah_name", - "ayah_number", - "arabic_text", - "translation", - "translator", - "secondary_translations", - "created_at", - "updated_at", - ] - read_only_fields = ["id", "created_at", "updated_at"] - - def create(self, validated_data): - from django.db import transaction - with transaction.atomic(): - secondary_data = validated_data.pop("secondary_translations", []) - verse = CategoryQuranVerse.objects.create(**validated_data) - for sec in secondary_data: - QuranSecondaryTranslation.objects.create( - quran_verse=verse, - language_code=sec.get("language_code", "ru"), - translation_text=sec.get("translation_text", ""), - translator=sec.get("translator", ""), - ) - return verse - - def update(self, instance, validated_data): - from django.db import transaction - with transaction.atomic(): - secondary_data = validated_data.pop("secondary_translations", None) - instance.surah_name = validated_data.get("surah_name", instance.surah_name) - instance.ayah_number = validated_data.get("ayah_number", instance.ayah_number) - instance.arabic_text = validated_data.get("arabic_text", instance.arabic_text) - instance.translation = validated_data.get("translation", instance.translation) - instance.translator = validated_data.get("translator", instance.translator) - instance.category = validated_data.get("category", instance.category) - instance.save() - - if secondary_data is not None: - keep_ids = [item.get("id") for item in secondary_data if item.get("id")] - instance.secondary_translations.exclude(id__in=keep_ids).delete() - for sec in secondary_data: - sec_id = sec.get("id") - if sec_id: - QuranSecondaryTranslation.objects.filter(id=sec_id, quran_verse=instance).update( - language_code=sec.get("language_code", "ru"), - translation_text=sec.get("translation_text", ""), - translator=sec.get("translator", ""), - ) - else: - QuranSecondaryTranslation.objects.create( - quran_verse=instance, - language_code=sec.get("language_code", "ru"), - translation_text=sec.get("translation_text", ""), - translator=sec.get("translator", ""), - ) - return instance - - -class AdminHadisTransmitterSerializer(serializers.ModelSerializer): - transmitter_detail = AdminTransmitterListSerializer(source="transmitter", read_only=True) - uncertain_transmitters = serializers.PrimaryKeyRelatedField( - many=True, - queryset=Transmitters.objects.all(), - required=False - ) - uncertain_transmitters_detail = AdminTransmitterListSerializer( - source="uncertain_transmitters", - many=True, - read_only=True - ) - - class Meta: - model = HadisTransmitter - fields = [ - "id", - "hadis", - "transmitter", - "transmitter_detail", - "is_uncertain", - "uncertain_transmitters", - "uncertain_transmitters_detail", - "chain_index", - "narrator_layer", - "status", - "order", - "is_gap", - ] - - def create(self, validated_data): - uncertain_transmitters = validated_data.pop("uncertain_transmitters", None) - instance = super().create(validated_data) - if uncertain_transmitters is not None: - instance.uncertain_transmitters.set(uncertain_transmitters) - return instance - - def update(self, instance, validated_data): - uncertain_transmitters = validated_data.pop("uncertain_transmitters", None) - instance = super().update(instance, validated_data) - if uncertain_transmitters is not None: - instance.uncertain_transmitters.set(uncertain_transmitters) - return instance - - -class AdminScholarlyOpinionSerializer(serializers.ModelSerializer): - opinion_text = serializers.JSONField(required=False, default=list) - status_detail = AdminOpinionStatusSerializer(source="status", read_only=True) - transmitters_count = serializers.SerializerMethodField() - - class Meta: - model = ScholarlyOpinion - fields = [ - "id", - "opinion_text", - "status", - "status_detail", - "transmitters_count", - "created_at", - "updated_at", - ] - - def get_transmitters_count(self, obj): - if hasattr(obj, "transmitters_count_val"): - return obj.transmitters_count_val - return obj.transmitter_opinions.values("transmitter_id").distinct().count() - - -class AdminTransmitterOpinionSerializer(serializers.ModelSerializer): - scholar_name = serializers.JSONField(required=False, default=list) - opinion_text = serializers.JSONField(required=False, default=list) - opinion_detail = AdminScholarlyOpinionSerializer(source="opinion", read_only=True) - status_detail = AdminTransmitterReliabilitySerializer(source="status", read_only=True) - scholar_detail = AdminBookAuthorSerializer(source="scholar", read_only=True) - - class Meta: - model = TransmitterOpinion - fields = [ - "id", - "transmitter", - "scholar", - "scholar_detail", - "opinion", - "opinion_detail", - "scholar_name", - "opinion_text", - "status", - "status_detail", - "order", - "created_at", - "updated_at", - ] - - def to_representation(self, instance): - data = super().to_representation(instance) - if instance.opinion_id and instance.opinion: - data["opinion_text"] = instance.opinion.opinion_text - data["status"] = instance.opinion.status_id - if instance.opinion.status: - data["status_detail"] = AdminOpinionStatusSerializer(instance.opinion.status, context=self.context).data - return data - - -class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer): - title = serializers.JSONField(required=False, default=list) - text = serializers.CharField(required=False, allow_blank=True, default="") - translation = serializers.JSONField(required=False, default=list) - references = AdminOriginalTextReferenceSerializer(many=True, read_only=True) - - # Writable relation fields - book_reference_id = serializers.PrimaryKeyRelatedField( - queryset=BookReference.objects.all(), - required=False, - write_only=True, - allow_null=True - ) - address = serializers.JSONField( - required=False, - write_only=True, - allow_null=True - ) - delete_image_ids = serializers.ListField( - child=serializers.IntegerField(), - required=False, - write_only=True - ) - - class Meta: - model = TransmitterOriginalText - fields = [ - "id", - "transmitter", - "slug", - "title", - "text", - "translation", - "share_link", - "embedded_in", - "order", - "references", - "book_reference_id", - "address", - "delete_image_ids", - ] - - def to_internal_value(self, data): - import json - data = safe_copy_data(data) - - # Handle backward compatibility: if text is passed as a JSON list, extract primary text - text_val = data.get("text") - if isinstance(text_val, str) and (text_val.strip().startswith("[") or text_val.strip().startswith("{")): - try: - parsed = json.loads(text_val) - if isinstance(parsed, list): - p_txt = "" - for it in parsed: - if isinstance(it, dict) and it.get("language_code") == "ar": - p_txt = it.get("text") or it.get("title") or "" - break - if not p_txt and parsed: - p_txt = parsed[0].get("text") or parsed[0].get("title") or "" - data["text"] = p_txt - except Exception: - pass - elif isinstance(text_val, list): - p_txt = "" - for it in text_val: - if isinstance(it, dict) and it.get("language_code") == "ar": - p_txt = it.get("text") or it.get("title") or "" - break - if not p_txt and text_val: - p_txt = text_val[0].get("text") or text_val[0].get("title") or "" - data["text"] = p_txt - - for json_field in ["title", "translation", "address"]: - val = data.get(json_field) - if isinstance(val, str): - try: - data[json_field] = json.loads(val) - except ValueError: - pass - 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 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 - - addr_val = ref.address if ref and ref.address else "" - if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): - try: - ret["address"] = json.loads(addr_val) - except (ValueError, TypeError): - ret["address"] = addr_val - else: - ret["address"] = addr_val - - request = self.context.get("request") - ret["images"] = [ - { - "id": img.id, - "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) - } - for img in (ref.images.all() if ref else []) - ] - return ret - - def create(self, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - # Pre-fill title from book reference if title is empty - title_val = validated_data.get("title") - has_title = isinstance(title_val, list) and any( - isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() - for item in title_val - ) - if not has_title and book_reference_id: - try: - b_ref = BookReference.objects.filter(id=book_reference_id.id if hasattr(book_reference_id, 'id') else book_reference_id).first() - if b_ref and b_ref.title: - validated_data["title"] = b_ref.title - except Exception: - pass - - instance = super().create(validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text") - - if not instance.title or (isinstance(instance.title, list) and not any(isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() for item in instance.title)): - if instance.auto_populate_title(): - instance.save(update_fields=['title', 'slug', 'share_link']) - - return instance - - def update(self, instance, validated_data): - request = self.context.get("request") - book_reference_id = validated_data.pop("book_reference_id", None) - address = validated_data.pop("address", None) - delete_image_ids = validated_data.pop("delete_image_ids", []) - - # Pre-fill title from book reference if title is empty - title_val = validated_data.get("title") - has_title = isinstance(title_val, list) and any( - isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() - for item in title_val - ) - if not has_title and book_reference_id: - try: - b_ref = BookReference.objects.filter(id=book_reference_id.id if hasattr(book_reference_id, 'id') else book_reference_id).first() - if b_ref and b_ref.title: - validated_data["title"] = b_ref.title - except Exception: - pass - - instance = super().update(instance, validated_data) - - ref_data = { - "book_reference_id": book_reference_id, - "address": address, - "delete_image_ids": delete_image_ids, - } - save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text") - - if not instance.title or (isinstance(instance.title, list) and not any(isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() for item in instance.title)): - if instance.auto_populate_title(): - instance.save(update_fields=['title', 'slug', 'share_link']) - - return instance - - -class AdminBookVolumeSerializer(serializers.ModelSerializer): - remove_image = serializers.BooleanField(write_only=True, required=False, default=False) - remove_file = serializers.BooleanField(write_only=True, required=False, default=False) - - class Meta: - model = BookVolume - fields = [ - "id", - "book_reference", - "edition", - "title", - "image", - "file", - "remove_image", - "remove_file", - "created_at", - "updated_at", - ] - - def create(self, validated_data): - validated_data.pop("remove_image", False) - validated_data.pop("remove_file", False) - return super().create(validated_data) - - def update(self, instance, validated_data): - remove_image = validated_data.pop("remove_image", False) - remove_file = validated_data.pop("remove_file", False) - if remove_image and instance.image: - instance.image.delete(save=False) - instance.image = None - if remove_file and instance.file: - instance.file.delete(save=False) - instance.file = None - return super().update(instance, validated_data) - - -class AdminBookResearcherSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(required=False) - name = serializers.JSONField(required=False, default=list) - - class Meta: - model = BookResearcher - fields = ["id", "name", "slug", "author"] - - -class AdminBookEditorSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(required=False) - name = serializers.JSONField(required=False, default=list) - - class Meta: - model = BookEditor - fields = ["id", "name", "slug", "author"] - - -class AdminBookEditionSerializer(serializers.ModelSerializer): - title = serializers.JSONField(required=False, default=list) - books = serializers.PrimaryKeyRelatedField( - queryset=BookReference.objects.all(), - many=True, - required=False, - ) - publisher = serializers.JSONField(required=False, default=list) - city_of_publication = serializers.JSONField(required=False, default=list) - publisher_item = serializers.PrimaryKeyRelatedField( - queryset=BookPublisher.objects.all(), - required=False, - allow_null=True, - ) - publisher_item_detail = AdminBookPublisherSerializer(source="publisher_item", read_only=True) - city_of_publication_city = serializers.PrimaryKeyRelatedField( - queryset=City.objects.all(), - required=False, - allow_null=True, - ) - city_of_publication_city_detail = AdminCitySerializer(source="city_of_publication_city", read_only=True) - country_of_publication = serializers.JSONField(required=False, default=list) - country_of_publication_country = serializers.PrimaryKeyRelatedField( - queryset=Country.objects.all(), - required=False, - allow_null=True, - ) - country_of_publication_country_detail = AdminCountrySerializer(source="country_of_publication_country", read_only=True) - notes = serializers.JSONField(required=False, default=list) - tag = serializers.PrimaryKeyRelatedField( - queryset=BookTag.objects.all(), - required=False, - allow_null=True, - ) - tag_detail = AdminBookTagSerializer(source="tag", read_only=True) - volumes_detail = AdminBookVolumeSerializer(source="volumes", many=True, read_only=True) - number_of_volumes = serializers.SerializerMethodField() - books_detail = serializers.SerializerMethodField() - researchers = AdminBookResearcherSerializer(many=True, required=False) - editors = AdminBookEditorSerializer(many=True, required=False) - researcher_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True) - editor_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True) - - class Meta: - model = BookEdition - fields = [ - "id", - "title", - "book_reference", - "books", - "books_detail", - "publisher", - "publisher_item", - "publisher_item_detail", - "city_of_publication", - "city_of_publication_city", - "city_of_publication_city_detail", - "country_of_publication", - "country_of_publication_country", - "country_of_publication_country_detail", - "edition_number", - "year_of_publication", - "number_of_volumes", - "source_url", - "isbn", - "notes", - "tag", - "tag_detail", - "volumes_detail", - "researchers", - "editors", - "researcher_author_id", - "editor_author_id", - "created_at", - "updated_at", - ] - - def to_representation(self, instance): - ret = super().to_representation(instance) - res = instance.researchers.first() - ret["researcher_author_id"] = res.author_id if res and res.author_id else None - - ed_obj = instance.editors.first() - ret["editor_author_id"] = ed_obj.author_id if ed_obj and ed_obj.author_id else None - ret["books"] = list(instance.books.values_list('id', flat=True)) - ret["books_detail"] = [ - { - "id": b.id, - "title": b.title, - "slug": b.slug, - } - for b in instance.books.all() - ] - return ret - - def get_number_of_volumes(self, obj): - return obj.volumes.count() - - def get_books_detail(self, obj): - return [ - { - "id": b.id, - "title": b.title, - "slug": b.slug, - } - for b in obj.books.all() - ] - - def _handle_author_links(self, edition, validated_data): - request = self.context.get("request") - data = request.data if request else {} - - if "researcher_author_id" in data or "researcher_author_id" in validated_data: - res_val = validated_data.get("researcher_author_id") if "researcher_author_id" in validated_data else data.get("researcher_author_id") - edition.researchers.all().delete() - if res_val and res_val not in ["none", "", "null", None]: - try: - author_id = int(res_val) - author_obj = BookAuthor.objects.filter(id=author_id).first() - if author_obj: - BookResearcher.objects.create( - book_edition=edition, - author=author_obj, - name=author_obj.name, - slug=author_obj.slug - ) - except (ValueError, TypeError): - pass - - if "editor_author_id" in data or "editor_author_id" in validated_data: - ed_val = validated_data.get("editor_author_id") if "editor_author_id" in validated_data else data.get("editor_author_id") - edition.editors.all().delete() - if ed_val and ed_val not in ["none", "", "null", None]: - try: - author_id = int(ed_val) - author_obj = BookAuthor.objects.filter(id=author_id).first() - if author_obj: - BookEditor.objects.create( - book_edition=edition, - author=author_obj, - name=author_obj.name, - slug=author_obj.slug - ) - except (ValueError, TypeError): - pass - - def create(self, validated_data): - books_data = validated_data.pop("books", None) - researchers_data = validated_data.pop("researchers", []) - editors_data = validated_data.pop("editors", []) - has_res_id = "researcher_author_id" in validated_data - has_ed_id = "editor_author_id" in validated_data - res_author_id = validated_data.pop("researcher_author_id", None) - ed_author_id = validated_data.pop("editor_author_id", None) - edition = super().create(validated_data) - for res_data in researchers_data: - BookResearcher.objects.create(book_edition=edition, **res_data) - for ed_data in editors_data: - BookEditor.objects.create(book_edition=edition, **ed_data) - author_links_dict = {} - if has_res_id: - author_links_dict["researcher_author_id"] = res_author_id - if has_ed_id: - author_links_dict["editor_author_id"] = ed_author_id - self._handle_author_links(edition, author_links_dict) - if books_data is not None: - edition.books.set(books_data) - if edition.book_reference and not edition.books.filter(id=edition.book_reference_id).exists(): - edition.books.add(edition.book_reference) - return edition - - def update(self, instance, validated_data): - books_data = validated_data.pop("books", None) - researchers_data = validated_data.pop("researchers", None) - editors_data = validated_data.pop("editors", None) - has_res_id = "researcher_author_id" in validated_data - has_ed_id = "editor_author_id" in validated_data - res_author_id = validated_data.pop("researcher_author_id", None) - ed_author_id = validated_data.pop("editor_author_id", None) - edition = super().update(instance, validated_data) - if researchers_data is not None: - # Delete researchers no longer in the request payload - keep_ids = [r.get("id") for r in researchers_data if r.get("id")] - instance.researchers.exclude(id__in=keep_ids).delete() - for res_data in researchers_data: - res_id = res_data.get("id") - if res_id: - # Update existing researcher - res_data_clean = {k: v for k, v in res_data.items() if k != "id"} - BookResearcher.objects.filter(id=res_id, book_edition=instance).update(**res_data_clean) - res_obj = BookResearcher.objects.filter(id=res_id, book_edition=instance).first() - if res_obj: - res_obj.save() - else: - # Create new researcher - BookResearcher.objects.create(book_edition=instance, **res_data) - - if editors_data is not None: - # Delete editors no longer in the request payload - keep_ids = [e.get("id") for e in editors_data if e.get("id")] - instance.editors.exclude(id__in=keep_ids).delete() - if books_data is not None: - edition.books.set(books_data) - if edition.book_reference and not edition.books.filter(id=edition.book_reference_id).exists(): - edition.books.add(edition.book_reference) - for ed_data in editors_data: - ed_id = ed_data.get("id") - if ed_id: - # Update existing editor - ed_data_clean = {k: v for k, v in ed_data.items() if k != "id"} - BookEditor.objects.filter(id=ed_id, book_edition=instance).update(**ed_data_clean) - ed_obj = BookEditor.objects.filter(id=ed_id, book_edition=instance).first() - if ed_obj: - ed_obj.save() - else: - # Create new editor - BookEditor.objects.create(book_edition=instance, **ed_data) - - author_links_dict = {} - if has_res_id: - author_links_dict["researcher_author_id"] = res_author_id - if has_ed_id: - author_links_dict["editor_author_id"] = ed_author_id - self._handle_author_links(edition, author_links_dict) - return edition - - -class AdminContentReleaseSerializer(serializers.ModelSerializer): - class Meta: - model = ContentRelease - fields = [ - "id", - "version_name", - "published_at", - "description", - "is_active", - ] - read_only_fields = ["id", "published_at"] - +from rest_framework import serializers +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils.translation import gettext_lazy as _ +from django.db.models import Exists, OuterRef, Q +from django.core.exceptions import ValidationError as DjangoValidationError + +from utils.image_compression import maybe_compress_uploaded_file +from utils import absolute_https_url +from ..models import ( + Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage, + BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookPublisher, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, + Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, ScholarlyOpinion, TransmitterOpinion, TransmitterOriginalText, + City, Country, Kunya, TransmitterFeature, TransmitterProfession, SahabiStatus, Madhhab, RelationType, YearValue, NarratorDate, + CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, + OriginalTextReference, OriginalTextReferenceImage, + ContentRelease +) +from .category import LocalizedField, get_localized_text + + +def safe_copy_data(data): + if hasattr(data, "lists"): + new_data = {} + for key, values in data.lists(): + if key in ["subject_area", "authors", "book_references", "delete_image_ids", "uploaded_images"] or key.endswith("[]"): + new_data[key] = values + elif len(values) > 1: + new_data[key] = values + else: + new_data[key] = values[0] if values else None + return new_data + elif hasattr(data, "copy"): + return data.copy() + else: + return dict(data) + + +class AbsoluteImageField(serializers.ImageField): + def to_internal_value(self, data): + uploaded = super().to_internal_value(data) + if not uploaded: + return uploaded + + compressed_bytes = maybe_compress_uploaded_file(uploaded) + if compressed_bytes is None: + return uploaded + + return SimpleUploadedFile( + name=getattr(uploaded, "name", "image"), + content=compressed_bytes, + content_type=getattr(uploaded, "content_type", None), + ) + + def to_representation(self, value): + if not value: + return None + request = self.context.get("request") + url = value.url if hasattr(value, "url") else str(value) + return absolute_https_url(url, request) + + +class AdminHadisCategorySerializer(serializers.ModelSerializer): + sect_type = serializers.CharField(source='sect.sect_type', read_only=True) + title = LocalizedField() + description = LocalizedField() + has_children = serializers.SerializerMethodField() + is_leaf = serializers.SerializerMethodField() + has_hadith = serializers.SerializerMethodField() + has_quran_verse = serializers.SerializerMethodField() + ancestors = serializers.SerializerMethodField() + + def get_has_children(self, obj): + if hasattr(obj, "annotated_has_children"): + return bool(obj.annotated_has_children) + return obj.children.exists() + + def get_is_leaf(self, obj): + if hasattr(obj, "annotated_has_children"): + return not bool(obj.annotated_has_children) + return not obj.children.exists() + + def get_has_hadith(self, obj): + if hasattr(obj, "annotated_has_hadith"): + return bool(obj.annotated_has_hadith) + return obj.hadis_set.exists() or (hasattr(obj, "hadis_items") and obj.hadis_items.exists()) + + def get_has_quran_verse(self, obj): + if hasattr(obj, "annotated_has_quran_verse"): + return bool(obj.annotated_has_quran_verse) + return hasattr(obj, "quran_verse") and obj.quran_verse is not None + + def get_ancestors(self, obj): + if not obj: + return [] + ancestors_qs = obj.get_ancestors(include_self=True).select_related('sect') + return [ + { + "id": a.id, + "title": a.title, + "slug": a.slug, + "is_topic": a.is_topic, + "source_type": a.source_type, + "sect_type": a.sect.sect_type if a.sect else None, + "order": a.order, + "is_root": a.parent_id is None, + "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, + } + for a in ancestors_qs + ] + + class Meta: + model = HadisCategory + fields = [ + "id", + "parent", + "title", + "slug", + "is_topic", + "source_type", + "order", + "sect", + "sect_type", + "description", + "has_children", + "is_leaf", + "has_hadith", + "has_quran_verse", + "ancestors", + ] + + +class AdminHadisStatusSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField() + + class Meta: + model = HadisStatus + fields = ["id", "title", "slug", "color", "main_color_code", "order", "description"] + + +class AdminHadisTagSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = HadisTag + fields = ["id", "title", "status"] + + +class AdminHadisCategoryListSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = HadisCategory + fields = ["id", "title", "slug", "source_type", "order"] + + +class AdminHadisListSerializer(serializers.ModelSerializer): + category_detail = AdminHadisCategoryListSerializer(source="category", read_only=True) + categories_detail = AdminHadisCategoryListSerializer(source="categories", many=True, read_only=True) + hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True) + collections_detail = serializers.SerializerMethodField() + + class Meta: + model = Hadis + fields = [ + "id", + "number", + "slug", + "title", + "title_narrator", + "text", + "translation", + "status", + "category", + "category_detail", + "categories", + "categories_detail", + "hadis_status", + "hadis_status_detail", + "collections_detail", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + def get_collections_detail(self, obj): + return [ + {"id": item.collection_id, "title": item.collection.title, "slug": item.collection.slug} + for item in obj.collection_items.all() + ] + + +class HadisCollectionsField(serializers.PrimaryKeyRelatedField): + def to_representation(self, value): + return value.collection_id + + +class AdminHadisCollectionSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + summary = serializers.JSONField() + thumbnail = AbsoluteImageField(required=False, allow_null=True) + remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) + + class Meta: + model = HadisCollection + fields = [ + "id", + "title", + "slug", + "summary", + "status", + "display_position", + "order", + "thumbnail", + "remove_thumbnail", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at"] + + def to_internal_value(self, data): + import json + mutable_data = safe_copy_data(data) + for json_field in ["title", "summary"]: + val = mutable_data.get(json_field) + if isinstance(val, str): + try: + mutable_data[json_field] = json.loads(val) + except ValueError: + pass + thumbnail = mutable_data.get("thumbnail") if hasattr(mutable_data, "get") else None + if thumbnail and hasattr(thumbnail, "read"): + compressed_bytes = maybe_compress_uploaded_file(thumbnail) + if compressed_bytes is not None: + mutable_data = safe_copy_data(mutable_data) + mutable_data["thumbnail"] = SimpleUploadedFile( + name=getattr(thumbnail, "name", "image"), + content=compressed_bytes, + content_type=getattr(thumbnail, "content_type", None), + ) + return super().to_internal_value(mutable_data) + + def create(self, validated_data): + validated_data.pop("remove_thumbnail", False) + return super().create(validated_data) + + def update(self, instance, validated_data): + remove_thumbnail = validated_data.pop("remove_thumbnail", False) + if remove_thumbnail and instance.thumbnail: + instance.thumbnail.delete(save=False) + instance.thumbnail = None + 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) + categories_detail = AdminHadisCategorySerializer(source="categories", many=True, read_only=True) + hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True) + tags_detail = AdminHadisTagSerializer(source="tags", many=True, read_only=True) + + category = serializers.PrimaryKeyRelatedField( + queryset=HadisCategory.objects.all(), + required=False, + allow_null=True, + ) + categories = serializers.PrimaryKeyRelatedField( + queryset=HadisCategory.objects.all(), + many=True, + required=False, + ) + hadis_status = serializers.PrimaryKeyRelatedField( + queryset=HadisStatus.objects.all(), + required=False, + allow_null=True, + ) + tags = serializers.PrimaryKeyRelatedField( + queryset=HadisTag.objects.all(), + many=True, + required=False, + ) + collections = HadisCollectionsField( + source="collection_items", + queryset=HadisCollection.objects.all(), + many=True, + required=False, + ) + collections_detail = serializers.SerializerMethodField() + category_path = serializers.SerializerMethodField() + categories_paths = serializers.SerializerMethodField() + book_reference_id = serializers.PrimaryKeyRelatedField( + queryset=BookReference.objects.all(), + required=False, + 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() + + def get_category_path(self, obj): + cat = obj.category + if not cat and hasattr(obj, "categories"): + cat = obj.categories.first() + if not cat: + return [] + ancestors_qs = cat.get_ancestors(include_self=True).select_related('sect') + return [ + { + "id": a.id, + "title": a.title, + "slug": a.slug, + "source_type": a.source_type, + "sect_type": a.sect.sect_type if a.sect else None, + "order": a.order, + "is_root": a.parent_id is None, + "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, + } + for a in ancestors_qs + ] + + def get_categories_paths(self, obj): + cats = list(obj.categories.all()) if hasattr(obj, "categories") else [] + if not cats and obj.category: + cats = [obj.category] + all_paths = [] + for cat in cats: + ancestors_qs = cat.get_ancestors(include_self=True).select_related('sect') + all_paths.append([ + { + "id": a.id, + "title": a.title, + "slug": a.slug, + "source_type": a.source_type, + "sect_type": a.sect.sect_type if a.sect else None, + "order": a.order, + "is_root": a.parent_id is None, + "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0, + } + for a in ancestors_qs + ]) + return all_paths + + class Meta: + model = Hadis + fields = [ + "id", + "number", + "slug", + "category", + "category_detail", + "category_path", + "categories", + "categories_detail", + "categories_paths", + "title_narrator", + "title", + "description", + "text", + "translation", + "status", + "hadis_status", + "hadis_status_detail", + "hadis_status_text", + "address", + "links", + "tags", + "tags_detail", + "collections", + "collections_detail", + "book_reference_id", + "edition_id", + "book_volume_id", + "volume", + "pages", + "hadith_number", + "share_link", + "explanation", + "bookmark_count", + "created_at", + "updated_at", + "embedded_in", + ] + read_only_fields = ["id", "slug", "share_link", "bookmark_count", "created_at", "updated_at"] + + 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": 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, request) if img.thumbnail else None + ref_images.append({ + "id": img.id, + "reference_id": r.id, + "hadis_id": instance.id, + "thumbnail": url, + "image": url, + "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): + from apps.bookmark.models.bookmark import Bookmark + return Bookmark.objects.filter(service=Bookmark.ServiceChoices.HADITH, content_id=obj.id, status=True).count() + + def validate_category(self, value): + if value and value.children.exists(): + raise serializers.ValidationError( + _("Cannot assign a hadith/item to a category that has subcategories. Only leaf categories can contain hadiths.") + ) + return value + + def validate_categories(self, values): + for val in values: + if val and val.children.exists(): + raise serializers.ValidationError( + _("Cannot assign a hadith/item to a category that has subcategories. Only leaf categories can contain hadiths.") + ) + return values + + def get_collections_detail(self, obj): + collections = [item.collection for item in obj.collection_items.all()] + return AdminHadisCollectionSerializer(collections, many=True).data + + 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", []) + categories = validated_data.pop("categories", None) + hadis = super().create(validated_data) + if categories is not None: + hadis.categories.set(categories) + if not hadis.category and categories: + hadis.category = categories[0] + hadis.save(update_fields=['category']) + elif hadis.category: + hadis.categories.set([hadis.category]) + if tags: + hadis.tags.set(tags) + if collections: + for idx, col in enumerate(collections): + HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx) + 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 "" + ) + if not hadis.title_narrator or (isinstance(hadis.title_narrator, list) and not any(isinstance(item, dict) and item.get('text', '').strip() for item in hadis.title_narrator)): + if hadis.auto_populate_title_narrator(): + hadis.save(update_fields=['title_narrator', 'normalized_text']) + 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) + categories = validated_data.pop("categories", None) + hadis = super().update(instance, validated_data) + if categories is not None: + hadis.categories.set(categories) + if not hadis.category and categories: + hadis.category = categories[0] + hadis.save(update_fields=['category']) + elif "category" in validated_data and validated_data.get("category"): + if not hadis.categories.exists(): + hadis.categories.set([validated_data["category"]]) + + if tags is not None: + hadis.tags.set(tags) + if collections is not None: + instance.collection_items.all().delete() + for idx, col in enumerate(collections): + HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx) + + 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() + if not hadis.title_narrator or (isinstance(hadis.title_narrator, list) and not any(isinstance(item, dict) and item.get('text', '').strip() for item in hadis.title_narrator)): + if hadis.auto_populate_title_narrator(): + hadis.save(update_fields=['title_narrator', 'normalized_text']) + return hadis + + +class AdminHadisBriefSerializer(serializers.ModelSerializer): + class Meta: + model = Hadis + fields = ["id", "number", "slug", "title", "title_narrator", "status"] + + + +class AdminHadisCategoryBriefSerializer(serializers.ModelSerializer): + title = LocalizedField() + + class Meta: + model = HadisCategory + fields = ["id", "title"] + + +def save_relation_reference_data(instance, validated_data, request=None, is_correction=True, ref_type=None): + book_reference = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + new_images = [] + if request: + files_obj = getattr(request, "FILES", None) + if not files_obj and hasattr(request, "_request"): + files_obj = getattr(request._request, "FILES", None) + + if files_obj: + if hasattr(files_obj, "getlist"): + new_images = files_obj.getlist("uploaded_images") or files_obj.getlist("uploaded_images[]") + if not new_images: + val = files_obj.get("uploaded_images") or files_obj.get("uploaded_images[]") + if val: + new_images = [val] if not isinstance(val, list) else val + + if not new_images and hasattr(request, "data"): + data_obj = request.data + if hasattr(data_obj, "getlist"): + new_images = data_obj.getlist("uploaded_images") or data_obj.getlist("uploaded_images[]") + if not new_images and hasattr(data_obj, "get"): + val = data_obj.get("uploaded_images") or data_obj.get("uploaded_images[]") + if val: + new_images = val if isinstance(val, list) else [val] + + actual_images = [img for img in new_images if img and not isinstance(img, (str, int, float, bool, dict))] + + ref = instance.references.first() + if not ref: + if book_reference or address or actual_images: + if ref_type == "original_text": + ref = OriginalTextReference.objects.create(original_text=instance) + elif is_correction: + ref = CorrectionReference.objects.create(correction=instance) + else: + ref = InterpretationReference.objects.create(interpretation=instance) + else: + return + + if book_reference is not None: + ref.book_reference = book_reference + if address is not None: + if isinstance(address, (list, dict)): + import json + ref.address = json.dumps(address, ensure_ascii=False) + else: + ref.address = str(address) + ref.save() + + if delete_image_ids: + ref.images.filter(id__in=delete_image_ids).delete() + + for img in actual_images: + if ref_type == "original_text": + OriginalTextReferenceImage.objects.create(reference=ref, image=img) + elif is_correction: + CorrectionReferenceImage.objects.create(reference=ref, image=img) + else: + 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( + queryset=BookReference.objects.all(), + required=False, + write_only=True, + allow_null=True + ) + address = serializers.JSONField( + required=False, + write_only=True, + allow_null=True + ) + delete_image_ids = serializers.ListField( + child=serializers.IntegerField(), + required=False, + write_only=True + ) + + class Meta: + model = HadisCorrection + fields = [ + "id", + "hadis", + "hadis_detail", + "title", + "slug", + "text", + "translation", + "priority", + "bookmark_count", + "links", + "book_reference_id", + "address", + "delete_image_ids", + "references", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "slug", "bookmark_count", "references", "created_at", "updated_at"] + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + for json_field in ["title", "translation", "links", "address"]: + val = data.get(json_field) + if isinstance(val, str): + try: + data[json_field] = json.loads(val) + except ValueError: + pass + 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 to_representation(self, instance): + import json + ret = super().to_representation(instance) + refs = list(instance.references.all()) + ref = refs[0] if refs else None + ret["book_reference"] = ref.book_reference_id if ref else None + + addr_val = ref.address if ref and ref.address else "" + if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): + try: + ret["address"] = json.loads(addr_val) + except (ValueError, TypeError): + ret["address"] = addr_val + else: + ret["address"] = addr_val + + request = self.context.get("request") + images = list(ref.images.all()) if ref else [] + ret["images"] = [ + { + "id": img.id, + "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) + } + for img in images + ] + ret["references"] = AdminCorrectionReferenceSerializer(instance.references.all(), many=True, context=self.context).data + return ret + + def create(self, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + instance = super().create(validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, is_correction=True) + return instance + + def update(self, instance, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + instance = super().update(instance, validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, is_correction=True) + 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( + queryset=BookReference.objects.all(), + required=False, + write_only=True, + allow_null=True + ) + address = serializers.JSONField( + required=False, + write_only=True, + allow_null=True + ) + delete_image_ids = serializers.ListField( + child=serializers.IntegerField(), + required=False, + write_only=True + ) + + class Meta: + model = HadisInterpretation + fields = [ + "id", + "category", + "category_detail", + "title", + "text", + "translation", + "priority", + "links", + "book_reference_id", + "address", + "delete_image_ids", + "references", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "references", "created_at", "updated_at"] + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + for json_field in ["title", "translation", "links", "address"]: + val = data.get(json_field) + if isinstance(val, str): + try: + data[json_field] = json.loads(val) + except ValueError: + pass + 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 to_representation(self, instance): + import json + ret = super().to_representation(instance) + refs = list(instance.references.all()) + ref = refs[0] if refs else None + ret["book_reference"] = ref.book_reference_id if ref else None + + addr_val = ref.address if ref and ref.address else "" + if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): + try: + ret["address"] = json.loads(addr_val) + except (ValueError, TypeError): + ret["address"] = addr_val + else: + ret["address"] = addr_val + + request = self.context.get("request") + images = list(ref.images.all()) if ref else [] + ret["images"] = [ + { + "id": img.id, + "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) + } + for img in images + ] + ret["references"] = AdminInterpretationReferenceSerializer(instance.references.all(), many=True, context=self.context).data + return ret + + def create(self, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + instance = super().create(validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, is_correction=False) + return instance + + def update(self, instance, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + instance = super().update(instance, validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, is_correction=False) + return instance + +class AbsoluteFileField(serializers.FileField): + def to_representation(self, value): + if not value: + return None + request = self.context.get("request") + url = value.url if hasattr(value, "url") else str(value) + if request: + return request.build_absolute_uri(url) + return url + + +class AdminHadisSectArchiveSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField() + + class Meta: + model = HadisSect + fields = ["id", "sect_type", "title", "description", "is_active", "order"] + + +class AdminHadisCategoryArchiveParentSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = HadisCategory + fields = ["id", "title", "slug"] + + +class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer): + parent_detail = AdminHadisCategoryArchiveParentSerializer(source="parent", read_only=True) + sect_detail = AdminHadisSectArchiveSerializer(source="sect", read_only=True) + title = serializers.JSONField() + description = serializers.JSONField() + xmind_file = AbsoluteFileField(required=False, allow_null=True) + remove_xmind_file = serializers.BooleanField(write_only=True, required=False, default=False) + has_hadith = serializers.SerializerMethodField() + has_children = serializers.SerializerMethodField() + is_leaf = serializers.SerializerMethodField() + + parent = serializers.PrimaryKeyRelatedField( + queryset=HadisCategory.objects.all(), + required=False, + allow_null=True, + ) + sect = serializers.PrimaryKeyRelatedField( + queryset=HadisSect.objects.all(), + required=True, + ) + + def get_has_hadith(self, obj): + if hasattr(obj, "annotated_has_hadith"): + return bool(obj.annotated_has_hadith) + return obj.hadis_set.exists() + + def get_has_children(self, obj): + if hasattr(obj, "annotated_has_children"): + return bool(obj.annotated_has_children) + return obj.children.exists() + + def get_is_leaf(self, obj): + if hasattr(obj, "annotated_has_children"): + return not bool(obj.annotated_has_children) + return not obj.children.exists() + + def validate_parent(self, value): + if value and value.hadis_set.exists(): + raise serializers.ValidationError( + _("Cannot set parent to a category that already contains hadiths/content items.") + ) + return value + + class Meta: + model = HadisCategory + fields = [ + "id", + "parent", + "parent_detail", + "sect", + "sect_detail", + "is_topic", + "source_type", + "title", + "description", + "order", + "xmind_file", + "remove_xmind_file", + "slug", + "share_link", + "xmind_share_link", + "has_hadith", + "has_children", + "is_leaf", + ] + read_only_fields = ["id", "share_link", "xmind_share_link"] + extra_kwargs = { + "description": {"required": False}, + "source_type": {"required": False, "allow_null": True, "allow_blank": True}, + } + + def validate(self, attrs): + is_topic = attrs.get('is_topic', getattr(self.instance, 'is_topic', False)) + source_type = attrs.get('source_type', getattr(self.instance, 'source_type', None)) + if is_topic: + if not source_type: + raise serializers.ValidationError({"source_type": _("A topic must have a source type.")}) + else: + attrs['source_type'] = None + + parent = attrs.get('parent', getattr(self.instance, 'parent', None)) + if parent and getattr(parent, 'is_topic', False): + raise serializers.ValidationError({"parent": _("Cannot set parent to a topic. Parent must be a category.")}) + + return super().validate(attrs) + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + for json_field in ["title", "description"]: + val = data.get(json_field) + if isinstance(val, str): + try: + data[json_field] = json.loads(val) + except ValueError: + pass + return super().to_internal_value(data) + + @staticmethod + def _raise_drf_validation_error(exc): + if hasattr(exc, "message_dict"): + detail = { + ("non_field_errors" if key == "__all__" else key): value + for key, value in exc.message_dict.items() + } + else: + detail = {"non_field_errors": exc.messages} + raise serializers.ValidationError(detail) + + def create(self, validated_data): + validated_data.pop("remove_xmind_file", False) + try: + return super().create(validated_data) + except DjangoValidationError as exc: + self._raise_drf_validation_error(exc) + + +class AdminHadisCategoryArchiveDetailSerializer(AdminHadisCategoryArchiveSerializer): + ancestors = serializers.SerializerMethodField() + children_type = serializers.SerializerMethodField() + child_categories = serializers.SerializerMethodField() + child_arguments = serializers.SerializerMethodField() + child_count = serializers.SerializerMethodField() + + def get_ancestors(self, obj): + if not obj or not obj.pk: + return [] + ancestors_qs = obj.get_ancestors(include_self=True).select_related("sect") + return [ + { + "id": a.id, + "title": a.title, + "slug": a.slug, + "is_topic": a.is_topic, + "source_type": a.source_type, + "sect_type": a.sect.sect_type if a.sect else None, + "order": a.order, + "is_root": a.parent_id is None, + "level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, "_mptt_meta") else 0, + } + for a in ancestors_qs + ] + + def get_children_type(self, obj): + if not obj or not obj.pk: + return "none" + has_children = bool(getattr(obj, "annotated_has_children", obj.children.exists())) + if has_children: + return "categories" + has_hadith = bool( + getattr( + obj, + "annotated_has_hadith", + obj.hadis_set.exists() or (hasattr(obj, "hadis_items") and obj.hadis_items.exists()) + ) + ) + if has_hadith: + return "arguments" + return "none" + + def get_child_categories(self, obj): + if not obj or not obj.pk: + return [] + child_hadis_subquery = Hadis.objects.filter(Q(category=OuterRef("pk")) | Q(categories=OuterRef("pk"))) + child_children_subquery = HadisCategory.objects.filter(parent=OuterRef("pk")) + children = obj.children.all().annotate( + annotated_has_hadith=Exists(child_hadis_subquery), + annotated_has_children=Exists(child_children_subquery), + ).order_by("order", "id") + return [ + { + "id": c.id, + "title": c.title, + "slug": c.slug, + "order": c.order, + "is_topic": c.is_topic, + "source_type": c.source_type, + "has_children": bool(c.annotated_has_children), + "has_hadith": bool(c.annotated_has_hadith), + } + for c in children + ] + + def get_child_arguments(self, obj): + if not obj or not obj.pk: + return [] + hadis_qs = Hadis.objects.filter( + Q(category=obj) | Q(categories=obj) + ).distinct().select_related("hadis_status").order_by("number", "-id")[:100] + return [ + { + "id": h.id, + "number": h.number, + "title": h.title, + "title_narrator": h.title_narrator, + "slug": h.slug, + "status": h.status, + "status_detail": { + "id": h.hadis_status.id, + "title": h.hadis_status.title, + "color": h.hadis_status.color, + "main_color_code": getattr(h.hadis_status, "main_color_code", None), + } if h.hadis_status else None, + } + for h in hadis_qs + ] + + def get_child_count(self, obj): + if not obj or not obj.pk: + return 0 + cat_count = obj.children.count() + if cat_count > 0: + return cat_count + return Hadis.objects.filter(Q(category=obj) | Q(categories=obj)).distinct().count() + + class Meta(AdminHadisCategoryArchiveSerializer.Meta): + fields = AdminHadisCategoryArchiveSerializer.Meta.fields + [ + "ancestors", + "children_type", + "child_categories", + "child_arguments", + "child_count", + ] + + def update(self, instance, validated_data): + remove_xmind_file = validated_data.pop("remove_xmind_file", False) + if remove_xmind_file and instance.xmind_file: + instance.xmind_file.delete(save=False) + instance.xmind_file = None + try: + return super().update(instance, validated_data) + except DjangoValidationError as exc: + self._raise_drf_validation_error(exc) + +class AdminTransmitterReliabilitySerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = TransmitterReliability + fields = ["id", "title", "slug", "color", "main_color_code", "description"] + + +class AdminNarratorLayerSerializer(serializers.ModelSerializer): + name = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + hadis = serializers.PrimaryKeyRelatedField( + queryset=Hadis.objects.all(), + required=True, + ) + hadis_detail = serializers.SerializerMethodField() + + class Meta: + model = NarratorLayer + fields = [ + "id", + "hadis", + "hadis_detail", + "name", + "number", + "description", + "slug", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "hadis_detail", "created_at", "updated_at"] + + def get_hadis_detail(self, obj): + if not obj.hadis: + return None + return { + "id": obj.hadis.id, + "number": obj.hadis.number, + "slug": obj.hadis.slug, + "title": obj.hadis.title, + } + + +class AdminOpinionStatusSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = OpinionStatus + fields = ["id", "title", "slug", "color", "main_color_code", "description"] + + +class AdminYearValueSerializer(serializers.ModelSerializer): + translations = serializers.JSONField(required=False, default=list) + values_list = serializers.JSONField(required=False, default=list) + narrators_count = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = YearValue + fields = [ + "id", + "category", + "raw_value", + "parse_type", + "value", + "value_min", + "value_max", + "values_list", + "translations", + "narrators_count", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "narrators_count", "created_at", "updated_at"] + + def get_narrators_count(self, obj): + if hasattr(obj, "narrators_count_val"): + return obj.narrators_count_val + if obj.parse_type == "context" and obj.raw_value.startswith("#"): + return NarratorDate.objects.filter(raw_value=obj.raw_value).values("transmitter_id").distinct().count() + return obj.narrator_dates.count() + + def to_representation(self, instance): + rep = super().to_representation(instance) + rep["canonical"] = rep.get("raw_value") + rep["date_type"] = rep.get("category") + rep["date_mode"] = rep.get("parse_type") + return rep + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + if not data.get("raw_value") and data.get("canonical"): + data["raw_value"] = data["canonical"] + if not data.get("category") and data.get("date_type"): + data["category"] = data["date_type"] + if not data.get("parse_type") and data.get("date_mode"): + data["parse_type"] = data["date_mode"] + for jf in ["translations", "values_list"]: + val = data.get(jf) + if isinstance(val, str): + try: + data[jf] = json.loads(val) + except ValueError: + pass + for nf in ["value", "value_min", "value_max"]: + if nf in data and data[nf] in ("", "null", "undefined", None): + data[nf] = None + if not data.get("category"): + data["category"] = "death" + return super().to_internal_value(data) + + def validate(self, attrs): + from ..utils.date_engine import parse_canonical_date, build_translations_for_parsed + category = attrs.get("category", getattr(self.instance, "category", "death")) + raw_value = attrs.get("raw_value", getattr(self.instance, "raw_value", "")) + if raw_value is not None: + raw_str = str(raw_value).strip() + parsed = parse_canonical_date(raw_str, category=category) + if not parsed.is_valid: + raise serializers.ValidationError({"raw_value": parsed.error}) + attrs["raw_value"] = parsed.raw_value + attrs["parse_type"] = parsed.parse_type + if parsed.parse_type != "context": + attrs["value"] = parsed.value + attrs["value_min"] = parsed.value_min + attrs["value_max"] = parsed.value_max + attrs["values_list"] = parsed.values_list + if not attrs.get("translations"): + attrs["translations"] = build_translations_for_parsed(parsed, category=category) + return attrs + + def create(self, validated_data): + raw_value = validated_data.get("raw_value", "") + category = validated_data.get("category", "death") + existing = YearValue.objects.filter(category=category, raw_value=raw_value).first() + if existing: + for k, v in validated_data.items(): + setattr(existing, k, v) + existing.save() + instance = existing + else: + instance = super().create(validated_data) + + # Sync context tag across birth and death so #tag works seamlessly for both + if instance.parse_type == "context" and instance.raw_value.startswith("#"): + other_cat = "birth" if instance.category == "death" else "death" + YearValue.objects.update_or_create( + category=other_cat, + raw_value=instance.raw_value, + defaults={ + "parse_type": "context", + "value": instance.value, + "value_min": instance.value_min, + "value_max": instance.value_max, + "values_list": instance.values_list, + "translations": instance.translations, + } + ) + return instance + + def update(self, instance, validated_data): + instance = super().update(instance, validated_data) + if instance.parse_type == "context" and instance.raw_value.startswith("#"): + YearValue.objects.filter(raw_value=instance.raw_value, parse_type="context").exclude(id=instance.id).update( + value=instance.value, + value_min=instance.value_min, + value_max=instance.value_max, + values_list=instance.values_list, + translations=instance.translations, + ) + return instance + + +class AdminCountrySerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = Country + fields = ["id", "title", "slug", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + +class AdminCitySerializer(serializers.ModelSerializer): + title = serializers.JSONField() + country_detail = AdminCountrySerializer(source="country", read_only=True) + + class Meta: + model = City + fields = ["id", "title", "slug", "country", "country_detail", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "country_detail", "created_at", "updated_at"] + + +class AdminBookPublisherSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + city_detail = AdminCitySerializer(source="city", read_only=True) + country_detail = AdminCountrySerializer(source="country", read_only=True) + + class Meta: + model = BookPublisher + fields = [ + "id", + "title", + "slug", + "city", + "city_detail", + "country", + "country_detail", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "slug", "city_detail", "country_detail", "created_at", "updated_at"] + + +class AdminKunyaSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = Kunya + fields = ["id", "title", "slug", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + + + +class AdminTransmitterFeatureSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = TransmitterFeature + fields = ["id", "title", "slug", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + +class AdminTransmitterProfessionSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + + class Meta: + model = TransmitterProfession + fields = ["id", "title", "slug", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + + + +class AdminSahabiStatusSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = SahabiStatus + fields = ["id", "title", "slug", "description", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + + + + +class AdminRelationTypeSerializer(serializers.ModelSerializer): + code = serializers.CharField(required=False, allow_blank=True) + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = RelationType + fields = ["id", "code", "title", "slug", "description", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + +class AdminMadhhabSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = Madhhab + fields = ["id", "title", "slug", "description", "created_at", "updated_at"] + read_only_fields = ["id", "slug", "created_at", "updated_at"] + + +def _build_admin_date_preview(obj, date_type: str, raw_val: str): + if not raw_val or not str(raw_val).strip(): + return None + return { + "ru": obj.get_formatted_date(date_type, lang="ru"), + "en": obj.get_formatted_date(date_type, lang="en"), + "ar": obj.get_formatted_date(date_type, lang="ar"), + } + + +class AdminTransmitterListSerializer(serializers.ModelSerializer): + reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) + kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True) + features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) + professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True) + sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True) + madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True) + full_name = serializers.JSONField() + known_as = serializers.JSONField() + nickname = serializers.JSONField() + thumbnail = AbsoluteImageField(required=False, allow_null=True) + share_link = serializers.CharField(read_only=True) + birth_year_preview = serializers.SerializerMethodField(read_only=True) + death_year_preview = serializers.SerializerMethodField(read_only=True) + age_at_death_preview = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = Transmitters + fields = [ + "id", + "full_name", + "slug", + "gender", + "birth_year_hijri", + "death_year_hijri", + "age_at_death", + "birth_year_preview", + "death_year_preview", + "age_at_death_preview", + "known_as", + "nickname", + "kunyas_detail", + "features_detail", + "professions_detail", + "sahabi_status", + "sahabi_status_detail", + "reliability", + "reliability_detail", + "madhhab", + "madhhabs_detail", + "generation", + "thumbnail", + "share_link", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at"] + + def get_birth_year_preview(self, obj): + return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) + + def get_death_year_preview(self, obj): + return _build_admin_date_preview(obj, "death", obj.death_year_hijri) + + def get_age_at_death_preview(self, obj): + return _build_admin_date_preview(obj, "age", obj.age_at_death) + + +class AdminTransmitterDetailSerializer(serializers.ModelSerializer): + reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) + full_name = serializers.JSONField() + kunya = serializers.JSONField(required=False, default=list) + kunyas = serializers.PrimaryKeyRelatedField( + queryset=Kunya.objects.all(), + many=True, + required=False, + ) + kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True) + features = serializers.JSONField(required=False, default=list) + features_list = serializers.PrimaryKeyRelatedField( + queryset=TransmitterFeature.objects.all(), + many=True, + required=False, + ) + features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) + profession = serializers.JSONField(required=False, default=list) + professions_list = serializers.PrimaryKeyRelatedField( + queryset=TransmitterProfession.objects.all(), + many=True, + required=False, + ) + professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True) + sahabi_status = serializers.PrimaryKeyRelatedField( + queryset=SahabiStatus.objects.all(), + required=False, + allow_null=True, + ) + sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True) + madhhabs = serializers.PrimaryKeyRelatedField( + queryset=Madhhab.objects.all(), + many=True, + required=False, + ) + madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True) + companion_type = serializers.JSONField(required=False, default=list) + known_as = serializers.JSONField() + nickname = serializers.JSONField() + origin = serializers.JSONField(required=False, allow_null=True) + lived_in = serializers.JSONField(required=False, allow_null=True) + died_in = serializers.JSONField(required=False, allow_null=True) + origin_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + origin_city_detail = AdminCitySerializer(source="origin_city", read_only=True) + lived_in_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + lived_in_city_detail = AdminCitySerializer(source="lived_in_city", read_only=True) + died_in_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + died_in_city_detail = AdminCitySerializer(source="died_in_city", read_only=True) + description = serializers.JSONField() + thumbnail = AbsoluteImageField(required=False, allow_null=True) + remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) + reliability = serializers.PrimaryKeyRelatedField( + queryset=TransmitterReliability.objects.all(), + required=False, + ) + father = serializers.PrimaryKeyRelatedField( + queryset=Transmitters.objects.all(), + required=False, + allow_null=True, + ) + father_detail = serializers.SerializerMethodField(read_only=True) + sons = serializers.SerializerMethodField(read_only=True) + sons_ids = serializers.ListField( + child=serializers.IntegerField(), + required=False, + write_only=True, + ) + share_link = serializers.CharField(read_only=True) + birth_year_preview = serializers.SerializerMethodField(read_only=True) + death_year_preview = serializers.SerializerMethodField(read_only=True) + age_at_death_preview = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = Transmitters + fields = [ + "id", + "full_name", + "kunya", + "kunyas", + "kunyas_detail", + "features", + "features_list", + "features_detail", + "profession", + "professions_list", + "professions_detail", "sahabi_status_detail", "madhhabs_detail", + "sahabi_status", + "sahabi_status_detail", + "companion_type", + "sahabi_status", + "known_as", + "nickname", + "gender", + "slug", + "legacy_number", + "legacy_id", + "birth_year_hijri", + "death_year_hijri", + "age_at_death", + "birth_year_preview", + "death_year_preview", + "age_at_death_preview", + "generation", + "reliability", + "reliability_detail", + "madhhab", + "madhhabs", + "madhhabs_detail", + "in_sahih_muslim", + "in_sahih_bukhari", + "origin", + "origin_city", + "origin_city_detail", + "lived_in", + "lived_in_city", + "lived_in_city_detail", + "died_in", + "died_in_city", + "died_in_city_detail", + "description", + "thumbnail", + "remove_thumbnail", + "father", + "father_detail", + "sons", + "sons_ids", + "share_link", + "created_at", + "updated_at", + ] + read_only_fields = [ + "id", "slug", "share_link", "created_at", "updated_at", + "reliability_detail", "father_detail", "sons", + "birth_year_preview", "death_year_preview", "age_at_death_preview", + "origin_city_detail", "lived_in_city_detail", "died_in_city_detail", + "kunyas_detail", "features_detail", "professions_detail", + ] + + def get_birth_year_preview(self, obj): + return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) + + def get_death_year_preview(self, obj): + return _build_admin_date_preview(obj, "death", obj.death_year_hijri) + + def get_age_at_death_preview(self, obj): + return _build_admin_date_preview(obj, "age", obj.age_at_death) + + def get_father_detail(self, obj): + if obj.father: + name = obj.father.get_name('en') if hasattr(obj.father, 'get_name') else None + if not name and obj.father.full_name and isinstance(obj.father.full_name, list) and len(obj.father.full_name) > 0: + first = obj.father.full_name[0] + if isinstance(first, dict): + name = first.get('text', '—') + return { + "id": obj.father.id, + "full_name": obj.father.full_name, + "name": name or '—', + "slug": obj.father.slug, + } + return None + + def get_sons(self, obj): + result = [] + for son in obj.children_as_father.all(): + name = son.get_name('en') if hasattr(son, 'get_name') else None + if not name and son.full_name and isinstance(son.full_name, list) and len(son.full_name) > 0: + first = son.full_name[0] + if isinstance(first, dict): + name = first.get('text', '—') + result.append({ + "id": son.id, + "full_name": son.full_name, + "name": name or '—', + "slug": son.slug, + }) + return result + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + for json_field in [ + "full_name", + "kunya", + "features", + "profession", + "known_as", + "nickname", + "origin", + "lived_in", + "died_in", + "description", + ]: + val = data.get(json_field) + if isinstance(val, str): + try: + data[json_field] = json.loads(val) + except ValueError: + pass + + # Handle empty/null values for optional fields (prevent unique constraint violations on empty strings) + for field in [ + "legacy_id", + "legacy_number", + "age_at_death", + "generation", + "birth_year_hijri", + "death_year_hijri", + "companion_type", + "father", + "origin_city", + "lived_in_city", + "died_in_city", + ]: + if field in data and (data[field] == "" or data[field] is None or data[field] == "null" or data[field] == "undefined" or (isinstance(data[field], str) and not data[field].strip())): + data[field] = None + elif field in ("birth_year_hijri", "death_year_hijri", "age_at_death") and field in data and data[field] is not None: + data[field] = str(data[field]).strip() + + if "sons_ids" in data: + val = data["sons_ids"] + if isinstance(val, str): + try: + data["sons_ids"] = json.loads(val) + except ValueError: + pass + + if "kunyas" in data: + val = data["kunyas"] + if isinstance(val, str): + try: + data["kunyas"] = json.loads(val) + except ValueError: + pass + + if "features_list" in data: + val = data["features_list"] + if isinstance(val, str): + try: + data["features_list"] = json.loads(val) + except ValueError: + pass + + if "professions_list" in data: + val = data["professions_list"] + if isinstance(val, str): + try: + data["professions_list"] = json.loads(val) + except ValueError: + pass + + if "madhhabs" in data: + val = data["madhhabs"] + if isinstance(val, str): + try: + data["madhhabs"] = json.loads(val) + except ValueError: + pass + + return super().to_internal_value(data) + + def validate(self, attrs): + from ..utils.date_engine import parse_canonical_date + date_checks = [ + ("birth_year_hijri", "birth"), + ("death_year_hijri", "death"), + ("age_at_death", "age"), + ] + errors = {} + for field_name, category in date_checks: + if field_name in attrs and attrs[field_name] is not None: + raw_val = str(attrs[field_name]).strip() + if raw_val: + parsed = parse_canonical_date(raw_val, category=category) + if not parsed.is_valid: + errors[field_name] = parsed.error + else: + attrs[field_name] = parsed.raw_value + if errors: + raise serializers.ValidationError(errors) + return super().validate(attrs) + + def create(self, validated_data): + validated_data.pop("remove_thumbnail", False) + sons_ids = validated_data.pop("sons_ids", None) + kunyas_objs = validated_data.pop("kunyas", None) + features_objs = validated_data.pop("features_list", None) + professions_objs = validated_data.pop("professions_list", None) + madhhabs_objs = validated_data.pop("madhhabs", None) + instance = super().create(validated_data) + if sons_ids is not None: + Transmitters.objects.filter(id__in=sons_ids).update(father=instance) + if kunyas_objs is not None: + instance.kunyas.set(kunyas_objs) + if kunyas_objs: + first_kunya = kunyas_objs[0] + instance.kunya = first_kunya.title or [] + instance.save(update_fields=['kunya']) + else: + instance.kunya = [] + instance.save(update_fields=['kunya']) + if features_objs is not None: + instance.features_list.set(features_objs) + if features_objs: + first_feat = features_objs[0] + instance.features = first_feat.title or [] + instance.save(update_fields=['features']) + else: + instance.features = [] + instance.save(update_fields=['features']) + if professions_objs is not None: + instance.professions_list.set(professions_objs) + if professions_objs: + first_prof = professions_objs[0] + instance.profession = first_prof.title or [] + instance.save(update_fields=['profession']) + else: + instance.profession = [] + instance.save(update_fields=['profession']) + if "sahabi_status" in validated_data: + st = validated_data.get("sahabi_status") + instance.companion_type = st.title if st else [] + instance.save(update_fields=['companion_type']) + if madhhabs_objs is not None: + instance.madhhabs.set(madhhabs_objs) + for m in madhhabs_objs: + if m.slug == "sunni": + instance.madhhab = "sunni" + instance.save(update_fields=["madhhab"]) + break + elif m.slug == "shia": + instance.madhhab = "shia" + instance.save(update_fields=["madhhab"]) + break + return instance + + def update(self, instance, validated_data): + remove_thumbnail = validated_data.pop("remove_thumbnail", False) + sons_ids = validated_data.pop("sons_ids", None) + kunyas_objs = validated_data.pop("kunyas", None) + features_objs = validated_data.pop("features_list", None) + professions_objs = validated_data.pop("professions_list", None) + madhhabs_objs = validated_data.pop("madhhabs", None) + if remove_thumbnail and instance.thumbnail: + instance.thumbnail.delete(save=False) + instance.thumbnail = None + instance = super().update(instance, validated_data) + if sons_ids is not None: + instance.children_as_father.exclude(id__in=sons_ids).update(father=None) + Transmitters.objects.filter(id__in=sons_ids).update(father=instance) + if kunyas_objs is not None: + instance.kunyas.set(kunyas_objs) + if kunyas_objs: + first_kunya = kunyas_objs[0] + instance.kunya = first_kunya.title or [] + instance.save(update_fields=['kunya']) + else: + instance.kunya = [] + instance.save(update_fields=['kunya']) + if features_objs is not None: + instance.features_list.set(features_objs) + if features_objs: + first_feat = features_objs[0] + instance.features = first_feat.title or [] + instance.save(update_fields=['features']) + else: + instance.features = [] + instance.save(update_fields=['features']) + if professions_objs is not None: + instance.professions_list.set(professions_objs) + if professions_objs: + first_prof = professions_objs[0] + instance.profession = first_prof.title or [] + instance.save(update_fields=['profession']) + else: + instance.profession = [] + instance.save(update_fields=['profession']) + if "sahabi_status" in validated_data: + st = validated_data.get("sahabi_status") + instance.companion_type = st.title if st else [] + instance.save(update_fields=['companion_type']) + if madhhabs_objs is not None: + instance.madhhabs.set(madhhabs_objs) + for m in madhhabs_objs: + if m.slug == "sunni": + instance.madhhab = "sunni" + instance.save(update_fields=["madhhab"]) + break + elif m.slug == "shia": + instance.madhhab = "shia" + instance.save(update_fields=["madhhab"]) + break + return instance + + +class AdminBookSubjectAreaSerializer(serializers.ModelSerializer): + class Meta: + model = BookSubjectArea + fields = ["id", "title", "created_at", "updated_at"] + read_only_fields = ["id", "created_at", "updated_at"] + + +class AdminBookTypeSerializer(serializers.ModelSerializer): + class Meta: + model = BookType + fields = ["id", "title", "sect", "created_at", "updated_at"] + read_only_fields = ["id", "created_at", "updated_at"] + + +class AdminBookAuthorRutbaSerializer(serializers.ModelSerializer): + title = serializers.JSONField() + description = serializers.JSONField(required=False, default=list) + + class Meta: + model = BookAuthorRutba + fields = ["id", "title", "slug", "color", "main_color_code", "description"] + + +class AdminBookAuthorSerializer(serializers.ModelSerializer): + image = AbsoluteImageField(required=False, allow_null=True) + remove_image = serializers.BooleanField(write_only=True, required=False, default=False) + rutba_status = serializers.PrimaryKeyRelatedField( + queryset=BookAuthorRutba.objects.all(), + required=False, + allow_null=True, + ) + rutba_detail = AdminBookAuthorRutbaSerializer(source="rutba_status", read_only=True) + features_list = serializers.PrimaryKeyRelatedField( + queryset=TransmitterFeature.objects.all(), + many=True, + required=False, + ) + features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) + book_references = serializers.PrimaryKeyRelatedField( + many=True, + required=False, + queryset=BookReference.objects.all(), + ) + birth_year_preview = serializers.SerializerMethodField(read_only=True) + death_year_preview = serializers.SerializerMethodField(read_only=True) + age_at_death_preview = serializers.SerializerMethodField(read_only=True) + origin = serializers.JSONField(required=False, allow_null=True) + lived_in = serializers.JSONField(required=False, allow_null=True) + died_in = serializers.JSONField(required=False, allow_null=True) + origin_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + origin_city_detail = AdminCitySerializer(source="origin_city", read_only=True) + lived_in_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + lived_in_city_detail = AdminCitySerializer(source="lived_in_city", read_only=True) + died_in_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + died_in_city_detail = AdminCitySerializer(source="died_in_city", read_only=True) + + class Meta: + model = BookAuthor + fields = [ + "id", + "name", + "slug", + "image", + "remove_image", + "birth_year_hijri", + "death_year_hijri", + "age_at_death", + "birth_year_preview", + "death_year_preview", + "age_at_death_preview", + "birth_year_miladi", + "death_year_miladi", + "related_narrator", + "known_as", + "scholarity", + "tabaqa", + "rutba", + "rutba_status", + "rutba_detail", + "features", + "features_list", + "features_detail", + "origin", + "origin_city", + "origin_city_detail", + "lived_in", + "lived_in_city", + "lived_in_city_detail", + "died_in", + "died_in_city", + "died_in_city_detail", + "book_references", + ] + read_only_fields = [ + "id", + "slug", + "birth_year_preview", + "death_year_preview", + "age_at_death_preview", + "origin_city_detail", + "lived_in_city_detail", + "died_in_city_detail", + "features_detail", + ] + + def get_birth_year_preview(self, obj): + return _build_admin_date_preview(obj, "birth", obj.birth_year_hijri) + + def get_death_year_preview(self, obj): + return _build_admin_date_preview(obj, "death", obj.death_year_hijri) + + def get_age_at_death_preview(self, obj): + return _build_admin_date_preview(obj, "age", getattr(obj, "age_at_death", None)) + + def to_internal_value(self, data): + import json + mutable_data = safe_copy_data(data) + for json_field in ["name", "known_as", "scholarity", "tabaqa", "rutba", "features", "origin", "lived_in", "died_in", "book_references"]: + if json_field in mutable_data: + val = mutable_data.get(json_field) + if isinstance(val, str): + try: + mutable_data[json_field] = json.loads(val) + except ValueError: + pass + for field in [ + "birth_year_hijri", + "death_year_hijri", + "age_at_death", + "birth_year_miladi", + "death_year_miladi", + "related_narrator", + "rutba_status", + "origin_city", + "lived_in_city", + "died_in_city", + ]: + if field in mutable_data: + val = mutable_data.get(field) + if val == "" or val is None or val == "null" or val == "undefined" or val == "none" or (isinstance(val, str) and not val.strip()): + mutable_data[field] = None + elif field in ("birth_year_hijri", "death_year_hijri", "age_at_death") and val is not None: + mutable_data[field] = str(val).strip() + if "features_list" in mutable_data: + val = mutable_data["features_list"] + if isinstance(val, str): + try: + mutable_data["features_list"] = json.loads(val) + except ValueError: + pass + return super().to_internal_value(mutable_data) + + def validate(self, attrs): + from ..utils.date_engine import parse_canonical_date + date_checks = [ + ("birth_year_hijri", "birth"), + ("death_year_hijri", "death"), + ("age_at_death", "age"), + ] + errors = {} + for field_name, category in date_checks: + if field_name in attrs and attrs[field_name] is not None: + raw_val = str(attrs[field_name]).strip() + if raw_val: + parsed = parse_canonical_date(raw_val, category=category) + if not parsed.is_valid: + errors[field_name] = parsed.error + else: + attrs[field_name] = parsed.raw_value + if errors: + raise serializers.ValidationError(errors) + return super().validate(attrs) + + def create(self, validated_data): + validated_data.pop("remove_image", False) + book_references = validated_data.pop("book_references", None) + features_objs = validated_data.pop("features_list", None) + author = super().create(validated_data) + if book_references: + for book in book_references: + book.author = author + book.save(update_fields=["author"]) + if features_objs is not None: + author.features_list.set(features_objs) + if features_objs: + first_feat = features_objs[0] + author.features = first_feat.title or [] + author.save(update_fields=['features']) + else: + author.features = [] + author.save(update_fields=['features']) + return author + + def update(self, instance, validated_data): + remove_image = validated_data.pop("remove_image", False) + book_references = validated_data.pop("book_references", None) + features_objs = validated_data.pop("features_list", None) + if remove_image and instance.image: + instance.image.delete(save=False) + instance.image = None + author = super().update(instance, validated_data) + if book_references is not None: + current_ids = set(author.book_references.values_list('id', flat=True)) + new_ids = {b.id for b in book_references} + to_remove = current_ids - new_ids + if to_remove: + BookReference.objects.filter(id__in=to_remove, author=author).update(author=None) + to_add = new_ids - current_ids + if to_add: + BookReference.objects.filter(id__in=to_add).update(author=author) + if features_objs is not None: + author.features_list.set(features_objs) + if features_objs: + first_feat = features_objs[0] + author.features = first_feat.title or [] + author.save(update_fields=['features']) + else: + author.features = [] + author.save(update_fields=['features']) + return author + + +class AdminBookAttributeSerializer(serializers.ModelSerializer): + class Meta: + model = BookAttribute + fields = ["id", "book_reference", "title", "value"] + + +class AdminBookReferenceImageSerializer(serializers.ModelSerializer): + image = AbsoluteImageField(required=True) + volume = serializers.SerializerMethodField() + + class Meta: + model = BookReferenceImage + fields = ["id", "book_reference", "image", "volume", "order", "description"] + + def get_volume(self, obj): + return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" + + +class AdminHadisReferenceImageSerializer(serializers.ModelSerializer): + thumbnail = AbsoluteImageField(required=True) + hadis_id = serializers.IntegerField(source="reference.hadis_id", read_only=True) + + class Meta: + model = ReferenceImage + fields = ["id", "reference", "hadis_id", "thumbnail", "priority"] + + +class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer): + file = AbsoluteFileField(required=True) + volume = serializers.SerializerMethodField() + + class Meta: + model = BookReferenceDocument + fields = ["id", "book_reference", "file", "volume", "title", "order"] + + def get_volume(self, obj): + return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" + + +class AdminBookTagSerializer(serializers.ModelSerializer): + title = serializers.JSONField(default=list) + + class Meta: + model = BookTag + fields = ["id", "title", "created_at", "updated_at"] + + +class AdminBookReferenceListSerializer(serializers.ModelSerializer): + author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True) + author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True) + tag_id = serializers.IntegerField(source="tag.id", read_only=True, allow_null=True) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) + volume = serializers.SerializerMethodField() + publisher = serializers.SerializerMethodField() + year_of_publication = serializers.SerializerMethodField() + hadis_count = serializers.SerializerMethodField() + thumbnail = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() + + class Meta: + model = BookReference + fields = [ + "id", + "title", + "slug", + "has_editions", + "priority", + "author_id", + "author_name", + "tag_id", + "tag_detail", + "volume", + "publisher", + "year_of_publication", + "rate", + "hadis_count", + "thumbnail", + "image", + "created_at", + ] + + def get_hadis_count(self, obj): + if hasattr(obj, "annotated_hadis_count"): + return obj.annotated_hadis_count + if not obj or not obj.pk: + return 0 + return Hadis.objects.filter(references__book_reference=obj).distinct().count() + + def get_thumbnail(self, obj): + first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first() + if first_img and first_img.image: + request = self.context.get("request") + if request: + return absolute_https_url(first_img.image.url, request) + return absolute_https_url(first_img.image.url) + first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first() + if first_vol and first_vol.image: + request = self.context.get("request") + if request: + return absolute_https_url(first_vol.image.url, request) + return absolute_https_url(first_vol.image.url) + return None + + def get_image(self, obj): + return self.get_thumbnail(obj) + + def get_volume(self, obj): + vols = list(obj.volumes.all()) + return vols[0].title if vols else "" + + def get_publisher(self, obj): + editions = list(obj.editions.all()) + return editions[0].publisher if editions else [] + + def get_year_of_publication(self, obj): + editions = list(obj.editions.all()) + return editions[0].year_of_publication if editions else "" + + +class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): + subject_area_detail = AdminBookSubjectAreaSerializer(source="subject_area", many=True, read_only=True) + type_detail = AdminBookTypeSerializer(source="type", read_only=True) + author_detail = AdminBookAuthorSerializer(source="author", read_only=True) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) + + subject_area = serializers.PrimaryKeyRelatedField( + queryset=BookSubjectArea.objects.all(), + many=True, + required=False, + ) + type = serializers.PrimaryKeyRelatedField( + queryset=BookType.objects.all(), + required=False, + allow_null=True, + ) + author = serializers.PrimaryKeyRelatedField( + queryset=BookAuthor.objects.all(), + required=False, + allow_null=True, + ) + tag = serializers.PrimaryKeyRelatedField( + queryset=BookTag.objects.all(), + required=False, + allow_null=True, + ) + + source_url = serializers.SerializerMethodField() + isbn = serializers.SerializerMethodField() + volume = serializers.SerializerMethodField() + number_of_volumes = serializers.SerializerMethodField() + year_of_publication = serializers.SerializerMethodField() + publisher = serializers.SerializerMethodField() + city_of_publication = serializers.SerializerMethodField() + city_of_publication_city = serializers.SerializerMethodField() + city_of_publication_city_detail = serializers.SerializerMethodField() + country_of_publication = serializers.SerializerMethodField() + edition_number = serializers.SerializerMethodField() + hadis_count = serializers.SerializerMethodField() + child_arguments = serializers.SerializerMethodField() + + thumbnail = serializers.SerializerMethodField() + image = serializers.SerializerMethodField() + remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False) + remove_image = serializers.BooleanField(write_only=True, required=False, default=False) + + def get_thumbnail(self, obj): + first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first() + if first_img and first_img.image: + request = self.context.get("request") + if request: + return absolute_https_url(first_img.image.url, request) + return absolute_https_url(first_img.image.url) + first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first() + if first_vol and first_vol.image: + request = self.context.get("request") + if request: + return absolute_https_url(first_vol.image.url, request) + return absolute_https_url(first_vol.image.url) + return None + + def get_image(self, obj): + return self.get_thumbnail(obj) + + def get_source_url(self, obj): + ed = obj.editions.first() + return ed.source_url if ed else "" + + def get_isbn(self, obj): + ed = obj.editions.first() + return ed.isbn if ed else "" + + def get_volume(self, obj): + vol = obj.volumes.first() + return vol.title if vol else "" + + def get_number_of_volumes(self, obj): + ed = obj.editions.first() + return ed.number_of_volumes if ed else None + + def get_year_of_publication(self, obj): + ed = obj.editions.first() + return ed.year_of_publication if ed else "" + + def get_publisher(self, obj): + ed = obj.editions.first() + return ed.publisher if ed else [] + + def get_city_of_publication(self, obj): + ed = obj.editions.first() + return ed.city_of_publication if ed else [] + + def get_city_of_publication_city(self, obj): + ed = obj.editions.first() + return ed.city_of_publication_city_id if ed else None + + def get_city_of_publication_city_detail(self, obj): + ed = obj.editions.first() + if ed and ed.city_of_publication_city: + return AdminCitySerializer(ed.city_of_publication_city).data + return None + + def get_country_of_publication(self, obj): + ed = obj.editions.first() + return ed.country_of_publication if ed else [] + + def get_edition_number(self, obj): + ed = obj.editions.first() + return ed.edition_number if ed else "" + + def get_hadis_count(self, obj): + if hasattr(obj, "annotated_hadis_count"): + return obj.annotated_hadis_count + if not obj or not obj.pk: + return 0 + return Hadis.objects.filter(references__book_reference=obj).distinct().count() + + def get_child_arguments(self, obj): + if not obj or not obj.pk: + return [] + hadises = ( + Hadis.objects.filter(references__book_reference=obj) + .select_related("hadis_status", "category") + .distinct() + .order_by("number", "id") + ) + return [ + { + "id": h.id, + "number": h.number, + "title": h.title, + "title_narrator": h.title_narrator, + "slug": h.slug, + "status": h.status, + "source_type": h.category.source_type if h.category else "hadith", + "status_detail": { + "id": h.hadis_status.id, + "title": h.hadis_status.title, + "color": h.hadis_status.color, + "main_color_code": getattr(h.hadis_status, "main_color_code", None), + } if h.hadis_status else None, + } + for h in hadises + ] + + class Meta: + model = BookReference + fields = [ + "id", + "legacy_id", + "source_url", + "title", + "description", + "language", + "has_editions", + "isbn", + "volume", + "number_of_volumes", + "year_of_publication", + "number_page", + "slug", + "publisher", + "subject_area", + "subject_area_detail", + "type", + "type_detail", + "tag", + "tag_detail", + "order", + "priority", + "city_of_publication", + "city_of_publication_city", + "city_of_publication_city_detail", + "country_of_publication", + "edition_number", + "rate", + "author", + "author_detail", + "hadis_count", + "child_arguments", + "thumbnail", + "image", + "remove_thumbnail", + "remove_image", + "created_at", + "updated_at", + "share_link", + ] + read_only_fields = ["id", "slug", "hadis_count", "child_arguments", "created_at", "updated_at", "share_link", "city_of_publication_city", "city_of_publication_city_detail"] + + def to_internal_value(self, data): + import json + mutable_data = safe_copy_data(data) + for json_field in ["title", "description", "language", "publisher", "city_of_publication", "country_of_publication"]: + if json_field in mutable_data: + val = mutable_data.get(json_field) + if isinstance(val, str): + try: + mutable_data[json_field] = json.loads(val) + except ValueError: + pass + + for list_field in ["subject_area", "authors"]: + if list_field in mutable_data: + val = mutable_data.get(list_field) + if isinstance(val, str): + try: + parsed = json.loads(val) + mutable_data[list_field] = parsed if isinstance(parsed, list) else [parsed] + except ValueError: + if val.isdigit(): + mutable_data[list_field] = [int(val)] + elif val.strip(): + mutable_data[list_field] = [int(v.strip()) for v in val.split(",") if v.strip().isdigit()] + else: + mutable_data[list_field] = [] + elif isinstance(val, int): + mutable_data[list_field] = [val] + elif isinstance(val, list): + cleaned_list = [] + for item in val: + if isinstance(item, str): + try: + item_parsed = json.loads(item) + if isinstance(item_parsed, list): + cleaned_list.extend(item_parsed) + else: + cleaned_list.append(item_parsed) + except ValueError: + cleaned_list.append(int(item) if item.isdigit() else item) + else: + cleaned_list.append(item) + mutable_data[list_field] = cleaned_list + + if "authors" in mutable_data and ("author" not in mutable_data or mutable_data.get("author") is None): + auth_val = mutable_data.get("authors") + if isinstance(auth_val, list): + mutable_data["author"] = auth_val[0] if len(auth_val) > 0 else None + elif auth_val and str(auth_val).isdigit(): + mutable_data["author"] = int(auth_val) + + for int_field in ["number_page", "order", "type", "tag", "author"]: + if int_field in mutable_data: + val = mutable_data.get(int_field) + if val == "" or val == "null" or val == "undefined": + mutable_data[int_field] = None + + if "rate" in mutable_data: + val = mutable_data.get("rate") + if val == "" or val == "null" or val == "undefined": + mutable_data["rate"] = None + + if "has_editions" in mutable_data: + val = mutable_data.get("has_editions") + if isinstance(val, str): + mutable_data["has_editions"] = val.lower() in ["true", "1"] + + return super().to_internal_value(mutable_data) + + def to_representation(self, instance): + ret = super().to_representation(instance) + if instance.author_id: + ret["authors"] = [instance.author_id] + if ret.get("author_detail"): + ret["authors_detail"] = [ret["author_detail"]] + else: + ret["authors"] = [] + ret["authors_detail"] = [] + return ret + + def create(self, validated_data): + subject_area = validated_data.pop("subject_area", []) + validated_data.pop("remove_thumbnail", False) + validated_data.pop("remove_image", False) + + request = self.context.get("request") + thumbnail = None + if request and hasattr(request, "FILES"): + thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image") + + book = super().create(validated_data) + if subject_area: + book.subject_area.set(subject_area) + + if thumbnail: + BookReferenceImage.objects.create( + book_reference=book, + book_volume=None, + image=thumbnail, + order=0, + ) + return book + + def update(self, instance, validated_data): + subject_area = validated_data.pop("subject_area", None) + remove_thumbnail = validated_data.pop("remove_thumbnail", False) or validated_data.pop("remove_image", False) + + request = self.context.get("request") + thumbnail = None + if request and hasattr(request, "FILES"): + thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image") + + book = super().update(instance, validated_data) + if subject_area is not None: + book.subject_area.set(subject_area) + + if remove_thumbnail: + instance.images.filter(book_volume__isnull=True, order=0).delete() + + if thumbnail: + main_img = instance.images.filter(book_volume__isnull=True, order=0).first() + if main_img: + main_img.image = thumbnail + main_img.save() + else: + BookReferenceImage.objects.create( + book_reference=instance, + book_volume=None, + image=thumbnail, + order=0, + ) + return book + + + +# Category Quran Verse Admin Serializers +from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation + +class AdminQuranSecondaryTranslationSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + language_code = serializers.CharField(max_length=10, default="ru") + translation_text = serializers.CharField(allow_blank=True, default="") + translator = serializers.CharField(max_length=255, allow_blank=True, default="") + + class Meta: + model = QuranSecondaryTranslation + fields = ["id", "language_code", "translation_text", "translator"] + +class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer): + category_detail = AdminHadisCategorySerializer(source="category", read_only=True) + surah_name = serializers.JSONField(default=list) + translation = serializers.JSONField(default=list) + translator = serializers.JSONField(default=list) + secondary_translations = AdminQuranSecondaryTranslationSerializer(many=True, required=False) + + class Meta: + model = CategoryQuranVerse + fields = [ + "id", + "category", + "category_detail", + "surah_name", + "ayah_number", + "arabic_text", + "translation", + "translator", + "secondary_translations", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at"] + + def create(self, validated_data): + from django.db import transaction + with transaction.atomic(): + secondary_data = validated_data.pop("secondary_translations", []) + verse = CategoryQuranVerse.objects.create(**validated_data) + for sec in secondary_data: + QuranSecondaryTranslation.objects.create( + quran_verse=verse, + language_code=sec.get("language_code", "ru"), + translation_text=sec.get("translation_text", ""), + translator=sec.get("translator", ""), + ) + return verse + + def update(self, instance, validated_data): + from django.db import transaction + with transaction.atomic(): + secondary_data = validated_data.pop("secondary_translations", None) + instance.surah_name = validated_data.get("surah_name", instance.surah_name) + instance.ayah_number = validated_data.get("ayah_number", instance.ayah_number) + instance.arabic_text = validated_data.get("arabic_text", instance.arabic_text) + instance.translation = validated_data.get("translation", instance.translation) + instance.translator = validated_data.get("translator", instance.translator) + instance.category = validated_data.get("category", instance.category) + instance.save() + + if secondary_data is not None: + keep_ids = [item.get("id") for item in secondary_data if item.get("id")] + instance.secondary_translations.exclude(id__in=keep_ids).delete() + for sec in secondary_data: + sec_id = sec.get("id") + if sec_id: + QuranSecondaryTranslation.objects.filter(id=sec_id, quran_verse=instance).update( + language_code=sec.get("language_code", "ru"), + translation_text=sec.get("translation_text", ""), + translator=sec.get("translator", ""), + ) + else: + QuranSecondaryTranslation.objects.create( + quran_verse=instance, + language_code=sec.get("language_code", "ru"), + translation_text=sec.get("translation_text", ""), + translator=sec.get("translator", ""), + ) + return instance + + +class AdminHadisTransmitterSerializer(serializers.ModelSerializer): + transmitter_detail = AdminTransmitterListSerializer(source="transmitter", read_only=True) + uncertain_transmitters = serializers.PrimaryKeyRelatedField( + many=True, + queryset=Transmitters.objects.all(), + required=False + ) + uncertain_transmitters_detail = AdminTransmitterListSerializer( + source="uncertain_transmitters", + many=True, + read_only=True + ) + + class Meta: + model = HadisTransmitter + fields = [ + "id", + "hadis", + "transmitter", + "transmitter_detail", + "is_uncertain", + "uncertain_transmitters", + "uncertain_transmitters_detail", + "chain_index", + "narrator_layer", + "status", + "order", + "is_gap", + ] + + def create(self, validated_data): + uncertain_transmitters = validated_data.pop("uncertain_transmitters", None) + instance = super().create(validated_data) + if uncertain_transmitters is not None: + instance.uncertain_transmitters.set(uncertain_transmitters) + return instance + + def update(self, instance, validated_data): + uncertain_transmitters = validated_data.pop("uncertain_transmitters", None) + instance = super().update(instance, validated_data) + if uncertain_transmitters is not None: + instance.uncertain_transmitters.set(uncertain_transmitters) + return instance + + +class AdminScholarlyOpinionSerializer(serializers.ModelSerializer): + opinion_text = serializers.JSONField(required=False, default=list) + status_detail = AdminOpinionStatusSerializer(source="status", read_only=True) + transmitters_count = serializers.SerializerMethodField() + + class Meta: + model = ScholarlyOpinion + fields = [ + "id", + "opinion_text", + "status", + "status_detail", + "transmitters_count", + "created_at", + "updated_at", + ] + + def get_transmitters_count(self, obj): + if hasattr(obj, "transmitters_count_val"): + return obj.transmitters_count_val + return obj.transmitter_opinions.values("transmitter_id").distinct().count() + + +class AdminTransmitterOpinionSerializer(serializers.ModelSerializer): + scholar_name = serializers.JSONField(required=False, default=list) + opinion_text = serializers.JSONField(required=False, default=list) + opinion_detail = AdminScholarlyOpinionSerializer(source="opinion", read_only=True) + status_detail = AdminTransmitterReliabilitySerializer(source="status", read_only=True) + scholar_detail = AdminBookAuthorSerializer(source="scholar", read_only=True) + + class Meta: + model = TransmitterOpinion + fields = [ + "id", + "transmitter", + "scholar", + "scholar_detail", + "opinion", + "opinion_detail", + "scholar_name", + "opinion_text", + "status", + "status_detail", + "order", + "created_at", + "updated_at", + ] + + def to_representation(self, instance): + data = super().to_representation(instance) + if instance.opinion_id and instance.opinion: + data["opinion_text"] = instance.opinion.opinion_text + data["status"] = instance.opinion.status_id + if instance.opinion.status: + data["status_detail"] = AdminOpinionStatusSerializer(instance.opinion.status, context=self.context).data + return data + + +class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer): + title = serializers.JSONField(required=False, default=list) + text = serializers.CharField(required=False, allow_blank=True, default="") + translation = serializers.JSONField(required=False, default=list) + references = AdminOriginalTextReferenceSerializer(many=True, read_only=True) + + # Writable relation fields + book_reference_id = serializers.PrimaryKeyRelatedField( + queryset=BookReference.objects.all(), + required=False, + write_only=True, + allow_null=True + ) + address = serializers.JSONField( + required=False, + write_only=True, + allow_null=True + ) + delete_image_ids = serializers.ListField( + child=serializers.IntegerField(), + required=False, + write_only=True + ) + + class Meta: + model = TransmitterOriginalText + fields = [ + "id", + "transmitter", + "slug", + "title", + "text", + "translation", + "share_link", + "embedded_in", + "order", + "references", + "book_reference_id", + "address", + "delete_image_ids", + ] + + def to_internal_value(self, data): + import json + data = safe_copy_data(data) + + # Handle backward compatibility: if text is passed as a JSON list, extract primary text + text_val = data.get("text") + if isinstance(text_val, str) and (text_val.strip().startswith("[") or text_val.strip().startswith("{")): + try: + parsed = json.loads(text_val) + if isinstance(parsed, list): + p_txt = "" + for it in parsed: + if isinstance(it, dict) and it.get("language_code") == "ar": + p_txt = it.get("text") or it.get("title") or "" + break + if not p_txt and parsed: + p_txt = parsed[0].get("text") or parsed[0].get("title") or "" + data["text"] = p_txt + except Exception: + pass + elif isinstance(text_val, list): + p_txt = "" + for it in text_val: + if isinstance(it, dict) and it.get("language_code") == "ar": + p_txt = it.get("text") or it.get("title") or "" + break + if not p_txt and text_val: + p_txt = text_val[0].get("text") or text_val[0].get("title") or "" + data["text"] = p_txt + + for json_field in ["title", "translation", "address"]: + val = data.get(json_field) + if isinstance(val, str): + try: + data[json_field] = json.loads(val) + except ValueError: + pass + 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 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 + + addr_val = ref.address if ref and ref.address else "" + if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")): + try: + ret["address"] = json.loads(addr_val) + except (ValueError, TypeError): + ret["address"] = addr_val + else: + ret["address"] = addr_val + + request = self.context.get("request") + ret["images"] = [ + { + "id": img.id, + "image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url) + } + for img in (ref.images.all() if ref else []) + ] + return ret + + def create(self, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + # Pre-fill title from book reference if title is empty + title_val = validated_data.get("title") + has_title = isinstance(title_val, list) and any( + isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() + for item in title_val + ) + if not has_title and book_reference_id: + try: + b_ref = BookReference.objects.filter(id=book_reference_id.id if hasattr(book_reference_id, 'id') else book_reference_id).first() + if b_ref and b_ref.title: + validated_data["title"] = b_ref.title + except Exception: + pass + + instance = super().create(validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text") + + if not instance.title or (isinstance(instance.title, list) and not any(isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() for item in instance.title)): + if instance.auto_populate_title(): + instance.save(update_fields=['title', 'slug', 'share_link']) + + return instance + + def update(self, instance, validated_data): + request = self.context.get("request") + book_reference_id = validated_data.pop("book_reference_id", None) + address = validated_data.pop("address", None) + delete_image_ids = validated_data.pop("delete_image_ids", []) + + # Pre-fill title from book reference if title is empty + title_val = validated_data.get("title") + has_title = isinstance(title_val, list) and any( + isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() + for item in title_val + ) + if not has_title and book_reference_id: + try: + b_ref = BookReference.objects.filter(id=book_reference_id.id if hasattr(book_reference_id, 'id') else book_reference_id).first() + if b_ref and b_ref.title: + validated_data["title"] = b_ref.title + except Exception: + pass + + instance = super().update(instance, validated_data) + + ref_data = { + "book_reference_id": book_reference_id, + "address": address, + "delete_image_ids": delete_image_ids, + } + save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text") + + if not instance.title or (isinstance(instance.title, list) and not any(isinstance(item, dict) and (item.get('text', '') or item.get('title', '')).strip() for item in instance.title)): + if instance.auto_populate_title(): + instance.save(update_fields=['title', 'slug', 'share_link']) + + return instance + + +class AdminBookVolumeSerializer(serializers.ModelSerializer): + remove_image = serializers.BooleanField(write_only=True, required=False, default=False) + remove_file = serializers.BooleanField(write_only=True, required=False, default=False) + + class Meta: + model = BookVolume + fields = [ + "id", + "book_reference", + "edition", + "title", + "image", + "file", + "remove_image", + "remove_file", + "created_at", + "updated_at", + ] + + def create(self, validated_data): + validated_data.pop("remove_image", False) + validated_data.pop("remove_file", False) + return super().create(validated_data) + + def update(self, instance, validated_data): + remove_image = validated_data.pop("remove_image", False) + remove_file = validated_data.pop("remove_file", False) + if remove_image and instance.image: + instance.image.delete(save=False) + instance.image = None + if remove_file and instance.file: + instance.file.delete(save=False) + instance.file = None + return super().update(instance, validated_data) + + +class AdminBookResearcherSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + name = serializers.JSONField(required=False, default=list) + + class Meta: + model = BookResearcher + fields = ["id", "name", "slug", "author"] + + +class AdminBookEditorSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + name = serializers.JSONField(required=False, default=list) + + class Meta: + model = BookEditor + fields = ["id", "name", "slug", "author"] + + +class AdminBookEditionSerializer(serializers.ModelSerializer): + title = serializers.JSONField(required=False, default=list) + books = serializers.PrimaryKeyRelatedField( + queryset=BookReference.objects.all(), + many=True, + required=False, + ) + publisher = serializers.JSONField(required=False, default=list) + city_of_publication = serializers.JSONField(required=False, default=list) + publisher_item = serializers.PrimaryKeyRelatedField( + queryset=BookPublisher.objects.all(), + required=False, + allow_null=True, + ) + publisher_item_detail = AdminBookPublisherSerializer(source="publisher_item", read_only=True) + city_of_publication_city = serializers.PrimaryKeyRelatedField( + queryset=City.objects.all(), + required=False, + allow_null=True, + ) + city_of_publication_city_detail = AdminCitySerializer(source="city_of_publication_city", read_only=True) + country_of_publication = serializers.JSONField(required=False, default=list) + country_of_publication_country = serializers.PrimaryKeyRelatedField( + queryset=Country.objects.all(), + required=False, + allow_null=True, + ) + country_of_publication_country_detail = AdminCountrySerializer(source="country_of_publication_country", read_only=True) + notes = serializers.JSONField(required=False, default=list) + tag = serializers.PrimaryKeyRelatedField( + queryset=BookTag.objects.all(), + required=False, + allow_null=True, + ) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) + volumes_detail = AdminBookVolumeSerializer(source="volumes", many=True, read_only=True) + number_of_volumes = serializers.SerializerMethodField() + books_detail = serializers.SerializerMethodField() + researchers = AdminBookResearcherSerializer(many=True, required=False) + editors = AdminBookEditorSerializer(many=True, required=False) + researcher_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True) + editor_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True) + + class Meta: + model = BookEdition + fields = [ + "id", + "title", + "book_reference", + "books", + "books_detail", + "publisher", + "publisher_item", + "publisher_item_detail", + "city_of_publication", + "city_of_publication_city", + "city_of_publication_city_detail", + "country_of_publication", + "country_of_publication_country", + "country_of_publication_country_detail", + "edition_number", + "year_of_publication", + "number_of_volumes", + "source_url", + "isbn", + "notes", + "tag", + "tag_detail", + "volumes_detail", + "researchers", + "editors", + "researcher_author_id", + "editor_author_id", + "created_at", + "updated_at", + ] + + def to_representation(self, instance): + ret = super().to_representation(instance) + # Use prefetched relations (no extra queries per row) + researchers = list(instance.researchers.all()) + editors = list(instance.editors.all()) + res = researchers[0] if researchers else None + ret["researcher_author_id"] = res.author_id if res and res.author_id else None + ed_obj = editors[0] if editors else None + ret["editor_author_id"] = ed_obj.author_id if ed_obj and ed_obj.author_id else None + ret["books"] = [b.id for b in instance.books.all()] + return ret + + def get_number_of_volumes(self, obj): + return len(obj.volumes.all()) + + def get_books_detail(self, obj): + return [ + { + "id": b.id, + "title": b.title, + "slug": b.slug, + } + for b in obj.books.all() + ] + + def _handle_author_links(self, edition, validated_data): + request = self.context.get("request") + data = request.data if request else {} + + if "researcher_author_id" in data or "researcher_author_id" in validated_data: + res_val = validated_data.get("researcher_author_id") if "researcher_author_id" in validated_data else data.get("researcher_author_id") + edition.researchers.all().delete() + if res_val and res_val not in ["none", "", "null", None]: + try: + author_id = int(res_val) + author_obj = BookAuthor.objects.filter(id=author_id).first() + if author_obj: + BookResearcher.objects.create( + book_edition=edition, + author=author_obj, + name=author_obj.name, + slug=author_obj.slug + ) + except (ValueError, TypeError): + pass + + if "editor_author_id" in data or "editor_author_id" in validated_data: + ed_val = validated_data.get("editor_author_id") if "editor_author_id" in validated_data else data.get("editor_author_id") + edition.editors.all().delete() + if ed_val and ed_val not in ["none", "", "null", None]: + try: + author_id = int(ed_val) + author_obj = BookAuthor.objects.filter(id=author_id).first() + if author_obj: + BookEditor.objects.create( + book_edition=edition, + author=author_obj, + name=author_obj.name, + slug=author_obj.slug + ) + except (ValueError, TypeError): + pass + + def create(self, validated_data): + books_data = validated_data.pop("books", None) + researchers_data = validated_data.pop("researchers", []) + editors_data = validated_data.pop("editors", []) + has_res_id = "researcher_author_id" in validated_data + has_ed_id = "editor_author_id" in validated_data + res_author_id = validated_data.pop("researcher_author_id", None) + ed_author_id = validated_data.pop("editor_author_id", None) + edition = super().create(validated_data) + for res_data in researchers_data: + BookResearcher.objects.create(book_edition=edition, **res_data) + for ed_data in editors_data: + BookEditor.objects.create(book_edition=edition, **ed_data) + author_links_dict = {} + if has_res_id: + author_links_dict["researcher_author_id"] = res_author_id + if has_ed_id: + author_links_dict["editor_author_id"] = ed_author_id + self._handle_author_links(edition, author_links_dict) + if books_data is not None: + edition.books.set(books_data) + if edition.book_reference and not edition.books.filter(id=edition.book_reference_id).exists(): + edition.books.add(edition.book_reference) + return edition + + def update(self, instance, validated_data): + books_data = validated_data.pop("books", None) + researchers_data = validated_data.pop("researchers", None) + editors_data = validated_data.pop("editors", None) + has_res_id = "researcher_author_id" in validated_data + has_ed_id = "editor_author_id" in validated_data + res_author_id = validated_data.pop("researcher_author_id", None) + ed_author_id = validated_data.pop("editor_author_id", None) + edition = super().update(instance, validated_data) + if researchers_data is not None: + # Delete researchers no longer in the request payload + keep_ids = [r.get("id") for r in researchers_data if r.get("id")] + instance.researchers.exclude(id__in=keep_ids).delete() + for res_data in researchers_data: + res_id = res_data.get("id") + if res_id: + # Update existing researcher + res_data_clean = {k: v for k, v in res_data.items() if k != "id"} + BookResearcher.objects.filter(id=res_id, book_edition=instance).update(**res_data_clean) + res_obj = BookResearcher.objects.filter(id=res_id, book_edition=instance).first() + if res_obj: + res_obj.save() + else: + # Create new researcher + BookResearcher.objects.create(book_edition=instance, **res_data) + + if editors_data is not None: + # Delete editors no longer in the request payload + keep_ids = [e.get("id") for e in editors_data if e.get("id")] + instance.editors.exclude(id__in=keep_ids).delete() + for ed_data in editors_data: + ed_id = ed_data.get("id") + if ed_id: + # Update existing editor + ed_data_clean = {k: v for k, v in ed_data.items() if k != "id"} + BookEditor.objects.filter(id=ed_id, book_edition=instance).update(**ed_data_clean) + ed_obj = BookEditor.objects.filter(id=ed_id, book_edition=instance).first() + if ed_obj: + ed_obj.save() + else: + # Create new editor + BookEditor.objects.create(book_edition=instance, **ed_data) + + if books_data is not None: + edition.books.set(books_data) + if edition.book_reference and not edition.books.filter(id=edition.book_reference_id).exists(): + edition.books.add(edition.book_reference) + elif not edition.book_reference and edition.books.exists(): + first_b = edition.books.first() + if first_b: + edition.book_reference = first_b + edition.save(update_fields=['book_reference']) + + author_links_dict = {} + if has_res_id: + author_links_dict["researcher_author_id"] = res_author_id + if has_ed_id: + author_links_dict["editor_author_id"] = ed_author_id + self._handle_author_links(edition, author_links_dict) + return edition + + +class AdminContentReleaseSerializer(serializers.ModelSerializer): + class Meta: + model = ContentRelease + fields = [ + "id", + "version_name", + "published_at", + "description", + "is_active", + ] + read_only_fields = ["id", "published_at"] + diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 7e6e62f..8aeb403 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -2565,13 +2565,19 @@ class AdminBookEditionViewSet(ModelViewSet): serializer_class = AdminBookEditionSerializer - pagination_class = None + pagination_class = OptionalPagination def get_queryset(self): - queryset = BookEdition.objects.all().prefetch_related("books", "volumes") + queryset = BookEdition.objects.all().select_related( + "tag", + "publisher_item__city__country", + "publisher_item__country", + "city_of_publication_city__country", + "country_of_publication_country", + ).prefetch_related("books", "volumes", "researchers", "editors") book_reference_id = self.request.query_params.get("book_reference_id") @@ -2583,9 +2589,13 @@ class AdminBookEditionViewSet(ModelViewSet): if search_query: from django.db.models import Q queryset = queryset.filter( + Q(title__icontains=search_query) | Q(edition_number__icontains=search_query) | - Q(isbn__icontains=search_query) - ) + Q(publisher__icontains=search_query) | + Q(year_of_publication__icontains=search_query) | + Q(isbn__icontains=search_query) | + Q(books__title__icontains=search_query) + ).distinct() return queryset.order_by("-created_at")