From 541a381812ac13a5ba42a4c3e8dd6537cdae7cd3 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 20 Sep 2026 12:47:55 +0330 Subject: [PATCH] feat(hadis): support per-language secondary translations and category verse filtering - Refactor QuranSecondaryTranslation to store language_code, translation_text and translator per record - Add migrations 0042 and 0043 to migrate existing secondary translations - Update AdminCategoryQuranVerseSerializer and AdminHadisCategorySerializer with proper secondary translations handling - Support category filtering by source_type, has_quran_verse and exclude_verse_id in admin category viewset --- .../0042_add_secondary_quran_fields.py | 77 ++++++++++++++++ ...3_finalize_secondary_quran_translations.py | 30 +++++++ apps/hadis/models/category.py | 18 ++-- apps/hadis/serializers/category.py | 19 ++-- apps/hadis/serializers/serializers_admin.py | 88 ++++++++++++------- apps/hadis/views_admin.py | 60 +++++++++++-- 6 files changed, 242 insertions(+), 50 deletions(-) create mode 100644 apps/hadis/migrations/0042_add_secondary_quran_fields.py create mode 100644 apps/hadis/migrations/0043_finalize_secondary_quran_translations.py diff --git a/apps/hadis/migrations/0042_add_secondary_quran_fields.py b/apps/hadis/migrations/0042_add_secondary_quran_fields.py new file mode 100644 index 0000000..42a8a49 --- /dev/null +++ b/apps/hadis/migrations/0042_add_secondary_quran_fields.py @@ -0,0 +1,77 @@ +from django.db import migrations, models + + +def migrate_secondary_translations_forward(apps, schema_editor): + QuranSecondaryTranslation = apps.get_model('hadis', 'QuranSecondaryTranslation') + + for obj in list(QuranSecondaryTranslation.objects.all()): + trans_list = obj.translation_text or [] + auth_list = obj.translator or [] + + trans_map = {} + if isinstance(trans_list, list): + for item in trans_list: + if isinstance(item, dict) and item.get('language_code'): + trans_map[item['language_code']] = item.get('text') or item.get('title') or '' + elif isinstance(trans_list, str): + trans_map['ru'] = trans_list + + auth_map = {} + if isinstance(auth_list, list): + for item in auth_list: + if isinstance(item, dict) and item.get('language_code'): + auth_map[item['language_code']] = item.get('text') or item.get('title') or '' + elif isinstance(auth_list, str): + auth_map['ru'] = auth_list + + langs = list(set(trans_map.keys()) | set(auth_map.keys())) + if not langs: + langs = ['ru'] + + first_lang = langs[0] + obj.language_code = first_lang + obj.new_translation_text = trans_map.get(first_lang, '') + obj.new_translator = auth_map.get(first_lang, '') + obj.save(update_fields=['language_code', 'new_translation_text', 'new_translator']) + + for extra_lang in langs[1:]: + QuranSecondaryTranslation.objects.create( + quran_verse_id=obj.quran_verse_id, + language_code=extra_lang, + new_translation_text=trans_map.get(extra_lang, ''), + new_translator=auth_map.get(extra_lang, ''), + ) + + +def migrate_secondary_translations_backward(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('hadis', '0041_alter_hadisdatabaseversion_version_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='quransecondarytranslation', + name='language_code', + field=models.CharField(db_index=True, default='ru', max_length=10, verbose_name='Language Code'), + ), + migrations.AddField( + model_name='quransecondarytranslation', + name='new_translation_text', + field=models.TextField(default='', verbose_name='Translation Text'), + ), + migrations.AddField( + model_name='quransecondarytranslation', + name='new_translator', + field=models.CharField(default='', max_length=255, verbose_name='Translator Name'), + ), + migrations.RunPython( + migrate_secondary_translations_forward, + migrate_secondary_translations_backward, + ), + ] diff --git a/apps/hadis/migrations/0043_finalize_secondary_quran_translations.py b/apps/hadis/migrations/0043_finalize_secondary_quran_translations.py new file mode 100644 index 0000000..3f4a059 --- /dev/null +++ b/apps/hadis/migrations/0043_finalize_secondary_quran_translations.py @@ -0,0 +1,30 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('hadis', '0042_add_secondary_quran_fields'), + ] + + operations = [ + migrations.RemoveField( + model_name='quransecondarytranslation', + name='translation_text', + ), + migrations.RemoveField( + model_name='quransecondarytranslation', + name='translator', + ), + migrations.RenameField( + model_name='quransecondarytranslation', + old_name='new_translation_text', + new_name='translation_text', + ), + migrations.RenameField( + model_name='quransecondarytranslation', + old_name='new_translator', + new_name='translator', + ), + ] diff --git a/apps/hadis/models/category.py b/apps/hadis/models/category.py index dc9cab9..6dde2ef 100644 --- a/apps/hadis/models/category.py +++ b/apps/hadis/models/category.py @@ -214,7 +214,7 @@ class CategoryQuranVerse(models.Model): class QuranSecondaryTranslation(models.Model): """ - Model to store multiple secondary translations for a Quran verse. + Model to store multiple secondary translations for a Quran verse per language. """ quran_verse = models.ForeignKey( CategoryQuranVerse, @@ -222,8 +222,14 @@ class QuranSecondaryTranslation(models.Model): related_name='secondary_translations', verbose_name=_('Quran Verse') ) - translation_text = models.JSONField(default=list, verbose_name=_('Translation Text')) - translator = models.JSONField(default=list, verbose_name=_('Translator Name')) + language_code = models.CharField( + max_length=10, + db_index=True, + default='ru', + verbose_name=_('Language Code') + ) + translation_text = models.TextField(verbose_name=_('Translation Text')) + translator = models.CharField(max_length=255, verbose_name=_('Translator Name')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At')) @@ -234,8 +240,4 @@ class QuranSecondaryTranslation(models.Model): ordering = ['id'] def __str__(self): - try: - translator_name = self.translator[0]['text'] if self.translator else "Unknown Translator" - except (IndexError, KeyError, TypeError): - translator_name = "Unknown Translator" - return f"Translation by {translator_name}" \ No newline at end of file + return f"[{self.language_code}] {self.translator}: {self.translation_text[:50]}" \ No newline at end of file diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py index 10d5005..73bfd39 100644 --- a/apps/hadis/serializers/category.py +++ b/apps/hadis/serializers/category.py @@ -326,18 +326,27 @@ class CategorySerializer(serializers.ModelSerializer): from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation class QuranSecondaryTranslationSerializer(serializers.ModelSerializer): - translation_text = LocalizedField() - translator = LocalizedField() - class Meta: model = QuranSecondaryTranslation - fields = ['id', 'translation_text', 'translator'] + fields = ['id', 'language_code', 'translation_text', 'translator'] class CategoryQuranVerseSerializer(serializers.ModelSerializer): surah_name = LocalizedField() translation = LocalizedField() translator = LocalizedField() - secondary_translations = QuranSecondaryTranslationSerializer(many=True, read_only=True) + secondary_translations = serializers.SerializerMethodField() + + def get_secondary_translations(self, obj): + request = self.context.get('request') + lang = None + if request: + lang = request.query_params.get('lang') or getattr(request, 'LANGUAGE_CODE', None) + if not lang: + lang = 'ru' + + # Return only the secondary translations for the requested language + qs = obj.secondary_translations.filter(language_code=lang) + return QuranSecondaryTranslationSerializer(qs, many=True).data class Meta: model = CategoryQuranVerse diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 5e71acf..a0bfb0e 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -64,6 +64,7 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer): 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): @@ -81,6 +82,11 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer): return bool(obj.annotated_has_hadith) return obj.hadis_set.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 [] @@ -114,6 +120,7 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer): "has_children", "is_leaf", "has_hadith", + "has_quran_verse", "ancestors", ] @@ -1877,14 +1884,16 @@ 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) + 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", "translation_text", "translator"] + 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) @@ -1895,44 +1904,63 @@ class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer): 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): - 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 + 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): - 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 + 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): diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 6356335..872f426 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -14,7 +14,8 @@ from .models import ( BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText, CorrectionReference, - ContentRelease + ContentRelease, + CategoryQuranVerse, ) from .serializers.serializers_admin import ( AdminHadisCategorySerializer, @@ -63,13 +64,34 @@ class AdminHadisCategoryViewSet(ReadOnlyModelViewSet): def get_queryset(self): hadis_subquery = Hadis.objects.filter(category=OuterRef("pk")) children_subquery = HadisCategory.objects.filter(parent=OuterRef("pk")) + quran_verse_subquery = CategoryQuranVerse.objects.filter(category=OuterRef("pk")) queryset = HadisCategory.objects.select_related("parent", "sect").annotate( annotated_has_hadith=Exists(hadis_subquery), annotated_has_children=Exists(children_subquery), + annotated_has_quran_verse=Exists(quran_verse_subquery), ) - source_type = self.request.query_params.get("source_type") - if source_type: - queryset = queryset.filter(source_type=source_type) + + has_quran_verse = self.request.query_params.get("has_quran_verse") + exclude_verse_id = self.request.query_params.get("exclude_verse_id") + + if has_quran_verse in ["false", "False", "0"]: + if exclude_verse_id: + queryset = queryset.filter( + Q(source_type="quran", quran_verse__isnull=True) + | Q(quran_verse__id=exclude_verse_id) + ) + else: + queryset = queryset.filter(source_type="quran", quran_verse__isnull=True) + elif has_quran_verse in ["true", "True", "1"]: + queryset = queryset.filter(quran_verse__isnull=False) + source_type = self.request.query_params.get("source_type") + if source_type: + queryset = queryset.filter(source_type=source_type) + else: + source_type = self.request.query_params.get("source_type") + if source_type: + queryset = queryset.filter(source_type=source_type) + search = self.request.query_params.get("search") if search: queryset = queryset.filter( @@ -752,6 +774,7 @@ class AdminCategoryQuranVerseViewSet(ModelViewSet): permission_classes = [IsAuthenticated, IsSuperAdmin] authentication_classes = [TokenAuthentication] parser_classes = (JSONParser, MultiPartParser, FormParser) + pagination_class = OptionalPagination def get_serializer_class(self): from .serializers.serializers_admin import AdminCategoryQuranVerseSerializer @@ -759,11 +782,34 @@ class AdminCategoryQuranVerseViewSet(ModelViewSet): def get_queryset(self): from .models.category import CategoryQuranVerse - queryset = CategoryQuranVerse.objects.all().prefetch_related("secondary_translations") + queryset = ( + CategoryQuranVerse.objects.all() + .select_related("category", "category__sect") + .prefetch_related("secondary_translations") + ) + + search_query = self.request.query_params.get("search") + if search_query: + q_filter = ( + Q(surah_name__icontains=search_query) + | Q(ayah_number__icontains=search_query) + | Q(arabic_text__icontains=search_query) + | Q(translation__icontains=search_query) + | Q(translator__icontains=search_query) + | Q(category__title__icontains=search_query) + | Q(category__slug__icontains=search_query) + ) + queryset = queryset.filter(q_filter) + category_id = self.request.query_params.get("category") - if category_id: + if category_id and category_id != "all": queryset = queryset.filter(category_id=category_id) - return queryset + + source_type = self.request.query_params.get("source_type") + if source_type: + queryset = queryset.filter(category__source_type=source_type) + + return queryset.order_by("-id") class AdminHadisTransmitterViewSet(ModelViewSet):