Browse Source

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
master
Mohsen Taba 3 weeks ago
parent
commit
541a381812
  1. 77
      apps/hadis/migrations/0042_add_secondary_quran_fields.py
  2. 30
      apps/hadis/migrations/0043_finalize_secondary_quran_translations.py
  3. 18
      apps/hadis/models/category.py
  4. 19
      apps/hadis/serializers/category.py
  5. 88
      apps/hadis/serializers/serializers_admin.py
  6. 60
      apps/hadis/views_admin.py

77
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,
),
]

30
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',
),
]

18
apps/hadis/models/category.py

@ -214,7 +214,7 @@ class CategoryQuranVerse(models.Model):
class QuranSecondaryTranslation(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( quran_verse = models.ForeignKey(
CategoryQuranVerse, CategoryQuranVerse,
@ -222,8 +222,14 @@ class QuranSecondaryTranslation(models.Model):
related_name='secondary_translations', related_name='secondary_translations',
verbose_name=_('Quran Verse') 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')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At'))
@ -234,8 +240,4 @@ class QuranSecondaryTranslation(models.Model):
ordering = ['id'] ordering = ['id']
def __str__(self): 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}"
return f"[{self.language_code}] {self.translator}: {self.translation_text[:50]}"

19
apps/hadis/serializers/category.py

@ -326,18 +326,27 @@ class CategorySerializer(serializers.ModelSerializer):
from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation
class QuranSecondaryTranslationSerializer(serializers.ModelSerializer): class QuranSecondaryTranslationSerializer(serializers.ModelSerializer):
translation_text = LocalizedField()
translator = LocalizedField()
class Meta: class Meta:
model = QuranSecondaryTranslation model = QuranSecondaryTranslation
fields = ['id', 'translation_text', 'translator']
fields = ['id', 'language_code', 'translation_text', 'translator']
class CategoryQuranVerseSerializer(serializers.ModelSerializer): class CategoryQuranVerseSerializer(serializers.ModelSerializer):
surah_name = LocalizedField() surah_name = LocalizedField()
translation = LocalizedField() translation = LocalizedField()
translator = 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: class Meta:
model = CategoryQuranVerse model = CategoryQuranVerse

88
apps/hadis/serializers/serializers_admin.py

@ -64,6 +64,7 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer):
has_children = serializers.SerializerMethodField() has_children = serializers.SerializerMethodField()
is_leaf = serializers.SerializerMethodField() is_leaf = serializers.SerializerMethodField()
has_hadith = serializers.SerializerMethodField() has_hadith = serializers.SerializerMethodField()
has_quran_verse = serializers.SerializerMethodField()
ancestors = serializers.SerializerMethodField() ancestors = serializers.SerializerMethodField()
def get_has_children(self, obj): def get_has_children(self, obj):
@ -81,6 +82,11 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer):
return bool(obj.annotated_has_hadith) return bool(obj.annotated_has_hadith)
return obj.hadis_set.exists() 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): def get_ancestors(self, obj):
if not obj: if not obj:
return [] return []
@ -114,6 +120,7 @@ class AdminHadisCategorySerializer(serializers.ModelSerializer):
"has_children", "has_children",
"is_leaf", "is_leaf",
"has_hadith", "has_hadith",
"has_quran_verse",
"ancestors", "ancestors",
] ]
@ -1877,14 +1884,16 @@ from ..models.category import CategoryQuranVerse, QuranSecondaryTranslation
class AdminQuranSecondaryTranslationSerializer(serializers.ModelSerializer): class AdminQuranSecondaryTranslationSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False) 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: class Meta:
model = QuranSecondaryTranslation model = QuranSecondaryTranslation
fields = ["id", "translation_text", "translator"]
fields = ["id", "language_code", "translation_text", "translator"]
class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer): class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer):
category_detail = AdminHadisCategorySerializer(source="category", read_only=True)
surah_name = serializers.JSONField(default=list) surah_name = serializers.JSONField(default=list)
translation = serializers.JSONField(default=list) translation = serializers.JSONField(default=list)
translator = serializers.JSONField(default=list) translator = serializers.JSONField(default=list)
@ -1895,44 +1904,63 @@ class AdminCategoryQuranVerseSerializer(serializers.ModelSerializer):
fields = [ fields = [
"id", "id",
"category", "category",
"category_detail",
"surah_name", "surah_name",
"ayah_number", "ayah_number",
"arabic_text", "arabic_text",
"translation", "translation",
"translator", "translator",
"secondary_translations", "secondary_translations",
"created_at",
"updated_at",
] ]
read_only_fields = ["id", "created_at", "updated_at"]
def create(self, validated_data): 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): 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): class AdminHadisTransmitterSerializer(serializers.ModelSerializer):

60
apps/hadis/views_admin.py

@ -14,7 +14,8 @@ from .models import (
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText,
CorrectionReference, CorrectionReference,
ContentRelease
ContentRelease,
CategoryQuranVerse,
) )
from .serializers.serializers_admin import ( from .serializers.serializers_admin import (
AdminHadisCategorySerializer, AdminHadisCategorySerializer,
@ -63,13 +64,34 @@ class AdminHadisCategoryViewSet(ReadOnlyModelViewSet):
def get_queryset(self): def get_queryset(self):
hadis_subquery = Hadis.objects.filter(category=OuterRef("pk")) hadis_subquery = Hadis.objects.filter(category=OuterRef("pk"))
children_subquery = HadisCategory.objects.filter(parent=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( queryset = HadisCategory.objects.select_related("parent", "sect").annotate(
annotated_has_hadith=Exists(hadis_subquery), annotated_has_hadith=Exists(hadis_subquery),
annotated_has_children=Exists(children_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") search = self.request.query_params.get("search")
if search: if search:
queryset = queryset.filter( queryset = queryset.filter(
@ -752,6 +774,7 @@ class AdminCategoryQuranVerseViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin] permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
parser_classes = (JSONParser, MultiPartParser, FormParser) parser_classes = (JSONParser, MultiPartParser, FormParser)
pagination_class = OptionalPagination
def get_serializer_class(self): def get_serializer_class(self):
from .serializers.serializers_admin import AdminCategoryQuranVerseSerializer from .serializers.serializers_admin import AdminCategoryQuranVerseSerializer
@ -759,11 +782,34 @@ class AdminCategoryQuranVerseViewSet(ModelViewSet):
def get_queryset(self): def get_queryset(self):
from .models.category import CategoryQuranVerse 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") 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) 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): class AdminHadisTransmitterViewSet(ModelViewSet):

Loading…
Cancel
Save