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.
 
 
 
 
 

2052 lines
74 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
from utils.image_compression import maybe_compress_uploaded_file
from utils import absolute_https_url
from ..models import (
Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText,
CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage,
OriginalTextReference, OriginalTextReferenceImage,
ContentRelease
)
from .category import LocalizedField
def safe_copy_data(data):
if hasattr(data, "lists"):
new_data = {}
for key, values in data.lists():
if key in ["subject_area", "authors", "book_references", "delete_image_ids", "uploaded_images"] or key.endswith("[]"):
new_data[key] = values
elif len(values) > 1:
new_data[key] = values
else:
new_data[key] = values[0] if values else None
return new_data
elif hasattr(data, "copy"):
return data.copy()
else:
return dict(data)
class AbsoluteImageField(serializers.ImageField):
def to_representation(self, value):
if not value:
return None
request = self.context.get("request")
url = value.url if hasattr(value, "url") else str(value)
return absolute_https_url(url, request)
class AdminHadisCategorySerializer(serializers.ModelSerializer):
sect_type = serializers.CharField(source='sect.sect_type', read_only=True)
title = LocalizedField()
description = LocalizedField()
has_children = serializers.SerializerMethodField()
is_leaf = serializers.SerializerMethodField()
has_hadith = serializers.SerializerMethodField()
ancestors = serializers.SerializerMethodField()
def get_has_children(self, obj):
if hasattr(obj, "annotated_has_children"):
return bool(obj.annotated_has_children)
return obj.children.exists()
def get_is_leaf(self, obj):
if hasattr(obj, "annotated_has_children"):
return not bool(obj.annotated_has_children)
return not obj.children.exists()
def get_has_hadith(self, obj):
if hasattr(obj, "annotated_has_hadith"):
return bool(obj.annotated_has_hadith)
return obj.hadis_set.exists()
def get_ancestors(self, obj):
if not obj:
return []
ancestors_qs = obj.get_ancestors(include_self=True).select_related('sect')
return [
{
"id": a.id,
"title": a.title,
"slug": a.slug,
"source_type": a.source_type,
"sect_type": a.sect.sect_type if a.sect else None,
"order": a.order,
"is_root": a.parent_id is None,
"level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0,
}
for a in ancestors_qs
]
class Meta:
model = HadisCategory
fields = [
"id",
"parent",
"title",
"slug",
"source_type",
"order",
"sect",
"sect_type",
"description",
"has_children",
"is_leaf",
"has_hadith",
"ancestors",
]
class AdminHadisStatusSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
description = serializers.JSONField()
class Meta:
model = HadisStatus
fields = ["id", "title", "slug", "color", "main_color_code", "order", "description"]
class AdminHadisTagSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = HadisTag
fields = ["id", "title", "status"]
class AdminHadisCategoryListSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = HadisCategory
fields = ["id", "title", "slug", "source_type", "order"]
class AdminHadisListSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategoryListSerializer(source="category", read_only=True)
hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True)
collections_detail = serializers.SerializerMethodField()
class Meta:
model = Hadis
fields = [
"id",
"number",
"slug",
"title",
"title_narrator",
"text",
"translation",
"status",
"category",
"category_detail",
"hadis_status",
"hadis_status_detail",
"collections_detail",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "created_at", "updated_at"]
def get_collections_detail(self, obj):
return [
{"id": item.collection_id, "title": item.collection.title, "slug": item.collection.slug}
for item in obj.collection_items.all()
]
class HadisCollectionsField(serializers.PrimaryKeyRelatedField):
def to_representation(self, value):
return value.collection_id
class AdminHadisCollectionSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
summary = serializers.JSONField()
thumbnail = AbsoluteImageField(required=False, allow_null=True)
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
class Meta:
model = HadisCollection
fields = [
"id",
"title",
"slug",
"summary",
"status",
"display_position",
"order",
"thumbnail",
"remove_thumbnail",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
mutable_data = safe_copy_data(data)
for json_field in ["title", "summary"]:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
thumbnail = mutable_data.get("thumbnail") if hasattr(mutable_data, "get") else None
if thumbnail and hasattr(thumbnail, "read"):
compressed_bytes = maybe_compress_uploaded_file(thumbnail)
if compressed_bytes is not None:
mutable_data = safe_copy_data(mutable_data)
mutable_data["thumbnail"] = SimpleUploadedFile(
name=getattr(thumbnail, "name", "image"),
content=compressed_bytes,
content_type=getattr(thumbnail, "content_type", None),
)
return super().to_internal_value(mutable_data)
def create(self, validated_data):
validated_data.pop("remove_thumbnail", False)
return super().create(validated_data)
def update(self, instance, validated_data):
remove_thumbnail = validated_data.pop("remove_thumbnail", False)
if remove_thumbnail and instance.thumbnail:
instance.thumbnail.delete(save=False)
instance.thumbnail = None
return super().update(instance, validated_data)
class AdminHadisDetailSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategorySerializer(source="category", read_only=True)
hadis_status_detail = AdminHadisStatusSerializer(source="hadis_status", read_only=True)
tags_detail = AdminHadisTagSerializer(source="tags", many=True, read_only=True)
category = serializers.PrimaryKeyRelatedField(
queryset=HadisCategory.objects.all(),
required=False,
allow_null=True,
)
hadis_status = serializers.PrimaryKeyRelatedField(
queryset=HadisStatus.objects.all(),
required=False,
allow_null=True,
)
tags = serializers.PrimaryKeyRelatedField(
queryset=HadisTag.objects.all(),
many=True,
required=False,
)
collections = HadisCollectionsField(
source="collection_items",
queryset=HadisCollection.objects.all(),
many=True,
required=False,
)
collections_detail = serializers.SerializerMethodField()
category_path = serializers.SerializerMethodField()
book_reference_id = serializers.PrimaryKeyRelatedField(
queryset=BookReference.objects.all(),
required=False,
write_only=True,
allow_null=True,
)
bookmark_count = serializers.SerializerMethodField()
def get_category_path(self, obj):
if not obj.category:
return []
ancestors_qs = obj.category.get_ancestors(include_self=True).select_related('sect')
return [
{
"id": a.id,
"title": a.title,
"slug": a.slug,
"source_type": a.source_type,
"sect_type": a.sect.sect_type if a.sect else None,
"order": a.order,
"is_root": a.parent_id is None,
"level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, '_mptt_meta') else 0,
}
for a in ancestors_qs
]
class Meta:
model = Hadis
fields = [
"id",
"number",
"slug",
"category",
"category_detail",
"category_path",
"title_narrator",
"title",
"description",
"text",
"translation",
"status",
"hadis_status",
"hadis_status_detail",
"hadis_status_text",
"address",
"links",
"tags",
"tags_detail",
"collections",
"collections_detail",
"book_reference_id",
"share_link",
"explanation",
"bookmark_count",
"created_at",
"updated_at",
"embedded_in",
]
read_only_fields = ["id", "slug", "share_link", "bookmark_count", "created_at", "updated_at"]
def to_representation(self, instance):
ret = super().to_representation(instance)
refs = list(instance.references.all())
ref = refs[0] if refs else None
ret["book_reference"] = ref.book_reference_id if ref else None
if ref and ref.book_reference:
ret["book_reference_detail"] = {
"id": ref.book_reference.id,
"title": ref.book_reference.title,
"slug": ref.book_reference.slug,
}
else:
ret["book_reference_detail"] = None
ref_images = []
for r in refs:
for img in r.images.all().order_by("priority", "id"):
url = absolute_https_url(img.thumbnail.url, self.context.get("request")) if img.thumbnail else None
ref_images.append({
"id": img.id,
"reference_id": r.id,
"hadis_id": instance.id,
"thumbnail": url,
"image": url,
"priority": img.priority,
})
ret["reference_images"] = ref_images
return ret
def get_bookmark_count(self, obj):
from apps.bookmark.models.bookmark import Bookmark
return Bookmark.objects.filter(service=Bookmark.ServiceChoices.HADITH, content_id=obj.id, status=True).count()
def validate_category(self, value):
if value and value.children.exists():
raise serializers.ValidationError(
_("Cannot assign a hadith/item to a category that has subcategories. Only leaf categories can contain hadiths.")
)
return value
def get_collections_detail(self, obj):
collections = [item.collection for item in obj.collection_items.all()]
return AdminHadisCollectionSerializer(collections, many=True).data
def create(self, validated_data):
book_reference = validated_data.pop("book_reference_id", None)
tags = validated_data.pop("tags", [])
collections = validated_data.pop("collection_items", [])
hadis = super().create(validated_data)
if tags:
hadis.tags.set(tags)
if collections:
for idx, col in enumerate(collections):
HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx)
if book_reference:
HadisReference.objects.create(hadis=hadis, book_reference=book_reference)
return hadis
def update(self, instance, validated_data):
book_reference_provided = "book_reference_id" in validated_data
book_reference = validated_data.pop("book_reference_id", None)
tags = validated_data.pop("tags", None)
collections = validated_data.pop("collection_items", None)
hadis = super().update(instance, validated_data)
if tags is not None:
hadis.tags.set(tags)
if collections is not None:
instance.collection_items.all().delete()
for idx, col in enumerate(collections):
HadisInCollection.objects.create(hadis=hadis, collection=col, order=idx)
if book_reference_provided:
ref = instance.references.first()
if ref:
ref.book_reference = book_reference
ref.save()
elif book_reference:
HadisReference.objects.create(hadis=hadis, book_reference=book_reference)
return hadis
class AdminHadisBriefSerializer(serializers.ModelSerializer):
class Meta:
model = Hadis
fields = ["id", "number", "title"]
class AdminHadisCategoryBriefSerializer(serializers.ModelSerializer):
title = LocalizedField()
class Meta:
model = HadisCategory
fields = ["id", "title"]
def save_relation_reference_data(instance, validated_data, request=None, is_correction=True, ref_type=None):
book_reference = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
new_images = []
if request:
files_obj = getattr(request, "FILES", None)
if not files_obj and hasattr(request, "_request"):
files_obj = getattr(request._request, "FILES", None)
if files_obj:
if hasattr(files_obj, "getlist"):
new_images = files_obj.getlist("uploaded_images") or files_obj.getlist("uploaded_images[]")
if not new_images:
val = files_obj.get("uploaded_images") or files_obj.get("uploaded_images[]")
if val:
new_images = [val] if not isinstance(val, list) else val
if not new_images and hasattr(request, "data"):
data_obj = request.data
if hasattr(data_obj, "getlist"):
new_images = data_obj.getlist("uploaded_images") or data_obj.getlist("uploaded_images[]")
if not new_images and hasattr(data_obj, "get"):
val = data_obj.get("uploaded_images") or data_obj.get("uploaded_images[]")
if val:
new_images = val if isinstance(val, list) else [val]
actual_images = [img for img in new_images if img and not isinstance(img, (str, int, float, bool, dict))]
ref = instance.references.first()
if not ref:
if book_reference or address or actual_images:
if ref_type == "original_text":
ref = OriginalTextReference.objects.create(original_text=instance)
elif is_correction:
ref = CorrectionReference.objects.create(correction=instance)
else:
ref = InterpretationReference.objects.create(interpretation=instance)
else:
return
if book_reference is not None:
ref.book_reference = book_reference
if address is not None:
if isinstance(address, (list, dict)):
import json
ref.address = json.dumps(address, ensure_ascii=False)
else:
ref.address = str(address)
ref.save()
if delete_image_ids:
ref.images.filter(id__in=delete_image_ids).delete()
for img in actual_images:
if ref_type == "original_text":
OriginalTextReferenceImage.objects.create(reference=ref, image=img)
elif is_correction:
CorrectionReferenceImage.objects.create(reference=ref, image=img)
else:
InterpretationReferenceImage.objects.create(reference=ref, image=img)
class AdminHadisCorrectionSerializer(serializers.ModelSerializer):
hadis_detail = AdminHadisBriefSerializer(source="hadis", read_only=True)
bookmark_count = serializers.IntegerField(source="annotated_bookmark_count", read_only=True)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
queryset=BookReference.objects.all(),
required=False,
write_only=True,
allow_null=True
)
address = serializers.JSONField(
required=False,
write_only=True,
allow_null=True
)
delete_image_ids = serializers.ListField(
child=serializers.IntegerField(),
required=False,
write_only=True
)
class Meta:
model = HadisCorrection
fields = [
"id",
"hadis",
"hadis_detail",
"title",
"slug",
"text",
"translation",
"priority",
"bookmark_count",
"links",
"book_reference_id",
"address",
"delete_image_ids",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "bookmark_count", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in ["title", "translation", "links", "address"]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
delete_ids = data.get("delete_image_ids")
if delete_ids:
if isinstance(delete_ids, str):
try:
data["delete_image_ids"] = json.loads(delete_ids)
except ValueError:
data["delete_image_ids"] = [int(x) for x in delete_ids.split(",") if x.strip().isdigit()]
elif isinstance(delete_ids, list):
parsed = []
for x in delete_ids:
if isinstance(x, str) and x.strip().isdigit():
parsed.append(int(x))
elif isinstance(x, int):
parsed.append(x)
data["delete_image_ids"] = parsed
return super().to_internal_value(data)
def to_representation(self, instance):
import json
ret = super().to_representation(instance)
refs = list(instance.references.all())
ref = refs[0] if refs else None
ret["book_reference"] = ref.book_reference_id if ref else None
addr_val = ref.address if ref and ref.address else ""
if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")):
try:
ret["address"] = json.loads(addr_val)
except (ValueError, TypeError):
ret["address"] = addr_val
else:
ret["address"] = addr_val
request = self.context.get("request")
images = list(ref.images.all()) if ref else []
ret["images"] = [
{
"id": img.id,
"image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url)
}
for img in images
]
return ret
def create(self, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().create(validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, is_correction=True)
return instance
def update(self, instance, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().update(instance, validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, is_correction=True)
return instance
class AdminHadisInterpretationSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategoryBriefSerializer(source="category", read_only=True)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
queryset=BookReference.objects.all(),
required=False,
write_only=True,
allow_null=True
)
address = serializers.JSONField(
required=False,
write_only=True,
allow_null=True
)
delete_image_ids = serializers.ListField(
child=serializers.IntegerField(),
required=False,
write_only=True
)
class Meta:
model = HadisInterpretation
fields = [
"id",
"category",
"category_detail",
"title",
"text",
"translation",
"priority",
"links",
"book_reference_id",
"address",
"delete_image_ids",
"created_at",
"updated_at",
]
read_only_fields = ["id", "created_at", "updated_at"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in ["title", "translation", "links", "address"]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
delete_ids = data.get("delete_image_ids")
if delete_ids:
if isinstance(delete_ids, str):
try:
data["delete_image_ids"] = json.loads(delete_ids)
except ValueError:
data["delete_image_ids"] = [int(x) for x in delete_ids.split(",") if x.strip().isdigit()]
elif isinstance(delete_ids, list):
parsed = []
for x in delete_ids:
if isinstance(x, str) and x.strip().isdigit():
parsed.append(int(x))
elif isinstance(x, int):
parsed.append(x)
data["delete_image_ids"] = parsed
return super().to_internal_value(data)
def to_representation(self, instance):
import json
ret = super().to_representation(instance)
refs = list(instance.references.all())
ref = refs[0] if refs else None
ret["book_reference"] = ref.book_reference_id if ref else None
addr_val = ref.address if ref and ref.address else ""
if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")):
try:
ret["address"] = json.loads(addr_val)
except (ValueError, TypeError):
ret["address"] = addr_val
else:
ret["address"] = addr_val
request = self.context.get("request")
images = list(ref.images.all()) if ref else []
ret["images"] = [
{
"id": img.id,
"image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url)
}
for img in images
]
return ret
def create(self, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().create(validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, is_correction=False)
return instance
def update(self, instance, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().update(instance, validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, is_correction=False)
return instance
class AbsoluteFileField(serializers.FileField):
def to_representation(self, value):
if not value:
return None
request = self.context.get("request")
url = value.url if hasattr(value, "url") else str(value)
if request:
return request.build_absolute_uri(url)
return url
class AdminHadisSectArchiveSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
description = serializers.JSONField()
class Meta:
model = HadisSect
fields = ["id", "sect_type", "title", "description", "is_active", "order"]
class AdminHadisCategoryArchiveParentSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = HadisCategory
fields = ["id", "title", "slug"]
class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer):
parent_detail = AdminHadisCategoryArchiveParentSerializer(source="parent", read_only=True)
sect_detail = AdminHadisSectArchiveSerializer(source="sect", read_only=True)
title = serializers.JSONField()
description = serializers.JSONField()
xmind_file = AbsoluteFileField(required=False, allow_null=True)
remove_xmind_file = serializers.BooleanField(write_only=True, required=False, default=False)
has_hadith = serializers.SerializerMethodField()
has_children = serializers.SerializerMethodField()
is_leaf = serializers.SerializerMethodField()
parent = serializers.PrimaryKeyRelatedField(
queryset=HadisCategory.objects.all(),
required=False,
allow_null=True,
)
sect = serializers.PrimaryKeyRelatedField(
queryset=HadisSect.objects.all(),
required=True,
)
def get_has_hadith(self, obj):
if hasattr(obj, "annotated_has_hadith"):
return bool(obj.annotated_has_hadith)
return obj.hadis_set.exists()
def get_has_children(self, obj):
if hasattr(obj, "annotated_has_children"):
return bool(obj.annotated_has_children)
return obj.children.exists()
def get_is_leaf(self, obj):
if hasattr(obj, "annotated_has_children"):
return not bool(obj.annotated_has_children)
return not obj.children.exists()
def validate_parent(self, value):
if value and value.hadis_set.exists():
raise serializers.ValidationError(
_("Cannot set parent to a category that already contains hadiths/content items.")
)
return value
class Meta:
model = HadisCategory
fields = [
"id",
"parent",
"parent_detail",
"sect",
"sect_detail",
"source_type",
"title",
"description",
"order",
"xmind_file",
"remove_xmind_file",
"slug",
"share_link",
"xmind_share_link",
"has_hadith",
"has_children",
"is_leaf",
]
read_only_fields = ["id", "share_link", "xmind_share_link"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in ["title", "description"]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
return super().to_internal_value(data)
def create(self, validated_data):
validated_data.pop("remove_xmind_file", False)
return super().create(validated_data)
class AdminHadisCategoryArchiveDetailSerializer(AdminHadisCategoryArchiveSerializer):
ancestors = serializers.SerializerMethodField()
children_type = serializers.SerializerMethodField()
child_categories = serializers.SerializerMethodField()
child_arguments = serializers.SerializerMethodField()
child_count = serializers.SerializerMethodField()
def get_ancestors(self, obj):
if not obj or not obj.pk:
return []
ancestors_qs = obj.get_ancestors(include_self=True).select_related("sect")
return [
{
"id": a.id,
"title": a.title,
"slug": a.slug,
"source_type": a.source_type,
"sect_type": a.sect.sect_type if a.sect else None,
"order": a.order,
"is_root": a.parent_id is None,
"level": getattr(a, a._mptt_meta.level_attr) if hasattr(a, "_mptt_meta") else 0,
}
for a in ancestors_qs
]
def get_children_type(self, obj):
if not obj or not obj.pk:
return "none"
has_children = bool(getattr(obj, "annotated_has_children", obj.children.exists()))
if has_children:
return "categories"
has_hadith = bool(getattr(obj, "annotated_has_hadith", obj.hadis_set.exists()))
if has_hadith:
return "arguments"
return "none"
def get_child_categories(self, obj):
if not obj or not obj.pk:
return []
child_hadis_subquery = Hadis.objects.filter(category=OuterRef("pk"))
child_children_subquery = HadisCategory.objects.filter(parent=OuterRef("pk"))
children = obj.children.all().annotate(
annotated_has_hadith=Exists(child_hadis_subquery),
annotated_has_children=Exists(child_children_subquery),
).order_by("order", "id")
return [
{
"id": c.id,
"title": c.title,
"slug": c.slug,
"order": c.order,
"source_type": c.source_type,
"has_children": bool(c.annotated_has_children),
"has_hadith": bool(c.annotated_has_hadith),
}
for c in children
]
def get_child_arguments(self, obj):
if not obj or not obj.pk:
return []
return [
{
"id": h.id,
"number": h.number,
"title": h.title,
"title_narrator": h.title_narrator,
"slug": h.slug,
"status": h.status,
"status_detail": {
"id": h.hadis_status.id,
"title": h.hadis_status.title,
"color": h.hadis_status.color,
"main_color_code": getattr(h.hadis_status, "main_color_code", None),
} if h.hadis_status else None,
}
for h in obj.hadis_set.all().select_related("hadis_status").order_by("number", "-id")[:100]
]
def get_child_count(self, obj):
if not obj or not obj.pk:
return 0
cat_count = obj.children.count()
if cat_count > 0:
return cat_count
return obj.hadis_set.count()
class Meta(AdminHadisCategoryArchiveSerializer.Meta):
fields = AdminHadisCategoryArchiveSerializer.Meta.fields + [
"ancestors",
"children_type",
"child_categories",
"child_arguments",
"child_count",
]
def update(self, instance, validated_data):
remove_xmind_file = validated_data.pop("remove_xmind_file", False)
if remove_xmind_file and instance.xmind_file:
instance.xmind_file.delete(save=False)
instance.xmind_file = None
return super().update(instance, validated_data)
class AdminTransmitterReliabilitySerializer(serializers.ModelSerializer):
class Meta:
model = TransmitterReliability
fields = ["id", "title", "slug", "color", "main_color_code"]
class AdminNarratorLayerSerializer(serializers.ModelSerializer):
name = serializers.JSONField()
description = serializers.JSONField(required=False, default=list)
hadis = serializers.PrimaryKeyRelatedField(
queryset=Hadis.objects.all(),
required=True,
)
hadis_detail = serializers.SerializerMethodField()
class Meta:
model = NarratorLayer
fields = [
"id",
"hadis",
"hadis_detail",
"name",
"number",
"description",
"slug",
"created_at",
"updated_at",
]
read_only_fields = ["id", "hadis_detail", "created_at", "updated_at"]
def get_hadis_detail(self, obj):
if not obj.hadis:
return None
return {
"id": obj.hadis.id,
"number": obj.hadis.number,
"slug": obj.hadis.slug,
"title": obj.hadis.title,
}
class AdminOpinionStatusSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = OpinionStatus
fields = ["id", "title", "slug", "color", "main_color_code"]
class AdminTransmitterListSerializer(serializers.ModelSerializer):
reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True)
full_name = serializers.JSONField()
known_as = serializers.JSONField()
nickname = serializers.JSONField()
thumbnail = AbsoluteImageField(required=False, allow_null=True)
share_link = serializers.CharField(read_only=True)
class Meta:
model = Transmitters
fields = [
"id",
"full_name",
"slug",
"birth_year_hijri",
"death_year_hijri",
"known_as",
"nickname",
"reliability",
"reliability_detail",
"madhhab",
"generation",
"thumbnail",
"share_link",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at"]
class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True)
full_name = serializers.JSONField()
kunya = serializers.JSONField()
known_as = serializers.JSONField()
nickname = serializers.JSONField()
origin = serializers.JSONField()
lived_in = serializers.JSONField()
died_in = serializers.JSONField()
description = serializers.JSONField()
thumbnail = AbsoluteImageField(required=False, allow_null=True)
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
reliability = serializers.PrimaryKeyRelatedField(
queryset=TransmitterReliability.objects.all(),
required=False,
)
share_link = serializers.CharField(read_only=True)
class Meta:
model = Transmitters
fields = [
"id",
"full_name",
"kunya",
"known_as",
"nickname",
"slug",
"legacy_number",
"legacy_id",
"birth_year_hijri",
"death_year_hijri",
"age_at_death",
"generation",
"reliability",
"reliability_detail",
"madhhab",
"in_sahih_muslim",
"in_sahih_bukhari",
"origin",
"lived_in",
"died_in",
"description",
"thumbnail",
"remove_thumbnail",
"share_link",
"created_at",
"updated_at",
]
read_only_fields = ["id", "slug", "share_link", "created_at", "updated_at", "reliability_detail"]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in [
"full_name",
"kunya",
"known_as",
"nickname",
"origin",
"lived_in",
"died_in",
"description",
]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
# Handle empty/null values for optional fields (prevent unique constraint violations on empty strings)
for field in [
"legacy_id",
"legacy_number",
"age_at_death",
"generation",
"birth_year_hijri",
"death_year_hijri",
"companion_type",
]:
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
return super().to_internal_value(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 AdminBookSubjectAreaSerializer(serializers.ModelSerializer):
class Meta:
model = BookSubjectArea
fields = ["id", "title", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
class AdminBookTypeSerializer(serializers.ModelSerializer):
class Meta:
model = BookType
fields = ["id", "title", "sect", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
class AdminBookAuthorSerializer(serializers.ModelSerializer):
image = AbsoluteImageField(required=False, allow_null=True)
remove_image = serializers.BooleanField(write_only=True, required=False, default=False)
book_references = serializers.PrimaryKeyRelatedField(
many=True,
required=False,
queryset=BookReference.objects.all(),
)
class Meta:
model = BookAuthor
fields = [
"id",
"name",
"slug",
"image",
"remove_image",
"birth_year_hijri",
"death_year_hijri",
"birth_year_miladi",
"death_year_miladi",
"related_narrator",
"known_as",
"scholarity",
"tabaqa",
"rutba",
"features",
"book_references",
]
read_only_fields = ["id", "slug"]
def to_internal_value(self, data):
import json
mutable_data = safe_copy_data(data)
for json_field in ["name", "known_as", "scholarity", "tabaqa", "rutba", "features", "book_references"]:
if json_field in mutable_data:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
for int_field in ["birth_year_hijri", "death_year_hijri", "birth_year_miladi", "death_year_miladi", "related_narrator"]:
if int_field in mutable_data:
val = mutable_data.get(int_field)
if val == "" or val == "null" or val == "undefined":
mutable_data[int_field] = None
return super().to_internal_value(mutable_data)
def create(self, validated_data):
validated_data.pop("remove_image", False)
book_references = validated_data.pop("book_references", None)
author = super().create(validated_data)
if book_references:
for book in book_references:
book.author = author
book.save(update_fields=["author"])
return author
def update(self, instance, validated_data):
remove_image = validated_data.pop("remove_image", False)
book_references = validated_data.pop("book_references", None)
if remove_image and instance.image:
instance.image.delete(save=False)
instance.image = None
author = super().update(instance, validated_data)
if book_references is not None:
current_ids = set(author.book_references.values_list('id', flat=True))
new_ids = {b.id for b in book_references}
to_remove = current_ids - new_ids
if to_remove:
BookReference.objects.filter(id__in=to_remove, author=author).update(author=None)
to_add = new_ids - current_ids
if to_add:
BookReference.objects.filter(id__in=to_add).update(author=author)
return author
class AdminBookAttributeSerializer(serializers.ModelSerializer):
class Meta:
model = BookAttribute
fields = ["id", "book_reference", "title", "value"]
class AdminBookReferenceImageSerializer(serializers.ModelSerializer):
image = AbsoluteImageField(required=True)
volume = serializers.SerializerMethodField()
class Meta:
model = BookReferenceImage
fields = ["id", "book_reference", "image", "volume", "order", "description"]
def get_volume(self, obj):
return obj.book_volume.title if getattr(obj, 'book_volume', None) else ""
class AdminHadisReferenceImageSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=True)
hadis_id = serializers.IntegerField(source="reference.hadis_id", read_only=True)
class Meta:
model = ReferenceImage
fields = ["id", "reference", "hadis_id", "thumbnail", "priority"]
class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer):
file = AbsoluteFileField(required=True)
volume = serializers.SerializerMethodField()
class Meta:
model = BookReferenceDocument
fields = ["id", "book_reference", "file", "volume", "title", "order"]
def get_volume(self, obj):
return obj.book_volume.title if getattr(obj, 'book_volume', None) else ""
class AdminBookTagSerializer(serializers.ModelSerializer):
title = serializers.JSONField(default=list)
class Meta:
model = BookTag
fields = ["id", "title", "created_at", "updated_at"]
class AdminBookReferenceListSerializer(serializers.ModelSerializer):
author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True)
author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True)
tag_id = serializers.IntegerField(source="tag.id", read_only=True, allow_null=True)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
volume = serializers.SerializerMethodField()
publisher = serializers.SerializerMethodField()
year_of_publication = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
class Meta:
model = BookReference
fields = [
"id",
"title",
"slug",
"has_editions",
"author_id",
"author_name",
"tag_id",
"tag_detail",
"volume",
"publisher",
"year_of_publication",
"rate",
"thumbnail",
"image",
"created_at",
]
def get_thumbnail(self, obj):
first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first()
if first_img and first_img.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_img.image.url, request)
return absolute_https_url(first_img.image.url)
first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first()
if first_vol and first_vol.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_vol.image.url, request)
return absolute_https_url(first_vol.image.url)
return None
def get_image(self, obj):
return self.get_thumbnail(obj)
def get_volume(self, obj):
vols = list(obj.volumes.all())
return vols[0].title if vols else ""
def get_publisher(self, obj):
editions = list(obj.editions.all())
return editions[0].publisher if editions else []
def get_year_of_publication(self, obj):
editions = list(obj.editions.all())
return editions[0].year_of_publication if editions else ""
class AdminBookReferenceDetailSerializer(serializers.ModelSerializer):
subject_area_detail = AdminBookSubjectAreaSerializer(source="subject_area", many=True, read_only=True)
type_detail = AdminBookTypeSerializer(source="type", read_only=True)
author_detail = AdminBookAuthorSerializer(source="author", read_only=True)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
subject_area = serializers.PrimaryKeyRelatedField(
queryset=BookSubjectArea.objects.all(),
many=True,
required=False,
)
type = serializers.PrimaryKeyRelatedField(
queryset=BookType.objects.all(),
required=False,
allow_null=True,
)
author = serializers.PrimaryKeyRelatedField(
queryset=BookAuthor.objects.all(),
required=False,
allow_null=True,
)
tag = serializers.PrimaryKeyRelatedField(
queryset=BookTag.objects.all(),
required=False,
allow_null=True,
)
source_url = serializers.SerializerMethodField()
isbn = serializers.SerializerMethodField()
volume = serializers.SerializerMethodField()
number_of_volumes = serializers.SerializerMethodField()
year_of_publication = serializers.SerializerMethodField()
publisher = serializers.SerializerMethodField()
city_of_publication = serializers.SerializerMethodField()
country_of_publication = serializers.SerializerMethodField()
edition_number = serializers.SerializerMethodField()
thumbnail = serializers.SerializerMethodField()
image = serializers.SerializerMethodField()
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
remove_image = serializers.BooleanField(write_only=True, required=False, default=False)
def get_thumbnail(self, obj):
first_img = obj.images.filter(book_volume__isnull=True).order_by("order", "id").first()
if first_img and first_img.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_img.image.url, request)
return absolute_https_url(first_img.image.url)
first_vol = obj.volumes.filter(image__isnull=False).exclude(image="").first()
if first_vol and first_vol.image:
request = self.context.get("request")
if request:
return absolute_https_url(first_vol.image.url, request)
return absolute_https_url(first_vol.image.url)
return None
def get_image(self, obj):
return self.get_thumbnail(obj)
def get_source_url(self, obj):
ed = obj.editions.first()
return ed.source_url if ed else ""
def get_isbn(self, obj):
ed = obj.editions.first()
return ed.isbn if ed else ""
def get_volume(self, obj):
vol = obj.volumes.first()
return vol.title if vol else ""
def get_number_of_volumes(self, obj):
ed = obj.editions.first()
return ed.number_of_volumes if ed else None
def get_year_of_publication(self, obj):
ed = obj.editions.first()
return ed.year_of_publication if ed else ""
def get_publisher(self, obj):
ed = obj.editions.first()
return ed.publisher if ed else []
def get_city_of_publication(self, obj):
ed = obj.editions.first()
return ed.city_of_publication if ed else []
def get_country_of_publication(self, obj):
ed = obj.editions.first()
return ed.country_of_publication if ed else []
def get_edition_number(self, obj):
ed = obj.editions.first()
return ed.edition_number if ed else ""
class Meta:
model = BookReference
fields = [
"id",
"legacy_id",
"source_url",
"title",
"description",
"language",
"has_editions",
"isbn",
"volume",
"number_of_volumes",
"year_of_publication",
"number_page",
"slug",
"publisher",
"subject_area",
"subject_area_detail",
"type",
"type_detail",
"tag",
"tag_detail",
"order",
"city_of_publication",
"country_of_publication",
"edition_number",
"rate",
"author",
"author_detail",
"thumbnail",
"image",
"remove_thumbnail",
"remove_image",
"created_at",
"updated_at",
"share_link",
]
read_only_fields = ["id", "slug", "created_at", "updated_at", "share_link"]
def to_internal_value(self, data):
import json
mutable_data = safe_copy_data(data)
for json_field in ["title", "description", "language", "publisher", "city_of_publication", "country_of_publication"]:
if json_field in mutable_data:
val = mutable_data.get(json_field)
if isinstance(val, str):
try:
mutable_data[json_field] = json.loads(val)
except ValueError:
pass
for list_field in ["subject_area", "authors"]:
if list_field in mutable_data:
val = mutable_data.get(list_field)
if isinstance(val, str):
try:
parsed = json.loads(val)
mutable_data[list_field] = parsed if isinstance(parsed, list) else [parsed]
except ValueError:
if val.isdigit():
mutable_data[list_field] = [int(val)]
elif val.strip():
mutable_data[list_field] = [int(v.strip()) for v in val.split(",") if v.strip().isdigit()]
else:
mutable_data[list_field] = []
elif isinstance(val, int):
mutable_data[list_field] = [val]
elif isinstance(val, list):
cleaned_list = []
for item in val:
if isinstance(item, str):
try:
item_parsed = json.loads(item)
if isinstance(item_parsed, list):
cleaned_list.extend(item_parsed)
else:
cleaned_list.append(item_parsed)
except ValueError:
cleaned_list.append(int(item) if item.isdigit() else item)
else:
cleaned_list.append(item)
mutable_data[list_field] = cleaned_list
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 create(self, validated_data):
subject_area = validated_data.pop("subject_area", [])
validated_data.pop("remove_thumbnail", False)
validated_data.pop("remove_image", False)
request = self.context.get("request")
thumbnail = None
if request and hasattr(request, "FILES"):
thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image")
book = super().create(validated_data)
if subject_area:
book.subject_area.set(subject_area)
if thumbnail:
BookReferenceImage.objects.create(
book_reference=book,
book_volume=None,
image=thumbnail,
order=0,
)
return book
def update(self, instance, validated_data):
subject_area = validated_data.pop("subject_area", None)
remove_thumbnail = validated_data.pop("remove_thumbnail", False) or validated_data.pop("remove_image", False)
request = self.context.get("request")
thumbnail = None
if request and hasattr(request, "FILES"):
thumbnail = request.FILES.get("thumbnail") or request.FILES.get("image")
book = super().update(instance, validated_data)
if subject_area is not None:
book.subject_area.set(subject_area)
if remove_thumbnail:
instance.images.filter(book_volume__isnull=True, order=0).delete()
if thumbnail:
main_img = instance.images.filter(book_volume__isnull=True, order=0).first()
if main_img:
main_img.image = thumbnail
main_img.save()
else:
BookReferenceImage.objects.create(
book_reference=instance,
book_volume=None,
image=thumbnail,
order=0,
)
return book
# Category Quran Verse Admin Serializers
from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation
class AdminQuranSecondaryTranslationSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
translation_text = serializers.JSONField(default=list)
translator = serializers.JSONField(default=list)
class Meta:
model = QuranSecondaryTranslation
fields = ["id", "translation_text", "translator"]
class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer):
surah_name = serializers.JSONField(default=list)
translation = serializers.JSONField(default=list)
translator = serializers.JSONField(default=list)
secondary_translations = AdminQuranSecondaryTranslationSerializer(many=True, required=False)
class Meta:
model = CategoryQuranVerse
fields = [
"id",
"category",
"surah_name",
"ayah_number",
"arabic_text",
"translation",
"translator",
"secondary_translations",
]
def create(self, validated_data):
secondary_data = validated_data.pop("secondary_translations", [])
verse = CategoryQuranVerse.objects.create(**validated_data)
for sec in secondary_data:
QuranSecondaryTranslation.objects.create(quran_verse=verse, **sec)
return verse
def update(self, instance, validated_data):
secondary_data = validated_data.pop("secondary_translations", None)
instance.surah_name = validated_data.get("surah_name", instance.surah_name)
instance.ayah_number = validated_data.get("ayah_number", instance.ayah_number)
instance.arabic_text = validated_data.get("arabic_text", instance.arabic_text)
instance.translation = validated_data.get("translation", instance.translation)
instance.translator = validated_data.get("translator", instance.translator)
instance.category = validated_data.get("category", instance.category)
instance.save()
if secondary_data is not None:
keep_ids = [item.get("id") for item in secondary_data if item.get("id")]
instance.secondary_translations.exclude(id__in=keep_ids).delete()
for sec in secondary_data:
sec_id = sec.get("id")
if sec_id:
QuranSecondaryTranslation.objects.filter(id=sec_id, quran_verse=instance).update(
translation_text=sec.get("translation_text", []),
translator=sec.get("translator", [])
)
else:
QuranSecondaryTranslation.objects.create(quran_verse=instance, **sec)
return instance
class AdminHadisTransmitterSerializer(serializers.ModelSerializer):
transmitter_detail = AdminTransmitterListSerializer(source="transmitter", read_only=True)
uncertain_transmitters = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Transmitters.objects.all(),
required=False
)
uncertain_transmitters_detail = AdminTransmitterListSerializer(
source="uncertain_transmitters",
many=True,
read_only=True
)
class Meta:
model = HadisTransmitter
fields = [
"id",
"hadis",
"transmitter",
"transmitter_detail",
"is_uncertain",
"uncertain_transmitters",
"uncertain_transmitters_detail",
"chain_index",
"narrator_layer",
"status",
"order",
"is_gap",
]
def create(self, validated_data):
uncertain_transmitters = validated_data.pop("uncertain_transmitters", None)
instance = super().create(validated_data)
if uncertain_transmitters is not None:
instance.uncertain_transmitters.set(uncertain_transmitters)
return instance
def update(self, instance, validated_data):
uncertain_transmitters = validated_data.pop("uncertain_transmitters", None)
instance = super().update(instance, validated_data)
if uncertain_transmitters is not None:
instance.uncertain_transmitters.set(uncertain_transmitters)
return instance
class AdminTransmitterOpinionSerializer(serializers.ModelSerializer):
scholar_name = serializers.JSONField(required=False, default=list)
opinion_text = serializers.JSONField(required=False, default=list)
status_detail = AdminTransmitterReliabilitySerializer(source="status", read_only=True)
scholar_detail = AdminBookAuthorSerializer(source="scholar", read_only=True)
class Meta:
model = TransmitterOpinion
fields = [
"id",
"transmitter",
"scholar",
"scholar_detail",
"scholar_name",
"opinion_text",
"status",
"status_detail",
"order",
"created_at",
"updated_at",
]
class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer):
title = serializers.JSONField(required=False, default=list)
text = serializers.JSONField(required=False, default=list)
translation = serializers.JSONField(required=False, default=list)
# Writable relation fields
book_reference_id = serializers.PrimaryKeyRelatedField(
queryset=BookReference.objects.all(),
required=False,
write_only=True,
allow_null=True
)
address = serializers.JSONField(
required=False,
write_only=True,
allow_null=True
)
delete_image_ids = serializers.ListField(
child=serializers.IntegerField(),
required=False,
write_only=True
)
class Meta:
model = TransmitterOriginalText
fields = [
"id",
"transmitter",
"slug",
"title",
"text",
"translation",
"share_link",
"embedded_in",
"order",
"book_reference_id",
"address",
"delete_image_ids",
]
def to_internal_value(self, data):
import json
data = safe_copy_data(data)
for json_field in ["title", "text", "translation", "address"]:
val = data.get(json_field)
if isinstance(val, str):
try:
data[json_field] = json.loads(val)
except ValueError:
pass
delete_ids = data.get("delete_image_ids")
if delete_ids:
if isinstance(delete_ids, str):
try:
data["delete_image_ids"] = json.loads(delete_ids)
except ValueError:
data["delete_image_ids"] = [int(x) for x in delete_ids.split(",") if x.strip().isdigit()]
elif isinstance(delete_ids, list):
parsed = []
for x in delete_ids:
if isinstance(x, str) and x.strip().isdigit():
parsed.append(int(x))
elif isinstance(x, int):
parsed.append(x)
data["delete_image_ids"] = parsed
return super().to_internal_value(data)
def to_representation(self, instance):
import json
ret = super().to_representation(instance)
ref = instance.references.first()
ret["book_reference"] = ref.book_reference_id if ref else None
addr_val = ref.address if ref and ref.address else ""
if isinstance(addr_val, str) and (addr_val.strip().startswith("[") or addr_val.strip().startswith("{")):
try:
ret["address"] = json.loads(addr_val)
except (ValueError, TypeError):
ret["address"] = addr_val
else:
ret["address"] = addr_val
request = self.context.get("request")
ret["images"] = [
{
"id": img.id,
"image": absolute_https_url(img.image.url, request) if request and img.image else absolute_https_url(img.image.url)
}
for img in (ref.images.all() if ref else [])
]
return ret
def create(self, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().create(validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text")
return instance
def update(self, instance, validated_data):
request = self.context.get("request")
book_reference_id = validated_data.pop("book_reference_id", None)
address = validated_data.pop("address", None)
delete_image_ids = validated_data.pop("delete_image_ids", [])
instance = super().update(instance, validated_data)
ref_data = {
"book_reference_id": book_reference_id,
"address": address,
"delete_image_ids": delete_image_ids,
}
save_relation_reference_data(instance, ref_data, request=request, ref_type="original_text")
return instance
class AdminBookVolumeSerializer(serializers.ModelSerializer):
remove_image = serializers.BooleanField(write_only=True, required=False, default=False)
class Meta:
model = BookVolume
fields = [
"id",
"book_reference",
"edition",
"title",
"image",
"remove_image",
"created_at",
"updated_at",
]
def create(self, validated_data):
validated_data.pop("remove_image", False)
return super().create(validated_data)
def update(self, instance, validated_data):
remove_image = validated_data.pop("remove_image", False)
if remove_image and instance.image:
instance.image.delete(save=False)
instance.image = None
return super().update(instance, validated_data)
class AdminBookResearcherSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
name = serializers.JSONField(required=False, default=list)
class Meta:
model = BookResearcher
fields = ["id", "name", "slug", "author"]
class AdminBookEditorSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False)
name = serializers.JSONField(required=False, default=list)
class Meta:
model = BookEditor
fields = ["id", "name", "slug", "author"]
class AdminBookEditionSerializer(serializers.ModelSerializer):
publisher = serializers.JSONField(required=False, default=list)
city_of_publication = serializers.JSONField(required=False, default=list)
country_of_publication = serializers.JSONField(required=False, default=list)
notes = serializers.JSONField(required=False, default=list)
tag = serializers.PrimaryKeyRelatedField(
queryset=BookTag.objects.all(),
required=False,
allow_null=True,
)
tag_detail = AdminBookTagSerializer(source="tag", read_only=True)
volumes_detail = AdminBookVolumeSerializer(source="volumes", many=True, read_only=True)
number_of_volumes = serializers.SerializerMethodField()
researchers = AdminBookResearcherSerializer(many=True, required=False)
editors = AdminBookEditorSerializer(many=True, required=False)
researcher_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True)
editor_author_id = serializers.IntegerField(write_only=True, required=False, allow_null=True)
class Meta:
model = BookEdition
fields = [
"id",
"book_reference",
"publisher",
"city_of_publication",
"country_of_publication",
"edition_number",
"year_of_publication",
"number_of_volumes",
"source_url",
"isbn",
"notes",
"tag",
"tag_detail",
"volumes_detail",
"researchers",
"editors",
"researcher_author_id",
"editor_author_id",
"created_at",
"updated_at",
]
def to_representation(self, instance):
ret = super().to_representation(instance)
res = instance.researchers.first()
ret["researcher_author_id"] = res.author_id if res and res.author_id else None
ed_obj = instance.editors.first()
ret["editor_author_id"] = ed_obj.author_id if ed_obj and ed_obj.author_id else None
return ret
def get_number_of_volumes(self, obj):
return obj.volumes.count()
def _handle_author_links(self, edition, validated_data):
request = self.context.get("request")
data = request.data if request else {}
if "researcher_author_id" in data or "researcher_author_id" in validated_data:
res_val = validated_data.get("researcher_author_id") if "researcher_author_id" in validated_data else data.get("researcher_author_id")
edition.researchers.all().delete()
if res_val and res_val not in ["none", "", "null", None]:
try:
author_id = int(res_val)
author_obj = BookAuthor.objects.filter(id=author_id).first()
if author_obj:
BookResearcher.objects.create(
book_edition=edition,
author=author_obj,
name=author_obj.name,
slug=author_obj.slug
)
except (ValueError, TypeError):
pass
if "editor_author_id" in data or "editor_author_id" in validated_data:
ed_val = validated_data.get("editor_author_id") if "editor_author_id" in validated_data else data.get("editor_author_id")
edition.editors.all().delete()
if ed_val and ed_val not in ["none", "", "null", None]:
try:
author_id = int(ed_val)
author_obj = BookAuthor.objects.filter(id=author_id).first()
if author_obj:
BookEditor.objects.create(
book_edition=edition,
author=author_obj,
name=author_obj.name,
slug=author_obj.slug
)
except (ValueError, TypeError):
pass
def create(self, validated_data):
researchers_data = validated_data.pop("researchers", [])
editors_data = validated_data.pop("editors", [])
has_res_id = "researcher_author_id" in validated_data
has_ed_id = "editor_author_id" in validated_data
res_author_id = validated_data.pop("researcher_author_id", None)
ed_author_id = validated_data.pop("editor_author_id", None)
edition = super().create(validated_data)
for res_data in researchers_data:
BookResearcher.objects.create(book_edition=edition, **res_data)
for ed_data in editors_data:
BookEditor.objects.create(book_edition=edition, **ed_data)
author_links_dict = {}
if has_res_id:
author_links_dict["researcher_author_id"] = res_author_id
if has_ed_id:
author_links_dict["editor_author_id"] = ed_author_id
self._handle_author_links(edition, author_links_dict)
return edition
def update(self, instance, validated_data):
researchers_data = validated_data.pop("researchers", None)
editors_data = validated_data.pop("editors", None)
has_res_id = "researcher_author_id" in validated_data
has_ed_id = "editor_author_id" in validated_data
res_author_id = validated_data.pop("researcher_author_id", None)
ed_author_id = validated_data.pop("editor_author_id", None)
edition = super().update(instance, validated_data)
if researchers_data is not None:
# Delete researchers no longer in the request payload
keep_ids = [r.get("id") for r in researchers_data if r.get("id")]
instance.researchers.exclude(id__in=keep_ids).delete()
for res_data in researchers_data:
res_id = res_data.get("id")
if res_id:
# Update existing researcher
res_data_clean = {k: v for k, v in res_data.items() if k != "id"}
BookResearcher.objects.filter(id=res_id, book_edition=instance).update(**res_data_clean)
res_obj = BookResearcher.objects.filter(id=res_id, book_edition=instance).first()
if res_obj:
res_obj.save()
else:
# Create new researcher
BookResearcher.objects.create(book_edition=instance, **res_data)
if editors_data is not None:
# Delete editors no longer in the request payload
keep_ids = [e.get("id") for e in editors_data if e.get("id")]
instance.editors.exclude(id__in=keep_ids).delete()
for ed_data in editors_data:
ed_id = ed_data.get("id")
if ed_id:
# Update existing editor
ed_data_clean = {k: v for k, v in ed_data.items() if k != "id"}
BookEditor.objects.filter(id=ed_id, book_edition=instance).update(**ed_data_clean)
ed_obj = BookEditor.objects.filter(id=ed_id, book_edition=instance).first()
if ed_obj:
ed_obj.save()
else:
# Create new editor
BookEditor.objects.create(book_edition=instance, **ed_data)
author_links_dict = {}
if has_res_id:
author_links_dict["researcher_author_id"] = res_author_id
if has_ed_id:
author_links_dict["editor_author_id"] = ed_author_id
self._handle_author_links(edition, author_links_dict)
return edition
class AdminContentReleaseSerializer(serializers.ModelSerializer):
class Meta:
model = ContentRelease
fields = [
"id",
"version_name",
"published_at",
"description",
"is_active",
]
read_only_fields = ["id", "published_at"]