diff --git a/articles/views.py b/articles/views.py index e5c88c0601..472fa9f3a1 100644 --- a/articles/views.py +++ b/articles/views.py @@ -1,3 +1,5 @@ +from django.conf import settings +from django.utils.decorators import method_decorator from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import viewsets from rest_framework.pagination import LimitOffsetPagination @@ -6,6 +8,7 @@ from articles.models import Article from articles.serializers import ArticleSerializer from main.constants import VALID_HTTP_METHODS +from main.utils import cache_page_for_all_users, clear_search_cache # Create your views here. @@ -37,3 +40,19 @@ class ArticleViewSet(viewsets.ModelViewSet): permission_classes = [IsAdminUser] http_method_names = VALID_HTTP_METHODS + + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + clear_search_cache() + return super().create(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + clear_search_cache() + return super().destroy(request, *args, **kwargs) diff --git a/channels/views_test.py b/channels/views_test.py index 12b4e1d7dc..00c0334e7a 100644 --- a/channels/views_test.py +++ b/channels/views_test.py @@ -483,8 +483,14 @@ def test_channel_counts_view(client): ) -def test_channel_counts_view_is_cached_for_anonymous_users(client): +def test_channel_counts_view_is_cached_for_anonymous_users(client, settings): """Test the channel counts view is cached for anonymous users""" + settings.CACHES["redis"] = { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": settings.CELERY_BROKER_URL, + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, + } + channel_count = 5 channels = ChannelFactory.create_batch(channel_count, channel_type="unit") url = reverse( @@ -499,8 +505,13 @@ def test_channel_counts_view_is_cached_for_anonymous_users(client): assert len(response) == channel_count -def test_channel_counts_view_is_cached_for_authenticated_users(client): +def test_channel_counts_view_is_cached_for_authenticated_users(client, settings): """Test the channel counts view is cached for authenticated users""" + settings.CACHES["redis"] = { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": settings.CELERY_BROKER_URL, + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, + } channel_count = 5 channel_user = UserFactory.create() client.force_login(channel_user) diff --git a/conftest.py b/conftest.py index 0ea2a33f4d..c569bd3fce 100644 --- a/conftest.py +++ b/conftest.py @@ -20,3 +20,12 @@ def prevent_requests(mocker, request): # noqa: PT004 autospec=True, side_effect=DoNotUseRequestException, ) + + +@pytest.fixture(autouse=True) +def _use_dummy_redis_cache_backend(settings): + new_cache_settings = settings.CACHES.copy() + new_cache_settings["redis"] = { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + settings.CACHES = new_cache_settings diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 4ce15dede9..21e09ba257 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -16195,8 +16195,7 @@ export const LearningpathsApiAxiosParamCreator = function ( } }, /** - * Get a list of related learning resources for a learning resource. - * @summary Nested Learning Resource List + * Viewset for LearningPath related resources * @param {number} learning_resource_id The learning resource id of the learning path * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. @@ -16779,8 +16778,7 @@ export const LearningpathsApiFp = function (configuration?: Configuration) { )(axios, operationBasePath || basePath) }, /** - * Get a list of related learning resources for a learning resource. - * @summary Nested Learning Resource List + * Viewset for LearningPath related resources * @param {number} learning_resource_id The learning resource id of the learning path * @param {number} [limit] Number of results to return per page. * @param {number} [offset] The initial index from which to return the results. @@ -17133,8 +17131,7 @@ export const LearningpathsApiFactory = function ( .then((request) => request(axios, basePath)) }, /** - * Get a list of related learning resources for a learning resource. - * @summary Nested Learning Resource List + * Viewset for LearningPath related resources * @param {LearningpathsApiLearningpathsItemsListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} @@ -17673,8 +17670,7 @@ export class LearningpathsApi extends BaseAPI { } /** - * Get a list of related learning resources for a learning resource. - * @summary Nested Learning Resource List + * Viewset for LearningPath related resources * @param {LearningpathsApiLearningpathsItemsListRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. * @throws {RequiredError} diff --git a/learning_resources/management/commands/populate_featured_lists.py b/learning_resources/management/commands/populate_featured_lists.py index a209b0b2dd..4f33a9b157 100644 --- a/learning_resources/management/commands/populate_featured_lists.py +++ b/learning_resources/management/commands/populate_featured_lists.py @@ -16,7 +16,7 @@ LearningResource, LearningResourceOfferor, ) -from main.utils import now_in_utc +from main.utils import clear_cache, now_in_utc class Command(BaseCommand): @@ -90,3 +90,4 @@ def handle(self, *args, **options): # noqa: ARG002 "Population of unit channel featured lists finished, " f"took {total_seconds} seconds" ) + clear_cache() diff --git a/learning_resources/management/commands/update_departments_schools.py b/learning_resources/management/commands/update_departments_schools.py index 6d6de62542..8832959531 100644 --- a/learning_resources/management/commands/update_departments_schools.py +++ b/learning_resources/management/commands/update_departments_schools.py @@ -6,7 +6,7 @@ upsert_department_data, upsert_school_data, ) -from main.utils import now_in_utc +from main.utils import clear_cache, now_in_utc class Command(BaseCommand): @@ -26,3 +26,4 @@ def handle(self, *args, **options): # noqa: ARG002 f"Update of {len(schools)} schools & {len(departments)} " f"departments finished, took {total_seconds} seconds" ) + clear_cache() diff --git a/learning_resources/management/commands/update_offered_by.py b/learning_resources/management/commands/update_offered_by.py index 0088c1476e..826ed1b129 100644 --- a/learning_resources/management/commands/update_offered_by.py +++ b/learning_resources/management/commands/update_offered_by.py @@ -3,7 +3,7 @@ from django.core.management import BaseCommand from learning_resources.utils import upsert_offered_by_data -from main.utils import now_in_utc +from main.utils import clear_cache, now_in_utc class Command(BaseCommand): @@ -21,3 +21,4 @@ def handle(self, *args, **options): # noqa: ARG002 self.stdout.write( f"Update of {len(offerors)} offerors finished, took {total_seconds} seconds" ) + clear_cache() diff --git a/learning_resources/management/commands/update_platforms.py b/learning_resources/management/commands/update_platforms.py index d1af4774e4..309071a180 100644 --- a/learning_resources/management/commands/update_platforms.py +++ b/learning_resources/management/commands/update_platforms.py @@ -3,7 +3,7 @@ from django.core.management import BaseCommand from learning_resources.utils import upsert_platform_data -from main.utils import now_in_utc +from main.utils import clear_cache, now_in_utc class Command(BaseCommand): @@ -21,3 +21,4 @@ def handle(self, *args, **options): # noqa: ARG002 self.stdout.write( f"Upserted {len(platform_codes)} platforms, took {total_seconds} seconds" ) + clear_cache() diff --git a/learning_resources/views.py b/learning_resources/views.py index adabf4954d..aa1fdad051 100644 --- a/learning_resources/views.py +++ b/learning_resources/views.py @@ -91,7 +91,7 @@ AnonymousAccessReadonlyPermission, is_admin_user, ) -from main.utils import chunks +from main.utils import cache_page_for_all_users, cache_page_for_anonymous_users, chunks log = logging.getLogger(__name__) @@ -159,6 +159,14 @@ def get_queryset(self) -> QuerySet: """ return self._get_base_queryset().filter(published=True) + @method_decorator( + cache_page_for_anonymous_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + @extend_schema_view( list=extend_schema( @@ -508,6 +516,14 @@ def learning_paths(self, request, *args, **kwargs): # noqa: ARG002 serializer = SerializerClass(current_relationships, many=True) return Response(serializer.data) + @method_decorator( + cache_page_for_anonymous_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + @extend_schema_view( create=extend_schema(summary="Learning Path Resource Relationship Add"), @@ -533,6 +549,14 @@ class LearningPathItemsViewSet(ResourceListItemsViewSet, viewsets.ModelViewSet): permission_classes = (permissions.HasLearningPathItemPermissions,) http_method_names = VALID_HTTP_METHODS + @method_decorator( + cache_page_for_anonymous_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + def create(self, request, *args, **kwargs): request.data["parent"] = request.data.get("parent_id") return super().create(request, *args, **kwargs) @@ -568,6 +592,14 @@ class TopicViewSet(viewsets.ReadOnlyModelViewSet): filter_backends = [DjangoFilterBackend] filterset_class = TopicFilter + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + @extend_schema_view( list=extend_schema(summary="List"), @@ -819,6 +851,14 @@ class ContentTagViewSet(viewsets.ReadOnlyModelViewSet): pagination_class = LargePagination permission_classes = (AnonymousAccessReadonlyPermission,) + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, *args, **kwargs): + return super().list(*args, **kwargs) + @extend_schema_view( list=extend_schema(summary="List"), @@ -838,6 +878,14 @@ class DepartmentViewSet(viewsets.ReadOnlyModelViewSet): lookup_url_kwarg = "department_id" lookup_field = "department_id__iexact" + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, *args, **kwargs): + return super().list(*args, **kwargs) + @extend_schema_view( list=extend_schema(summary="List"), @@ -853,6 +901,14 @@ class SchoolViewSet(viewsets.ReadOnlyModelViewSet): pagination_class = LargePagination permission_classes = (AnonymousAccessReadonlyPermission,) + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, *args, **kwargs): + return super().list(*args, **kwargs) + @extend_schema_view( list=extend_schema(summary="List"), @@ -868,6 +924,14 @@ class PlatformViewSet(viewsets.ReadOnlyModelViewSet): pagination_class = LargePagination permission_classes = (AnonymousAccessReadonlyPermission,) + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, *args, **kwargs): + return super().list(*args, **kwargs) + @extend_schema_view( list=extend_schema(summary="List"), @@ -884,6 +948,14 @@ class OfferedByViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = (AnonymousAccessReadonlyPermission,) lookup_field = "code" + @method_decorator( + cache_page_for_anonymous_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + @extend_schema_view( list=extend_schema( @@ -994,6 +1066,11 @@ def get_queryset(self) -> QuerySet: .distinct() ) + @method_decorator( + cache_page_for_anonymous_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) @extend_schema( summary="List", description="Get a paginated list of featured resources", diff --git a/main/utils.py b/main/utils.py index 5c4cf8216d..d227c63f93 100644 --- a/main/utils.py +++ b/main/utils.py @@ -357,7 +357,8 @@ def clean_data(data: str) -> str: def clear_search_cache(): cache = caches["redis"] - search_keys = cache.keys("views.decorators.cache.cache_page.search.*") - cache.delete_many(search_keys) - search_keys = cache.keys("views.decorators.cache.cache_header.search.*") - cache.delete_many(search_keys) + if hasattr(cache, "keys"): + search_keys = cache.keys("views.decorators.cache.cache_page.search.*") + cache.delete_many(search_keys) + search_keys = cache.keys("views.decorators.cache.cache_header.search.*") + cache.delete_many(search_keys) diff --git a/news_events/management/commands/backpopulate_news_events.py b/news_events/management/commands/backpopulate_news_events.py index 7002cea57d..193539ee21 100644 --- a/news_events/management/commands/backpopulate_news_events.py +++ b/news_events/management/commands/backpopulate_news_events.py @@ -4,6 +4,7 @@ from django.core.management import BaseCommand +from main.utils import clear_search_cache from news_events.etl import pipelines from news_events.models import FeedImage, FeedSource @@ -55,3 +56,4 @@ def handle(self, *args, **options): # noqa: ARG002 f"Processed {etl_func} pipeline with {item_count} items" ) self.stdout.write("Finished running news/events ETL pipelines") + clear_search_cache() diff --git a/news_events/tasks.py b/news_events/tasks.py index 935d37bb91..932927ffc7 100644 --- a/news_events/tasks.py +++ b/news_events/tasks.py @@ -1,6 +1,7 @@ """Tasks for news_events""" from main.celery import app +from main.utils import clear_search_cache from news_events.etl import pipelines @@ -8,33 +9,39 @@ def get_medium_mit_news(): """Run the Medium MIT News ETL pipeline""" pipelines.medium_mit_news_etl() + clear_search_cache() @app.task def get_ol_events(): """Run the Open Learning Events ETL pipeline""" pipelines.ol_events_etl() + clear_search_cache() @app.task def get_sloan_exec_news(): """Run the Sloan executive education news ETL pipeline""" pipelines.sloan_exec_news_etl() + clear_search_cache() @app.task def get_sloan_exec_webinars(): """Run the Sloan webinars ETL pipeline""" pipelines.sloan_webinars_etl() + clear_search_cache() @app.task def get_mitpe_news(): """Run the MIT Professional Education news ETL pipeline""" pipelines.mitpe_news_etl() + clear_search_cache() @app.task def get_mitpe_events(): """Run the MIT Professional Education events ETL pipeline""" pipelines.mitpe_events_etl() + clear_search_cache() diff --git a/news_events/views.py b/news_events/views.py index 3277c7f044..ebe7882519 100644 --- a/news_events/views.py +++ b/news_events/views.py @@ -1,11 +1,14 @@ """Views for news_events""" +from django.conf import settings +from django.utils.decorators import method_decorator from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import viewsets from rest_framework.pagination import LimitOffsetPagination from main.filters import MultipleOptionsFilterBackend from main.permissions import AnonymousAccessReadonlyPermission +from main.utils import cache_page_for_all_users from news_events.filters import FeedItemFilter, FeedSourceFilter from news_events.models import FeedItem, FeedSource from news_events.serializers import FeedItemSerializer, FeedSourceSerializer @@ -49,6 +52,14 @@ class FeedItemViewSet(viewsets.ReadOnlyModelViewSet): ) ) + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) + @extend_schema_view( list=extend_schema( @@ -70,3 +81,11 @@ class FeedSourceViewSet(viewsets.ReadOnlyModelViewSet): filter_backends = [MultipleOptionsFilterBackend] filterset_class = FeedSourceFilter queryset = FeedSource.objects.all().order_by("id") + + @method_decorator( + cache_page_for_all_users( + settings.SEARCH_PAGE_CACHE_DURATION, cache="redis", key_prefix="search" + ) + ) + def list(self, request, *args, **kwargs): + return super().list(request, *args, **kwargs) diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 8e9711f615..4e6b173689 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -4923,8 +4923,7 @@ paths: /api/v1/learningpaths/{learning_resource_id}/items/: get: operationId: learningpaths_items_list - description: Get a list of related learning resources for a learning resource. - summary: Nested Learning Resource List + description: Viewset for LearningPath related resources parameters: - in: path name: learning_resource_id