diff --git a/contentcuration/contentcuration/models.py b/contentcuration/contentcuration/models.py index 180db9ce25..0478a36c5c 100644 --- a/contentcuration/contentcuration/models.py +++ b/contentcuration/contentcuration/models.py @@ -835,8 +835,8 @@ def on_create(self): node_id=self.id, ) - # if this change affects the public channel list, clear the channel cache - if self.public: + # if this change affects the published channel list, clear the channel cache + if self.public and (self.main_tree and self.main_tree.published): delete_public_channel_cache_keys() def on_update(self): @@ -876,8 +876,8 @@ def on_update(self): if self.main_tree and self.main_tree._field_updates.changed(): self.main_tree.save() - # if this change affects the public channel list, clear the channel cache - if "public" in original_values: + # if this change affects the published channel list, clear the channel cache + if "public" in original_values and (self.main_tree and self.main_tree.published): delete_public_channel_cache_keys() def save(self, *args, **kwargs): diff --git a/contentcuration/contentcuration/utils/cache.py b/contentcuration/contentcuration/utils/cache.py index 412ad3d731..436d02c17e 100644 --- a/contentcuration/contentcuration/utils/cache.py +++ b/contentcuration/contentcuration/utils/cache.py @@ -103,8 +103,11 @@ def delete_public_channel_cache_keys(): """ Delete all caches related to the public channel caching. """ + from contentcuration.views.base import PUBLIC_CHANNELS_CACHE_KEYS + delete_cache_keys("*get_public_channel_list*") delete_cache_keys("*get_user_public_channels*") + django_cache.delete_many(list(PUBLIC_CHANNELS_CACHE_KEYS.values())) def redis_retry(func): @@ -134,6 +137,7 @@ class ResourceSizeCache: If the django_cache is Redis, then we use the lower level Redis client to use its hash commands, HSET and HGET, to ensure we can store lots of data in performant way """ + def __init__(self, node, cache=None): self.node = node self.cache = cache or django_cache diff --git a/contentcuration/contentcuration/utils/publish.py b/contentcuration/contentcuration/utils/publish.py index aa7b797eb9..63fc79c1b0 100644 --- a/contentcuration/contentcuration/utils/publish.py +++ b/contentcuration/contentcuration/utils/publish.py @@ -41,6 +41,7 @@ from contentcuration import models as ccmodels from contentcuration.statistics import record_publish_stats +from contentcuration.utils.cache import delete_public_channel_cache_keys from contentcuration.utils.files import create_thumbnail_from_base64 from contentcuration.utils.files import get_thumbnail_encoding from contentcuration.utils.parser import extract_value @@ -808,6 +809,10 @@ def publish_channel( channel.main_tree.published = True channel.main_tree.save() + # Delete public channel cache. + if channel.public: + delete_public_channel_cache_keys() + if send_email: send_emails(channel, user_id, version_notes=version_notes) diff --git a/contentcuration/contentcuration/views/base.py b/contentcuration/contentcuration/views/base.py index 78f774c1c7..e8f9e4196d 100644 --- a/contentcuration/contentcuration/views/base.py +++ b/contentcuration/contentcuration/views/base.py @@ -5,6 +5,7 @@ from django.conf import settings from django.contrib.auth.decorators import login_required +from django.core.cache import cache from django.core.exceptions import PermissionDenied from django.db.models import Count from django.db.models import IntegerField @@ -49,6 +50,13 @@ from contentcuration.viewsets.channelset import PublicChannelSetSerializer PUBLIC_CHANNELS_CACHE_DURATION = 30 # seconds +PUBLIC_CHANNELS_CACHE_KEYS = { + "list": "public_channel_list", + "languages": "public_channel_languages", + "licenses": "public_channel_licenses", + "kinds": "public_channel_kinds", + "collections": "public_channel_collections", +} MESSAGES = "i18n_messages" PREFERENCES = "user_preferences" @@ -125,53 +133,70 @@ def channel_list(request): current_user = current_user_for_context(None if anon else request.user) preferences = DEFAULT_USER_PREFERENCES if anon else request.user.content_defaults - public_channel_list = Channel.objects.filter( - public=True, main_tree__published=True, deleted=False, - ).values_list("main_tree__tree_id", flat=True) + public_channel_list = cache.get(PUBLIC_CHANNELS_CACHE_KEYS["list"]) + if public_channel_list is None: + public_channel_list = Channel.objects.filter( + public=True, main_tree__published=True, deleted=False, + ).values_list("main_tree__tree_id", flat=True) + cache.set(PUBLIC_CHANNELS_CACHE_KEYS["list"], public_channel_list, None) # Get public channel languages - public_lang_query = ( - Language.objects.filter( - channel_language__public=True, - channel_language__main_tree__published=True, - channel_language__deleted=False, + languages = cache.get(PUBLIC_CHANNELS_CACHE_KEYS["languages"]) + if languages is None: + public_lang_query = ( + Language.objects.filter( + channel_language__public=True, + channel_language__main_tree__published=True, + channel_language__deleted=False, + ) + .values("lang_code") + .annotate(count=Count("lang_code")) + .order_by("lang_code") ) - .values("lang_code") - .annotate(count=Count("lang_code")) - .order_by("lang_code") - ) - languages = {lang["lang_code"]: lang["count"] for lang in public_lang_query} + languages = {lang["lang_code"]: lang["count"] for lang in public_lang_query} + cache.set(PUBLIC_CHANNELS_CACHE_KEYS["languages"], json_for_parse_from_data(languages), None) # Get public channel licenses - public_license_query = ( - License.objects.filter(contentnode__tree_id__in=public_channel_list) - .values_list("id", flat=True) - .order_by("id") - .distinct() - ) - licenses = list(public_license_query) + licenses = cache.get(PUBLIC_CHANNELS_CACHE_KEYS["licenses"]) + if licenses is None: + public_license_query = ( + License.objects.filter(contentnode__tree_id__in=public_channel_list) + .values_list("id", flat=True) + .order_by("id") + .distinct() + ) + licenses = list(public_license_query) + cache.set(PUBLIC_CHANNELS_CACHE_KEYS["licenses"], json_for_parse_from_data(licenses), None) # Get public channel kinds - public_kind_query = ( - ContentKind.objects.filter(contentnodes__tree_id__in=public_channel_list) - .values_list("kind", flat=True) - .order_by("kind") - .distinct() - ) - kinds = list(public_kind_query) + kinds = cache.get(PUBLIC_CHANNELS_CACHE_KEYS["kinds"]) + if kinds is None: + public_kind_query = ( + ContentKind.objects.filter(contentnodes__tree_id__in=public_channel_list) + .values_list("kind", flat=True) + .order_by("kind") + .distinct() + ) + kinds = list(public_kind_query) + cache.set(PUBLIC_CHANNELS_CACHE_KEYS["kinds"], json_for_parse_from_data(kinds), None) # Get public channel sets - public_channelset_query = ChannelSet.objects.filter(public=True).annotate( - count=SQCountDistinct( - Channel.objects.filter( - secret_tokens=OuterRef("secret_token"), - public=True, - main_tree__published=True, - deleted=False, - ).values_list("id", flat=True), - field="id", + collections = cache.get(PUBLIC_CHANNELS_CACHE_KEYS["collections"]) + if collections is None: + public_channelset_query = ChannelSet.objects.filter(public=True).annotate( + count=SQCountDistinct( + Channel.objects.filter( + secret_tokens=OuterRef("secret_token"), + public=True, + main_tree__published=True, + deleted=False, + ).values_list("id", flat=True), + field="id", + ) ) - ) + cache.set(PUBLIC_CHANNELS_CACHE_KEYS["collections"], json_for_parse_from_serializer( + PublicChannelSetSerializer(public_channelset_query, many=True)), None) + return render( request, "channel_list.html", @@ -180,12 +205,10 @@ def channel_list(request): PREFERENCES: json_for_parse_from_data(preferences), MESSAGES: json_for_parse_from_data(get_messages()), "LIBRARY_MODE": settings.LIBRARY_MODE, - "public_languages": json_for_parse_from_data(languages), - "public_kinds": json_for_parse_from_data(kinds), - "public_licenses": json_for_parse_from_data(licenses), - "public_collections": json_for_parse_from_serializer( - PublicChannelSetSerializer(public_channelset_query, many=True) - ), + "public_languages": languages, + "public_kinds": kinds, + "public_licenses": licenses, + "public_collections": collections, }, )