From 7155de108b3b498873596349aebd64e2e323e28c Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Wed, 8 Jan 2025 21:57:38 +0100 Subject: [PATCH 1/7] Solution --- cinema/pagination.py | 7 +++ cinema/serializers.py | 57 +++++++++++++++++++++++- cinema/urls.py | 8 +++- cinema/views.py | 87 ++++++++++++++++++++++++++++++++++++- cinema_service/settings.py | 7 ++- db.sqlite3 | Bin 0 -> 225280 bytes 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 cinema/pagination.py create mode 100644 db.sqlite3 diff --git a/cinema/pagination.py b/cinema/pagination.py new file mode 100644 index 00000000..1999e5d7 --- /dev/null +++ b/cinema/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class OrdersPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 100 diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..5335680b 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,16 @@ +from django.db import transaction from rest_framework import serializers - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from rest_framework.relations import PrimaryKeyRelatedField + +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket, + ) class GenreSerializer(serializers.ModelSerializer): @@ -78,3 +88,46 @@ class MovieSessionDetailSerializer(MovieSessionSerializer): class Meta: model = MovieSession fields = ("id", "show_time", "movie", "cinema_hall") + + +class TicketSerializer(serializers.ModelSerializer): + movie_session = PrimaryKeyRelatedField( + queryset=MovieSession.objects.all() + ) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + +class TicketDetailSerializer(TicketSerializer): + movie_session = MovieSessionListSerializer(many=False, read_only=True) + + def validate(self, data): + data = super(TicketSerializer, self).validate(data) + ticket = Ticket( + movie_session=data.get["movie_session"], + row=data.get["row"], + seat=data.get["seat"], + ) + ticket.full_clean() + return data + + +class OrderSerializer(serializers.ModelSerializer): + tickets = TicketSerializer(many=True, read_only=False, allow_empty=False) + + class Meta: + model = Order + fields = ["id", "tickets", "created_at"] + + def create(self, validated_data): + with transaction.atomic(): + tickets_data = validated_data.pop("tickets") + order = Order.objects.create(**validated_data) + for ticket_data in tickets_data: + Ticket.objects.create(order=order, **ticket_data) + return order + + +class OrderDetailSerializer(OrderSerializer): + tickets = TicketDetailSerializer(many=True, read_only=True) diff --git a/cinema/urls.py b/cinema/urls.py index e3586f00..8de69236 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -7,15 +7,19 @@ CinemaHallViewSet, MovieViewSet, MovieSessionViewSet, + OrderViewSet, + TicketViewSet, ) +app_name = "cinema" + router = routers.DefaultRouter() router.register("genres", GenreViewSet) router.register("actors", ActorViewSet) router.register("cinema_halls", CinemaHallViewSet) router.register("movies", MovieViewSet) router.register("movie_sessions", MovieSessionViewSet) +router.register("orders", OrderViewSet, basename="orders") +router.register("tickets", TicketViewSet) urlpatterns = [path("", include(router.urls))] - -app_name = "cinema" diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..df2fa648 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,16 @@ +from django.db.models import F, Count from rest_framework import viewsets -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket, +) +from cinema.pagination import OrdersPagination from cinema.serializers import ( GenreSerializer, @@ -12,6 +22,10 @@ MovieDetailSerializer, MovieSessionDetailSerializer, MovieListSerializer, + OrderSerializer, + TicketSerializer, + TicketDetailSerializer, + OrderDetailSerializer, ) @@ -43,6 +57,28 @@ def get_serializer_class(self): return MovieSerializer + def get_queryset(self): + queryset = super().get_queryset() + if self.action == ("list", "retrieve"): + queryset = self.queryset.prefetch_related("actors", "genres") + + actors = self.request.query_params.get("actors") + genres = self.request.query_params.get("genres") + title = self.request.query_params.get("title") + + if actors: + actors_ids = [int(str_id) for str_id in actors.split(",")] + queryset = queryset.filter(actors__id__in=actors_ids) + + if genres: + genres_ids = [int(str_id) for str_id in genres.split(",")] + queryset = queryset.filter(genres__id__in=genres_ids) + + if title: + queryset = queryset.filter(title__icontains=title) + + return queryset.distinct() + class MovieSessionViewSet(viewsets.ModelViewSet): queryset = MovieSession.objects.all() @@ -56,3 +92,52 @@ def get_serializer_class(self): return MovieSessionDetailSerializer return MovieSessionSerializer + + def get_queryset(self): + queryset = super().get_queryset() + + if self.action == "retrieve": + queryset = queryset.select_related("movie") + + elif self.action == "list": + queryset = ( + queryset + .select_related("cinema_hall") + .annotate( + tickets_available=( + F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") + ) + ) + ).order_by("id") + + movie = self.request.query_params.get("movie") + date = self.request.query_params.get("date") + + if movie: + queryset = queryset.filter(movie__id=movie) + + if date: + queryset = queryset.filter(show_time__date=date) + + return queryset.distinct() + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + pagination_class = OrdersPagination + + def get_serializer_class(self): + if self.action in ["create", "update", "partial_update"]: + return OrderSerializer + return OrderDetailSerializer + + def get_queryset(self): + return Order.objects.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + +class TicketViewSet(viewsets.ModelViewSet): + queryset = Ticket.objects.all() diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..22956ebd 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -124,7 +124,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) @@ -136,3 +136,8 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 10 +} diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..82297d39f22075d768df31551c2dbaa8cacb62ce GIT binary patch literal 225280 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 Date: Wed, 8 Jan 2025 22:02:56 +0100 Subject: [PATCH 2/7] Solution_v2 --- cinema/serializers.py | 3 ++- cinema/views.py | 7 ++++--- cinema_service/settings.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cinema/serializers.py b/cinema/serializers.py index 5335680b..8a20c151 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -10,7 +10,7 @@ MovieSession, Order, Ticket, - ) +) class GenreSerializer(serializers.ModelSerializer): @@ -99,6 +99,7 @@ class Meta: model = Ticket fields = ("id", "row", "seat", "movie_session") + class TicketDetailSerializer(TicketSerializer): movie_session = MovieSessionListSerializer(many=False, read_only=True) diff --git a/cinema/views.py b/cinema/views.py index df2fa648..5c4c6c1b 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -105,9 +105,9 @@ def get_queryset(self): .select_related("cinema_hall") .annotate( tickets_available=( - F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") + F("cinema_hall__rows") * + F("cinema_hall__seats_in_row") - + Count("tickets") ) ) ).order_by("id") @@ -139,5 +139,6 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(user=self.request.user) + class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 22956ebd..96d39158 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -139,5 +139,5 @@ REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", - "PAGE_SIZE": 10 + "PAGE_SIZE": 10, } From 2c81d4853d0913e5a325816400b643065d0d3ff5 Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Wed, 8 Jan 2025 22:08:43 +0100 Subject: [PATCH 3/7] Solution_v3 --- cinema/views.py | 9 ++++----- cinema_service/settings.py | 3 ++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 5c4c6c1b..b6510d67 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -24,7 +24,6 @@ MovieListSerializer, OrderSerializer, TicketSerializer, - TicketDetailSerializer, OrderDetailSerializer, ) @@ -105,9 +104,9 @@ def get_queryset(self): .select_related("cinema_hall") .annotate( tickets_available=( - F("cinema_hall__rows") * - F("cinema_hall__seats_in_row") - - Count("tickets") + F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") ) ) ).order_by("id") @@ -139,6 +138,6 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(user=self.request.user) - class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() + serializer_class = TicketSerializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 96d39158..a93368c3 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -138,6 +138,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "DEFAULT_PAGINATION_CLASS": + "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 10, } From 8eb8f49e4c468143269b6a5bb7acac2345b781b1 Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Wed, 8 Jan 2025 22:10:55 +0100 Subject: [PATCH 4/7] Solution_v4 --- cinema/views.py | 1 + cinema_service/settings.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index b6510d67..69ba117e 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -138,6 +138,7 @@ def get_queryset(self): def perform_create(self, serializer): serializer.save(user=self.request.user) + class TicketViewSet(viewsets.ModelViewSet): queryset = Ticket.objects.all() serializer_class = TicketSerializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a93368c3..a4e585cc 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -138,7 +138,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { - "DEFAULT_PAGINATION_CLASS": - "rest_framework.pagination.LimitOffsetPagination", - "PAGE_SIZE": 10, + "DEFAULT_PAGINATION_CLASS": + "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 10, } From 05b21f6a57afef371206a007f3403a88e48f6ff4 Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Wed, 8 Jan 2025 22:16:29 +0100 Subject: [PATCH 5/7] Solution_v5 --- cinema/serializers.py | 6 +++--- cinema/views.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cinema/serializers.py b/cinema/serializers.py index 8a20c151..651f4c58 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -106,9 +106,9 @@ class TicketDetailSerializer(TicketSerializer): def validate(self, data): data = super(TicketSerializer, self).validate(data) ticket = Ticket( - movie_session=data.get["movie_session"], - row=data.get["row"], - seat=data.get["seat"], + movie_session=data.get("movie_session"), + row=data.get("row"), + seat=data.get("seat"), ) ticket.full_clean() return data diff --git a/cinema/views.py b/cinema/views.py index 69ba117e..ec6d2e1f 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -58,7 +58,7 @@ def get_serializer_class(self): def get_queryset(self): queryset = super().get_queryset() - if self.action == ("list", "retrieve"): + if self.action in ["list", "retrieve"]: queryset = self.queryset.prefetch_related("actors", "genres") actors = self.request.query_params.get("actors") From 3ab1d5b1e03fd60b7f20d4295c850c38a83ece6a Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Fri, 10 Jan 2025 19:41:28 +0100 Subject: [PATCH 6/7] Solution_v6 --- cinema/serializers.py | 39 ++++++++++++++------ cinema/views.py | 74 ++++++++++++++++++-------------------- cinema_service/settings.py | 6 ---- requirements.txt | 13 +++++-- 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/cinema/serializers.py b/cinema/serializers.py index 651f4c58..a24e25a2 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -69,6 +69,7 @@ class MovieSessionListSerializer(MovieSessionSerializer): cinema_hall_capacity = serializers.IntegerField( source="cinema_hall.capacity", read_only=True ) + tickets_available = serializers.SerializerMethodField() class Meta: model = MovieSession @@ -78,16 +79,32 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available" ) + def get_tickets_available(self, obj): + tickets_sold = obj.tickets.count() + return obj.cinema_hall.capacity - tickets_sold + + +class TicketRowSeatSerializer(serializers.ModelSerializer): + 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 = TicketRowSeatSerializer( + many=True, + read_only=True, + source="tickets" + ) class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") class TicketSerializer(serializers.ModelSerializer): @@ -99,20 +116,20 @@ class Meta: model = Ticket fields = ("id", "row", "seat", "movie_session") + def validate(self, attrs): + movie_session = attrs["movie_session"] + seats_in_row = movie_session.cinema_hall.seats_in_row + if not (1 <= attrs["seat"] <= seats_in_row): + raise serializers.ValidationError( + { + "seat": f"seats must be between 1 and {seats_in_row}" + } + ) + class TicketDetailSerializer(TicketSerializer): movie_session = MovieSessionListSerializer(many=False, read_only=True) - def validate(self, data): - data = super(TicketSerializer, self).validate(data) - ticket = Ticket( - movie_session=data.get("movie_session"), - row=data.get("row"), - seat=data.get("seat"), - ) - ticket.full_clean() - return data - class OrderSerializer(serializers.ModelSerializer): tickets = TicketSerializer(many=True, read_only=False, allow_empty=False) diff --git a/cinema/views.py b/cinema/views.py index ec6d2e1f..5517aa3d 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,14 +1,14 @@ -from django.db.models import F, Count +from datetime import datetime +from rest_framework.exceptions import ValidationError from rest_framework import viewsets - from cinema.models import ( Genre, Actor, CinemaHall, Movie, - MovieSession, Order, Ticket, + MovieSession, ) from cinema.pagination import OrdersPagination @@ -17,14 +17,14 @@ ActorSerializer, CinemaHallSerializer, MovieSerializer, - MovieSessionSerializer, - MovieSessionListSerializer, MovieDetailSerializer, - MovieSessionDetailSerializer, MovieListSerializer, OrderSerializer, TicketSerializer, OrderDetailSerializer, + MovieSessionListSerializer, + MovieSessionDetailSerializer, + MovieSessionSerializer, ) @@ -57,68 +57,62 @@ def get_serializer_class(self): return MovieSerializer def get_queryset(self): - queryset = super().get_queryset() - if self.action in ["list", "retrieve"]: - queryset = self.queryset.prefetch_related("actors", "genres") - actors = self.request.query_params.get("actors") genres = self.request.query_params.get("genres") title = self.request.query_params.get("title") + queryset = Movie.objects.all() + if actors: actors_ids = [int(str_id) for str_id in actors.split(",")] - queryset = queryset.filter(actors__id__in=actors_ids) + queryset = Movie.objects.filter(actors__id__in=actors_ids) if genres: genres_ids = [int(str_id) for str_id in genres.split(",")] - queryset = queryset.filter(genres__id__in=genres_ids) + queryset = Movie.objects.filter(genres__id__in=genres_ids) if title: queryset = queryset.filter(title__icontains=title) + if self.action in ["retrieve", "list"]: + queryset = queryset.prefetch_related("genres", "actors") + return queryset.distinct() class MovieSessionViewSet(viewsets.ModelViewSet): queryset = MovieSession.objects.all() - serializer_class = MovieSessionSerializer + serializer_class = MovieSessionListSerializer def get_serializer_class(self): - if self.action == "list": - return MovieSessionListSerializer - if self.action == "retrieve": return MovieSessionDetailSerializer - - return MovieSessionSerializer + elif self.action == "create": + return MovieSessionSerializer + return MovieSessionListSerializer def get_queryset(self): - queryset = super().get_queryset() - - if self.action == "retrieve": - queryset = queryset.select_related("movie") - - elif self.action == "list": - queryset = ( - queryset - .select_related("cinema_hall") - .annotate( - tickets_available=( - F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") - ) - ) - ).order_by("id") - - movie = self.request.query_params.get("movie") date = self.request.query_params.get("date") - + movie = self.request.query_params.get("movie") + queryset = MovieSession.objects.all() if movie: - queryset = queryset.filter(movie__id=movie) + try: + movie_ids = [int(str_id) for str_id in movie.split(",")] + queryset = queryset.filter(movie_id__in=movie_ids) + except ValueError: + raise ValidationError( + {"movie": "Invalid movie ID. Must be an integer."} + ) if date: - queryset = queryset.filter(show_time__date=date) + try: + parsed_date = datetime.strptime(date, "%Y-%m-%d").date() + queryset = queryset.filter(show_time__date=parsed_date) + except ValueError: + raise ValidationError({"date": "Invalid date format. Use YYYY-MM-DD."}) + + if self.action in ["retrieve", "list"]: + queryset = queryset.prefetch_related("movie") return queryset.distinct() diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a4e585cc..068db3d6 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -136,9 +136,3 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - -REST_FRAMEWORK = { - "DEFAULT_PAGINATION_CLASS": - "rest_framework.pagination.LimitOffsetPagination", - "PAGE_SIZE": 10, -} diff --git a/requirements.txt b/requirements.txt index 56e13554..af5a1d76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,14 @@ -django==4.1 +asgiref==3.8.1 +Django==4.1 +django-debug-toolbar==3.2.4 +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 -django-debug-toolbar==3.2.4 -djangorestframework==3.13.1 \ No newline at end of file +pycodestyle==2.9.1 +pyflakes==2.5.0 +pytz==2024.2 +sqlparse==0.5.3 +tzdata==2024.2 From 7dbf0c9d25c2cb8f4f2b37627f3c43e620e0b1e8 Mon Sep 17 00:00:00 2001 From: Anton Hryhorenko Date: Fri, 10 Jan 2025 19:42:49 +0100 Subject: [PATCH 7/7] Solution_v7 --- cinema/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cinema/views.py b/cinema/views.py index 5517aa3d..4e81792d 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -109,7 +109,9 @@ def get_queryset(self): parsed_date = datetime.strptime(date, "%Y-%m-%d").date() queryset = queryset.filter(show_time__date=parsed_date) except ValueError: - raise ValidationError({"date": "Invalid date format. Use YYYY-MM-DD."}) + raise ValidationError( + {"date": "Invalid date format. Use YYYY-MM-DD."} + ) if self.action in ["retrieve", "list"]: queryset = queryset.prefetch_related("movie")