Browse Source

feat(reference): add BookAuthorRutba model, admin, sync serializer, and migration

- Introduce BookAuthorRutba model with title, slug, color, and description
- Link BookAuthor to BookAuthorRutba via rutba_status foreign key
- Update reference admin to manage BookAuthorRutba and assign rutba status
- Include rutba_detail in BookAuthorDetailSerializer and BookAuthorSyncSerializer
- Add migration 0054 to seed rutba records from existing authors
master
Mohsen Taba 2 weeks ago
parent
commit
d375b232cf
  1. 25
      apps/hadis/admin/reference.py
  2. 76
      apps/hadis/migrations/0054_bookauthorrutba_opinionstatus_description_and_more.py
  3. 59
      apps/hadis/models/reference.py
  4. 47
      apps/hadis/serializers/reference_v2.py
  5. 4
      apps/hadis/views/reference_v2.py

25
apps/hadis/admin/reference.py

@ -14,6 +14,7 @@ from ..models import (
BookReference, BookReference,
BookReferenceImage, BookReferenceImage,
BookAuthor, BookAuthor,
BookAuthorRutba,
BookAttribute, BookAttribute,
BookSubjectArea, BookSubjectArea,
BookType, BookType,
@ -533,7 +534,7 @@ class BookAuthorAdmin(ModelAdmin):
fieldsets = ( fieldsets = (
(_('Basic Info'), { (_('Basic Info'), {
'fields': ('name',)
'fields': ('name', 'rutba_status')
}), }),
(_('Dates (Hijri)'), { (_('Dates (Hijri)'), {
'fields': ('birth_year_hijri', 'death_year_hijri') 'fields': ('birth_year_hijri', 'death_year_hijri')
@ -555,6 +556,27 @@ class BookAuthorAdmin(ModelAdmin):
return first.get('text', '-') return first.get('text', '-')
return '-' 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): class BookReferenceImageAdmin(ModelAdmin):
"""Admin for BookReferenceImage model""" """Admin for BookReferenceImage model"""
form = BookReferenceImageAdminForm form = BookReferenceImageAdminForm
@ -670,6 +692,7 @@ class BookTagAdmin(ModelAdmin):
dovoodi_admin_site.register(BookReference, BookReferenceAdmin) dovoodi_admin_site.register(BookReference, BookReferenceAdmin)
dovoodi_admin_site.register(BookAuthor, BookAuthorAdmin) dovoodi_admin_site.register(BookAuthor, BookAuthorAdmin)
dovoodi_admin_site.register(BookAuthorRutba, BookAuthorRutbaAdmin)
dovoodi_admin_site.register(BookAttribute, BookAttributeAdmin) dovoodi_admin_site.register(BookAttribute, BookAttributeAdmin)
dovoodi_admin_site.register(BookReferenceImage, BookReferenceImageAdmin) dovoodi_admin_site.register(BookReferenceImage, BookReferenceImageAdmin)
dovoodi_admin_site.register(BookTag, BookTagAdmin) dovoodi_admin_site.register(BookTag, BookTagAdmin)

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

59
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.utils.text import slugify
from django.conf import settings from django.conf import settings
from typing import Optional from typing import Optional
from utils.mixins import LowercaseSlugMixin
from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin
class BookSubjectArea(models.Model): class BookSubjectArea(models.Model):
title = models.JSONField(default = list , verbose_name=_('Title')) 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)}" 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): class BookAuthor(LowercaseSlugMixin, models.Model):
slug_source_field = 'name' slug_source_field = 'name'
""" """
@ -412,6 +459,14 @@ class BookAuthor(LowercaseSlugMixin, models.Model):
scholarity = models.JSONField(default=list, blank=True, verbose_name=_('Scholarity')) scholarity = models.JSONField(default=list, blank=True, verbose_name=_('Scholarity'))
tabaqa = models.JSONField(default=list, blank=True, verbose_name=_('Tabaqa')) tabaqa = models.JSONField(default=list, blank=True, verbose_name=_('Tabaqa'))
rutba = models.JSONField(default=list, blank=True, verbose_name=_('Rutba')) 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')) features = models.JSONField(default=list, blank=True, verbose_name=_('Features'))
birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) 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 return None
def save(self, *args, **kwargs): 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) super().save(*args, **kwargs)
self.researcher_profiles.all().update(name=self.name, slug=self.slug) self.researcher_profiles.all().update(name=self.name, slug=self.slug)

47
apps/hadis/serializers/reference_v2.py

@ -308,7 +308,8 @@ class BookAuthorDetailSerializer(serializers.ModelSerializer):
known_as = LocalizedField() known_as = LocalizedField()
scholarity = LocalizedField() scholarity = LocalizedField()
tabaqa = LocalizedField() tabaqa = LocalizedField()
rutba = LocalizedField()
rutba = serializers.SerializerMethodField()
rutba_detail = serializers.SerializerMethodField()
features = LocalizedField() features = LocalizedField()
related_narrator_slug = serializers.CharField(source='related_narrator.slug', read_only=True, allow_null=True) 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 model = BookAuthor
fields = [ fields = [
'id', 'slug', 'name', 'image', 'known_as', 'scholarity', 'id', 'slug', 'name', 'image', 'known_as', 'scholarity',
'tabaqa', 'rutba', 'features',
'tabaqa', 'rutba', 'rutba_detail', 'features',
'birth_year_hijri', 'death_year_hijri', 'birth_year_hijri', 'death_year_hijri',
'birth_year_miladi', 'death_year_miladi', 'birth_year_miladi', 'death_year_miladi',
'related_narrator_slug' '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): class AuthorBookReferenceSerializer(serializers.ModelSerializer):
"""سریالایزر برای لیست رفرنس‌های (کتاب‌های) یک نویسنده""" """سریالایزر برای لیست رفرنس‌های (کتاب‌های) یک نویسنده"""
@ -365,7 +385,8 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer):
known_as = LocalizedField() known_as = LocalizedField()
scholarity = LocalizedField() scholarity = LocalizedField()
tabaqa = LocalizedField() tabaqa = LocalizedField()
rutba = LocalizedField()
rutba = serializers.SerializerMethodField()
rutba_detail = serializers.SerializerMethodField()
features = LocalizedField() features = LocalizedField()
related_narrator_slug = serializers.CharField(source='related_narrator.slug', read_only=True, allow_null=True) related_narrator_slug = serializers.CharField(source='related_narrator.slug', read_only=True, allow_null=True)
@ -382,6 +403,7 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer):
'scholarity', 'scholarity',
'tabaqa', 'tabaqa',
'rutba', 'rutba',
'rutba_detail',
'features', 'features',
'birth_year_hijri', 'birth_year_hijri',
'death_year_hijri', 'death_year_hijri',
@ -390,3 +412,22 @@ class BookAuthorSyncSerializer(serializers.ModelSerializer):
'books', # 👈 لیست کتاب‌های هر نویسنده 'books', # 👈 لیست کتاب‌های هر نویسنده
'related_narrator_slug' '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

4
apps/hadis/views/reference_v2.py

@ -285,7 +285,7 @@ class BookAuthorDetailView(RetrieveAPIView):
""" """
دریافت اطلاعات دقیق یک نویسنده بر اساس slug دریافت اطلاعات دقیق یک نویسنده بر اساس slug
""" """
queryset = BookAuthor.objects.select_related('related_narrator').all()
queryset = BookAuthor.objects.select_related('related_narrator', 'rutba_status').all()
serializer_class = BookAuthorDetailSerializer serializer_class = BookAuthorDetailSerializer
lookup_field = 'slug' lookup_field = 'slug'
lookup_url_kwarg = 'author_slug' lookup_url_kwarg = 'author_slug'
@ -330,7 +330,7 @@ class BookAuthorSyncView(generics.ListAPIView):
Returns complete author data and their associated books without pagination. Returns complete author data and their associated books without pagination.
""" """
# 👇 استفاده از prefetch_related برای جلوگیری از کرش کردن دیتابیس به خاطر N+1 Query # 👇 استفاده از 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 serializer_class = BookAuthorSyncSerializer
pagination_class = NoPagination pagination_class = NoPagination
permission_classes = [AllowAny] permission_classes = [AllowAny]

Loading…
Cancel
Save