2 Commits

Author SHA1 Message Date
Mohsen Taba 59ad3cd064 perf(caching): add redis and conditional caching across apps, fix bookmark filtering 1 month ago
Mohsen Taba d1094a4725 perf(hadis): optimize sync api, eliminate N+1 queries, add redis caching and gzip 1 month ago
  1. 28
      apps/article/urls.py
  2. 7
      apps/article/views.py
  3. 12
      apps/dobodbi_calendar/urls.py
  4. 25
      apps/hadis/serializers/hadis.py
  5. 86
      apps/hadis/urls.py
  6. 4
      apps/hadis/views/hadis.py
  7. 2
      apps/library/serializers.py
  8. 31
      apps/library/urls.py
  9. 23
      apps/library/views.py
  10. 29
      apps/podcast/urls.py
  11. 10
      apps/podcast/views.py
  12. 126
      apps/video/management/commands/assign_random_videos_to_playlists.py
  13. 13
      apps/video/serializers.py
  14. 30
      apps/video/urls.py
  15. 81
      apps/video/views.py
  16. 1
      config/settings/base.py

28
apps/article/urls.py

@ -1,8 +1,28 @@
from functools import wraps
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from .views import * from .views import *
from .views_admin import AdminArticleCategoryViewSet, AdminArticleContentViewSet, AdminArticleViewSet from .views_admin import AdminArticleCategoryViewSet, AdminArticleContentViewSet, AdminArticleViewSet
# Helper function to cache public views
def cached_view(view_func):
return cache_page(60 * 60 * 2, key_prefix='article_api')(vary_on_headers('Accept-Language')(view_func))
# Helper to conditionally cache list views (bypasses cache when is_bookmark=true)
def conditional_cached_view(timeout, key_prefix):
def decorator(view_func):
cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func))
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1'):
return view_func(request, *args, **kwargs)
return cached_func(request, *args, **kwargs)
return _wrapped_view
return decorator
app_name = 'article' app_name = 'article'
admin_router = SimpleRouter() admin_router = SimpleRouter()
@ -20,11 +40,11 @@ dovodi_router.register(r'collections', DovodiArticleCollectionViewSet, basename=
urlpatterns = [ urlpatterns = [
path('dovodi/', include(dovodi_router.urls)), path('dovodi/', include(dovodi_router.urls)),
path('categories/', ArticleCategoryListAPIView.as_view(), name='category-list'),
path('pinned-collections/', PinnedArticleCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleArticleCollectionListView.as_view(), name='collection-list'),
path('categories/', cached_view(ArticleCategoryListAPIView.as_view()), name='category-list'),
path('pinned-collections/', cached_view(PinnedArticleCollectionListView.as_view()), name='pinned-collection-list'),
path('collections/', cached_view(MiddleArticleCollectionListView.as_view()), name='collection-list'),
path('list/', ArticleListAPIView.as_view(), name='article-list'),
path('list/', conditional_cached_view(60 * 30, 'article_list')(ArticleListAPIView.as_view()), name='article-list'),
path('detail/<str:slug>/', ArticleDetailAPIView.as_view(), name='article-detail'), path('detail/<str:slug>/', ArticleDetailAPIView.as_view(), name='article-detail'),
re_path(r'detail/(?P<slug>.+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'), re_path(r'detail/(?P<slug>.+)/$', ArticleDetailAPIView.as_view(), name='podcast-detail'),
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),

7
apps/article/views.py

@ -72,14 +72,9 @@ class PinnedArticleCollectionListView(generics.ListAPIView):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
categories_count = ArticleCategory.objects.filter(status=True).count() categories_count = ArticleCategory.objects.filter(status=True).count()
from apps.bookmark.models import Bookmark
bookmarks_count = Bookmark.objects.filter(
service=Bookmark.ServiceChoices.ARTICLE,
).count()
info = { info = {
"categories_count": categories_count, "categories_count": categories_count,
"bookmarks_count": bookmarks_count,
} }
data = { data = {
@ -199,7 +194,7 @@ class ArticleListAPIView(generics.ListAPIView):
# Filter by bookmarks if provided # Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark == 'true': if is_bookmark == 'true':
# Import Bookmark model here to avoid circular imports # Import Bookmark model here to avoid circular imports
from apps.bookmark.models import Bookmark from apps.bookmark.models import Bookmark

12
apps/dobodbi_calendar/urls.py

@ -1,8 +1,14 @@
from django.urls import include, path from django.urls import include, path
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from apps.dobodbi_calendar.views import CalendarList, AdjustmentConfigView, OccasionsList from apps.dobodbi_calendar.views import CalendarList, AdjustmentConfigView, OccasionsList
from apps.dobodbi_calendar.views_admin import AdminCalendarOccasionViewSet from apps.dobodbi_calendar.views_admin import AdminCalendarOccasionViewSet
# Helper function for caching calendar endpoints
def cached_view(view_func):
return cache_page(60 * 60 * 6, key_prefix='calendar_api')(vary_on_headers('Accept-Language')(view_func))
admin_router = SimpleRouter() admin_router = SimpleRouter()
admin_router.register(r'occasions', AdminCalendarOccasionViewSet, basename='admin-calendar-occasions') admin_router.register(r'occasions', AdminCalendarOccasionViewSet, basename='admin-calendar-occasions')
@ -11,7 +17,7 @@ for prefix, viewset, basename in admin_router.registry:
viewset.swagger_schema = None viewset.swagger_schema = None
urlpatterns = [ urlpatterns = [
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),
path('sync-occasions/', CalendarList.as_view()),
path('adjustemnts/', AdjustmentConfigView.as_view()),
path('occasions/', OccasionsList.as_view()),
path('sync-occasions/', cached_view(CalendarList.as_view())),
path('adjustemnts/', cached_view(AdjustmentConfigView.as_view())),
path('occasions/', cached_view(OccasionsList.as_view())),
] ]

25
apps/hadis/serializers/hadis.py

@ -303,7 +303,8 @@ class HadisListSerializer(serializers.ModelSerializer):
formatted = format_address_output(obj.address) formatted = format_address_output(obj.address)
if formatted: if formatted:
return 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: if first_ref and hasattr(first_ref, 'address') and first_ref.address:
return format_address_output(first_ref.address) return format_address_output(first_ref.address)
return [] return []
@ -315,7 +316,7 @@ class HadisListSerializer(serializers.ModelSerializer):
request = self.context.get('request') request = self.context.get('request')
images_list = [] images_list = []
for ref in obj.references.all(): 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 url = None
if img.thumbnail: if img.thumbnail:
url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url) 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'] fields = ['id', 'title', 'text', 'translation', 'share_link', 'slug', 'address', 'images']
def get_address(self, obj): 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): def get_images(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -1175,7 +1177,8 @@ class HadisCorrectionSerializer(serializers.ModelSerializer):
return book_mark.get('is_bookmarked', False) return book_mark.get('is_bookmarked', False)
def get_address(self, obj): 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): def get_images(self, obj):
request = self.context.get('request') 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} return {"id": obj.hadis.id, "number": obj.hadis.number, "slug": obj.hadis.slug}
def get_address(self, obj): 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): def get_images(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -1505,7 +1509,8 @@ class HadisInterpretationDetailSerializer(serializers.ModelSerializer):
} }
def get_address(self, obj): 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): def get_images(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -1543,7 +1548,8 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
} }
def get_address(self, obj): 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): def get_images(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -1590,7 +1596,8 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
if formatted: if formatted:
return formatted return formatted
# 2. Fallback: references address # 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: if first_ref and hasattr(first_ref, 'address') and first_ref.address:
return format_address_output(first_ref.address) return format_address_output(first_ref.address)
return [] return []
@ -1602,7 +1609,7 @@ class HadisSourceDetailsSerializer(serializers.ModelSerializer):
request = self.context.get('request') request = self.context.get('request')
images_list = [] images_list = []
for ref in obj.references.all(): 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 url = None
if img.thumbnail: if img.thumbnail:
url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url) url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url)

