You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

3960 lines
159 KiB

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"]