diff --git a/apps/hadis/admin/reference.py b/apps/hadis/admin/reference.py index fadc960..922de5d 100644 --- a/apps/hadis/admin/reference.py +++ b/apps/hadis/admin/reference.py @@ -487,11 +487,11 @@ class BookReferenceAdmin(ModelAdmin): inlines = [BookReferenceImageInline, BookAttributeInline] # Use filter_horizontal for ManyToMany fields to make selection easier - filter_horizontal = ('subject_area', 'tags') + filter_horizontal = ('subject_area',) fieldsets = ( (_('Basic Info'), { - 'fields': ('title', 'description', 'slug', 'language', 'tags', 'type', 'author') + 'fields': ('title', 'description', 'slug', 'language', 'tag', 'type', 'author') }), (_('Publication Info'), { 'fields': ('number_page',) diff --git a/apps/hadis/migrations/0037_remove_bookedition_tags_remove_bookreference_tags_and_more.py b/apps/hadis/migrations/0037_remove_bookedition_tags_remove_bookreference_tags_and_more.py new file mode 100644 index 0000000..db3705b --- /dev/null +++ b/apps/hadis/migrations/0037_remove_bookedition_tags_remove_bookreference_tags_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.30 on 2026-08-29 17:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0036_alter_narratorlayer_options_narratorlayer_hadis_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='bookedition', + name='tags', + ), + migrations.RemoveField( + model_name='bookreference', + name='tags', + ), + migrations.AddField( + model_name='bookedition', + name='tag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='book_editions', to='hadis.booktag', verbose_name='Tag'), + ), + migrations.AddField( + model_name='bookreference', + name='tag', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='book_references', to='hadis.booktag', verbose_name='Tag'), + ), + migrations.AddField( + model_name='bookvolume', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='hadis/volume_images/', verbose_name='Image'), + ), + ] diff --git a/apps/hadis/models/reference.py b/apps/hadis/models/reference.py index 93fdd77..c42cbb0 100644 --- a/apps/hadis/models/reference.py +++ b/apps/hadis/models/reference.py @@ -101,11 +101,13 @@ class BookReference(LowercaseSlugMixin, models.Model): verbose_name=_('Subject Area'), blank=True ) - tags = models.ManyToManyField( + tag = models.ForeignKey( 'BookTag', - verbose_name=_('Tags'), + on_delete=models.SET_NULL, + null=True, blank=True, - related_name='book_references' + related_name='book_references', + verbose_name=_('Tag') ) type = models.ForeignKey('BookType', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Type')) author = models.ForeignKey( @@ -215,7 +217,14 @@ class BookEdition(models.Model): 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')) + tag = models.ForeignKey( + 'BookTag', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='book_editions', + verbose_name=_('Tag') + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) @@ -235,6 +244,12 @@ class BookVolume(models.Model): 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) + image = models.ImageField( + upload_to='hadis/volume_images/', + null=True, + blank=True, + verbose_name=_('Image') + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) diff --git a/apps/hadis/serializers/reference_v2.py b/apps/hadis/serializers/reference_v2.py index 826a4dd..a34b734 100644 --- a/apps/hadis/serializers/reference_v2.py +++ b/apps/hadis/serializers/reference_v2.py @@ -36,13 +36,13 @@ class BookEditorV2Serializer(serializers.ModelSerializer): class BookReferenceV2ListSerializer(serializers.ModelSerializer): title = LocalizedField() authors = serializers.SerializerMethodField() - tags = BookTagV2Serializer(many=True, read_only=True) + tag = BookTagV2Serializer(read_only=True) number_of_volumes = serializers.SerializerMethodField() image = serializers.SerializerMethodField() class Meta: model = BookReference - fields = ['id', 'slug', 'title', 'authors', 'number_of_volumes', 'tags', 'image'] + fields = ['id', 'slug', 'title', 'authors', 'number_of_volumes', 'tag', 'image'] def get_authors(self, obj): if obj.author: @@ -53,8 +53,18 @@ class BookReferenceV2ListSerializer(serializers.ModelSerializer): return obj.volumes.count() def get_image(self, obj): + if hasattr(obj, 'thumbnail') and obj.thumbnail: + request = self.context.get('request') + if request: + return absolute_https_url(obj.thumbnail.url, request) + return absolute_https_url(obj.thumbnail.url) first_volume = obj.volumes.first() if first_volume: + if first_volume.image: + request = self.context.get('request') + if request: + return absolute_https_url(first_volume.image.url, request) + return absolute_https_url(first_volume.image.url) first_image = first_volume.images.order_by('order').first() if first_image and first_image.image: request = self.context.get('request') @@ -66,7 +76,7 @@ class BookReferenceV2ListSerializer(serializers.ModelSerializer): class BookEditionV2Serializer(serializers.ModelSerializer): publisher = LocalizedField() notes = LocalizedField() - tags = LocalizedField() + tag = BookTagV2Serializer(read_only=True) publication_place = serializers.SerializerMethodField() researchers = BookResearcherV2Serializer(many=True, read_only=True) editors = BookEditorV2Serializer(many=True, read_only=True) @@ -75,7 +85,7 @@ class BookEditionV2Serializer(serializers.ModelSerializer): model = BookEdition fields = [ 'id', 'publisher', 'publication_place', 'edition_number', - 'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tags', + 'year_of_publication', 'number_of_volumes', 'notes', 'source_url', 'tag', 'researchers', 'editors' ] @@ -96,9 +106,13 @@ class BookVolumeV2Serializer(serializers.ModelSerializer): fields = ['id', 'title', 'image'] def get_image(self, obj): + request = self.context.get('request') + if obj.image: + if request: + return absolute_https_url(obj.image.url, request) + return absolute_https_url(obj.image.url) first_image = obj.images.order_by('order').first() if first_image and first_image.image: - request = self.context.get('request') if request: return absolute_https_url(first_image.image.url, request) return absolute_https_url(first_image.image.url) diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index a65c325..70c243e 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -7,7 +7,7 @@ from utils.image_compression import maybe_compress_uploaded_file from utils import absolute_https_url from ..models import ( Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, - BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume, BookResearcher, BookEditor, + BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText, CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, OriginalTextReference, OriginalTextReferenceImage @@ -1179,9 +1179,19 @@ class AdminBookReferenceDocumentSerializer(serializers.ModelSerializer): return obj.book_volume.title if getattr(obj, 'book_volume', None) else "" +class AdminBookTagSerializer(serializers.ModelSerializer): + title = serializers.JSONField(default=list) + + class Meta: + model = BookTag + fields = ["id", "title", "created_at", "updated_at"] + + class AdminBookReferenceListSerializer(serializers.ModelSerializer): author_id = serializers.IntegerField(source="author.id", read_only=True, allow_null=True) author_name = serializers.JSONField(source="author.name", read_only=True, allow_null=True) + tag_id = serializers.IntegerField(source="tag.id", read_only=True, allow_null=True) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) volume = serializers.SerializerMethodField() publisher = serializers.SerializerMethodField() year_of_publication = serializers.SerializerMethodField() @@ -1195,6 +1205,8 @@ class AdminBookReferenceListSerializer(serializers.ModelSerializer): "has_editions", "author_id", "author_name", + "tag_id", + "tag_detail", "volume", "publisher", "year_of_publication", @@ -1219,6 +1231,7 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): subject_area_detail = AdminBookSubjectAreaSerializer(source="subject_area", many=True, read_only=True) type_detail = AdminBookTypeSerializer(source="type", read_only=True) author_detail = AdminBookAuthorSerializer(source="author", read_only=True) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) subject_area = serializers.PrimaryKeyRelatedField( queryset=BookSubjectArea.objects.all(), @@ -1235,6 +1248,11 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): required=False, allow_null=True, ) + tag = serializers.PrimaryKeyRelatedField( + queryset=BookTag.objects.all(), + required=False, + allow_null=True, + ) source_url = serializers.SerializerMethodField() isbn = serializers.SerializerMethodField() @@ -1303,6 +1321,8 @@ class AdminBookReferenceDetailSerializer(serializers.ModelSerializer): "subject_area_detail", "type", "type_detail", + "tag", + "tag_detail", "order", "city_of_publication", "country_of_publication", @@ -1548,6 +1568,8 @@ class AdminTransmitterOriginalTextSerializer(serializers.ModelSerializer): class AdminBookVolumeSerializer(serializers.ModelSerializer): + remove_image = serializers.BooleanField(write_only=True, required=False, default=False) + class Meta: model = BookVolume fields = [ @@ -1555,10 +1577,23 @@ class AdminBookVolumeSerializer(serializers.ModelSerializer): "book_reference", "edition", "title", + "image", + "remove_image", "created_at", "updated_at", ] + def create(self, validated_data): + validated_data.pop("remove_image", False) + return super().create(validated_data) + + def update(self, instance, validated_data): + remove_image = validated_data.pop("remove_image", False) + if remove_image and instance.image: + instance.image.delete(save=False) + instance.image = None + return super().update(instance, validated_data) + class AdminBookResearcherSerializer(serializers.ModelSerializer): id = serializers.IntegerField(required=False) @@ -1583,7 +1618,12 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): city_of_publication = serializers.JSONField(required=False, default=list) country_of_publication = serializers.JSONField(required=False, default=list) notes = serializers.JSONField(required=False, default=list) - tags = serializers.JSONField(required=False, default=list) + tag = serializers.PrimaryKeyRelatedField( + queryset=BookTag.objects.all(), + required=False, + allow_null=True, + ) + tag_detail = AdminBookTagSerializer(source="tag", read_only=True) volumes_detail = AdminBookVolumeSerializer(source="volumes", many=True, read_only=True) number_of_volumes = serializers.SerializerMethodField() researchers = AdminBookResearcherSerializer(many=True, required=False) @@ -1605,7 +1645,8 @@ class AdminBookEditionSerializer(serializers.ModelSerializer): "source_url", "isbn", "notes", - "tags", + "tag", + "tag_detail", "volumes_detail", "researchers", "editors", diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index b58e049..2291ff4 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -3,19 +3,19 @@ from rest_framework.routers import SimpleRouter from .views.category import HadisCategorySectListView, HadisCategoryTreeView, CategoriesView, CategoriesBySectView, HadisCategorySelectBySectView, HadisCategorySelectBySectSourceView , HadisCategoryTreeNormalView ,test_deploy,debug_headers,HadisCategoryXMindView from .views.hadis import ( HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView, - HadisCorrectionDetailView, HadisInterpretationDetailView + HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView ) from .views.transmitter import ( TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, NarratorArgumentsListView , NarratorTeachersView, NarratorStudentsView, TransmitterOriginalTextDetailView ) -from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView +from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView, ReferenceExcerptsListView from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView from .views.version import ContentReleaseSyncView from .views.info import HadisInfoView from .views_admin import ( AdminHadisCategoryViewSet, AdminHadisViewSet, AdminHadisCorrectionViewSet, AdminHadisInterpretationViewSet, AdminHadisStatusViewSet, AdminHadisTagViewSet, AdminHadisCollectionViewSet, - AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet, + AdminBookSubjectAreaViewSet, AdminBookTypeViewSet, AdminBookTagViewSet, AdminBookAuthorViewSet, AdminBookAttributeViewSet, AdminBookReferenceImageViewSet, AdminBookReferenceDocumentViewSet, AdminBookReferenceViewSet, AdminTransmitterViewSet, AdminTransmitterReliabilityViewSet, DovodiBookSubjectAreaViewSet, DovodiBookTypeViewSet, DovodiNarratorLayerViewSet, DovodiTransmitterReliabilityViewSet, DovodiOpinionStatusViewSet, DovodiHadisSectViewSet, DovodiHadisCategoryViewSet, AdminCategoryQuranVerseViewSet, AdminHadisTransmitterViewSet, @@ -43,6 +43,7 @@ admin_router.register(r'collections', AdminHadisCollectionViewSet, basename='adm admin_router.register(r'references', AdminBookReferenceViewSet, basename='admin-book-references') admin_router.register(r'subject-areas', AdminBookSubjectAreaViewSet, basename='admin-book-subject-areas') admin_router.register(r'book-types', AdminBookTypeViewSet, basename='admin-book-types') +admin_router.register(r'book-tags', AdminBookTagViewSet, basename='admin-book-tags') admin_router.register(r'dovodi-subject-areas', DovodiBookSubjectAreaViewSet, basename='dovodi-book-subject-areas') admin_router.register(r'dovodi-book-types', DovodiBookTypeViewSet, basename='dovodi-book-types') admin_router.register(r'book-authors', AdminBookAuthorViewSet, basename='admin-book-authors') @@ -112,9 +113,12 @@ urlpatterns = [ path('narrators/', TransmitterView.as_view(), name='narrators'), # Reference paths + path('v2/references//excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts-v2'), path('v2/references//', BookReferenceV2DetailView.as_view(), name='reference-detail-v2'), path('v2/references/', BookReferenceV2ListView.as_view(), name='reference-list-v2'), - path('references/', BookDetailView.as_view(), name='reference-detail'), + path('references//excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts'), + path('references//', BookDetailView.as_view(), name='reference-detail'), + path('references/', BookDetailView.as_view(), name='reference-detail-no-slash'), path('references/', BookReferencesView.as_view(), name='references'), # Hadis detail paths (with slug, more specific) @@ -122,6 +126,7 @@ urlpatterns = [ path('/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'), path('/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'), path('/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'), + path('/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'), path('/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST # path('test/test-deploy',test_deploy , name='test'), diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index 40a8cf0..0ee6880 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -1,937 +1,977 @@ -from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.generics import ListAPIView, RetrieveAPIView -from django.shortcuts import get_object_or_404 -from utils.pagination import NoPagination, StandardResultsSetPagination -from rest_framework.pagination import PageNumberPagination -from rest_framework.response import Response -from django.db.models import Count -from django.db.models import Prefetch -from ..serializers.category import get_localized_text -from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage -from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer -from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger -from django.db.models import Q -from ..serializers.category import get_localized_text - -from rest_framework import status -from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin - -class PinnedHadisCollectionListView(ListAPIView): - """ - API view to list pinned hadis collections with metadata info - """ - serializer_class = PinnedHadisCollectionSerializer - pagination_class = NoPagination - permission_classes = [IsAuthenticated] - authentication_classes = [TokenAuthentication] - - @hadis_collections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - return HadisCollection.objects.filter( - status=True, - display_position=HadisCollection.DisplayPosition.PINNED, - hadis_items__hadis__status=True - ).distinct().order_by('order', '-created_at') - - def list(self, request, *args, **kwargs): - # 1. گرفتن خروجی استاندارد لیست مجموعه‌ها - response = super().list(request, *args, **kwargs) - - # 2. محاسبه آمار (Info) - # تعداد کتگوری‌های فعال - categories_count = HadisCategory.objects.count() - - # تعداد نریتورها (راویان) - narrators_count = Transmitters.objects.count() - - # تعداد سورس‌ها (کتاب‌های منبع) - # فرض بر این است که BookReference مدل منابع شماست - from apps.hadis.models import BookReference - sources_count = BookReference.objects.count() - - # تعداد بوکمارک‌های کاربر در سرویس حدیث - from apps.bookmark.models.bookmark import Bookmark - bookmarks_count = Bookmark.objects.filter( - user=request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).count() - - # تعداد نویسنده‌ها - from apps.hadis.models import BookAuthor - authors_count = BookAuthor.objects.count() - - # 3. ساختن دیکشنری info - info = { - "categories_count": categories_count, - # "bookmarks_count": bookmarks_count, - "narrators_count": narrators_count, - "sources_count": sources_count, - "authors_count": authors_count, - } - - # 4. بازسازی دیتای نهایی مشابه اپ ویدیو - custom_data = { - "count": len(response.data) if isinstance(response.data, list) else response.data.get("count"), - "next": response.data.get("next") if isinstance(response.data, dict) else None, - "previous": response.data.get("previous") if isinstance(response.data, dict) else None, - "info": info, - "results": response.data if isinstance(response.data, list) else response.data.get("results") - } - - return Response(custom_data, status=status.HTTP_200_OK) - - -class HadisCollectionListView(ListAPIView): - """ - API view to list all hadis collections - """ - serializer_class = HadisCollectionListSerializer - pagination_class = NoPagination - - @hadis_collections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - return HadisCollection.objects.filter( - status=True, - display_position=HadisCollection.DisplayPosition.MIDDLE - ).order_by('order', '-created_at') - - -class HadisSyncView(ListAPIView): - """ - API view to sync all hadis data for offline mode - """ - serializer_class = HadisSyncSerializer - pagination_class = NoPagination - - def get_queryset(self): - return ( - Hadis.objects - .filter(status=True) - .select_related('category', 'hadis_status') - .prefetch_related( - 'tags', - Prefetch( - 'transmitters', - queryset=HadisTransmitter.objects.select_related( - 'transmitter', - 'transmitter__reliability', - 'narrator_layer' - ).order_by('order') - ), - Prefetch( - 'references', - queryset=HadisReference.objects - .select_related('book_reference') - .prefetch_related( - 'book_reference__author', - Prefetch( - 'images', - queryset=ReferenceImage.objects.order_by('priority') - ) - ) - ), - # 👇 اضافه شدن واکشی عمیق برای تصحیحات - Prefetch( - 'hadiscorrection_set', - queryset=HadisCorrection.objects.prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - ), - # 👇 اضافه شدن واکشی عمیق برای تفاسیر - Prefetch( - 'category__interpretations', - queryset=HadisInterpretation.objects.prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - ), - ) - .order_by('id') - ) - - @hadis_sync_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - serializer = self.get_serializer( - queryset, - many=True, - context={'request': request}, - ) - return Response({ - 'count': len(serializer.data), - 'results': serializer.data - }) - -from ..serializers.category import CategoryQuranVerseSerializer -class HadisListView(ListAPIView): - """ - API view to list Hadis by category_id - """ - serializer_class = HadisListSerializer - pagination_class = StandardResultsSetPagination - authentication_classes = [TokenAuthentication] - - @hadis_list_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - # 1. Run the standard list logic (get pagination, filter, results) - response = super().list(request, *args, **kwargs) - - # 2. Find the "Parent" Category based on the URL slug - category_slug = self.kwargs.get('category_slug') - category_obj = get_object_or_404(HadisCategory, slug=category_slug) - - # 3. Serialize this single category for the Hero section - category_data = SimpleCategory(category_obj).data - - # 4. Check if it's a Quran Category and fetch verse data - quran_data = None - if category_obj.source_type == HadisCategory.SourceType.QURAN: - try: - from ..models.category import CategoryQuranVerse - verse_obj = CategoryQuranVerse.objects.prefetch_related( - 'secondary_translations' - ).get(category=category_obj) - - from ..serializers.category import CategoryQuranVerseSerializer - quran_data = CategoryQuranVerseSerializer( - verse_obj, - context={'request': request} - ).data - except CategoryQuranVerse.DoesNotExist: - pass - - # 👇 5. NEW LOGIC: Fetch all statuses for frontend filtering UI - lang = getattr(request, "LANGUAGE_CODE", None) or "fa" - statuses_list = [ - { - "id": status.id, - "slug": status.slug, - "title": get_localized_text(status.title, request=request, language_code=lang) or "", - "color": status.color, - "main_color_code": status.main_color_code, - } - for status in HadisStatus.objects.all().order_by('order') - ] - - # 6. Inject it into the response data - if isinstance(response.data, dict): - ordered_data = {} - for key in ['count', 'next', 'previous']: - if key in response.data: - ordered_data[key] = response.data[key] - - ordered_data['current_category'] = category_data - ordered_data['quran_data'] = quran_data - - # 👇 تزریق لیست استاتوس‌ها قبل از نتایج - ordered_data['statuses'] = statuses_list - - if 'results' in response.data: - ordered_data['results'] = response.data['results'] - response.data = ordered_data - - return response - - def get_queryset(self): - category_slug = self.kwargs.get('category_slug') - if not HadisCategory.objects.filter(slug=category_slug).exists(): - return Hadis.objects.none() - - queryset = Hadis.objects.filter( - category__slug=category_slug, - status=True - ).annotate( - layer_count=Count('transmitters__narrator_layer', distinct=True) - ).select_related('category') - - # 👇 1. Apply Search Filter - search_query = self.request.query_params.get('search', None) - if search_query: - search_conditions = Q(text__icontains=search_query) - search_conditions |= Q(title__icontains=search_query) - search_conditions |= Q(title_narrator__icontains=search_query) - search_conditions |= Q(translation__icontains=search_query) - queryset = queryset.filter(search_conditions) - - # 👇 2. Apply Status Filter (Supports multiple comma-separated slugs) - status_filter = self.request.query_params.get('status', None) - if status_filter: - status_slugs = [s.strip() for s in status_filter.split(',')] - queryset = queryset.filter(hadis_status__slug__in=status_slugs) - - # 👇 3. Apply Transmitter Filter (Supports multiple comma-separated slugs) - transmitter_filter = self.request.query_params.get('transmitter', None) - if transmitter_filter: - transmitter_slugs = [t.strip() for t in transmitter_filter.split(',')] - queryset = queryset.filter(transmitters__transmitter__slug__in=transmitter_slugs) - - # 👇 4. Apply Source Filter (Supports multiple comma-separated slugs) - source_filter = self.request.query_params.get('source', None) - if source_filter: - source_slugs = [s.strip() for s in source_filter.split(',')] - queryset = queryset.filter(references__book_reference__slug__in=source_slugs) - - # 👇 5. Apply Source Author Filter (Supports author or source_author, multiple comma-separated slugs/IDs) - author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None) - if author_filter: - author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()] - author_q = Q(references__book_reference__author__slug__in=author_slugs) - numeric_ids = [int(a) for a in author_slugs if a.isdigit()] - if numeric_ids: - author_q |= Q(references__book_reference__author_id__in=numeric_ids) - queryset = queryset.filter(author_q) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - # ❗️ distinct() برای جلوگیری از دابلیکیت شدن احادیث به خاطر ManyToMany ها الزامی است - return queryset.distinct() - - def get_serializer_context(self): - """Add user bookmarks to serializer context to avoid caching issues""" - context = super().get_serializer_context() - - # Add user's bookmarked hadis IDs to context - user = self.request.user - if user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - user_bookmarks = Bookmark.objects.filter( - user=user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - context['user_bookmarked_hadis_ids'] = set(user_bookmarks) - else: - context['user_bookmarked_hadis_ids'] = set() - - return context - - - - -class HadisMainListView(ListAPIView): - """ - API view to list Hadis by category_id - """ - serializer_class = HadisListSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - pagination_class = StandardResultsSetPagination - - @hadis_main_list_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - # queryset = Hadis.objects.select_related('category', 'hadis_status') - queryset = Hadis.objects.select_related('category__sect', 'hadis_status') - - # Get search parameters - search_query = self.request.query_params.get('search', None) - status_filter = self.request.query_params.get('status', None) - category_filter = self.request.query_params.get('category', None) - - # Apply search filter - if search_query: - queryset = self.apply_search_filter(queryset, search_query) - - # Apply status filter - if status_filter: - queryset = queryset.filter(hadis_status__title__icontains=status_filter) - - # Apply category filter - if category_filter: - queryset = queryset.filter(category__title__icontains=category_filter) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - return queryset - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - - # Apply pagination - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - paginated_response = self.get_paginated_response(serializer.data) - - # Get category titles - category_titles = self.get_category_titles(request) - - # Get status titles - status_titles = self.get_status_titles(request) - - # Modify the paginated response to include our custom data - response_data = paginated_response.data - # response_data['category_titles'] = self.get_cached_category_titles(request) - # response_data['status_titles'] = self.get_cached_status_titles(request) - - return Response(response_data) - - # Fallback for when pagination is disabled - serializer = self.get_serializer(queryset, many=True) - return Response({ - 'count': queryset.count(), - 'results': serializer.data - }) - - return Response(response_data) - - def get_category_titles(self,request): - """Get list of category titles based on language""" - from ..models import HadisCategory - - categories = HadisCategory.objects.all() - category_titles = [] - - for category in categories: - title = get_localized_text(category.title,request) - category_titles.append(title) - - return category_titles - - def get_status_titles(self, request): - """Get list of status titles based on language""" - from ..models import HadisStatus - - statuses = HadisStatus.objects.all().order_by('order') - status_titles = [] - - for status in statuses: - title = get_localized_text(status.title,request) - status_titles.append(title) - return status_titles - - def apply_search_filter(self, queryset, search_query): - """ - Apply search filter across multiple fields including JSONFields. - Searches in: title, title_narrator, text, translation - """ - from django.db.models import Q - - # Basic search conditions - search_conditions = Q(text__icontains=search_query) - - # For JSONFields, search in the JSON string representation - # This will find matches in the "text" values within the JSON arrays - search_conditions |= Q(title__icontains=search_query) - search_conditions |= Q(title_narrator__icontains=search_query) - search_conditions |= Q(translation__icontains=search_query) - - return queryset.filter(search_conditions) - - #we add this later - - # def get_cached_category_titles(self, request): - # """Fetches categories, cached for 1 hour to reduce DB load""" - # lang = getattr(request, "LANGUAGE_CODE", "en") - # cache_key = f"hadis_meta_categories_{lang}" - - # data = cache.get(cache_key) - # if not data: - # # If not in cache, fetch from DB - # from ..models import HadisCategory - # categories = HadisCategory.objects.all().only('id', 'title') # Fetch only needed fields - - # # Build list - # data = [get_localized_text(c.title, request) for c in categories] - - # # Save to cache - # cache.set(cache_key, data, timeout=60 * 60) # 1 Hour - - # return data - - # def get_cached_status_titles(self, request): - # """Fetches statuses, cached for 1 hour""" - # lang = getattr(request, "LANGUAGE_CODE", "en") - # cache_key = f"hadis_meta_statuses_{lang}" - - # data = cache.get(cache_key) - # if not data: - # from ..models import HadisStatus - # statuses = HadisStatus.objects.all().order_by('order') - # data = [get_localized_text(s.title, request) for s in statuses] - # cache.set(cache_key, data, timeout=60 * 60) - - # return data - - -class HadisBasicView(RetrieveAPIView): - """ - API view to retrieve basic Hadis information by hadis_slug - """ - serializer_class = HadisBasicSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - authentication_classes = [TokenAuthentication] - - @hadis_basic_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - return Hadis.objects.filter(status=True) - - def get_serializer_context(self): - """Add user bookmarks to serializer context to avoid caching issues""" - context = super().get_serializer_context() - - # Add user's bookmarked hadis IDs to context - user = self.request.user - if user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - user_bookmarks = Bookmark.objects.filter( - user=user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - context['user_bookmarked_hadis_ids'] = set(user_bookmarks) - else: - context['user_bookmarked_hadis_ids'] = set() - - return context - - -class HadisDetailView(Gone410ViewMixin, CanonicalSlugViewSetMixin, RetrieveAPIView): - """ - API view to retrieve detailed Hadis information by hadis_slug (excluding transmitters and corrections) - """ - serializer_class = HadisDetailSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - active_field_name = 'status' - active_expected_value = True - - @hadis_detail_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - return Hadis.objects.all().select_related( - 'category', 'hadis_status' - ).prefetch_related( - 'tags', - 'references__book_reference', - 'references__book_reference__author', - 'references__images', - ) - -class HadisTransmittersView(RetrieveAPIView): - """ - Fetches a single Hadis but filters the nested Transmitters list - if a ?layer=slug param is provided. - """ - serializer_class = HadisTransmitterListSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - pagination_class = NoPagination - @hadis_transmitters_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - # 1. Get the filter param - layer_slug = self.request.query_params.get('layer') - - # 2. Build the query for the "Child" (Transmitters) - # We start with the base optimization (select_related) - transmitter_qs = HadisTransmitter.objects.select_related( - 'transmitter', - 'narrator_layer' - ).order_by('order') - - # 3. Apply the filter to the Child Query (if param exists) - if layer_slug: - # Assumes 'NarratorLayer' has a 'slug' field. - # If not, use 'narrator_layer__name' or 'narrator_layer__id'. - transmitter_qs = transmitter_qs.filter(narrator_layer__slug=layer_slug) - - # 4. Use the Prefetch object to inject this filtered list into the Parent - return Hadis.objects.filter(status=True).prefetch_related( - Prefetch('transmitters', queryset=transmitter_qs) - ) - -class HadisCorrectionsView(ListAPIView): - """ - API view to retrieve corrections for a specific hadis - """ - serializer_class = HadisCorrectionSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - pagination_class = StandardResultsSetPagination - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - @hadis_corrections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - # hadis = self.get_object() - # corrections_data = [] - - # for correction in hadis.hadiscorrection_set.all(): - # correction_info = { - # 'id': correction.id, - # 'title': correction.title, - # 'description': correction.description, - # 'translation': correction.translation - # } - # corrections_data.append(correction_info) - - # return Response({ - # 'hadis_id': hadis.id, - # 'corrections_count': len(corrections_data), - # 'corrections': corrections_data - # }) - - def get_queryset(self): - hadis_slug = self.kwargs.get('hadis_slug') - try: - hadis = Hadis.objects.get(slug=hadis_slug, status=True) - if not HadisCorrection.objects.filter(hadis=hadis).exists(): - return Hadis.objects.none() - queryset = HadisCorrection.objects.filter(hadis=hadis) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH_CORRECTION, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - return queryset - except Hadis.DoesNotExist: - return HadisCorrection.objects.none() - - - -class HadisLayersView(ListAPIView): - """ - API view to retrieve all narrator layers for a specific hadis - """ - serializer_class = NarratorLayerSerializer - pagination_class = NoPagination - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - @hadis_layers_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - from ..models import NarratorLayer - - hadis_slug = self.kwargs.get('hadis_slug') - - # Get the hadis object to ensure it exists - hadis = get_object_or_404(Hadis, slug=hadis_slug, status=True) - - # Return the layer objects for this hadis ordered by number - return NarratorLayer.objects.filter(hadis=hadis).order_by('number') - - -class HadisFiltersView(ListAPIView): - """ - API view to return filter data for hadis - Returns statuses and categories for filtering - """ - pagination_class = NoPagination - - @arguments_filters_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - # This view doesn't need a queryset, it returns computed data - return Hadis.objects.none() - - def list(self, request, *args, **kwargs): - # Get statuses from HadisStatus model - statuses = [] - for status in HadisStatus.objects.all().order_by('order'): - title_text = get_localized_text(status.title, request) - if title_text and status.slug: - statuses.append({ - 'text': title_text, - 'slug': status.slug - }) - - # Get categories from HadisCategory model - categories = [] - for category in HadisCategory.objects.all().order_by('order'): - title_text = get_localized_text(category.title, request) - if title_text and category.slug: - categories.append({ - 'text': title_text, - 'slug': category.slug - }) - - response_data = { - 'statuses': statuses, - 'categories': categories - } - - return Response(response_data) - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import AllowAny -from rest_framework.authentication import TokenAuthentication -from drf_yasg.utils import swagger_auto_schema -from django.utils.translation import get_language - -from apps.hadis.models import HadisStatus, HadisCategory, BookReference, Transmitters -from drf_yasg import openapi -class HadisFiltersSyncAPIView(APIView): - """ - API view to return all filter data for hadis (Sync API for Mobile App) - Returns statuses, categories, sources (books), and narrators. - Optimized for offline client-side searching and sorting. - """ - permission_classes = [AllowAny] - authentication_classes = [TokenAuthentication] - - @swagger_auto_schema( - operation_description=( - "Sync API for Flutter/mobile clients: returns flat hadis filter data for offline " - "searching and local filtering. Titles are localized from multilingual fields based " - "on the active request language." - ), - operation_summary="Sync Hadis Filters", - tags=['Dobodbi - Hadis'], - manual_parameters=[ - openapi.Parameter( - 'Accept-Language', - openapi.IN_HEADER, - description="Language code for localized titles, for example `en`, `fa`, `ar`, `ur`, or `ru`.", - type=openapi.TYPE_STRING, - required=False, - ) - ], - responses={ - status.HTTP_200_OK: openapi.Response( - description="Flat filter payload for hadis categories, statuses, sources, and narrators.", - examples={ - "application/json": { - "statuses": [ - { - "id": 133, - "slug": "sahih", - "title": "Authentic", - "order": 1, - "color": "green", - "main_color_code": "#1DAC43" - } - ], - "categories": [ - { - "id": 1036, - "slug": "general-hadiths", - "title": "General Hadiths", - "order": 1 - } - ], - "sources": [ - { - "id": 2538, - "slug": "man-la-yahduruhu-al-faqih", - "title": "Man La Yahduruhu al-Faqih", - "search_keywords": "Shaykh al-Saduq" - } - ], - "narrators": [ - { - "id": 501, - "slug": "muhammad-ibn-muslim", - "title": "Muhammad ibn Muslim", - "search_aliases": "Abu Ja'far" - } - ] - } - } - ) - } - ) - def get(self, request, *args, **kwargs): - # تشخیص زبان برای ارسال دیتای لوکالایز شده - lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "ru" - - # ۱. استتوس‌ها (مرتب‌شده بر اساس فیلد order) - statuses = [ - { - "id": status.id, - "slug": status.slug, - "title": get_localized_text(status.title, request=request, language_code=lang) or "", - "order": status.order, - "color": status.color, - "main_color_code": status.main_color_code, - } - for status in HadisStatus.objects.all().order_by('order') - ] - - # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order) - categories = [ - { - "id": cat.id, - "slug": cat.slug, - "title": get_localized_text(cat.title, request=request, language_code=lang) or "", - "order": cat.order, - } - for cat in HadisCategory.objects.all().order_by('order') - ] - - # ۳. منابع / کتاب‌ها - sources = [] - # استفاده از select_related برای جلوگیری از مشکل N+1 Query هنگام گرفتن نویسنده‌ها - books = BookReference.objects.select_related('author').all().order_by('order', '-id') - - for book in books: - # استخراج نام نویسنده‌ها برای سرچ آفلاین فلاتر - authors_names = [ - get_localized_text(book.author.name, request=request, language_code=lang) or "" - ] if book.author else [] - - sources.append({ - "id": book.id, - "slug": book.slug, - "title": get_localized_text(book.title, request=request, language_code=lang) or "", - # تمام اسم‌های نویسنده‌ها را به هم می‌چسبانیم تا فلاتر بتواند روی آن‌ها سرچ بزند - # "search_keywords": " ".join(authors_names).strip() - }) - - # ۴. راویان (نریتورها) - narrators = [] - transmitters = Transmitters.objects.all().order_by('id') - - for narrator in transmitters: - # گرفتن نام‌های مستعار - known_as = get_localized_text(narrator.known_as, request=request, language_code=lang) or "" - nickname = get_localized_text(narrator.nickname, request=request, language_code=lang) or "" - kunya = get_localized_text(narrator.kunya, request=request, language_code=lang) or "" - - # تجمیع نام‌های جایگزین - search_aliases = f"{known_as} {nickname} {kunya}".strip() - - narrators.append({ - "id": narrator.id, - "slug": narrator.slug, - "title": get_localized_text(narrator.full_name, request=request, language_code=lang) or "", - # فلاتر روی این فیلد + title سرچ می‌زند - # "search_aliases": search_aliases - }) - - # خروجی نهایی بدون پجینیشن - return Response({ - "statuses": statuses, - "categories": categories, - "sources": sources, - "narrators": narrators - }) - -from ..serializers.hadis import HadisCorrectionDetailSerializer, HadisInterpretationDetailSerializer -from ..models import HadisInterpretation - -class HadisCorrectionDetailView(RetrieveAPIView): - """API View to get full details of a specific HadisCorrection by slug""" - serializer_class = HadisCorrectionDetailSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'correction_slug' - permission_classes = [AllowAny] - - @swagger_auto_schema( - operation_summary="Get Hadis Correction Detail", - operation_description="Returns the full details of a specific hadis correction, including text, translations, and its book references.", - tags=['Dobodbi - Hadis (V2)'], - ) - def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_queryset(self): - return HadisCorrection.objects.all().select_related( - 'hadis' - ).prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - -class HadisInterpretationDetailView(RetrieveAPIView): - """API View to get full details of a specific HadisInterpretation by slug""" - serializer_class = HadisInterpretationDetailSerializer - lookup_field = 'slug' # 👈 تغییر به slug - lookup_url_kwarg = 'interpretation_slug' - permission_classes = [AllowAny] - - @swagger_auto_schema( - operation_summary="Get Hadis Interpretation Detail", - operation_description="Returns the full details of a specific hadis interpretation, including text, translations, and its book references.", - tags=['Dobodbi - Hadis (V2)'], - ) - def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_queryset(self): - return HadisInterpretation.objects.all().select_related( - 'category' - ).prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - - # 👈 هندل کردن حالت امن برای زمانی که چند تفسیر در یک کتگوری باشند - def get_object(self): - queryset = self.filter_queryset(self.get_queryset()) - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} - - obj = queryset.filter(**filter_kwargs).first() # گرفتن اولین آبجکت - - if not obj: - raise Http404("No HadisInterpretation matches the given query.") - - self.check_object_permissions(self.request, obj) - return obj \ No newline at end of file +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.generics import ListAPIView, RetrieveAPIView +from django.shortcuts import get_object_or_404 +from utils.pagination import NoPagination, StandardResultsSetPagination +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response +from django.db.models import Count +from django.db.models import Prefetch +from ..serializers.category import get_localized_text +from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage +from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer +from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger +from django.db.models import Q +from ..serializers.category import get_localized_text + +from rest_framework import status +from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin + +class PinnedHadisCollectionListView(ListAPIView): + """ + API view to list pinned hadis collections with metadata info + """ + serializer_class = PinnedHadisCollectionSerializer + pagination_class = NoPagination + permission_classes = [IsAuthenticated] + authentication_classes = [TokenAuthentication] + + @hadis_collections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + return HadisCollection.objects.filter( + status=True, + display_position=HadisCollection.DisplayPosition.PINNED, + hadis_items__hadis__status=True + ).distinct().order_by('order', '-created_at') + + def list(self, request, *args, **kwargs): + # 1. گرفتن خروجی استاندارد لیست مجموعه‌ها + response = super().list(request, *args, **kwargs) + + # 2. محاسبه آمار (Info) + # تعداد کتگوری‌های فعال + categories_count = HadisCategory.objects.count() + + # تعداد نریتورها (راویان) + narrators_count = Transmitters.objects.count() + + # تعداد سورس‌ها (کتاب‌های منبع) + # فرض بر این است که BookReference مدل منابع شماست + from apps.hadis.models import BookReference + sources_count = BookReference.objects.count() + + # تعداد بوکمارک‌های کاربر در سرویس حدیث + from apps.bookmark.models.bookmark import Bookmark + bookmarks_count = Bookmark.objects.filter( + user=request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).count() + + # تعداد نویسنده‌ها + from apps.hadis.models import BookAuthor + authors_count = BookAuthor.objects.count() + + # 3. ساختن دیکشنری info + info = { + "categories_count": categories_count, + # "bookmarks_count": bookmarks_count, + "narrators_count": narrators_count, + "sources_count": sources_count, + "authors_count": authors_count, + } + + # 4. بازسازی دیتای نهایی مشابه اپ ویدیو + custom_data = { + "count": len(response.data) if isinstance(response.data, list) else response.data.get("count"), + "next": response.data.get("next") if isinstance(response.data, dict) else None, + "previous": response.data.get("previous") if isinstance(response.data, dict) else None, + "info": info, + "results": response.data if isinstance(response.data, list) else response.data.get("results") + } + + return Response(custom_data, status=status.HTTP_200_OK) + + +class HadisCollectionListView(ListAPIView): + """ + API view to list all hadis collections + """ + serializer_class = HadisCollectionListSerializer + pagination_class = NoPagination + + @hadis_collections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + return HadisCollection.objects.filter( + status=True, + display_position=HadisCollection.DisplayPosition.MIDDLE + ).order_by('order', '-created_at') + + +class HadisSyncView(ListAPIView): + """ + API view to sync all hadis data for offline mode + """ + serializer_class = HadisSyncSerializer + pagination_class = NoPagination + + def get_queryset(self): + return ( + Hadis.objects + .filter(status=True) + .select_related('category', 'hadis_status') + .prefetch_related( + 'tags', + Prefetch( + 'transmitters', + queryset=HadisTransmitter.objects.select_related( + 'transmitter', + 'transmitter__reliability', + 'narrator_layer' + ).order_by('order') + ), + Prefetch( + 'references', + queryset=HadisReference.objects + .select_related('book_reference') + .prefetch_related( + 'book_reference__author', + Prefetch( + 'images', + queryset=ReferenceImage.objects.order_by('priority') + ) + ) + ), + # 👇 اضافه شدن واکشی عمیق برای تصحیحات + Prefetch( + 'hadiscorrection_set', + queryset=HadisCorrection.objects.prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + ), + # 👇 اضافه شدن واکشی عمیق برای تفاسیر + Prefetch( + 'category__interpretations', + queryset=HadisInterpretation.objects.prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + ), + ) + .order_by('id') + ) + + @hadis_sync_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = self.get_serializer( + queryset, + many=True, + context={'request': request}, + ) + return Response({ + 'count': len(serializer.data), + 'results': serializer.data + }) + +from ..serializers.category import CategoryQuranVerseSerializer +class HadisListView(ListAPIView): + """ + API view to list Hadis by category_id + """ + serializer_class = HadisListSerializer + pagination_class = StandardResultsSetPagination + authentication_classes = [TokenAuthentication] + + @hadis_list_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + # 1. Run the standard list logic (get pagination, filter, results) + response = super().list(request, *args, **kwargs) + + # 2. Find the "Parent" Category based on the URL slug + category_slug = self.kwargs.get('category_slug') + category_obj = get_object_or_404(HadisCategory, slug=category_slug) + + # 3. Serialize this single category for the Hero section + category_data = SimpleCategory(category_obj).data + + # 4. Check if it's a Quran Category and fetch verse data + quran_data = None + if category_obj.source_type == HadisCategory.SourceType.QURAN: + try: + from ..models.category import CategoryQuranVerse + verse_obj = CategoryQuranVerse.objects.prefetch_related( + 'secondary_translations' + ).get(category=category_obj) + + from ..serializers.category import CategoryQuranVerseSerializer + quran_data = CategoryQuranVerseSerializer( + verse_obj, + context={'request': request} + ).data + except CategoryQuranVerse.DoesNotExist: + pass + + # 👇 5. NEW LOGIC: Fetch all statuses for frontend filtering UI + lang = getattr(request, "LANGUAGE_CODE", None) or "fa" + statuses_list = [ + { + "id": status.id, + "slug": status.slug, + "title": get_localized_text(status.title, request=request, language_code=lang) or "", + "color": status.color, + "main_color_code": status.main_color_code, + } + for status in HadisStatus.objects.all().order_by('order') + ] + + # 6. Inject it into the response data + if isinstance(response.data, dict): + ordered_data = {} + for key in ['count', 'next', 'previous']: + if key in response.data: + ordered_data[key] = response.data[key] + + ordered_data['current_category'] = category_data + ordered_data['quran_data'] = quran_data + + # 👇 تزریق لیست استاتوس‌ها قبل از نتایج + ordered_data['statuses'] = statuses_list + + if 'results' in response.data: + ordered_data['results'] = response.data['results'] + response.data = ordered_data + + return response + + def get_queryset(self): + category_slug = self.kwargs.get('category_slug') + if not HadisCategory.objects.filter(slug=category_slug).exists(): + return Hadis.objects.none() + + queryset = Hadis.objects.filter( + category__slug=category_slug, + status=True + ).annotate( + layer_count=Count('transmitters__narrator_layer', distinct=True) + ).select_related('category') + + # 👇 1. Apply Search Filter + search_query = self.request.query_params.get('search', None) + if search_query: + search_conditions = Q(text__icontains=search_query) + search_conditions |= Q(title__icontains=search_query) + search_conditions |= Q(title_narrator__icontains=search_query) + search_conditions |= Q(translation__icontains=search_query) + queryset = queryset.filter(search_conditions) + + # 👇 2. Apply Status Filter (Supports multiple comma-separated slugs) + status_filter = self.request.query_params.get('status', None) + if status_filter: + status_slugs = [s.strip() for s in status_filter.split(',')] + queryset = queryset.filter(hadis_status__slug__in=status_slugs) + + # 👇 3. Apply Transmitter Filter (Supports multiple comma-separated slugs) + transmitter_filter = self.request.query_params.get('transmitter', None) + if transmitter_filter: + transmitter_slugs = [t.strip() for t in transmitter_filter.split(',')] + queryset = queryset.filter(transmitters__transmitter__slug__in=transmitter_slugs) + + # 👇 4. Apply Source Filter (Supports multiple comma-separated slugs) + source_filter = self.request.query_params.get('source', None) + if source_filter: + source_slugs = [s.strip() for s in source_filter.split(',')] + queryset = queryset.filter(references__book_reference__slug__in=source_slugs) + + # 👇 5. Apply Source Author Filter (Supports author or source_author, multiple comma-separated slugs/IDs) + author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None) + if author_filter: + author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()] + author_q = Q(references__book_reference__author__slug__in=author_slugs) + numeric_ids = [int(a) for a in author_slugs if a.isdigit()] + if numeric_ids: + author_q |= Q(references__book_reference__author_id__in=numeric_ids) + queryset = queryset.filter(author_q) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + # ❗️ distinct() برای جلوگیری از دابلیکیت شدن احادیث به خاطر ManyToMany ها الزامی است + return queryset.distinct() + + def get_serializer_context(self): + """Add user bookmarks to serializer context to avoid caching issues""" + context = super().get_serializer_context() + + # Add user's bookmarked hadis IDs to context + user = self.request.user + if user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + user_bookmarks = Bookmark.objects.filter( + user=user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + context['user_bookmarked_hadis_ids'] = set(user_bookmarks) + else: + context['user_bookmarked_hadis_ids'] = set() + + return context + + + + +class HadisMainListView(ListAPIView): + """ + API view to list Hadis by category_id + """ + serializer_class = HadisListSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination + + @hadis_main_list_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + # queryset = Hadis.objects.select_related('category', 'hadis_status') + queryset = Hadis.objects.select_related('category__sect', 'hadis_status') + + # Get search parameters + search_query = self.request.query_params.get('search', None) + status_filter = self.request.query_params.get('status', None) + category_filter = self.request.query_params.get('category', None) + + # Apply search filter + if search_query: + queryset = self.apply_search_filter(queryset, search_query) + + # Apply status filter + if status_filter: + queryset = queryset.filter(hadis_status__title__icontains=status_filter) + + # Apply category filter + if category_filter: + queryset = queryset.filter(category__title__icontains=category_filter) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + return queryset + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + + # Apply pagination + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + paginated_response = self.get_paginated_response(serializer.data) + + # Get category titles + category_titles = self.get_category_titles(request) + + # Get status titles + status_titles = self.get_status_titles(request) + + # Modify the paginated response to include our custom data + response_data = paginated_response.data + # response_data['category_titles'] = self.get_cached_category_titles(request) + # response_data['status_titles'] = self.get_cached_status_titles(request) + + return Response(response_data) + + # Fallback for when pagination is disabled + serializer = self.get_serializer(queryset, many=True) + return Response({ + 'count': queryset.count(), + 'results': serializer.data + }) + + return Response(response_data) + + def get_category_titles(self,request): + """Get list of category titles based on language""" + from ..models import HadisCategory + + categories = HadisCategory.objects.all() + category_titles = [] + + for category in categories: + title = get_localized_text(category.title,request) + category_titles.append(title) + + return category_titles + + def get_status_titles(self, request): + """Get list of status titles based on language""" + from ..models import HadisStatus + + statuses = HadisStatus.objects.all().order_by('order') + status_titles = [] + + for status in statuses: + title = get_localized_text(status.title,request) + status_titles.append(title) + return status_titles + + def apply_search_filter(self, queryset, search_query): + """ + Apply search filter across multiple fields including JSONFields. + Searches in: title, title_narrator, text, translation + """ + from django.db.models import Q + + # Basic search conditions + search_conditions = Q(text__icontains=search_query) + + # For JSONFields, search in the JSON string representation + # This will find matches in the "text" values within the JSON arrays + search_conditions |= Q(title__icontains=search_query) + search_conditions |= Q(title_narrator__icontains=search_query) + search_conditions |= Q(translation__icontains=search_query) + + return queryset.filter(search_conditions) + + #we add this later + + # def get_cached_category_titles(self, request): + # """Fetches categories, cached for 1 hour to reduce DB load""" + # lang = getattr(request, "LANGUAGE_CODE", "en") + # cache_key = f"hadis_meta_categories_{lang}" + + # data = cache.get(cache_key) + # if not data: + # # If not in cache, fetch from DB + # from ..models import HadisCategory + # categories = HadisCategory.objects.all().only('id', 'title') # Fetch only needed fields + + # # Build list + # data = [get_localized_text(c.title, request) for c in categories] + + # # Save to cache + # cache.set(cache_key, data, timeout=60 * 60) # 1 Hour + + # return data + + # def get_cached_status_titles(self, request): + # """Fetches statuses, cached for 1 hour""" + # lang = getattr(request, "LANGUAGE_CODE", "en") + # cache_key = f"hadis_meta_statuses_{lang}" + + # data = cache.get(cache_key) + # if not data: + # from ..models import HadisStatus + # statuses = HadisStatus.objects.all().order_by('order') + # data = [get_localized_text(s.title, request) for s in statuses] + # cache.set(cache_key, data, timeout=60 * 60) + + # return data + + +class HadisBasicView(RetrieveAPIView): + """ + API view to retrieve basic Hadis information by hadis_slug + """ + serializer_class = HadisBasicSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + authentication_classes = [TokenAuthentication] + + @hadis_basic_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + return Hadis.objects.filter(status=True) + + def get_serializer_context(self): + """Add user bookmarks to serializer context to avoid caching issues""" + context = super().get_serializer_context() + + # Add user's bookmarked hadis IDs to context + user = self.request.user + if user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + user_bookmarks = Bookmark.objects.filter( + user=user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + context['user_bookmarked_hadis_ids'] = set(user_bookmarks) + else: + context['user_bookmarked_hadis_ids'] = set() + + return context + + +class HadisDetailView(Gone410ViewMixin, CanonicalSlugViewSetMixin, RetrieveAPIView): + """ + API view to retrieve detailed Hadis information by hadis_slug (excluding transmitters and corrections) + """ + serializer_class = HadisDetailSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + active_field_name = 'status' + active_expected_value = True + + @hadis_detail_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + return Hadis.objects.all().select_related( + 'category', 'hadis_status' + ).prefetch_related( + 'tags', + 'references__book_reference', + 'references__book_reference__author', + 'references__images', + ) + +class HadisTransmittersView(RetrieveAPIView): + """ + Fetches a single Hadis but filters the nested Transmitters list + if a ?layer=slug param is provided. + """ + serializer_class = HadisTransmitterListSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + pagination_class = NoPagination + @hadis_transmitters_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + # 1. Get the filter param + layer_slug = self.request.query_params.get('layer') + + # 2. Build the query for the "Child" (Transmitters) + # We start with the base optimization (select_related) + transmitter_qs = HadisTransmitter.objects.select_related( + 'transmitter', + 'narrator_layer' + ).order_by('order') + + # 3. Apply the filter to the Child Query (if param exists) + if layer_slug: + # Assumes 'NarratorLayer' has a 'slug' field. + # If not, use 'narrator_layer__name' or 'narrator_layer__id'. + transmitter_qs = transmitter_qs.filter(narrator_layer__slug=layer_slug) + + # 4. Use the Prefetch object to inject this filtered list into the Parent + return Hadis.objects.filter(status=True).prefetch_related( + Prefetch('transmitters', queryset=transmitter_qs) + ) + +class HadisCorrectionsView(ListAPIView): + """ + API view to retrieve corrections for a specific hadis + """ + serializer_class = HadisCorrectionSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @hadis_corrections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + # hadis = self.get_object() + # corrections_data = [] + + # for correction in hadis.hadiscorrection_set.all(): + # correction_info = { + # 'id': correction.id, + # 'title': correction.title, + # 'description': correction.description, + # 'translation': correction.translation + # } + # corrections_data.append(correction_info) + + # return Response({ + # 'hadis_id': hadis.id, + # 'corrections_count': len(corrections_data), + # 'corrections': corrections_data + # }) + + def get_queryset(self): + hadis_slug = self.kwargs.get('hadis_slug') + try: + hadis = Hadis.objects.get(slug=hadis_slug, status=True) + if not HadisCorrection.objects.filter(hadis=hadis).exists(): + return Hadis.objects.none() + queryset = HadisCorrection.objects.filter(hadis=hadis) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH_CORRECTION, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + return queryset + except Hadis.DoesNotExist: + return HadisCorrection.objects.none() + + + +class HadisLayersView(ListAPIView): + """ + API view to retrieve all narrator layers for a specific hadis + """ + serializer_class = NarratorLayerSerializer + pagination_class = NoPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @hadis_layers_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + from ..models import NarratorLayer + + hadis_slug = self.kwargs.get('hadis_slug') + + # Get the hadis object to ensure it exists + hadis = get_object_or_404(Hadis, slug=hadis_slug, status=True) + + # Return the layer objects for this hadis ordered by number + return NarratorLayer.objects.filter(hadis=hadis).order_by('number') + + +class HadisFiltersView(ListAPIView): + """ + API view to return filter data for hadis + Returns statuses and categories for filtering + """ + pagination_class = NoPagination + + @arguments_filters_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + # This view doesn't need a queryset, it returns computed data + return Hadis.objects.none() + + def list(self, request, *args, **kwargs): + # Get statuses from HadisStatus model + statuses = [] + for status in HadisStatus.objects.all().order_by('order'): + title_text = get_localized_text(status.title, request) + if title_text and status.slug: + statuses.append({ + 'text': title_text, + 'slug': status.slug + }) + + # Get categories from HadisCategory model + categories = [] + for category in HadisCategory.objects.all().order_by('order'): + title_text = get_localized_text(category.title, request) + if title_text and category.slug: + categories.append({ + 'text': title_text, + 'slug': category.slug + }) + + response_data = { + 'statuses': statuses, + 'categories': categories + } + + return Response(response_data) + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from rest_framework.authentication import TokenAuthentication +from drf_yasg.utils import swagger_auto_schema +from django.utils.translation import get_language + +from apps.hadis.models import HadisStatus, HadisCategory, BookReference, Transmitters +from drf_yasg import openapi +class HadisFiltersSyncAPIView(APIView): + """ + API view to return all filter data for hadis (Sync API for Mobile App) + Returns statuses, categories, sources (books), and narrators. + Optimized for offline client-side searching and sorting. + """ + permission_classes = [AllowAny] + authentication_classes = [TokenAuthentication] + + @swagger_auto_schema( + operation_description=( + "Sync API for Flutter/mobile clients: returns flat hadis filter data for offline " + "searching and local filtering. Titles are localized from multilingual fields based " + "on the active request language." + ), + operation_summary="Sync Hadis Filters", + tags=['Dobodbi - Hadis'], + manual_parameters=[ + openapi.Parameter( + 'Accept-Language', + openapi.IN_HEADER, + description="Language code for localized titles, for example `en`, `fa`, `ar`, `ur`, or `ru`.", + type=openapi.TYPE_STRING, + required=False, + ) + ], + responses={ + status.HTTP_200_OK: openapi.Response( + description="Flat filter payload for hadis categories, statuses, sources, and narrators.", + examples={ + "application/json": { + "statuses": [ + { + "id": 133, + "slug": "sahih", + "title": "Authentic", + "order": 1, + "color": "green", + "main_color_code": "#1DAC43" + } + ], + "categories": [ + { + "id": 1036, + "slug": "general-hadiths", + "title": "General Hadiths", + "order": 1 + } + ], + "sources": [ + { + "id": 2538, + "slug": "man-la-yahduruhu-al-faqih", + "title": "Man La Yahduruhu al-Faqih", + "search_keywords": "Shaykh al-Saduq" + } + ], + "narrators": [ + { + "id": 501, + "slug": "muhammad-ibn-muslim", + "title": "Muhammad ibn Muslim", + "search_aliases": "Abu Ja'far" + } + ] + } + } + ) + } + ) + def get(self, request, *args, **kwargs): + # تشخیص زبان برای ارسال دیتای لوکالایز شده + lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "ru" + + # ۱. استتوس‌ها (مرتب‌شده بر اساس فیلد order) + statuses = [ + { + "id": status.id, + "slug": status.slug, + "title": get_localized_text(status.title, request=request, language_code=lang) or "", + "order": status.order, + "color": status.color, + "main_color_code": status.main_color_code, + } + for status in HadisStatus.objects.all().order_by('order') + ] + + # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order) + categories = [ + { + "id": cat.id, + "slug": cat.slug, + "title": get_localized_text(cat.title, request=request, language_code=lang) or "", + "order": cat.order, + } + for cat in HadisCategory.objects.all().order_by('order') + ] + + # ۳. منابع / کتاب‌ها + sources = [] + # استفاده از select_related برای جلوگیری از مشکل N+1 Query هنگام گرفتن نویسنده‌ها + books = BookReference.objects.select_related('author').all().order_by('order', '-id') + + for book in books: + # استخراج نام نویسنده‌ها برای سرچ آفلاین فلاتر + authors_names = [ + get_localized_text(book.author.name, request=request, language_code=lang) or "" + ] if book.author else [] + + sources.append({ + "id": book.id, + "slug": book.slug, + "title": get_localized_text(book.title, request=request, language_code=lang) or "", + # تمام اسم‌های نویسنده‌ها را به هم می‌چسبانیم تا فلاتر بتواند روی آن‌ها سرچ بزند + # "search_keywords": " ".join(authors_names).strip() + }) + + # ۴. راویان (نریتورها) + narrators = [] + transmitters = Transmitters.objects.all().order_by('id') + + for narrator in transmitters: + # گرفتن نام‌های مستعار + known_as = get_localized_text(narrator.known_as, request=request, language_code=lang) or "" + nickname = get_localized_text(narrator.nickname, request=request, language_code=lang) or "" + kunya = get_localized_text(narrator.kunya, request=request, language_code=lang) or "" + + # تجمیع نام‌های جایگزین + search_aliases = f"{known_as} {nickname} {kunya}".strip() + + narrators.append({ + "id": narrator.id, + "slug": narrator.slug, + "title": get_localized_text(narrator.full_name, request=request, language_code=lang) or "", + # فلاتر روی این فیلد + title سرچ می‌زند + # "search_aliases": search_aliases + }) + + # خروجی نهایی بدون پجینیشن + return Response({ + "statuses": statuses, + "categories": categories, + "sources": sources, + "narrators": narrators + }) + +from ..serializers.hadis import HadisCorrectionDetailSerializer, HadisInterpretationDetailSerializer +from ..models import HadisInterpretation + +class HadisCorrectionDetailView(RetrieveAPIView): + """API View to get full details of a specific HadisCorrection by slug""" + serializer_class = HadisCorrectionDetailSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'correction_slug' + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Hadis Correction Detail", + operation_description="Returns the full details of a specific hadis correction, including text, translations, and its book references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return HadisCorrection.objects.all().select_related( + 'hadis' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + +class HadisInterpretationDetailView(RetrieveAPIView): + """API View to get full details of a specific HadisInterpretation by slug""" + serializer_class = HadisInterpretationDetailSerializer + lookup_field = 'slug' # 👈 تغییر به slug + lookup_url_kwarg = 'interpretation_slug' + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Hadis Interpretation Detail", + operation_description="Returns the full details of a specific hadis interpretation, including text, translations, and its book references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return HadisInterpretation.objects.all().select_related( + 'category' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + + # 👈 هندل کردن حالت امن برای زمانی که چند تفسیر در یک کتگوری باشند + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} + + obj = queryset.filter(**filter_kwargs).first() # گرفتن اولین آبجکت + + if not obj: + raise Http404("No HadisInterpretation matches the given query.") + + self.check_object_permissions(self.request, obj) + return obj + + +class HadisInterpretsListView(ListAPIView): + """ + API View to list all interpretations for the category of a specific Hadis. + """ + serializer_class = HadisInterpretationDetailSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [AllowAny] + pagination_class = StandardResultsSetPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @swagger_auto_schema( + operation_summary="Get Hadis Interpretations", + operation_description="Returns a paginated list of all interpretations registered for the category of this hadis, with full details and references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + hadis_slug = self.kwargs.get('hadis_slug') + try: + hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True) + if not hadis.category_id: + return HadisInterpretation.objects.none() + + return HadisInterpretation.objects.filter( + category_id=hadis.category_id + ).select_related( + 'category' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ).order_by('priority', 'id') + except Hadis.DoesNotExist: + return HadisInterpretation.objects.none() \ No newline at end of file diff --git a/apps/hadis/views/reference.py b/apps/hadis/views/reference.py index f8b8bd7..d840958 100644 --- a/apps/hadis/views/reference.py +++ b/apps/hadis/views/reference.py @@ -1,10 +1,12 @@ from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated -from rest_framework.generics import ListAPIView, RetrieveAPIView,ListCreateAPIView +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.generics import ListAPIView, RetrieveAPIView, ListCreateAPIView from rest_framework.response import Response from django.db.models import Q -from ..models import BookReference , BookAuthor , BookReferenceImage, BookAttribute -from ..serializers.reference import BookAuthorSerializer, BookDetailSerializer , BookReferenceSerializer, BookReferenceSyncSerializer, BookAttributeSerializer +from drf_yasg.utils import swagger_auto_schema +from ..models import BookReference, BookAuthor, BookReferenceImage, BookAttribute, Hadis +from ..serializers.reference import BookAuthorSerializer, BookDetailSerializer, BookReferenceSerializer, BookReferenceSyncSerializer, BookAttributeSerializer +from ..serializers.hadis import HadisListSerializer from ..serializers.category import get_localized_text from ..docs import book_attributes_create_swagger, book_attributes_list_swagger, book_references_list_swagger, book_authors_list_swagger, book_detail_swagger, reference_sync_swagger from utils.pagination import NoPagination @@ -218,3 +220,49 @@ class BookAttributeView(ListCreateAPIView): return self.create(request, *args, **kwargs) +class ReferenceExcerptsListView(ListAPIView): + """ + API view to retrieve all hadiths associated with a specific book reference (excerpts) + """ + serializer_class = HadisListSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [AllowAny] + pagination_class = StandardResultsSetPagination + lookup_field = 'slug' + lookup_url_kwarg = 'reference_slug' + + @swagger_auto_schema( + operation_summary="Get Reference Excerpts (Hadiths)", + operation_description="Returns a paginated list of all hadiths connected to this book reference.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + reference_slug = self.kwargs.get('reference_slug') + return Hadis.objects.filter( + references__book_reference__slug=reference_slug, + status=True + ).select_related( + 'category__sect', + 'hadis_status' + ).distinct().order_by('id') + + def get_serializer_context(self): + context = super().get_serializer_context() + user = self.request.user + if user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + user_bookmarks = Bookmark.objects.filter( + user=user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + context['user_bookmarked_hadis_ids'] = set(user_bookmarks) + else: + context['user_bookmarked_hadis_ids'] = set() + return context + + + diff --git a/apps/hadis/views/reference_v2.py b/apps/hadis/views/reference_v2.py index 026a25e..4160b69 100644 --- a/apps/hadis/views/reference_v2.py +++ b/apps/hadis/views/reference_v2.py @@ -156,7 +156,7 @@ class BookReferenceV2ListView(generics.ListAPIView): return values def get_queryset(self): - qs = BookReference.objects.select_related('author').prefetch_related('tags', 'volumes__images').order_by('-created_at') + qs = BookReference.objects.select_related('author', 'tag').prefetch_related('volumes__images').order_by('-created_at') search = self.request.query_params.get('search') if search: diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 58b31be..a36df9c 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -11,7 +11,7 @@ from utils.pagination import StandardResultsSetPagination, OptionalPagination from .models import ( Hadis, HadisCategory, HadisCollection, HadisInCollection, HadisCorrection, HadisInterpretation, HadisStatus, HadisTag, HadisSect, HadisTransmitter, - BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookEdition, BookVolume, + BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, TransmitterOpinion, TransmitterOriginalText ) from .serializers.serializers_admin import ( @@ -29,6 +29,7 @@ from .serializers.serializers_admin import ( AdminHadisTagSerializer, AdminBookSubjectAreaSerializer, AdminBookTypeSerializer, + AdminBookTagSerializer, AdminBookAuthorSerializer, AdminBookAttributeSerializer, AdminBookReferenceImageSerializer, @@ -770,11 +771,27 @@ class AdminBookEditionViewSet(ModelViewSet): return queryset.order_by("-created_at") +class AdminBookTagViewSet(ModelViewSet): + permission_classes = [IsAuthenticated, IsSuperAdmin] + authentication_classes = [TokenAuthentication] + serializer_class = AdminBookTagSerializer + pagination_class = None + parser_classes = (JSONParser, MultiPartParser, FormParser) + + def get_queryset(self): + queryset = BookTag.objects.all() + search_query = self.request.query_params.get("search") + if search_query: + queryset = queryset.filter(title__icontains=search_query) + return queryset.order_by("-id") + + class AdminBookVolumeViewSet(ModelViewSet): permission_classes = [IsAuthenticated, IsSuperAdmin] authentication_classes = [TokenAuthentication] serializer_class = AdminBookVolumeSerializer pagination_class = None + parser_classes = (MultiPartParser, FormParser, JSONParser) def get_queryset(self): queryset = BookVolume.objects.all()