diff --git a/apps/hadis/admin/reference.py b/apps/hadis/admin/reference.py index 922de5d..b94d0de 100644 --- a/apps/hadis/admin/reference.py +++ b/apps/hadis/admin/reference.py @@ -14,6 +14,7 @@ from ..models import ( BookReference, BookReferenceImage, BookAuthor, + BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, @@ -533,7 +534,7 @@ class BookAuthorAdmin(ModelAdmin): fieldsets = ( (_('Basic Info'), { - 'fields': ('name',) + 'fields': ('name', 'rutba_status') }), (_('Dates (Hijri)'), { 'fields': ('birth_year_hijri', 'death_year_hijri') @@ -555,6 +556,27 @@ class BookAuthorAdmin(ModelAdmin): return first.get('text', '-') return '-' + +class BookAuthorRutbaAdmin(ModelAdmin): + list_display = ('get_title_display', 'slug', 'color') + list_filter = ('color',) + search_fields = ('title', 'slug', 'description') + readonly_fields = ('slug',) + + fieldsets = ( + (None, { + 'fields': ('title', 'slug', 'color', 'description') + }), + ) + + @display(description=_('Title'), ordering='title') + def get_title_display(self, obj): + if obj.title and isinstance(obj.title, list) and len(obj.title) > 0: + first = obj.title[0] + if isinstance(first, dict): + return first.get('text', '-') + return '-' + class BookReferenceImageAdmin(ModelAdmin): """Admin for BookReferenceImage model""" form = BookReferenceImageAdminForm @@ -670,6 +692,7 @@ class BookTagAdmin(ModelAdmin): dovoodi_admin_site.register(BookReference, BookReferenceAdmin) dovoodi_admin_site.register(BookAuthor, BookAuthorAdmin) +dovoodi_admin_site.register(BookAuthorRutba, BookAuthorRutbaAdmin) dovoodi_admin_site.register(BookAttribute, BookAttributeAdmin) dovoodi_admin_site.register(BookReferenceImage, BookReferenceImageAdmin) dovoodi_admin_site.register(BookTag, BookTagAdmin) diff --git a/apps/hadis/migrations/0054_bookauthorrutba_opinionstatus_description_and_more.py b/apps/hadis/migrations/0054_bookauthorrutba_opinionstatus_description_and_more.py new file mode 100644 index 0000000..d2e2452 --- /dev/null +++ b/apps/hadis/migrations/0054_bookauthorrutba_opinionstatus_description_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.30 on 2026-10-19 12:08 + +from django.db import migrations, models +import django.db.models.deletion +from django.utils.text import slugify + + +def migrate_existing_author_rutbas(apps, schema_editor): + BookAuthor = apps.get_model('hadis', 'BookAuthor') + BookAuthorRutba = apps.get_model('hadis', 'BookAuthorRutba') + + seen = {} + for author in BookAuthor.objects.exclude(rutba=[]): + if not author.rutba or not isinstance(author.rutba, list): + continue + first_text = "" + for entry in author.rutba: + if isinstance(entry, dict): + txt = (entry.get("text") or entry.get("title") or "").strip() + if txt: + first_text = txt + break + if not first_text: + continue + key = first_text.lower() + if key not in seen: + slug_val = slugify(first_text, allow_unicode=True) or f"rutba-{author.id}" + rutba_obj = BookAuthorRutba.objects.create( + title=author.rutba, + slug=slug_val, + color='green', + description=[], + ) + seen[key] = rutba_obj + author.rutba_status = seen[key] + author.save(update_fields=['rutba_status']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0053_scholarlyopinion_and_transmitteropinion_opinion'), + ] + + operations = [ + migrations.CreateModel( + name='BookAuthorRutba', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('color', models.CharField(choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue'), ('yellow', 'Yellow'), ('orange', 'Orange'), ('purple', 'Purple'), ('gray', 'Gray')], default='gray', max_length=20, verbose_name='color')), + ('title', models.JSONField(default=list, verbose_name='Title')), + ('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug')), + ('description', models.JSONField(default=list, verbose_name='Description')), + ], + options={ + 'verbose_name': 'Author Rutba', + 'verbose_name_plural': 'Author Rutbas', + }, + ), + migrations.AddField( + model_name='opinionstatus', + name='description', + field=models.JSONField(default=list, verbose_name='Description'), + ), + migrations.AddField( + model_name='transmitterreliability', + name='description', + field=models.JSONField(default=list, verbose_name='Description'), + ), + migrations.AddField( + model_name='bookauthor', + name='rutba_status', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='authors', to='hadis.bookauthorrutba', verbose_name='Rutba Status'), + ), + migrations.RunPython(migrate_existing_author_rutbas, migrations.RunPython.noop), + ] diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index c475d76..feb0a98 100644 --- a/apps/hadis/models/reference.py +++ b/apps/hadis/models/reference.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django.utils.text import slugify from django.conf import settings from typing import Optional -from utils.mixins import LowercaseSlugMixin +from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin class BookSubjectArea(models.Model): title = models.JSONField(default = list , verbose_name=_('Title')) @@ -390,6 +390,53 @@ class BookReferenceDocument(models.Model): return f"{self.book_reference} - {self.title or 'Doc ' + str(self.id)}" +class BookAuthorRutba(ColorPaletteMixin, LowercaseSlugMixin, models.Model): + slug_source_field = 'title' + + title = models.JSONField(default=list, verbose_name=_('Title')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, null=True, allow_unicode=True) + description = models.JSONField(default=list, verbose_name=_('Description')) + + def __str__(self): + if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict): + return self.title[0].get('text') or self.title[0].get('title') or str(self.id) + return str(self.id) + + def get_title(self, lang): + if not self.title or not isinstance(self.title, list): + return None + + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == lang: + return tr.get('text', '') or tr.get('title', '') + + for tr in self.title: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + return tr.get('text', '') or tr.get('title', '') + return None + + def get_description(self, lang): + if not self.description or not isinstance(self.description, list): + return None + + for tr in self.description: + if isinstance(tr, dict) and tr.get('language_code') == lang: + return tr.get('text', '') or tr.get('title', '') + + for tr in self.description: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + return tr.get('text', '') or tr.get('title', '') + return None + + class Meta: + verbose_name = _('Author Rutba') + verbose_name_plural = _('Author Rutbas') + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.authors.all().update(rutba=self.title or []) + + class BookAuthor(LowercaseSlugMixin, models.Model): slug_source_field = 'name' """ @@ -412,6 +459,14 @@ class BookAuthor(LowercaseSlugMixin, models.Model): scholarity = models.JSONField(default=list, blank=True, verbose_name=_('Scholarity')) tabaqa = models.JSONField(default=list, blank=True, verbose_name=_('Tabaqa')) rutba = models.JSONField(default=list, blank=True, verbose_name=_('Rutba')) + rutba_status = models.ForeignKey( + 'BookAuthorRutba', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='authors', + verbose_name=_('Rutba Status') + ) features = models.JSONField(default=list, blank=True, verbose_name=_('Features')) birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) @@ -456,6 +511,8 @@ class BookAuthor(LowercaseSlugMixin, models.Model): return None def save(self, *args, **kwargs): + if self.rutba_status_id and self.rutba_status: + self.rutba = self.rutba_status.title or [] super().save(*args, **kwargs) self.researcher_profiles.all().update(name=self.name, slug=self.slug) diff --git a/apps/hadis/serializers/reference_v2.py b/apps/hadis/serializers/reference_v2.py index 9ebd870..ab9d651 100644 --- a/apps/hadis/serializers/reference_v2.py +++ b/apps/hadis/serializers/reference_v2.py @@ -308,7 +308,8 @@ class BookAuthorDetailSerializer(serializers.ModelSerializer): known_as = LocalizedField() scholarity = LocalizedField() tabaqa = LocalizedField() - rutba = LocalizedField() + rutba = serializers.SerializerMethodField() + rutba_detail = serializers.SerializerMethodField() features = LocalizedField() related_narrator_slug = serializers.CharField(source='related_narrator.slug', read_only=True, allow_null=True) @@ -316,12 +317,31 @@ class BookAuthorDetailSerializer(serializers.ModelSerializer): model = BookAuthor fields = [ 'id', 'slug', 'name', 'image', 'known_as', 'scholarity', - 'tabaqa', 'rutba', 'features', + 'tabaqa', 'rutba', 'rutba_detail', 'features', 'birth_year_hijri', 'death_year_hijri', 'birth_year_miladi', 'death_year_miladi', 'related_narrator_slug' ] + def get_rutba(self, obj): + request = self.context.get('request') + if obj.rutba_status: + return get_localized_text(obj.rutba_status.title, request) + return get_localized_text(obj.rutba, request) if obj.rutba else None + + def get_rutba_detail(self, obj): + if obj.rutba_status: + request = self.context.get('request') + return { + 'id': obj.rutba_status.id, + 'title': get_localized_text(obj.rutba_status.title, request), + 'slug': obj.rutba_status.slug, + 'color': obj.rutba_status.color, + 'main_color_code': obj.rutba_status.main_color_code, + 'description': get_localized_text(obj.rutba_status.description, request), + } + return None + class AuthorBookReferenceSerializer(serializers.ModelSerializer): """سریالایزر برای لیست رفرنس‌های (کتاب‌های) یک نویسنده""" @@ -365,7 +385,8 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer): known_as = LocalizedField() scholarity = LocalizedField() tabaqa = LocalizedField() - rutba = LocalizedField() + rutba = serializers.SerializerMethodField() + rutba_detail = serializers.SerializerMethodField() features = LocalizedField() related_narrator_slug = serializers.CharField(source='related_narrator.slug', read_only=True, allow_null=True) @@ -382,6 +403,7 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer): 'scholarity', 'tabaqa', 'rutba', + 'rutba_detail', 'features', 'birth_year_hijri', 'death_year_hijri', @@ -390,3 +412,22 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer): 'books', # 👈 لیست کتاب‌های هر نویسنده 'related_narrator_slug' ] + + def get_rutba(self, obj): + request = self.context.get('request') + if obj.rutba_status: + return get_localized_text(obj.rutba_status.title, request) + return get_localized_text(obj.rutba, request) if obj.rutba else None + + def get_rutba_detail(self, obj): + if obj.rutba_status: + request = self.context.get('request') + return { + 'id': obj.rutba_status.id, + 'title': get_localized_text(obj.rutba_status.title, request), + 'slug': obj.rutba_status.slug, + 'color': obj.rutba_status.color, + 'main_color_code': obj.rutba_status.main_color_code, + 'description': get_localized_text(obj.rutba_status.description, request), + } + return None diff --git a/apps/hadis/views/reference_v2.py b/apps/hadis/views/reference_v2.py index 228d383..4914287 100644 --- a/apps/hadis/views/reference_v2.py +++ b/apps/hadis/views/reference_v2.py @@ -285,7 +285,7 @@ class BookAuthorDetailView(RetrieveAPIView): """ دریافت اطلاعات دقیق یک نویسنده بر اساس slug """ - queryset = BookAuthor.objects.select_related('related_narrator').all() + queryset = BookAuthor.objects.select_related('related_narrator', 'rutba_status').all() serializer_class = BookAuthorDetailSerializer lookup_field = 'slug' lookup_url_kwarg = 'author_slug' @@ -330,7 +330,7 @@ class BookAuthorSyncView(generics.ListAPIView): Returns complete author data and their associated books without pagination. """ # 👇 استفاده از prefetch_related برای جلوگیری از کرش کردن دیتابیس به خاطر N+1 Query - queryset = BookAuthor.objects.select_related('related_narrator').prefetch_related('book_references').order_by('id') + queryset = BookAuthor.objects.select_related('related_narrator', 'rutba_status').prefetch_related('book_references').order_by('id') serializer_class = BookAuthorSyncSerializer pagination_class = NoPagination permission_classes = [AllowAny]