diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index cd29611..b58e049 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -67,9 +67,9 @@ for prefix, viewset, basename in admin_router.registry: urlpatterns = [ path('sync/authors/', BookAuthorSyncView.as_view(), name='sync-author-list'), - path('authors/', BookAuthorListView.as_view(), name='author-list'), - path('authors//', BookAuthorDetailView.as_view(), name='author-detail'), path('authors//references/', AuthorReferencesListView.as_view(), name='author-references'), + path('authors//', BookAuthorDetailView.as_view(), name='author-detail'), + path('authors/', BookAuthorListView.as_view(), name='author-list'), # Admin endpoints @@ -98,17 +98,17 @@ urlpatterns = [ # Hadis paths path('category//xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths path('category//', HadisListView.as_view(), name='hadis-list'), - path('arguments/', HadisMainListView.as_view(), name='hadis-main-list'), path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'), + path('arguments/', HadisMainListView.as_view(), name='hadis-main-list'), # Narrator paths + path('narrators/filters/', TransmitterFiltersView.as_view(), name='narrator-filters'), path('narrators//teachers/', NarratorTeachersView.as_view(), name='narrator-teachers'), path('narrators//students/', NarratorStudentsView.as_view(), name='narrator-students'), path('narrators//arguments/', NarratorArgumentsListView.as_view(), name='narrator-arguments'), path('narrators//opinions/', TransmitterOpinionView.as_view(), name='narrator-opinions'), path('narrators//original_texts/', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'), path('narrators//', TransmitterDetailView.as_view(), name='narrator-detail'), - path('narrators/filters/', TransmitterFiltersView.as_view(), name='narrator-filters'), path('narrators/', TransmitterView.as_view(), name='narrators'), # Reference paths diff --git a/apps/hadis/views/reference.py b/apps/hadis/views/reference.py index 58e28d8..f8b8bd7 100644 --- a/apps/hadis/views/reference.py +++ b/apps/hadis/views/reference.py @@ -95,41 +95,74 @@ class BookReferenceSyncView(ListAPIView): def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + def _get_multi_param(self, *param_names): + values = [] + for name in param_names: + for item in self.request.query_params.getlist(name): + for val in str(item).split(','): + val = val.strip() + if val: + values.append(val) + return values + def get_queryset(self): """ Prefetch ALL related data to avoid N+1 queries """ - qs = BookReference.objects.select_related().prefetch_related( - 'authors', 'attributes', 'images', 'hadis_references__hadis' + qs = BookReference.objects.select_related('author', 'type').prefetch_related( + 'attributes', 'images', 'hadis_references__hadis' ).distinct().order_by('id') - category = self.request.query_params.get('category') - if category: - qs = qs.filter(subject_area__id=category) + # Category (Multi-select) + categories = self._get_multi_param('category', 'categories', 'category[]', 'categories[]') + category_ids = [int(c) for c in categories if c.isdigit()] + if category_ids: + qs = qs.filter(subject_area__id__in=category_ids).distinct() - author = self.request.query_params.get('author') - if author: - qs = qs.filter(authors__id=author) + # Author (Multi-select) + authors = self._get_multi_param('author', 'authors', 'author[]', 'authors[]') + author_ids = [int(a) for a in authors if a.isdigit()] + if author_ids: + qs = qs.filter(author_id__in=author_ids) - sect = self.request.query_params.get('sect') - if sect: - qs = qs.filter(type__id=sect) + # Sect / Madhab (Multi-select) + sects = self._get_multi_param('sect', 'sects', 'sect[]', 'sects[]', 'madhab', 'madhabs', 'madhab[]', 'madhabs[]') + if sects: + from django.db.models import Q + sect_q = Q() + type_ids = [] + for s in sects: + if s.lower() in ['shia', 'sunni']: + sect_q |= Q(type__sect=s.lower()) + elif s.isdigit(): + type_ids.append(int(s)) + if type_ids: + sect_q |= Q(type__id__in=type_ids) + if sect_q: + qs = qs.filter(sect_q) + # PDF availability pdf_available = self.request.query_params.get('pdf_available') if pdf_available == 'true': - qs = qs.filter(documents__isnull=False) + qs = qs.filter(documents__isnull=False).distinct() elif pdf_available == 'false': qs = qs.filter(documents__isnull=True) - century = self.request.query_params.get('century') - if century: - if century == 'before_islam': - qs = qs.filter(authors__death_year_hijri__lt=0) - elif century.isdigit(): - century_num = int(century) - start_year = (century_num - 1) * 100 + 1 - end_year = century_num * 100 - qs = qs.filter(authors__death_year_hijri__gte=start_year, authors__death_year_hijri__lte=end_year) + # Century (Multi-select) + centuries = self._get_multi_param('century', 'centuries', 'century[]', 'centuries[]') + if centuries: + from django.db.models import Q + century_q = Q() + for c in centuries: + if c == 'before_islam': + century_q |= Q(author__death_year_hijri__lt=0) + elif c.isdigit(): + century_num = int(c) + start_year = (century_num - 1) * 100 + 1 + end_year = century_num * 100 + century_q |= Q(author__death_year_hijri__gte=start_year, author__death_year_hijri__lte=end_year) + if century_q: + qs = qs.filter(century_q) return qs diff --git a/apps/hadis/views/reference_v2.py b/apps/hadis/views/reference_v2.py index 4bfb5e1..026a25e 100644 --- a/apps/hadis/views/reference_v2.py +++ b/apps/hadis/views/reference_v2.py @@ -133,17 +133,28 @@ class BookReferenceV2ListView(generics.ListAPIView): "Use query parameters to filter the results.", tags=['Dobodbi - Hadis (V2)'], manual_parameters=[ - openapi.Parameter('category', openapi.IN_QUERY, description="Filter by Category/Subject Area ID", type=openapi.TYPE_INTEGER), - openapi.Parameter('author', openapi.IN_QUERY, description="Filter by Author ID", type=openapi.TYPE_INTEGER), - openapi.Parameter('madhab', openapi.IN_QUERY, description="Filter by Madhab ('shia' or 'sunni') or Type ID", type=openapi.TYPE_STRING, enum=['shia', 'sunni']), + openapi.Parameter('category', openapi.IN_QUERY, description="Filter by Category/Subject Area ID(s). Supports comma-separated (e.g. '1,2,3') or multiple params", type=openapi.TYPE_STRING), + openapi.Parameter('author', openapi.IN_QUERY, description="Filter by Author ID(s). Supports comma-separated (e.g. '1,2,3') or multiple params", type=openapi.TYPE_STRING), + openapi.Parameter('madhab', openapi.IN_QUERY, description="Filter by Madhab ('shia' or 'sunni') or Type ID(s). Supports comma-separated", type=openapi.TYPE_STRING), openapi.Parameter('pdf_available', openapi.IN_QUERY, description="Filter by PDF availability ('true' or 'false')", type=openapi.TYPE_STRING), - openapi.Parameter('century', openapi.IN_QUERY, description="Filter by Century (e.g. '1', '2', 'before_islam')", type=openapi.TYPE_STRING), + openapi.Parameter('century', openapi.IN_QUERY, description="Filter by Century (e.g. '1,2', 'before_islam'). Supports comma-separated", type=openapi.TYPE_STRING), openapi.Parameter('search', openapi.IN_QUERY, description="Search term for book title, description, or author name", type=openapi.TYPE_STRING), ] ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) + def _get_multi_param(self, *param_names): + """Extract multi-value params supporting comma-separated, repeated keys, and [] suffix.""" + values = [] + for name in param_names: + for item in self.request.query_params.getlist(name): + for val in str(item).split(','): + val = val.strip() + if val: + values.append(val) + return values + def get_queryset(self): qs = BookReference.objects.select_related('author').prefetch_related('tags', 'volumes__images').order_by('-created_at') @@ -156,36 +167,56 @@ class BookReferenceV2ListView(generics.ListAPIView): Q(author__name__icontains=search) ) - category = self.request.query_params.get('category') - if category: - qs = qs.filter(subject_area__id=category) + # Category (Multi-select) + categories = self._get_multi_param('category', 'categories', 'category[]', 'categories[]') + category_ids = [int(c) for c in categories if c.isdigit()] + if category_ids: + qs = qs.filter(subject_area__id__in=category_ids).distinct() - author = self.request.query_params.get('author') - if author: - qs = qs.filter(author_id=author) + # Author (Multi-select) + authors = self._get_multi_param('author', 'authors', 'author[]', 'authors[]') + author_ids = [int(a) for a in authors if a.isdigit()] + if author_ids: + qs = qs.filter(author_id__in=author_ids) - madhab = self.request.query_params.get('madhab') - if madhab: - if madhab in ['shia', 'sunni']: - qs = qs.filter(type__sect=madhab) - else: - qs = qs.filter(type__id=madhab) + # Madhab / Sect (Multi-select) + madhabs = self._get_multi_param('madhab', 'madhabs', 'madhab[]', 'madhabs[]', 'sect', 'sects', 'sect[]', 'sects[]') + if madhabs: + from django.db.models import Q + madhab_q = Q() + type_ids = [] + for m in madhabs: + if m.lower() in ['shia', 'sunni']: + madhab_q |= Q(type__sect=m.lower()) + elif m.isdigit(): + type_ids.append(int(m)) + if type_ids: + madhab_q |= Q(type__id__in=type_ids) + if madhab_q: + qs = qs.filter(madhab_q) + # PDF availability pdf_available = self.request.query_params.get('pdf_available') if pdf_available == 'true': - qs = qs.filter(documents__isnull=False) + qs = qs.filter(documents__isnull=False).distinct() elif pdf_available == 'false': qs = qs.filter(documents__isnull=True) - century = self.request.query_params.get('century') - if century: - if century == 'before_islam': - qs = qs.filter(authors__death_year_hijri__lt=0) - elif century.isdigit(): - century_num = int(century) - start_year = (century_num - 1) * 100 + 1 - end_year = century_num * 100 - qs = qs.filter(authors__death_year_hijri__gte=start_year, authors__death_year_hijri__lte=end_year) + # Century (Multi-select) + centuries = self._get_multi_param('century', 'centuries', 'century[]', 'centuries[]') + if centuries: + from django.db.models import Q + century_q = Q() + for c in centuries: + if c == 'before_islam': + century_q |= Q(author__death_year_hijri__lt=0) + elif c.isdigit(): + century_num = int(c) + start_year = (century_num - 1) * 100 + 1 + end_year = century_num * 100 + century_q |= Q(author__death_year_hijri__gte=start_year, author__death_year_hijri__lte=end_year) + if century_q: + qs = qs.filter(century_q) return qs