Skip to content
Open

S #1005

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
__pycache__/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Using a very new base tag python:3.13-slim can cause missing binary wheels for some packages. Verify this tag exists and is compatible with your requirements; consider pinning to a stable minor version (for example python:3.11-slim) to avoid unexpected build failures.

env
.env
.venv
.git
*.pyc
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[flake8]
inline-quotes = "
ignore = E203, E266, W503, N807, N818, F401
max-line-length = 79
max-line-length = 89
max-complexity = 18
select = B,C,E,F,W,T4,B9,Q0,N8,VNE
exclude =
Expand Down
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.13-slim
LABEL authors="barsu"

ENV PYTHONUNBUFFERED=1

WORKDIR /app
COPY requirements.txt requirements.txt

RUN apt-get update && apt-get install -y --no-install-recommends gcc \
&& pip install --no-cache-dir -r requirements.txt \
&& apt-get remove -y gcc && apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

COPY . .


26 changes: 6 additions & 20 deletions cinema/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,10 @@ def __str__(self):
class MovieSession(models.Model):
show_time = models.DateTimeField()
movie = models.ForeignKey(
Movie,
on_delete=models.CASCADE,
related_name="movie_sessions"
Movie, on_delete=models.CASCADE, related_name="movie_sessions"
)
cinema_hall = models.ForeignKey(
CinemaHall,
on_delete=models.CASCADE,
related_name="movie_sessions"
CinemaHall, on_delete=models.CASCADE, related_name="movie_sessions"
)

class Meta:
Expand All @@ -92,9 +88,7 @@ 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,
related_name="orders"
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="orders"
)

def __str__(self):
Expand All @@ -106,15 +100,9 @@ class Meta:

class Ticket(models.Model):
movie_session = models.ForeignKey(
MovieSession,
on_delete=models.CASCADE,
related_name="tickets"
)
order = models.ForeignKey(
Order,
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")
row = models.IntegerField()
seat = models.IntegerField()

Expand Down Expand Up @@ -157,9 +145,7 @@ def save(
)

def __str__(self):
return (
f"{str(self.movie_session)} (row: {self.row}, seat: {self.seat})"
)
return f"{str(self.movie_session)} (row: {self.row}, seat: {self.seat})"

class Meta:
unique_together = ("movie_session", "row", "seat")
Expand Down
16 changes: 6 additions & 10 deletions cinema/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class Meta:


class ActorSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(read_only=True)

class Meta:
model = Actor
fields = ("id", "first_name", "last_name", "full_name")
Expand All @@ -45,9 +47,7 @@ class Meta:


class MovieListSerializer(MovieSerializer):
genres = serializers.SlugRelatedField(
many=True, read_only=True, slug_field="name"
)
genres = serializers.SlugRelatedField(many=True, read_only=True, slug_field="name")
actors = serializers.SlugRelatedField(
many=True, read_only=True, slug_field="full_name"
)
Expand Down Expand Up @@ -89,9 +89,7 @@ class Meta:
class MovieSessionListSerializer(MovieSessionSerializer):
movie_title = serializers.CharField(source="movie.title", read_only=True)
movie_image = serializers.ImageField(source="movie.image", read_only=True)
cinema_hall_name = serializers.CharField(
source="cinema_hall.name", 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
)
Expand All @@ -117,7 +115,7 @@ def validate(self, attrs):
attrs["row"],
attrs["seat"],
attrs["movie_session"].cinema_hall,
ValidationError
ValidationError,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You're passing rest_framework.exceptions.ValidationError into the model-level validator function. Note models.clean() uses django.core.exceptions.ValidationError elsewhere; while passing DRF's ValidationError here works for serializer-level validation, be mindful of the distinction (they are different classes and handled differently in other contexts).

)
return data

