From 676171f8b1eecfe5c1fbae41f6c4acfff2e72927 Mon Sep 17 00:00:00 2001 From: Yulia Pylypets Date: Fri, 13 Dec 2024 07:42:46 +0200 Subject: [PATCH 1/2] Solution --- .gitignore | 4 +- cinema/migrations/0003_movie_duration.py | 6 +- cinema/migrations/0004_alter_genre_name.py | 6 +- cinema/models.py | 106 ++++++++------------- cinema/serializers.py | 65 ++++++++----- cinema/tests/test_actor_api.py | 4 +- cinema/tests/test_cinema_hall_api.py | 12 +-- cinema/tests/test_movie_api.py | 20 +--- cinema/tests/test_movie_session_api.py | 27 ++---- cinema/tests/test_order_api.py | 12 +-- cinema/urls.py | 13 ++- cinema/views.py | 30 +++--- cinema_service/settings.py | 19 ++-- cinema_service/urls.py | 3 +- 14 files changed, 139 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index fd1f3e48..ea472c41 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ venv/ .pytest_cache/ **__pycache__/ *.pyc -app/db.sqlite3 \ No newline at end of file +app/db.sqlite3 +.sqlite +.db3 \ 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..35b43c35 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -1,19 +1,6 @@ -from django.core.exceptions import ValidationError from django.db import models from django.conf import settings - - -class CinemaHall(models.Model): - name = models.CharField(max_length=255) - rows = models.IntegerField() - seats_in_row = models.IntegerField() - - @property - def capacity(self) -> int: - return self.rows * self.seats_in_row - - def __str__(self): - return self.name +from django.core.exceptions import ValidationError class Genre(models.Model): @@ -27,13 +14,26 @@ class Actor(models.Model): first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) - def __str__(self): - return self.first_name + " " + self.last_name - @property def full_name(self): return f"{self.first_name} {self.last_name}" + def __str__(self): + return self.full_name + + +class CinemaHall(models.Model): + name = models.CharField(max_length=255) + rows = models.IntegerField() + seats_in_row = models.IntegerField() + + @property + def capacity(self): + return self.rows * self.seats_in_row + + def __str__(self): + return self.name + class Movie(models.Model): title = models.CharField(max_length=255) @@ -42,9 +42,6 @@ class Movie(models.Model): genres = models.ManyToManyField(Genre) actors = models.ManyToManyField(Actor) - class Meta: - ordering = ["title"] - def __str__(self): return self.title @@ -54,70 +51,47 @@ class MovieSession(models.Model): movie = models.ForeignKey(Movie, on_delete=models.CASCADE) cinema_hall = models.ForeignKey(CinemaHall, on_delete=models.CASCADE) - class Meta: - ordering = ["-show_time"] - def __str__(self): - return self.movie.title + " " + str(self.show_time) + return f"{self.movie.title} - {self.show_time}" 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) - - class Meta: - ordering = ["-created_at"] + return f"Order {self.id} - {self.created_at}" 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() + class Meta: + unique_together = ("movie_session", "row", "seat") + 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"), - ]: - count_attrs = getattr( - self.movie_session.cinema_hall, cinema_hall_attr_name + if not (1 <= self.row <= self.movie_session.cinema_hall.rows): + raise ValidationError( + f"Row number must be between 1 and " + f"{self.movie_session.cinema_hall.rows}" ) - if not (1 <= ticket_attr_value <= count_attrs): - raise ValidationError( - { - ticket_attr_name: f"{ticket_attr_name} " - f"number must be in available range: " - f"(1, {cinema_hall_attr_name}): " - f"(1, {count_attrs})" - } - ) - - def save( - self, - force_insert=False, - force_update=False, - using=None, - update_fields=None, - ): + if not (1 <= self.seat <= self.movie_session.cinema_hall.seats_in_row): + raise ValidationError( + f"Seat number must be between 1 and " + f"{self.movie_session.cinema_hall.seats_in_row}" + ) + + def save(self, *args, **kwargs): self.full_clean() - super(Ticket, self).save( - force_insert, force_update, using, update_fields - ) + super().save(*args, **kwargs) def __str__(self): - return ( - f"{str(self.movie_session)} (row: {self.row}, seat: {self.seat})" - ) - - class Meta: - unique_together = ("movie_session", "row", "seat") + return (f"Ticket for {self.movie_session} - " + f"Row {self.row}, Seat {self.seat}") diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..d1d2c994 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,12 @@ 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): @@ -10,6 +16,8 @@ class Meta: class ActorSerializer(serializers.ModelSerializer): + full_name = serializers.CharField(source="full_name", read_only=True) + class Meta: model = Actor fields = ("id", "first_name", "last_name", "full_name") @@ -27,38 +35,26 @@ class Meta: fields = ("id", "title", "description", "duration", "genres", "actors") -class MovieListSerializer(MovieSerializer): - genres = serializers.SlugRelatedField( - many=True, read_only=True, slug_field="name" - ) +class MovieListSerializer(serializers.ModelSerializer): + genres = serializers.SlugRelatedField(many=True, read_only=True, + slug_field="name") actors = serializers.SlugRelatedField( many=True, read_only=True, slug_field="full_name" ) - -class MovieDetailSerializer(MovieSerializer): - genres = GenreSerializer(many=True, read_only=True) - actors = ActorSerializer(many=True, read_only=True) - class Meta: model = Movie fields = ("id", "title", "description", "duration", "genres", "actors") -class MovieSessionSerializer(serializers.ModelSerializer): - class Meta: - model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") - - -class MovieSessionListSerializer(MovieSessionSerializer): +class MovieSessionListSerializer(serializers.ModelSerializer): movie_title = serializers.CharField(source="movie.title", read_only=True) - cinema_hall_name = serializers.CharField( - source="cinema_hall.name", 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.SerializerMethodField() class Meta: model = MovieSession @@ -68,13 +64,30 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available", ) + def get_tickets_available(self, obj): + return obj.cinema_hall.capacity - obj.tickets.count() -class MovieSessionDetailSerializer(MovieSessionSerializer): - movie = MovieListSerializer(many=False, read_only=True) - cinema_hall = CinemaHallSerializer(many=False, read_only=True) +class TicketSerializer(serializers.ModelSerializer): class Meta: - model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + +class OrderSerializer(serializers.ModelSerializer): + tickets = TicketSerializer(many=True) + + class Meta: + model = Order + fields = ("id", "tickets", "created_at") + + def create(self, validated_data): + tickets_data = validated_data.pop("tickets") + user = self.context["request"].user + order = Order.objects.create(user=user) + 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..40ebbdad 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -1,21 +1,24 @@ from django.urls import path, include -from rest_framework import routers - +from rest_framework.routers import DefaultRouter from cinema.views import ( GenreViewSet, ActorViewSet, CinemaHallViewSet, MovieViewSet, MovieSessionViewSet, + OrderViewSet, ) -router = routers.DefaultRouter() +router = 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) +router.register("orders", OrderViewSet, basename="order") -urlpatterns = [path("", include(router.urls))] -app_name = "cinema" +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..ed8117ed 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,17 +1,14 @@ from rest_framework import viewsets - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession - +from rest_framework.permissions import IsAuthenticated +from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order from cinema.serializers import ( GenreSerializer, ActorSerializer, CinemaHallSerializer, + MovieListSerializer, MovieSerializer, - MovieSessionSerializer, MovieSessionListSerializer, - MovieDetailSerializer, - MovieSessionDetailSerializer, - MovieListSerializer, + OrderSerializer, ) @@ -32,27 +29,22 @@ class CinemaHallViewSet(viewsets.ModelViewSet): class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() - serializer_class = MovieSerializer def get_serializer_class(self): if self.action == "list": return MovieListSerializer - - if self.action == "retrieve": - return MovieDetailSerializer - return MovieSerializer 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 +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + serializer_class = OrderSerializer + permission_classes = [IsAuthenticated] - return MovieSessionSerializer + def get_queryset(self): + return Order.objects.filter(user=self.request.user) diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..99014c63 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 = ( +SECRET_KEY = \ "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" -) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -44,6 +43,7 @@ "django.contrib.staticfiles", "rest_framework", "debug_toolbar", + "django_filters", "cinema", "user", ] @@ -101,20 +101,27 @@ }, { "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", + "MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", + "CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", + "NumericPasswordValidator", }, ] AUTH_USER_MODEL = "user.User" +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": + "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 2, +} + + # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ @@ -124,7 +131,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) diff --git a/cinema_service/urls.py b/cinema_service/urls.py index bf903c00..fda5a995 100644 --- a/cinema_service/urls.py +++ b/cinema_service/urls.py @@ -3,6 +3,5 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/cinema/", include("cinema.urls", namespace="cinema")), - path("__debug__/", include("debug_toolbar.urls")), + path("api/cinema/", include("cinema.urls")), ] From 27ba226f44e4b85e8c6b3956a8b973ae52a3ca15 Mon Sep 17 00:00:00 2001 From: Yulia Pylypets Date: Fri, 20 Dec 2024 14:58:13 +0200 Subject: [PATCH 2/2] Fixed --- cinema/models.py | 106 +++++++++++++++---------- cinema/serializers.py | 89 +++++++++++++++------ cinema/tests/test_actor_api.py | 13 ++- cinema/tests/test_cinema_hall_api.py | 14 +++- cinema/tests/test_genre_api.py | 2 +- cinema/tests/test_movie_api.py | 22 +++-- cinema/tests/test_movie_session_api.py | 22 +++-- cinema/tests/test_order_api.py | 14 +++- cinema/urls.py | 11 ++- cinema/views.py | 95 ++++++++++++++++++++-- cinema_service/settings.py | 12 +-- cinema_service/urls.py | 3 +- 12 files changed, 287 insertions(+), 116 deletions(-) diff --git a/cinema/models.py b/cinema/models.py index 35b43c35..1a18b7ff 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -1,6 +1,19 @@ +from django.core.exceptions import ValidationError from django.db import models from django.conf import settings -from django.core.exceptions import ValidationError + + +class CinemaHall(models.Model): + name = models.CharField(max_length=255) + rows = models.IntegerField() + seats_in_row = models.IntegerField() + + @property + def capacity(self) -> int: + return self.rows * self.seats_in_row + + def __str__(self): + return self.name class Genre(models.Model): @@ -14,25 +27,12 @@ class Actor(models.Model): first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) - @property - def full_name(self): - return f"{self.first_name} {self.last_name}" - def __str__(self): - return self.full_name - - -class CinemaHall(models.Model): - name = models.CharField(max_length=255) - rows = models.IntegerField() - seats_in_row = models.IntegerField() + return self.first_name + " " + self.last_name @property - def capacity(self): - return self.rows * self.seats_in_row - - def __str__(self): - return self.name + def full_name(self): + return f"{self.first_name} {self.last_name}" class Movie(models.Model): @@ -42,6 +42,9 @@ class Movie(models.Model): genres = models.ManyToManyField(Genre) actors = models.ManyToManyField(Actor) + class Meta: + ordering = ["title"] + def __str__(self): return self.title @@ -51,47 +54,70 @@ class MovieSession(models.Model): movie = models.ForeignKey(Movie, on_delete=models.CASCADE) cinema_hall = models.ForeignKey(CinemaHall, on_delete=models.CASCADE) + class Meta: + ordering = ["-show_time"] + def __str__(self): - return f"{self.movie.title} - {self.show_time}" + return self.movie.title + " " + str(self.show_time) 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 f"Order {self.id} - {self.created_at}" + return str(self.created_at) + + class Meta: + ordering = ["-created_at"] 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() - class Meta: - unique_together = ("movie_session", "row", "seat") - def clean(self): - if not (1 <= self.row <= self.movie_session.cinema_hall.rows): - raise ValidationError( - f"Row number must be between 1 and " - f"{self.movie_session.cinema_hall.rows}" + 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 ) - if not (1 <= self.seat <= self.movie_session.cinema_hall.seats_in_row): - raise ValidationError( - f"Seat number must be between 1 and " - f"{self.movie_session.cinema_hall.seats_in_row}" - ) - - def save(self, *args, **kwargs): + if not (1 <= ticket_attr_value <= count_attrs): + raise ValidationError( + { + ticket_attr_name: ( + f"{ticket_attr_name} " + f"number must be in available range: " + f"(1, {cinema_hall_attr_name}): (1, {count_attrs})" + ) + } + ) + + def save( + self, + force_insert=False, + force_update=False, + using=None, + update_fields=None, + ): self.full_clean() - super().save(*args, **kwargs) + super(Ticket, self).save( + force_insert, force_update, using, update_fields + ) def __str__(self): - return (f"Ticket for {self.movie_session} - " - f"Row {self.row}, Seat {self.seat}") + return (f"{str(self.movie_session)} " + f"(row: {self.row}, seat: {self.seat})") + + class Meta: + unique_together = ("movie_session", "row", "seat") diff --git a/cinema/serializers.py b/cinema/serializers.py index d1d2c994..3160e2a8 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,12 +1,14 @@ from rest_framework import serializers -from cinema.models import (Genre, - Actor, - CinemaHall, - Movie, - MovieSession, - Ticket, - Order - ) + +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket, +) class GenreSerializer(serializers.ModelSerializer): @@ -16,8 +18,6 @@ class Meta: class ActorSerializer(serializers.ModelSerializer): - full_name = serializers.CharField(source="full_name", read_only=True) - class Meta: model = Actor fields = ("id", "first_name", "last_name", "full_name") @@ -35,26 +35,39 @@ class Meta: fields = ("id", "title", "description", "duration", "genres", "actors") -class MovieListSerializer(serializers.ModelSerializer): - genres = serializers.SlugRelatedField(many=True, read_only=True, - slug_field="name") +class MovieListSerializer(MovieSerializer): + genres = serializers.SlugRelatedField( + many=True, read_only=True, slug_field="name" + ) actors = serializers.SlugRelatedField( many=True, read_only=True, slug_field="full_name" ) + +class MovieDetailSerializer(MovieSerializer): + genres = GenreSerializer(many=True, read_only=True) + actors = ActorSerializer(many=True, read_only=True) + class Meta: model = Movie fields = ("id", "title", "description", "duration", "genres", "actors") -class MovieSessionListSerializer(serializers.ModelSerializer): +class MovieSessionSerializer(serializers.ModelSerializer): + class Meta: + model = MovieSession + fields = ("id", "show_time", "movie", "cinema_hall") + + +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) + 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.SerializerMethodField() + tickets_available = serializers.IntegerField(read_only=True) class Meta: model = MovieSession @@ -67,27 +80,55 @@ class Meta: "tickets_available", ) - def get_tickets_available(self, obj): - return obj.cinema_hall.capacity - obj.tickets.count() + +class TicketSeatRowSerializer(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 = TicketSeatRowSerializer( + many=True, + read_only=True, + source="tickets", + ) + + class Meta: + model = MovieSession + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") class TicketSerializer(serializers.ModelSerializer): + movie_session = MovieSessionListSerializer(many=False, read_only=True) + class Meta: model = Ticket - fields = ("id", "row", "seat", "movie_session") + fields = ("id", "movie_session", "row", "seat") + + def validate(self, data): + data = super(TicketSerializer).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) + tickets = TicketSerializer(many=True, read_only=True, allow_null=False) class Meta: model = Order - fields = ("id", "tickets", "created_at") + fields = ("id", "created_at", "tickets") def create(self, validated_data): tickets_data = validated_data.pop("tickets") - user = self.context["request"].user - order = Order.objects.create(user=user) + 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 097e42ca..5eeb3537 100644 --- a/cinema/tests/test_actor_api.py +++ b/cinema/tests/test_actor_api.py @@ -1,5 +1,4 @@ from django.test import TestCase - from rest_framework import status from rest_framework.test import APIClient @@ -16,7 +15,9 @@ 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( @@ -57,15 +58,11 @@ def test_put_actor(self): ) def test_delete_actor(self): - response = self.client.delete( - "/api/cinema/actors/1/", - ) + response = self.client.delete("/api/cinema/actors/1/") db_actors_id_1 = Actor.objects.filter(id=1) self.assertEqual(db_actors_id_1.count(), 0) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) def test_delete_invalid_actor(self): - response = self.client.delete( - "/api/cinema/actors/1000/", - ) + response = self.client.delete("/api/cinema/actors/1000/") self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cinema/tests/test_cinema_hall_api.py b/cinema/tests/test_cinema_hall_api.py index be74b47a..fe5b62f1 100644 --- a/cinema/tests/test_cinema_hall_api.py +++ b/cinema/tests/test_cinema_hall_api.py @@ -31,7 +31,9 @@ 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, @@ -41,7 +43,9 @@ 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( @@ -68,7 +72,9 @@ 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): @@ -121,4 +127,4 @@ def test_delete_invalid_cinema_hall(self): response = self.client.delete( "/api/cinema/cinema_halls/1000/", ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/cinema/tests/test_genre_api.py b/cinema/tests/test_genre_api.py index a81daaf4..4da47eb1 100644 --- a/cinema/tests/test_genre_api.py +++ b/cinema/tests/test_genre_api.py @@ -59,4 +59,4 @@ def test_delete_invalid_genre(self): response = self.client.delete( "/api/cinema/genres/1000/", ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/cinema/tests/test_movie_api.py b/cinema/tests/test_movie_api.py index c129b279..3b4a2cf5 100644 --- a/cinema/tests/test_movie_api.py +++ b/cinema/tests/test_movie_api.py @@ -15,7 +15,9 @@ 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", @@ -40,15 +42,21 @@ 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) @@ -103,7 +111,9 @@ 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/") @@ -142,4 +152,4 @@ def test_delete_invalid_movie(self): response = self.client.delete( "/api/cinema/movies/1000/", ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) \ No newline at end of file diff --git a/cinema/tests/test_movie_session_api.py b/cinema/tests/test_movie_session_api.py index 7ce021d0..ee599b55 100644 --- a/cinema/tests/test_movie_session_api.py +++ b/cinema/tests/test_movie_session_api.py @@ -46,14 +46,20 @@ 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) @@ -64,7 +70,9 @@ 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) @@ -104,11 +112,13 @@ 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"]) self.assertEqual(response.data["cinema_hall"]["capacity"], 140) self.assertEqual(response.data["cinema_hall"]["rows"], 10) self.assertEqual(response.data["cinema_hall"]["seats_in_row"], 14) - self.assertEqual(response.data["cinema_hall"]["name"], "White") + self.assertEqual(response.data["cinema_hall"]["name"], "White") \ No newline at end of file diff --git a/cinema/tests/test_order_api.py b/cinema/tests/test_order_api.py index b510ef58..88f31950 100644 --- a/cinema/tests/test_order_api.py +++ b/cinema/tests/test_order_api.py @@ -26,7 +26,9 @@ 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", @@ -71,8 +73,12 @@ 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/") @@ -80,4 +86,4 @@ def test_movie_session_list_tickets_available(self): self.assertEqual( response.data[0]["tickets_available"], self.cinema_hall.capacity - 1, - ) + ) \ No newline at end of file diff --git a/cinema/urls.py b/cinema/urls.py index 40ebbdad..ee3dbe5a 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include -from rest_framework.routers import DefaultRouter +from rest_framework import routers + from cinema.views import ( GenreViewSet, ActorViewSet, @@ -7,18 +8,20 @@ MovieViewSet, MovieSessionViewSet, OrderViewSet, + TicketViewSet, ) -router = DefaultRouter() +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) -router.register("orders", OrderViewSet, basename="order") - +router.register("tickets", TicketViewSet) urlpatterns = [ path("", include(router.urls)), ] + +app_name = "cinema" diff --git a/cinema/views.py b/cinema/views.py index ed8117ed..44601097 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,14 +1,29 @@ +from django.db.models import Count, F from rest_framework import viewsets -from rest_framework.permissions import IsAuthenticated -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order +from rest_framework.pagination import PageNumberPagination + +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket, +) + from cinema.serializers import ( GenreSerializer, ActorSerializer, CinemaHallSerializer, - MovieListSerializer, MovieSerializer, + MovieSessionSerializer, MovieSessionListSerializer, + MovieDetailSerializer, + MovieSessionDetailSerializer, + MovieListSerializer, OrderSerializer, + TicketSerializer, ) @@ -29,22 +44,86 @@ class CinemaHallViewSet(viewsets.ModelViewSet): class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() + serializer_class = MovieSerializer def get_serializer_class(self): if self.action == "list": return MovieListSerializer + if self.action == "retrieve": + return MovieDetailSerializer 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") + + 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() - serializer_class = MovieSessionListSerializer + serializer_class = MovieSessionSerializer + + def get_serializer_class(self): + if self.action == "list": + return MovieSessionListSerializer + if self.action == "retrieve": + return MovieSessionDetailSerializer + return MovieSessionSerializer + + def get_queryset(self): + queryset =\ + super().get_queryset().select_related("movie", "cinema_hall") + + if self.action == "list": + queryset = queryset.annotate( + tickets_available=( + F("cinema_hall__rows") * F("cinema_hall__seats_in_row") + - Count("tickets") + ) + ).order_by("id") + + movie_id = self.request.query_params.get("movie") + date = self.request.query_params.get("date") + + if movie_id: + queryset = queryset.filter(movie__id=movie_id) + + if date: + try: + queryset = queryset.filter(show_time__date=date) + except ValueError: + pass + + return queryset.distinct() + + +class TicketViewSet(viewsets.ModelViewSet): + queryset = Ticket.objects.all() + serializer_class = TicketSerializer + + +class OrderPagination(PageNumberPagination): + page_size = 5 class OrderViewSet(viewsets.ModelViewSet): queryset = Order.objects.all() serializer_class = OrderSerializer - permission_classes = [IsAuthenticated] - - def get_queryset(self): - return Order.objects.filter(user=self.request.user) + pagination_class = OrderPagination diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 99014c63..70626827 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -20,7 +20,7 @@ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = \ +SECRET_KEY =\ "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" # SECURITY WARNING: don't run with debug turned on in production! @@ -43,7 +43,6 @@ "django.contrib.staticfiles", "rest_framework", "debug_toolbar", - "django_filters", "cinema", "user", ] @@ -115,13 +114,6 @@ AUTH_USER_MODEL = "user.User" -REST_FRAMEWORK = { - "DEFAULT_PAGINATION_CLASS": - "rest_framework.pagination.PageNumberPagination", - "PAGE_SIZE": 2, -} - - # Internationalization # https://docs.djangoproject.com/en/4.0/topics/i18n/ @@ -131,7 +123,7 @@ USE_I18N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) diff --git a/cinema_service/urls.py b/cinema_service/urls.py index fda5a995..bf903c00 100644 --- a/cinema_service/urls.py +++ b/cinema_service/urls.py @@ -3,5 +3,6 @@ urlpatterns = [ path("admin/", admin.site.urls), - path("api/cinema/", include("cinema.urls")), + path("api/cinema/", include("cinema.urls", namespace="cinema")), + path("__debug__/", include("debug_toolbar.urls")), ]