├── cinema ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_genre_api.py │ ├── test_actor_api.py │ ├── test_order_api.py │ ├── test_cinema_hall_api.py │ ├── test_movie_session_api.py │ └── test_movie_api.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_genre_name.py │ ├── 0003_movie_duration.py │ ├── 0002_initial.py │ └── 0001_initial.py ├── apps.py ├── admin.py ├── urls.py ├── views.py ├── serializers.py └── models.py ├── user ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── tests.py ├── models.py ├── admin.py └── apps.py ├── cinema_service ├── __init__.py ├── urls.py ├── asgi.py ├── wsgi.py └── settings.py ├── .gitignore ├── requirements.txt ├── .flake8 ├── .github └── workflows │ └── test.yml ├── manage.py ├── checklist.md ├── README.md └── cinema_service_db_data.json /cinema/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cinema/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cinema_service/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cinema/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /user/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class User(AbstractUser): 5 | pass 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | *.iml 4 | .env 5 | .DS_Store 6 | venv/ 7 | .pytest_cache/ 8 | **__pycache__/ 9 | *.pyc 10 | app/db.sqlite3 -------------------------------------------------------------------------------- /user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from .models import User 4 | 5 | admin.site.register(User, UserAdmin) 6 | -------------------------------------------------------------------------------- /user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "user" 7 | -------------------------------------------------------------------------------- /cinema/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CinemaConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "cinema" 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==4.1 2 | flake8==5.0.4 3 | flake8-quotes==3.3.1 4 | flake8-variables-names==0.0.5 5 | pep8-naming==0.13.2 6 | django-debug-toolbar==3.2.4 7 | djangorestframework==3.13.1 -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | inline-quotes = " 3 | ignore = E203, E266, W503, N807, N818, F401 4 | max-line-length = 79 5 | max-complexity = 18 6 | select = B,C,E,F,W,T4,B9,Q0,N8,VNE 7 | exclude = 8 | **migrations 9 | venv 10 | tests -------------------------------------------------------------------------------- /cinema_service/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path("admin/", admin.site.urls), 6 | path("api/cinema/", include("cinema.urls", namespace="cinema")), 7 | path("__debug__/", include("debug_toolbar.urls")), 8 | ] 9 | -------------------------------------------------------------------------------- /cinema/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import ( 4 | CinemaHall, 5 | Genre, 6 | Actor, 7 | Movie, 8 | MovieSession, 9 | Order, 10 | Ticket, 11 | ) 12 | 13 | admin.site.register(CinemaHall) 14 | admin.site.register(Genre) 15 | admin.site.register(Actor) 16 | admin.site.register(Movie) 17 | admin.site.register(MovieSession) 18 | admin.site.register(Order) 19 | admin.site.register(Ticket) 20 | -------------------------------------------------------------------------------- /cinema_service/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for cinema_service project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /cinema_service/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for cinema_service project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /cinema/migrations/0004_alter_genre_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-06-16 08:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('cinema', '0003_movie_duration'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='genre', 15 | name='name', 16 | field=models.CharField(max_length=255, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cinema/migrations/0003_movie_duration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-10 12:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('cinema', '0002_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='movie', 15 | name='duration', 16 | field=models.IntegerField(default=123), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cinema/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework import routers 3 | 4 | from cinema.views import ( 5 | GenreViewSet, 6 | ActorViewSet, 7 | CinemaHallViewSet, 8 | MovieViewSet, 9 | MovieSessionViewSet, 10 | ) 11 | 12 | router = routers.DefaultRouter() 13 | router.register("genres", GenreViewSet) 14 | router.register("actors", ActorViewSet) 15 | router.register("cinema_halls", CinemaHallViewSet) 16 | router.register("movies", MovieViewSet) 17 | router.register("movie_sessions", MovieSessionViewSet) 18 | 19 | urlpatterns = [path("", include(router.urls))] 20 | 21 | app_name = "cinema" 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 10 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Set Up Python 3.10 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: "3.10" 21 | 22 | - name: Install requirements 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -r requirements.txt 26 | 27 | - name: Run flake8 28 | run: flake8 29 | 30 | - name: Run tests 31 | timeout-minutes: 5 32 | run: python manage.py test 33 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /checklist.md: -------------------------------------------------------------------------------- 1 | # Сheck Your Code Against the Following Points 2 | 3 | ## Don't Push db files 4 | 5 | Make sure you don't push db files (files with `.sqlite`, `.db3`, etc. extension). 6 | 7 | ## Code Efficiency 8 | Don't split the date, it's already in the format needed. 9 | 10 | Good example: 11 | 12 | ```python 13 | queryset = queryset.filter(show_time=date) 14 | ``` 15 | 16 | Bad example: 17 | 18 | ```python 19 | date = date.split("-") 20 | queryset = queryset.filter(show_time__year=date[0], 21 | show_time__month=date[1], 22 | show_time__day=date[2]) 23 | ``` 24 | 25 | ## Code Style 26 | 1. Make sure you've added a blank line at the end to all your files. 27 | 2. Group imports using `()` if needed. 28 | 29 | Good example: 30 | 31 | ```python 32 | from django.contrib.auth.mixins import ( 33 | LoginRequiredMixin, 34 | UserPassesTestMixin, 35 | PermissionRequiredMixin, 36 | ) 37 | ``` 38 | 39 | Bad example: 40 | 41 | ```python 42 | from django.contrib.auth.mixins import LoginRequiredMixin, \ 43 | UserPassesTestMixin, PermissionRequiredMixin 44 | ``` 45 | 46 | Another bad example: 47 | 48 | ```python 49 | from django.contrib.auth.mixins import ( 50 | LoginRequiredMixin, 51 | UserPassesTestMixin, PermissionRequiredMixin, 52 | ) 53 | ``` 54 | 55 | ## Clean Code 56 | 57 | Add comments, prints, and functions to check your solution when you write your code. 58 | Don't forget to delete them when you are ready to commit and push your code. 59 | -------------------------------------------------------------------------------- /cinema/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession 4 | 5 | from cinema.serializers import ( 6 | GenreSerializer, 7 | ActorSerializer, 8 | CinemaHallSerializer, 9 | MovieSerializer, 10 | MovieSessionSerializer, 11 | MovieSessionListSerializer, 12 | MovieDetailSerializer, 13 | MovieSessionDetailSerializer, 14 | MovieListSerializer, 15 | ) 16 | 17 | 18 | class GenreViewSet(viewsets.ModelViewSet): 19 | queryset = Genre.objects.all() 20 | serializer_class = GenreSerializer 21 | 22 | 23 | class ActorViewSet(viewsets.ModelViewSet): 24 | queryset = Actor.objects.all() 25 | serializer_class = ActorSerializer 26 | 27 | 28 | class CinemaHallViewSet(viewsets.ModelViewSet): 29 | queryset = CinemaHall.objects.all() 30 | serializer_class = CinemaHallSerializer 31 | 32 | 33 | class MovieViewSet(viewsets.ModelViewSet): 34 | queryset = Movie.objects.all() 35 | serializer_class = MovieSerializer 36 | 37 | def get_serializer_class(self): 38 | if self.action == "list": 39 | return MovieListSerializer 40 | 41 | if self.action == "retrieve": 42 | return MovieDetailSerializer 43 | 44 | return MovieSerializer 45 | 46 | 47 | class MovieSessionViewSet(viewsets.ModelViewSet): 48 | queryset = MovieSession.objects.all() 49 | serializer_class = MovieSessionSerializer 50 | 51 | def get_serializer_class(self): 52 | if self.action == "list": 53 | return MovieSessionListSerializer 54 | 55 | if self.action == "retrieve": 56 | return MovieSessionDetailSerializer 57 | 58 | return MovieSessionSerializer 59 | -------------------------------------------------------------------------------- /cinema/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-09 18:11 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ("cinema", "0001_initial"), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.AddField( 19 | model_name="order", 20 | name="user", 21 | field=models.ForeignKey( 22 | on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL 23 | ), 24 | ), 25 | migrations.AddField( 26 | model_name="moviesession", 27 | name="cinema_hall", 28 | field=models.ForeignKey( 29 | on_delete=django.db.models.deletion.CASCADE, to="cinema.cinemahall" 30 | ), 31 | ), 32 | migrations.AddField( 33 | model_name="moviesession", 34 | name="movie", 35 | field=models.ForeignKey( 36 | on_delete=django.db.models.deletion.CASCADE, to="cinema.movie" 37 | ), 38 | ), 39 | migrations.AddField( 40 | model_name="movie", 41 | name="actors", 42 | field=models.ManyToManyField(to="cinema.actor"), 43 | ), 44 | migrations.AddField( 45 | model_name="movie", 46 | name="genres", 47 | field=models.ManyToManyField(to="cinema.genre"), 48 | ), 49 | migrations.AlterUniqueTogether( 50 | name="ticket", 51 | unique_together={("movie_session", "row", "seat")}, 52 | ), 53 | ] 54 | -------------------------------------------------------------------------------- /cinema/tests/test_genre_api.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from rest_framework import status 4 | from rest_framework.test import APIClient 5 | 6 | from cinema.models import Genre 7 | 8 | 9 | class GenreApiTests(TestCase): 10 | def setUp(self): 11 | self.client = APIClient() 12 | Genre.objects.create( 13 | name="Comedy", 14 | ) 15 | Genre.objects.create( 16 | name="Drama", 17 | ) 18 | 19 | def test_get_genres(self): 20 | response = self.client.get("/api/cinema/genres/") 21 | genres = [genre["name"] for genre in response.data] 22 | self.assertEqual(response.status_code, status.HTTP_200_OK) 23 | self.assertEqual(sorted(genres), ["Comedy", "Drama"]) 24 | 25 | def test_post_genres(self): 26 | response = self.client.post( 27 | "/api/cinema/genres/", 28 | { 29 | "name": "Sci-fi", 30 | }, 31 | ) 32 | db_genres = Genre.objects.all() 33 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 34 | self.assertEqual(db_genres.count(), 3) 35 | self.assertEqual(db_genres.filter(name="Sci-fi").count(), 1) 36 | 37 | def test_get_invalid_genre(self): 38 | response = self.client.get("/api/cinema/genres/1001/") 39 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 40 | 41 | def test_put_genre(self): 42 | response = self.client.put( 43 | "/api/cinema/genres/1/", 44 | { 45 | "name": "Sci-fi", 46 | }, 47 | ) 48 | self.assertEqual(response.status_code, status.HTTP_200_OK) 49 | 50 | def test_delete_genre(self): 51 | response = self.client.delete( 52 | "/api/cinema/genres/1/", 53 | ) 54 | db_genres_id_1 = Genre.objects.filter(id=1) 55 | self.assertEqual(db_genres_id_1.count(), 0) 56 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 57 | 58 | def test_delete_invalid_genre(self): 59 | response = self.client.delete( 60 | "/api/cinema/genres/1000/", 61 | ) 62 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 63 | -------------------------------------------------------------------------------- /cinema/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession 4 | 5 | 6 | class GenreSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = Genre 9 | fields = ("id", "name") 10 | 11 | 12 | class ActorSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = Actor 15 | fields = ("id", "first_name", "last_name", "full_name") 16 | 17 | 18 | class CinemaHallSerializer(serializers.ModelSerializer): 19 | class Meta: 20 | model = CinemaHall 21 | fields = ("id", "name", "rows", "seats_in_row", "capacity") 22 | 23 | 24 | class MovieSerializer(serializers.ModelSerializer): 25 | class Meta: 26 | model = Movie 27 | fields = ("id", "title", "description", "duration", "genres", "actors") 28 | 29 | 30 | class MovieListSerializer(MovieSerializer): 31 | genres = serializers.SlugRelatedField( 32 | many=True, read_only=True, slug_field="name" 33 | ) 34 | actors = serializers.SlugRelatedField( 35 | many=True, read_only=True, slug_field="full_name" 36 | ) 37 | 38 | 39 | class MovieDetailSerializer(MovieSerializer): 40 | genres = GenreSerializer(many=True, read_only=True) 41 | actors = ActorSerializer(many=True, read_only=True) 42 | 43 | class Meta: 44 | model = Movie 45 | fields = ("id", "title", "description", "duration", "genres", "actors") 46 | 47 | 48 | class MovieSessionSerializer(serializers.ModelSerializer): 49 | class Meta: 50 | model = MovieSession 51 | fields = ("id", "show_time", "movie", "cinema_hall") 52 | 53 | 54 | class MovieSessionListSerializer(MovieSessionSerializer): 55 | movie_title = serializers.CharField(source="movie.title", read_only=True) 56 | cinema_hall_name = serializers.CharField( 57 | source="cinema_hall.name", read_only=True 58 | ) 59 | cinema_hall_capacity = serializers.IntegerField( 60 | source="cinema_hall.capacity", read_only=True 61 | ) 62 | 63 | class Meta: 64 | model = MovieSession 65 | fields = ( 66 | "id", 67 | "show_time", 68 | "movie_title", 69 | "cinema_hall_name", 70 | "cinema_hall_capacity", 71 | ) 72 | 73 | 74 | class MovieSessionDetailSerializer(MovieSessionSerializer): 75 | movie = MovieListSerializer(many=False, read_only=True) 76 | cinema_hall = CinemaHallSerializer(many=False, read_only=True) 77 | 78 | class Meta: 79 | model = MovieSession 80 | fields = ("id", "show_time", "movie", "cinema_hall") 81 | -------------------------------------------------------------------------------- /cinema/tests/test_actor_api.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from rest_framework import status 4 | from rest_framework.test import APIClient 5 | 6 | from cinema.models import Actor 7 | 8 | 9 | class ActorApiTests(TestCase): 10 | def setUp(self): 11 | self.client = APIClient() 12 | Actor.objects.create(first_name="George", last_name="Clooney") 13 | Actor.objects.create(first_name="Keanu", last_name="Reeves") 14 | 15 | def test_get_actors(self): 16 | response = self.client.get("/api/cinema/actors/") 17 | self.assertEqual(response.status_code, status.HTTP_200_OK) 18 | actors_full_names = [actor["full_name"] for actor in response.data] 19 | self.assertEqual( 20 | sorted(actors_full_names), ["George Clooney", "Keanu Reeves"] 21 | ) 22 | 23 | def test_post_actors(self): 24 | response = self.client.post( 25 | "/api/cinema/actors/", 26 | { 27 | "first_name": "Scarlett", 28 | "last_name": "Johansson", 29 | }, 30 | ) 31 | db_actors = Actor.objects.all() 32 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 33 | self.assertEqual(db_actors.count(), 3) 34 | self.assertEqual(db_actors.filter(first_name="Scarlett").count(), 1) 35 | 36 | def test_get_invalid_actor(self): 37 | response = self.client.get("/api/cinema/actors/1001/") 38 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 39 | 40 | def test_put_actor(self): 41 | response = self.client.put( 42 | "/api/cinema/actors/1/", 43 | { 44 | "first_name": "Scarlett", 45 | "last_name": "Johansson", 46 | }, 47 | ) 48 | actor_pk_1 = Actor.objects.get(pk=1) 49 | self.assertEqual(response.status_code, status.HTTP_200_OK) 50 | self.assertEqual( 51 | [ 52 | actor_pk_1.first_name, 53 | actor_pk_1.last_name, 54 | ], 55 | [ 56 | "Scarlett", 57 | "Johansson", 58 | ], 59 | ) 60 | 61 | def test_delete_actor(self): 62 | response = self.client.delete( 63 | "/api/cinema/actors/1/", 64 | ) 65 | db_actors_id_1 = Actor.objects.filter(id=1) 66 | self.assertEqual(db_actors_id_1.count(), 0) 67 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 68 | 69 | def test_delete_invalid_actor(self): 70 | response = self.client.delete( 71 | "/api/cinema/actors/1000/", 72 | ) 73 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 74 | -------------------------------------------------------------------------------- /cinema/tests/test_order_api.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.test import TestCase 4 | 5 | from rest_framework.test import APIClient 6 | from rest_framework import status 7 | 8 | from cinema.models import ( 9 | Movie, 10 | Genre, 11 | Actor, 12 | CinemaHall, 13 | MovieSession, 14 | Ticket, 15 | Order, 16 | ) 17 | from user.models import User 18 | 19 | 20 | class OrderApiTests(TestCase): 21 | def setUp(self): 22 | self.client = APIClient() 23 | self.drama = Genre.objects.create( 24 | name="Drama", 25 | ) 26 | self.comedy = Genre.objects.create( 27 | name="Comedy", 28 | ) 29 | self.actress = Actor.objects.create( 30 | first_name="Kate", last_name="Winslet" 31 | ) 32 | self.movie = Movie.objects.create( 33 | title="Titanic", 34 | description="Titanic description", 35 | duration=123, 36 | ) 37 | self.movie.genres.add(self.drama) 38 | self.movie.genres.add(self.comedy) 39 | self.movie.actors.add(self.actress) 40 | self.cinema_hall = CinemaHall.objects.create( 41 | name="White", 42 | rows=10, 43 | seats_in_row=14, 44 | ) 45 | self.movie_session = MovieSession.objects.create( 46 | movie=self.movie, 47 | cinema_hall=self.cinema_hall, 48 | show_time=datetime.now(), 49 | ) 50 | self.user = User.objects.create(username="admin") 51 | self.order = Order.objects.create(user=self.user) 52 | self.ticket = Ticket.objects.create( 53 | movie_session=self.movie_session, row=2, seat=12, order=self.order 54 | ) 55 | 56 | def test_get_order(self): 57 | self.client.force_authenticate(user=self.user) 58 | orders_response = self.client.get("/api/cinema/orders/") 59 | self.assertEqual(orders_response.status_code, status.HTTP_200_OK) 60 | self.assertEqual(orders_response.data["count"], 1) 61 | order = orders_response.data["results"][0] 62 | self.assertEqual(len(order["tickets"]), 1) 63 | ticket = order["tickets"][0] 64 | self.assertEqual(ticket["row"], 2) 65 | self.assertEqual(ticket["seat"], 12) 66 | movie_session = ticket["movie_session"] 67 | self.assertEqual(movie_session["movie_title"], "Titanic") 68 | self.assertEqual(movie_session["cinema_hall_name"], "White") 69 | self.assertEqual(movie_session["cinema_hall_capacity"], 140) 70 | 71 | def test_movie_session_detail_tickets(self): 72 | response = self.client.get( 73 | f"/api/cinema/movie_sessions/{self.movie_session.id}/" 74 | ) 75 | self.assertEqual(response.status_code, status.HTTP_200_OK) 76 | self.assertEqual( 77 | response.data["taken_places"][0]["row"], self.ticket.row 78 | ) 79 | self.assertEqual( 80 | response.data["taken_places"][0]["seat"], self.ticket.seat 81 | ) 82 | 83 | def test_movie_session_list_tickets_available(self): 84 | response = self.client.get(f"/api/cinema/movie_sessions/") 85 | self.assertEqual(response.status_code, status.HTTP_200_OK) 86 | self.assertEqual( 87 | response.data[0]["tickets_available"], 88 | self.cinema_hall.capacity - 1, 89 | ) 90 | -------------------------------------------------------------------------------- /cinema/models.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.db import models 3 | from django.conf import settings 4 | 5 | 6 | class CinemaHall(models.Model): 7 | name = models.CharField(max_length=255) 8 | rows = models.IntegerField() 9 | seats_in_row = models.IntegerField() 10 | 11 | @property 12 | def capacity(self) -> int: 13 | return self.rows * self.seats_in_row 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | 19 | class Genre(models.Model): 20 | name = models.CharField(max_length=255, unique=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class Actor(models.Model): 27 | first_name = models.CharField(max_length=255) 28 | last_name = models.CharField(max_length=255) 29 | 30 | def __str__(self): 31 | return self.first_name + " " + self.last_name 32 | 33 | @property 34 | def full_name(self): 35 | return f"{self.first_name} {self.last_name}" 36 | 37 | 38 | class Movie(models.Model): 39 | title = models.CharField(max_length=255) 40 | description = models.TextField() 41 | duration = models.IntegerField() 42 | genres = models.ManyToManyField(Genre) 43 | actors = models.ManyToManyField(Actor) 44 | 45 | class Meta: 46 | ordering = ["title"] 47 | 48 | def __str__(self): 49 | return self.title 50 | 51 | 52 | class MovieSession(models.Model): 53 | show_time = models.DateTimeField() 54 | movie = models.ForeignKey(Movie, on_delete=models.CASCADE) 55 | cinema_hall = models.ForeignKey(CinemaHall, on_delete=models.CASCADE) 56 | 57 | class Meta: 58 | ordering = ["-show_time"] 59 | 60 | def __str__(self): 61 | return self.movie.title + " " + str(self.show_time) 62 | 63 | 64 | class Order(models.Model): 65 | created_at = models.DateTimeField(auto_now_add=True) 66 | user = models.ForeignKey( 67 | settings.AUTH_USER_MODEL, on_delete=models.CASCADE 68 | ) 69 | 70 | def __str__(self): 71 | return str(self.created_at) 72 | 73 | class Meta: 74 | ordering = ["-created_at"] 75 | 76 | 77 | class Ticket(models.Model): 78 | movie_session = models.ForeignKey( 79 | MovieSession, on_delete=models.CASCADE, related_name="tickets" 80 | ) 81 | order = models.ForeignKey( 82 | Order, on_delete=models.CASCADE, related_name="tickets" 83 | ) 84 | row = models.IntegerField() 85 | seat = models.IntegerField() 86 | 87 | def clean(self): 88 | for ticket_attr_value, ticket_attr_name, cinema_hall_attr_name in [ 89 | (self.row, "row", "rows"), 90 | (self.seat, "seat", "seats_in_row"), 91 | ]: 92 | count_attrs = getattr( 93 | self.movie_session.cinema_hall, cinema_hall_attr_name 94 | ) 95 | if not (1 <= ticket_attr_value <= count_attrs): 96 | raise ValidationError( 97 | { 98 | ticket_attr_name: f"{ticket_attr_name} " 99 | f"number must be in available range: " 100 | f"(1, {cinema_hall_attr_name}): " 101 | f"(1, {count_attrs})" 102 | } 103 | ) 104 | 105 | def save( 106 | self, 107 | force_insert=False, 108 | force_update=False, 109 | using=None, 110 | update_fields=None, 111 | ): 112 | self.full_clean() 113 | super(Ticket, self).save( 114 | force_insert, force_update, using, update_fields 115 | ) 116 | 117 | def __str__(self): 118 | return ( 119 | f"{str(self.movie_session)} (row: {self.row}, seat: {self.seat})" 120 | ) 121 | 122 | class Meta: 123 | unique_together = ("movie_session", "row", "seat") 124 | -------------------------------------------------------------------------------- /cinema_service/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for cinema_service project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = ( 24 | "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" 25 | ) 26 | 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | 30 | ALLOWED_HOSTS = [] 31 | 32 | INTERNAL_IPS = [ 33 | "127.0.0.1", 34 | ] 35 | 36 | # Application definition 37 | 38 | INSTALLED_APPS = [ 39 | "django.contrib.admin", 40 | "django.contrib.auth", 41 | "django.contrib.contenttypes", 42 | "django.contrib.sessions", 43 | "django.contrib.messages", 44 | "django.contrib.staticfiles", 45 | "rest_framework", 46 | "debug_toolbar", 47 | "cinema", 48 | "user", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "debug_toolbar.middleware.DebugToolbarMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "cinema_service.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "cinema_service.wsgi.application" 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 85 | 86 | DATABASES = { 87 | "default": { 88 | "ENGINE": "django.db.backends.sqlite3", 89 | "NAME": BASE_DIR / "db.sqlite3", 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | "NAME": "django.contrib.auth.password_validation." 100 | "UserAttributeSimilarityValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation." 104 | "MinimumLengthValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation." 108 | "CommonPasswordValidator", 109 | }, 110 | { 111 | "NAME": "django.contrib.auth.password_validation." 112 | "NumericPasswordValidator", 113 | }, 114 | ] 115 | 116 | AUTH_USER_MODEL = "user.User" 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_TZ = False 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 132 | 133 | STATIC_URL = "static/" 134 | 135 | # Default primary key field type 136 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 137 | 138 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tickets and Orders API 2 | 3 | Read [the guideline](https://github.com/mate-academy/py-task-guideline/blob/main/README.md) before starting. 4 | - Use the following command to load prepared data from fixture to test and debug your code: 5 | 6 | `python manage.py loaddata cinema_service_db_data.json` 7 | 8 | - After loading data from fixture you can use following superuser (or create another one by yourself): 9 | - Login: `admin.user` 10 | - Password: `1qazcde3` 11 | 12 | `In this task you will add the functionality of working with orders. 13 | 14 | 1. Create serializers and views to support the following endpoints: 15 | 16 | * `GET api/cinema/orders/` - should return a list of the all orders that filtered by the authenticated user. 17 | Add detail information about movie session and implement pagination. 18 | 19 | Example: 20 | ``` 21 | GET /api/cinema/orders/?page=2 22 | ``` 23 | 24 | ``` 25 | HTTP 200 OK 26 | Allow: GET, POST, HEAD, OPTIONS 27 | Content-Type: application/json 28 | Vary: Accept 29 | 30 | { 31 | "count": 3, 32 | "next": "http://127.0.0.1:8000/api/cinema/orders/?page=3", 33 | "previous": "http://127.0.0.1:8000/api/cinema/orders/", 34 | "results": [ 35 | { 36 | "id": 2, 37 | "tickets": [ 38 | { 39 | "id": 2, 40 | "row": 2, 41 | "seat": 3, 42 | "movie_session": { 43 | "id": 1, 44 | "show_time": "2022-12-12T12:32:00Z", 45 | "movie_title": "Movie", 46 | "cinema_hall_name": "Green", 47 | "cinema_hall_capacity": 2829 48 | } 49 | } 50 | ], 51 | "created_at": "2022-05-16T13:45:30.911367Z" 52 | } 53 | ] 54 | } 55 | ``` 56 | 57 | * `POST api/cinema/orders/` - should create a new order for the authenticated user. 58 | It should support the following request structure: 59 | ```json 60 | { 61 | "tickets": [ 62 | { 63 | "row": 2, 64 | "seat": 1, 65 | "movie_session": 1 66 | }, 67 | { 68 | "row": 2, 69 | "seat": 2, 70 | "movie_session": 1 71 | } 72 | ] 73 | } 74 | ``` 75 | 76 | 2. Provide filtering for movies by genres, actors and title. Use `?actors=`, `?genres=` and `?title=` parameters. 77 | Filtering by title with the `string` parameter should return all movies whose title contains `string`. 78 | 79 | 3. Implement filtering for movie sessions by date and movie. The date should be provided in `year-month-day` format, 80 | the movie by its id. 81 | Example: 82 | ``` 83 | GET /api/cinema/movie_sessions/?date=2022-12-12&movie=1 84 | ``` 85 | ``` 86 | HTTP 200 OK 87 | Allow: GET, POST, HEAD, OPTIONS 88 | Content-Type: application/json 89 | Vary: Accept 90 | 91 | [ 92 | { 93 | "id": 1, 94 | "show_time": "2022-12-12T12:32:00Z", 95 | "movie_title": "Movie", 96 | "cinema_hall_name": "Green", 97 | "cinema_hall_capacity": 2829 98 | } 99 | ] 100 | ``` 101 | 102 | 103 | 4. Return taken places for movie session details endpoint 104 | ``` 105 | GET /api/cinema/movie_sessions/1/ 106 | ``` 107 | ``` 108 | HTTP 200 OK 109 | Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS 110 | Content-Type: application/json 111 | Vary: Accept 112 | 113 | { 114 | "id": 1, 115 | "show_time": "2022-12-12T12:32:00Z", 116 | "movie": { 117 | "id": 1, 118 | "title": "Movie", 119 | "description": "description", 120 | "duration": 123, 121 | "genres": [ 122 | "drama" 123 | ], 124 | "actors": [ 125 | "F F" 126 | ] 127 | }, 128 | "cinema_hall": { 129 | "id": 1, 130 | "name": "Green", 131 | "rows": 123, 132 | "seats_in_row": 23, 133 | "capacity": 2829 134 | }, 135 | "taken_places": [ 136 | { 137 | "row": 2, 138 | "seat": 1 139 | }, 140 | { 141 | "row": 2, 142 | "seat": 3 143 | }, 144 | { 145 | "row": 2, 146 | "seat": 10 147 | } 148 | ] 149 | } 150 | 151 | ``` 152 | 5. Add `tickets_available` field to movie sessions list endpoint, 153 | which says about how many `tickets` are still available for each `movie_session` 154 | 155 | 156 | Optional tasks: 157 | - Provide validation for creating tickets on serializer level 158 | 159 | ### Note: Check your code using this [checklist](checklist.md) before pushing your solution. 160 | -------------------------------------------------------------------------------- /cinema/tests/test_cinema_hall_api.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from rest_framework.test import APIClient 4 | from rest_framework import status 5 | 6 | from cinema.models import CinemaHall 7 | 8 | 9 | class CinemaHallApiTests(TestCase): 10 | def setUp(self): 11 | self.client = APIClient() 12 | CinemaHall.objects.create( 13 | name="Blue", 14 | rows=15, 15 | seats_in_row=20, 16 | ) 17 | CinemaHall.objects.create( 18 | name="VIP", 19 | rows=6, 20 | seats_in_row=8, 21 | ) 22 | 23 | def test_get_cinema_halls(self): 24 | response = self.client.get("/api/cinema/cinema_halls/") 25 | blue_hall = { 26 | "name": "Blue", 27 | "rows": 15, 28 | "seats_in_row": 20, 29 | "capacity": 300, 30 | } 31 | self.assertEqual(response.status_code, status.HTTP_200_OK) 32 | self.assertEqual(response.data[0]["name"], blue_hall["name"]) 33 | self.assertEqual(response.data[0]["rows"], blue_hall["rows"]) 34 | self.assertEqual( 35 | response.data[0]["seats_in_row"], blue_hall["seats_in_row"] 36 | ) 37 | vip_hall = { 38 | "name": "VIP", 39 | "rows": 6, 40 | "seats_in_row": 8, 41 | "capacity": 48, 42 | } 43 | self.assertEqual(response.status_code, status.HTTP_200_OK) 44 | self.assertEqual(response.data[1]["name"], vip_hall["name"]) 45 | self.assertEqual(response.data[1]["rows"], vip_hall["rows"]) 46 | self.assertEqual( 47 | response.data[1]["seats_in_row"], vip_hall["seats_in_row"] 48 | ) 49 | 50 | def test_post_cinema_halls(self): 51 | response = self.client.post( 52 | "/api/cinema/cinema_halls/", 53 | { 54 | "name": "Yellow", 55 | "rows": 14, 56 | "seats_in_row": 15, 57 | }, 58 | ) 59 | db_cinema_halls = CinemaHall.objects.all() 60 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 61 | self.assertEqual(db_cinema_halls.count(), 3) 62 | self.assertEqual(db_cinema_halls.filter(name="Yellow").count(), 1) 63 | 64 | def test_get_cinema_hall(self): 65 | response = self.client.get("/api/cinema/cinema_halls/2/") 66 | vip_hall = { 67 | "name": "VIP", 68 | "rows": 6, 69 | "seats_in_row": 8, 70 | "capacity": 48, 71 | } 72 | self.assertEqual(response.status_code, status.HTTP_200_OK) 73 | self.assertEqual(response.data["name"], vip_hall["name"]) 74 | self.assertEqual(response.data["rows"], vip_hall["rows"]) 75 | self.assertEqual( 76 | response.data["seats_in_row"], vip_hall["seats_in_row"] 77 | ) 78 | self.assertEqual(response.data["capacity"], vip_hall["capacity"]) 79 | 80 | def test_get_invalid_cinema_hall(self): 81 | response = self.client.get("/api/cinema/cinema_halls/1001/") 82 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 83 | 84 | def test_put_cinema_hall(self): 85 | response = self.client.put( 86 | "/api/cinema/cinema_halls/1/", 87 | { 88 | "name": "Yellow", 89 | "rows": 14, 90 | "seats_in_row": 15, 91 | }, 92 | ) 93 | cinema_hall_pk_1 = CinemaHall.objects.get(pk=1) 94 | self.assertEqual(response.status_code, status.HTTP_200_OK) 95 | self.assertEqual( 96 | [ 97 | cinema_hall_pk_1.name, 98 | cinema_hall_pk_1.rows, 99 | cinema_hall_pk_1.seats_in_row, 100 | ], 101 | [ 102 | "Yellow", 103 | 14, 104 | 15, 105 | ], 106 | ) 107 | 108 | def test_patch_cinema_hall(self): 109 | response = self.client.patch( 110 | "/api/cinema/cinema_halls/1/", 111 | { 112 | "name": "Green", 113 | }, 114 | ) 115 | self.assertEqual(response.status_code, status.HTTP_200_OK) 116 | self.assertEqual(CinemaHall.objects.get(id=1).name, "Green") 117 | 118 | def test_delete_cinema_hall(self): 119 | response = self.client.delete( 120 | "/api/cinema/cinema_halls/1/", 121 | ) 122 | db_cinema_halls_id_1 = CinemaHall.objects.filter(id=1) 123 | self.assertEqual(db_cinema_halls_id_1.count(), 0) 124 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 125 | 126 | def test_delete_invalid_cinema_hall(self): 127 | response = self.client.delete( 128 | "/api/cinema/cinema_halls/1000/", 129 | ) 130 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 131 | -------------------------------------------------------------------------------- /cinema/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-09 18:11 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Actor", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("first_name", models.CharField(max_length=255)), 27 | ("last_name", models.CharField(max_length=255)), 28 | ], 29 | ), 30 | migrations.CreateModel( 31 | name="CinemaHall", 32 | fields=[ 33 | ( 34 | "id", 35 | models.BigAutoField( 36 | auto_created=True, 37 | primary_key=True, 38 | serialize=False, 39 | verbose_name="ID", 40 | ), 41 | ), 42 | ("name", models.CharField(max_length=255)), 43 | ("rows", models.IntegerField()), 44 | ("seats_in_row", models.IntegerField()), 45 | ], 46 | ), 47 | migrations.CreateModel( 48 | name="Genre", 49 | fields=[ 50 | ( 51 | "id", 52 | models.BigAutoField( 53 | auto_created=True, 54 | primary_key=True, 55 | serialize=False, 56 | verbose_name="ID", 57 | ), 58 | ), 59 | ("name", models.CharField(max_length=255)), 60 | ], 61 | ), 62 | migrations.CreateModel( 63 | name="Movie", 64 | fields=[ 65 | ( 66 | "id", 67 | models.BigAutoField( 68 | auto_created=True, 69 | primary_key=True, 70 | serialize=False, 71 | verbose_name="ID", 72 | ), 73 | ), 74 | ("title", models.CharField(max_length=255)), 75 | ("description", models.TextField()), 76 | ], 77 | options={ 78 | "ordering": ["title"], 79 | }, 80 | ), 81 | migrations.CreateModel( 82 | name="MovieSession", 83 | fields=[ 84 | ( 85 | "id", 86 | models.BigAutoField( 87 | auto_created=True, 88 | primary_key=True, 89 | serialize=False, 90 | verbose_name="ID", 91 | ), 92 | ), 93 | ("show_time", models.DateTimeField()), 94 | ], 95 | options={ 96 | "ordering": ["-show_time"], 97 | }, 98 | ), 99 | migrations.CreateModel( 100 | name="Order", 101 | fields=[ 102 | ( 103 | "id", 104 | models.BigAutoField( 105 | auto_created=True, 106 | primary_key=True, 107 | serialize=False, 108 | verbose_name="ID", 109 | ), 110 | ), 111 | ("created_at", models.DateTimeField(auto_now_add=True)), 112 | ], 113 | options={ 114 | "ordering": ["-created_at"], 115 | }, 116 | ), 117 | migrations.CreateModel( 118 | name="Ticket", 119 | fields=[ 120 | ( 121 | "id", 122 | models.BigAutoField( 123 | auto_created=True, 124 | primary_key=True, 125 | serialize=False, 126 | verbose_name="ID", 127 | ), 128 | ), 129 | ("row", models.IntegerField()), 130 | ("seat", models.IntegerField()), 131 | ( 132 | "movie_session", 133 | models.ForeignKey( 134 | on_delete=django.db.models.deletion.CASCADE, 135 | related_name="tickets", 136 | to="cinema.moviesession", 137 | ), 138 | ), 139 | ( 140 | "order", 141 | models.ForeignKey( 142 | on_delete=django.db.models.deletion.CASCADE, 143 | related_name="tickets", 144 | to="cinema.order", 145 | ), 146 | ), 147 | ], 148 | ), 149 | ] 150 | -------------------------------------------------------------------------------- /user/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-05-10 11:54 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ("auth", "0012_alter_user_first_name_max_length"), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name="User", 20 | fields=[ 21 | ( 22 | "id", 23 | models.BigAutoField( 24 | auto_created=True, 25 | primary_key=True, 26 | serialize=False, 27 | verbose_name="ID", 28 | ), 29 | ), 30 | ("password", models.CharField(max_length=128, verbose_name="password")), 31 | ( 32 | "last_login", 33 | models.DateTimeField( 34 | blank=True, null=True, verbose_name="last login" 35 | ), 36 | ), 37 | ( 38 | "is_superuser", 39 | models.BooleanField( 40 | default=False, 41 | help_text="Designates that this user has all permissions without explicitly assigning them.", 42 | verbose_name="superuser status", 43 | ), 44 | ), 45 | ( 46 | "username", 47 | models.CharField( 48 | error_messages={ 49 | "unique": "A user with that username already exists." 50 | }, 51 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 52 | max_length=150, 53 | unique=True, 54 | validators=[ 55 | django.contrib.auth.validators.UnicodeUsernameValidator() 56 | ], 57 | verbose_name="username", 58 | ), 59 | ), 60 | ( 61 | "first_name", 62 | models.CharField( 63 | blank=True, max_length=150, verbose_name="first name" 64 | ), 65 | ), 66 | ( 67 | "last_name", 68 | models.CharField( 69 | blank=True, max_length=150, verbose_name="last name" 70 | ), 71 | ), 72 | ( 73 | "email", 74 | models.EmailField( 75 | blank=True, max_length=254, verbose_name="email address" 76 | ), 77 | ), 78 | ( 79 | "is_staff", 80 | models.BooleanField( 81 | default=False, 82 | help_text="Designates whether the user can log into this admin site.", 83 | verbose_name="staff status", 84 | ), 85 | ), 86 | ( 87 | "is_active", 88 | models.BooleanField( 89 | default=True, 90 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 91 | verbose_name="active", 92 | ), 93 | ), 94 | ( 95 | "date_joined", 96 | models.DateTimeField( 97 | default=django.utils.timezone.now, verbose_name="date joined" 98 | ), 99 | ), 100 | ( 101 | "groups", 102 | models.ManyToManyField( 103 | blank=True, 104 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 105 | related_name="user_set", 106 | related_query_name="user", 107 | to="auth.group", 108 | verbose_name="groups", 109 | ), 110 | ), 111 | ( 112 | "user_permissions", 113 | models.ManyToManyField( 114 | blank=True, 115 | help_text="Specific permissions for this user.", 116 | related_name="user_set", 117 | related_query_name="user", 118 | to="auth.permission", 119 | verbose_name="user permissions", 120 | ), 121 | ), 122 | ], 123 | options={ 124 | "verbose_name": "user", 125 | "verbose_name_plural": "users", 126 | "abstract": False, 127 | }, 128 | managers=[ 129 | ("objects", django.contrib.auth.models.UserManager()), 130 | ], 131 | ), 132 | ] 133 | -------------------------------------------------------------------------------- /cinema/tests/test_movie_session_api.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.test import TestCase 4 | 5 | from rest_framework.test import APIClient 6 | from rest_framework import status 7 | 8 | from cinema.models import Movie, Genre, Actor, MovieSession, CinemaHall, Ticket 9 | 10 | 11 | class MovieSessionApiTests(TestCase): 12 | def setUp(self): 13 | self.client = APIClient() 14 | drama = Genre.objects.create( 15 | name="Drama", 16 | ) 17 | comedy = Genre.objects.create( 18 | name="Comedy", 19 | ) 20 | actress = Actor.objects.create(first_name="Kate", last_name="Winslet") 21 | self.movie = Movie.objects.create( 22 | title="Titanic", 23 | description="Titanic description", 24 | duration=123, 25 | ) 26 | self.movie.genres.add(drama) 27 | self.movie.genres.add(comedy) 28 | self.movie.actors.add(actress) 29 | self.cinema_hall = CinemaHall.objects.create( 30 | name="White", 31 | rows=10, 32 | seats_in_row=14, 33 | ) 34 | self.movie_session = MovieSession.objects.create( 35 | movie=self.movie, 36 | cinema_hall=self.cinema_hall, 37 | show_time=datetime.datetime( 38 | year=2022, 39 | month=9, 40 | day=2, 41 | hour=9 42 | ), 43 | ) 44 | 45 | def test_get_movie_sessions(self): 46 | movie_sessions = self.client.get("/api/cinema/movie_sessions/") 47 | movie_session = { 48 | "movie_title": "Titanic", 49 | "cinema_hall_name": "White", 50 | "cinema_hall_capacity": 140, 51 | } 52 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 53 | for field in movie_session: 54 | self.assertEqual( 55 | movie_sessions.data[0][field], movie_session[field] 56 | ) 57 | 58 | def test_get_movie_sessions_filtered_by_date(self): 59 | movie_sessions = self.client.get( 60 | "/api/cinema/movie_sessions/?date=2022-09-02" 61 | ) 62 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 63 | self.assertEqual(len(movie_sessions.data), 1) 64 | 65 | movie_sessions = self.client.get( 66 | "/api/cinema/movie_sessions/?date=2022-09-01" 67 | ) 68 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 69 | self.assertEqual(len(movie_sessions.data), 0) 70 | 71 | def test_get_movie_sessions_filtered_by_movie(self): 72 | movie_sessions = self.client.get( 73 | f"/api/cinema/movie_sessions/?movie={self.movie.id}" 74 | ) 75 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 76 | self.assertEqual(len(movie_sessions.data), 1) 77 | 78 | movie_sessions = self.client.get( 79 | "/api/cinema/movie_sessions/?movie=1234" 80 | ) 81 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 82 | self.assertEqual(len(movie_sessions.data), 0) 83 | 84 | def test_get_movie_sessions_filtered_by_movie_and_data(self): 85 | movie_sessions = self.client.get( 86 | f"/api/cinema/movie_sessions/?movie={self.movie.id}&date=2022-09-2" 87 | ) 88 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 89 | self.assertEqual(len(movie_sessions.data), 1) 90 | 91 | movie_sessions = self.client.get( 92 | "/api/cinema/movie_sessions/?movie=1234&date=2022-09-2" 93 | ) 94 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 95 | self.assertEqual(len(movie_sessions.data), 0) 96 | 97 | movie_sessions = self.client.get( 98 | f"/api/cinema/movie_sessions/?movie={self.movie.id}&date=2022-09-3" 99 | ) 100 | self.assertEqual(movie_sessions.status_code, status.HTTP_200_OK) 101 | self.assertEqual(len(movie_sessions.data), 0) 102 | 103 | def test_post_movie_session(self): 104 | movies = self.client.post( 105 | "/api/cinema/movie_sessions/", 106 | { 107 | "movie": 1, 108 | "cinema_hall": 1, 109 | "show_time": datetime.datetime.now(), 110 | }, 111 | ) 112 | movie_sessions = MovieSession.objects.all() 113 | self.assertEqual(movies.status_code, status.HTTP_201_CREATED) 114 | self.assertEqual(movie_sessions.count(), 2) 115 | 116 | def test_get_movie_session(self): 117 | response = self.client.get("/api/cinema/movie_sessions/1/") 118 | self.assertEqual(response.status_code, status.HTTP_200_OK) 119 | self.assertEqual(response.data["movie"]["title"], "Titanic") 120 | self.assertEqual( 121 | response.data["movie"]["description"], "Titanic description" 122 | ) 123 | self.assertEqual(response.data["movie"]["duration"], 123) 124 | self.assertEqual(response.data["movie"]["genres"], ["Drama", "Comedy"]) 125 | self.assertEqual(response.data["movie"]["actors"], ["Kate Winslet"]) 126 | self.assertEqual(response.data["cinema_hall"]["capacity"], 140) 127 | self.assertEqual(response.data["cinema_hall"]["rows"], 10) 128 | self.assertEqual(response.data["cinema_hall"]["seats_in_row"], 14) 129 | self.assertEqual(response.data["cinema_hall"]["name"], "White") 130 | -------------------------------------------------------------------------------- /cinema/tests/test_movie_api.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from rest_framework.test import APIClient 4 | from rest_framework import status 5 | 6 | from cinema.models import Movie, Genre, Actor 7 | 8 | 9 | class MovieApiTests(TestCase): 10 | def setUp(self): 11 | self.client = APIClient() 12 | self.drama = Genre.objects.create( 13 | name="Drama", 14 | ) 15 | self.comedy = Genre.objects.create( 16 | name="Comedy", 17 | ) 18 | self.actress = Actor.objects.create( 19 | first_name="Kate", last_name="Winslet" 20 | ) 21 | self.movie = Movie.objects.create( 22 | title="Titanic", 23 | description="Titanic description", 24 | duration=123, 25 | ) 26 | self.movie.genres.add(self.drama) 27 | self.movie.genres.add(self.comedy) 28 | self.movie.actors.add(self.actress) 29 | 30 | def test_get_movies(self): 31 | movies = self.client.get("/api/cinema/movies/") 32 | titanic = { 33 | "title": "Titanic", 34 | "description": "Titanic description", 35 | "duration": 123, 36 | "genres": ["Drama", "Comedy"], 37 | "actors": ["Kate Winslet"], 38 | } 39 | print(movies.data) 40 | self.assertEqual(movies.status_code, status.HTTP_200_OK) 41 | for field in titanic: 42 | self.assertEqual(movies.data[0][field], titanic[field]) 43 | 44 | def test_get_movies_with_genres_filtering(self): 45 | movies = self.client.get( 46 | f"/api/cinema/movies/?genres={self.comedy.id}" 47 | ) 48 | self.assertEqual(len(movies.data), 1) 49 | movies = self.client.get( 50 | f"/api/cinema/movies/?genres={self.comedy.id},2,3" 51 | ) 52 | self.assertEqual(len(movies.data), 1) 53 | movies = self.client.get("/api/cinema/movies/?genres=123213") 54 | self.assertEqual(len(movies.data), 0) 55 | 56 | def test_get_movies_with_actors_filtering(self): 57 | movies = self.client.get( 58 | f"/api/cinema/movies/?actors={self.actress.id}" 59 | ) 60 | self.assertEqual(len(movies.data), 1) 61 | movies = self.client.get(f"/api/cinema/movies/?actors={123}") 62 | self.assertEqual(len(movies.data), 0) 63 | 64 | def test_get_movies_with_title_filtering(self): 65 | movies = self.client.get(f"/api/cinema/movies/?title=ita") 66 | self.assertEqual(len(movies.data), 1) 67 | movies = self.client.get(f"/api/cinema/movies/?title=ati") 68 | self.assertEqual(len(movies.data), 0) 69 | 70 | def test_post_movies(self): 71 | movies = self.client.post( 72 | "/api/cinema/movies/", 73 | { 74 | "title": "Superman", 75 | "description": "Superman description", 76 | "duration": 123, 77 | "actors": [1], 78 | "genres": [1, 2], 79 | }, 80 | ) 81 | db_movies = Movie.objects.all() 82 | self.assertEqual(movies.status_code, status.HTTP_201_CREATED) 83 | self.assertEqual(db_movies.count(), 2) 84 | self.assertEqual(db_movies.filter(title="Superman").count(), 1) 85 | 86 | def test_post_invalid_movies(self): 87 | movies = self.client.post( 88 | "/api/cinema/movies/", 89 | { 90 | "title": "Superman", 91 | "description": "Superman description", 92 | "duration": 123, 93 | "actors": [ 94 | { 95 | "id": 3, 96 | } 97 | ], 98 | }, 99 | ) 100 | superman_movies = Movie.objects.filter(title="Superman") 101 | self.assertEqual(movies.status_code, status.HTTP_400_BAD_REQUEST) 102 | self.assertEqual(superman_movies.count(), 0) 103 | 104 | def test_get_movie(self): 105 | response = self.client.get("/api/cinema/movies/1/") 106 | self.assertEqual(response.status_code, status.HTTP_200_OK) 107 | self.assertEqual(response.data["title"], "Titanic") 108 | self.assertEqual(response.data["description"], "Titanic description") 109 | self.assertEqual(response.data["duration"], 123) 110 | self.assertEqual(response.data["genres"][0]["name"], "Drama") 111 | self.assertEqual(response.data["genres"][1]["name"], "Comedy") 112 | self.assertEqual(response.data["actors"][0]["first_name"], "Kate") 113 | self.assertEqual(response.data["actors"][0]["last_name"], "Winslet") 114 | self.assertEqual( 115 | response.data["actors"][0]["full_name"], "Kate Winslet" 116 | ) 117 | 118 | def test_get_invalid_movie(self): 119 | response = self.client.get("/api/cinema/movies/100/") 120 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 121 | 122 | def test_put_movie(self): 123 | self.client.put( 124 | "/api/cinema/movies/1/", 125 | { 126 | "title": "Watchman", 127 | "description": "Watchman description", 128 | "duration": 321, 129 | "genres": [1, 2], 130 | "actors": [1], 131 | }, 132 | ) 133 | db_movie = Movie.objects.get(id=1) 134 | self.assertEqual( 135 | [db_movie.title, db_movie.description], 136 | [ 137 | "Watchman", 138 | "Watchman description", 139 | ], 140 | ) 141 | self.assertEqual(db_movie.title, "Watchman") 142 | 143 | def test_delete_movie(self): 144 | response = self.client.delete( 145 | "/api/cinema/movies/1/", 146 | ) 147 | db_movies_id_1 = Movie.objects.filter(id=1) 148 | self.assertEqual(db_movies_id_1.count(), 0) 149 | self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) 150 | 151 | def test_delete_invalid_movie(self): 152 | response = self.client.delete( 153 | "/api/cinema/movies/1000/", 154 | ) 155 | self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 156 | -------------------------------------------------------------------------------- /cinema_service_db_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "cinema.cinemahall", 4 | "pk": 1, 5 | "fields": { 6 | "name": "Ricciotto Canudo", 7 | "rows": 25, 8 | "seats_in_row": 30 9 | } 10 | }, 11 | { 12 | "model": "cinema.cinemahall", 13 | "pk": 2, 14 | "fields": { 15 | "name": "Robin Wood", 16 | "rows": 24, 17 | "seats_in_row": 18 18 | } 19 | }, 20 | { 21 | "model": "cinema.cinemahall", 22 | "pk": 3, 23 | "fields": { 24 | "name": "Peter Wollen", 25 | "rows": 30, 26 | "seats_in_row": 20 27 | } 28 | }, 29 | { 30 | "model": "cinema.cinemahall", 31 | "pk": 4, 32 | "fields": { 33 | "name": "Jean Mitry", 34 | "rows": 14, 35 | "seats_in_row": 8 36 | } 37 | }, 38 | { 39 | "model": "cinema.genre", 40 | "pk": 1, 41 | "fields": { 42 | "name": "Crime" 43 | } 44 | }, 45 | { 46 | "model": "cinema.genre", 47 | "pk": 2, 48 | "fields": { 49 | "name": "Drama" 50 | } 51 | }, 52 | { 53 | "model": "cinema.genre", 54 | "pk": 3, 55 | "fields": { 56 | "name": "Thriller" 57 | } 58 | }, 59 | { 60 | "model": "cinema.genre", 61 | "pk": 4, 62 | "fields": { 63 | "name": "Action" 64 | } 65 | }, 66 | { 67 | "model": "cinema.genre", 68 | "pk": 5, 69 | "fields": { 70 | "name": "Adventure" 71 | } 72 | }, 73 | { 74 | "model": "cinema.genre", 75 | "pk": 6, 76 | "fields": { 77 | "name": "Sci-Fi" 78 | } 79 | }, 80 | { 81 | "model": "cinema.genre", 82 | "pk": 7, 83 | "fields": { 84 | "name": "Mystery" 85 | } 86 | }, 87 | { 88 | "model": "cinema.actor", 89 | "pk": 1, 90 | "fields": { 91 | "first_name": "Jack", 92 | "last_name": "Nicholson" 93 | } 94 | }, 95 | { 96 | "model": "cinema.actor", 97 | "pk": 2, 98 | "fields": { 99 | "first_name": "Leonardo", 100 | "last_name": "DiCaprio" 101 | } 102 | }, 103 | { 104 | "model": "cinema.actor", 105 | "pk": 3, 106 | "fields": { 107 | "first_name": "Matt", 108 | "last_name": "Damon" 109 | } 110 | }, 111 | { 112 | "model": "cinema.actor", 113 | "pk": 4, 114 | "fields": { 115 | "first_name": "Joseph", 116 | "last_name": "Gordon-Levitt" 117 | } 118 | }, 119 | { 120 | "model": "cinema.actor", 121 | "pk": 5, 122 | "fields": { 123 | "first_name": "Elliot", 124 | "last_name": "Page" 125 | } 126 | }, 127 | { 128 | "model": "cinema.actor", 129 | "pk": 6, 130 | "fields": { 131 | "first_name": "Bruce", 132 | "last_name": "Willis" 133 | } 134 | }, 135 | { 136 | "model": "cinema.actor", 137 | "pk": 7, 138 | "fields": { 139 | "first_name": "Emily", 140 | "last_name": "Blunt" 141 | } 142 | }, 143 | { 144 | "model": "cinema.actor", 145 | "pk": 8, 146 | "fields": { 147 | "first_name": "Samuel", 148 | "last_name": "Jackson" 149 | } 150 | }, 151 | { 152 | "model": "cinema.actor", 153 | "pk": 9, 154 | "fields": { 155 | "first_name": "Robin", 156 | "last_name": "Wright" 157 | } 158 | }, 159 | { 160 | "model": "cinema.movie", 161 | "pk": 1, 162 | "fields": { 163 | "title": "The Departed", 164 | "description": "An undercover cop and a mole in the police attempt to identify each other while infiltrating an Irishgang in South Boston.", 165 | "duration": 151, 166 | "genres": [ 167 | 1, 168 | 2, 169 | 3 170 | ], 171 | "actors": [ 172 | 1, 173 | 2, 174 | 3 175 | ] 176 | } 177 | }, 178 | { 179 | "model": "cinema.movie", 180 | "pk": 2, 181 | "fields": { 182 | "title": "Inception", 183 | "description": "A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.", 184 | "duration": 148, 185 | "genres": [ 186 | 4, 187 | 5, 188 | 6 189 | ], 190 | "actors": [ 191 | 2, 192 | 4, 193 | 5 194 | ] 195 | } 196 | }, 197 | { 198 | "model": "cinema.movie", 199 | "pk": 3, 200 | "fields": { 201 | "title": "Looper", 202 | "description": "In 2074, when the mob wants to get rid of someone, the target is sent into the past, where a hired gun awaits - someone like Joe - who one day learns the mob wants to 'close the loop' by sending back Joe's future self for assassination.", 203 | "duration": 119, 204 | "genres": [ 205 | 2, 206 | 4, 207 | 6 208 | ], 209 | "actors": [ 210 | 4, 211 | 6, 212 | 7 213 | ] 214 | } 215 | }, 216 | { 217 | "model": "cinema.movie", 218 | "pk": 4, 219 | "fields": { 220 | "title": "Unbreakable", 221 | "description": "A man learns something extraordinary about himself after a devastating accident.", 222 | "duration": 106, 223 | "genres": [ 224 | 2, 225 | 6, 226 | 7 227 | ], 228 | "actors": [ 229 | 6, 230 | 8, 231 | 9 232 | ] 233 | } 234 | }, 235 | { 236 | "model": "cinema.moviesession", 237 | "pk": 1, 238 | "fields": { 239 | "show_time": "2024-10-08T13:00:00Z", 240 | "movie": 1, 241 | "cinema_hall": 1 242 | } 243 | }, 244 | { 245 | "model": "cinema.moviesession", 246 | "pk": 2, 247 | "fields": { 248 | "show_time": "2024-10-09T13:00:00Z", 249 | "movie": 2, 250 | "cinema_hall": 2 251 | } 252 | }, 253 | { 254 | "model": "cinema.moviesession", 255 | "pk": 3, 256 | "fields": { 257 | "show_time": "2024-10-10T13:00:00Z", 258 | "movie": 3, 259 | "cinema_hall": 3 260 | } 261 | }, 262 | { 263 | "model": "cinema.moviesession", 264 | "pk": 4, 265 | "fields": { 266 | "show_time": "2024-10-11T13:00:00Z", 267 | "movie": 4, 268 | "cinema_hall": 4 269 | } 270 | }, 271 | { 272 | "model": "cinema.moviesession", 273 | "pk": 5, 274 | "fields": { 275 | "show_time": "2024-10-12T13:00:00Z", 276 | "movie": 4, 277 | "cinema_hall": 1 278 | } 279 | }, 280 | { 281 | "model": "cinema.moviesession", 282 | "pk": 6, 283 | "fields": { 284 | "show_time": "2024-10-13T13:00:00Z", 285 | "movie": 3, 286 | "cinema_hall": 2 287 | } 288 | }, 289 | { 290 | "model": "cinema.moviesession", 291 | "pk": 7, 292 | "fields": { 293 | "show_time":"2024-10-14T13:00:00Z", 294 | "movie": 1, 295 | "cinema_hall": 3 296 | } 297 | }, 298 | { 299 | "model": "cinema.moviesession", 300 | "pk": 8, 301 | "fields": { 302 | "show_time": "2024-10-15T13:00:00Z", 303 | "movie": 2, 304 | "cinema_hall": 4 305 | } 306 | }, 307 | { 308 | "model": "cinema.order", 309 | "pk": 1, 310 | "fields": { 311 | "created_at": "2022-08-09T09:06:18.876Z", 312 | "user": 1 313 | } 314 | }, 315 | { 316 | "model": "cinema.order", 317 | "pk": 2, 318 | "fields": { 319 | "created_at": "2022-08-09T09:06:18.876Z", 320 | "user": 2 321 | } 322 | }, 323 | { 324 | "model": "cinema.order", 325 | "pk": 3, 326 | "fields": { 327 | "created_at": "2022-08-09T09:06:18.876Z", 328 | "user": 1 329 | } 330 | }, 331 | { 332 | "model": "cinema.order", 333 | "pk": 4, 334 | "fields": { 335 | "created_at": "2022-08-09T09:06:18.876Z", 336 | "user": 3 337 | } 338 | }, 339 | { 340 | "model": "cinema.ticket", 341 | "pk": 1, 342 | "fields": { 343 | "movie_session": 1, 344 | "order": 1, 345 | "row": 1, 346 | "seat": 1 347 | } 348 | }, 349 | { 350 | "model": "cinema.ticket", 351 | "pk": 2, 352 | "fields": { 353 | "movie_session": 1, 354 | "order": 1, 355 | "row": 2, 356 | "seat": 4 357 | } 358 | }, 359 | { 360 | "model": "cinema.ticket", 361 | "pk": 3, 362 | "fields": { 363 | "movie_session": 2, 364 | "order": 1, 365 | "row": 3, 366 | "seat": 3 367 | } 368 | }, 369 | { 370 | "model": "cinema.ticket", 371 | "pk": 4, 372 | "fields": { 373 | "movie_session": 2, 374 | "order": 1, 375 | "row": 4, 376 | "seat": 3 377 | } 378 | }, 379 | { 380 | "model": "cinema.ticket", 381 | "pk": 5, 382 | "fields": { 383 | "movie_session": 3, 384 | "order": 1, 385 | "row": 5, 386 | "seat": 1 387 | } 388 | }, 389 | { 390 | "model": "cinema.ticket", 391 | "pk": 6, 392 | "fields": { 393 | "movie_session": 4, 394 | "order": 1, 395 | "row": 6, 396 | "seat": 2 397 | } 398 | }, 399 | { 400 | "model": "cinema.ticket", 401 | "pk": 7, 402 | "fields": { 403 | "movie_session": 5, 404 | "order": 1, 405 | "row": 7, 406 | "seat": 3 407 | } 408 | }, 409 | { 410 | "model": "cinema.ticket", 411 | "pk": 8, 412 | "fields": { 413 | "movie_session": 5, 414 | "order": 2, 415 | "row": 8, 416 | "seat": 4 417 | } 418 | }, 419 | { 420 | "model": "cinema.ticket", 421 | "pk": 9, 422 | "fields": { 423 | "movie_session": 5, 424 | "order": 2, 425 | "row": 1, 426 | "seat": 5 427 | } 428 | }, 429 | { 430 | "model": "cinema.ticket", 431 | "pk": 10, 432 | "fields": { 433 | "movie_session": 6, 434 | "order": 2, 435 | "row": 1, 436 | "seat": 6 437 | } 438 | }, 439 | { 440 | "model": "cinema.ticket", 441 | "pk": 11, 442 | "fields": { 443 | "movie_session": 7, 444 | "order": 3, 445 | "row": 4, 446 | "seat": 7 447 | } 448 | }, 449 | { 450 | "model": "cinema.ticket", 451 | "pk": 12, 452 | "fields": { 453 | "movie_session": 8, 454 | "order": 3, 455 | "row": 3, 456 | "seat": 7 457 | } 458 | }, 459 | { 460 | "model": "cinema.ticket", 461 | "pk": 13, 462 | "fields": { 463 | "movie_session": 8, 464 | "order": 3, 465 | "row": 5, 466 | "seat": 4 467 | } 468 | }, 469 | { 470 | "model": "cinema.ticket", 471 | "pk": 14, 472 | "fields": { 473 | "movie_session": 8, 474 | "order": 4, 475 | "row": 4, 476 | "seat": 4 477 | } 478 | }, 479 | { 480 | "model": "cinema.ticket", 481 | "pk": 15, 482 | "fields": { 483 | "movie_session": 1, 484 | "order": 4, 485 | "row": 2, 486 | "seat": 3 487 | } 488 | }, 489 | { 490 | "model": "cinema.ticket", 491 | "pk": 16, 492 | "fields": { 493 | "movie_session": 2, 494 | "order": 4, 495 | "row": 5, 496 | "seat": 3 497 | } 498 | }, 499 | { 500 | "model": "user.user", 501 | "pk": 1, 502 | "fields": { 503 | "password": "pbkdf2_sha256$320000$oZqdjfUqKC500V7kk3kMmb$TBVlOMhfJxWrxbUnNpWZK0+rYM242g7do/YM5tkhn+M=", 504 | "last_login": null, 505 | "is_superuser": true, 506 | "username": "admin.user", 507 | "first_name": "Admin", 508 | "last_name": "User", 509 | "email": "admin.user@cinema.com", 510 | "is_staff": true, 511 | "is_active": true, 512 | "date_joined": "2022-08-09T09:06:18.876Z", 513 | "groups": [], 514 | "user_permissions": [] 515 | } 516 | }, 517 | { 518 | "model": "user.user", 519 | "pk": 2, 520 | "fields": { 521 | "password": "pbkdf2_sha256$320000$73Z1Y57bcoscZ37s9rE9v0$4SEQ0TtXwJKzO5/CRdFSrTifatiPjkZ38gM2H8Kqsdg=", 522 | "last_login": null, 523 | "is_superuser": false, 524 | "username": "ivan.khripko", 525 | "first_name": "Ivan", 526 | "last_name": "Khripko", 527 | "email": "ivan.khripko@cinema.com", 528 | "is_staff": false, 529 | "is_active": true, 530 | "date_joined": "2022-08-09T09:06:18.876Z", 531 | "groups": [], 532 | "user_permissions": [] 533 | } 534 | }, 535 | { 536 | "model": "user.user", 537 | "pk": 3, 538 | "fields": { 539 | "password": "pbkdf2_sha256$320000$QtPol3T7x9quBj4BnbnVwW$UwT4kihkRfSvQEWEeuqVKwMgis2ikKVZfJDBcQMvOKY=", 540 | "last_login": null, 541 | "is_superuser": false, 542 | "username": "mykola.hmara", 543 | "first_name": "Mykola", 544 | "last_name": "Hmara", 545 | "email": "mykola.hmara@cinema.com", 546 | "is_staff": false, 547 | "is_active": true, 548 | "date_joined": "2022-08-09T09:06:18.876Z", 549 | "groups": [], 550 | "user_permissions": [] 551 | } 552 | } 553 | ] 554 | --------------------------------------------------------------------------------