From dc777498f050d0bd4312b7cce1fcbf267623bc1d Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Mon, 4 May 2020 16:42:36 +0200 Subject: [PATCH] do make cache invalidation from product model --- shop/admin/product.py | 45 +++++++++++++++++++----------------------- shop/apps.py | 14 ++++++++++++- shop/models/product.py | 20 ++++++++++++++----- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/shop/admin/product.py b/shop/admin/product.py index 2b427e718..ec3531f91 100644 --- a/shop/admin/product.py +++ b/shop/admin/product.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import warnings - from django import forms -from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from django.contrib import admin from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ +try: + from django_elasticsearch_dsl.registries import registry as elasticsearch_registry +except ImportError: + elasticsearch_registry = type('DocumentRegistry', (), {'get_documents': lambda *args: []})() + from adminsortable2.admin import SortableInlineAdminMixin from cms.models import Page @@ -100,33 +102,26 @@ def save_related(self, request, form, formsets, change): return super(CMSPageAsCategoryMixin, self).save_related(request, form, formsets, change) -class InvalidateProductCacheMixin(object): +class SearchProductIndexMixin: """ - If caching is enabled, add this class as the first mixin to Django's model admin for the - corresponding product. + If Elasticsearch is used to create a full text search index, add this mixin class to Django's + ``ModelAdmin`` backend for the corresponding product model. """ - def __init__(self, *args, **kwargs): - if not hasattr(cache, 'delete_pattern'): - warnings.warn("\n" - "Your caching backend does not support deletion by key patterns.\n" - "Please use 'django-redis-cache', or wait until the product's HTML\n" - "snippet cache expires by itself.") - super(InvalidateProductCacheMixin, self).__init__(*args, **kwargs) + def save_model(self, request, product, form, change): + super().save_model(request, product, form, change) + if change: + product.update_search_index() + +class InvalidateProductCacheMixin: + """ + If Redis caching is used to create a HTML snippets for product representation, add this mixin + class to Django's ``ModelAdmin`` backend for the corresponding product model. + """ def save_model(self, request, product, form, change): if change: - self.invalidate_cache(product) - super(InvalidateProductCacheMixin, self).save_model(request, product, form, change) - - def invalidate_cache(self, product): - """ - The method ``ProductCommonSerializer.render_html`` caches the rendered HTML snippets. - Invalidate them after changing something in the product. - """ - try: - cache.delete_pattern('product:{}|*'.format(product.id)) - except AttributeError: - pass + product.invalidate_cache() + return super().save_model(request, product, form, change) class UnitPriceMixin(object): diff --git a/shop/apps.py b/shop/apps.py index b2df32ffe..d5f93da06 100644 --- a/shop/apps.py +++ b/shop/apps.py @@ -1,15 +1,19 @@ +import warnings + from django.apps import AppConfig +from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ class ShopConfig(AppConfig): name = 'shop' verbose_name = _("Shop") + cache_supporting_wildcard = False def ready(self): - from shop.models.fields import JSONField from rest_framework.serializers import ModelSerializer from shop.deferred import ForeignKeyBuilder + from shop.models.fields import JSONField from shop.rest.fields import JSONSerializerField from shop.patches import PageAttribute from cms.templatetags import cms_tags @@ -21,3 +25,11 @@ def ready(self): ForeignKeyBuilder.check_for_pending_mappings() cms_tags.register.tags['page_attribute'] = PageAttribute + + if callable(getattr(cache, 'delete_pattern', None)): + self.cache_supporting_wildcard = True + else: + warnings.warn("\n" + "Your caching backend does not support invalidation by key pattern.\n" + "Please use `django-redis-cache`, or wait until the product's HTML\n" + "snippet cache expires by itself.") diff --git a/shop/models/product.py b/shop/models/product.py index 1ea6d30a9..903e13257 100644 --- a/shop/models/product.py +++ b/shop/models/product.py @@ -6,8 +6,9 @@ import operator from cms import __version__ as CMS_VERSION +from django.apps import apps from django.conf import settings -from django.core import checks +from django.core import cache, checks from django.db import models from django.db.models.aggregates import Sum from django.db.models.functions import Coalesce @@ -351,11 +352,11 @@ def check(cls, **kwargs): errors.append(checks.Error(msg.format(cls.__name__))) return errors - def save(self, *args, **kwargs): - super().save(*args, **kwargs) - self.update_search_index() - def update_search_index(self): + """ + Update the document model inside the Elasticsearch index after changing relevant parts + of the product. + """ documents = elasticsearch_registry.get_documents([ProductModel]) if settings.USE_I18N: for language, _ in settings.LANGUAGES: @@ -368,6 +369,15 @@ def update_search_index(self): document = next(doc for doc in documents) document().update(self) + def invalidate_cache(self): + """ + Method ``ProductCommonSerializer.render_html()`` caches the rendered HTML snippets. + Invalidate this HTML snippet after changing relevant parts of the product. + """ + shop_app = apps.get_app_config('shop') + if shop_app.cache_supporting_wildcard: + cache.delete_pattern('product:{}|*'.format(self.id)) + ProductModel = deferred.MaterializedModel(BaseProduct)