diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5634ae17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.venv +.env +__pycache__/ +*.pyc +.idea +media +.pytest_cache/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..45c12656 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-slim + +LABEL maintainer="rarturus" + +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +COPY requirements.txt . + +RUN apt-get update && apt-get install -y build-essential libpq-dev && rm -rf /var/lib/apt/lists/* +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN mkdir -p /files/media + +RUN adduser \ + --disabled-password \ + --no-create-home \ + my_user + +RUN chown -R my_user:my_user /files/media +RUN chmod -R 755 /files/media + +USER my_user diff --git a/cinema/management/__init__.py b/cinema/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cinema/management/commands/__init__.py b/cinema/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cinema/management/commands/wait_for_db.py b/cinema/management/commands/wait_for_db.py new file mode 100644 index 00000000..209248e1 --- /dev/null +++ b/cinema/management/commands/wait_for_db.py @@ -0,0 +1,17 @@ +import time +from django.core.management.base import BaseCommand +from django.db import connections +from django.db.utils import OperationalError + + +class WaitForDbCommand(BaseCommand): + def handle(self, *args, **options): + self.stdout.write("Waiting for database...") + while True: + try: + connections["default"].cursor() + break + 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/0001_initial.py b/cinema/migrations/0001_initial.py index 9a4ce64b..e74ad8e1 100644 --- a/cinema/migrations/0001_initial.py +++ b/cinema/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 4.0.4 on 2022-06-14 12:26 +# Generated by Django 6.0.4 on 2026-04-28 21:22 import cinema.models +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -16,179 +16,79 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name="Actor", + name='Actor', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("first_name", models.CharField(max_length=255)), - ("last_name", models.CharField(max_length=255)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=255)), + ('last_name', models.CharField(max_length=255)), ], ), migrations.CreateModel( - name="CinemaHall", + name='CinemaHall', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=255)), - ("rows", models.IntegerField()), - ("seats_in_row", models.IntegerField()), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('rows', models.IntegerField()), + ('seats_in_row', models.IntegerField()), ], ), migrations.CreateModel( - name="Genre", + name='Genre', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=255, unique=True)), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), ], ), migrations.CreateModel( - name="Movie", + name='Movie', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("title", models.CharField(max_length=255)), - ("description", models.TextField()), - ("duration", models.IntegerField()), - ( - "image", - models.ImageField( - null=True, - upload_to=cinema.models.movie_image_file_path, - ), - ), - ( - "actors", - models.ManyToManyField(blank=True, to="cinema.actor"), - ), - ( - "genres", - models.ManyToManyField(blank=True, to="cinema.genre"), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField()), + ('duration', models.IntegerField()), + ('image', models.ImageField(null=True, upload_to=cinema.models.movie_image_file_path)), + ('actors', models.ManyToManyField(blank=True, related_name='movies', to='cinema.actor')), + ('genres', models.ManyToManyField(blank=True, related_name='movies', to='cinema.genre')), ], options={ - "ordering": ["title"], + 'ordering': ['title'], }, ), migrations.CreateModel( - name="MovieSession", + name='MovieSession', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("show_time", models.DateTimeField()), - ( - "cinema_hall", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="cinema.cinemahall", - ), - ), - ( - "movie", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="cinema.movie", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('show_time', models.DateTimeField()), + ('cinema_hall', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.cinemahall')), + ('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='cinema.movie')), ], options={ - "ordering": ["-show_time"], + 'ordering': ['-show_time'], }, ), migrations.CreateModel( - name="Order", + name='Order', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("created_at", models.DateTimeField(auto_now_add=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)), ], options={ - "ordering": ["-created_at"], + 'ordering': ['-created_at'], }, ), migrations.CreateModel( - name="Ticket", + name='Ticket', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("row", models.IntegerField()), - ("seat", models.IntegerField()), - ( - "movie_session", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="cinema.moviesession", - ), - ), - ( - "order", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="tickets", - to="cinema.order", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('row', models.IntegerField()), + ('seat', models.IntegerField()), + ('movie_session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='cinema.moviesession')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to='cinema.order')), ], options={ - "ordering": ["row", "seat"], - "unique_together": {("movie_session", "row", "seat")}, + 'ordering': ['row', 'seat'], + 'unique_together': {('movie_session', 'row', 'seat')}, }, ), ] diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 90dde772..c3aa6a93 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -9,6 +9,7 @@ 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 @@ -86,12 +87,15 @@ DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": os.environ.get("SQL_ENGINE", "django.db.backends.sqlite3"), + "NAME": os.environ.get("SQL_DATABASE", BASE_DIR / "db.sqlite3"), + "USER": os.environ.get("SQL_USER", "user"), + "PASSWORD": os.environ.get("SQL_PASSWORD", "password"), + "HOST": os.environ.get("SQL_HOST", "localhost"), + "PORT": os.environ.get("SQL_PORT", "5432"), } } - # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators @@ -134,7 +138,8 @@ STATIC_URL = "static/" MEDIA_URL = "/media/" -MEDIA_ROOT = BASE_DIR / "media" +MEDIA_ROOT = "/files/media" + # Default primary key field type # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field @@ -167,7 +172,7 @@ } SIMPLE_JWT = { - "ACCESS_TOKEN_LIFETIME": timedelta(minutes=5), + "ACCESS_TOKEN_LIFETIME": timedelta(days=1), "REFRESH_TOKEN_LIFETIME": timedelta(days=1), "ROTATE_REFRESH_TOKENS": False, } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..a432af78 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + app: + build: + context: . + env_file: + - .env + command: > + sh -c "python manage.py wait_for_db && python manage.py migrate && + python manage.py runserver 0.0.0.0:8000" + volumes: + - media_data:/files/media + depends_on: + - db + ports: + - "8000:8000" + + db: + image: postgres:16.0-alpine3.17 + environment: + POSTGRES_USER: "rarturus" + POSTGRES_PASSWORD: "password" + POSTGRES_DB: "db" + restart: always + ports: + - "5432:5432" + env_file: + - .env + volumes: + - db_data:/var/lib/postgresql/data + + +volumes: + db_data: + media_data: diff --git a/requirements.txt b/requirements.txt index 2dc12c67..60af6ce3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ djangorestframework djangorestframework-simplejwt drf-spectacular Pillow +psycopg==3.3.3 +psycopg-binary==3.3.3 diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py index c11bcb14..e4d415c1 100644 --- a/user/migrations/0001_initial.py +++ b/user/migrations/0001_initial.py @@ -1,9 +1,8 @@ -# Generated by Django 4.0.4 on 2022-05-10 11:54 +# Generated by Django 6.0.4 on 2026-04-28 21:22 -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models import django.utils.timezone +import user.models +from django.db import migrations, models class Migration(migrations.Migration): @@ -11,128 +10,33 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), + ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( - name="User", + name='User', fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "password", - models.CharField(max_length=128, verbose_name="password"), - ), - ( - "last_login", - models.DateTimeField( - blank=True, null=True, verbose_name="last login" - ), - ), - ( - "is_superuser", - models.BooleanField( - default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", - verbose_name="superuser status", - ), - ), - ( - "username", - models.CharField( - error_messages={ - "unique": "A user with that username already exists." - }, - help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", - max_length=150, - unique=True, - validators=[ - django.contrib.auth.validators.UnicodeUsernameValidator() - ], - verbose_name="username", - ), - ), - ( - "first_name", - models.CharField( - blank=True, max_length=150, verbose_name="first name" - ), - ), - ( - "last_name", - models.CharField( - blank=True, max_length=150, verbose_name="last name" - ), - ), - ( - "email", - models.EmailField( - blank=True, - max_length=254, - verbose_name="email address", - ), - ), - ( - "is_staff", - models.BooleanField( - default=False, - help_text="Designates whether the user can log into this admin site.", - verbose_name="staff status", - ), - ), - ( - "is_active", - models.BooleanField( - default=True, - help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", - verbose_name="active", - ), - ), - ( - "date_joined", - models.DateTimeField( - default=django.utils.timezone.now, - verbose_name="date joined", - ), - ), - ( - "groups", - models.ManyToManyField( - blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", - related_name="user_set", - related_query_name="user", - to="auth.group", - verbose_name="groups", - ), - ), - ( - "user_permissions", - models.ManyToManyField( - blank=True, - help_text="Specific permissions for this user.", - related_name="user_set", - related_query_name="user", - to="auth.permission", - verbose_name="user permissions", - ), - ), + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='email address')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), ], options={ - "verbose_name": "user", - "verbose_name_plural": "users", - "abstract": False, + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, }, managers=[ - ("objects", django.contrib.auth.models.UserManager()), + ('objects', user.models.UserManager()), ], ), ] diff --git a/user/migrations/0002_alter_user_managers_remove_user_username_and_more.py b/user/migrations/0002_alter_user_managers_remove_user_username_and_more.py deleted file mode 100644 index 66e2274f..00000000 --- a/user/migrations/0002_alter_user_managers_remove_user_username_and_more.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.0.4 on 2022-06-14 12:37 - -from django.db import migrations, models -import user.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("user", "0001_initial"), - ] - - operations = [ - migrations.AlterModelManagers( - name="user", - managers=[ - ("objects", user.models.UserManager()), - ], - ), - migrations.RemoveField( - model_name="user", - name="username", - ), - migrations.AlterField( - model_name="user", - name="email", - field=models.EmailField( - max_length=254, unique=True, verbose_name="email address" - ), - ), - ]