86
apps/hadis/urls.py

@ -70,80 +70,80 @@ for prefix, viewset, basename in admin_router.registry:
urlpatterns = [ urlpatterns = [
path('sync/authors/', BookAuthorSyncView.as_view(), name='sync-author-list'),
path('authors/<str:author_slug>/references/', AuthorReferencesListView.as_view(), name='author-references'),
path('authors/<str:author_slug>/', 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/<str:author_slug>/references/', cached_view(AuthorReferencesListView.as_view()), name='author-references'),
path('authors/<str:author_slug>/', cached_view(BookAuthorDetailView.as_view()), name='author-detail'),
path('authors/', cached_view(BookAuthorListView.as_view()), name='author-list'),
# Admin endpoints # Admin endpoints
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),
# Most specific first (with parameters) # 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/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) # Category paths (more specific first)
path('categories/tree/', HadisCategoryTreeNormalView.as_view(), name='hadis-category-tree-normal'),
path('categories/<str:sect_type>/<str:slug>/<str:source_type>/', HadisCategorySelectBySectSourceView.as_view(), name='categories-tree-by-sect-source'),
path('categories/<str:sect_type>/<str:slug>/', HadisCategorySelectBySectView.as_view(), name='categories-tree-by-sect'),
path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'),
path('categories/<str:sect_type>/<str:slug>/<str:source_type>/', cached_view(HadisCategorySelectBySectSourceView.as_view()), name='categories-tree-by-sect-source'),
path('categories/<str:sect_type>/<str:slug>/', cached_view(HadisCategorySelectBySectView.as_view()), name='categories-tree-by-sect'),
path('categories/<str:sect_type>/', CategoriesBySectView.as_view(), name='categories-by-sect'), path('categories/<str:sect_type>/', 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 # Hadis paths
path('category/<str:category_slug>/xmind/', HadisCategoryXMindView.as_view(), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/xmind/', cached_view(HadisCategoryXMindView.as_view()), name='hadis-category-xmind'), # ← Must be before other category paths
path('category/<str:category_slug>/corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'), path('category/<str:category_slug>/corrections/', CategoryHadisCorrectionsView.as_view(), name='category-hadis-corrections'),
path('category/<str:category_slug>/', HadisListView.as_view(), name='hadis-list'), path('category/<str:category_slug>/', 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'), path('arguments/', HadisMainListView.as_view(), name='hadis-main-list'),
# Narrator paths # Narrator paths
path('narrators/filters/', TransmitterFiltersView.as_view(), name='narrator-filters'),
path('narrators/<str:narrator_slug>/teachers/', NarratorTeachersView.as_view(), name='narrator-teachers'),
path('narrators/<str:narrator_slug>/students/', NarratorStudentsView.as_view(), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments/', NarratorArgumentsListView.as_view(), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions/', TransmitterOpinionView.as_view(), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts/', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/', 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/<str:narrator_slug>/teachers/', cached_view(NarratorTeachersView.as_view()), name='narrator-teachers'),
path('narrators/<str:narrator_slug>/students/', cached_view(NarratorStudentsView.as_view()), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments/', cached_view(NarratorArgumentsListView.as_view()), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions/', cached_view(TransmitterOpinionView.as_view()), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts/', cached_view(TransmitterOriginalTextView.as_view()), name='narrator-original-texts'),
path('narrators/<str:narrator_slug>/', cached_view(TransmitterDetailView.as_view()), name='narrator-detail'),
path('narrators/', cached_view(TransmitterView.as_view()), name='narrators'),
# Reference paths # Reference paths
path('v2/references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts-v2'),
path('v2/references/<str:slug>/', BookReferenceV2DetailView.as_view(), name='reference-detail-v2'),
path('v2/references/', BookReferenceV2ListView.as_view(), name='reference-list-v2'),
path('references/<str:reference_slug>/excerpts/', ReferenceExcerptsListView.as_view(), name='reference-excerpts'),
path('references/<str:reference_slug>/', BookDetailView.as_view(), name='reference-detail'),
path('references/<str:reference_slug>', BookDetailView.as_view(), name='reference-detail-no-slash'),
path('references/', BookReferencesView.as_view(), name='references'),
path('v2/references/<str:reference_slug>/excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts-v2'),
path('v2/references/<str:slug>/', cached_view(BookReferenceV2DetailView.as_view()), name='reference-detail-v2'),
path('v2/references/', cached_view(BookReferenceV2ListView.as_view()), name='reference-list-v2'),
path('references/<str:reference_slug>/excerpts/', cached_view(ReferenceExcerptsListView.as_view()), name='reference-excerpts'),
path('references/<str:reference_slug>/', cached_view(BookDetailView.as_view()), name='reference-detail'),
path('references/<str:reference_slug>', 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) # Hadis detail paths (with slug, more specific)
path('<str:hadis_slug>/source-details/', HadisSourceDetailsView.as_view(), name='hadis-source-details'),
path('<str:hadis_slug>/sources/', HadisSourceDetailsView.as_view(), name='hadis-sources'),
path('<str:hadis_slug>/source-details/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-source-details'),
path('<str:hadis_slug>/sources/', cached_view(HadisSourceDetailsView.as_view()), name='hadis-sources'),
path('<str:hadis_slug>/detail/', HadisDetailView.as_view(), name='hadis-detail'), path('<str:hadis_slug>/detail/', HadisDetailView.as_view(), name='hadis-detail'),
path('<str:hadis_slug>/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'),
path('<str:hadis_slug>/transmitters/', cached_view(HadisTransmittersView.as_view()), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', cached_view(HadisLayersView.as_view()), name='hadis-layers'),
path('<str:hadis_slug>/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'), path('<str:hadis_slug>/corrections/', HadisCorrectionsView.as_view(), name='hadis-corrections'),
path('<str:hadis_slug>/interprets/', HadisInterpretsListView.as_view(), name='hadis-interprets'),
path('<str:hadis_slug>/interprets/', cached_view(HadisInterpretsListView.as_view()), name='hadis-interprets'),
path('<str:hadis_slug>/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST path('<str:hadis_slug>/', HadisBasicView.as_view(), name='hadis-basic'), # ← Least specific LAST
# path('test/test-deploy',test_deploy , name='test'), # path('test/test-deploy',test_deploy , name='test'),
# path('debug-headers',debug_headers , name='headers'), # path('debug-headers',debug_headers , name='headers'),
# Interpretation Detail Endpoint (تفسیر بر اساس آی‌دی) # Interpretation Detail Endpoint (تفسیر بر اساس آی‌دی)
path('interpretations/<str:interpretation_slug>/', HadisInterpretationDetailView.as_view(), name='interpretation-detail'),
path('interpretations/<str:interpretation_slug>/', cached_view(HadisInterpretationDetailView.as_view()), name='interpretation-detail'),
# Correction Detail Endpoint (تصحیح بر اساس اسلاگ) # Correction Detail Endpoint (تصحیح بر اساس اسلاگ)
path('corrections/<str:correction_slug>/', HadisCorrectionDetailView.as_view(), name='correction-detail'),
path('corrections/<str:correction_slug>/', cached_view(HadisCorrectionDetailView.as_view()), name='correction-detail'),
# Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ) # Original Text Detail Endpoint (متن اصلی راوی بر اساس اسلاگ)
path('original-texts/<str:original_text_slug>/', TransmitterOriginalTextDetailView.as_view(), name='original-text-detail'),
path('original-texts/<str:original_text_slug>/', cached_view(TransmitterOriginalTextDetailView.as_view()), name='original-text-detail'),
] ]

4
apps/hadis/views/hadis.py

@ -146,7 +146,7 @@ class HadisSyncView(ListAPIView):
# 👇 اضافه شدن واکشی عمیق برای تصحیحات # 👇 اضافه شدن واکشی عمیق برای تصحیحات
Prefetch( Prefetch(
'hadiscorrection_set', 'hadiscorrection_set',
queryset=HadisCorrection.objects.prefetch_related(
queryset=HadisCorrection.objects.select_related('hadis').prefetch_related(
'references__book_reference__author', 'references__book_reference__author',
'references__edition', 'references__edition',
'references__book_volume', 'references__book_volume',
@ -156,7 +156,7 @@ class HadisSyncView(ListAPIView):
# 👇 اضافه شدن واکشی عمیق برای تفاسیر # 👇 اضافه شدن واکشی عمیق برای تفاسیر
Prefetch( Prefetch(
'category__interpretations', 'category__interpretations',
queryset=HadisInterpretation.objects.prefetch_related(
queryset=HadisInterpretation.objects.select_related('category').prefetch_related(
'references__book_reference__author', 'references__book_reference__author',
'references__edition', 'references__edition',
'references__book_volume', 'references__book_volume',

2
apps/library/serializers.py

@ -185,7 +185,7 @@ class BookSerializer(serializers.ModelSerializer):
# user = User.objects.get(email='[email protected]') # user = User.objects.get(email='[email protected]')
# # Get the current user from the request context # # Get the current user from the request context
request = self.context.get('request') request = self.context.get('request')
user = request.user if request and request.user.is_authenticated else None
user = request.user if request and getattr(request.user, 'is_authenticated', False) else None
if not user: if not user:
return { return {

31
apps/library/urls.py

@ -1,4 +1,6 @@
from django.urls import include, path from django.urls import include, path
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from apps.library.views import ( from apps.library.views import (
@ -13,6 +15,25 @@ from apps.library.views import (
DownloadedBooksListView, DownloadedBooksListView,
BookDownloadCreateAPIView, BookDownloadCreateAPIView,
) )
from functools import wraps
# Helper function to cache public views
def cached_view(view_func):
return cache_page(60 * 60 * 2, key_prefix='library_api')(vary_on_headers('Accept-Language')(view_func))
# Helper to conditionally cache list views (bypasses cache when is_bookmark=true)
def conditional_cached_view(timeout, key_prefix):
def decorator(view_func):
cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func))
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1'):
return view_func(request, *args, **kwargs)
return cached_func(request, *args, **kwargs)
return _wrapped_view
return decorator
from apps.library.views_admin import ( from apps.library.views_admin import (
AdminAuthorViewSet, AdminAuthorViewSet,
AdminBookViewSet, AdminBookViewSet,
@ -40,13 +61,13 @@ for prefix, viewset, basename in admin_router.registry:
urlpatterns = [ urlpatterns = [
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),
path('dovodi/', include(dovodi_router.urls)), path('dovodi/', include(dovodi_router.urls)),
path('categories/', CategoryListView.as_view(), name='category-list'),
path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleBookCollectionListView.as_view(), name='collection-list'),
path('authors/', AuthorListView.as_view(), name='author-list'),
path('categories/', cached_view(CategoryListView.as_view()), name='category-list'),
path('pinned-collections/', cached_view(PinnedBookCollectionListView.as_view()), name='pinned-collection-list'),
path('collections/', cached_view(MiddleBookCollectionListView.as_view()), name='collection-list'),
path('authors/', cached_view(AuthorListView.as_view()), name='author-list'),
path('authors/<str:slug>/', AuthorDetailView.as_view(), name='author-detail'), path('authors/<str:slug>/', AuthorDetailView.as_view(), name='author-detail'),
path('authors/<str:slug>/books/', AuthorBookListView.as_view(), name='author-books'), path('authors/<str:slug>/books/', AuthorBookListView.as_view(), name='author-books'),
path('books/', BookListView.as_view(), name='book-list'),
path('books/', conditional_cached_view(60 * 30, 'library_books')(BookListView.as_view()), name='book-list'),
path('books/<str:slug>/', BookDetailView.as_view(), name='book-detail'), path('books/<str:slug>/', BookDetailView.as_view(), name='book-detail'),
path('books/downloaded/', DownloadedBooksListView.as_view(), name='downloaded-books-list'), path('books/downloaded/', DownloadedBooksListView.as_view(), name='downloaded-books-list'),
path('books/download/', BookDownloadCreateAPIView.as_view(), name='book-download'), path('books/download/', BookDownloadCreateAPIView.as_view(), name='book-download'),

23
apps/library/views.py

@ -75,14 +75,9 @@ class PinnedBookCollectionListView(ListAPIView):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
categories_count = Category.objects.filter(status=True).count() categories_count = Category.objects.filter(status=True).count()
from apps.bookmark.models import Bookmark
bookmarks_count = Bookmark.objects.filter(
service=Bookmark.ServiceChoices.LIBRARY,
).count()
downloads_count = BookDownload.objects.all().count() downloads_count = BookDownload.objects.all().count()
info = { info = {
"categories_count": categories_count, "categories_count": categories_count,
"bookmarks_count": bookmarks_count,
"downloads_count": downloads_count "downloads_count": downloads_count
} }
data = { data = {
@ -110,6 +105,20 @@ class BookListView(ListAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_serializer_context(self):
context = super().get_serializer_context()
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark not in ('true', '1') and 'request' in context:
from django.contrib.auth.models import AnonymousUser
class PublicRequestWrapper:
def __init__(self, req):
self._req = req
self.user = AnonymousUser()
def __getattr__(self, item):
return getattr(self._req, item)
context['request'] = PublicRequestWrapper(context['request'])
return context
def get_queryset(self): def get_queryset(self):
queryset = Book.objects.filter(status=True).select_related('author') queryset = Book.objects.filter(status=True).select_related('author')
@ -148,8 +157,8 @@ class BookListView(ListAPIView):
# queryset = queryset.filter(collections__in=bottom_collections) # queryset = queryset.filter(collections__in=bottom_collections)
# Filter by bookmarked books if requested # Filter by bookmarked books if requested
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true' and self.request.user.is_authenticated:
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1') and self.request.user.is_authenticated:
# Import Bookmark model here to avoid circular imports # Import Bookmark model here to avoid circular imports
from apps.bookmark.models import Bookmark from apps.bookmark.models import Bookmark

29
apps/podcast/urls.py

@ -1,8 +1,29 @@
from functools import wraps
from django.urls import path, re_path , include from django.urls import path, re_path , include
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from .views import * from .views import *
from .views_admin import * from .views_admin import *
# Helper function to cache public views
def cached_view(view_func):
return cache_page(60 * 60 * 2, key_prefix='podcast_api')(vary_on_headers('Accept-Language')(view_func))
# Helper to conditionally cache list views (bypasses cache when is_bookmark=true or in_playlist=true)
def conditional_cached_view(timeout, key_prefix):
def decorator(view_func):
cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func))
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower()
in_playlist = request.GET.get('in_playlist', '').lower()
if is_bookmark in ('true', '1') or in_playlist in ('true', '1'):
return view_func(request, *args, **kwargs)
return cached_func(request, *args, **kwargs)
return _wrapped_view
return decorator
admin_router = SimpleRouter() admin_router = SimpleRouter()
admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast') admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast')
admin_router.register(r'playlists', AdminPodcastPlaylistViewSet, basename='admin-podcast-playlist') admin_router.register(r'playlists', AdminPodcastPlaylistViewSet, basename='admin-podcast-playlist')
@ -23,11 +44,11 @@ app_name = 'podcast'
urlpatterns = [ urlpatterns = [
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),
path('dovodi/', include(dovodi_router.urls)), path('dovodi/', include(dovodi_router.urls)),
path('categories/', PodcastCategoryListAPIView.as_view(), name='category-list'),
path('pinned-collections/', PinnedPodcastCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddlePodcastCollectionListView.as_view(), name='collection-list'),
path('categories/', cached_view(PodcastCategoryListAPIView.as_view()), name='category-list'),
path('pinned-collections/', cached_view(PinnedPodcastCollectionListView.as_view()), name='pinned-collection-list'),
path('collections/', cached_view(MiddlePodcastCollectionListView.as_view()), name='collection-list'),
path('list/', PodcastListAPIView.as_view(), name='podcast-list'),
path('list/', conditional_cached_view(60 * 30, 'podcast_list')(PodcastListAPIView.as_view()), name='podcast-list'),
re_path(r'detail/(?P<slug>[\w-]+)/$', PodcastDetailAPIView.as_view(), name='podcast-detail'), re_path(r'detail/(?P<slug>[\w-]+)/$', PodcastDetailAPIView.as_view(), name='podcast-detail'),
# User playlist endpoints # User playlist endpoints

10
apps/podcast/views.py

@ -84,17 +84,9 @@ class PinnedPodcastCollectionListView(generics.ListAPIView):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
categories_count = PodcastCategory.objects.filter(status=True).count() categories_count = PodcastCategory.objects.filter(status=True).count()
# Count podcasts in the user's playlist
user_playlist_count = 0
if request.user.is_authenticated:
user_playlist_count = UserPlaylist.objects.filter(
user=request.user,
status=True
).count()
info = { info = {
"categories_count": categories_count, "categories_count": categories_count,
"user_playlist_count": user_playlist_count,
} }
data = { data = {
"count": response.data.get("count"), "count": response.data.get("count"),
@ -219,7 +211,7 @@ class PodcastListAPIView(generics.ListAPIView):
) )
# Filter by bookmarks if provided # Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark == 'true': if is_bookmark == 'true':
from apps.bookmark.models import Bookmark from apps.bookmark.models import Bookmark

126
apps/video/management/commands/assign_random_videos_to_playlists.py

@ -0,0 +1,126 @@
import random
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.video.models import Video, VideoPlaylist, PlaylistItem
class Command(BaseCommand):
help = 'Assign random videos from the database to all video playlists so each has at least 7-8 items'
def add_arguments(self, parser):
parser.add_argument(
'--min-items',
type=int,
default=7,
help='Minimum number of videos per playlist (default: 7)'
)
parser.add_argument(
'--max-items',
type=int,
default=8,
help='Maximum number of videos per playlist (default: 8)'
)
parser.add_argument(
'--clear-existing',
action='store_true',
help='Clear existing playlist items before assigning new random videos'
)
def handle(self, *args, **options):
min_items = options['min_items']
max_items = options['max_items']
clear_existing = options['clear_existing']
if max_items < min_items:
max_items = min_items
videos = list(Video.objects.filter(status=True))
if not videos:
videos = list(Video.objects.all())
if not videos:
self.stdout.write(self.style.ERROR('No videos found in the database!'))
return
playlists = list(VideoPlaylist.objects.all())
if not playlists:
self.stdout.write(self.style.ERROR('No video playlists found in the database!'))
return
self.stdout.write(self.style.NOTICE(
f'Found {len(videos)} videos and {len(playlists)} playlists.'
))
self.stdout.write(f'Target items per playlist: {min_items} to {max_items}\n')
total_created = 0
with transaction.atomic():
for playlist in playlists:
if clear_existing:
playlist.playlist_items.all().delete()
existing_video_ids = set()
current_count = 0
else:
existing_items = list(playlist.playlist_items.all())
existing_video_ids = {item.video_id for item in existing_items}
current_count = len(existing_video_ids)
target_count = random.randint(min_items, max_items)
# If the playlist already has at least min_items and clear_existing is False, skip or top up
needed_count = target_count - current_count
if needed_count <= 0:
self.stdout.write(
f' [=] Playlist #{playlist.id} already has {current_count} videos (>= {min_items}).'
)
continue
# Available videos not already in this playlist
available_videos = [v for v in videos if v.id not in existing_video_ids]
if not available_videos:
self.stdout.write(
self.style.WARNING(
f' [!] Playlist #{playlist.id}: No more unique videos available to add.'
)
)
continue
# Select random sample of available videos
sample_size = min(needed_count, len(available_videos))
selected_videos = random.sample(available_videos, sample_size)
# Determine starting priority
start_priority = current_count + 1
new_items = []
for idx, video in enumerate(selected_videos):
new_items.append(
PlaylistItem(
playlist=playlist,
video=video,
priority=start_priority + idx
)
)
PlaylistItem.objects.bulk_create(new_items)
total_created += len(new_items)
# Update total duration of the playlist
try:
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=['total_time'])
except Exception as e:
self.stdout.write(self.style.WARNING(f' Warning updating total_time: {e}'))
new_total = current_count + len(new_items)
self.stdout.write(
self.style.SUCCESS(
f' [+] Playlist #{playlist.id}: Added {len(new_items)} videos (Total: {new_total})'
)
)
self.stdout.write(
self.style.SUCCESS(
f'\nDone! Successfully assigned {total_created} video items across {len(playlists)} playlists.'
)
)

13
apps/video/serializers.py

@ -329,17 +329,4 @@ class MiddleVideoCollectionSerializer(serializers.ModelSerializer):
status=True, status=True,
playlist_items__video__status=True playlist_items__video__status=True
).distinct().order_by('order', '-created_at') ).distinct().order_by('order', '-created_at')
# Filter by bookmarks if requested in context
is_bookmark = self.context.get('is_bookmark')
request = self.context.get('request')
if is_bookmark == 'true' and request and request.user.is_authenticated:
from apps.bookmark.models import Bookmark
bookmarked_ids = Bookmark.objects.filter(
user=request.user,
service=Bookmark.ServiceChoices.VIDEO_PLAYLIST,
status=True
).values_list('content_id', flat=True)
playlists = playlists.filter(id__in=bookmarked_ids)
return VideoPlaylistListSerializer(playlists, many=True, context=self.context).data return VideoPlaylistListSerializer(playlists, many=True, context=self.context).data

30
apps/video/urls.py

@ -1,8 +1,28 @@
from functools import wraps
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from .views import * from .views import *
from .views_admin import * from .views_admin import *
# Helper function to cache public views
def cached_view(view_func):
return cache_page(60 * 60 * 2, key_prefix='video_api')(vary_on_headers('Accept-Language')(view_func))
# Helper to conditionally cache list views (bypasses cache when is_bookmark=true)
def conditional_cached_view(timeout, key_prefix):
def decorator(view_func):
cached_func = cache_page(timeout, key_prefix=key_prefix)(vary_on_headers('Accept-Language')(view_func))
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
is_bookmark = (request.GET.get('is_bookmark') or request.GET.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1'):
return view_func(request, *args, **kwargs)
return cached_func(request, *args, **kwargs)
return _wrapped_view
return decorator
app_name = 'video' app_name = 'video'
admin_router = SimpleRouter() admin_router = SimpleRouter()
@ -23,15 +43,15 @@ dovodi_router.register(r'items', DovodiVideoItemViewSet, basename='dovodi-video-
urlpatterns = [ urlpatterns = [
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),
path('dovodi/', include(dovodi_router.urls)), path('dovodi/', include(dovodi_router.urls)),
path('categories/', VideoCategoryListAPIView.as_view(), name='category-list'),
path('pinned-collections/', PinnedVideoCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleVideoCollectionListView.as_view(), name='collection-list'),
path('categories/', cached_view(VideoCategoryListAPIView.as_view()), name='category-list'),
path('pinned-collections/', cached_view(PinnedVideoCollectionListView.as_view()), name='pinned-collection-list'),
path('collections/', cached_view(MiddleVideoCollectionListView.as_view()), name='collection-list'),
path('playlists/', VideoPlaylistListAPIView.as_view(), name='playlist-list'),
path('playlists/', conditional_cached_view(60 * 30, 'video_playlists')(VideoPlaylistListAPIView.as_view()), name='playlist-list'),
re_path(r'playlists/(?P<slug>[\w-]+)/$', VideoPlaylistDetailAPIView.as_view(), name='playlist-detail'), re_path(r'playlists/(?P<slug>[\w-]+)/$', VideoPlaylistDetailAPIView.as_view(), name='playlist-detail'),
# Keep old video endpoints for backward compatibility if needed # Keep old video endpoints for backward compatibility if needed
path('list/', VideoListAPIView.as_view(), name='video-list'),
path('list/', conditional_cached_view(60 * 30, 'video_list')(VideoListAPIView.as_view()), name='video-list'),
re_path(r'detail/(?P<slug>[\w-]+)/$', VideoDetailAPIView.as_view(), name='video-detail'), re_path(r'detail/(?P<slug>[\w-]+)/$', VideoDetailAPIView.as_view(), name='video-detail'),
path('resolve-youtube/', ResolveYouTubeStreamURLAPIView.as_view(), name='resolve-youtube'), path('resolve-youtube/', ResolveYouTubeStreamURLAPIView.as_view(), name='resolve-youtube'),
] ]

81
apps/video/views.py

@ -85,38 +85,18 @@ class PinnedVideoCollectionListView(generics.ListAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
queryset = PinnedVideoCollection.objects.filter(
return PinnedVideoCollection.objects.filter(
status=True, status=True,
display_position=VideoCollection.DisplayPosition.PINNED, display_position=VideoCollection.DisplayPosition.PINNED,
related_playlists__status=True, related_playlists__status=True,
related_playlists__playlist_items__video__status=True related_playlists__playlist_items__video__status=True
).distinct().order_by('order', '-created_at') ).distinct().order_by('order', '-created_at')
# Filter by bookmarks if requested
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true' and self.request.user.is_authenticated:
from apps.bookmark.models import Bookmark
bookmarked_ids = Bookmark.objects.filter(
user=self.request.user,
service=Bookmark.ServiceChoices.VIDEO_PLAYLIST,
status=True
).values_list('content_id', flat=True)
# Only include collections that contain at least one bookmarked playlist
queryset = queryset.filter(related_playlists__id__in=bookmarked_ids).distinct()
return queryset
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs) response = super().list(request, *args, **kwargs)
categories_count = VideoCategory.objects.filter(status=True).count() categories_count = VideoCategory.objects.filter(status=True).count()
from apps.bookmark.models import Bookmark
bookmarks_count = Bookmark.objects.filter(
service=Bookmark.ServiceChoices.VIDEO,
).count()
info = { info = {
"categories_count": categories_count, "categories_count": categories_count,
"bookmarks_count": bookmarks_count,
} }
data = { data = {
"count": response.data.get("count"), "count": response.data.get("count"),
@ -167,24 +147,7 @@ class MiddleVideoCollectionListView(generics.ListAPIView):
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
queryset = self.get_queryset() queryset = self.get_queryset()
# Pass is_bookmark to serializer context
is_bookmark = request.query_params.get('is_bookmark', '').lower()
# If is_bookmark=true, we filter the queryset to only include collections that
# have at least one bookmarked playlist
if is_bookmark == 'true' and request.user.is_authenticated:
from apps.bookmark.models import Bookmark
bookmarked_ids = Bookmark.objects.filter(
user=request.user,
service=Bookmark.ServiceChoices.VIDEO_PLAYLIST,
status=True
).values_list('content_id', flat=True)
# Filter collections that have any of these playlists
queryset = queryset.filter(related_playlists__id__in=bookmarked_ids).distinct()
serializer = self.get_serializer(queryset, many=True, context={'request': request, 'is_bookmark': is_bookmark})
serializer = self.get_serializer(queryset, many=True, context={'request': request})
return Response(serializer.data) return Response(serializer.data)
@ -261,20 +224,26 @@ class VideoPlaylistListAPIView(generics.ListAPIView):
if collection_slug: if collection_slug:
queryset = queryset.filter(collections__slug=collection_slug) queryset = queryset.filter(collections__slug=collection_slug)
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true':
# Import Bookmark model here to avoid circular imports
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1') and self.request.user.is_authenticated:
from apps.bookmark.models import Bookmark from apps.bookmark.models import Bookmark
from django.db.models import Q
# Get all bookmarked playlist IDs for the current user
bookmarked_ids = Bookmark.objects.filter(
bookmarked_playlist_ids = Bookmark.objects.filter(
user=self.request.user, user=self.request.user,
service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, service=Bookmark.ServiceChoices.VIDEO_PLAYLIST,
status=True status=True
).values_list('content_id', flat=True) ).values_list('content_id', flat=True)
# Filter playlists by these IDs
queryset = queryset.filter(id__in=bookmarked_ids)
bookmarked_video_ids = Bookmark.objects.filter(
user=self.request.user,
service=Bookmark.ServiceChoices.VIDEO,
status=True
).values_list('content_id', flat=True)
queryset = queryset.filter(
Q(id__in=bookmarked_playlist_ids) | Q(playlist_items__video__id__in=bookmarked_video_ids)
).distinct()
sort = self.request.query_params.get('sort', '-created_at') sort = self.request.query_params.get('sort', '-created_at')
allowed_sorts = [ allowed_sorts = [
'created_at', '-created_at', 'view_count', '-view_count', 'created_at', '-created_at', 'view_count', '-view_count',
@ -373,20 +342,26 @@ class VideoListAPIView(generics.ListAPIView):
if collection_slug: if collection_slug:
queryset = queryset.filter(collections__slug=collection_slug) queryset = queryset.filter(collections__slug=collection_slug)
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true':
# Import Bookmark model here to avoid circular imports
is_bookmark = (self.request.query_params.get('is_bookmark') or self.request.query_params.get('is_bookmarked') or '').lower()
if is_bookmark in ('true', '1') and self.request.user.is_authenticated:
from apps.bookmark.models import Bookmark from apps.bookmark.models import Bookmark
from django.db.models import Q
# Get all bookmarked playlist IDs for the current user
bookmarked_ids = Bookmark.objects.filter(
bookmarked_playlist_ids = Bookmark.objects.filter(
user=self.request.user, user=self.request.user,
service=Bookmark.ServiceChoices.VIDEO_PLAYLIST, service=Bookmark.ServiceChoices.VIDEO_PLAYLIST,
status=True status=True
).values_list('content_id', flat=True) ).values_list('content_id', flat=True)
# Filter playlists by these IDs
queryset = queryset.filter(id__in=bookmarked_ids)
bookmarked_video_ids = Bookmark.objects.filter(
user=self.request.user,
service=Bookmark.ServiceChoices.VIDEO,
status=True
).values_list('content_id', flat=True)
queryset = queryset.filter(
Q(id__in=bookmarked_playlist_ids) | Q(playlist_items__video__id__in=bookmarked_video_ids)
).distinct()
sort = self.request.query_params.get('sort', '-created_at') sort = self.request.query_params.get('sort', '-created_at')
allowed_sorts = [ allowed_sorts = [
'created_at', '-created_at', 'view_count', '-view_count', 'created_at', '-created_at', 'view_count', '-view_count',

1
config/settings/base.py

@ -117,6 +117,7 @@ AUTH_USER_MODEL = "account.User"
MIDDLEWARE = [ MIDDLEWARE = [
'config.middleware.site_middleware.SiteMiddleware', # Must be first to route by domain 'config.middleware.site_middleware.SiteMiddleware', # Must be first to route by domain
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.middleware.gzip.GZipMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',

Loading…
Cancel
Save