Browse Source

feat(cache): implement automatic cache invalidation and smart cache bypass

- Add utils/cache.py with invalidate_cache_patterns and smart_cached_view
- Support cache bypass on Cache-Control: no-cache headers and query params
- Add automatic signals (post_save, post_delete, m2m_changed) for library, video, podcast, article, calendar, and hadis
- Hook cache invalidation in custom admin viewset actions
master
Mohsen Taba 4 days ago
parent
commit
0dbdad0880
  1. 4
      apps/article/apps.py
  2. 39
      apps/article/signals.py
  3. 23
      apps/article/urls.py
  4. 2
      apps/article/views_admin.py
  5. 4
      apps/dobodbi_calendar/apps.py
  6. 20
      apps/dobodbi_calendar/signals.py
  7. 8
      apps/dobodbi_calendar/urls.py
  8. 26
      apps/hadis/signals.py
  9. 8
      apps/hadis/urls.py
  10. 4
      apps/library/apps.py
  11. 27
      apps/library/signals.py
  12. 21
      apps/library/urls.py
  13. 4
      apps/library/views_admin.py
  14. 3
      apps/podcast/apps.py
  15. 41
      apps/podcast/signals.py
  16. 26
      apps/podcast/urls.py
  17. 7
      apps/podcast/views_admin.py
  18. 4
      apps/video/apps.py
  19. 41
      apps/video/signals.py
  20. 23
      apps/video/urls.py
  21. 7
      apps/video/views_admin.py
  22. 69
      utils/cache.py

4
apps/article/apps.py

@ -4,3 +4,7 @@ from django.apps import AppConfig
class ArticleConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.article'
def ready(self):
import apps.article.signals

39
apps/article/signals.py

@ -0,0 +1,39 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from utils.cache import invalidate_cache_patterns
from .models import (
Article,
ArticleCategory,
ArticleCollection,
ArticleContent,
ArticleInCollection,
)
TARGET_MODELS = [
ArticleCategory,
ArticleCollection,
Article,
ArticleContent,
ArticleInCollection,
]
def invalidate_article_cache():
"""
Invalidates all Redis cache keys for article API views.
"""
return invalidate_cache_patterns("*article_api*", "*article_list*")
@receiver(post_save)
@receiver(post_delete)
def clear_article_cache_on_save_or_delete(sender, instance, **kwargs):
if sender in TARGET_MODELS:
invalidate_article_cache()
@receiver(m2m_changed)
def clear_article_cache_on_m2m(sender, instance, action, **kwargs):
if action in ('post_add', 'post_remove', 'post_clear'):
if isinstance(instance, (Article, ArticleCollection, ArticleCategory)):
invalidate_article_cache()

23
apps/article/urls.py

@ -1,27 +1,12 @@
from functools import wraps
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 utils.cache import smart_cached_view
from .views import *
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
# Helper function to cache public views with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 2, key_prefix='article_api')
conditional_cached_view = smart_cached_view
app_name = 'article'

2
apps/article/views_admin.py

@ -10,6 +10,7 @@ from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import Article, ArticleCategory, ArticleContent
from .signals import invalidate_article_cache
from .serializers_admin import (
AdminArticleCategorySerializer,
AdminArticleContentSerializer,
@ -88,4 +89,5 @@ class AdminArticleContentViewSet(ModelViewSet):
for index, content_id in enumerate(content_ids, start=1):
ArticleContent.objects.filter(id=content_id).update(priority=index)
invalidate_article_cache()
return Response({"status": "success"})

4
apps/dobodbi_calendar/apps.py

@ -4,3 +4,7 @@ from django.apps import AppConfig
class DobodbiCalendarConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.dobodbi_calendar'
def ready(self):
import apps.dobodbi_calendar.signals

20
apps/dobodbi_calendar/signals.py

@ -0,0 +1,20 @@
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from utils.cache import invalidate_cache_patterns
from .models import CalendarOccasions
TARGET_MODELS = [CalendarOccasions]
def invalidate_calendar_cache():
"""
Invalidates all Redis cache keys for calendar API views.
"""
return invalidate_cache_patterns("*calendar_api*")
@receiver(post_save)
@receiver(post_delete)
def clear_calendar_cache_on_save_or_delete(sender, instance, **kwargs):
if sender in TARGET_MODELS:
invalidate_calendar_cache()

8
apps/dobodbi_calendar/urls.py

@ -1,13 +1,11 @@
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 utils.cache import smart_cached_view
from apps.dobodbi_calendar.views import CalendarList, AdjustmentConfigView, OccasionsList
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))
# Helper function for caching calendar endpoints with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 6, key_prefix='calendar_api')
admin_router = SimpleRouter()
admin_router.register(r'occasions', AdminCalendarOccasionViewSet, basename='admin-calendar-occasions')