Expand All @@ -139,9 +137,7 @@ class Meta:
class MovieSessionDetailSerializer(MovieSessionSerializer):
movie = MovieListSerializer(many=False, read_only=True)
cinema_hall = CinemaHallSerializer(many=False, read_only=True)
taken_places = TicketSeatsSerializer(
source="tickets", many=True, read_only=True
)
taken_places = TicketSeatsSerializer(source="tickets", many=True, read_only=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MovieListSerializer uses slug_field='full_name' for actors which relies on the Actor model's full_name property being accessible — this reinforces the need to add the full_name = serializers.CharField(read_only=True) field to ActorSerializer so the list/detail serializers and DRF renderers work correctly.


class Meta:
model = MovieSession
Expand Down
16 changes: 4 additions & 12 deletions cinema/tests/test_movie_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ def sample_movie(**params):


def sample_movie_session(**params):
cinema_hall = CinemaHall.objects.create(
name="Blue", rows=20, seats_in_row=20
)
cinema_hall = CinemaHall.objects.create(name="Blue", rows=20, seats_in_row=20)

defaults = {
"show_time": "2022-06-02 14:00:00",
Expand Down Expand Up @@ -93,9 +91,7 @@ def test_filter_movies_by_genres(self):

movie3 = sample_movie(title="Movie without genres")

res = self.client.get(
MOVIE_URL, {"genres": f"{genre1.id},{genre2.id}"}
)
res = self.client.get(MOVIE_URL, {"genres": f"{genre1.id},{genre2.id}"})

serializer1 = MovieListSerializer(movie1)
serializer2 = MovieListSerializer(movie2)
Expand All @@ -117,9 +113,7 @@ def test_filter_movies_by_actors(self):

movie3 = sample_movie(title="Movie without actors")

res = self.client.get(
MOVIE_URL, {"actors": f"{actor1.id},{actor2.id}"}
)
res = self.client.get(MOVIE_URL, {"actors": f"{actor1.id},{actor2.id}"})

serializer1 = MovieListSerializer(movie1)
serializer2 = MovieListSerializer(movie2)
Expand Down Expand Up @@ -147,9 +141,7 @@ def test_filter_movies_by_title(self):
def test_retrieve_movie_detail(self):
movie = sample_movie()
movie.genres.add(Genre.objects.create(name="Genre"))
movie.actors.add(
Actor.objects.create(first_name="Actor", last_name="Last")
)
movie.actors.add(Actor.objects.create(first_name="Actor", last_name="Last"))

url = detail_url(movie.id)
res = self.client.get(url)
Expand Down
3 changes: 1 addition & 2 deletions cinema/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,7 @@ def get_serializer_class(self):
"date",
type=OpenApiTypes.DATE,
description=(
"Filter by datetime of MovieSession "
"(ex. ?date=2022-10-23)"
"Filter by datetime of MovieSession " "(ex. ?date=2022-10-23)"
),
),
]
Expand Down
Empty file.
Empty file.
17 changes: 17 additions & 0 deletions cinema_service/management/commands/wait_for_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import time

from django.core.management.base import BaseCommand
from django.db import connection
from django.db.utils import OperationalError


class Command(BaseCommand):
def handle(self, *args, **options):
while True:
try:
self.stdout.write("Waiting for database...")
connection.ensure_connection()
self.stdout.write("Database available!")
break
except OperationalError:
time.sleep(3)
37 changes: 18 additions & 19 deletions cinema_service/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,20 @@
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
Comment on lines 9 to 11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Dockerfile installs only gcc before pip installing requirements. If your requirements.txt uses non-binary psycopg2, the build will fail because libpq-dev and Python dev headers are missing. Either switch to psycopg2-binary in requirements.txt or install the necessary system packages (e.g. libpq-dev, python3-dev and optionally build-essential) in the same RUN layer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

