From 3899e2a3db9fc82a8b98fac8dfdea61a0c8ee08e Mon Sep 17 00:00:00 2001 From: Gabriel Augendre Date: Sun, 25 Sep 2022 21:08:44 +0200 Subject: [PATCH] Implement caching --- poetry.lock | 14 +++++++- pyproject.toml | 1 + src/purchase/admin.py | 6 +++- src/purchase/apps.py | 11 +++++++ .../migrations/0009_basketitemetag.py | 33 +++++++++++++++++++ .../0010_rename_basketitemetag_cacheetag.py | 17 ++++++++++ src/purchase/models.py | 13 ++++++++ src/purchase/signals.py | 4 +++ src/purchase/views/basket.py | 6 +++- src/purchase/views/reports.py | 10 +++++- 10 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 src/purchase/migrations/0009_basketitemetag.py create mode 100644 src/purchase/migrations/0010_rename_basketitemetag_cacheetag.py create mode 100644 src/purchase/signals.py diff --git a/poetry.lock b/poetry.lock index ba2d41b..984d833 100644 --- a/poetry.lock +++ b/poetry.lock @@ -353,6 +353,17 @@ python-versions = ">=3.7" [package.dependencies] Django = ">=3.2" +[[package]] +name = "django-solo" +version = "2.0.0" +description = "Django Solo helps working with singletons" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +django = ">=2.2" + [[package]] name = "factory-boy" version = "3.2.1" @@ -1164,7 +1175,7 @@ h11 = ">=0.9.0,<1" [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "e0332f1446a90fc3a5dc2fa2ca5d1745eb0d90d8f033094d79b2df9376b79639" +content-hash = "517db1c26bc459a29fdeae5f5ea18620c54121f95110d641b740ffdc322fb584" [metadata.files] ansicon = [] @@ -1288,6 +1299,7 @@ django-environ = [ ] django-extensions = [] django-htmx = [] +django-solo = [] factory-boy = [] faker = [] filelock = [] diff --git a/pyproject.toml b/pyproject.toml index 8c2c321..1600fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ crispy-bootstrap5 = "^0.6" matplotlib = "^3.5.1" freezegun = "^1.2.1" django-htmx = "^1.12.2" +django-solo = "^2.0.0" [tool.poetry.dev-dependencies] pre-commit = "^2.7" diff --git a/src/purchase/admin.py b/src/purchase/admin.py index 8a69190..a25c379 100644 --- a/src/purchase/admin.py +++ b/src/purchase/admin.py @@ -1,8 +1,9 @@ from django.contrib import admin from django.contrib.admin import register from django.utils.translation import gettext_lazy as _ +from solo.admin import SingletonModelAdmin -from purchase.models import Basket, BasketItem, PaymentMethod, Product +from purchase.models import Basket, BasketItem, CacheEtag, PaymentMethod, Product from purchase.templatetags.purchase import currency @@ -70,3 +71,6 @@ class BasketAdmin(admin.ModelAdmin): @admin.display(description=_("price")) def price(self, instance) -> str: return currency(instance.price) + + +admin.site.register(CacheEtag, SingletonModelAdmin) diff --git a/src/purchase/apps.py b/src/purchase/apps.py index 4f36992..d661bb0 100644 --- a/src/purchase/apps.py +++ b/src/purchase/apps.py @@ -1,6 +1,17 @@ from django.apps import AppConfig +from django.db.models.signals import post_save class PurchaseConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "purchase" + + def ready(self): + from purchase.models import Basket, BasketItem, PaymentMethod, Product + + from .signals import basket_item_on_save + + post_save.connect(basket_item_on_save, sender=BasketItem) + post_save.connect(basket_item_on_save, sender=Basket) + post_save.connect(basket_item_on_save, sender=Product) + post_save.connect(basket_item_on_save, sender=PaymentMethod) diff --git a/src/purchase/migrations/0009_basketitemetag.py b/src/purchase/migrations/0009_basketitemetag.py new file mode 100644 index 0000000..f90ba72 --- /dev/null +++ b/src/purchase/migrations/0009_basketitemetag.py @@ -0,0 +1,33 @@ +# Generated by Django 4.1.1 on 2022-09-25 19:00 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("purchase", "0008_basketitem_unique_product_per_basket"), + ] + + operations = [ + migrations.CreateModel( + name="BasketItemEtag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.UUIDField(default=uuid.uuid4)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/purchase/migrations/0010_rename_basketitemetag_cacheetag.py b/src/purchase/migrations/0010_rename_basketitemetag_cacheetag.py new file mode 100644 index 0000000..1075e82 --- /dev/null +++ b/src/purchase/migrations/0010_rename_basketitemetag_cacheetag.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.1 on 2022-09-25 19:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("purchase", "0009_basketitemetag"), + ] + + operations = [ + migrations.RenameModel( + old_name="BasketItemEtag", + new_name="CacheEtag", + ), + ] diff --git a/src/purchase/models.py b/src/purchase/models.py index e909800..4400290 100644 --- a/src/purchase/models.py +++ b/src/purchase/models.py @@ -1,6 +1,7 @@ from __future__ import annotations import hashlib +import uuid from django.db import models from django.db.models import Avg, Count, F, Sum, UniqueConstraint @@ -9,6 +10,7 @@ from django.urls import reverse from django.utils.translation import gettext from django.utils.translation import gettext_lazy as _ from PIL import Image, ImageOps +from solo.models import SingletonModel class Model(models.Model): @@ -220,3 +222,14 @@ class BasketItem(Model): constraints = [ UniqueConstraint("product", "basket", name="unique_product_per_basket") ] + + +class CacheEtag(SingletonModel): + value = models.UUIDField(default=uuid.uuid4) + + def __str__(self) -> str: + return str(self.value) + + def refresh(self): + self.value = uuid.uuid4() + self.save() diff --git a/src/purchase/signals.py b/src/purchase/signals.py new file mode 100644 index 0000000..b3ed4e6 --- /dev/null +++ b/src/purchase/signals.py @@ -0,0 +1,4 @@ +def basket_item_on_save(sender, **kwargs): + from purchase.models import CacheEtag + + CacheEtag.get_solo().refresh() diff --git a/src/purchase/views/basket.py b/src/purchase/views/basket.py index 2cf1473..2532427 100644 --- a/src/purchase/views/basket.py +++ b/src/purchase/views/basket.py @@ -5,14 +5,16 @@ from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.decorators.http import require_http_methods +from django.views.decorators.http import condition, require_http_methods from purchase.forms import BasketForm from purchase.models import Basket +from purchase.views.reports import reports_etag @require_http_methods(["GET", "POST"]) @permission_required("purchase.add_basket") +@condition(etag_func=reports_etag) def new_basket(request: HttpRequest) -> HttpResponse: if request.method == "POST": form = BasketForm(request.POST) @@ -32,6 +34,7 @@ def new_basket(request: HttpRequest) -> HttpResponse: @require_http_methods(["GET", "POST"]) @permission_required("purchase.change_basket") +@condition(etag_func=reports_etag) def update_basket(request: HttpRequest, pk: int) -> HttpResponse: basket = get_object_or_404(Basket.objects.priced(), pk=pk) if request.method == "POST": @@ -49,6 +52,7 @@ def update_basket(request: HttpRequest, pk: int) -> HttpResponse: @permission_required("purchase.view_basket") +@condition(etag_func=reports_etag) def list_baskets(request: HttpRequest) -> HttpResponse: context = {"baskets": Basket.objects.priced().order_by("-id")} return TemplateResponse(request, "purchase/basket_list.html", context) diff --git a/src/purchase/views/reports.py b/src/purchase/views/reports.py index ed8e7fd..58c4e51 100644 --- a/src/purchase/views/reports.py +++ b/src/purchase/views/reports.py @@ -10,6 +10,7 @@ from django.contrib.auth.decorators import permission_required from django.shortcuts import render from django.template.response import TemplateResponse from django.utils.translation import gettext as _ +from django.views.decorators.http import condition from matplotlib import pyplot as plt from matplotlib import ticker from matplotlib.axes import Axes @@ -17,12 +18,17 @@ from matplotlib.container import BarContainer from matplotlib.dates import AutoDateLocator, ConciseDateFormatter, HourLocator from matplotlib.figure import Figure -from purchase.models import Basket, PaymentMethod, Product, ProductQuerySet +from purchase.models import Basket, CacheEtag, PaymentMethod, Product, ProductQuerySet matplotlib.use("SVG") +def reports_etag(request): + return str(CacheEtag.get_solo().value) + + @permission_required("purchase.view_basket") +@condition(etag_func=reports_etag) def products_plots_view(request): products = Product.objects.with_turnover().with_sold() ( @@ -37,6 +43,7 @@ def products_plots_view(request): @permission_required("purchase.view_basket") +@condition(etag_func=reports_etag) def by_hour_plot_view(request): baskets = list(Basket.objects.priced().order_by("created_at")) context = { @@ -46,6 +53,7 @@ def by_hour_plot_view(request): @permission_required("purchase.view_basket") +@condition(etag_func=reports_etag) def reports(request): template_name = "purchase/reports.html" baskets = list(Basket.objects.priced().order_by("created_at"))