Browse Source

feat(hadis): add tags list endpoint and support tag filtering and search

master
Mohsen Taba 5 days ago
parent
commit
23aa54d18b
  1. 13
      apps/hadis/serializers/category.py
  2. 21
      apps/hadis/serializers/hadis.py
  3. 3
      apps/hadis/urls.py
  4. 58
      apps/hadis/views/hadis.py

13
apps/hadis/serializers/category.py

@ -36,7 +36,18 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod
# Get target language if not provided
if not language_code:
language_code = getattr(request, "LANGUAGE_CODE", None) if request else None
if request:
query_params = getattr(request, "query_params", None) or getattr(request, "GET", None)
if query_params:
language_code = query_params.get("language_code") or query_params.get("lang")
if not language_code:
language_code = getattr(request, "LANGUAGE_CODE", None)
if not language_code and hasattr(request, "headers"):
language_code = request.headers.get("Accept-Language")
elif not language_code and hasattr(request, "META"):
language_code = request.META.get("HTTP_ACCEPT_LANGUAGE")
if language_code:
language_code = language_code.split(",")[0].split(";")[0].split("-")[0].strip().lower()
if not language_code:
language_code = get_language() or fallback_lang

21
apps/hadis/serializers/hadis.py

@ -304,11 +304,21 @@ class HadisSyncSerializer(serializers.ModelSerializer):
).data
return []
class HadisTagSerializer(serializers.ModelSerializer):
"""Serializer for HadisTag"""
title = LocalizedField()
class Meta:
model = HadisTag
fields = ['id', 'title']
class HadisListSerializer(serializers.ModelSerializer):
"""Serializer for Hadis list"""
category = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
bookmark = serializers.SerializerMethodField()
tags = HadisTagSerializer(many=True, read_only=True)
translation = LocalizedField()
title = LocalizedField()
title_narrator = LocalizedField()
@ -321,7 +331,7 @@ class HadisListSerializer(serializers.ModelSerializer):
model = Hadis
fields = ['id', 'number', 'slug', 'title', 'title_narrator', 'text',
'translation', 'category', 'status', 'bookmark', 'share_link',
'address', 'links', 'reference_images', 'images']
'address', 'links', 'reference_images', 'images', 'tags']
def get_address(self, obj):
formatted = format_address_output(obj.address)
@ -420,15 +430,6 @@ class HadisStatusSerializer(serializers.ModelSerializer):
fields = ['id', 'title', 'color', 'main_color_code', 'description']
class HadisTagSerializer(serializers.ModelSerializer):
"""Serializer for HadisTag"""
title = LocalizedField()
class Meta:
model = HadisTag
fields = ['id', 'title']
class TransmitterSerializer(serializers.ModelSerializer):
"""Serializer for Transmitters"""
full_name = LocalizedField()

3
apps/hadis/urls.py

