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 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, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText, CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, OriginalTextReference, OriginalTextReferenceImage, ContentRelease ) from .category import LocalizedField 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 from utils.watermark import apply_watermark_to_upload watermarked = apply_watermark_to_upload(uploaded, position="bottom_left") target_file = watermarked or uploaded compressed_bytes = maybe_compress_uploaded_file(target_file) if compressed_bytes is None: return target_file return SimpleUploadedFile( name=getattr(target_file, "name", "image"), content=compressed_bytes, content_type=getattr(target_file, "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() 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() 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, "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", "source_type", "order", "sect", "sect_type", "description", "has_children", "is_leaf", "has_hadith", "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) 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", "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 AdminHadisDetailSerializer(serializers.ModelSerializer): category_detail = AdminHadisCategorySerializer(source="category", 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, ) 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() book_reference_id = serializers.PrimaryKeyRelatedField( queryset=BookReference.objects.all(), required=False, write_only=True, allow_null=True, ) bookmark_count = serializers.SerializerMethodField() def get_category_path(self, obj): if not obj.category: return [] ancestors_qs = obj.category.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 ] class Meta: model = Hadis fields = [ "id", "number", "slug", "category", "category_detail", "category_path", "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", "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) refs = list(instance.references.all()) ref = refs[0] if refs else None ret["book_reference"] = ref.book_reference_id if ref else None if ref and ref.book_reference: ret["book_reference_detail"] = { "id": ref.book_reference.id, "title": ref.book_reference.title, "slug": ref.book_reference.slug, } else: ret["book_reference_detail"] = None ref_images = [] for r in refs: for img in r.images.all().order_by("priority", "id"): url = absolute_https_url(img.thumbnail.url, self.context.get("request")) if img.thumbnail else None 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 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 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) tags = validated_data.pop("tags", []) collections = validated_data.pop("collection_items", []) hadis = super().create(validated_data) 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: HadisReference.objects.create(hadis=hadis, book_reference=book_reference) 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) tags = validated_data.pop("tags", None) collections = validated_data.pop("collection_items", None) hadis = super().update(instance, validated_data) 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) if book_reference_provided: ref = instance.references.first() if ref: ref.book_reference = book_reference ref.save() elif book_reference: HadisReference.objects.create(hadis=hadis, book_reference=book_reference) return hadis class AdminHadisBriefSerializer(serializers.ModelSerializer): class Meta: model = Hadis fields = ["id", "number", "title"] 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 AdminHadisCorrectionSerializer(serializers.ModelSerializer): hadis_detail = AdminHadisBriefSerializer(source="hadis", read_only=True) bookmark_count = serializers.IntegerField(source="annotated_bookmark_count", 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", "created_at", "updated_at", ] read_only_fields = ["id", "slug", "bookmark_count", "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 ] 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 AdminHadisInterpretationSerializer(serializers.ModelSerializer): category_detail = AdminHadisCategoryBriefSerializer(source="category", 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", "created_at", "updated_at", ] read_only_fields = ["id", "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 ] 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", "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"] 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) def create(self, validated_data): validated_data.pop("remove_xmind_file", False) return super().create(validated_data) 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, "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())) 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(category=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, "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 [] 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 obj.hadis_set.all().select_related("hadis_status").order_by("number", "-id")[:100] ] 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 obj.hadis_set.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 return super().update(instance, validated_data) class AdminTransmitterReliabilitySerializer(serializers.ModelSerializer): class Meta: model = TransmitterReliability fields = ["id", "title", "slug", "color", "main_color_code"] 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() class Meta: model = OpinionStatus fields = ["id", "title", "slug", "color", "main_color_code"] class AdminTransmitterListSerializer(serializers.ModelSerializer): reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", 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) class Meta: model = Transmitters fields = [ "id", "full_name", "slug", "birth_year_hijri", "death_year_hijri", "known_as", "nickname", "reliability", "reliability_detail", "madhhab", "generation", "thumbnail", "share_link", "created_at", "updated_at", ] read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at"] class AdminTransmitterDetailSerializer(serializers.ModelSerializer): reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) full_name = serializers.JSONField() kunya = serializers.JSONField() known_as = serializers.JSONField() nickname = serializers.JSONField() origin = serializers.JSONField() lived_in = serializers.JSONField() died_in = serializers.JSONField() 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) class Meta: model = Transmitters fields = [ "id", "full_name", "kunya", "known_as", "nickname", "slug", "legacy_number", "legacy_id", "birth_year_hijri", "death_year_hijri", "age_at_death", "generation", "reliability", "reliability_detail", "madhhab", "in_sahih_muslim", "in_sahih_bukhari", "origin", "lived_in", "died_in", "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"] 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", "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", ]: 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 if "sons_ids" in data: val = data["sons_ids"] if isinstance(val, str): try: data["sons_ids"] = json.loads(val) except ValueError: pass return super().to_internal_value(data) def create(self, validated_data): validated_data.pop("remove_thumbnail", False) sons_ids = validated_data.pop("sons_ids", None) instance = super().create(validated_data) if sons_ids is not None: Transmitters.objects.filter(id__in=sons_ids).update(father=instance) return instance def update(self, instance, validated_data): remove_thumbnail = validated_data.pop("remove_thumbnail", False) sons_ids = validated_data.pop("sons_ids", 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) 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 AdminBookAuthorSerializer(serializers.ModelSerializer): image = AbsoluteImageField(required=False, allow_null=True) remove_image = serializers.BooleanField(write_only=True, required=False, default=False) book_references = serializers.PrimaryKeyRelatedField( many=True, required=False, queryset=BookReference.objects.all(), ) class Meta: model = BookAuthor fields = [ "id", "name", "slug", "image", "remove_image", "birth_year_hijri", "death_year_hijri", "birth_year_miladi", "death_year_miladi", "related_narrator", "known_as", "scholarity", "tabaqa", "rutba", "features", "book_references", ] read_only_fields = ["id", "slug"] 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", "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 int_field in ["birth_year_hijri", "death_year_hijri", "birth_year_miladi", "death_year_miladi", "related_narrator"]: 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 return super().to_internal_value(mutable_data) def create(self, validated_data): validated_data.pop("remove_image", False) book_references = validated_data.pop("book_references", None) author = super().create(validated_data) if book_references: for book in book_references: book.author = author book.save(update_fields=["author"]) return author def update(self, instance, validated_data): remove_image = validated_data.pop("remove_image", False) book_references = validated_data.pop("book_references", 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) 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() thumbnail = serializers.SerializerMethodField() image = serializers.SerializerMethodField() class Meta: model = BookReference fields = [ "id", "title", "slug", "has_editions", "author_id", "author_name", "tag_id", "tag_detail", "volume", "publisher", "year_of_publication", "rate", "thumbnail", "image", "created_at", ] 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() country_of_publication = serializers.SerializerMethodField() edition_number = 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_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 "" 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", "city_of_publication", "country_of_publication", "edition_number", "rate", "author", "author_detail", "thumbnail", "image", "remove_thumbnail", "remove_image", "created_at", "updated_at", "share_link", ] read_only_fields = ["id", "slug", "created_at", "updated_at", "share_link"] 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) translation_text = serializers.JSONField(default=list) translator = serializers.JSONField(default=list) class Meta: model = QuranSecondaryTranslation fields = ["id", "translation_text", "translator"] class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer): 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", "surah_name", "ayah_number", "arabic_text", "translation", "translator", "secondary_translations", ] def create(self, validated_data): secondary_data = validated_data.pop("secondary_translations", []) verse = CategoryQuranVerse.objects.create(**validated_data) for sec in secondary_data: QuranSecondaryTranslation.objects.create(quran_verse=verse, **sec) return verse def update(self, instance, validated_data): 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( translation_text=sec.get("translation_text", []), translator=sec.get("translator", []) ) else: QuranSecondaryTranslation.objects.create(quran_verse=instance, **sec) 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 AdminTransmitterOpinionSerializer(serializers.ModelSerializer): scholar_name = serializers.JSONField(required=False, default=list) opinion_text = serializers.JSONField(required=False, default=list) 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", "scholar_name", "opinion_text", "status", "status_detail", "order", "created_at", "updated_at", ] class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer): title = serializers.JSONField(required=False, default=list) text = serializers.JSONField(required=False, default=list) translation = serializers.JSONField(required=False, default=list) # 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", "book_reference_id", "address", "delete_image_ids", ] def to_internal_value(self, data): import json data = safe_copy_data(data) for json_field in ["title", "text", "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) 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", []) 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") 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, ref_type="original_text") 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): publisher = serializers.JSONField(required=False, default=list) city_of_publication = serializers.JSONField(required=False, default=list) country_of_publication = serializers.JSONField(required=False, default=list) 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() 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", "book_reference", "publisher", "city_of_publication", "country_of_publication", "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 return ret def get_number_of_volumes(self, obj): return obj.volumes.count() 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): 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) return edition def update(self, instance, validated_data): 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) 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"]