diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..deba6a8d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.venv +__pycache__/ +*.pyc +*.pyo +*.pyd +*.sqlite3 +.git +.github +.env* +media/ 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..fb1c5358 --- /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_service.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..a0b13e3a 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 = [] - -INTERNAL_IPS = [ - "127.0.0.1", -] +ALLOWED_HOSTS = os.getenv("DJANGO_ALLOWED_HOSTS", "localhost").split(",") -# Application definition +INTERNAL_IPS = ["127.0.0.1"] INSTALLED_APPS = [ "django.contrib.admin", @@ -80,77 +56,72 @@ 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." + "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" + ) }, ] 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", - ), + "DEFAULT_THROTTLE_RATES": { + "anon": "10/day", + "user": "30/day", + }, } SPECTACULAR_SETTINGS = { @@ -158,12 +129,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..e361636f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +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: + - 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 2dc12c67..3c558225 100644 Binary files a/requirements.txt and b/requirements.txt differ