@ -3,7 +3,7 @@ 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, CategoryHadisCorrectionsView, HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView,
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView, HadisTagListView
)
from .views.transmitter import (
TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView,
@ -91,6 +91,7 @@ urlpatterns = [
path('sync/database/', HadisDatabaseVersionView.as_view(), name='hadis-database-sync'),
path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'),
path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'),
path('tags/', cached_view(HadisTagListView.as_view()), name='hadis-tag-list'),
# Category paths (more specific first)
path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'),

58
apps/hadis/views/hadis.py

@ -10,15 +10,25 @@ 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, CorrectionReference, HadisInterpretation, InterpretationReference
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer
from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage, CorrectionReference, HadisInterpretation, InterpretationReference, HadisTag
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer, HadisTagSerializer
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 HadisTagListView(ListAPIView):
"""
API view to list active Hadis Tags
"""
serializer_class = HadisTagSerializer
pagination_class = NoPagination
permission_classes = [AllowAny]
def get_queryset(self):
return HadisTag.objects.filter(status=True).order_by('id')
class PinnedHadisCollectionListView(ListAPIView):
"""
API view to list pinned hadis collections with metadata info
@ -293,6 +303,14 @@ class HadisListView(ListAPIView):
for status in HadisStatus.objects.all().order_by('order')
]
tags_list = [
{
"id": tag.id,
"title": get_localized_text(tag.title, request=request, language_code=lang) or "",
}
for tag in HadisTag.objects.filter(status=True).order_by('id')
]
# 6. Inject it into the response data
if isinstance(response.data, dict):
ordered_data = {}
@ -303,8 +321,9 @@ class HadisListView(ListAPIView):
ordered_data['current_category'] = category_data
ordered_data['quran_data'] = quran_data
# 👇 تزریق لیست استاتوس‌ها قبل از نتایج
# 👇 تزریق لیست استاتوس‌ها و تگ‌ها قبل از نتایج
ordered_data['statuses'] = statuses_list
ordered_data['tags'] = tags_list
if 'results' in response.data:
ordered_data['results'] = response.data['results']
@ -323,6 +342,7 @@ class HadisListView(ListAPIView):
).annotate(
layer_count=Count('transmitters__narrator_layer', distinct=True)
).select_related('category').prefetch_related(
'tags',
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
@ -338,6 +358,7 @@ class HadisListView(ListAPIView):
search_conditions |= Q(title__icontains=search_query)
search_conditions |= Q(title_narrator__icontains=search_query)
search_conditions |= Q(translation__icontains=search_query)
search_conditions |= Q(tags__title__icontains=search_query)
queryset = queryset.filter(search_conditions)
# 👇 2. Apply Status Filter (Supports multiple comma-separated slugs)
@ -368,6 +389,20 @@ class HadisListView(ListAPIView):
author_q |= Q(references__book_reference__author_id__in=numeric_ids)
queryset = queryset.filter(author_q)
# 👇 6. Apply Tag Filter (Supports comma-separated IDs or titles)
tag_filter = self.request.query_params.get('tag', None) or self.request.query_params.get('tags', None)
if tag_filter:
tag_items = [t.strip() for t in tag_filter.split(',') if t.strip()]
numeric_tag_ids = [int(t) for t in tag_items if t.isdigit()]
non_numeric_tags = [t for t in tag_items if not t.isdigit()]
tag_q = Q()
if numeric_tag_ids:
tag_q |= Q(tags__id__in=numeric_tag_ids)
for t_val in non_numeric_tags:
tag_q |= Q(tags__title__icontains=t_val)
if tag_q:
queryset = queryset.filter(tag_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:
@ -431,6 +466,7 @@ class HadisMainListView(ListAPIView):
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)
tag_filter = self.request.query_params.get('tag', None) or self.request.query_params.get('tags', None)
# Apply search filter
if search_query:
@ -444,6 +480,19 @@ class HadisMainListView(ListAPIView):
if category_filter:
queryset = queryset.filter(category__title__icontains=category_filter)
# Apply tag filter
if tag_filter:
tag_items = [t.strip() for t in tag_filter.split(',') if t.strip()]
numeric_tag_ids = [int(t) for t in tag_items if t.isdigit()]
non_numeric_tags = [t for t in tag_items if not t.isdigit()]
tag_q = Q()
if numeric_tag_ids:
tag_q |= Q(tags__id__in=numeric_tag_ids)
for t_val in non_numeric_tags:
tag_q |= Q(tags__title__icontains=t_val)
if tag_q:
queryset = queryset.filter(tag_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:
@ -528,6 +577,7 @@ class HadisMainListView(ListAPIView):
search_conditions |= Q(title__icontains=search_query)
search_conditions |= Q(title_narrator__icontains=search_query)
search_conditions |= Q(translation__icontains=search_query)
search_conditions |= Q(tags__title__icontains=search_query)
return queryset.filter(search_conditions)

Loading…
Cancel
Save