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 00000000..ca850d22 Binary files /dev/null and b/db.sqlite3 differ