diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index 376383c..27b9d42 100644 --- a/apps/hadis/models/hadis.py +++ b/apps/hadis/models/hadis.py @@ -525,7 +525,29 @@ class HadisCorrection(models.Model): title = self.title[0]['text'] if self.title else "No Title" return f"{self.hadis.number} - {title}" + @staticmethod + def _has_localized_value(value): + return isinstance(value, list) and any( + isinstance(x, dict) and str(x.get('text', '') or x.get('title', '')).strip() + for x in value + ) + + def auto_populate_title(self): + """ + Auto-fills self.title if empty, using the title of the first linked BookReference (source). + """ + if self._has_localized_value(self.title): + return False + if self.pk: + ref = self.references.filter(book_reference__isnull=False).select_related('book_reference').order_by('id').first() + if ref and ref.book_reference and self._has_localized_value(ref.book_reference.title): + self.title = ref.book_reference.title + return True + return False + def save(self, *args, **kwargs): + if not self._has_localized_value(self.title): + self.auto_populate_title() # Generate smart slug from utils.slug import build_slug_for_instance build_slug_for_instance(self, source_field='title') @@ -550,7 +572,7 @@ class HadisCorrection(models.Model): ) super().save(*args, **kwargs) - + @property def share_link(self): if self.slug and self.hadis_id and self.hadis and self.hadis.slug: @@ -604,8 +626,33 @@ class HadisInterpretation(models.Model): class Meta: verbose_name = _("Hadis Interpretation") verbose_name_plural = _("Hadis Interpretations") + + @staticmethod + def _has_localized_value(value): + return isinstance(value, list) and any( + isinstance(x, dict) and str(x.get('text', '') or x.get('title', '')).strip() + for x in value + ) + + def auto_populate_title(self): + """ + Auto-fills self.title if empty, using the title of the first linked BookReference (source). + """ + if self._has_localized_value(self.title): + return False + if self.pk: + ref = self.references.filter(book_reference__isnull=False).select_related('book_reference').order_by('id').first() + if ref and ref.book_reference and self._has_localized_value(ref.book_reference.title): + self.title = ref.book_reference.title + return True + return False def save(self, *args, **kwargs): + + # Auto-fill title from the first source (book reference) if empty + if not self._has_localized_value(self.title): + self.auto_populate_title() + # 👈 ست کردن اسلاگ تفسیر برابر با اسلاگ کتگوری if self.category and self.category.slug: self.slug = self.category.slug diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 438b8f7..3754e59 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -1,3960 +1,7875 @@ -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"] - +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", []) + + # اگر تایتل فرستاده نشده بود و کتاب انتخاب شده بود، تایتل را مستقیم از کتاب بخواند + 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: + validated_data["title"] = book_reference_id.title + + 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) + + # اطمینان از پر شدن تایتل در صورت خالی بودن + 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']) + + 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) + + 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']) + + 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", []) + + 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: + validated_data["title"] = book_reference_id.title + + 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) + + 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']) + + 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) + + 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']) + + 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"] + + +