diff --git a/cinema/pagination.py b/cinema/pagination.py new file mode 100644 index 00000000..1999e5d7 --- /dev/null +++ b/cinema/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class OrdersPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = "page_size" + max_page_size = 100 diff --git a/cinema/serializers.py b/cinema/serializers.py index a1a4d7d4..a24e25a2 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,6 +1,16 @@ +from django.db import transaction from rest_framework import serializers +from rest_framework.relations import PrimaryKeyRelatedField -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order, + Ticket, +) class GenreSerializer(serializers.ModelSerializer): @@ -59,6 +69,7 @@ class MovieSessionListSerializer(MovieSessionSerializer): cinema_hall_capacity = serializers.IntegerField( source="cinema_hall.capacity", read_only=True ) + tickets_available = serializers.SerializerMethodField() class Meta: model = MovieSession @@ -68,13 +79,73 @@ class Meta: "movie_title", "cinema_hall_name", "cinema_hall_capacity", + "tickets_available" ) + def get_tickets_available(self, obj): + tickets_sold = obj.tickets.count() + return obj.cinema_hall.capacity - tickets_sold + + +class TicketRowSeatSerializer(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 = TicketRowSeatSerializer( + 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 = PrimaryKeyRelatedField( + queryset=MovieSession.objects.all() + ) + + class Meta: + model = Ticket + fields = ("id", "row", "seat", "movie_session") + + def validate(self, attrs): + movie_session = attrs["movie_session"] + seats_in_row = movie_session.cinema_hall.seats_in_row + if not (1 <= attrs["seat"] <= seats_in_row): + raise serializers.ValidationError( + { + "seat": f"seats must be between 1 and {seats_in_row}" + } + ) + + +class TicketDetailSerializer(TicketSerializer): + movie_session = MovieSessionListSerializer(many=False, read_only=True) + + +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 + + +class OrderDetailSerializer(OrderSerializer): + tickets = TicketDetailSerializer(many=True, read_only=True) diff --git a/cinema/urls.py b/cinema/urls.py index e3586f00..8de69236 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -7,15 +7,19 @@ CinemaHallViewSet, MovieViewSet, MovieSessionViewSet, + OrderViewSet, + TicketViewSet, ) +app_name = "cinema" + 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, basename="orders") +router.register("tickets", TicketViewSet) urlpatterns = [path("", include(router.urls))] - -app_name = "cinema" diff --git a/cinema/views.py b/cinema/views.py index c4ff85e9..4e81792d 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,17 +1,30 @@ +from datetime import datetime +from rest_framework.exceptions import ValidationError from rest_framework import viewsets - -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + Order, + Ticket, + MovieSession, +) +from cinema.pagination import OrdersPagination from cinema.serializers import ( GenreSerializer, ActorSerializer, CinemaHallSerializer, MovieSerializer, - MovieSessionSerializer, - MovieSessionListSerializer, MovieDetailSerializer, - MovieSessionDetailSerializer, MovieListSerializer, + OrderSerializer, + TicketSerializer, + OrderDetailSerializer, + MovieSessionListSerializer, + MovieSessionDetailSerializer, + MovieSessionSerializer, ) @@ -43,16 +56,85 @@ def get_serializer_class(self): return MovieSerializer + def get_queryset(self): + actors = self.request.query_params.get("actors") + genres = self.request.query_params.get("genres") + title = self.request.query_params.get("title") + + queryset = Movie.objects.all() + + if actors: + actors_ids = [int(str_id) for str_id in actors.split(",")] + queryset = Movie.objects.filter(actors__id__in=actors_ids) + + if genres: + genres_ids = [int(str_id) for str_id in genres.split(",")] + queryset = Movie.objects.filter(genres__id__in=genres_ids) + + if title: + queryset = queryset.filter(title__icontains=title) + + if self.action in ["retrieve", "list"]: + queryset = queryset.prefetch_related("genres", "actors") + + return queryset.distinct() + 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 + elif self.action == "create": + return MovieSessionSerializer + return MovieSessionListSerializer + + def get_queryset(self): + date = self.request.query_params.get("date") + movie = self.request.query_params.get("movie") + queryset = MovieSession.objects.all() + if movie: + try: + movie_ids = [int(str_id) for str_id in movie.split(",")] + queryset = queryset.filter(movie_id__in=movie_ids) + except ValueError: + raise ValidationError( + {"movie": "Invalid movie ID. Must be an integer."} + ) + + if date: + try: + parsed_date = datetime.strptime(date, "%Y-%m-%d").date() + queryset = queryset.filter(show_time__date=parsed_date) + except ValueError: + raise ValidationError( + {"date": "Invalid date format. Use YYYY-MM-DD."} + ) + + if self.action in ["retrieve", "list"]: + queryset = queryset.prefetch_related("movie") + + return queryset.distinct() + + +class OrderViewSet(viewsets.ModelViewSet): + queryset = Order.objects.all() + pagination_class = OrdersPagination + + def get_serializer_class(self): + if self.action in ["create", "update", "partial_update"]: + return OrderSerializer + return OrderDetailSerializer + + def get_queryset(self): + return Order.objects.filter(user=self.request.user) + + def perform_create(self, serializer): + serializer.save(user=self.request.user) + - return MovieSessionSerializer +class TicketViewSet(viewsets.ModelViewSet): + queryset = Ticket.objects.all() + serializer_class = TicketSerializer diff --git a/cinema_service/settings.py b/cinema_service/settings.py index a7d6c992..068db3d6 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -124,7 +124,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 00000000..82297d39 Binary files /dev/null and b/db.sqlite3 differ diff --git a/requirements.txt b/requirements.txt index 56e13554..af5a1d76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,14 @@ -django==4.1 +asgiref==3.8.1 +Django==4.1 +django-debug-toolbar==3.2.4 +djangorestframework==3.13.1 flake8==5.0.4 flake8-quotes==3.3.1 flake8-variables-names==0.0.5 +mccabe==0.7.0 pep8-naming==0.13.2 -django-debug-toolbar==3.2.4 -djangorestframework==3.13.1 \ No newline at end of file +pycodestyle==2.9.1 +pyflakes==2.5.0 +pytz==2024.2 +sqlparse==0.5.3 +tzdata==2024.2