diff --git a/.gitignore b/.gitignore index fd1f3e48..0d634a9e 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 \ No newline at end of file diff --git a/cinema/migrations/0003_movie_duration.py b/cinema/migrations/0003_movie_duration.py index 7355c91a..e75f6794 100644 --- a/cinema/migrations/0003_movie_duration.py +++ b/cinema/migrations/0003_movie_duration.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('cinema', '0002_initial'), + ("cinema", "0002_initial"), ] operations = [ migrations.AddField( - model_name='movie', - name='duration', + model_name="movie", + name="duration", field=models.IntegerField(default=123), preserve_default=False, ), diff --git a/cinema/migrations/0004_alter_genre_name.py b/cinema/migrations/0004_alter_genre_name.py index 83f65fd1..eb3d0ffb 100644 --- a/cinema/migrations/0004_alter_genre_name.py +++ b/cinema/migrations/0004_alter_genre_name.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('cinema', '0003_movie_duration'), + ("cinema", "0003_movie_duration"), ] operations = [ migrations.AlterField( - model_name='genre', - name='name', + model_name="genre", + name="name", field=models.CharField(max_length=255, unique=True), ), ] diff --git a/cinema/models.py b/cinema/models.py index f18f166c..793d192d 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -63,9 +63,8 @@ def __str__(self): class Order(models.Model): created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE - ) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE) def __str__(self): return str(self.created_at) @@ -78,20 +77,19 @@ class Ticket(models.Model): movie_session = models.ForeignKey( MovieSession, on_delete=models.CASCADE, related_name="tickets" ) - order = models.ForeignKey( - Order, on_delete=models.CASCADE, related_name="tickets" - ) + order = models.ForeignKey(Order, on_delete=models.CASCADE, + related_name="tickets") row = models.IntegerField() seat = models.IntegerField() def clean(self): - for ticket_attr_value, ticket_attr_name, cinema_hall_attr_name in [ + for (ticket_attr_value, ticket_attr_name, + cinema_hall_attr_name) in [ (self.row, "row", "rows"), (self.seat, "seat", "seats_in_row"), ]: - count_attrs = getattr( - self.movie_session.cinema_hall, cinema_hall_attr_name - ) + count_attrs = getattr(self.movie_session.cinema_hall, + cinema_hall_attr_name) if not (1 <= ticket_attr_value <= count_attrs): raise ValidationError( { @@ -110,14 +108,13 @@ def save( update_fields=None, ): self.full_clean() - super(Ticket, self).save( - force_insert, force_update, using, update_fields - ) + super(Ticket, self).save(force_insert, force_update, using, + update_fields) def __str__(self): - return ( - f"{str(self.movie_session)} (row: {self.row}, seat: {self.seat})" - ) + return (f"{str(self.movie_session)} (row: {self.row}, seat: " + f"{self.seat})") class Meta: unique_together = ("movie_session", "row", "seat") + ordering = ("row",) diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..d1c9590d 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,14 @@ +from django.db import transaction from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import (Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket) class GenreSerializer(serializers.ModelSerializer): @@ -28,9 +36,9 @@ class Meta: class MovieListSerializer(MovieSerializer): - genres = serializers.SlugRelatedField( - many=True, read_only=True, slug_field="name" - ) + genres = serializers.SlugRelatedField(many=True, + read_only=True, + slug_field="name") actors = serializers.SlugRelatedField( many=True, read_only=True, slug_field="full_name" ) @@ -52,13 +60,14 @@ class Meta: class MovieSessionListSerializer(MovieSessionSerializer): - movie_title = serializers.CharField(source="movie.title", read_only=True) - cinema_hall_name = serializers.CharField( - source="cinema_hall.name", read_only=True - ) + movie_title = serializers.CharField(source="movie.title", + read_only=True) + cinema_hall_name = serializers.CharField(source="cinema_hall.name", + read_only=True) 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 +77,65 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available", + ) + + +class TicketDetailSerializer(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 = TicketDetailSerializer(many=True, source="tickets", + read_only=True) class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") + + +class TicketSerializer(serializers.ModelSerializer): + movie_session = MovieSessionListSerializer(read_only=True) + + class Meta: + model = Ticket + fields = ( + "id", + "row", + "seat", + "movie_session", + ) + validators = [ + UniqueTogetherValidator( + queryset=Ticket.objects.all(), fields=("row", + "seat", + "movie_session") + ) + ] + + +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 diff --git a/cinema/tests/test_actor_api.py b/cinema/tests/test_actor_api.py index 3fa7a469..097e42ca 100644 --- a/cinema/tests/test_actor_api.py +++ b/cinema/tests/test_actor_api.py @@ -16,9 +16,7 @@ def test_get_actors(self): response = self.client.get("/api/cinema/actors/") self.assertEqual(response.status_code, status.HTTP_200_OK) actors_full_names = [actor["full_name"] for actor in response.data] - self.assertEqual( - sorted(actors_full_names), ["George Clooney", "Keanu Reeves"] - ) + self.assertEqual(sorted(actors_full_names), ["George Clooney", "Keanu Reeves"]) def test_post_actors(self): response = self.client.post( diff --git a/cinema/tests/test_cinema_hall_api.py b/cinema/tests/test_cinema_hall_api.py index d3e9ea77..be74b47a 100644 --- a/cinema/tests/test_cinema_hall_api.py +++ b/cinema/tests/test_cinema_hall_api.py @@ -31,9 +31,7 @@ def test_get_cinema_halls(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data[0]["name"], blue_hall["name"]) self.assertEqual(response.data[0]["rows"], blue_hall["rows"]) - self.assertEqual( - response.data[0]["seats_in_row"], blue_hall["seats_in_row"] - ) + self.assertEqual(response.data[0]["seats_in_row"], blue_hall["seats_in_row"]) vip_hall = { "name": "VIP", "rows": 6, @@ -43,9 +41,7 @@ def test_get_cinema_halls(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data[1]["name"], vip_hall["name"]) self.assertEqual(response.data[1]["rows"], vip_hall["rows"]) - self.assertEqual( - response.data[1]["seats_in_row"], vip_hall["seats_in_row"] - ) + self.assertEqual(response.data[1]["seats_in_row"], vip_hall["seats_in_row"]) def test_post_cinema_halls(self): response = self.client.post( @@ -72,9 +68,7 @@ def test_get_cinema_hall(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["name"], vip_hall["name"]) self.assertEqual(response.data["rows"], vip_hall["rows"]) - self.assertEqual( - response.data["seats_in_row"], vip_hall["seats_in_row"] - ) + self.assertEqual(response.data["seats_in_row"], vip_hall["seats_in_row"]) self.assertEqual(response.data["capacity"], vip_hall["capacity"]) def test_get_invalid_cinema_hall(self): diff --git a/cinema/tests/test_movie_api.py b/cinema/tests/test_movie_api.py index 958c09a1..c129b279 100644 --- a/cinema/tests/test_movie_api.py +++ b/cinema/tests/test_movie_api.py @@ -15,9 +15,7 @@ def setUp(self): self.comedy = Genre.objects.create( name="Comedy", ) - self.actress = Actor.objects.create( - first_name="Kate", last_name="Winslet" - ) + self.actress = Actor.objects.create(first_name="Kate", last_name="Winslet") self.movie = Movie.objects.create( title="Titanic", description="Titanic description", @@ -42,21 +40,15 @@ def test_get_movies(self): self.assertEqual(movies.data[0][field], titanic[field]) def test_get_movies_with_genres_filtering(self): - movies = self.client.get( - f"/api/cinema/movies/?genres={self.comedy.id}" - ) + movies = self.client.get(f"/api/cinema/movies/?genres={self.comedy.id}") self.assertEqual(len(movies.data), 1) - movies = self.client.get( - f"/api/cinema/movies/?genres={self.comedy.id},2,3" - ) + movies = self.client.get(f"/api/cinema/movies/?genres={self.comedy.id},2,3") self.assertEqual(len(movies.data), 1) movies = self.client.get("/api/cinema/movies/?genres=123213") self.assertEqual(len(movies.data), 0) def test_get_movies_with_actors_filtering(self): - movies = self.client.get( - f"/api/cinema/movies/?actors={self.actress.id}" - ) + movies = self.client.get(f"/api/cinema/movies/?actors={self.actress.id}") self.assertEqual(len(movies.data), 1) movies = self.client.get(f"/api/cinema/movies/?actors={123}") self.assertEqual(len(movies.data), 0) @@ -111,9 +103,7 @@ def test_get_movie(self): self.assertEqual(response.data["genres"][1]["name"], "Comedy") self.assertEqual(response.data["actors"][0]["first_name"], "Kate") self.assertEqual(response.data["actors"][0]["last_name"], "Winslet") - self.assertEqual( - response.data["actors"][0]["full_name"], "Kate Winslet" - ) + self.assertEqual(response.data["actors"][0]["full_name"], "Kate Winslet") def test_get_invalid_movie(self): response = self.client.get("/api/cinema/movies/100/") diff --git a/cinema/tests/test_movie_session_api.py b/cinema/tests/test_movie_session_api.py index 9b75d7ed..7ce021d0 100644 --- a/cinema/tests/test_movie_session_api.py +++ b/cinema/tests/test_movie_session_api.py @@ -34,12 +34,7 @@ def setUp(self): self.movie_session = MovieSession.objects.create( movie=self.movie, cinema_hall=self.cinema_hall, - show_time=datetime.datetime( - year=2022, - month=9, - day=2, - hour=9 - ), + show_time=datetime.datetime(year=2022, month=9, day=2, hour=9), ) def test_get_movie_sessions(self): @@ -51,20 +46,14 @@ def test_get_movie_sessions(self): } self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) for field in movie_session: - self.assertEqual( - movie_sessions.data[0][field], movie_session[field] - ) + self.assertEqual(movie_sessions.data[0][field], movie_session[field]) def test_get_movie_sessions_filtered_by_date(self): - movie_sessions = self.client.get( - "/api/cinema/movie_sessions/?date=2022-09-02" - ) + movie_sessions = self.client.get("/api/cinema/movie_sessions/?date=2022-09-02") self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 1) - movie_sessions = self.client.get( - "/api/cinema/movie_sessions/?date=2022-09-01" - ) + movie_sessions = self.client.get("/api/cinema/movie_sessions/?date=2022-09-01") self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 0) @@ -75,9 +64,7 @@ def test_get_movie_sessions_filtered_by_movie(self): self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 1) - movie_sessions = self.client.get( - "/api/cinema/movie_sessions/?movie=1234" - ) + movie_sessions = self.client.get("/api/cinema/movie_sessions/?movie=1234") self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) self.assertEqual(len(movie_sessions.data), 0) @@ -117,9 +104,7 @@ def test_get_movie_session(self): response = self.client.get("/api/cinema/movie_sessions/1/") self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["movie"]["title"], "Titanic") - self.assertEqual( - response.data["movie"]["description"], "Titanic description" - ) + self.assertEqual(response.data["movie"]["description"], "Titanic description") self.assertEqual(response.data["movie"]["duration"], 123) self.assertEqual(response.data["movie"]["genres"], ["Drama", "Comedy"]) self.assertEqual(response.data["movie"]["actors"], ["Kate Winslet"]) diff --git a/cinema/tests/test_order_api.py b/cinema/tests/test_order_api.py index dd15df53..b510ef58 100644 --- a/cinema/tests/test_order_api.py +++ b/cinema/tests/test_order_api.py @@ -26,9 +26,7 @@ def setUp(self): self.comedy = Genre.objects.create( name="Comedy", ) - self.actress = Actor.objects.create( - first_name="Kate", last_name="Winslet" - ) + self.actress = Actor.objects.create(first_name="Kate", last_name="Winslet") self.movie = Movie.objects.create( title="Titanic", description="Titanic description", @@ -73,12 +71,8 @@ def test_movie_session_detail_tickets(self): f"/api/cinema/movie_sessions/{self.movie_session.id}/" ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.data["taken_places"][0]["row"], self.ticket.row - ) - self.assertEqual( - response.data["taken_places"][0]["seat"], self.ticket.seat - ) + self.assertEqual(response.data["taken_places"][0]["row"], self.ticket.row) + self.assertEqual(response.data["taken_places"][0]["seat"], self.ticket.seat) def test_movie_session_list_tickets_available(self): response = self.client.get(f"/api/cinema/movie_sessions/") 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..a5de4e95 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,12 @@ +from django.db.models import Count, F from rest_framework import viewsets - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from rest_framework.pagination import PageNumberPagination +from cinema.models import (Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order) from cinema.serializers import ( GenreSerializer, @@ -12,6 +18,7 @@ MovieDetailSerializer, MovieSessionDetailSerializer, MovieListSerializer, + OrderSerializer, ) @@ -33,6 +40,10 @@ class CinemaHallViewSet(viewsets.ModelViewSet): class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer + # filter_backends = [DjangoFilterBackend, SearchFilter] + # filterset_class = MovieFilter + # filterset_fields = ["actors", "genres",] + # search_fields = ['title'] def get_serializer_class(self): if self.action == "list": @@ -43,6 +54,30 @@ def get_serializer_class(self): return MovieSerializer + @staticmethod + def _params_to_ints(query_string): + return [int(str_id) for str_id in query_string.split(",")] + + def get_queryset(self): + queryset = self.queryset + + genres = self.request.query_params.get("genres") + actors = self.request.query_params.get("actors") + title = self.request.query_params.get("title") + + if genres: + genres = self._params_to_ints(genres) + queryset = queryset.filter(genres__id__in=genres) + + if actors: + actors = self._params_to_ints(actors) + queryset = queryset.filter(actors__id__in=actors) + + if title: + queryset = queryset.filter(title__icontains=title) + + return queryset.distinct() + class MovieSessionViewSet(viewsets.ModelViewSet): queryset = MovieSession.objects.all() @@ -56,3 +91,55 @@ def get_serializer_class(self): return MovieSessionDetailSerializer return MovieSessionSerializer + + def get_queryset(self): + queryset = self.queryset + + date = self.request.query_params.get("date") + movie = self.request.query_params.get("movie") + + if date: + queryset = queryset.filter(show_time__date=date) + + if movie: + movie_ids = [int(str_id) for str_id in movie.split(",")] + queryset = queryset.filter(movie__id__in=movie_ids) + + if self.action == "list": + queryset = queryset.select_related( + "cinema_hall", + "movie", + ).annotate( + tickets_available=F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") + ) + + return queryset.distinct() + + +class Pagination(PageNumberPagination): + page_size = 2 + page_size_query_param = "page_size" + max_page_size = 20 + + +class OrderViewSet(viewsets.ModelViewSet): + serializer_class = OrderSerializer + queryset = Order.objects.prefetch_related( + "tickets__movie_session__movie", + "tickets__movie_session__cinema_hall" + ) + pagination_class = Pagination + + def get_queryset(self): + queryset = self.queryset.filter(user=self.request.user) + return queryset + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def get_serializer_class(self): + serializer = self.serializer_class + + return serializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..66b5f2f2 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -20,9 +20,8 @@ # 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 = ("django-insecure-6vubhk2$++agn" + "ctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3") # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -101,15 +100,15 @@ }, { "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", + "MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", + "CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", + "NumericPasswordValidator", }, ] @@ -120,7 +119,7 @@ LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Kyiv" USE_I18N = True