From 8adbd4efc89e4d365e3ecbce705d873805551619 Mon Sep 17 00:00:00 2001 From: Roman Sokolov Date: Thu, 12 Dec 2024 17:34:20 +0200 Subject: [PATCH 1/4] add validators --- cinema/models.py | 28 +++++++++++---- cinema/serializers.py | 71 +++++++++++++++++++++++++++++++++++++- cinema/urls.py | 2 ++ cinema/views.py | 20 ++++++++++- cinema_service/settings.py | 3 +- requirements.txt | 22 ++++++++---- 6 files changed, 129 insertions(+), 17 deletions(-) diff --git a/cinema/models.py b/cinema/models.py index f18f166c..1a695435 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -84,16 +84,21 @@ class Ticket(models.Model): row = models.IntegerField() seat = models.IntegerField() - def clean(self): + @staticmethod + def validate_ticket( + row: int, + seat: int, + movie_session: int, + error_to_raise + ) -> None: + cinema_hall = movie_session.cinema_hall for ticket_attr_value, ticket_attr_name, cinema_hall_attr_name in [ - (self.row, "row", "rows"), - (self.seat, "seat", "seats_in_row"), + (row, "row", "rows"), + (seat, "seat", "seats_in_row"), ]: - count_attrs = getattr( - self.movie_session.cinema_hall, cinema_hall_attr_name - ) + count_attrs = getattr(cinema_hall, cinema_hall_attr_name) if not (1 <= ticket_attr_value <= count_attrs): - raise ValidationError( + raise error_to_raise( { ticket_attr_name: f"{ticket_attr_name} " f"number must be in available range: " @@ -102,6 +107,15 @@ def clean(self): } ) + def clean(self): + Ticket.validate_ticket( + row=self.row, + seat=self.seat, + movie_session=self.movie_session, + error_to_raise=ValueError + ) + + def save( self, force_insert=False, diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..3a289ac5 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,16 @@ +from rest_framework.validators import UniqueTogetherValidator, ValidationError +from django.db import transaction from rest_framework import serializers -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket +) class GenreSerializer(serializers.ModelSerializer): @@ -78,3 +88,62 @@ class MovieSessionDetailSerializer(MovieSessionSerializer): class Meta: model = MovieSession fields = ("id", "show_time", "movie", "cinema_hall") + + +class TicketSerializer(serializers.ModelSerializer): + movie_session = MovieSessionListSerializer(many=False, read_only=True) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + +class TicketCreateSerializer(serializers.ModelSerializer): + + class Meta: + model = Ticket + fields = ("row", "seat", "movie_session") + validators = [ + UniqueTogetherValidator( + queryset=Ticket.objects.all(), + fields=("row", "seat", "movie_session"), + ), + ] + def validate(self, attrs): + Ticket.validate_ticket( + row=attrs["row"], + seat=attrs["seat"], + movie_session=attrs["movie_session"], + error_to_raise=serializers.ValidationError + ) + return attrs + + +class OrderGetSerializer(serializers.ModelSerializer): + tickets = TicketSerializer(many=True, read_only=True) + + class Meta: + model = Order + fields = ("id", "tickets", "created_at") + + + +class OrderCreateSerializer(serializers.ModelSerializer): + tickets = TicketCreateSerializer( + many=True, + read_only=False, + allow_empty=False + ) + + class Meta: + model = Order + fields = ("tickets",) + + def create(self, validated_data): + with transaction.atomic(): + tickets_data = validated_data.pop("tickets") + order = Order.objects.create(**validated_data) + for tickets_data in tickets_data: + ticket = Ticket.objects.create(order=order, **tickets_data) + order.tickets.add(ticket) + return order \ No newline at end of file diff --git a/cinema/urls.py b/cinema/urls.py index e3586f00..5ad6fb5b 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -7,6 +7,7 @@ CinemaHallViewSet, MovieViewSet, MovieSessionViewSet, + OrderViewSet, ) router = routers.DefaultRouter() @@ -15,6 +16,7 @@ router.register("cinema_halls", CinemaHallViewSet) router.register("movies", MovieViewSet) router.register("movie_sessions", MovieSessionViewSet) +router.register("orders", OrderViewSet) urlpatterns = [path("", include(router.urls))] diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..202226f3 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,6 @@ from rest_framework import viewsets -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order from cinema.serializers import ( GenreSerializer, @@ -12,6 +12,8 @@ MovieDetailSerializer, MovieSessionDetailSerializer, MovieListSerializer, + OrderGetSerializer, + OrderCreateSerializer, ) @@ -56,3 +58,19 @@ def get_serializer_class(self): return MovieSessionDetailSerializer return MovieSessionSerializer + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + + def get_serializer_class(self): + if self.action == "list": + return OrderGetSerializer + elif self.action == "create": + return OrderCreateSerializer + + def get_queryset(self): + return self.queryset.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..280806ef 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -46,6 +46,7 @@ "debug_toolbar", "cinema", "user", + "django_extensions", ] MIDDLEWARE = [ @@ -124,7 +125,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) diff --git a/requirements.txt b/requirements.txt index 56e13554..13aa0e7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,15 @@ -django==4.1 -flake8==5.0.4 -flake8-quotes==3.3.1 -flake8-variables-names==0.0.5 -pep8-naming==0.13.2 -django-debug-toolbar==3.2.4 -djangorestframework==3.13.1 \ No newline at end of file +asgiref==3.8.1 +Django==4.1 +django-debug-toolbar==3.2.4 +django-extensions==3.2.3 +djangorestframework==3.13.1 +flake8==5.0.4 +flake8-quotes==3.3.1 +flake8-variables-names==0.0.5 +mccabe==0.7.0 +pep8-naming==0.13.2 +pycodestyle==2.9.1 +pyflakes==2.5.0 +pytz==2024.2 +sqlparse==0.5.3 +tzdata==2024.2 From 99652fb5d6e308979a5e2f1a445a7cccc75cc147 Mon Sep 17 00:00:00 2001 From: Roman Sokolov Date: Sat, 14 Dec 2024 14:40:09 +0200 Subject: [PATCH 2/4] Solution --- .gitignore | 3 +- cinema/models.py | 1 - cinema/serializers.py | 56 ++++++++++++---- cinema/tests/test_movie_session_api.py | 3 +- cinema/tests/test_order_api.py | 1 + cinema/views.py | 92 +++++++++++++++++++++++--- cinema_service/settings.py | 14 ++-- env.sample | 3 + requirements.txt | 1 + 9 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 env.sample diff --git a/.gitignore b/.gitignore index fd1f3e48..f88f0bd1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ venv/ .pytest_cache/ **__pycache__/ *.pyc -app/db.sqlite3 \ No newline at end of file +app/db.sqlite3 +tests \ No newline at end of file diff --git a/cinema/models.py b/cinema/models.py index 1a695435..229e8be5 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -115,7 +115,6 @@ def clean(self): error_to_raise=ValueError ) - def save( self, force_insert=False, diff --git a/cinema/serializers.py b/cinema/serializers.py index 3a289ac5..38398948 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -56,12 +56,13 @@ class Meta: class MovieSessionSerializer(serializers.ModelSerializer): + class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = ["id", "show_time", "movie", "cinema_hall"] -class MovieSessionListSerializer(MovieSessionSerializer): +class MovieSessionForOrderSerializer(serializers.ModelSerializer): movie_title = serializers.CharField(source="movie.title", read_only=True) cinema_hall_name = serializers.CharField( source="cinema_hall.name", read_only=True @@ -72,22 +73,24 @@ class MovieSessionListSerializer(MovieSessionSerializer): class Meta: model = MovieSession - fields = ( + fields = [ "id", "show_time", + "cinema_hall", "movie_title", "cinema_hall_name", - "cinema_hall_capacity", - ) + "cinema_hall_capacity" + ] -class MovieSessionDetailSerializer(MovieSessionSerializer): - movie = MovieListSerializer(many=False, read_only=True) - cinema_hall = CinemaHallSerializer(many=False, read_only=True) +class MovieSessionListSerializer(MovieSessionForOrderSerializer): + tickets_available = serializers.IntegerField(read_only=True) class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = MovieSessionForOrderSerializer.Meta.fields + [ + "tickets_available", + ] class TicketSerializer(serializers.ModelSerializer): @@ -98,6 +101,35 @@ class Meta: fields = ("id", "row", "seat", "movie_session") +class TicketForOrderSerializer(serializers.ModelSerializer): + movie_session = MovieSessionForOrderSerializer(many=False, read_only=True) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + +class TicketSeatsSerializer(TicketSerializer): + + class Meta: + model = Ticket + fields = ("row", "seat") + + +class MovieSessionDetailSerializer(MovieSessionSerializer): + movie = MovieListSerializer(many=False, read_only=True) + cinema_hall = CinemaHallSerializer(many=False, read_only=True) + taken_places = TicketSeatsSerializer( + many=True, + read_only=True, + source="tickets", + ) + + class Meta: + model = MovieSession + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") + + class TicketCreateSerializer(serializers.ModelSerializer): class Meta: @@ -109,6 +141,7 @@ class Meta: fields=("row", "seat", "movie_session"), ), ] + def validate(self, attrs): Ticket.validate_ticket( row=attrs["row"], @@ -120,14 +153,13 @@ def validate(self, attrs): class OrderGetSerializer(serializers.ModelSerializer): - tickets = TicketSerializer(many=True, read_only=True) + tickets = TicketForOrderSerializer(many=True, read_only=True) class Meta: model = Order fields = ("id", "tickets", "created_at") - class OrderCreateSerializer(serializers.ModelSerializer): tickets = TicketCreateSerializer( many=True, @@ -146,4 +178,4 @@ def create(self, validated_data): for tickets_data in tickets_data: ticket = Ticket.objects.create(order=order, **tickets_data) order.tickets.add(ticket) - return order \ No newline at end of file + return order diff --git a/cinema/tests/test_movie_session_api.py b/cinema/tests/test_movie_session_api.py index 9b75d7ed..cd1b509b 100644 --- a/cinema/tests/test_movie_session_api.py +++ b/cinema/tests/test_movie_session_api.py @@ -83,7 +83,8 @@ def test_get_movie_sessions_filtered_by_movie(self): def test_get_movie_sessions_filtered_by_movie_and_data(self): movie_sessions = self.client.get( - f"/api/cinema/movie_sessions/?movie={self.movie.id}&date=2022-09-2" + f"/api/cinema/movie_sessions/?movie=" + f"{self.movie.id}&date=2022-09-2" ) self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 1) diff --git a/cinema/tests/test_order_api.py b/cinema/tests/test_order_api.py index dd15df53..6d71e0df 100644 --- a/cinema/tests/test_order_api.py +++ b/cinema/tests/test_order_api.py @@ -82,6 +82,7 @@ def test_movie_session_detail_tickets(self): def test_movie_session_list_tickets_available(self): response = self.client.get(f"/api/cinema/movie_sessions/") + print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data[0]["tickets_available"], diff --git a/cinema/views.py b/cinema/views.py index 202226f3..17813954 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,17 @@ -from rest_framework import viewsets +from django.db.models import Count, F -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order +from rest_framework import viewsets +from rest_framework.pagination import PageNumberPagination + +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket +) from cinema.serializers import ( GenreSerializer, @@ -17,6 +28,12 @@ ) +class MixinParamsToListInt: + @staticmethod + def params_to_list_int(query_string): + return [int(litera) for litera in query_string.split(",")] + + class GenreViewSet(viewsets.ModelViewSet): queryset = Genre.objects.all() serializer_class = GenreSerializer @@ -32,8 +49,8 @@ class CinemaHallViewSet(viewsets.ModelViewSet): serializer_class = CinemaHallSerializer -class MovieViewSet(viewsets.ModelViewSet): - queryset = Movie.objects.all() +class MovieViewSet(MixinParamsToListInt, viewsets.ModelViewSet): + queryset = Movie.objects.all().prefetch_related("actors", "genres") serializer_class = MovieSerializer def get_serializer_class(self): @@ -45,10 +62,34 @@ def get_serializer_class(self): return MovieSerializer - -class MovieSessionViewSet(viewsets.ModelViewSet): - queryset = MovieSession.objects.all() - serializer_class = MovieSessionSerializer + def get_queryset(self): + queryset = self.queryset + + actors = self.request.query_params.get("actors") + genres = self.request.query_params.get("genres") + title = self.request.query_params.get("title") + + if actors: + actors = self.params_to_list_int(actors) + queryset = queryset.filter( + actors__in=actors) + if genres: + genres = self.params_to_list_int(genres) + queryset = queryset.filter(genres__in=genres) + if title: + queryset = queryset.filter(title__icontains=title) + + return queryset.distinct() + + +class MovieSessionViewSet(MixinParamsToListInt, viewsets.ModelViewSet): + queryset = MovieSession.objects.all().select_related( + "movie", + "cinema_hall" + ).prefetch_related("tickets").annotate( + tickets_available=F("cinema_hall__rows") * F( + "cinema_hall__seats_in_row") - Count("tickets") + ) def get_serializer_class(self): if self.action == "list": @@ -59,9 +100,42 @@ def get_serializer_class(self): return MovieSessionSerializer + def get_queryset(self): + queryset = self.queryset + + actors = self.request.query_params.get("actors") + genres = self.request.query_params.get("genres") + date = self.request.query_params.get("date") + movie = self.request.query_params.get("movie") + + if actors: + actors = self.params_to_list_int(actors) + queryset = queryset.filter(movie__actors__in=actors) + if genres: + genres = self.params_to_list_int(genres) + queryset = queryset.filter(movie__genres__in=genres) + if date: + queryset = queryset.filter(show_time__date=date) + if movie: + movie = self.params_to_list_int(movie) + queryset = queryset.filter(movie__in=movie) + + return queryset.distinct() + + +class OrderResultsSetPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 20 + class OrderViewSet(viewsets.ModelViewSet): - queryset = Order.objects.all() + queryset = Order.objects.all().select_related("user").prefetch_related( + "tickets__movie_session__cinema_hall", + "tickets__movie_session__movie" + ) + + pagination_class = OrderResultsSetPagination def get_serializer_class(self): if self.action == "list": diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 280806ef..ef21943b 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -11,23 +11,23 @@ """ from pathlib import Path +import os +from dotenv import load_dotenv # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - +load_dotenv(BASE_DIR / ".env") # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = ( - "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" -) +SECRET_KEY = os.getenv("SECRET_KEY", "your_sekreat_key") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = os.getenv("DEBUG", "True") == "True" -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",") INTERNAL_IPS = [ "127.0.0.1", @@ -125,7 +125,7 @@ USE_I18N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) diff --git a/env.sample b/env.sample new file mode 100644 index 00000000..55f9ab40 --- /dev/null +++ b/env.sample @@ -0,0 +1,3 @@ +SECRET_KEY= +DEBAG=False +ALLOWED_HOSTS = [, ,...] diff --git a/requirements.txt b/requirements.txt index 13aa0e7d..2f35c45f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,4 @@ pyflakes==2.5.0 pytz==2024.2 sqlparse==0.5.3 tzdata==2024.2 +python-dotenv==1.0.1 From 75784000d9d5abce1e5146e43e8cad6141d2e79a Mon Sep 17 00:00:00 2001 From: Roman Sokolov Date: Sat, 14 Dec 2024 15:16:54 +0200 Subject: [PATCH 3/4] Solution --- cinema/models.py | 2 +- cinema/tests/test_movie_session_api.py | 3 +-- cinema/tests/test_order_api.py | 1 - cinema_service/settings.py | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cinema/models.py b/cinema/models.py index 229e8be5..4bfc393c 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -112,7 +112,7 @@ def clean(self): row=self.row, seat=self.seat, movie_session=self.movie_session, - error_to_raise=ValueError + error_to_raise=ValidationError ) def save( diff --git a/cinema/tests/test_movie_session_api.py b/cinema/tests/test_movie_session_api.py index cd1b509b..1bd25f3b 100644 --- a/cinema/tests/test_movie_session_api.py +++ b/cinema/tests/test_movie_session_api.py @@ -83,8 +83,7 @@ def test_get_movie_sessions_filtered_by_movie(self): def test_get_movie_sessions_filtered_by_movie_and_data(self): movie_sessions = self.client.get( - f"/api/cinema/movie_sessions/?movie=" - f"{self.movie.id}&date=2022-09-2" + f"/api/cinema/movie_sessions/?movie="{self.movie.id}&date=2022-09-2" ) self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 1) diff --git a/cinema/tests/test_order_api.py b/cinema/tests/test_order_api.py index 6d71e0df..dd15df53 100644 --- a/cinema/tests/test_order_api.py +++ b/cinema/tests/test_order_api.py @@ -82,7 +82,6 @@ def test_movie_session_detail_tickets(self): def test_movie_session_list_tickets_available(self): response = self.client.get(f"/api/cinema/movie_sessions/") - print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual( response.data[0]["tickets_available"], diff --git a/cinema_service/settings.py b/cinema_service/settings.py index ef21943b..e70e2ef9 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -25,7 +25,7 @@ SECRET_KEY = os.getenv("SECRET_KEY", "your_sekreat_key") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = os.getenv("DEBUG", "True") == "True" +DEBUG = os.getenv("DEBUG", "False") == "True" ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "localhost").split(",") From 8d0b71274c4c3228944dd4126a42a6956df14292 Mon Sep 17 00:00:00 2001 From: Roman Sokolov Date: Sat, 14 Dec 2024 15:21:12 +0200 Subject: [PATCH 4/4] Solution --- cinema/tests/test_movie_session_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cinema/tests/test_movie_session_api.py b/cinema/tests/test_movie_session_api.py index 1bd25f3b..cd1b509b 100644 --- a/cinema/tests/test_movie_session_api.py +++ b/cinema/tests/test_movie_session_api.py @@ -83,7 +83,8 @@ def test_get_movie_sessions_filtered_by_movie(self): def test_get_movie_sessions_filtered_by_movie_and_data(self): movie_sessions = self.client.get( - f"/api/cinema/movie_sessions/?movie="{self.movie.id}&date=2022-09-2" + f"/api/cinema/movie_sessions/?movie=" + f"{self.movie.id}&date=2022-09-2" ) self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 1)