To make the image thinner, clean apt caches and use --no-install-recommends when installing packages, and remove /var/lib/apt/lists/* in the same RUN layer. Right now you remove gcc but do not clean apt lists which keeps unnecessary files in the final image.


import os
from datetime import timedelta
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = (
"django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3"
)
SECRET_KEY = os.environ.get("SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
DEBUG = os.environ.get("DEBUG", "False").lower() in ("true", "1")

ALLOWED_HOSTS = []

Expand All @@ -45,6 +42,7 @@
"rest_framework",
"drf_spectacular",
"debug_toolbar",
"cinema_service",
"cinema",
"user",
]
Expand Down Expand Up @@ -80,18 +78,21 @@

WSGI_APPLICATION = "cinema_service.wsgi.application"


# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases


DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "postgres"),
"USER": os.environ.get("DB_USER", "postgres"),
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ.get("DB_HOST", "db"),
"PORT": os.environ.get("DB_PORT", "5432"),
Comment on lines +87 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Database settings correctly use environment variables and default HOST='db' (good for docker-compose). However DB_PASSWORD defaults to an empty string — ensure your .env provides DB_NAME/DB_USER/DB_PASSWORD (otherwise DB auth will fail).

}
}


# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

Expand All @@ -101,16 +102,13 @@
"UserAttributeSimilarityValidator",
},
{
"NAME": "django.contrib.auth.password_validation."
"MinimumLengthValidator",
"NAME": "django.contrib.auth.password_validation." "MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation."
"CommonPasswordValidator",
"NAME": "django.contrib.auth.password_validation." "CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation."
"NumericPasswordValidator",
"NAME": "django.contrib.auth.password_validation." "NumericPasswordValidator",
},
]

Expand All @@ -127,14 +125,15 @@

USE_TZ = False


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/

STATIC_URL = "static/"
STATIC_URL = "/static/"

MEDIA_URL = "/media/"

MEDIA_ROOT = BASE_DIR / "media"
STATIC_ROOT = BASE_DIR / "static"
Comment on lines +134 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

MEDIA_ROOT and STATIC_ROOT are set which is good for mounting volumes. However STATIC_URL is currently set to a relative path ("static/"). Use an absolute path starting with a slash, e.g. STATIC_URL = "/static/", to avoid incorrect static file URLs.


# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
Expand Down
23 changes: 23 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
services:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recommendation: verify the chosen base image tag python:3.13-slim exists and is intended. Also consider setting ENV PYTHONUNBUFFERED=1 and creating a non-root user to improve logging and security (these are recommended in the checklist).

app:
build:
context: .
ports:
- "8000:8000"
command: sh -c "python manage.py wait_for_db && python manage.py migrate && python manage.py runserver 0.0.0.0:8000"
env_file:
- .env
Comment on lines +7 to +9
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

HIGH: your Dockerfile installs only gcc. If your requirements include non-binary psycopg2 the build will fail unless you also install the PostgreSQL client dev headers and Python headers (e.g. libpq-dev, python3-dev or build-essential). Either add those packages to the apt-get install command (use --no-install-recommends and clean apt cache to keep the image thin) or switch to psycopg2-binary in requirements.txt to avoid needing these build deps.

depends_on:
- db
Comment on lines +9 to +11
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Dockerfile installs only gcc before running pip install. If your requirements include the non-binary psycopg2, the build will fail because PostgreSQL headers and Python dev headers are missing. Either switch to psycopg2-binary in requirements.txt or install required system packages here (for Debian-slim images: libpq-dev and python3-dev or build-essential).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You remove gcc after pip install but do not clean apt lists. To make the image thinner, clean apt caches and remove /var/lib/apt/lists/* in the same RUN layer (and use --no-install-recommends). Combine install and cleanup in one RUN to reduce final image size.

volumes:
- ./media:/app/media
- ./static:/app/static
db:
image: postgres:14-alpine
env_file:
- .env
volumes:
- db_volume:/var/lib/postgresql/data

volumes:
db_volume:
1 change: 1 addition & 0 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""

import os
import sys

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ djangorestframework
djangorestframework-simplejwt
drf-spectacular
Pillow
psycopg2-binary
Loading