diff --git a/apps/hadis/management/commands/seed_edition_editors.py b/apps/hadis/management/commands/seed_edition_editors.py new file mode 100644 index 0000000..16560b6 --- /dev/null +++ b/apps/hadis/management/commands/seed_edition_editors.py @@ -0,0 +1,81 @@ +# backend/apps/hadis/management/commands/seed_edition_editors.py + +import random +from django.core.management.base import BaseCommand +from django.db import transaction +from django.utils.text import slugify +from apps.hadis.models import BookEdition, BookAuthor, BookEditor + +class Command(BaseCommand): + help = 'Assigns editors to each BookEdition: one random text-only and one linked to a random existing BookAuthor.' + + def handle(self, *args, **options): + self.stdout.write(self.style.WARNING("Starting to seed edition editors...")) + + editions = list(BookEdition.objects.all()) + authors = list(BookAuthor.objects.all()) + + if not editions: + self.stdout.write(self.style.ERROR("No BookEditions found in the database.")) + return + + if not authors: + self.stdout.write(self.style.ERROR("No BookAuthors found. Please add some authors first.")) + return + + # 1. Clean existing editors + self.stdout.write("Deleting existing BookEditors...") + BookEditor.objects.all().delete() + + random_editor_names = [ + {"fa": "دکتر حسین نوری", "en": "Dr. Hossein Noori"}, + {"fa": "استاد عباس محمودی", "en": "Professor Abbas Mahmoudi"}, + {"fa": "دکتر زهرا طاهری", "en": "Dr. Zahra Taheri"}, + {"fa": "مصحح مهدی حیدری", "en": "Editor Mehdi Heidari"}, + {"fa": "دکتر کاظم مرادی", "en": "Dr. Kazem Moradi"}, + {"fa": "استاد سارا ابراهیمی", "en": "Professor Sara Ebrahimi"}, + {"fa": "دکتر قاسم باقری", "en": "Dr. Ghasem Bagheri"}, + ] + + text_only_count = 0 + linked_author_count = 0 + + with transaction.atomic(): + for edition in editions: + # 1. First editor: Text-only (Random Name & Slug) + name_data = random.choice(random_editor_names) + name_field = [ + {"language_code": "fa", "text": name_data["fa"]}, + {"language_code": "en", "text": name_data["en"]}, + ] + # Ensure clean slug + base_slug = slugify(name_data["en"]) + slug_val = base_slug + counter = 1 + while BookEditor.objects.filter(slug=slug_val).exists(): + slug_val = f"{base_slug}-{counter}" + counter += 1 + + BookEditor.objects.create( + book_edition=edition, + name=name_field, + slug=slug_val, + author=None + ) + text_only_count += 1 + + # 2. Second editor: Linked to a random BookAuthor + random_author = random.choice(authors) + BookEditor.objects.create( + book_edition=edition, + author=random_author + ) + linked_author_count += 1 + + self.stdout.write( + self.style.SUCCESS( + f"Successfully seeded editors for {len(editions)} BookEditions!\n" + f"- Created {text_only_count} text-only editors.\n" + f"- Created {linked_author_count} editors linked to existing authors." + ) + ) diff --git a/apps/hadis/migrations/0033_bookeditor.py b/apps/hadis/migrations/0033_bookeditor.py new file mode 100644 index 0000000..f6bab98 --- /dev/null +++ b/apps/hadis/migrations/0033_bookeditor.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.30 on 2026-08-01 10:51 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0032_alter_bookauthor_slug_alter_bookreference_slug_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='BookEditor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.JSONField(blank=True, default=list, verbose_name='Name')), + ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='editor_profiles', to='hadis.bookauthor', verbose_name='Author')), + ('book_edition', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editors', to='hadis.bookedition', verbose_name='Book Edition')), + ], + options={ + 'verbose_name': 'Book Editor', + 'verbose_name_plural': 'Book Editors', + }, + ), + ] diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index 45fffd2..be0f4f8 100644 --- a/apps/hadis/models/reference.py +++ b/apps/hadis/models/reference.py @@ -483,3 +483,40 @@ class BookResearcher(LowercaseSlugMixin, models.Model): return self.name[0]['text'] except: return self.slug or f"Researcher {self.id}" + + +class BookEditor(LowercaseSlugMixin, models.Model): + slug_source_field = 'name' + book_edition = models.ForeignKey( + 'BookEdition', + on_delete=models.CASCADE, + related_name='editors', + verbose_name=_('Book Edition') + ) + author = models.ForeignKey( + 'BookAuthor', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='editor_profiles', + verbose_name=_('Author') + ) + name = models.JSONField(default=list, blank=True, verbose_name=_('Name')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, allow_unicode=True) + + class Meta: + verbose_name = _('Book Editor') + verbose_name_plural = _('Book Editors') + + def save(self, *args, **kwargs): + if self.author: + self.name = self.author.name + self.slug = self.author.slug + super().save(*args, **kwargs) + + def __str__(self): + try: + return self.name[0]['text'] + except: + return self.slug or f"Editor {self.id}" + diff --git a/apps/hadis/serializers/reference_v2.py b/apps/hadis/serializers/reference_v2.py index 62d5295..5ddd3af 100644 --- a/apps/hadis/serializers/reference_v2.py +++ b/apps/hadis/serializers/reference_v2.py @@ -1,6 +1,6 @@ from rest_framework import serializers from rest_framework.pagination import PageNumberPagination -from ..models import BookReference, BookAuthor, BookEdition, BookVolume, BookTag, BookResearcher +from ..models import BookReference, BookAuthor, BookEdition, BookVolume, BookTag, BookResearcher, BookEditor from .category import get_localized_text from apps.hadis.serializers import HadisListSerializer, HadisShortSerializer from ..serializers.category import LocalizedField @@ -24,6 +24,12 @@ class BookResearcherV2Serializer(serializers.ModelSerializer): model = BookResearcher fields = ['id', 'name', 'slug'] +class BookEditorV2Serializer(serializers.ModelSerializer): + name = LocalizedField() + class Meta: + model = BookEditor + fields = ['id', 'name', 'slug'] + class BookReferenceV2ListSerializer(serializers.ModelSerializer): title = LocalizedField() authors = serializers.SerializerMethodField() @@ -60,13 +66,14 @@ class BookEditionV2Serializer(serializers.ModelSerializer): tags = LocalizedField() publication_place = serializers.SerializerMethodField() researchers = BookResearcherV2Serializer(many=True, read_only=True) + editors = BookEditorV2Serializer(many=True, read_only=True) class Meta: model = BookEdition fields = [ 'id', 'publisher', 'publication_place', 'edition_number', 'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tags', - 'researchers' + 'researchers', 'editors' ] def get_publication_place(self, obj): diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index c8947d2..150472d 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -4,7 +4,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from utils.image_compression import maybe_compress_uploaded_file from ..models import ( Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, - BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume, BookResearcher, + BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume, BookResearcher, BookEditor, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText, CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, OriginalTextReference, OriginalTextReferenceImage @@ -577,6 +577,7 @@ class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer): 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() parent = serializers.PrimaryKeyRelatedField( queryset=HadisCategory.objects.all(), @@ -588,6 +589,9 @@ class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer): required=True, ) + def get_has_hadith(self, obj): + return obj.hadis_set.exists() + class Meta: model = HadisCategory fields = [ @@ -605,6 +609,7 @@ class AdminHadisCategoryArchiveSerializer(serializers.ModelSerializer): "slug", "share_link", "xmind_share_link", + "has_hadith", ] read_only_fields = ["id", "share_link", "xmind_share_link"] @@ -1224,6 +1229,15 @@ class AdminBookResearcherSerializer(serializers.ModelSerializer): 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) @@ -1233,6 +1247,7 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): 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) class Meta: model = BookEdition @@ -1251,6 +1266,7 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): "tags", "volumes_detail", "researchers", + "editors", "created_at", "updated_at", ] @@ -1260,13 +1276,17 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): def create(self, validated_data): researchers_data = validated_data.pop("researchers", []) + editors_data = validated_data.pop("editors", []) 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) return edition def update(self, instance, validated_data): researchers_data = validated_data.pop("researchers", None) + editors_data = validated_data.pop("editors", None) edition = super().update(instance, validated_data) if researchers_data is not None: # Delete researchers no longer in the request payload @@ -1277,13 +1297,29 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): if res_id: # Update existing researcher res_data_clean = {k: v for k, v in res_data.items() if k != "id"} - # Filter and update BookResearcher.objects.filter(id=res_id, book_edition=instance).update(**res_data_clean) - # Trigger save to execute save() hook (for author update propagation if author is changed) 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) + return edition diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index b6579d9..f226c7a 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -100,7 +100,7 @@ class DovodiHadisCategoryViewSet(ModelViewSet): source_type = self.request.query_params.get("source_type") if source_type: queryset = queryset.filter(source_type=source_type) - return queryset.order_by("tree_id", "lft", "order", "id") + return queryset.order_by("-id") class AdminHadisStatusViewSet(ModelViewSet):