diff --git a/cinema/filters.py b/cinema/filters.py new file mode 100644 index 00000000..40adbcfd --- /dev/null +++ b/cinema/filters.py @@ -0,0 +1,47 @@ +from django.db.models import QuerySet +from django.http import HttpRequest +from rest_framework import filters +from rest_framework.viewsets import ModelViewSet + +from cinema.models import MovieSession, Movie + + +class FilterMovieSessionByDateAndMovie(filters.BaseFilterBackend): + def filter_queryset( + self, request: HttpRequest, + queryset: QuerySet[MovieSession], + view: ModelViewSet + ) -> QuerySet[MovieSession]: + date = request.query_params.get("date") + movie = request.query_params.get("movie") + + if date: + queryset = queryset.filter(show_time__date=date) + if movie: + queryset = queryset.filter(movie__id=movie) + + return queryset + + +class FilterMovieViewSet(filters.BaseFilterBackend): + def filter_queryset( + self, request: HttpRequest, + queryset: QuerySet[Movie], + view: ModelViewSet + ): + actors = request.query_params.get("actors") + genres = request.query_params.get("genres") + title = request.query_params.get("title") + + if actors: + actor_ids = actors.split(",") + queryset = queryset.filter(actors__id__in=actor_ids) + + if genres: + genre_ids = genres.split(",") + queryset = queryset.filter(genres__id__in=genre_ids) + + if title: + queryset = queryset.filter(title__icontains=title) + + return queryset diff --git a/cinema/migrations/0005_alter_movie_actors_alter_movie_genres_and_more.py b/cinema/migrations/0005_alter_movie_actors_alter_movie_genres_and_more.py new file mode 100644 index 00000000..c6c6a903 --- /dev/null +++ b/cinema/migrations/0005_alter_movie_actors_alter_movie_genres_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1 on 2024-10-03 10:37 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cinema', '0004_alter_genre_name'), + ] + + operations = [ + migrations.AlterField( + model_name='movie', + name='actors', + field=models.ManyToManyField(related_name='movies', to='cinema.actor'), + ), + migrations.AlterField( + model_name='movie', + name='genres', + field=models.ManyToManyField(related_name='movies', to='cinema.genre'), + ), + migrations.AlterField( + model_name='moviesession', + name='cinema_hall', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.cinemahall'), + ), + migrations.AlterField( + model_name='moviesession', + name='movie', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.movie'), + ), + ] diff --git a/cinema/migrations/0006_alter_movie_actors_alter_movie_genres_and_more.py b/cinema/migrations/0006_alter_movie_actors_alter_movie_genres_and_more.py new file mode 100644 index 00000000..81964de0 --- /dev/null +++ b/cinema/migrations/0006_alter_movie_actors_alter_movie_genres_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1 on 2024-10-03 10:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cinema', '0005_alter_movie_actors_alter_movie_genres_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='movie', + name='actors', + field=models.ManyToManyField(to='cinema.actor'), + ), + migrations.AlterField( + model_name='movie', + name='genres', + field=models.ManyToManyField(to='cinema.genre'), + ), + migrations.AlterField( + model_name='moviesession', + name='cinema_hall', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cinema.cinemahall'), + ), + migrations.AlterField( + model_name='moviesession', + name='movie', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cinema.movie'), + ), + ] diff --git a/cinema/models.py b/cinema/models.py index f18f166c..528198a9 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -1,3 +1,5 @@ +from typing import Callable + from django.core.exceptions import ValidationError from django.db import models from django.conf import settings @@ -64,7 +66,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 + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE ) def __str__(self): @@ -76,42 +79,66 @@ class Meta: class Ticket(models.Model): movie_session = models.ForeignKey( - MovieSession, on_delete=models.CASCADE, related_name="tickets" + MovieSession, + on_delete=models.CASCADE, + related_name="tickets" ) order = models.ForeignKey( - Order, on_delete=models.CASCADE, related_name="tickets" + Order, + on_delete=models.CASCADE, + related_name="tickets" ) row = models.IntegerField() seat = models.IntegerField() - def clean(self): + @staticmethod + def ticket_validation( + row: int, + seat: int, + cinema_hall: CinemaHall, + error_class: Callable + ) -> None: for ticket_attr_value, ticket_attr_name, cinema_hall_attr_name in [ - (self.row, "row", "rows"), - (self.seat, "seat", "seats_in_row"), + (row, "row", "rows"), + (seat, "seat", "seats_in_row"), ]: count_attrs = getattr( - self.movie_session.cinema_hall, cinema_hall_attr_name + cinema_hall, cinema_hall_attr_name ) if not (1 <= ticket_attr_value <= count_attrs): - raise ValidationError( + error_message = ( + f"{ticket_attr_name} " + f"number must be in available range: " + f"(1, {cinema_hall_attr_name}): " + f"(1, {count_attrs})" + ) + raise error_class( { - 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: error_message } ) + def clean(self): + self.ticket_validation( + self.row, + self.seat, + self.movie_session.cinema_hall, + 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( - force_insert, force_update, using, update_fields + force_insert, + force_update, + using, + update_fields ) def __str__(self): diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..b0587cb0 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, + Order, + Ticket +) class GenreSerializer(serializers.ModelSerializer): @@ -29,10 +38,14 @@ class Meta: class MovieListSerializer(MovieSerializer): genres = serializers.SlugRelatedField( - many=True, read_only=True, slug_field="name" + many=True, + read_only=True, + slug_field="name" ) actors = serializers.SlugRelatedField( - many=True, read_only=True, slug_field="full_name" + many=True, + read_only=True, + slug_field="full_name" ) @@ -54,11 +67,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 + source="cinema_hall.name", + read_only=True ) cinema_hall_capacity = serializers.IntegerField( - source="cinema_hall.capacity", read_only=True + source="cinema_hall.capacity", + read_only=True ) + tickets_available = serializers.IntegerField(read_only=True) class Meta: model = MovieSession @@ -68,13 +84,80 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available" ) +class TicketTakenPlacesSerializer(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 = TicketTakenPlacesSerializer( + 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): + movie_session = MovieSessionListSerializer(read_only=True, many=False) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + +class TicketPerformSerializer(serializers.ModelSerializer): + movie_session = serializers.PrimaryKeyRelatedField( + queryset=MovieSession.objects.all() + ) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + +class OrderSerializer(serializers.ModelSerializer): + tickets = TicketPerformSerializer( + many=True, + read_only=False, + allow_empty=False + ) + + class Meta: + model = Order + fields = ("id", "tickets", "created_at") + + def create(self, validated_data): + tickets_data = validated_data.pop("tickets") + + with transaction.atomic(): + order = Order.objects.create(**validated_data) + for ticket_data in tickets_data: + Ticket.objects.create(order=order, **ticket_data) + + return order + + def validate(self, attrs): + data = super().validate(attrs) + Ticket.ticket_validation( + row=attrs["row"], + seat=attrs["seat"], + cinema_hall=attrs["movie_session"].cinema_hall, + error_class=serializers.ValidationError + ) + + return data + + +class OrderListSerializer(OrderSerializer): + tickets = TicketSerializer(many=True, read_only=True) 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..520fb992 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,7 +1,16 @@ +from django.db.models import F, Count from rest_framework import viewsets - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession - +from rest_framework.pagination import PageNumberPagination + +from cinema.filters import FilterMovieSessionByDateAndMovie, FilterMovieViewSet +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order +) from cinema.serializers import ( GenreSerializer, ActorSerializer, @@ -12,6 +21,8 @@ MovieDetailSerializer, MovieSessionDetailSerializer, MovieListSerializer, + OrderSerializer, + OrderListSerializer ) @@ -33,6 +44,7 @@ class CinemaHallViewSet(viewsets.ModelViewSet): class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer + filter_backends = [FilterMovieViewSet] def get_serializer_class(self): if self.action == "list": @@ -43,10 +55,14 @@ def get_serializer_class(self): return MovieSerializer + def get_queryset(self): + return self.queryset.prefetch_related("genres", "actors") + class MovieSessionViewSet(viewsets.ModelViewSet): queryset = MovieSession.objects.all() serializer_class = MovieSessionSerializer + filter_backends = [FilterMovieSessionByDateAndMovie] def get_serializer_class(self): if self.action == "list": @@ -56,3 +72,55 @@ def get_serializer_class(self): return MovieSessionDetailSerializer return MovieSessionSerializer + + def get_queryset(self): + queryset = self.queryset + + if self.action == "list": + queryset = ( + queryset + .select_related("cinema_hall", "movie") + .prefetch_related("tickets") + .annotate( + tickets_available=(F("cinema_hall__rows") * F( + "cinema_hall__seats_in_row")) - Count("tickets") + ).order_by("id") + ) + + return queryset + + +class OrderResultsSetPagination(PageNumberPagination): + page_size = 5 + page_size_query_param = "page_size" + max_page_size = 1 + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + serializer_class = OrderSerializer + pagination_class = OrderResultsSetPagination + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + def get_queryset(self): + query_set = self.queryset.select_related( + "user" + ).prefetch_related( + "tickets__movie_session__cinema_hall", + "tickets__movie_session__movie" + ) + + if self.action == "list": + query_set = query_set.filter(user=self.request.user) + + return query_set + + def get_serializer_class(self): + serializer = self.serializer_class + + if self.action == "list": + serializer = OrderListSerializer + + return serializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..191abf08 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -43,6 +43,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "django_filters", "debug_toolbar", "cinema", "user", @@ -124,7 +125,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) diff --git a/requirements.txt b/requirements.txt index 56e13554..3ec23e7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ flake8-quotes==3.3.1 flake8-variables-names==0.0.5 pep8-naming==0.13.2 django-debug-toolbar==3.2.4 -djangorestframework==3.13.1 \ No newline at end of file +djangorestframework==3.13.1 +django-filter==22.1 \ No newline at end of file