Add the ability to cache model attributes in redis
This not only means they persist longer than the instance, but can also be shared between processes. This is especially useful for list pages, as rendering content for summaries etc is quite expensive
This commit is contained in:
parent
d1523a886b
commit
7c008c2149
2 changed files with 64 additions and 2 deletions
|
@ -25,6 +25,7 @@ from wagtail.snippets.models import register_snippet
|
||||||
from wagtailmetadata.models import MetadataMixin
|
from wagtailmetadata.models import MetadataMixin
|
||||||
|
|
||||||
from website.contrib.unsplash.widgets import UnsplashPhotoChooser
|
from website.contrib.unsplash.widgets import UnsplashPhotoChooser
|
||||||
|
from website.utils.cache import cached_model_property
|
||||||
|
|
||||||
from .serializers import PaginationSerializer
|
from .serializers import PaginationSerializer
|
||||||
from .streamfield import add_heading_anchors, get_blocks, get_content_html
|
from .streamfield import add_heading_anchors, get_blocks, get_content_html
|
||||||
|
@ -127,11 +128,11 @@ class BaseContentPage(BasePage, MetadataMixin):
|
||||||
def _body_html(self) -> str:
|
def _body_html(self) -> str:
|
||||||
return str(self.body)
|
return str(self.body)
|
||||||
|
|
||||||
@cached_property
|
@cached_model_property
|
||||||
def content_html(self) -> str:
|
def content_html(self) -> str:
|
||||||
return get_content_html(self._body_html)
|
return get_content_html(self._body_html)
|
||||||
|
|
||||||
@cached_property
|
@cached_model_property
|
||||||
def plain_text(self) -> str:
|
def plain_text(self) -> str:
|
||||||
return extract_text(self.content_html)
|
return extract_text(self.content_html)
|
||||||
|
|
||||||
|
|
61
website/utils/cache.py
Normal file
61
website/utils/cache.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import inspect
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Callable, Type, TypeVar
|
||||||
|
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_key(instance: Model, method: Callable) -> str:
|
||||||
|
return f"page_{method.__name__}_{instance.pk}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_model_properties(model: Type[Model]) -> list[str]:
|
||||||
|
return [
|
||||||
|
name
|
||||||
|
for name, _ in inspect.getmembers(
|
||||||
|
model, predicate=lambda p: hasattr(p, "__cached__")
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def cached_model_property(f: Callable[[Model], T]) -> T:
|
||||||
|
@cached_property
|
||||||
|
@wraps(f)
|
||||||
|
def wrapped(self: Model) -> T:
|
||||||
|
cache_key = get_cache_key(self, f)
|
||||||
|
value = cache.get(cache_key)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
value = f(self)
|
||||||
|
# Cache for 1 week
|
||||||
|
cache.set(cache_key, value, 604800)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
wrapped.__cached__ = True
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save)
|
||||||
|
def clear_cached_model_properties(
|
||||||
|
sender: Type, instance: Model, **kwargs: dict
|
||||||
|
) -> None:
|
||||||
|
cached_model_properties = get_cached_model_properties(instance.__class__)
|
||||||
|
|
||||||
|
if cached_model_properties:
|
||||||
|
cache.delete_many(
|
||||||
|
[
|
||||||
|
get_cache_key(instance, getattr(instance.__class__, name).real_func)
|
||||||
|
for name in cached_model_properties
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prime caches again
|
||||||
|
for name in cached_model_properties:
|
||||||
|
getattr(instance, name)
|
Loading…
Reference in a new issue