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

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.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)

47
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

4
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]

Loading…
Cancel
Save