diff --git a/apps/hadis/admin/reference.py b/apps/hadis/admin/reference.py index 41d5940..46f683d 100644 --- a/apps/hadis/admin/reference.py +++ b/apps/hadis/admin/reference.py @@ -433,17 +433,15 @@ class BookReferenceAdmin(ModelAdmin): list_display = ( 'get_title_display', 'slug', - 'get_publisher_display', - 'year_of_publication', 'rate', 'created_at' ) - list_filter = ('year_of_publication', 'rate', 'created_at', 'type') + list_filter = ('rate', 'created_at', 'type') # Searching by JSON fields via string is limited in Django, # so we focus on standard fields - search_fields = ('isbn', 'slug', 'volume') + search_fields = ('slug',) readonly_fields = ('created_at', 'updated_at') @@ -458,7 +456,7 @@ class BookReferenceAdmin(ModelAdmin): 'fields': ('title', 'description', 'slug', 'language') }), (_('Publication Info'), { - 'fields': ('publisher', 'isbn', 'year_of_publication', 'number_page', 'volume') + 'fields': ('number_page',) }), (_('Rating & Stats'), { 'fields': ('rate', 'created_at', 'updated_at') @@ -471,10 +469,6 @@ class BookReferenceAdmin(ModelAdmin): def get_title_display(self, obj): return self._extract_first_text(obj.title) - @display(description=_('Publisher')) - def get_publisher_display(self, obj): - return self._extract_first_text(obj.publisher) - def _extract_first_text(self, json_data): """Helper to safely extract the first 'text' from a JSON list""" if json_data and isinstance(json_data, list) and len(json_data) > 0: diff --git a/apps/hadis/management/commands/populate_book_metadata.py b/apps/hadis/management/commands/populate_book_metadata.py new file mode 100644 index 0000000..850a6f6 --- /dev/null +++ b/apps/hadis/management/commands/populate_book_metadata.py @@ -0,0 +1,74 @@ +import json +from django.core.management.base import BaseCommand +from apps.hadis.models import BookReference, BookAuthor, BookEdition, BookVolume + +class Command(BaseCommand): + help = 'Populates BookReference, BookEdition, and BookVolume with default test data if empty.' + + def handle(self, *args, **kwargs): + self.stdout.write("Starting to populate book metadata...") + + # Default values + default_title = "Musnad Abu Ya'la" + default_author_name = "Abu Ya'la al-Mawsili, Imam Hafiz Ahmad ibn Ali ibn al-Muthanna al-Tamimi" + default_summary = "Various publishers (originally compiled in manuscript form; published by multiple Islamic publishers over time, such as Dar al-Ihya al-Turath al-Arabi, Beirut)" + + default_publisher = "Dar al-Hadith" + default_city = "Cairo" + default_country = "Egypt" + default_edition_stmt = "1st edition" + default_year = "1434 AH / 2013 CE" + default_source_url = "https://shamela.ws/book/181" + default_tag = "Hadith collection" + + books = BookReference.objects.all() + count = 0 + + for book in books: + # 1. Update Title and Description if empty + if not book.title: + book.title = [{"language_code": "en", "text": default_title}] + + if not book.description: + book.description = [{"language_code": "en", "text": default_summary}] + + book.save() + + # 2. Assign Author if none exists + if not book.authors.exists(): + author, created = BookAuthor.objects.get_or_create( + slug="abu-yala-al-mawsili", + defaults={ + "name": [{"language_code": "en", "text": default_author_name}] + } + ) + book.authors.add(author) + + # 3. Create BookEdition if none exists + if not book.editions.exists(): + edition = BookEdition.objects.create( + book_reference=book, + publisher=[{"language_code": "en", "text": default_publisher}], + city_of_publication=[{"language_code": "en", "text": default_city}], + country_of_publication=[{"language_code": "en", "text": default_country}], + edition_number=default_edition_stmt, + year_of_publication=default_year, + source_url=default_source_url, + number_of_volumes=1, + tags=[{"language_code": "en", "text": default_tag}], + notes=[] + ) + else: + edition = book.editions.first() + + # 4. Create BookVolume if none exists + if not book.volumes.exists(): + BookVolume.objects.create( + book_reference=book, + edition=edition, + title="Volume 1" + ) + + count += 1 + + self.stdout.write(self.style.SUCCESS(f"Successfully populated metadata for {count} books.")) diff --git a/apps/hadis/migrations/0016_remove_bookreference_city_of_publication_and_more.py b/apps/hadis/migrations/0016_remove_bookreference_city_of_publication_and_more.py new file mode 100644 index 0000000..482397a --- /dev/null +++ b/apps/hadis/migrations/0016_remove_bookreference_city_of_publication_and_more.py @@ -0,0 +1,113 @@ +# Generated by Django 5.2.12 on 2026-06-20 12:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0015_categoryquranverse_quransecondarytranslation'), + ] + + operations = [ + migrations.RemoveField( + model_name='bookreference', + name='city_of_publication', + ), + migrations.RemoveField( + model_name='bookreference', + name='country_of_publication', + ), + migrations.RemoveField( + model_name='bookreference', + name='edition_number', + ), + migrations.RemoveField( + model_name='bookreference', + name='isbn', + ), + migrations.RemoveField( + model_name='bookreference', + name='number_of_volumes', + ), + migrations.RemoveField( + model_name='bookreference', + name='publisher', + ), + migrations.RemoveField( + model_name='bookreference', + name='source_url', + ), + migrations.RemoveField( + model_name='bookreference', + name='volume', + ), + migrations.RemoveField( + model_name='bookreference', + name='year_of_publication', + ), + migrations.RemoveField( + model_name='bookreferencedocument', + name='volume', + ), + migrations.RemoveField( + model_name='bookreferenceimage', + name='volume', + ), + migrations.AddField( + model_name='bookauthor', + name='slug', + field=models.SlugField(blank=True, max_length=255, null=True, unique=True, verbose_name='slug'), + ), + migrations.CreateModel( + name='BookEdition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('publisher', models.JSONField(blank=True, default=list, verbose_name='Publisher')), + ('city_of_publication', models.JSONField(blank=True, default=list, verbose_name='City of Publication')), + ('country_of_publication', models.JSONField(blank=True, default=list, verbose_name='Country of Publication')), + ('edition_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Edition Number')), + ('year_of_publication', models.CharField(blank=True, max_length=50, null=True, verbose_name='year of publication')), + ('number_of_volumes', models.PositiveIntegerField(blank=True, null=True, verbose_name='Total Number of Volumes')), + ('source_url', models.URLField(blank=True, max_length=1000, null=True, verbose_name='Online Source URL')), + ('isbn', models.CharField(blank=True, max_length=100, null=True, verbose_name='ISBN')), + ('notes', models.JSONField(blank=True, default=list, verbose_name='Notes')), + ('tags', models.JSONField(blank=True, default=list, verbose_name='Tags')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('book_reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='editions', to='hadis.bookreference', verbose_name='book reference')), + ], + options={ + 'verbose_name': 'Book Edition', + 'verbose_name_plural': 'Book Editions', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='BookVolume', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, help_text='e.g., Volume 1', max_length=255, null=True, verbose_name='Title')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), + ('book_reference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='hadis.bookreference', verbose_name='book reference')), + ('edition', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='volumes', to='hadis.bookedition', verbose_name='edition')), + ], + options={ + 'verbose_name': 'Book Volume', + 'verbose_name_plural': 'Book Volumes', + 'ordering': ['title'], + }, + ), + migrations.AddField( + model_name='bookreferencedocument', + name='book_volume', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='hadis.bookvolume', verbose_name='book volume'), + ), + migrations.AddField( + model_name='bookreferenceimage', + name='book_volume', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='hadis.bookvolume', verbose_name='book volume'), + ), + ] diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index 2d9dd97..4ee0861 100644 --- a/apps/hadis/models/reference.py +++ b/apps/hadis/models/reference.py @@ -61,24 +61,15 @@ class BookReference(models.Model): This is different from library books - these are reference books for hadis """ legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True) - source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Source URL')) title = models.JSONField(default = list , verbose_name=_('Title')) description = models.JSONField(default = list , verbose_name=_('Description')) language = models.JSONField(default = list , verbose_name=_('Language')) - isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True) - volume = models.CharField(max_length=100, verbose_name=_('volume'), blank=True, null=True) - number_of_volumes = models.PositiveIntegerField(verbose_name=_('Total Number of Volumes'), null=True, blank=True) - year_of_publication = models.CharField(max_length=50, verbose_name=_('year of publication'), blank=True, null=True) number_page = models.PositiveIntegerField(verbose_name=_('number of pages'), blank=True, null=True) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) - publisher = models.JSONField(default = list , verbose_name=_('Publisher')) subject_area = models.ManyToManyField(BookSubjectArea, related_name="book_subjects", verbose_name=_('subject area'), blank=True) type = models.ForeignKey(BookType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('type')) order = models.IntegerField(default=0, verbose_name=_('Order')) researcher = models.JSONField(default=list, blank=True, verbose_name=_('Researcher')) - city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication')) - country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication')) - edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number')) rate = models.DecimalField( max_digits=3, decimal_places=2, @@ -142,10 +133,6 @@ class BookReference(models.Model): def get_description(self, lang): return self._get_json_field("description" , lang) - - def get_publisher(self, lang): - return self._get_json_field("publisher" , lang) - def get_language(self, lang): return self._get_json_field("language" , lang) @@ -208,6 +195,52 @@ class BookReference(models.Model): self.slug = base_slug super().save(*args, **kwargs) +class BookEdition(models.Model): + """ + Model representing a specific edition or print of a book reference. + """ + book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='editions', verbose_name=_('book reference')) + publisher = models.JSONField(default=list, blank=True, verbose_name=_('Publisher')) + city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication')) + country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication')) + edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number')) + year_of_publication = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('year of publication')) + number_of_volumes = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('Total Number of Volumes')) + source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Online Source URL')) + isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True) + notes = models.JSONField(default=list, blank=True, verbose_name=_('Notes')) + tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags')) + + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + class Meta: + verbose_name = _('Book Edition') + verbose_name_plural = _('Book Editions') + ordering = ['-created_at'] + + def __str__(self): + return f"{self.book_reference} - {self.edition_number or 'Edition ' + str(self.id)}" + +class BookVolume(models.Model): + """ + Model representing a single volume, which can belong to an edition or directly to the book. + """ + book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('book reference')) + edition = models.ForeignKey(BookEdition, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('edition'), null=True, blank=True) + title = models.CharField(max_length=255, verbose_name=_('Title'), help_text=_('e.g., Volume 1'), blank=True, null=True) + + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + class Meta: + verbose_name = _('Book Volume') + verbose_name_plural = _('Book Volumes') + ordering = ['title'] + + def __str__(self): + return f"{self.book_reference} - {self.title or 'Vol ' + str(self.id)}" + class BookReferenceImage(models.Model): """ Model for book reference images - multiple images per book reference @@ -218,8 +251,15 @@ class BookReferenceImage(models.Model): related_name='images', verbose_name=_('book reference') ) + book_volume = models.ForeignKey( + BookVolume, + on_delete=models.CASCADE, + related_name='images', + verbose_name=_('book volume'), + null=True, + blank=True + ) image = models.ImageField(upload_to='hadis/book_reference_images/', verbose_name=_('image')) - volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume')) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) description = models.JSONField(default = list , verbose_name=_('Description')) @@ -260,8 +300,15 @@ class BookReferenceDocument(models.Model): related_name='documents', verbose_name=_('book reference') ) + book_volume = models.ForeignKey( + BookVolume, + on_delete=models.CASCADE, + related_name='documents', + verbose_name=_('book volume'), + null=True, + blank=True + ) file = models.FileField(upload_to='hadis/book_reference_documents/', verbose_name=_('Document File')) - volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume')) title = models.CharField(max_length=255, blank=True, null=True, help_text=_('e.g., Volume 1, 01.pdf')) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) created_at = models.DateTimeField(auto_now_add=True) @@ -280,6 +327,7 @@ class BookAuthor(models.Model): Model for book reference authors """ name = models.JSONField(default = list , verbose_name=_('Name')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, unique=True) birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) death_year_hijri = models.IntegerField(verbose_name=_("Death Year (Hijri)"), null=True, blank=True) birth_year_miladi = models.IntegerField(verbose_name=_("Birth Year (Miladi)"), null=True, blank=True) @@ -319,6 +367,35 @@ class BookAuthor(models.Model): return tr.get('text', '') return None + def save(self, *args, **kwargs): + if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''): + try: + if self.name and isinstance(self.name, list) and len(self.name) > 0: + first_item = self.name[0] + if isinstance(first_item, dict): + name_text = first_item.get('text', '').strip() + if name_text: + base_slug = slugify(name_text, allow_unicode=True) + slug = base_slug + counter = 1 + while BookAuthor.objects.filter(slug=slug).exclude(pk=self.pk).exists(): + slug = f"{base_slug}-{counter}" + counter += 1 + self.slug = slug + else: + import time + self.slug = f"author-{int(time.time() * 1000) % 1000000}" + else: + import time + self.slug = f"author-{int(time.time() * 1000) % 1000000}" + else: + import time + self.slug = f"author-{int(time.time() * 1000) % 1000000}" + except Exception: + import time + self.slug = f"author-{int(time.time() * 1000) % 1000000}" + super().save(*args, **kwargs) + class BookAttribute(models.Model): """ diff --git a/apps/hadis/serializers/reference.py b/apps/hadis/serializers/reference.py index 4e37389..dc37d45 100644 --- a/apps/hadis/serializers/reference.py +++ b/apps/hadis/serializers/reference.py @@ -29,9 +29,15 @@ class BookReferenceSerializer(serializers.ModelSerializer): description = LocalizedField() author = serializers.SerializerMethodField() share_link = serializers.CharField(read_only=True) + volume = serializers.SerializerMethodField() + class Meta: model = BookReference fields = ['id','title','slug','rate','author','description','image','volume','share_link'] + + def get_volume(self, obj): + vol = obj.volumes.first() + return vol.title if vol else "" def get_author(self, obj): request = self.context.get("request") # Prefer request.LANGUAGE_CODE if you use LocaleMiddleware @@ -106,15 +112,32 @@ class BookDetailSerializer(serializers.ModelSerializer): hadis = serializers.SerializerMethodField() title = LocalizedField() language = LocalizedField() - publisher = LocalizedField() description = LocalizedField() - share_link = serializers.CharField(read_only=True) + + publisher = serializers.SerializerMethodField() + isbn = serializers.SerializerMethodField() + volume = serializers.SerializerMethodField() class Meta: model = BookReference fields = ['id','title','rate','isbn','share_link','language','number_page','publisher','description','volume','slug','attribute','author','image','hadis'] + def get_publisher(self, obj): + edition = obj.editions.first() + if edition and getattr(edition, 'publisher', None): + request = self.context.get("request") + return get_localized_text(edition.publisher, request) + return "" + + def get_isbn(self, obj): + edition = obj.editions.first() + return edition.isbn if edition else "" + + def get_volume(self, obj): + vol = obj.volumes.first() + return vol.title if vol else "" + def get_hadis(self,obj): from apps.hadis.models import Hadis # 1. Fetch related hadis QuerySet for better pagination efficiency @@ -181,16 +204,23 @@ class BookReferenceSyncSerializer(serializers.ModelSerializer): def get_detail(self, obj): """Get basic book information""" request = self.context.get('request') + edition = obj.editions.first() + vol = obj.volumes.first() + + publisher_data = edition.publisher if edition else [] + isbn_data = edition.isbn if edition else "" + year_data = edition.year_of_publication if edition else "" + volume_data = vol.title if vol else "" return { 'description': get_localized_text(obj.description, request), - 'publisher': get_localized_text(obj.publisher, request), - 'volume': obj.volume, + 'publisher': get_localized_text(publisher_data, request), + 'volume': volume_data, 'language': get_localized_text(obj.language, request), - 'isbn': obj.isbn, + 'isbn': isbn_data, 'number_page': obj.number_page, - 'year_of_publication': obj.year_of_publication, - 'volume_info': obj.volume, + 'year_of_publication': year_data, + 'volume_info': volume_data, 'rating': obj.rate } diff --git a/apps/hadis/serializers/reference_v2.py b/apps/hadis/serializers/reference_v2.py new file mode 100644 index 0000000..8ffe9da --- /dev/null +++ b/apps/hadis/serializers/reference_v2.py @@ -0,0 +1,171 @@ +from rest_framework import serializers +from rest_framework.pagination import PageNumberPagination +from ..models import BookReference, BookAuthor, BookEdition, BookVolume +from .category import get_localized_text +from apps.hadis.serializers import HadisListSerializer, HadisShortSerializer +from ..serializers.category import LocalizedField +from ..serializers.reference import BookAttributeSerializer, BookReferenceImageSerializer +from rest_framework.pagination import PageNumberPagination +from ..models import BookReference, BookAuthor, BookEdition, BookVolume +from .category import get_localized_text +from apps.hadis.serializers import HadisListSerializer +from ..serializers.category import LocalizedField + +class BookAuthorV2Serializer(serializers.ModelSerializer): + name = LocalizedField() + class Meta: + model = BookAuthor + fields = ['id', 'name', 'slug'] + +class BookEditionV2Serializer(serializers.ModelSerializer): + publisher = LocalizedField() + notes = LocalizedField() + tags = LocalizedField() + publication_place = serializers.SerializerMethodField() + + class Meta: + model = BookEdition + fields = [ + 'id', 'publisher', 'publication_place', 'edition_number', + 'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tags' + ] + + def get_publication_place(self, obj): + request = self.context.get("request") + city = get_localized_text(obj.city_of_publication, request) if obj.city_of_publication else "" + country = get_localized_text(obj.country_of_publication, request) if obj.country_of_publication else "" + return { + "city": city, + "country": country + } + +class BookVolumeV2Serializer(serializers.ModelSerializer): + class Meta: + model = BookVolume + fields = ['id', 'title'] + +class BookReferenceV2DetailSerializer(serializers.ModelSerializer): + title = LocalizedField() + authors = BookAuthorV2Serializer(many=True, read_only=True) + + information = serializers.SerializerMethodField() + excerpts = serializers.SerializerMethodField() + volumes = serializers.SerializerMethodField() + + class Meta: + model = BookReference + fields = ['id', 'title', 'authors', 'information', 'excerpts', 'volumes'] + + def get_information(self, obj): + request = self.context.get("request") + summary = get_localized_text(obj.description, request) if obj.description else "" + editions = BookEditionV2Serializer(obj.editions.all(), many=True, context=self.context).data + + return { + "summary": summary, + "editions": editions + } + + def get_excerpts(self, obj): + from apps.hadis.models import Hadis + hadis_ids = obj.hadis_references.values_list('hadis', flat=True) + hadis_qs = Hadis.objects.filter(id__in=hadis_ids, status=True).order_by('id') + + paginator = PageNumberPagination() + paginator.page_size = 20 + request = self.context.get("request") + + paginated_hadis = paginator.paginate_queryset(hadis_qs, request) + + if paginated_hadis is not None: + serializer = HadisListSerializer(paginated_hadis, many=True, context=self.context) + return { + 'count': paginator.page.paginator.count, + 'next': paginator.get_next_link(), + 'previous': paginator.get_previous_link(), + 'results': serializer.data + } + + return { + 'count': hadis_qs.count(), + 'next': None, + 'previous': None, + 'results': HadisListSerializer(hadis_qs, many=True, context=self.context).data + } + + def get_volumes(self, obj): + editions = obj.editions.all() + if editions.exists(): + editions_data = [] + for ed in editions: + editions_data.append({ + "edition_id": ed.id, + "edition_statement": ed.edition_number or f"Edition {ed.id}", + "volumes": BookVolumeV2Serializer(ed.volumes.all(), many=True).data + }) + return { + "has_editions": True, + "editions_volumes": editions_data, + "all_volumes": [] + } + else: + return { + "has_editions": False, + "editions_volumes": [], + "all_volumes": BookVolumeV2Serializer(obj.volumes.all(), many=True).data + } + +class BookReferenceV2SyncSerializer(serializers.ModelSerializer): + title = LocalizedField() + authors = BookAuthorV2Serializer(many=True, read_only=True) + attributes = BookAttributeSerializer(many=True, read_only=True) + images = BookReferenceImageSerializer(many=True, read_only=True) + + information = serializers.SerializerMethodField() + hadises = serializers.SerializerMethodField() + volumes = serializers.SerializerMethodField() + + class Meta: + model = BookReference + fields = [ + 'id', 'title', 'slug', 'share_link', 'rate', 'authors', + 'attributes', 'images', 'information', 'hadises', 'volumes' + ] + + def get_information(self, obj): + request = self.context.get("request") + summary = get_localized_text(obj.description, request) if obj.description else "" + editions = BookEditionV2Serializer(obj.editions.all(), many=True, context=self.context).data + return { + "summary": summary, + "editions": editions, + "language": get_localized_text(obj.language, request) if obj.language else "", + "number_page": obj.number_page + } + + def get_hadises(self, obj): + references = obj.hadis_references.all() + hadis_list = [ref.hadis for ref in references if ref.hadis] + return HadisShortSerializer(hadis_list, many=True, context=self.context).data + + def get_volumes(self, obj): + editions = obj.editions.all() + if editions.exists(): + editions_data = [] + for ed in editions: + editions_data.append({ + "edition_id": ed.id, + "edition_statement": ed.edition_number or f"Edition {ed.id}", + "volumes": BookVolumeV2Serializer(ed.volumes.all(), many=True).data + }) + return { + "has_editions": True, + "editions_volumes": editions_data, + "all_volumes": [] + } + else: + return { + "has_editions": False, + "editions_volumes": [], + "all_volumes": BookVolumeV2Serializer(obj.volumes.all(), many=True).data + } diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 4465410..3645c6d 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -447,21 +447,33 @@ class AdminBookAttributeSerializer(serializers.ModelSerializer): class AdminBookReferenceImageSerializer(serializers.ModelSerializer): image = AbsoluteImageField(required=True) + volume = serializers.SerializerMethodField() class Meta: model = BookReferenceImage fields = ["id", "book_reference", "image", "volume", "order", "description"] + def get_volume(self, obj): + return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" + class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer): file = AbsoluteFileField(required=True) + volume = serializers.SerializerMethodField() class Meta: model = BookReferenceDocument fields = ["id", "book_reference", "file", "volume", "title", "order"] + def get_volume(self, obj): + return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" + class AdminBookReferenceListSerializer(serializers.ModelSerializer): + volume = serializers.SerializerMethodField() + publisher = serializers.SerializerMethodField() + year_of_publication = serializers.SerializerMethodField() + class Meta: model = BookReference fields = [ @@ -474,6 +486,18 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer): "rate", "created_at", ] + + def get_volume(self, obj): + vol = obj.volumes.first() + return vol.title if vol else "" + + def get_publisher(self, obj): + edition = obj.editions.first() + return edition.publisher if edition else [] + + def get_year_of_publication(self, obj): + edition = obj.editions.first() + return edition.year_of_publication if edition else "" class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): @@ -496,6 +520,52 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): many=True, required=False, ) + + source_url = serializers.SerializerMethodField() + isbn = serializers.SerializerMethodField() + volume = serializers.SerializerMethodField() + number_of_volumes = serializers.SerializerMethodField() + year_of_publication = serializers.SerializerMethodField() + publisher = serializers.SerializerMethodField() + city_of_publication = serializers.SerializerMethodField() + country_of_publication = serializers.SerializerMethodField() + edition_number = serializers.SerializerMethodField() + + def get_source_url(self, obj): + ed = obj.editions.first() + return ed.source_url if ed else "" + + def get_isbn(self, obj): + ed = obj.editions.first() + return ed.isbn if ed else "" + + def get_volume(self, obj): + vol = obj.volumes.first() + return vol.title if vol else "" + + def get_number_of_volumes(self, obj): + ed = obj.editions.first() + return ed.number_of_volumes if ed else None + + def get_year_of_publication(self, obj): + ed = obj.editions.first() + return ed.year_of_publication if ed else "" + + def get_publisher(self, obj): + ed = obj.editions.first() + return ed.publisher if ed else [] + + def get_city_of_publication(self, obj): + ed = obj.editions.first() + return ed.city_of_publication if ed else [] + + def get_country_of_publication(self, obj): + ed = obj.editions.first() + return ed.country_of_publication if ed else [] + + def get_edition_number(self, obj): + ed = obj.editions.first() + return ed.edition_number if ed else "" class Meta: model = BookReference diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index c4aec4a..2f953c9 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -4,6 +4,7 @@ from .views.category import HadisCategorySectListView, HadisCategoryTreeView, Ca from .views.hadis import HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView from .views.transmitter import TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView +from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView from .views.version import ContentReleaseSyncView from .views.info import HadisInfoView from .views_admin import ( @@ -61,6 +62,7 @@ urlpatterns = [ path('sync/hadis/', HadisSyncView.as_view(), name='hadis-sync'), path('sync/narrators/', TransmitterSyncView.as_view(), name='transmitter-sync'), path('sync/references/', BookReferenceSyncView.as_view(), name='reference-sync'), + path('v2/sync/references/', BookReferenceV2SyncView.as_view(), name='reference-sync-v2'), path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'), path('sync/filters/', HadisFiltersSyncAPIView.as_view(), name='hadis-filters-sync'), path('info/', HadisInfoView.as_view(), name='hadis-info'), @@ -86,6 +88,7 @@ urlpatterns = [ path('narrators/', TransmitterView.as_view(), name='narrators'), # Reference paths + path('v2/references//', BookReferenceV2DetailView.as_view(), name='reference-detail-v2'), path('references/', BookDetailView.as_view(), name='reference-detail'), path('references/', BookReferencesView.as_view(), name='references'), diff --git a/apps/hadis/views/reference_v2.py b/apps/hadis/views/reference_v2.py new file mode 100644 index 0000000..bcd4a8d --- /dev/null +++ b/apps/hadis/views/reference_v2.py @@ -0,0 +1,69 @@ +from rest_framework import generics +from ..models import BookReference +from ..serializers.reference_v2 import BookReferenceV2DetailSerializer +from drf_yasg.utils import swagger_auto_schema + +class BookReferenceV2DetailView(generics.RetrieveAPIView): + """ + V2 View to retrieve Book Reference details, formatted with tabs + (Information, Excerpts, Volumes) and supporting multiple editions per book. + """ + queryset = BookReference.objects.all() + serializer_class = BookReferenceV2DetailSerializer + lookup_field = 'slug' + + def get_queryset(self): + return ( + BookReference.objects + .filter(slug=self.kwargs.get('slug')) + .prefetch_related( + 'authors', + 'editions__volumes', + 'volumes', + 'hadis_references__hadis' + ) + ) + + @swagger_auto_schema( + operation_summary="Get V2 Book Reference Detail", + operation_description="Retrieve structured metadata for a book reference, including its editions, related hadiths (excerpts), and volumes.", + tags=['Dobodbi - Hadis (V2)'], + responses={200: BookReferenceV2DetailSerializer} + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + +from utils.pagination import NoPagination +from ..serializers.reference_v2 import BookReferenceV2SyncSerializer + +class BookReferenceV2SyncView(generics.ListAPIView): + """ + API view to sync all V2 book reference data for offline mode + Returns all book references with editions, volumes, and related hadises + """ + serializer_class = BookReferenceV2SyncSerializer + pagination_class = NoPagination + + @swagger_auto_schema( + operation_summary="Sync V2 Book References", + operation_description="Returns all book references for offline sync.", + tags=['Dobodbi - Hadis (V2)'], + responses={200: BookReferenceV2SyncSerializer(many=True)} + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return ( + BookReference.objects + .select_related() + .prefetch_related( + 'authors', + 'attributes', + 'images', + 'editions__volumes', + 'volumes', + 'hadis_references__hadis' + ) + .order_by('id') + )