From 7c7d9060ef56c2ba3ac7b0ec87f78e83ae2ca30e Mon Sep 17 00:00:00 2001 From: dmytro Date: Mon, 6 Jan 2025 11:37:49 +0200 Subject: [PATCH] feat: added cache and pagination --- Dockerfile | 2 +- book/apps.py | 3 +++ book/signals.py | 11 +++++++++++ book/tests/test_book_view_set.py | 4 ++++ book/views.py | 13 +++++++++++++ borrowing/signals.py | 7 ++++++- borrowing/tests/test_borrowing_api.py | 4 ++++ borrowing/views.py | 13 +++++++++++++ core/settings.py | 21 +++++++++++++++++++++ poetry.lock | 21 +++++++++++++++++++-- pyproject.toml | 1 + 11 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 book/signals.py diff --git a/Dockerfile b/Dockerfile index 15977ee..73e49aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN adduser \ RUN pip install --no-cache-dir poetry COPY pyproject.toml poetry.lock ./ -RUN poetry config virtualenvs.create false && poetry install --no-dev --no-interaction +RUN poetry config virtualenvs.create false && poetry install --only main --no-interaction --no-root USER appuser diff --git a/book/apps.py b/book/apps.py index 0bdbe44..e92e816 100644 --- a/book/apps.py +++ b/book/apps.py @@ -4,3 +4,6 @@ class BookConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "book" + + def ready(self): + import book.signals # noqa diff --git a/book/signals.py b/book/signals.py new file mode 100644 index 0000000..12621d4 --- /dev/null +++ b/book/signals.py @@ -0,0 +1,11 @@ +from django.core.cache import cache +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from book.models import Book + + +@receiver([post_save, post_delete], sender=Book) +def invalidate_cache(sender, instance, **kwargs): + cache.delete_pattern("*book_view*") + cache.delete_pattern("*borrowing_view*") diff --git a/book/tests/test_book_view_set.py b/book/tests/test_book_view_set.py index 93a17d7..5a78f93 100644 --- a/book/tests/test_book_view_set.py +++ b/book/tests/test_book_view_set.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.core.cache import cache from rest_framework import status from rest_framework.test import APITestCase from book.models import Book @@ -27,6 +28,9 @@ def setUp(self): email="user@example.com", password="user123" ) + def tearDown(self): + cache.clear() + def test_list_books_unauthenticated(self): """Test: Unauthenticated users can view the list of books""" response = self.client.get("/api/books/") diff --git a/book/views.py b/book/views.py index 8b2990e..0a603f5 100644 --- a/book/views.py +++ b/book/views.py @@ -1,3 +1,5 @@ +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page from rest_framework import viewsets from book.models import Book @@ -17,3 +19,14 @@ class BookViewSet(viewsets.ModelViewSet): permission_classes = [ IsAdminOrReadOnly, ] + + @method_decorator(cache_page(60 * 5, key_prefix="book_view")) + def dispatch(self, request, *args, **kwargs): + """ + Method to dispatch the request, with caching applied + for the crew view. + + The response is cached for 5 minutes using the + key prefix 'book_view'. + """ + return super().dispatch(request, *args, **kwargs) diff --git a/borrowing/signals.py b/borrowing/signals.py index 4d13d92..c6234e0 100644 --- a/borrowing/signals.py +++ b/borrowing/signals.py @@ -1,4 +1,5 @@ -from django.db.models.signals import post_save +from django.core.cache import cache +from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from .models import Borrowing from tg_bot.utils import send_telegram_notification @@ -25,3 +26,7 @@ def handle_borrowing_creation(instance, created, **kwargs): f" Expected return date: {instance.expected_return_date}" ) send_telegram_notification.delay(message) + +@receiver([post_save, post_delete], sender=Borrowing) +def invalidate_cache(sender, instance, **kwargs): + cache.delete_pattern("*borrowing_view*") diff --git a/borrowing/tests/test_borrowing_api.py b/borrowing/tests/test_borrowing_api.py index 634a5c7..231ed9d 100644 --- a/borrowing/tests/test_borrowing_api.py +++ b/borrowing/tests/test_borrowing_api.py @@ -1,6 +1,7 @@ from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse +from django.core.cache import cache from datetime import timedelta from django.utils.timezone import now from django.contrib.auth import get_user_model @@ -24,6 +25,9 @@ def setUp(self): ) self.client.force_authenticate(user=self.user) + def tearDown(self): + cache.clear() + def test_create_borrowing_no_inventory(self): """Test creating a borrowing record when no copies are available.""" self.book.inventory = 0 diff --git a/borrowing/views.py b/borrowing/views.py index e822f1d..d46a5cc 100644 --- a/borrowing/views.py +++ b/borrowing/views.py @@ -1,5 +1,7 @@ import datetime +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_page from django.http import HttpResponseRedirect from rest_framework import viewsets, mixins, status from rest_framework.decorators import action @@ -118,3 +120,14 @@ def return_book(self, request, pk=None): return HttpResponseRedirect( payment.session_url, status=status.HTTP_302_FOUND ) + + @method_decorator(cache_page(60 * 5, key_prefix="borrowing_view")) + def dispatch(self, request, *args, **kwargs): + """ + Method to dispatch the request, with caching applied + for the crew view. + + The response is cached for 5 minutes using the + key prefix 'borrowing_view'. + """ + return super().dispatch(request, *args, **kwargs) diff --git a/core/settings.py b/core/settings.py index ddfeb4a..e0493bb 100644 --- a/core/settings.py +++ b/core/settings.py @@ -93,6 +93,26 @@ # Database # https://docs.djangoproject.com/en/5.1/ref/settings/#databases +if os.environ.get("ENVIRONMENT") == "local": + CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://127.0.0.1:6379/1', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } + } +else: + CACHES = { + 'default': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': 'redis://redis:6379/0', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + } + } + } if os.environ.get("ENVIRONMENT") == "local": DATABASES = { @@ -118,6 +138,7 @@ "rest_framework_simplejwt.authentication.JWTAuthentication", ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination" } SIMPLE_JWT = { diff --git a/poetry.lock b/poetry.lock index 9772539..838bdc3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -404,6 +404,24 @@ files = [ [package.dependencies] Django = ">=4.2" +[[package]] +name = "django-redis" +version = "5.4.0" +description = "Full featured redis cache backend for Django." +optional = false +python-versions = ">=3.6" +files = [ + {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, + {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, +] + +[package.dependencies] +Django = ">=3.2" +redis = ">=3,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1" + +[package.extras] +hiredis = ["redis[hiredis] (>=3,!=4.0.0,!=4.0.1)"] + [[package]] name = "django-timezone-field" version = "7.0" @@ -696,7 +714,6 @@ files = [ {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1"}, {file = "psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567"}, - {file = "psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:eb09aa7f9cecb45027683bb55aebaaf45a0df8bf6de68801a6afdc7947bb09d4"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b73d6d7f0ccdad7bc43e6d34273f70d587ef62f824d7261c4ae9b8b1b6af90e8"}, {file = "psycopg2_binary-2.9.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce5ab4bf46a211a8e924d307c1b1fcda82368586a19d0a24f8ae166f5c784864"}, @@ -1186,4 +1203,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12.8" -content-hash = "5f8b167b23adb24e025cabae018bcf416f97c23dd800275a5f01da5debffdc19" +content-hash = "cf76f316f3d584a044f37f837aae84a63a98b3dbfbcf8862cdbc1ce05fc8c616" diff --git a/pyproject.toml b/pyproject.toml index c43687a..93596a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ stripe = "^11.4.1" celery = {extras = ["redis"], version = "^5.4.0"} python-telegram-bot = "^21.10" django-celery-beat = "^2.7.0" +django-redis = "^5.4.0" [build-system] requires = ["poetry-core"]