diff --git a/db/migrations/0001_initial.py b/db/migrations/0001_initial.py index 8578293c0..63e831dd1 100644 --- a/db/migrations/0001_initial.py +++ b/db/migrations/0001_initial.py @@ -1,58 +1,114 @@ -# Generated by Django 4.0.2 on 2022-06-15 12:37 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Actor', - fields=[ - ('id', models.AutoField(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', - fields=[ - ('id', models.AutoField(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', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ], - ), - migrations.CreateModel( - name='Movie', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('description', models.TextField()), - ('actors', models.ManyToManyField(to='db.Actor')), - ('genres', models.ManyToManyField(to='db.Genre')), - ], - ), - migrations.CreateModel( - name='MovieSession', - fields=[ - ('id', models.AutoField(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='db.cinemahall')), - ('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='db.movie')), - ], - ), - ] +# Generated by Django 4.0.2 on 2024-10-15 13:43 + +from django.conf import settings +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(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')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='Actor', + fields=[ + ('id', models.AutoField(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', + fields=[ + ('id', models.AutoField(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', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.CreateModel( + name='Movie', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(db_index=True, max_length=255)), + ('description', models.TextField()), + ('actors', models.ManyToManyField(related_name='movies', to='db.Actor')), + ('genres', models.ManyToManyField(related_name='movies', to='db.Genre')), + ], + ), + migrations.CreateModel( + name='MovieSession', + fields=[ + ('id', models.AutoField(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='db.cinemahall')), + ('movie', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movie_sessions', to='db.movie')), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(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)), + ], + options={ + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Ticket', + fields=[ + ('id', models.AutoField(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='db.moviesession')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_tickets', to='db.order')), + ], + ), + migrations.AddConstraint( + model_name='ticket', + constraint=models.UniqueConstraint(fields=('movie_session', 'row', 'seat'), name='unique_movie_session'), + ), + ] diff --git a/db/models.py b/db/models.py index 2a3359a63..7668d347d 100644 --- a/db/models.py +++ b/db/models.py @@ -1,4 +1,8 @@ +from django.core.exceptions import ValidationError from django.db import models +from django.contrib.auth.models import AbstractUser +from django.db.models import UniqueConstraint +from django.conf import settings class Genre(models.Model): @@ -17,7 +21,7 @@ def __str__(self) -> str: class Movie(models.Model): - title = models.CharField(max_length=255) + title = models.CharField(max_length=255, db_index=True) description = models.TextField() actors = models.ManyToManyField(to=Actor, related_name="movies") genres = models.ManyToManyField(to=Genre, related_name="movies") @@ -50,3 +54,68 @@ class MovieSession(models.Model): def __str__(self) -> str: return f"{self.movie.title} {str(self.show_time)}" + + +class Order(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE + ) + + class Meta: + ordering = ["-created_at"] + + def __str__(self) -> str: + return f"{self.created_at}" + + +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="order_tickets" + ) + row = models.IntegerField() + seat = models.IntegerField() + + def clean(self) -> None: + errors = {} + + if not (1 <= self.row <= self.movie_session.cinema_hall.rows): + errors["row"] = [ + f"row number must be in available range: " + f"(1, rows): (1, {self.movie_session.cinema_hall.rows})" + ] + if not (1 <= self.seat <= self.movie_session.cinema_hall.seats_in_row): + errors["seat"] = [ + f"seat number must be in available range: " + f"(1, seats_in_row): " + f"(1, {self.movie_session.cinema_hall.seats_in_row})" + ] + if errors: + raise ValidationError(errors) + + def save(self, *args, **kwargs) -> None: + self.full_clean() + super().save(*args, **kwargs) + + class Meta: + constraints = [ + UniqueConstraint(fields=["movie_session", "row", "seat"], + name="unique_movie_session"), + ] + + def __str__(self) -> str: + return (f"{self.movie_session.movie.title} " + f"{self.movie_session.show_time}" + f" (row: {self.row}, seat: {self.seat})") + + +class User(AbstractUser): + pass diff --git a/requirements.txt b/requirements.txt index 4f9a17175..30a0f786a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ flake8-quotes==3.3.1 flake8-variables-names==0.0.5 pep8-naming==0.13.2 pytest==7.1.3 -pytest-django==4.5.2 \ No newline at end of file +pytest-django==4.5.2 +django-extensions==3.2.3 \ No newline at end of file diff --git a/services/movie.py b/services/movie.py index 4d0f63237..8cacc0e21 100644 --- a/services/movie.py +++ b/services/movie.py @@ -1,11 +1,12 @@ from django.db.models import QuerySet - +from django.db import transaction from db.models import Movie def get_movies( genres_ids: list[int] = None, actors_ids: list[int] = None, + title: str = None, ) -> QuerySet: queryset = Movie.objects.all() @@ -15,6 +16,9 @@ def get_movies( if actors_ids: queryset = queryset.filter(actors__id__in=actors_ids) + if title: + queryset = queryset.filter(title__icontains=title) + return queryset @@ -22,6 +26,7 @@ def get_movie_by_id(movie_id: int) -> Movie: return Movie.objects.get(id=movie_id) +@transaction.atomic def create_movie( movie_title: str, movie_description: str, diff --git a/services/movie_session.py b/services/movie_session.py index f326a082e..57e337d48 100644 --- a/services/movie_session.py +++ b/services/movie_session.py @@ -1,5 +1,4 @@ from django.db.models import QuerySet - from db.models import MovieSession @@ -42,3 +41,12 @@ def update_movie_session( def delete_movie_session_by_id(session_id: int) -> None: MovieSession.objects.get(id=session_id).delete() + + +def get_taken_seats(movie_session_id: int) -> list[dict]: + movie_session = get_movie_session_by_id(movie_session_id) + seats_rows = [ + {"row": ticket.row, "seat": ticket.seat} + for ticket in movie_session.tickets.all() + ] + return seats_rows diff --git a/services/order.py b/services/order.py new file mode 100644 index 000000000..cb2f1bc98 --- /dev/null +++ b/services/order.py @@ -0,0 +1,34 @@ +from django.contrib.auth import get_user_model +from django.db.models import QuerySet +from django.db import transaction + + +from db.models import Ticket, Order + + +@transaction.atomic +def create_order(tickets: list[dict], + username: str, + date: str = None + ) -> QuerySet: + user = get_user_model().objects.get(username=username) + order = Order.objects.create(user=user) + + if date: + order.created_at = date + + for ticket in tickets: + Ticket.objects.create(order=order, + movie_session_id=ticket["movie_session"], + row=ticket["row"], + seat=ticket["seat"] + ) + order.save() + return order + + +def get_orders(username: str = None) -> QuerySet: + orders = Order.objects.all() + if username: + return orders.filter(user__username=username) + return orders diff --git a/services/user.py b/services/user.py new file mode 100644 index 000000000..129cca2fa --- /dev/null +++ b/services/user.py @@ -0,0 +1,51 @@ +from django.contrib.auth import get_user_model + + +User = get_user_model() + + +def create_user(username: str, + password: str, + email: str = None, + first_name: str = None, + last_name: str = None + ) -> User: + user = get_user_model().objects.create_user( + username=username, + password=password + ) + update_user( + user_id=user.id, + email=email, + first_name=first_name, + last_name=last_name + ) + + return user + + +def get_user(user_id: int) -> User: + return get_user_model().objects.get(pk=user_id) + + +def update_user(user_id: int, + username: str = None, + password: str = None, + email: str = None, + first_name: str = None, + last_name: str = None) -> None: + user = get_user(user_id) + + if password: + user.set_password(password) + user_data = { + "username": username, + "email": email, + "first_name": first_name, + "last_name": last_name + } + for attr, value in user_data.items(): + if value is not None: + setattr(user, attr, value) + + user.save() diff --git a/settings.py b/settings.py index f25910b30..720e0b51d 100644 --- a/settings.py +++ b/settings.py @@ -1,5 +1,6 @@ import os + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -24,5 +25,9 @@ USE_TZ = False INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "django_extensions", "db", ] +AUTH_USER_MODEL = "db.User"