From f91b4d9406419fb87d1f3d194780146bda500ec9 Mon Sep 17 00:00:00 2001 From: zakotii Date: Wed, 8 Jan 2025 13:10:17 +0200 Subject: [PATCH] Solution --- cinema/filters.py | 44 +++++++ cinema/migrations/0005_alter_actor_options.py | 17 +++ cinema/migrations/0006_alter_movie_actors.py | 18 +++ cinema/models.py | 5 +- cinema/serializers.py | 118 ++++++++++++++---- cinema/urls.py | 5 + cinema/views.py | 32 +++-- cinema_service/settings.py | 12 ++ db.sqlite3 | Bin 0 -> 225280 bytes 9 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 cinema/filters.py create mode 100644 cinema/migrations/0005_alter_actor_options.py create mode 100644 cinema/migrations/0006_alter_movie_actors.py create mode 100644 db.sqlite3 diff --git a/cinema/filters.py b/cinema/filters.py new file mode 100644 index 00000000..a0234499 --- /dev/null +++ b/cinema/filters.py @@ -0,0 +1,44 @@ +from django.db.models import Q +from django.db.models import OuterRef, Subquery +from django_filters import rest_framework as filters +from cinema.models import MovieSession, Movie, Actor + + +class MovieSessionFilter(filters.FilterSet): + date = filters.DateFilter(field_name="show_time", lookup_expr="date") + movie = filters.NumberFilter(field_name="movie__id") + + class Meta: + model = MovieSession + fields = ["date", "movie"] + + +class MovieFilter(filters.FilterSet): + genres = filters.CharFilter(method="filter_by_genre_ids") + title = filters.CharFilter(field_name="title", lookup_expr="icontains") + actors = filters.CharFilter(method="filter_by_actor_ids") + + class Meta: + model = Movie + fields = ["genres", "title", "actors"] + + def filter_by_genre_ids(self, queryset, name, value): + genre_ids = [ + int(id.strip()) + for id in value.split(",") + if id.strip().isdigit() + ] + return queryset.filter(genres__id__in=genre_ids) + + def filter_by_actor_ids(self, queryset, name, value): + try: + # Преобразуем значения в список ID + actor_ids = [ + int(id.strip()) + for id in value.split(",") + if id.strip().isdigit() + ] + return queryset.filter(actors__id__in=actor_ids) + except ValueError: + # Если передан некорректный ID + return queryset.none() diff --git a/cinema/migrations/0005_alter_actor_options.py b/cinema/migrations/0005_alter_actor_options.py new file mode 100644 index 00000000..d07b94d5 --- /dev/null +++ b/cinema/migrations/0005_alter_actor_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.13 on 2025-01-07 08:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("cinema", "0004_alter_genre_name"), + ] + + operations = [ + migrations.AlterModelOptions( + name="actor", + options={"ordering": ["id"]}, + ), + ] diff --git a/cinema/migrations/0006_alter_movie_actors.py b/cinema/migrations/0006_alter_movie_actors.py new file mode 100644 index 00000000..1472d1bc --- /dev/null +++ b/cinema/migrations/0006_alter_movie_actors.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-01-07 13:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("cinema", "0005_alter_actor_options"), + ] + + operations = [ + migrations.AlterField( + model_name="movie", + name="actors", + field=models.ManyToManyField(related_name="movies", to="cinema.actor"), + ), + ] diff --git a/cinema/models.py b/cinema/models.py index f18f166c..262ea123 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -27,6 +27,9 @@ class Actor(models.Model): first_name = models.CharField(max_length=255) last_name = models.CharField(max_length=255) + class Meta: + ordering = ["id"] + def __str__(self): return self.first_name + " " + self.last_name @@ -40,7 +43,7 @@ class Movie(models.Model): description = models.TextField() duration = models.IntegerField() genres = models.ManyToManyField(Genre) - actors = models.ManyToManyField(Actor) + actors = models.ManyToManyField(Actor, related_name="movies") class Meta: ordering = ["title"] diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..3aeeb810 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,74 @@ 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 + +class MovieSessionListSerializer(serializers.ModelSerializer): + tickets_available = serializers.SerializerMethodField() + + def get_tickets_available(self, obj): + total_tickets = obj.cinema_hall.capacity + sold_tickets = obj.tickets.count() + return total_tickets - sold_tickets + + 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 + ) + + class Meta: + model = MovieSession + fields = ( + "id", + "show_time", + "movie_title", + "cinema_hall_name", + "cinema_hall_capacity", + "tickets_available", + ) + + +class TicketSerializer(serializers.ModelSerializer): + movie_session = MovieSessionListSerializer(read_only=True) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + def validate(self, data): + movie_session = data["movie_session"] + row = data["row"] + seat = data["seat"] + if Ticket.objects.filter( + movie_session=movie_session, row=row, seat=seat + ).exists(): + raise serializers.ValidationError("Это место уже занято.") + if not (1 <= row <= movie_session.cinema_hall.rows): + raise serializers.ValidationError("Недопустимый номер ряда.") + if not (1 <= seat <= movie_session.cinema_hалл.seats_in_row): + raise serializers.ValidationError("Недопустимый номер места.") + return data + + +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") + order = Order.objects.create(user=self.context["request"].user) + for ticket_data in tickets_data: + Ticket.objects.create(order=order, **ticket_data) + return order class GenreSerializer(serializers.ModelSerializer): @@ -10,10 +78,15 @@ class Meta: class ActorSerializer(serializers.ModelSerializer): + full_name = serializers.SerializerMethodField() + class Meta: model = Actor fields = ("id", "first_name", "last_name", "full_name") + def get_full_name(self, obj): + return f"{obj.first_name} {obj.last_name}" + class CinemaHallSerializer(serializers.ModelSerializer): class Meta: @@ -22,10 +95,26 @@ class Meta: class MovieSerializer(serializers.ModelSerializer): + genres = serializers.PrimaryKeyRelatedField( + many=True, queryset=Genre.objects.all() + ) + actors = serializers.PrimaryKeyRelatedField( + many=True, queryset=Actor.objects.all() + ) + class Meta: model = Movie fields = ("id", "title", "description", "duration", "genres", "actors") + def get_genres(self, obj): + return [genre.name for genre in obj.genres.all()] + + def get_actors(self, obj): + return [ + f"{actor.first_name} {actor.last_name}" + for actor in obj.actors.all() + ] + class MovieListSerializer(MovieSerializer): genres = serializers.SlugRelatedField( @@ -51,30 +140,15 @@ class Meta: 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_capacity = serializers.IntegerField( - source="cinema_hall.capacity", read_only=True - ) - - class Meta: - model = MovieSession - fields = ( - "id", - "show_time", - "movie_title", - "cinema_hall_name", - "cinema_hall_capacity", - ) - - class MovieSessionDetailSerializer(MovieSessionSerializer): movie = MovieListSerializer(many=False, read_only=True) cinema_hall = CinemaHallSerializer(many=False, read_only=True) + taken_places = serializers.SerializerMethodField() class Meta: model = MovieSession - fields = ("id", "show_time", "movie", "cinema_hall") + fields = ("id", "show_time", "movie", "cinema_hall", "taken_places") + + def get_taken_places(self, obj): + tickets = obj.tickets.all() + return [{"row": ticket.row, "seat": ticket.seat} for ticket in tickets] diff --git a/cinema/urls.py b/cinema/urls.py index e3586f00..72104d81 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -9,12 +9,17 @@ MovieSessionViewSet, ) +from cinema.views import OrderViewSet + + 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) + urlpatterns = [path("", include(router.urls))] diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..96ab2dbd 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,7 +1,6 @@ -from rest_framework import viewsets - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession - +from rest_framework import viewsets, permissions, filters +from django_filters.rest_framework import DjangoFilterBackend +from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order from cinema.serializers import ( GenreSerializer, ActorSerializer, @@ -12,7 +11,23 @@ MovieDetailSerializer, MovieSessionDetailSerializer, MovieListSerializer, + OrderSerializer, ) +from cinema.filters import MovieFilter, MovieSessionFilter +from rest_framework.response import Response +from rest_framework.pagination import PageNumberPagination + +from rest_framework.viewsets import ModelViewSet + + +class OrderPagination(PageNumberPagination): + page_size = 10 + + +class OrderViewSet(ModelViewSet): + queryset = Order.objects.prefetch_related("tickets").all() + serializer_class = OrderSerializer + pagination_class = OrderPagination class GenreViewSet(viewsets.ModelViewSet): @@ -33,26 +48,27 @@ class CinemaHallViewSet(viewsets.ModelViewSet): class MovieViewSet(viewsets.ModelViewSet): queryset = Movie.objects.all() serializer_class = MovieSerializer + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_class = MovieFilter + search_fields = ["title"] 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 + filter_backends = [DjangoFilterBackend] + filterset_class = MovieSessionFilter def get_serializer_class(self): if self.action == "list": return MovieSessionListSerializer - if self.action == "retrieve": return MovieSessionDetailSerializer - return MovieSessionSerializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..1f940231 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", @@ -136,3 +137,14 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +REST_FRAMEWORK = { + "DEFAULT_FILTER_BACKENDS": ( + "django_filters.rest_framework.DjangoFilterBackend", + ), +} + +REST_FRAMEWORK["DEFAULT_FILTER_BACKENDS"] = [ + "django_filters.rest_framework.DjangoFilterBackend" +] diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..ca850d2270773c5b5d00a5b8a3d26c1046f64c1c GIT binary patch literal 225280 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