26
apps/hadis/signals.py

@ -1,8 +1,6 @@
# hadith_app/signals.py
from django.db.models.signals import post_save, post_delete
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from django.core.cache import cache
from utils.cache import invalidate_cache_patterns
from .models import *
# 1. Define all models that affect the list
@ -20,18 +18,8 @@ TARGET_MODELS = [
def invalidate_hadis_cache():
"""
Deletes all Redis cache keys matching '*hadis_api*'.
Falls back to cache.clear() if cache backend doesn't support delete_pattern.
"""
try:
if hasattr(cache, 'delete_pattern'):
deleted = cache.delete_pattern("*hadis_api*")
print(f"Cache cleared for hadis_api (deleted {deleted} keys)!")
else:
cache.clear()
print("Cache cleared for hadis_api (fallback clear)!")
except Exception as e:
# Fail silently or log error, don't crash the transaction
print(f"Cache clear failed: {e}")
return invalidate_cache_patterns("*hadis_api*")
@receiver(post_save)
@ -42,4 +30,10 @@ def clear_hadis_cache(sender, instance, **kwargs):
"""
if sender in TARGET_MODELS:
invalidate_hadis_cache()
print(f"Cache cleared for {sender.__name__} update!")
@receiver(m2m_changed)
def clear_hadis_cache_on_m2m(sender, instance, action, **kwargs):
if action in ('post_add', 'post_remove', 'post_clear'):
if isinstance(instance, tuple(TARGET_MODELS)):
invalidate_hadis_cache()

8
apps/hadis/urls.py

@ -23,12 +23,10 @@ from .views_admin import (
AdminContentReleaseViewSet
)
from .views.reference_v2 import BookAuthorListView, BookAuthorDetailView, AuthorReferencesListView, BookAuthorSyncView
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
from utils.cache import smart_cached_view
# Helper function to avoid ugly nesting
def cached_view(view_func):
return cache_page(60*60*2,key_prefix='hadis_api')(vary_on_headers('Accept-Language')(view_func))
# Helper function to cache public views with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 2, key_prefix='hadis_api')
admin_router = SimpleRouter()
admin_router.register(r'hadises', AdminHadisViewSet, basename='admin-hadises')

4
apps/library/apps.py

@ -7,3 +7,7 @@ class LibraryConfig(AppConfig):
name = 'apps.library'
verbose_name = _('Library')
icon = 'mi-library-books'
def ready(self):
import apps.library.signals

27
apps/library/signals.py

@ -0,0 +1,27 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from utils.cache import invalidate_cache_patterns
from .models import Author, Book, BookCollection, Category
TARGET_MODELS = [Author, Book, BookCollection, Category]
def invalidate_library_cache():
"""
Invalidates all Redis cache keys for library API views.
"""
return invalidate_cache_patterns("*library_api*", "*library_books*")
@receiver(post_save)
@receiver(post_delete)
def clear_library_cache_on_save_or_delete(sender, instance, **kwargs):
if sender in TARGET_MODELS:
invalidate_library_cache()
@receiver(m2m_changed)
def clear_library_cache_on_m2m(sender, instance, action, **kwargs):
if action in ('post_add', 'post_remove', 'post_clear'):
if isinstance(instance, (Book, BookCollection, Category)):
invalidate_library_cache()

21
apps/library/urls.py

@ -16,24 +16,11 @@ from apps.library.views import (
BookDownloadCreateAPIView,
)
from functools import wraps
from utils.cache import smart_cached_view
# 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
# Helper function to cache public views with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 2, key_prefix='library_api')
conditional_cached_view = smart_cached_view
from apps.library.views_admin import (
AdminAuthorViewSet,
AdminBookViewSet,

4
apps/library/views_admin.py

@ -10,6 +10,7 @@ from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import Author, Book, BookCollection, Category
from .signals import invalidate_library_cache
from .serializers_admin import (
AdminAuthorSerializer,
AdminBookDetailSerializer,
@ -74,6 +75,7 @@ class AdminLibraryCollectionViewSet(ModelViewSet):
if remove_ids:
collection.related_collections.remove(*remove_ids)
invalidate_library_cache()
return Response({"status": "success", "attached_count": collection.related_collections.count()})
@ -113,6 +115,7 @@ class DovodiAdminLibraryCollectionViewSet(ModelViewSet):
if remove_ids:
collection.related_collections.remove(*remove_ids)
invalidate_library_cache()
return Response({"status": "success", "attached_count": collection.related_collections.count()})
@ -147,6 +150,7 @@ class AdminAuthorViewSet(ModelViewSet):
if remove_ids:
Book.objects.filter(id__in=remove_ids, author=author).update(author=None)
invalidate_library_cache()
return Response({
"status": "success",
"attached_count": author.books.count()

3
apps/podcast/apps.py

@ -5,4 +5,5 @@ class PodcastConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.podcast'
def ready(self):
import apps.podcast.signals

41
apps/podcast/signals.py

@ -0,0 +1,41 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from utils.cache import invalidate_cache_patterns
from .models import (
PlaylistItem,
Podcast,
PodcastCategory,
PodcastCollection,
PodcastPlaylist,
PodcastPlaylistInCollection,
)
TARGET_MODELS = [
PodcastCategory,
PodcastCollection,
Podcast,
PodcastPlaylist,
PodcastPlaylistInCollection,
PlaylistItem,
]
def invalidate_podcast_cache():
"""
Invalidates all Redis cache keys for podcast API views.
"""
return invalidate_cache_patterns("*podcast_api*", "*podcast_list*")
@receiver(post_save)
@receiver(post_delete)
def clear_podcast_cache_on_save_or_delete(sender, instance, **kwargs):
if sender in TARGET_MODELS:
invalidate_podcast_cache()
@receiver(m2m_changed)
def clear_podcast_cache_on_m2m(sender, instance, action, **kwargs):
if action in ('post_add', 'post_remove', 'post_clear'):
if isinstance(instance, (Podcast, PodcastPlaylist, PodcastCollection, PodcastCategory)):
invalidate_podcast_cache()

26
apps/podcast/urls.py

@ -1,28 +1,12 @@
from functools import wraps
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 django.urls import path, re_path, include
from rest_framework.routers import SimpleRouter
from utils.cache import smart_cached_view
from .views 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
# Helper function to cache public views with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 2, key_prefix='podcast_api')
conditional_cached_view = smart_cached_view
admin_router = SimpleRouter()
admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast')

7
apps/podcast/views_admin.py

@ -12,6 +12,7 @@ from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import PlaylistItem, Podcast, PodcastCollection, PodcastCategory, PodcastPlaylist, PodcastPlaylistInCollection
from .signals import invalidate_podcast_cache
from .serializers_admin import (
AdminPodcastCategorySerializer,
AdminPodcastCollectionSerializer,
@ -59,6 +60,7 @@ class AdminPodcastCollectionViewSet(ModelViewSet):
if remove_ids:
PodcastPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
invalidate_podcast_cache()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
@ -161,6 +163,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_podcast_cache()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["patch", "delete"], url_path=r"items/(?P<item_id>\d+)")
@ -172,6 +175,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
item.delete()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_podcast_cache()
return Response(status=status.HTTP_204_NO_CONTENT)
serializer = AdminPodcastPlaylistItemSerializer(
@ -184,6 +188,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_podcast_cache()
return Response(serializer.data)
@action(detail=True, methods=["post"], url_path="items/reorder")
@ -205,6 +210,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
item.priority = index
item.save(update_fields=["priority"])
invalidate_podcast_cache()
serializer = AdminPodcastPlaylistItemSerializer(
playlist.playlist_items.select_related("podcast").order_by("priority", "id"),
many=True,
@ -240,6 +246,7 @@ class AdminPodcastPlaylistViewSet(ModelViewSet):
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_podcast_cache()
serializer = AdminPodcastPlaylistItemSerializer(
playlist.playlist_items.select_related("podcast").order_by("priority", "id"),
many=True,

4
apps/video/apps.py

@ -4,3 +4,7 @@ from django.apps import AppConfig
class VideoConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.video'
def ready(self):
import apps.video.signals

41
apps/video/signals.py

@ -0,0 +1,41 @@
from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver
from utils.cache import invalidate_cache_patterns
from .models import (
PlaylistItem,
Video,
VideoCategory,
VideoCollection,
VideoPlaylist,
VideoPlaylistInCollection,
)
TARGET_MODELS = [
VideoCategory,
VideoCollection,
Video,
VideoPlaylist,
VideoPlaylistInCollection,
PlaylistItem,
]
def invalidate_video_cache():
"""
Invalidates all Redis cache keys for video API views.
"""
return invalidate_cache_patterns("*video_api*", "*video_playlists*", "*video_list*")
@receiver(post_save)
@receiver(post_delete)
def clear_video_cache_on_save_or_delete(sender, instance, **kwargs):
if sender in TARGET_MODELS:
invalidate_video_cache()
@receiver(m2m_changed)
def clear_video_cache_on_m2m(sender, instance, action, **kwargs):
if action in ('post_add', 'post_remove', 'post_clear'):
if isinstance(instance, (Video, VideoPlaylist, VideoCollection, VideoCategory)):
invalidate_video_cache()

23
apps/video/urls.py

@ -1,27 +1,12 @@
from functools import wraps
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 utils.cache import smart_cached_view
from .views 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
# Helper function to cache public views with smart invalidation and force-refresh support
cached_view = smart_cached_view(60 * 60 * 2, key_prefix='video_api')
conditional_cached_view = smart_cached_view
app_name = 'video'

7
apps/video/views_admin.py

@ -12,6 +12,7 @@ from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import PlaylistItem, Video, VideoCategory, VideoCollection, VideoPlaylist, VideoPlaylistInCollection
from .signals import invalidate_video_cache
from .serializers_admin import (
AdminVideoCategorySerializer,
AdminVideoCollectionSerializer,
@ -59,6 +60,7 @@ class AdminVideoCollectionViewSet(ModelViewSet):
if remove_ids:
VideoPlaylistInCollection.objects.filter(collection_id=collection.id, playlist_id__in=remove_ids).delete()
invalidate_video_cache()
return Response({"status": "success", "attached_count": collection.collection_playlists.count()})
@ -158,6 +160,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_video_cache()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["patch", "delete"], url_path=r"items/(?P<item_id>\d+)")
@ -169,6 +172,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
item.delete()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_video_cache()
return Response(status=status.HTTP_204_NO_CONTENT)
serializer = AdminVideoPlaylistItemSerializer(
@ -181,6 +185,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_video_cache()
return Response(serializer.data)
@action(detail=True, methods=["post"], url_path="items/reorder")
@ -202,6 +207,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
item.priority = index
item.save(update_fields=["priority"])
invalidate_video_cache()
serializer = AdminVideoPlaylistItemSerializer(
playlist.playlist_items.select_related("video").order_by("priority", "id"),
many=True,
@ -237,6 +243,7 @@ class AdminVideoPlaylistViewSet(ModelViewSet):
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
invalidate_video_cache()
serializer = AdminVideoPlaylistItemSerializer(
playlist.playlist_items.select_related("video").order_by("priority", "id"),
many=True,

69
utils/cache.py

@ -0,0 +1,69 @@
from functools import wraps
import logging
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers
logger = logging.getLogger(__name__)
def invalidate_cache_patterns(*patterns):
"""
Safely invalidates Redis cache keys matching given glob patterns.
Falls back to cache.clear() if the cache backend does not support delete_pattern.
"""
try:
if hasattr(cache, 'delete_pattern'):
total_deleted = 0
for pattern in patterns:
deleted = cache.delete_pattern(pattern)
total_deleted += deleted or 0
logger.info(f"Cache cleared for patterns {patterns} (deleted {total_deleted} keys)")
return total_deleted
else:
cache.clear()
logger.info(f"Cache cleared (fallback clear for patterns: {patterns})")
return 1
except Exception as e:
logger.error(f"Cache invalidation failed for patterns {patterns}: {e}")
return 0
def smart_cached_view(timeout=60 * 60 * 2, key_prefix=''):
"""
Smart cache decorator for API views that:
1. Caches responses per Accept-Language header using Django's cache_page.
2. Bypasses cache when:
- Cache-Control / Pragma: no-cache headers are present (e.g., Pull-to-refresh from mobile / web).
- Query parameters: no_cache=true, refresh=true, is_bookmark=true, in_playlist=true are passed.
"""
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):
# 1. Check HTTP headers for no-cache directives
cache_control = (request.META.get('HTTP_CACHE_CONTROL') or '').lower()
pragma = (request.META.get('HTTP_PRAGMA') or '').lower()
if 'no-cache' in cache_control or 'no-store' in cache_control or 'max-age=0' in cache_control or 'no-cache' in pragma:
return view_func(request, *args, **kwargs)
# 2. Check query params for explicit bypass
query_params = getattr(request, 'GET', {})
no_cache_param = (query_params.get('no_cache') or query_params.get('refresh') or '').lower()
if no_cache_param in ('1', 'true'):
return view_func(request, *args, **kwargs)
# 3. Check bookmark & playlist specific bypasses
is_bookmark = (query_params.get('is_bookmark') or query_params.get('is_bookmarked') or '').lower()
in_playlist = (query_params.get('in_playlist') or '').lower()
if is_bookmark in ('1', 'true') or in_playlist in ('1', 'true'):
return view_func(request, *args, **kwargs)
return cached_func(request, *args, **kwargs)
return _wrapped_view
return decorator
Loading…
Cancel
Save