Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Develop #744

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
6 changes: 3 additions & 3 deletions cinema/migrations/0003_movie_duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value is set to 123, but preserve_default is set to False. This means that the default value will not be preserved for existing rows in the database. If you intend to apply this default value to existing records, consider setting preserve_default to True or handling existing records separately.

preserve_default=False,
),
Expand Down
6 changes: 3 additions & 3 deletions cinema/migrations/0004_alter_genre_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the name field to unique=True means that all existing and future entries must have unique values for this field. Ensure that there are no duplicate name values in the existing database records, as this will cause the migration to fail.

),
]
54 changes: 36 additions & 18 deletions cinema/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.conf import settings
from django.utils.timezone import make_aware, is_naive


class CinemaHall(models.Model):
Expand Down Expand Up @@ -60,11 +61,17 @@ class Meta:
def __str__(self):
return self.movie.title + " " + str(self.show_time)

def save(self, *args, **kwargs):
if is_naive(self.show_time):
self.show_time = make_aware(self.show_time)
super().save(*args, **kwargs)


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):
Expand All @@ -76,10 +83,14 @@ 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()
Expand All @@ -90,34 +101,41 @@ def clean(self):
(self.seat, "seat", "seats_in_row"),
]:
count_attrs = getattr(
self.movie_session.cinema_hall, cinema_hall_attr_name
self.movie_session.cinema_hall,
cinema_hall_attr_name,
)
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})"
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,
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
(
super(Ticket, self)
.save(
force_insert,
force_update,
using,
update_fields
)
)

def __str__(self):
return (
f"{str(self.movie_session)} (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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unique_together constraint ensures that each combination of movie_session, row, and seat is unique. This is important for preventing duplicate tickets for the same seat in a session. Ensure that this constraint is properly enforced in your database schema.

146 changes: 141 additions & 5 deletions cinema/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from django.db import transaction
from rest_framework import serializers
from rest_framework.exceptions import ValidationError

from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession
from cinema.models import (
Genre, Actor, CinemaHall,
Movie, MovieSession, Order, Ticket
)


class GenreSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -29,7 +34,9 @@ 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"
Expand All @@ -51,14 +58,16 @@ class Meta:
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
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
Expand All @@ -68,13 +77,140 @@ class Meta:
"movie_title",
"cinema_hall_name",
"cinema_hall_capacity",
"tickets_available",
)

def get_tickets_available(self, obj):
total_capacity = obj.cinema_hall.capacity
tickets_sold = Ticket.objects.filter(movie_session=obj).count()
return total_capacity - tickets_sold


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):
"""
Retrieve all taken places (row and seat) for the movie session.
"""
tickets = Ticket.objects.filter(movie_session=obj)
return [{"row": ticket.row, "seat": ticket.seat} for ticket in tickets]


class TicketSerializer(serializers.ModelSerializer):
movie_session = serializers.PrimaryKeyRelatedField(
queryset=MovieSession.objects.all()
)

class Meta:
model = Ticket
fields = (
"id",
"row",
"seat",
"movie_session",
)

def validate(self, data):
"""
Validate ticket creation.
"""
movie_session = data.get("movie_session")
row = data.get("row")
seat = data.get("seat")

# Ensure row and seat are within the cinema hall's capacity
cinema_hall = movie_session.cinema_hall
if row < 1 or row > cinema_hall.rows:
raise ValidationError(
{
"row": (
f"Row {row} is out of range (1 to {cinema_hall.rows})."
)
}
)
if seat < 1 or seat > cinema_hall.seats_in_row:
raise ValidationError(
{
"seat": (
f"Seat {seat} is out of range (1 to "
f"{cinema_hall.seats_in_row})."
)
}
)

# Ensure row and seat are not already taken for this movie session
if Ticket.objects.filter(
movie_session=movie_session, row=row, seat=seat
).exists():
raise ValidationError(
{
"seat": (
f"Row {row}, Seat {seat} is already taken "
f"for this session."
)
}
)

return data


class TicketMovieListSerializer(TicketSerializer):
movie_session = MovieSessionListSerializer(read_only=True)

class Meta:
model = Ticket
fields = (
"id",
"row",
"seat",
"movie_session",
)


class OrderSerializer(serializers.ModelSerializer):
tickets = TicketSerializer(many=True, allow_empty=False)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The allow_empty=False parameter in the tickets field ensures that an order cannot be created without tickets. This is a good validation step to prevent empty orders.


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

with transaction.atomic():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using transaction.atomic() ensures that the order and all associated tickets are created as a single transaction, which helps maintain data integrity in case of errors during the creation process.

# Create the order
order = Order.objects.create(user=user)

for ticket_data in tickets_data:
# No need to fetch `movie_session` as it is already validated
Ticket.objects.create(order=order, **ticket_data)

return order


class OrderListSerializer(serializers.ModelSerializer):
tickets = TicketMovieListSerializer(
many=True,
read_only=False,
allow_empty=False
)

class Meta:
model = Order
fields = (
"id",
"tickets",
"created_at",
)
4 changes: 1 addition & 3 deletions cinema/tests/test_actor_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for retrieving actors checks that the full names are returned correctly and in the expected order. This ensures that the API endpoint is functioning as intended.


def test_post_actors(self):
response = self.client.post(
Expand Down
12 changes: 3 additions & 9 deletions cinema/tests/test_cinema_hall_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion checks that the 'Blue' cinema hall's details are correctly retrieved, ensuring the GET operation works as expected.

vip_hall = {
"name": "VIP",
"rows": 6,
Expand All @@ -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"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion checks that the 'VIP' cinema hall's details are correctly retrieved, ensuring the GET operation works as expected.


def test_post_cinema_halls(self):
response = self.client.post(
Expand All @@ -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):
Expand Down
Loading
Loading