From eaee942ecaee4c3150be26334c587e348591d3a5 Mon Sep 17 00:00:00 2001 From: Ihor Zasimenko Date: Wed, 18 Mar 2026 18:49:40 +0200 Subject: [PATCH 1/5] Setup Docker deployment, env files, fixtures, and admin access --- .dockerignore | 8 ++ .env.docker | 15 +++ .env.example | 10 ++ .env.local | 10 ++ .gitignore | 18 +++- Dockerfile | 10 ++ cinema/management/commands/wait_for_db.py | 17 +++ ...ovie_actors_alter_movie_genres_and_more.py | 41 +++++++ cinema_service/settings.py | 102 ++++-------------- docker-compose.yml | 32 ++++++ fixtures.json | 70 ++++++++++++ requirements.txt | Bin 157 -> 1236 bytes 12 files changed, 252 insertions(+), 81 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.docker create mode 100644 .env.example create mode 100644 .env.local create mode 100644 Dockerfile create mode 100644 cinema/management/commands/wait_for_db.py create mode 100644 cinema/migrations/0002_alter_movie_actors_alter_movie_genres_and_more.py create mode 100644 docker-compose.yml create mode 100644 fixtures.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3b45be49 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.venv +__pycache__/ +*.pyc +*.pyo +*.pyd +*.sqlite3 +.git +.github diff --git a/.env.docker b/.env.docker new file mode 100644 index 00000000..a8e0e664 --- /dev/null +++ b/.env.docker @@ -0,0 +1,15 @@ +DJANGO_SECRET_KEY=unsafe-secret-key +DJANGO_DEBUG=False + +POSTGRES_DB=cinema +POSTGRES_USER=cinema_user +POSTGRES_PASSWORD=cinema_pass +POSTGRES_HOST=db +POSTGRES_PORT=5432 + +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 + +# Автоматичний суперкористувач +DJANGO_SUPERUSER_USERNAME=admin2 +DJANGO_SUPERUSER_EMAIL=admin2@test.com +DJANGO_SUPERUSER_PASSWORD=Admin123! diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..30a9d9b6 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Django settings +DJANGO_SECRET_KEY=unsafe-secret-key +DJANGO_DEBUG=True + +# PostgreSQL settings +POSTGRES_DB=cinema +POSTGRES_USER=cinema_user +POSTGRES_PASSWORD=cinema_pass +POSTGRES_HOST=db +POSTGRES_PORT=5432 diff --git a/.env.local b/.env.local new file mode 100644 index 00000000..fdace40b --- /dev/null +++ b/.env.local @@ -0,0 +1,10 @@ +DJANGO_SECRET_KEY=unsafe-secret-key +DJANGO_DEBUG=True + +POSTGRES_DB=cinema +POSTGRES_USER=cinema_user +POSTGRES_PASSWORD=cinema_pass +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 + +DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1 diff --git a/.gitignore b/.gitignore index 0b1609c6..26553e37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,26 @@ +# IDE / Editor configs .idea/ .vscode/ *.iml + +# Environment files .env +.env.local +.env.docker + +# OS files .DS_Store + +# Virtual environments venv/ +.venv/ + +# Python cache .pytest_cache/ **__pycache__/ + +# SQLite DB (якщо використовується для тестів) **db.sqlite3 -media + +# Media files (зберігаються у volume) +media/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..13053331 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["gunicorn", "cinema.wsgi:application", "--bind", "0.0.0.0:8000"] diff --git a/cinema/management/commands/wait_for_db.py b/cinema/management/commands/wait_for_db.py new file mode 100644 index 00000000..f97ad166 --- /dev/null +++ b/cinema/management/commands/wait_for_db.py @@ -0,0 +1,17 @@ +import time +from django.db import connections +from django.db.utils import OperationalError +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + def handle(self, *args, **options): + self.stdout.write("Waiting for database...") + db_conn = None + while not db_conn: + try: + db_conn = connections["default"] + except OperationalError: + self.stdout.write("Database unavailable, waiting 1 second...") + time.sleep(1) + self.stdout.write(self.style.SUCCESS("Database available!")) diff --git a/cinema/migrations/0002_alter_movie_actors_alter_movie_genres_and_more.py b/cinema/migrations/0002_alter_movie_actors_alter_movie_genres_and_more.py new file mode 100644 index 00000000..bd2c49e3 --- /dev/null +++ b/cinema/migrations/0002_alter_movie_actors_alter_movie_genres_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.0.3 on 2026-03-18 16:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cinema', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='movie', + name='actors', + field=models.ManyToManyField(blank=True, related_name='movies', to='cinema.actor'), + ), + migrations.AlterField( + model_name='movie', + name='genres', + field=models.ManyToManyField(blank=True, related_name='movies', to='cinema.genre'), + ), + migrations.AlterField( + model_name='moviesession', + name='cinema_hall', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.cinemahall'), + ), + migrations.AlterField( + model_name='moviesession', + name='movie', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.movie'), + ), + migrations.AlterField( + model_name='order', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 90dde772..f723ea7f 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -1,39 +1,15 @@ -""" -Django settings for cinema_service project. - -Generated by 'django-admin startproject' using Django 4.0.4. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/topics/settings/ - -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 +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "unsafe-secret-key") +DEBUG = os.getenv("DJANGO_DEBUG", "True") == "True" -# 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" -) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost").split(",") -ALLOWED_HOSTS = [] - -INTERNAL_IPS = [ - "127.0.0.1", -] - -# Application definition +INTERNAL_IPS = ["127.0.0.1"] INSTALLED_APPS = [ "django.contrib.admin", @@ -80,77 +56,49 @@ 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.getenv("POSTGRES_DB", "cinema"), + "USER": os.getenv("POSTGRES_USER", "cinema_user"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD", "cinema_pass"), + "HOST": os.getenv("POSTGRES_HOST", "db"), + "PORT": os.getenv("POSTGRES_PORT", "5432"), } } - -# Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation." - "UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", - }, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] AUTH_USER_MODEL = "user.User" -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ - LANGUAGE_CODE = "en-us" - TIME_ZONE = "UTC" - USE_I18N = True +USE_TZ = True -USE_TZ = False - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ - -STATIC_URL = "static/" +STATIC_URL = "/static/" +STATIC_ROOT = os.getenv("DJANGO_STATIC_ROOT", BASE_DIR / "static") MEDIA_URL = "/media/" -MEDIA_ROOT = BASE_DIR / "media" - -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field +MEDIA_ROOT = os.getenv("DJANGO_MEDIA_ROOT", BASE_DIR / "media") DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": {"anon": "10/day", "user": "30/day"}, - "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_simplejwt.authentication.JWTAuthentication", - ), } SPECTACULAR_SETTINGS = { @@ -158,12 +106,6 @@ "DESCRIPTION": "Order cinema tickets", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, - "SWAGGER_UI_SETTINGS": { - "deepLinking": True, - "defaultModelRendering": "model", - "defaultModelsExpandDepth": 2, - "defaultModelExpandDepth": 2, - }, } SIMPLE_JWT = { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..047e7c2f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + db: + image: postgres:15 + env_file: + - .env.docker + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + + app: + build: . + env_file: + - .env.docker + command: > + sh -c "python manage.py wait_for_db && + python manage.py migrate && + python manage.py collectstatic --noinput && + gunicorn cinema_service.wsgi:application --bind 0.0.0.0:8000" + volumes: + - .:/app + - static_volume:/app/static + - media_volume:/app/media + ports: + - "8000:8000" + depends_on: + - db + +volumes: + postgres_data: + static_volume: + media_volume: diff --git a/fixtures.json b/fixtures.json new file mode 100644 index 00000000..995608f5 --- /dev/null +++ b/fixtures.json @@ -0,0 +1,70 @@ +[ +{ +"model": "cinema.genre", +"pk": 1, +"fields": { +"name": "Action" +} +}, +{ +"model": "cinema.genre", +"pk": 2, +"fields": { +"name": "Comedy" +} +}, +{ +"model": "cinema.actor", +"pk": 1, +"fields": { +"first_name": "Tom", +"last_name": "Hanks" +} +}, +{ +"model": "cinema.actor", +"pk": 2, +"fields": { +"first_name": "Scarlett", +"last_name": "Johansson" +} +}, +{ +"model": "cinema.cinemahall", +"pk": 1, +"fields": { +"name": "Hall 1", +"rows": 10, +"seats_in_row": 20 +} +}, +{ +"model": "cinema.cinemahall", +"pk": 2, +"fields": { +"name": "Hall 2", +"rows": 5, +"seats_in_row": 15 +} +}, +{ +"model": "cinema.movie", +"pk": 1, +"fields": { +"title": "Avengers", +"description": "Superhero team saving the world", +"duration": 120, +"genres": [1], +"actors": [2] +} +}, +{ +"model": "cinema.moviesession", +"pk": 1, +"fields": { +"show_time": "2026-03-20T18:00:00Z", +"cinema_hall": 1, +"movie": 1 +} +} +] diff --git a/requirements.txt b/requirements.txt index 2dc12c675b6b69a074b9401993e1f56a7fa0cc69..3c558225d80e4d57259453466a4dc2509c7f7fac 100644 GIT binary patch literal 1236 zcma)+PjAye5XI+QiSH1pXQRY{k{)_MLPD)TLIMeKLQeAsw@Ga5q*eIv!0*j;vqtIx zS>D*0H*em&o%NsJmG!o^+B#d>%C5Mk_R=O?T3^_iRkr8qz$seVYa*|>#~yuWcOZ6f zG#*!U%sut!Vh}BCL5;Z`z!oqzTpQjixWz0^)ehcarnL2BtUy+_Zn?PZ9(0FsaOx>B-QG*v`f~|z4F*S=$Z4qA(Fcq zMhp81Qv;{EOU`F!|HVFd#hP{QihES;c#|8EEhV}y-1(ZQ7(#VTot@VZBKLPkt|C2M z{Q$d#G3`YOQ|jtDTy~|VQVoM9B`T#^89&X7{0T?5;Dnr0O_iia9)){yxy8BEo4Qz^ zaFc=ZmP#A0hMg17#F?_wEj=Bn)WEk!ML|WeY2S{XQBpN@g|hYuuPN2E9o;=~3pp9g zypVZ!p4+=mAa3ldeK}Q{oqAO4o4vJ<_Q9j!a`Gq6;S!Zg)ZV)0?7(#3RYzwFlotF( zNAhy&K7^Y+9D8Q0WsTDoT4&yo(9wC%ByK>-8|9>Z$#b%|U)0k6#(b1ii7!E%^8=YR zQB9Cel#s!rd6s{nNZ*rl8{y1ndvDh~sa|IfzK(EJ=XwN3$I}AsI&(2o%3br%j(6>A M$5ZT%zE!!#U*u@I6951J literal 157 zcmZvUF%rWd3 Date: Wed, 18 Mar 2026 18:53:42 +0200 Subject: [PATCH 2/5] Fix flake8 E501 issues in settings.py --- cinema_service/settings.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/cinema_service/settings.py b/cinema_service/settings.py index f723ea7f..a0b13e3a 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -68,10 +68,30 @@ } AUTH_PASSWORD_VALIDATORS = [ - {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, - {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, - {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, - {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, + { + "NAME": ( + "django.contrib.auth.password_validation." + "UserAttributeSimilarityValidator" + ) + }, + { + "NAME": ( + "django.contrib.auth.password_validation." + "MinimumLengthValidator" + ) + }, + { + "NAME": ( + "django.contrib.auth.password_validation." + "CommonPasswordValidator" + ) + }, + { + "NAME": ( + "django.contrib.auth.password_validation." + "NumericPasswordValidator" + ) + }, ] AUTH_USER_MODEL = "user.User" @@ -98,7 +118,10 @@ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], - "DEFAULT_THROTTLE_RATES": {"anon": "10/day", "user": "30/day"}, + "DEFAULT_THROTTLE_RATES": { + "anon": "10/day", + "user": "30/day", + }, } SPECTACULAR_SETTINGS = { From 0c0da8c9a09b0bb4c5d48e566a30a9bedb5b8f8c Mon Sep 17 00:00:00 2001 From: Ihor Zasimenko Date: Wed, 18 Mar 2026 18:55:59 +0200 Subject: [PATCH 3/5] Remove bind mount from docker-compose to ensure independent container --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 047e7c2f..e361636f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,6 @@ services: python manage.py collectstatic --noinput && gunicorn cinema_service.wsgi:application --bind 0.0.0.0:8000" volumes: - - .:/app - static_volume:/app/static - media_volume:/app/media ports: From e8505c6157d92414b207398436946e118f841e31 Mon Sep 17 00:00:00 2001 From: Ihor Zasimenko Date: Wed, 18 Mar 2026 18:56:50 +0200 Subject: [PATCH 4/5] Update .dockerignore to exclude env files and media directory --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 3b45be49..deba6a8d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,5 @@ __pycache__/ *.sqlite3 .git .github +.env* +media/ From 801f54eaa35b0f4be950897714de7547031f4980 Mon Sep 17 00:00:00 2001 From: Ihor Zasimenko Date: Wed, 18 Mar 2026 18:58:21 +0200 Subject: [PATCH 5/5] Fix Dockerfile CMD to use cinema_service.wsgi:application --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 13053331..fb1c5358 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,4 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["gunicorn", "cinema.wsgi:application", "--bind", "0.0.0.0:8000"] +CMD ["gunicorn", "cinema_service.wsgi:application", "--bind", "0.0.0.0:8000"]