diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..451315f1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +__pycache__/ +env +.env +.venv +.git +*.pyc diff --git a/.flake8 b/.flake8 index 364ad762..acb8c7fb 100644 --- a/.flake8 +++ b/.flake8 @@ -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 = diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..fdfae0f5 --- /dev/null +++ b/Dockerfile @@ -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 . . + + diff --git a/cinema/models.py b/cinema/models.py index 49a5ea87..1adb1223 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -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: @@ -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): @@ -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() @@ -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") diff --git a/cinema/serializers.py b/cinema/serializers.py index fdff9ff8..049531f3 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -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") @@ -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" ) @@ -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 ) @@ -117,7 +115,7 @@ def validate(self, attrs): attrs["row"], attrs["seat"], attrs["movie_session"].cinema_hall, - ValidationError + ValidationError, ) return data @@ -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) class Meta: model = MovieSession diff --git a/cinema/tests/test_movie_api.py b/cinema/tests/test_movie_api.py index e8e32936..51b3502f 100644 --- a/cinema/tests/test_movie_api.py +++ b/cinema/tests/test_movie_api.py @@ -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", @@ -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) @@ -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) @@ -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) diff --git a/cinema/views.py b/cinema/views.py index 8833e27d..44818659 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -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)" ), ), ] diff --git a/cinema_service/management/__init__.py b/cinema_service/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cinema_service/management/commands/__init__.py b/cinema_service/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cinema_service/management/commands/wait_for_db.py b/cinema_service/management/commands/wait_for_db.py new file mode 100644 index 00000000..cea8d72f --- /dev/null +++ b/cinema_service/management/commands/wait_for_db.py @@ -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) diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 90dde772..d807ab17 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -9,23 +9,20 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.0/ref/settings/ """ + +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 = [] @@ -45,6 +42,7 @@ "rest_framework", "drf_spectacular", "debug_toolbar", + "cinema_service", "cinema", "user", ] @@ -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"), } } - # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators @@ -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", }, ] @@ -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" # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..8167d301 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +services: + 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 + depends_on: + - db + 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: diff --git a/manage.py b/manage.py index f64b2432..5045324a 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys diff --git a/requirements.txt b/requirements.txt index 2dc12c67..622155ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ djangorestframework djangorestframework-simplejwt drf-spectacular Pillow +psycopg2-binary