From 59e8d34fdffc84a5fe00ced2c5fa1dc60bb2ffd8 Mon Sep 17 00:00:00 2001 From: Yakiv Lobach Date: Tue, 1 Oct 2024 17:54:27 +0300 Subject: [PATCH 1/6] first_try --- cinema/models.py | 49 ++++++++++++++-------- cinema/pagination.py | 7 ++++ cinema/serializers.py | 62 ++++++++++++++++++++++++++- cinema/tests/test_actor_api.py | 1 + cinema/urls.py | 3 +- cinema/utils.py | 2 + cinema/views.py | 77 +++++++++++++++++++++++++++++++++- cinema_service/settings.py | 2 +- 8 files changed, 180 insertions(+), 23 deletions(-) create mode 100644 cinema/pagination.py create mode 100644 cinema/utils.py diff --git a/cinema/models.py b/cinema/models.py index f18f166c..7fed12dd 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -84,30 +84,45 @@ class Ticket(models.Model): row = models.IntegerField() seat = models.IntegerField() - def clean(self): - for ticket_attr_value, ticket_attr_name, cinema_hall_attr_name in [ - (self.row, "row", "rows"), - (self.seat, "seat", "seats_in_row"), + @staticmethod + def validate_seats( + seat: int, + row: int, + movie_session: MovieSession, + error_to_raise + ): + for ticket_atr_value, ticket_attr_name, cinema_hall_attr_name in [ + (seat, "seat", "seats_in_row"), + (row, "row", "rows") ]: count_attrs = getattr( - self.movie_session.cinema_hall, cinema_hall_attr_name - ) - if not (1 <= ticket_attr_value <= count_attrs): - raise ValidationError( + movie_session.cinema_hall, + cinema_hall_attr_name) + if not (1 <= ticket_atr_value <= count_attrs): + raise error_to_raise( { - ticket_attr_name: f"{ticket_attr_name} " - f"number must be in available range: " - f"(1, {cinema_hall_attr_name}): " - f"(1, {count_attrs})" + ticket_attr_name: + f"{ticket_attr_name} number must be " + f"in available range: " + f"(1, {cinema_hall_attr_name}): (1, {count_attrs})" } + ) + def clean(self): + Ticket.validate_seats( + self.seat, + self.row, + self.movie_session, + ValidationError + ) + def save( - self, - force_insert=False, - force_update=False, - using=None, - update_fields=None, + self, + force_insert=False, + force_update=False, + using=None, + update_fields=None, ): self.full_clean() super(Ticket, self).save( diff --git a/cinema/pagination.py b/cinema/pagination.py new file mode 100644 index 00000000..faadb8d7 --- /dev/null +++ b/cinema/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class OrderSetPagination(PageNumberPagination): + page_size = 3 + page_size_query_param = "page_size" + max_page_size = 5 diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..829c0f43 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,15 @@ +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, + Ticket, + Order +) class GenreSerializer(serializers.ModelSerializer): @@ -59,6 +68,7 @@ class MovieSessionListSerializer(MovieSessionSerializer): cinema_hall_capacity = serializers.IntegerField( source="cinema_hall.capacity", read_only=True ) + tickets_available = serializers.IntegerField(read_only=True) class Meta: model = MovieSession @@ -68,13 +78,61 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available", ) class MovieSessionDetailSerializer(MovieSessionSerializer): movie = MovieListSerializer(many=False, read_only=True) cinema_hall = CinemaHallSerializer(many=False, read_only=True) + taken_places = serializers.SerializerMethodField() + + def get_taken_places(self, movie_session): + return [ + {"row": ticket.row, "seat": ticket.seat} + for ticket in movie_session.tickets.all() + ] class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") + + +class TicketSerializer(serializers.ModelSerializer): + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + def validate(self, attr): + Ticket.validate_seats( + attr["seat"], + attr["row"], + attr["movie_session"], + error_to_raise=serializers.ValidationError( + "This seat is already taken" + ), + ) + + +class TicketListSerializer(TicketSerializer): + movie_session = MovieSessionListSerializer(read_only=True, many=False) + + +class OrderSerializer(serializers.ModelSerializer): + tickets = TicketSerializer(many=True, allow_empty=False) + + class Meta: + model = Order + fields = ("id", "created_at", "tickets") + + 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 OrderListSerializer(OrderSerializer): + tickets = TicketListSerializer(many=True, read_only=True) diff --git a/cinema/tests/test_actor_api.py b/cinema/tests/test_actor_api.py index 3fa7a469..c1c629fb 100644 --- a/cinema/tests/test_actor_api.py +++ b/cinema/tests/test_actor_api.py @@ -14,6 +14,7 @@ def setUp(self): def test_get_actors(self): response = self.client.get("/api/cinema/actors/") + print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) actors_full_names = [actor["full_name"] for actor in response.data] self.assertEqual( diff --git a/cinema/urls.py b/cinema/urls.py index e3586f00..b997592c 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -6,7 +6,7 @@ ActorViewSet, CinemaHallViewSet, MovieViewSet, - MovieSessionViewSet, + MovieSessionViewSet, OrderViewSet, ) router = routers.DefaultRouter() @@ -15,6 +15,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/utils.py b/cinema/utils.py new file mode 100644 index 00000000..303da11b --- /dev/null +++ b/cinema/utils.py @@ -0,0 +1,2 @@ +def filter_request_to_int(query_params): + return [int(str_id) for str_id in query_params.split(",")] diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..78c5f602 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,9 @@ +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 +from cinema.pagination import OrderSetPagination +from cinema.utils import filter_request_to_int from cinema.serializers import ( GenreSerializer, @@ -11,7 +14,7 @@ MovieSessionListSerializer, MovieDetailSerializer, MovieSessionDetailSerializer, - MovieListSerializer, + MovieListSerializer, OrderSerializer, OrderListSerializer, ) @@ -43,6 +46,28 @@ def get_serializer_class(self): return MovieSerializer + def get_queryset(self): + queryset = self.queryset + + if self.action == "list": + queryset = self.queryset.prefetch_related("actors", "genres") + + genres = self.request.query_params.get("genres") + actors = self.request.query_params.get("actors") + title = self.request.query_params.get("title") + if genres: + genres = filter_request_to_int(genres) + queryset = queryset.filter(genres__id__in=genres).distinct() + + if actors: + actors = filter_request_to_int(actors) + queryset = queryset.filter(actors__id__in=actors).distinct() + + if title: + queryset = queryset.filter(title__icontains=title) + + return queryset + class MovieSessionViewSet(viewsets.ModelViewSet): queryset = MovieSession.objects.all() @@ -56,3 +81,51 @@ def get_serializer_class(self): return MovieSessionDetailSerializer return MovieSessionSerializer + + def get_queryset(self): + queryset = self.queryset + if self.action == "list": + queryset = queryset.select_related() + 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) + + queryset = queryset.annotate( + tickets_available=F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") + ) + + return queryset + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + serializer_class = OrderSerializer + pagination_class = OrderSetPagination + + def get_queryset(self): + queryset = self.queryset.filter(user=self.request.user) + + if self.action in ("list", "retrieve"): + queryset = queryset.prefetch_related( + "tickets__movie_session__movie", + "tickets__movie_session__cinema_hall" + ) + + return queryset + + def get_serializer_class(self): + serializer = self.serializer_class + if self.action in ("list", "retrieve"): + return OrderListSerializer + + return serializer + + 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..068db3d6 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) From eb1f0456e45dc3ae48829c3a04bc9d6c69bfac97 Mon Sep 17 00:00:00 2001 From: Yakiv Lobach Date: Wed, 2 Oct 2024 14:18:18 +0300 Subject: [PATCH 2/6] first_try --- cinema/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cinema/serializers.py b/cinema/serializers.py index 829c0f43..45d6d2d9 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -112,6 +112,7 @@ def validate(self, attr): "This seat is already taken" ), ) + return attr class TicketListSerializer(TicketSerializer): From 46699486cf6df68146e6cdb934016dd00f8fc7b7 Mon Sep 17 00:00:00 2001 From: Yakiv Lobach Date: Wed, 2 Oct 2024 14:38:19 +0300 Subject: [PATCH 3/6] first_try --- cinema/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cinema/views.py b/cinema/views.py index 78c5f602..f4b1223d 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -100,6 +100,8 @@ def get_queryset(self): * F("cinema_hall__seats_in_row") - Count("tickets") ) + if self.action == "retrieve": + queryset = queryset.select_related("movie", "cinema_hall") return queryset From 18d3949b0c2d4855bdb8d19873d3568608a69cbc Mon Sep 17 00:00:00 2001 From: Yakiv Lobach Date: Thu, 10 Oct 2024 14:34:22 +0300 Subject: [PATCH 4/6] second_try --- cinema/models.py | 4 ++-- cinema/serializers.py | 9 ++++----- cinema/tests/test_actor_api.py | 1 - cinema/urls.py | 3 ++- cinema/utils.py | 2 +- cinema/views.py | 14 ++++++++------ db.sqlite3 | Bin 0 -> 225280 bytes 7 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 db.sqlite3 diff --git a/cinema/models.py b/cinema/models.py index 7fed12dd..3a17f258 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -31,7 +31,7 @@ def __str__(self): return self.first_name + " " + self.last_name @property - def full_name(self): + def full_name(self) -> str: return f"{self.first_name} {self.last_name}" @@ -90,7 +90,7 @@ def validate_seats( row: int, movie_session: MovieSession, error_to_raise - ): + ) -> None: for ticket_atr_value, ticket_attr_name, cinema_hall_attr_name in [ (seat, "seat", "seats_in_row"), (row, "row", "rows") diff --git a/cinema/serializers.py b/cinema/serializers.py index 45d6d2d9..e47cadca 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -83,11 +83,11 @@ class Meta: class MovieSessionDetailSerializer(MovieSessionSerializer): - movie = MovieListSerializer(many=False, read_only=True) - cinema_hall = CinemaHallSerializer(many=False, read_only=True) + movie = MovieListSerializer(read_only=True) + cinema_hall = CinemaHallSerializer(read_only=True) taken_places = serializers.SerializerMethodField() - def get_taken_places(self, movie_session): + def get_taken_places(self, movie_session: MovieSession) -> list: return [ {"row": ticket.row, "seat": ticket.seat} for ticket in movie_session.tickets.all() @@ -103,7 +103,7 @@ class Meta: model = Ticket fields = ("id", "row", "seat", "movie_session") - def validate(self, attr): + def validate(self, attr: dict) -> dict: Ticket.validate_seats( attr["seat"], attr["row"], @@ -132,7 +132,6 @@ def create(self, validated_data): order = Order.objects.create(**validated_data) for ticket_data in tickets_data: Ticket.objects.create(order=order, **ticket_data) - return order class OrderListSerializer(OrderSerializer): diff --git a/cinema/tests/test_actor_api.py b/cinema/tests/test_actor_api.py index c1c629fb..3fa7a469 100644 --- a/cinema/tests/test_actor_api.py +++ b/cinema/tests/test_actor_api.py @@ -14,7 +14,6 @@ def setUp(self): def test_get_actors(self): response = self.client.get("/api/cinema/actors/") - print(response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) actors_full_names = [actor["full_name"] for actor in response.data] self.assertEqual( diff --git a/cinema/urls.py b/cinema/urls.py index b997592c..5ad6fb5b 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -6,7 +6,8 @@ ActorViewSet, CinemaHallViewSet, MovieViewSet, - MovieSessionViewSet, OrderViewSet, + MovieSessionViewSet, + OrderViewSet, ) router = routers.DefaultRouter() diff --git a/cinema/utils.py b/cinema/utils.py index 303da11b..f7f58b3a 100644 --- a/cinema/utils.py +++ b/cinema/utils.py @@ -1,2 +1,2 @@ -def filter_request_to_int(query_params): +def filter_request_to_int(query_params: str) -> list: return [int(str_id) for str_id in query_params.split(",")] diff --git a/cinema/views.py b/cinema/views.py index f4b1223d..34995716 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -14,7 +14,9 @@ MovieSessionListSerializer, MovieDetailSerializer, MovieSessionDetailSerializer, - MovieListSerializer, OrderSerializer, OrderListSerializer, + MovieListSerializer, + OrderSerializer, + OrderListSerializer, ) @@ -47,7 +49,7 @@ def get_serializer_class(self): return MovieSerializer def get_queryset(self): - queryset = self.queryset + queryset = super().get_queryset() if self.action == "list": queryset = self.queryset.prefetch_related("actors", "genres") @@ -57,16 +59,16 @@ def get_queryset(self): title = self.request.query_params.get("title") if genres: genres = filter_request_to_int(genres) - queryset = queryset.filter(genres__id__in=genres).distinct() + queryset = queryset.filter(genres__id__in=genres) if actors: actors = filter_request_to_int(actors) - queryset = queryset.filter(actors__id__in=actors).distinct() + queryset = queryset.filter(actors__id__in=actors) if title: queryset = queryset.filter(title__icontains=title) - return queryset + return queryset.distinct() class MovieSessionViewSet(viewsets.ModelViewSet): @@ -83,7 +85,7 @@ def get_serializer_class(self): return MovieSessionSerializer def get_queryset(self): - queryset = self.queryset + queryset = super().get_queryset() if self.action == "list": queryset = queryset.select_related() movie = self.request.query_params.get("movie") diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..c9cc58419bae6a5c874e15ed74645c54dc797034 GIT binary patch literal 225280 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 Date: Tue, 15 Oct 2024 15:37:29 +0300 Subject: [PATCH 5/6] added TicketSeatsSerializer and fixed indentation --- cinema/models.py | 21 +++++++++++---------- cinema/serializers.py | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/cinema/models.py b/cinema/models.py index 3a17f258..6d442846 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -97,14 +97,15 @@ def validate_seats( ]: count_attrs = getattr( movie_session.cinema_hall, - cinema_hall_attr_name) + cinema_hall_attr_name + ) if not (1 <= ticket_atr_value <= count_attrs): raise error_to_raise( { ticket_attr_name: - f"{ticket_attr_name} number must be " - f"in available range: " - f"(1, {cinema_hall_attr_name}): (1, {count_attrs})" + f"{ticket_attr_name} number must be " + f"in available range: " + f"(1, {cinema_hall_attr_name}): (1, {count_attrs})" } ) @@ -118,14 +119,14 @@ def clean(self): ) def save( - self, - force_insert=False, - force_update=False, - using=None, - update_fields=None, + self, + force_insert=False, + force_update=False, + using=None, + update_fields=None, ): self.full_clean() - super(Ticket, self).save( + return super(Ticket, self).save( force_insert, force_update, using, update_fields ) diff --git a/cinema/serializers.py b/cinema/serializers.py index e47cadca..9b9b2a79 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -82,22 +82,6 @@ class Meta: ) -class MovieSessionDetailSerializer(MovieSessionSerializer): - movie = MovieListSerializer(read_only=True) - cinema_hall = CinemaHallSerializer(read_only=True) - taken_places = serializers.SerializerMethodField() - - def get_taken_places(self, movie_session: MovieSession) -> list: - return [ - {"row": ticket.row, "seat": ticket.seat} - for ticket in movie_session.tickets.all() - ] - - class Meta: - model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") - - class TicketSerializer(serializers.ModelSerializer): class Meta: model = Ticket @@ -115,6 +99,32 @@ def validate(self, attr: dict) -> dict: return attr +class TicketSeatsSerializer(TicketSerializer): + class Meta: + model = Ticket + fields = ("row", "seat") + + +class MovieSessionDetailSerializer(MovieSessionSerializer): + movie = MovieListSerializer(read_only=True) + cinema_hall = CinemaHallSerializer(read_only=True) + taken_places = TicketSeatsSerializer( + source="tickets", + many=True, + read_only=True + ) + + def get_taken_places(self, movie_session: MovieSession) -> list: + return [ + {"row": ticket.row, "seat": ticket.seat} + for ticket in movie_session.tickets.all() + ] + + class Meta: + model = MovieSession + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") + + class TicketListSerializer(TicketSerializer): movie_session = MovieSessionListSerializer(read_only=True, many=False) From 5a68c868935b9c7f53e39a33cf7df753ec17edcc Mon Sep 17 00:00:00 2001 From: Yakiv Lobach Date: Thu, 17 Oct 2024 10:00:59 +0300 Subject: [PATCH 6/6] Remove database from repository and add to .gitignore --- .gitignore | 3 ++- db.sqlite3 | Bin 225280 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 db.sqlite3 diff --git a/.gitignore b/.gitignore index fd1f3e48..6be49d86 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 +**db.sqlite3 diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index c9cc58419bae6a5c874e15ed74645c54dc797034..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 225280 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