diff --git a/.flake8 b/.flake8 index cabebb864..0e74d2cef 100644 --- a/.flake8 +++ b/.flake8 @@ -1,10 +1,10 @@ -[flake8] -inline-quotes = " -ignore = E203, E266, W503, N807, N818, F401, VNE003 -max-line-length = 79 -max-complexity = 18 -select = B,C,E,F,W,T4,B9,Q0,N8,VNE -exclude = - **migrations - venv +[flake8] +inline-quotes = " +ignore = E203, E266, W503, N807, N818, F401, VNE003 +max-line-length = 79 +max-complexity = 18 +select = B,C,E,F,W,T4,B9,Q0,N8,VNE +exclude = + **migrations + venv tests \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6d4b4ffb..20c0822b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,32 +1,32 @@ -name: Test - -on: - pull_request: - branches: - - "master" - -jobs: - test: - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout repo - uses: actions/checkout@v2 - - - name: Set Up Python 3.10 - uses: actions/setup-python@v2 - with: - python-version: "3.10" - - - name: Install requirements - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Run flake8 - run: flake8 - - - name: Run tests - timeout-minutes: 5 - run: python manage.py test +name: Test + +on: + pull_request: + branches: + - "master" + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repo + uses: actions/checkout@v2 + + - name: Set Up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run flake8 + run: flake8 + + - name: Run tests + timeout-minutes: 5 + run: python manage.py test diff --git a/.gitignore b/.gitignore index 9c18d6ac5..e25dda690 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ -.idea/ -.vscode/ -*.iml -.env -.DS_Store -venv/ -.pytest_cache/ -**__pycache__/ -*.pyc +.idea/ +.vscode/ +*.iml +.env +.DS_Store +venv/ +.pytest_cache/ +**__pycache__/ +*.pyc db.sqlite3 \ No newline at end of file diff --git a/README.md b/README.md index 555fe7763..0d810b491 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,38 @@ -# API Views - -- Read [the guideline](https://github.com/mate-academy/py-task-guideline/blob/main/README.md) before starting. - -Now, you are going to implement views via class-based views. - -Create `Genre`, `Actor`, `CinemaHall` models and update `Movie` model to -the ones you wrote in Django ORM module. Modules should have such fields: -- `Actor`: `first_name`, `last_name` -- `Genre`: `name` (note: must be unique) -- `CinemaHall`: `name`, `rows`, `seats_in_row` -- `Movie`: `title`, `description`, `actors`, `genres`, `duration`. (note: you -already have a new field here) - -Create serializers for all these models. Do not use related serializers for -ManyToMany relations. - -Use the following command to load prepared data from fixture to test and debug your code: - `python manage.py loaddata cinema_serviсe_db_data.json`. - -Create views for models interaction endpoints via different class-based views: -- For the `Genre` model use an `APIView` -- For the `Actor` model use a `GenericAPIView` -- For the `CinemaHall` model use a `GenericViewSet` -- For the `Movie` model use a `ModelViewSet` and `routers` - -Feel free to add more data using admin panel, if needed. - -For every `` from `actors`, `genres`, `cinema_halls`, `movies`, such -endpoints should work: -* `GET api/cinema//` - should return a list of the all entity items -* `POST api/cinema//` - should create a new entity based on passed data -* `GET api/cinema///` - should return an entity with given id -* `PUT api/cinema///` - should update the entity with given id based on passed data -* `PATCH api/cinema///` - should partially update the entity with given id based on passed data -* `DELETE api/cinema///` - should delete the entity with given id - -### Note: Check your code using this [checklist](checklist.md) before pushing your solution. +# API Views + +- Read [the guideline](https://github.com/mate-academy/py-task-guideline/blob/main/README.md) before starting. + +Now, you are going to implement views via class-based views. + +Create `Genre`, `Actor`, `CinemaHall` models and update `Movie` model to +the ones you wrote in Django ORM module. Modules should have such fields: +- `Actor`: `first_name`, `last_name` +- `Genre`: `name` (note: must be unique) +- `CinemaHall`: `name`, `rows`, `seats_in_row` +- `Movie`: `title`, `description`, `actors`, `genres`, `duration`. (note: you +already have a new field here) + +Create serializers for all these models. Do not use related serializers for +ManyToMany relations. + +Use the following command to load prepared data from fixture to test and debug your code: + `python manage.py loaddata cinema_serviсe_db_data.json`. + +Create views for models interaction endpoints via different class-based views: +- For the `Genre` model use an `APIView` +- For the `Actor` model use a `GenericAPIView` +- For the `CinemaHall` model use a `GenericViewSet` +- For the `Movie` model use a `ModelViewSet` and `routers` + +Feel free to add more data using admin panel, if needed. + +For every `` from `actors`, `genres`, `cinema_halls`, `movies`, such +endpoints should work: +* `GET api/cinema//` - should return a list of the all entity items +* `POST api/cinema//` - should create a new entity based on passed data +* `GET api/cinema///` - should return an entity with given id +* `PUT api/cinema///` - should update the entity with given id based on passed data +* `PATCH api/cinema///` - should partially update the entity with given id based on passed data +* `DELETE api/cinema///` - should delete the entity with given id + +### Note: Check your code using this [checklist](checklist.md) before pushing your solution. diff --git a/checklist.md b/checklist.md index 106167ef1..31d667909 100644 --- a/checklist.md +++ b/checklist.md @@ -1,87 +1,87 @@ -# Check Your Code Against the Following Points - -## Code Style - -1. Don't forget define the `related_name` for `ManyToManyField`. - -2. Don't forget return `Response` with `errors` if serializer is not valid: - -Good example: - -```python -class GenreList(APIView): - def post(self, request): - serializer = GenreSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -``` - -Another good example: - -```python -class GenreList(APIView): - def post(self, request): - serializer = GenreSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) -``` - -Bad example: - -```python -class GenreList(APIView): - def post(self, request): - serializer = GenreSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) -``` - -Another bad example: - -```python -class GenreList(APIView): - def post(self, request): - serializer = GenreSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST) -``` - -3. Group imports using `()` if needed. - -Good example: - -```python -from django.contrib.auth.mixins import ( - LoginRequiredMixin, - UserPassesTestMixin, - PermissionRequiredMixin, -) -``` - -Bad example: - -```python -from django.contrib.auth.mixins import LoginRequiredMixin, \ - UserPassesTestMixin, PermissionRequiredMixin -``` - -Another bad example: - -```python -from django.contrib.auth.mixins import ( - LoginRequiredMixin, - UserPassesTestMixin, PermissionRequiredMixin, -) -``` - -## Clean Code -Add comments, prints, and functions to check your solution when you write your code. -Don't forget to delete them when you are ready to commit and push your code. +# Check Your Code Against the Following Points + +## Code Style + +1. Don't forget define the `related_name` for `ManyToManyField`. + +2. Don't forget return `Response` with `errors` if serializer is not valid: + +Good example: + +```python +class GenreList(APIView): + def post(self, request): + serializer = GenreSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +``` + +Another good example: + +```python +class GenreList(APIView): + def post(self, request): + serializer = GenreSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) +``` + +Bad example: + +```python +class GenreList(APIView): + def post(self, request): + serializer = GenreSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) +``` + +Another bad example: + +```python +class GenreList(APIView): + def post(self, request): + serializer = GenreSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST) +``` + +3. Group imports using `()` if needed. + +Good example: + +```python +from django.contrib.auth.mixins import ( + LoginRequiredMixin, + UserPassesTestMixin, + PermissionRequiredMixin, +) +``` + +Bad example: + +```python +from django.contrib.auth.mixins import LoginRequiredMixin, \ + UserPassesTestMixin, PermissionRequiredMixin +``` + +Another bad example: + +```python +from django.contrib.auth.mixins import ( + LoginRequiredMixin, + UserPassesTestMixin, PermissionRequiredMixin, +) +``` + +## Clean Code +Add comments, prints, and functions to check your solution when you write your code. +Don't forget to delete them when you are ready to commit and push your code. diff --git a/cinema/admin.py b/cinema/admin.py index 06a2c4616..ed2b2002b 100644 --- a/cinema/admin.py +++ b/cinema/admin.py @@ -1,8 +1,8 @@ -from django.contrib import admin - -from cinema.models import Movie - - -@admin.register(Movie) -class MovieAdmin(admin.ModelAdmin): - pass +from django.contrib import admin + +from cinema.models import Movie + + +@admin.register(Movie) +class MovieAdmin(admin.ModelAdmin): + pass diff --git a/cinema/apps.py b/cinema/apps.py index c730aa2b0..b3adbc13d 100644 --- a/cinema/apps.py +++ b/cinema/apps.py @@ -1,6 +1,6 @@ -from django.apps import AppConfig - - -class CinemaConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "cinema" +from django.apps import AppConfig + + +class CinemaConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "cinema" diff --git a/cinema/migrations/0001_initial.py b/cinema/migrations/0001_initial.py index 514947a64..469206b95 100644 --- a/cinema/migrations/0001_initial.py +++ b/cinema/migrations/0001_initial.py @@ -1,30 +1,30 @@ -# Generated by Django 4.0.2 on 2022-05-03 13:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - 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()), - ], - ), - ] +# Generated by Django 4.0.2 on 2022-05-03 13:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + 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()), + ], + ), + ] diff --git a/cinema/migrations/0002_actor_cinemahall_genre_movie_actors_movie_genres.py b/cinema/migrations/0002_actor_cinemahall_genre_movie_actors_movie_genres.py new file mode 100644 index 000000000..18c47f8d9 --- /dev/null +++ b/cinema/migrations/0002_actor_cinemahall_genre_movie_actors_movie_genres.py @@ -0,0 +1,47 @@ +# Generated by Django 4.1 on 2024-12-10 13:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cinema', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Actor', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=55)), + ('last_name', models.CharField(max_length=55)), + ], + ), + migrations.CreateModel( + 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()), + ], + ), + migrations.CreateModel( + 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)), + ], + ), + migrations.AddField( + model_name='movie', + name='actors', + field=models.ManyToManyField(related_name='movies', to='cinema.actor'), + ), + migrations.AddField( + model_name='movie', + name='genres', + field=models.ManyToManyField(related_name='movies', to='cinema.genre'), + ), + ] diff --git a/cinema/models.py b/cinema/models.py index cc477513f..4bad4c832 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -1,10 +1,36 @@ -from django.db import models - - -class Movie(models.Model): - title = models.CharField(max_length=255) - description = models.TextField() - duration = models.IntegerField() - - def __str__(self): - return self.title +from django.db import models + + +class Genre(models.Model): + name = models.CharField(max_length=255, unique=True) + + def __str__(self): + return self.name + + +class Actor(models.Model): + first_name = models.CharField(max_length=55) + last_name = models.CharField(max_length=55) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + +class CinemaHall(models.Model): + name = models.CharField(max_length=255) + rows = models.IntegerField() + seats_in_row = models.IntegerField() + + def __str__(self): + return self.name + + +class Movie(models.Model): + title = models.CharField(max_length=255) + description = models.TextField() + duration = models.IntegerField() + actors = models.ManyToManyField(Actor, related_name="movies") + genres = models.ManyToManyField(Genre, related_name="movies") + + def __str__(self): + return self.title diff --git a/cinema/serializers.py b/cinema/serializers.py index 050db5771..5ec25ad68 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -1,24 +1,42 @@ -from rest_framework import serializers - -from cinema.models import Movie - - -class MovieSerializer(serializers.Serializer): - id = serializers.IntegerField(read_only=True) - title = serializers.CharField(max_length=255) - description = serializers.CharField() - duration = serializers.IntegerField() - - def create(self, validated_data): - return Movie.objects.create(**validated_data) - - def update(self, instance, validated_data): - instance.title = validated_data.get("title", instance.title) - instance.description = validated_data.get( - "description", instance.description - ) - instance.duration = validated_data.get("duration", instance.duration) - - instance.save() - - return instance +from rest_framework import serializers + +from cinema.models import Actor, CinemaHall, Genre, Movie + + +class MovieSerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + title = serializers.CharField(max_length=255) + description = serializers.CharField() + duration = serializers.IntegerField() + + def create(self, validated_data): + return Movie.objects.create(**validated_data) + + def update(self, instance, validated_data): + instance.title = validated_data.get("title", instance.title) + instance.description = validated_data.get( + "description", instance.description + ) + instance.duration = validated_data.get("duration", instance.duration) + + instance.save() + + return instance + + +class ActorSerializer(serializers.ModelSerializer): + class Meta: + model = Actor + fields = "first_name", "last_name" + + +class GenreSerializer(serializers.ModelSerializer): + class Meta: + model = Genre + fields = ["name"] + + +class CinemaHallSerializer(serializers.ModelSerializer): + class Meta: + model = CinemaHall + fields = "name", "rows", "seats_in_row" diff --git a/cinema/tests/test_actor_api.py b/cinema/tests/test_actor_api.py index 629be7028..b9a6c8ae0 100644 --- a/cinema/tests/test_actor_api.py +++ b/cinema/tests/test_actor_api.py @@ -1,113 +1,112 @@ -from django.test import TestCase - -from rest_framework import status, generics, mixins, viewsets -from rest_framework.test import APIClient - -from cinema.serializers import ActorSerializer -from cinema.models import Actor -from cinema.views import ActorList, ActorDetail - - -class ActorApiTests(TestCase): - def setUp(self): - self.client = APIClient() - Actor.objects.create(first_name="George", last_name="Clooney") - Actor.objects.create(first_name="Keanu", last_name="Reeves") - - def test_actor_list_is_subclass(self): - self.assertTrue(issubclass(ActorList, mixins.ListModelMixin)) - self.assertTrue(issubclass(ActorList, mixins.CreateModelMixin)) - self.assertTrue(issubclass(ActorList, generics.GenericAPIView)) - - def test_actor_list_is_not_subclass(self): - self.assertFalse(issubclass(ActorList, viewsets.GenericViewSet)) - - def test_actor_detail_is_subclass(self): - items = [ - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - generics.GenericAPIView, - ] - - for item in items: - with self.subTest(): - self.assertTrue(issubclass(ActorDetail, item)) - - def test_actor_detail_is_not_subclass(self): - self.assertFalse(issubclass(ActorDetail, viewsets.GenericViewSet)) - - def test_get_actors(self): - response = self.client.get("/api/cinema/actors/") - serializer = ActorSerializer(Actor.objects.all(), many=True) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_post_actors(self): - response = self.client.post( - "/api/cinema/actors/", - { - "first_name": "Scarlett", - "last_name": "Johansson", - }, - ) - db_actors = Actor.objects.all() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(db_actors.count(), 3) - self.assertEqual(db_actors.filter(first_name="Scarlett").count(), 1) - - def test_get_actor(self): - response = self.client.get("/api/cinema/actors/2/") - serializer = ActorSerializer( - Actor(id=2, first_name="Keanu", last_name="Reeves") - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_get_invalid_actor(self): - response = self.client.get("/api/cinema/actors/1001/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_put_actor(self): - response = self.client.put( - "/api/cinema/actors/1/", - { - "first_name": "Scarlett", - "last_name": "Johansson", - }, - ) - actor_pk_1 = Actor.objects.get(pk=1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - [ - actor_pk_1.first_name, - actor_pk_1.last_name, - ], - [ - "Scarlett", - "Johansson", - ], - ) - - def test_patch_actor(self): - response = self.client.patch( - "/api/cinema/actors/1/", - { - "first_name": "Scarlett", - }, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_delete_actor(self): - response = self.client.delete( - "/api/cinema/actors/1/", - ) - db_actors_id_1 = Actor.objects.filter(id=1) - self.assertEqual(db_actors_id_1.count(), 0) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_delete_invalid_actor(self): - response = self.client.delete( - "/api/cinema/actors/1000/", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) +from django.test import TestCase +from rest_framework import generics, mixins, status, viewsets +from rest_framework.test import APIClient + +from cinema.models import Actor +from cinema.serializers import ActorSerializer +from cinema.views import ActorDetail, ActorList + + +class ActorApiTests(TestCase): + def setUp(self): + self.client = APIClient() + Actor.objects.create(first_name="George", last_name="Clooney") + Actor.objects.create(first_name="Keanu", last_name="Reeves") + + def test_actor_list_is_subclass(self): + self.assertTrue(issubclass(ActorList, mixins.ListModelMixin)) + self.assertTrue(issubclass(ActorList, mixins.CreateModelMixin)) + self.assertTrue(issubclass(ActorList, generics.GenericAPIView)) + + def test_actor_list_is_not_subclass(self): + self.assertFalse(issubclass(ActorList, viewsets.GenericViewSet)) + + def test_actor_detail_is_subclass(self): + items = [ + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + generics.GenericAPIView, + ] + + for item in items: + with self.subTest(): + self.assertTrue(issubclass(ActorDetail, item)) + + def test_actor_detail_is_not_subclass(self): + self.assertFalse(issubclass(ActorDetail, viewsets.GenericViewSet)) + + def test_get_actors(self): + response = self.client.get("/api/cinema/actors/") + serializer = ActorSerializer(Actor.objects.all(), many=True) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_post_actors(self): + response = self.client.post( + "/api/cinema/actors/", + { + "first_name": "Scarlett", + "last_name": "Johansson", + }, + ) + db_actors = Actor.objects.all() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(db_actors.count(), 3) + self.assertEqual(db_actors.filter(first_name="Scarlett").count(), 1) + + def test_get_actor(self): + response = self.client.get("/api/cinema/actors/2/") + serializer = ActorSerializer( + Actor(id=2, first_name="Keanu", last_name="Reeves") + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_get_invalid_actor(self): + response = self.client.get("/api/cinema/actors/1001/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_put_actor(self): + response = self.client.put( + "/api/cinema/actors/1/", + { + "first_name": "Scarlett", + "last_name": "Johansson", + }, + ) + actor_pk_1 = Actor.objects.get(pk=1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + [ + actor_pk_1.first_name, + actor_pk_1.last_name, + ], + [ + "Scarlett", + "Johansson", + ], + ) + + def test_patch_actor(self): + response = self.client.patch( + "/api/cinema/actors/1/", + { + "first_name": "Scarlett", + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_actor(self): + response = self.client.delete( + "/api/cinema/actors/1/", + ) + db_actors_id_1 = Actor.objects.filter(id=1) + self.assertEqual(db_actors_id_1.count(), 0) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_delete_invalid_actor(self): + response = self.client.delete( + "/api/cinema/actors/1000/", + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cinema/tests/test_cinema_hall_api.py b/cinema/tests/test_cinema_hall_api.py index 86a6902ad..b1e88429f 100644 --- a/cinema/tests/test_cinema_hall_api.py +++ b/cinema/tests/test_cinema_hall_api.py @@ -1,114 +1,113 @@ -from django.test import TestCase - -from rest_framework.test import APIClient -from rest_framework import status, viewsets - -from cinema.serializers import CinemaHallSerializer -from cinema.models import CinemaHall -from cinema.views import CinemaHallViewSet - - -class CinemaHallApiTests(TestCase): - def setUp(self): - self.client = APIClient() - CinemaHall.objects.create( - name="Blue", - rows=15, - seats_in_row=20, - ) - CinemaHall.objects.create( - name="VIP", - rows=6, - seats_in_row=8, - ) - - def test_cinema_hall_is_subclass_generic_viewset(self): - self.assertTrue(issubclass(CinemaHallViewSet, viewsets.GenericViewSet)) - - def test_cinema_hall_is_not_subclass(self): - self.assertFalse(issubclass(CinemaHallViewSet, viewsets.ModelViewSet)) - - def test_get_cinema_halls(self): - response = self.client.get("/api/cinema/cinema_halls/") - serializer = CinemaHallSerializer(CinemaHall.objects.all(), many=True) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_post_cinema_halls(self): - response = self.client.post( - "/api/cinema/cinema_halls/", - { - "name": "Yellow", - "rows": 14, - "seats_in_row": 15, - }, - ) - db_cinema_halls = CinemaHall.objects.all() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(db_cinema_halls.count(), 3) - self.assertEqual(db_cinema_halls.filter(name="Yellow").count(), 1) - - def test_get_cinema_hall(self): - response = self.client.get("/api/cinema/cinema_halls/2/") - serializer = CinemaHallSerializer( - CinemaHall( - id=2, - name="VIP", - rows=6, - seats_in_row=8, - ) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_get_invalid_cinema_hall(self): - response = self.client.get("/api/cinema/cinema_halls/1001/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_put_cinema_hall(self): - response = self.client.put( - "/api/cinema/cinema_halls/1/", - { - "name": "Yellow", - "rows": 14, - "seats_in_row": 15, - }, - ) - cinema_hall_pk_1 = CinemaHall.objects.get(pk=1) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - [ - cinema_hall_pk_1.name, - cinema_hall_pk_1.rows, - cinema_hall_pk_1.seats_in_row, - ], - [ - "Yellow", - 14, - 15, - ], - ) - - def test_patch_cinema_hall(self): - response = self.client.patch( - "/api/cinema/cinema_halls/1/", - { - "name": "Green", - }, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(CinemaHall.objects.get(id=1).name, "Green") - - def test_delete_cinema_hall(self): - response = self.client.delete( - "/api/cinema/cinema_halls/1/", - ) - db_cinema_halls_id_1 = CinemaHall.objects.filter(id=1) - self.assertEqual(db_cinema_halls_id_1.count(), 0) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_delete_invalid_cinema_hall(self): - response = self.client.delete( - "/api/cinema/cinema_halls/1000/", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) +from django.test import TestCase +from rest_framework import status, viewsets +from rest_framework.test import APIClient + +from cinema.models import CinemaHall +from cinema.serializers import CinemaHallSerializer +from cinema.views import CinemaHallViewSet + + +class CinemaHallApiTests(TestCase): + def setUp(self): + self.client = APIClient() + CinemaHall.objects.create( + name="Blue", + rows=15, + seats_in_row=20, + ) + CinemaHall.objects.create( + name="VIP", + rows=6, + seats_in_row=8, + ) + + def test_cinema_hall_is_subclass_generic_viewset(self): + self.assertTrue(issubclass(CinemaHallViewSet, viewsets.GenericViewSet)) + + def test_cinema_hall_is_not_subclass(self): + self.assertFalse(issubclass(CinemaHallViewSet, viewsets.ModelViewSet)) + + def test_get_cinema_halls(self): + response = self.client.get("/api/cinema/cinema_halls/") + serializer = CinemaHallSerializer(CinemaHall.objects.all(), many=True) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_post_cinema_halls(self): + response = self.client.post( + "/api/cinema/cinema_halls/", + { + "name": "Yellow", + "rows": 14, + "seats_in_row": 15, + }, + ) + db_cinema_halls = CinemaHall.objects.all() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(db_cinema_halls.count(), 3) + self.assertEqual(db_cinema_halls.filter(name="Yellow").count(), 1) + + def test_get_cinema_hall(self): + response = self.client.get("/api/cinema/cinema_halls/2/") + serializer = CinemaHallSerializer( + CinemaHall( + id=2, + name="VIP", + rows=6, + seats_in_row=8, + ) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_get_invalid_cinema_hall(self): + response = self.client.get("/api/cinema/cinema_halls/1001/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_put_cinema_hall(self): + response = self.client.put( + "/api/cinema/cinema_halls/1/", + { + "name": "Yellow", + "rows": 14, + "seats_in_row": 15, + }, + ) + cinema_hall_pk_1 = CinemaHall.objects.get(pk=1) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + [ + cinema_hall_pk_1.name, + cinema_hall_pk_1.rows, + cinema_hall_pk_1.seats_in_row, + ], + [ + "Yellow", + 14, + 15, + ], + ) + + def test_patch_cinema_hall(self): + response = self.client.patch( + "/api/cinema/cinema_halls/1/", + { + "name": "Green", + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(CinemaHall.objects.get(id=1).name, "Green") + + def test_delete_cinema_hall(self): + response = self.client.delete( + "/api/cinema/cinema_halls/1/", + ) + db_cinema_halls_id_1 = CinemaHall.objects.filter(id=1) + self.assertEqual(db_cinema_halls_id_1.count(), 0) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_delete_invalid_cinema_hall(self): + response = self.client.delete( + "/api/cinema/cinema_halls/1000/", + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cinema/tests/test_genre_api.py b/cinema/tests/test_genre_api.py index 266056743..12b994896 100644 --- a/cinema/tests/test_genre_api.py +++ b/cinema/tests/test_genre_api.py @@ -1,105 +1,104 @@ -from django.test import TestCase - -from rest_framework import status, generics, viewsets -from rest_framework.test import APIClient -from rest_framework.views import APIView - -from cinema.serializers import GenreSerializer -from cinema.models import Genre -from cinema.views import GenreList, GenreDetail - - -class GenreApiTests(TestCase): - def setUp(self): - self.client = APIClient() - Genre.objects.create( - name="Comedy", - ) - Genre.objects.create( - name="Drama", - ) - - def test_genre_list_is_subclass(self): - self.assertTrue(issubclass(GenreList, APIView)) - - def test_genre_list_is_not_subclass(self): - items = [generics.GenericAPIView, viewsets.GenericViewSet] - - for item in items: - with self.subTest(str(item).split(".")[2]): - self.assertFalse(issubclass(GenreList, item)) - - def test_genre_detail_is_subclass(self): - self.assertTrue(issubclass(GenreDetail, APIView)) - - def test_genre_detail_is_not_subclass(self): - items = [generics.GenericAPIView, viewsets.GenericViewSet] - - for item in items: - with self.subTest(str(item).split(".")[2]): - self.assertFalse(issubclass(GenreDetail, item)) - - def test_get_genres(self): - response = self.client.get("/api/cinema/genres/") - serializer = GenreSerializer(Genre.objects.all(), many=True) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_post_genres(self): - response = self.client.post( - "/api/cinema/genres/", - { - "name": "Sci-fi", - }, - ) - db_genres = Genre.objects.all() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(db_genres.count(), 3) - self.assertEqual(db_genres.filter(name="Sci-fi").count(), 1) - - def test_get_genre(self): - response = self.client.get("/api/cinema/genres/2/") - serializer = GenreSerializer( - Genre( - id=2, - name="Drama", - ) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_get_invalid_genre(self): - response = self.client.get("/api/cinema/genres/1001/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_put_genre(self): - response = self.client.put( - "/api/cinema/genres/1/", - { - "name": "Sci-fi", - }, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_patch_genre(self): - response = self.client.patch( - "/api/cinema/genres/1/", - { - "name": "Sci-fi", - }, - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_delete_genre(self): - response = self.client.delete( - "/api/cinema/genres/1/", - ) - db_genres_id_1 = Genre.objects.filter(id=1) - self.assertEqual(db_genres_id_1.count(), 0) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_delete_invalid_genre(self): - response = self.client.delete( - "/api/cinema/genres/1000/", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) +from django.test import TestCase +from rest_framework import generics, status, viewsets +from rest_framework.test import APIClient +from rest_framework.views import APIView + +from cinema.models import Genre +from cinema.serializers import GenreSerializer +from cinema.views import GenreDetail, GenreList + + +class GenreApiTests(TestCase): + def setUp(self): + self.client = APIClient() + Genre.objects.create( + name="Comedy", + ) + Genre.objects.create( + name="Drama", + ) + + def test_genre_list_is_subclass(self): + self.assertTrue(issubclass(GenreList, APIView)) + + def test_genre_list_is_not_subclass(self): + items = [generics.GenericAPIView, viewsets.GenericViewSet] + + for item in items: + with self.subTest(str(item).split(".")[2]): + self.assertFalse(issubclass(GenreList, item)) + + def test_genre_detail_is_subclass(self): + self.assertTrue(issubclass(GenreDetail, APIView)) + + def test_genre_detail_is_not_subclass(self): + items = [generics.GenericAPIView, viewsets.GenericViewSet] + + for item in items: + with self.subTest(str(item).split(".")[2]): + self.assertFalse(issubclass(GenreDetail, item)) + + def test_get_genres(self): + response = self.client.get("/api/cinema/genres/") + serializer = GenreSerializer(Genre.objects.all(), many=True) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_post_genres(self): + response = self.client.post( + "/api/cinema/genres/", + { + "name": "Sci-fi", + }, + ) + db_genres = Genre.objects.all() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(db_genres.count(), 3) + self.assertEqual(db_genres.filter(name="Sci-fi").count(), 1) + + def test_get_genre(self): + response = self.client.get("/api/cinema/genres/2/") + serializer = GenreSerializer( + Genre( + id=2, + name="Drama", + ) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_get_invalid_genre(self): + response = self.client.get("/api/cinema/genres/1001/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_put_genre(self): + response = self.client.put( + "/api/cinema/genres/1/", + { + "name": "Sci-fi", + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_patch_genre(self): + response = self.client.patch( + "/api/cinema/genres/1/", + { + "name": "Sci-fi", + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_delete_genre(self): + response = self.client.delete( + "/api/cinema/genres/1/", + ) + db_genres_id_1 = Genre.objects.filter(id=1) + self.assertEqual(db_genres_id_1.count(), 0) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_delete_invalid_genre(self): + response = self.client.delete( + "/api/cinema/genres/1000/", + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cinema/tests/test_movie_api.py b/cinema/tests/test_movie_api.py index a665ace65..214584598 100644 --- a/cinema/tests/test_movie_api.py +++ b/cinema/tests/test_movie_api.py @@ -1,146 +1,145 @@ -from django.test import TestCase - -from rest_framework.test import APIClient -from rest_framework import status - -from cinema.serializers import MovieSerializer -from cinema.models import Movie -from cinema.views import MovieViewSet -from rest_framework.viewsets import ModelViewSet - - -class MovieApiTests(TestCase): - def setUp(self): - self.client = APIClient() - Movie.objects.create( - title="Titanic", - description="Titanic description", - duration=200, - ) - Movie.objects.create( - title="Batman", - description="Batman description", - duration=190, - ) - - def test_movie_viewset_is_subclass_model_viewset(self): - self.assertTrue(issubclass(MovieViewSet, ModelViewSet)) - - def test_get_movies(self): - movies = self.client.get("/api/cinema/movies/") - serializer = MovieSerializer(Movie.objects.all(), many=True) - self.assertEqual(movies.status_code, status.HTTP_200_OK) - self.assertEqual(movies.data, serializer.data) - - def test_post_movies(self): - movies = self.client.post( - "/api/cinema/movies/", - { - "title": "Superman", - "description": "Superman description", - "duration": 170, - }, - ) - db_movies = Movie.objects.all() - self.assertEqual(movies.status_code, status.HTTP_201_CREATED) - self.assertEqual(db_movies.count(), 3) - self.assertEqual(db_movies.filter(title="Superman").count(), 1) - - def test_post_invalid_movies(self): - movies = self.client.post( - "/api/cinema/movies/", - { - "title": "Superman", - "description": "Superman description", - "duration": "two hundred", - }, - ) - superman_movies = Movie.objects.filter(title="Superman") - self.assertEqual(movies.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(superman_movies.count(), 0) - - def test_get_movie(self): - response = self.client.get("/api/cinema/movies/2/") - serializer = MovieSerializer( - Movie( - id=2, - title="Batman", - description="Batman description", - duration=190, - ) - ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, serializer.data) - - def test_get_invalid_movie(self): - response = self.client.get("/api/cinema/movies/100/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_put_movie(self): - self.client.put( - "/api/cinema/movies/1/", - { - "title": "Watchman", - "description": "Watchman description", - "duration": 190, - }, - ) - db_movie = Movie.objects.get(id=1) - self.assertEqual( - [db_movie.title, db_movie.description, db_movie.duration], - [ - "Watchman", - "Watchman description", - 190, - ], - ) - self.assertEqual(db_movie.title, "Watchman") - - def test_put_invalid_movie(self): - response = self.client.put( - "/api/cinema/movies/1/", - { - "title": "Watchmen", - "description": "Watchmen description", - "duration": "fifty", - }, - ) - db_movie = Movie.objects.get(id=1) - self.assertEqual(db_movie.duration, 200) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_patch_movie(self): - response = self.client.patch( - "/api/cinema/movies/1/", - { - "title": "Watchmen", - }, - ) - db_movie = Movie.objects.get(id=1) - self.assertEqual(db_movie.title, "Watchmen") - self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_patch_invalid_movie(self): - response = self.client.patch( - "/api/cinema/movies/1/", - { - "duration": "fifty", - }, - ) - db_movie = Movie.objects.get(id=1) - self.assertEqual(db_movie.duration, 200) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_delete_movie(self): - response = self.client.delete( - "/api/cinema/movies/1/", - ) - db_movies_id_1 = Movie.objects.filter(id=1) - self.assertEqual(db_movies_id_1.count(), 0) - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - - def test_delete_invalid_movie(self): - response = self.client.delete( - "/api/cinema/movies/1000/", - ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) +from django.test import TestCase +from rest_framework import status +from rest_framework.test import APIClient +from rest_framework.viewsets import ModelViewSet + +from cinema.models import Movie +from cinema.serializers import MovieSerializer +from cinema.views import MovieViewSet + + +class MovieApiTests(TestCase): + def setUp(self): + self.client = APIClient() + Movie.objects.create( + title="Titanic", + description="Titanic description", + duration=200, + ) + Movie.objects.create( + title="Batman", + description="Batman description", + duration=190, + ) + + def test_movie_viewset_is_subclass_model_viewset(self): + self.assertTrue(issubclass(MovieViewSet, ModelViewSet)) + + def test_get_movies(self): + movies = self.client.get("/api/cinema/movies/") + serializer = MovieSerializer(Movie.objects.all(), many=True) + self.assertEqual(movies.status_code, status.HTTP_200_OK) + self.assertEqual(movies.data, serializer.data) + + def test_post_movies(self): + movies = self.client.post( + "/api/cinema/movies/", + { + "title": "Superman", + "description": "Superman description", + "duration": 170, + }, + ) + db_movies = Movie.objects.all() + self.assertEqual(movies.status_code, status.HTTP_201_CREATED) + self.assertEqual(db_movies.count(), 3) + self.assertEqual(db_movies.filter(title="Superman").count(), 1) + + def test_post_invalid_movies(self): + movies = self.client.post( + "/api/cinema/movies/", + { + "title": "Superman", + "description": "Superman description", + "duration": "two hundred", + }, + ) + superman_movies = Movie.objects.filter(title="Superman") + self.assertEqual(movies.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(superman_movies.count(), 0) + + def test_get_movie(self): + response = self.client.get("/api/cinema/movies/2/") + serializer = MovieSerializer( + Movie( + id=2, + title="Batman", + description="Batman description", + duration=190, + ) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, serializer.data) + + def test_get_invalid_movie(self): + response = self.client.get("/api/cinema/movies/100/") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_put_movie(self): + self.client.put( + "/api/cinema/movies/1/", + { + "title": "Watchman", + "description": "Watchman description", + "duration": 190, + }, + ) + db_movie = Movie.objects.get(id=1) + self.assertEqual( + [db_movie.title, db_movie.description, db_movie.duration], + [ + "Watchman", + "Watchman description", + 190, + ], + ) + self.assertEqual(db_movie.title, "Watchman") + + def test_put_invalid_movie(self): + response = self.client.put( + "/api/cinema/movies/1/", + { + "title": "Watchmen", + "description": "Watchmen description", + "duration": "fifty", + }, + ) + db_movie = Movie.objects.get(id=1) + self.assertEqual(db_movie.duration, 200) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_patch_movie(self): + response = self.client.patch( + "/api/cinema/movies/1/", + { + "title": "Watchmen", + }, + ) + db_movie = Movie.objects.get(id=1) + self.assertEqual(db_movie.title, "Watchmen") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_patch_invalid_movie(self): + response = self.client.patch( + "/api/cinema/movies/1/", + { + "duration": "fifty", + }, + ) + db_movie = Movie.objects.get(id=1) + self.assertEqual(db_movie.duration, 200) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_delete_movie(self): + response = self.client.delete( + "/api/cinema/movies/1/", + ) + db_movies_id_1 = Movie.objects.filter(id=1) + self.assertEqual(db_movies_id_1.count(), 0) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_delete_invalid_movie(self): + response = self.client.delete( + "/api/cinema/movies/1000/", + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/cinema/urls.py b/cinema/urls.py index 1ae7d5cb0..8d2e7b5a5 100644 --- a/cinema/urls.py +++ b/cinema/urls.py @@ -1,10 +1,19 @@ -from django.urls import path - -from cinema.views import movie_list, movie_detail - -urlpatterns = [ - path("movies/", movie_list, name="movie-list"), - path("movies//", movie_detail, name="movie-detail"), -] - -app_name = "cinema" +from django.urls import include, path +from rest_framework.routers import DefaultRouter + +from cinema.views import (ActorDetail, ActorList, CinemaHallViewSet, + GenreDetail, GenreList, MovieViewSet) + +app_name = "cinema" + +router = DefaultRouter() +router.register("cinema_halls", CinemaHallViewSet, basename="cinemahall") +router.register("movies", MovieViewSet, basename="movie") + +urlpatterns = [ + path("genres/", GenreList.as_view(), name="genre-list"), + path("genres//", GenreDetail.as_view(), name="genre-detail"), + path("actors/", ActorList.as_view(), name="actor-list"), + path("actors//", ActorDetail.as_view(), name="actor-detail"), + path("", include(router.urls)) +] diff --git a/cinema/views.py b/cinema/views.py index 78ba8a79c..958286633 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,45 +1,101 @@ -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework import status - -from django.shortcuts import get_object_or_404 - -from cinema.models import Movie -from cinema.serializers import MovieSerializer - - -@api_view(["GET", "POST"]) -def movie_list(request): - if request.method == "GET": - movies = Movie.objects.all() - serializer = MovieSerializer(movies, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - if request.method == "POST": - serializer = MovieSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -@api_view(["GET", "PUT", "DELETE"]) -def movie_detail(request, pk): - movie = get_object_or_404(Movie, pk=pk) - - if request.method == "GET": - serializer = MovieSerializer(movie) - return Response(serializer.data, status=status.HTTP_200_OK) - - if request.method == "PUT": - serializer = MovieSerializer(movie, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - if request.method == "DELETE": - movie.delete() - return Response(status=status.HTTP_204_NO_CONTENT) +from django.shortcuts import get_object_or_404 +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin, + ListModelMixin, RetrieveModelMixin, + UpdateModelMixin) +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from cinema.models import Actor, CinemaHall, Genre, Movie +from cinema.serializers import (ActorSerializer, CinemaHallSerializer, + GenreSerializer, MovieSerializer) + + +class GenreList(APIView): + def get(self, request): + genres = Genre.objects.all() + serializer = GenreSerializer(genres, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + def post(self, request): + serializer = GenreSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class GenreDetail(APIView): + def get(self, request, pk): + genre = get_object_or_404(Genre, pk=pk) + serializer = GenreSerializer(genre) + return Response(serializer.data) + + def put(self, request, pk): + genre = get_object_or_404(Genre, pk=pk) + serializer = GenreSerializer(genre, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def patch(self, request, pk): + genre = get_object_or_404(Genre, pk=pk) + serializer = GenreSerializer(genre, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, pk): + genre = get_object_or_404(Genre, pk=pk) + genre.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ActorList(ListModelMixin, CreateModelMixin, GenericAPIView): + queryset = Actor.objects.all() + serializer_class = ActorSerializer + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def post(self, request): + return self.create(request) + + +class ActorDetail(RetrieveModelMixin, + UpdateModelMixin, + DestroyModelMixin, + GenericAPIView): + queryset = Actor.objects.all() + serializer_class = ActorSerializer + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class CinemaHallViewSet(ListModelMixin, + CreateModelMixin, + RetrieveModelMixin, + UpdateModelMixin, + DestroyModelMixin, + GenericViewSet): + queryset = CinemaHall.objects.all() + serializer_class = CinemaHallSerializer + + +class MovieViewSet(ModelViewSet): + queryset = Movie.objects.all() + serializer_class = MovieSerializer diff --git a/cinema_service/asgi.py b/cinema_service/asgi.py index 40d094eb1..38e7b81ca 100644 --- a/cinema_service/asgi.py +++ b/cinema_service/asgi.py @@ -1,16 +1,16 @@ -""" -ASGI config for cinema_service project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") - -application = get_asgi_application() +""" +ASGI config for cinema_service project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") + +application = get_asgi_application() diff --git a/cinema_service/settings.py b/cinema_service/settings.py index def98786c..03f55bf0f 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -1,131 +1,131 @@ -""" -Django settings for cinema_service project. - -Generated by 'django-admin startproject' using Django 4.0.2. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.0/ref/settings/ -""" - -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = ( - "django-insecure-ru6ndfka@95_(lysua8yhdjq@vpiqgv3yru4r)q3h4_u8x7dfy" -) - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "rest_framework", - "cinema", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "cinema_service.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "cinema_service.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/4.0/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation." - "UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.0/howto/static-files/ - -STATIC_URL = "static/" - -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +""" +Django settings for cinema_service project. + +Generated by 'django-admin startproject' using Django 4.0.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.0/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = ( + "django-insecure-ru6ndfka@95_(lysua8yhdjq@vpiqgv3yru4r)q3h4_u8x7dfy" +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "cinema", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "cinema_service.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "cinema_service.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.0/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation." + "UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation." + "MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation." + "CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation." + "NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.0/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/cinema_service/urls.py b/cinema_service/urls.py index 229767ed9..1fd9fa0fe 100644 --- a/cinema_service/urls.py +++ b/cinema_service/urls.py @@ -1,7 +1,7 @@ -from django.contrib import admin -from django.urls import path, include - -urlpatterns = [ - path("admin/", admin.site.urls), - path("api/cinema/", include("cinema.urls", namespace="cinema")), -] +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path("admin/", admin.site.urls), + path("api/cinema/", include("cinema.urls", namespace="cinema")), +] diff --git a/cinema_service/wsgi.py b/cinema_service/wsgi.py index 7cc91620e..0e6a84834 100644 --- a/cinema_service/wsgi.py +++ b/cinema_service/wsgi.py @@ -1,16 +1,16 @@ -""" -WSGI config for cinema_service project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") - -application = get_wsgi_application() +""" +WSGI config for cinema_service project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") + +application = get_wsgi_application() diff --git "a/cinema_servi\321\201e_db_data.json" "b/cinema_servi\321\201e_db_data.json" index afb668155..ded85f401 100644 --- "a/cinema_servi\321\201e_db_data.json" +++ "b/cinema_servi\321\201e_db_data.json" @@ -1,235 +1,235 @@ -[ - { - "model": "cinema.cinemahall", - "pk": 1, - "fields": { - "name": "Ricciotto Canudo", - "rows": 25, - "seats_in_row": 30 - } - }, - { - "model": "cinema.cinemahall", - "pk": 2, - "fields": { - "name": "Robin Wood", - "rows": 24, - "seats_in_row": 18 - } - }, - { - "model": "cinema.cinemahall", - "pk": 3, - "fields": { - "name": "Peter Wollen", - "rows": 30, - "seats_in_row": 20 - } - }, - { - "model": "cinema.cinemahall", - "pk": 4, - "fields": { - "name": "Jean Mitry", - "rows": 14, - "seats_in_row": 8 - } - }, - { - "model": "cinema.genre", - "pk": 1, - "fields": { - "name": "Crime" - } - }, - { - "model": "cinema.genre", - "pk": 2, - "fields": { - "name": "Drama" - } - }, - { - "model": "cinema.genre", - "pk": 3, - "fields": { - "name": "Thriller" - } - }, - { - "model": "cinema.genre", - "pk": 4, - "fields": { - "name": "Action" - } - }, - { - "model": "cinema.genre", - "pk": 5, - "fields": { - "name": "Adventure" - } - }, - { - "model": "cinema.genre", - "pk": 6, - "fields": { - "name": "Sci-Fi" - } - }, - { - "model": "cinema.genre", - "pk": 7, - "fields": { - "name": "Mystery" - } - }, - { - "model": "cinema.actor", - "pk": 1, - "fields": { - "first_name": "Jack", - "last_name": "Nicholson" - } - }, - { - "model": "cinema.actor", - "pk": 2, - "fields": { - "first_name": "Leonardo", - "last_name": "DiCaprio" - } - }, - { - "model": "cinema.actor", - "pk": 3, - "fields": { - "first_name": "Matt", - "last_name": "Damon" - } - }, - { - "model": "cinema.actor", - "pk": 4, - "fields": { - "first_name": "Joseph", - "last_name": "Gordon-Levitt" - } - }, - { - "model": "cinema.actor", - "pk": 5, - "fields": { - "first_name": "Elliot", - "last_name": "Page" - } - }, - { - "model": "cinema.actor", - "pk": 6, - "fields": { - "first_name": "Bruce", - "last_name": "Willis" - } - }, - { - "model": "cinema.actor", - "pk": 7, - "fields": { - "first_name": "Emily", - "last_name": "Blunt" - } - }, - { - "model": "cinema.actor", - "pk": 8, - "fields": { - "first_name": "Samuel", - "last_name": "Jackson" - } - }, - { - "model": "cinema.actor", - "pk": 9, - "fields": { - "first_name": "Robin", - "last_name": "Wright" - } - }, - { - "model": "cinema.movie", - "pk": 1, - "fields": { - "title": "The Departed", - "description": "An undercover cop and a mole in the police attempt to identify each other while infiltrating an Irishgang in South Boston.", - "duration": 151, - "genres": [ - 1, - 2, - 3 - ], - "actors": [ - 1, - 2, - 3 - ] - } - }, - { - "model": "cinema.movie", - "pk": 2, - "fields": { - "title": "Inception", - "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.", - "duration": 148, - "genres": [ - 4, - 5, - 6 - ], - "actors": [ - 2, - 4, - 5 - ] - } - }, - { - "model": "cinema.movie", - "pk": 3, - "fields": { - "title": "Looper", - "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.", - "duration": 119, - "genres": [ - 2, - 4, - 6 - ], - "actors": [ - 4, - 6, - 7 - ] - } - }, - { - "model": "cinema.movie", - "pk": 4, - "fields": { - "title": "Unbreakable", - "description": "A man learns something extraordinary about himself after a devastating accident.", - "duration": 106, - "genres": [ - 2, - 6, - 7 - ], - "actors": [ - 6, - 8, - 9 - ] - } - } -] +[ + { + "model": "cinema.cinemahall", + "pk": 1, + "fields": { + "name": "Ricciotto Canudo", + "rows": 25, + "seats_in_row": 30 + } + }, + { + "model": "cinema.cinemahall", + "pk": 2, + "fields": { + "name": "Robin Wood", + "rows": 24, + "seats_in_row": 18 + } + }, + { + "model": "cinema.cinemahall", + "pk": 3, + "fields": { + "name": "Peter Wollen", + "rows": 30, + "seats_in_row": 20 + } + }, + { + "model": "cinema.cinemahall", + "pk": 4, + "fields": { + "name": "Jean Mitry", + "rows": 14, + "seats_in_row": 8 + } + }, + { + "model": "cinema.genre", + "pk": 1, + "fields": { + "name": "Crime" + } + }, + { + "model": "cinema.genre", + "pk": 2, + "fields": { + "name": "Drama" + } + }, + { + "model": "cinema.genre", + "pk": 3, + "fields": { + "name": "Thriller" + } + }, + { + "model": "cinema.genre", + "pk": 4, + "fields": { + "name": "Action" + } + }, + { + "model": "cinema.genre", + "pk": 5, + "fields": { + "name": "Adventure" + } + }, + { + "model": "cinema.genre", + "pk": 6, + "fields": { + "name": "Sci-Fi" + } + }, + { + "model": "cinema.genre", + "pk": 7, + "fields": { + "name": "Mystery" + } + }, + { + "model": "cinema.actor", + "pk": 1, + "fields": { + "first_name": "Jack", + "last_name": "Nicholson" + } + }, + { + "model": "cinema.actor", + "pk": 2, + "fields": { + "first_name": "Leonardo", + "last_name": "DiCaprio" + } + }, + { + "model": "cinema.actor", + "pk": 3, + "fields": { + "first_name": "Matt", + "last_name": "Damon" + } + }, + { + "model": "cinema.actor", + "pk": 4, + "fields": { + "first_name": "Joseph", + "last_name": "Gordon-Levitt" + } + }, + { + "model": "cinema.actor", + "pk": 5, + "fields": { + "first_name": "Elliot", + "last_name": "Page" + } + }, + { + "model": "cinema.actor", + "pk": 6, + "fields": { + "first_name": "Bruce", + "last_name": "Willis" + } + }, + { + "model": "cinema.actor", + "pk": 7, + "fields": { + "first_name": "Emily", + "last_name": "Blunt" + } + }, + { + "model": "cinema.actor", + "pk": 8, + "fields": { + "first_name": "Samuel", + "last_name": "Jackson" + } + }, + { + "model": "cinema.actor", + "pk": 9, + "fields": { + "first_name": "Robin", + "last_name": "Wright" + } + }, + { + "model": "cinema.movie", + "pk": 1, + "fields": { + "title": "The Departed", + "description": "An undercover cop and a mole in the police attempt to identify each other while infiltrating an Irishgang in South Boston.", + "duration": 151, + "genres": [ + 1, + 2, + 3 + ], + "actors": [ + 1, + 2, + 3 + ] + } + }, + { + "model": "cinema.movie", + "pk": 2, + "fields": { + "title": "Inception", + "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.", + "duration": 148, + "genres": [ + 4, + 5, + 6 + ], + "actors": [ + 2, + 4, + 5 + ] + } + }, + { + "model": "cinema.movie", + "pk": 3, + "fields": { + "title": "Looper", + "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.", + "duration": 119, + "genres": [ + 2, + 4, + 6 + ], + "actors": [ + 4, + 6, + 7 + ] + } + }, + { + "model": "cinema.movie", + "pk": 4, + "fields": { + "title": "Unbreakable", + "description": "A man learns something extraordinary about himself after a devastating accident.", + "duration": 106, + "genres": [ + 2, + 6, + 7 + ], + "actors": [ + 6, + 8, + 9 + ] + } + } +] diff --git a/manage.py b/manage.py index f64b24321..049c270e2 100755 --- a/manage.py +++ b/manage.py @@ -1,22 +1,22 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cinema_service.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/pytest.ini b/pytest.ini index e3d7d3d67..d5738353c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ -[pytest] -DJANGO_SETTINGS_MODULE = settings +[pytest] +DJANGO_SETTINGS_MODULE = settings addopts = --reuse-db \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 99938e15f..fe8ac10e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -django==4.1 -flake8==5.0.4 -flake8-quotes==3.3.1 -flake8-variables-names==0.0.5 -pep8-naming==0.13.2 -django-debug-toolbar==3.2.4 -djangorestframework==3.13.1 +django==4.1 +flake8==5.0.4 +flake8-quotes==3.3.1 +flake8-variables-names==0.0.5 +pep8-naming==0.13.2 +django-debug-toolbar==3.2.4 +djangorestframework==3.13.1