diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index fddaba0..89f23ea 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -303,7 +303,8 @@ class HadisListSerializer(serializers.ModelSerializer): formatted = format_address_output(obj.address) if formatted: return formatted - first_ref = obj.references.first() + refs = list(obj.references.all()) + first_ref = refs[0] if refs else None if first_ref and hasattr(first_ref, 'address') and first_ref.address: return format_address_output(first_ref.address) return [] @@ -315,7 +316,7 @@ class HadisListSerializer(serializers.ModelSerializer): request = self.context.get('request') images_list = [] for ref in obj.references.all(): - for img in ref.images.all().order_by('priority'): + for img in sorted(ref.images.all(), key=lambda x: getattr(x, 'priority', 0)): url = None if img.thumbnail: url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url) @@ -597,7 +598,8 @@ class TransmitterOriginalTextSerializer(serializers.ModelSerializer): fields = ['id', 'title', 'text', 'translation', 'share_link', 'slug', 'address', 'images'] def get_address(self, obj): - return format_address_output(obj.references.first()) + refs = list(obj.references.all()) + return format_address_output(refs[0] if refs else None) def get_images(self, obj): request = self.context.get('request') @@ -1175,7 +1177,8 @@ class HadisCorrectionSerializer(serializers.ModelSerializer): return book_mark.get('is_bookmarked', False) def get_address(self, obj): - return format_address_output(obj.references.first()) + refs = list(obj.references.all()) + return format_address_output(refs[0] if refs else None) def get_images(self, obj): request = self.context.get('request') @@ -1468,7 +1471,8 @@ class HadisCorrectionDetailSerializer(serializers.ModelSerializer): return {"id": obj.hadis.id, "number": obj.hadis.number, "slug": obj.hadis.slug} def get_address(self, obj): - return format_address_output(obj.references.first()) + refs = list(obj.references.all()) + return format_address_output(refs[0] if refs else None) def get_images(self, obj): request = self.context.get('request') @@ -1505,7 +1509,8 @@ class HadisInterpretationDetailSerializer(serializers.ModelSerializer): } def get_address(self, obj): - return format_address_output(obj.references.first()) + refs = list(obj.references.all()) + return format_address_output(refs[0] if refs else None) def get_images(self, obj): request = self.context.get('request') @@ -1543,7 +1548,8 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer): } def get_address(self, obj): - return format_address_output(obj.references.first()) + refs = list(obj.references.all()) + return format_address_output(refs[0] if refs else None) def get_images(self, obj): request = self.context.get('request') @@ -1590,7 +1596,8 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer): if formatted: return formatted # 2. Fallback: references address - first_ref = obj.references.first() + refs = list(obj.references.all()) + first_ref = refs[0] if refs else None if first_ref and hasattr(first_ref, 'address') and first_ref.address: return format_address_output(first_ref.address) return [] @@ -1602,7 +1609,7 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer): request = self.context.get('request') images_list = [] for ref in obj.references.all(): - for img in ref.images.all().order_by('priority'): + for img in sorted(ref.images.all(), key=lambda x: getattr(x, 'priority', 0)): url = None if img.thumbnail: url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url) diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 4825475..201e05d 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -70,80 +70,80 @@ for prefix, viewset, basename in admin_router.registry: urlpatterns = [ - path('sync/authors/', BookAuthorSyncView.as_view(), name='sync-author-list'), - 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'), + path('sync/authors/', cached_view(BookAuthorSyncView.as_view()), name='sync-author-list'), + path('authors//references/', cached_view(AuthorReferencesListView.as_view()), name='author-references'), + path('authors//', cached_view(BookAuthorDetailView.as_view()), name='author-detail'), + path('authors/', cached_view(BookAuthorListView.as_view()), name='author-list'), # Admin endpoints path('admin/', include(admin_router.urls)), # Most specific first (with parameters) - path('pinned-collections/', PinnedHadisCollectionListView.as_view(), name='pinned-hadis-collection-list'), - path('collections/', HadisCollectionListView.as_view(), name='hadis-collection-list'), - path('sync/sects/', HadisCategorySectListView.as_view(), name='hadis-sect-list'), - path('sync/categories/tree/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'), - path('sync/hadis/', HadisSyncView.as_view(), name='hadis-sync'), - path('sync/narrators/', TransmitterSyncView.as_view(), name='transmitter-sync'), - path('sync/references/', BookReferenceSyncView.as_view(), name='reference-sync'), - path('v2/sync/references/', BookReferenceV2SyncView.as_view(), name='reference-sync-v2'), + path('pinned-collections/', cached_view(PinnedHadisCollectionListView.as_view()), name='pinned-hadis-collection-list'), + path('collections/', cached_view(HadisCollectionListView.as_view()), name='hadis-collection-list'), + path('sync/sects/', cached_view(HadisCategorySectListView.as_view()), name='hadis-sect-list'), + path('sync/categories/tree/', cached_view(HadisCategoryTreeView.as_view()), name='hadis-category-tree'), + path('sync/hadis/', cached_view(HadisSyncView.as_view()), name='hadis-sync'), + path('sync/narrators/', cached_view(TransmitterSyncView.as_view()), name='transmitter-sync'), + path('sync/references/', cached_view(BookReferenceSyncView.as_view()), name='reference-sync'), + path('v2/sync/references/', cached_view(BookReferenceV2SyncView.as_view()), name='reference-sync-v2'), path('sync/version/', ContentReleaseSyncView.as_view(), name='content-release-sync'), - path('sync/filters/', HadisFiltersSyncAPIView.as_view(), name='hadis-filters-sync'), - path('info/', HadisInfoView.as_view(), name='hadis-info'), + path('sync/filters/', cached_view(HadisFiltersSyncAPIView.as_view()), name='hadis-filters-sync'), + path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'), # Category paths (more specific first) - path('categories/tree/', HadisCategoryTreeNormalView.as_view(), name='hadis-category-tree-normal'), - path('categories////', HadisCategorySelectBySectSourceView.as_view(), name='categories-tree-by-sect-source'), - path('categories///', HadisCategorySelectBySectView.as_view(), name='categories-tree-by-sect'), + path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'), + path('categories////', cached_view(HadisCategorySelectBySectSourceView.as_view()), name='categories-tree-by-sect-source'), + path('categories///', cached_view(HadisCategorySelectBySectView.as_view()), name='categories-tree-by-sect'), path('categories//', CategoriesBySectView.as_view(), name='categories-by-sect'), - path('categories/', CategoriesView.as_view(), name='categories'), # ← Least specific LAST + path('categories/', cached_view(CategoriesView.as_view()), name='categories'), # ← Least specific LAST # Hadis paths - path('category//xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths + path('category//xmind/', cached_view(HadisCategoryXMindView.as_view()), name='hadis-category-xmind'), # ← Must be before other category paths path('category//corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'), path('category//', HadisListView.as_view(), name='hadis-list'), - path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'), + path('arguments/filters/', cached_view(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/', TransmitterView.as_view(), name='narrators'), + path('narrators/filters/', cached_view(TransmitterFiltersView.as_view()), name='narrator-filters'), + path('narrators//teachers/', cached_view(NarratorTeachersView.as_view()), name='narrator-teachers'), + path('narrators//students/', cached_view(NarratorStudentsView.as_view()), name='narrator-students'), + path('narrators//arguments/', cached_view(NarratorArgumentsListView.as_view()), name='narrator-arguments'), + path('narrators//opinions/', cached_view(TransmitterOpinionView.as_view()), name='narrator-opinions'), + path('narrators//original_texts/', cached_view(TransmitterOriginalTextView.as_view()), name='narrator-original-texts'), + path('narrators//', cached_view(TransmitterDetailView.as_view()), name='narrator-detail'), + path('narrators/', cached_view(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//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'), + path('v2/references//excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts-v2'), + path('v2/references//', cached_view(BookReferenceV2DetailView.as_view()), name='reference-detail-v2'), + path('v2/references/', cached_view(BookReferenceV2ListView.as_view()), name='reference-list-v2'), + path('references//excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts'), + path('references//', cached_view(BookDetailView.as_view()), name='reference-detail'), + path('references/', cached_view(BookDetailView.as_view()), name='reference-detail-no-slash'), + path('references/', cached_view(BookReferencesView.as_view()), name='references'), # Hadis detail paths (with slug, more specific) - path('/source-details/', HadisSourceDetailsView.as_view(), name='hadis-source-details'), - path('/sources/', HadisSourceDetailsView.as_view(), name='hadis-sources'), + path('/source-details/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-source-details'), + path('/sources/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-sources'), path('/detail/', HadisDetailView.as_view(), name='hadis-detail'), - path('/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'), - path('/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'), + path('/transmitters/', cached_view(HadisTransmittersView.as_view()), name='hadis-transmitters'), + path('/transmitters/layers/', cached_view(HadisLayersView.as_view()), name='hadis-layers'), path('/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'), - path('/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'), + path('/interprets/', cached_view(HadisInterpretsListView.as_view()), name='hadis-interprets'), path('/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST # path('test/test-deploy',test_deploy , name='test'), # path('debug-headers',debug_headers , name='headers'), # Interpretation Detail Endpoint (تفسیر بر اساس آی‌دی) - path('interpretations//', HadisInterpretationDetailView.as_view(), name='interpretation-detail'), + path('interpretations//', cached_view(HadisInterpretationDetailView.as_view()), name='interpretation-detail'), # Correction Detail Endpoint (تصحیح بر اساس اسلاگ) - path('corrections//', HadisCorrectionDetailView.as_view(), name='correction-detail'), + path('corrections//', cached_view(HadisCorrectionDetailView.as_view()), name='correction-detail'), # Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ) - path('original-texts//', TransmitterOriginalTextDetailView.as_view(), name='original-text-detail'), + path('original-texts//', cached_view(TransmitterOriginalTextDetailView.as_view()), name='original-text-detail'), ] diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index cf66f0f..b2ecbdf 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -146,7 +146,7 @@ class HadisSyncView(ListAPIView): # 👇 اضافه شدن واکشی عمیق برای تصحیحات Prefetch( 'hadiscorrection_set', - queryset=HadisCorrection.objects.prefetch_related( + queryset=HadisCorrection.objects.select_related('hadis').prefetch_related( 'references__book_reference__author', 'references__edition', 'references__book_volume', @@ -156,7 +156,7 @@ class HadisSyncView(ListAPIView): # 👇 اضافه شدن واکشی عمیق برای تفاسیر Prefetch( 'category__interpretations', - queryset=HadisInterpretation.objects.prefetch_related( + queryset=HadisInterpretation.objects.select_related('category').prefetch_related( 'references__book_reference__author', 'references__edition', 'references__book_volume', diff --git a/config/settings/base.py b/config/settings/base.py index 4f62f3c..e76b254 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -117,6 +117,7 @@ AUTH_USER_MODEL = "account.User" MIDDLEWARE = [ 'config.middleware.site_middleware.SiteMiddleware', # Must be first to route by domain 'django.middleware.security.SecurityMiddleware', + 'django.middleware.gzip.GZipMiddleware', "whitenoise.middleware.WhiteNoiseMiddleware", 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware',