From a9b56edbe29ccf4dc418b7ffccef27ef72c7cd88 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:25:47 +0200 Subject: [PATCH 1/9] solution --- cinema/models.py | 14 ++++---- cinema/permissions.py | 13 +++++++ cinema/serializers.py | 30 ++++++++-------- cinema/tests/test_movie_api.py | 12 ++----- cinema/views.py | 62 ++++++++++++++++++++++++++++------ cinema_service/settings.py | 5 +-- cinema_service/urls.py | 1 + user/serializers.py | 34 +++++++++++++++++++ user/tests/test_user_api.py | 8 ++--- user/urls.py | 13 +++++++ user/views.py | 24 +++++++++++++ 11 files changed, 166 insertions(+), 50 deletions(-) create mode 100644 cinema/permissions.py diff --git a/cinema/models.py b/cinema/models.py index e9b22ecd..9fe83356 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -63,9 +63,8 @@ def __str__(self): class Order(models.Model): created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey( - settings.AUTH_USER_MODEL, on_delete=models.CASCADE - ) + user = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE) def __str__(self): return str(self.created_at) @@ -78,9 +77,8 @@ class Ticket(models.Model): movie_session = models.ForeignKey( MovieSession, on_delete=models.CASCADE, related_name="tickets" ) - order = models.ForeignKey( - Order, on_delete=models.CASCADE, related_name="tickets" - ) + order = models.ForeignKey(Order, on_delete=models.CASCADE, + related_name="tickets") row = models.IntegerField() seat = models.IntegerField() @@ -122,8 +120,8 @@ def save( ) def __str__(self): - return (f"{str(self.movie_session)} " - f"(row: {self.row}, seat: {self.seat})") + return f"{str(self.movie_session)} "\ + f"(row: {self.row}, seat: {self.seat})" class Meta: unique_together = ("movie_session", "row", "seat") diff --git a/cinema/permissions.py b/cinema/permissions.py new file mode 100644 index 00000000..2522d6b4 --- /dev/null +++ b/cinema/permissions.py @@ -0,0 +1,13 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + + +class IsAdminOrIfAuthenticatedReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + ( + request.method in SAFE_METHODS + and request.user + and request.user.is_authenticated + ) + or (request.user and request.user.is_staff) + ) diff --git a/cinema/serializers.py b/cinema/serializers.py index 7a6ae20c..eab13004 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -37,9 +37,9 @@ class Meta: class MovieListSerializer(MovieSerializer): - genres = serializers.SlugRelatedField( - many=True, read_only=True, slug_field="name" - ) + genres = serializers.SlugRelatedField(many=True, + read_only=True, + slug_field="name") actors = serializers.SlugRelatedField( many=True, read_only=True, slug_field="full_name" ) @@ -51,7 +51,8 @@ class MovieDetailSerializer(MovieSerializer): class Meta: model = Movie - fields = ("id", "title", "description", "duration", "genres", "actors") + fields = ("id", "title", "description", + "duration", "genres", "actors") class MovieSessionSerializer(serializers.ModelSerializer): @@ -61,10 +62,10 @@ class Meta: class MovieSessionListSerializer(MovieSessionSerializer): - movie_title = serializers.CharField(source="movie.title", read_only=True) - cinema_hall_name = serializers.CharField( - source="cinema_hall.name", read_only=True - ) + movie_title = serializers.CharField(source="movie.title", + read_only=True) + cinema_hall_name = serializers.CharField(source="cinema_hall.name", + read_only=True) cinema_hall_capacity = serializers.IntegerField( source="cinema_hall.capacity", read_only=True ) @@ -85,9 +86,8 @@ class Meta: class TicketSerializer(serializers.ModelSerializer): def validate(self, attrs): data = super(TicketSerializer, self).validate(attrs=attrs) - Ticket.validate_ticket( - attrs["row"], attrs["seat"], attrs["movie_session"] - ) + Ticket.validate_ticket(attrs["row"], attrs["seat"], + attrs["movie_session"]) return data class Meta: @@ -96,7 +96,8 @@ class Meta: class TicketListSerializer(TicketSerializer): - movie_session = MovieSessionListSerializer(many=False, read_only=True) + movie_session = MovieSessionListSerializer(many=False, + read_only=True) class TicketSeatsSerializer(TicketSerializer): @@ -108,9 +109,8 @@ class Meta: class MovieSessionDetailSerializer(MovieSessionSerializer): movie = MovieListSerializer(many=False, read_only=True) cinema_hall = CinemaHallSerializer(many=False, read_only=True) - taken_places = TicketSeatsSerializer( - source="tickets", many=True, read_only=True - ) + taken_places = TicketSeatsSerializer(source="tickets", many=True, + read_only=True) class Meta: model = MovieSession diff --git a/cinema/tests/test_movie_api.py b/cinema/tests/test_movie_api.py index 75c4ba31..877eb036 100644 --- a/cinema/tests/test_movie_api.py +++ b/cinema/tests/test_movie_api.py @@ -54,9 +54,7 @@ def test_get_movies(self): def test_retrieve_movie(self): movie = sample_movie() movie.genres.add(Genre.objects.create(name="Genre")) - movie.actors.add( - Actor.objects.create(first_name="Actor", last_name="Last") - ) + movie.actors.add(Actor.objects.create(first_name="Actor", last_name="Last")) url = detail_url(movie.id) response = self.client.get(url) @@ -114,9 +112,7 @@ def test_put_movie(self): } response = self.client.put(url, payload) - self.assertEqual( - response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED - ) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) def test_delete_movie(self): movie = sample_movie() @@ -124,6 +120,4 @@ def test_delete_movie(self): url = detail_url(movie.id) response = self.client.delete(url) - self.assertEqual( - response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED - ) + self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/cinema/views.py b/cinema/views.py index a191bf5f..8e547350 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,10 +1,18 @@ from datetime import datetime from django.db.models import F, Count -from rest_framework import viewsets +from rest_framework import viewsets, mixins +from rest_framework.authentication import TokenAuthentication from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import IsAuthenticated -from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order +from cinema.models import (Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order) +from cinema.permissions import IsAdminOrIfAuthenticatedReadOnly from cinema.serializers import ( GenreSerializer, @@ -21,24 +29,43 @@ ) -class GenreViewSet(viewsets.ModelViewSet): +class GenreViewSet( + viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin +): queryset = Genre.objects.all() serializer_class = GenreSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) -class ActorViewSet(viewsets.ModelViewSet): +class ActorViewSet( + viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin +): queryset = Actor.objects.all() serializer_class = ActorSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) -class CinemaHallViewSet(viewsets.ModelViewSet): +class CinemaHallViewSet( + viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin +): queryset = CinemaHall.objects.all() serializer_class = CinemaHallSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) -class MovieViewSet(viewsets.ModelViewSet): +class MovieViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, +): queryset = Movie.objects.prefetch_related("genres", "actors") serializer_class = MovieSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) @staticmethod def _params_to_ints(qs): @@ -76,17 +103,26 @@ def get_serializer_class(self): return MovieSerializer -class MovieSessionViewSet(viewsets.ModelViewSet): +class MovieSessionViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): queryset = ( MovieSession.objects.all() .select_related("movie", "cinema_hall") .annotate( - tickets_available=F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") + tickets_available=F("cinema_hall__rows") * + F("cinema_hall__seats_in_row") - Count("tickets") ) ) serializer_class = MovieSessionSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) def get_queryset(self): date = self.request.query_params.get("date") @@ -118,12 +154,18 @@ class OrderPagination(PageNumberPagination): max_page_size = 100 -class OrderViewSet(viewsets.ModelViewSet): +class OrderViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.CreateModelMixin +): queryset = Order.objects.prefetch_related( "tickets__movie_session__movie", "tickets__movie_session__cinema_hall" ) serializer_class = OrderSerializer pagination_class = OrderPagination + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) def get_queryset(self): return Order.objects.filter(user=self.request.user) diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 29ea7dea..90b1b56c 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -21,7 +21,8 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = ( - "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" + "django-insecure-6vubhk2$++agnctay_4" + "pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" ) # SECURITY WARNING: don't run with debug turned on in production! @@ -125,7 +126,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) diff --git a/cinema_service/urls.py b/cinema_service/urls.py index bf903c00..a282f874 100644 --- a/cinema_service/urls.py +++ b/cinema_service/urls.py @@ -4,5 +4,6 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/cinema/", include("cinema.urls", namespace="cinema")), + path("api/user/", include("user.urls", namespace="user")), path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/user/serializers.py b/user/serializers.py index fa56336e..ee7c1289 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1 +1,35 @@ # write your code here +from django.contrib.auth import get_user_model +from rest_framework import serializers + + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = get_user_model() + fields = ( + "id", + "username", + "email", + "password", + "is_staff", + ) + read_only = ( + "id", + "is_staff", + ) + extra_kwargs = {"password": {"write_only": True, "min_length": 5}} + + def create(self, validated_data): + """create user with encrypted password""" + return get_user_model().objects.create_user(**validated_data) + + def update(self, instance, validated_data): + password = validated_data.pop("password", None) + user = super().update(instance, validated_data) + + if password: + user.set_password(password) + user.save() + + return user diff --git a/user/tests/test_user_api.py b/user/tests/test_user_api.py index 022dca8b..9e5a6691 100644 --- a/user/tests/test_user_api.py +++ b/user/tests/test_user_api.py @@ -59,9 +59,7 @@ def test_password_too_short(self): res = self.client.post(CREATE_USER_URL, payload) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - user_exists = ( - get_user_model().objects.filter(email=payload["email"]).exists() - ) + user_exists = get_user_model().objects.filter(email=payload["email"]).exists() self.assertFalse(user_exists) def test_create_token_for_user(self): @@ -79,9 +77,7 @@ def test_create_token_for_user(self): def test_create_token_invalid_credentials(self): """Test that token is not created if invalid credentials are given""" - create_user( - username="user12345", email="test@test.com", password="test123" - ) + create_user(username="user12345", email="test@test.com", password="test123") payload = { "username": "user12345", "email": "test@test.com", diff --git a/user/urls.py b/user/urls.py index fa56336e..df98ca37 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1 +1,14 @@ # write your code here +from django.urls import path + +from user.views import (CreateUserView, + LoginUserView, + ManageUserView) + +app_name = "user" + +urlpatterns = [ + path("register/", CreateUserView.as_view(), name="create"), + path("login/", LoginUserView.as_view(), name="login"), + path("me/", ManageUserView.as_view(), name="manage"), +] diff --git a/user/views.py b/user/views.py index fa56336e..c674cf47 100644 --- a/user/views.py +++ b/user/views.py @@ -1 +1,25 @@ # write your code here +from rest_framework import generics +from rest_framework.authentication import TokenAuthentication +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.permissions import IsAuthenticated +from rest_framework.settings import api_settings + +from user.serializers import UserSerializer + + +class CreateUserView(generics.CreateAPIView): + serializer_class = UserSerializer + + +class LoginUserView(ObtainAuthToken): + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + + +class ManageUserView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def get_object(self): + return self.request.user From 566f7b56eb3f03a0b8ecbdbf443decad90b135f4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:28:37 +0200 Subject: [PATCH 2/9] fix flake --- cinema/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 8e547350..5fbb1529 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -115,9 +115,9 @@ class MovieSessionViewSet( MovieSession.objects.all() .select_related("movie", "cinema_hall") .annotate( - tickets_available=F("cinema_hall__rows") * - F("cinema_hall__seats_in_row") - - Count("tickets") + tickets_available=F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") ) ) serializer_class = MovieSessionSerializer From 15debdb79ce9a0b279c720c16238913f63c55d15 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:30:45 +0200 Subject: [PATCH 3/9] fix flake --- cinema/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 5fbb1529..ee5ea057 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -115,9 +115,10 @@ class MovieSessionViewSet( MovieSession.objects.all() .select_related("movie", "cinema_hall") .annotate( - tickets_available=F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") + tickets_available= + F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") ) ) serializer_class = MovieSessionSerializer From 25191b4e7276d0d943071ebe7187af9f56197269 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:32:32 +0200 Subject: [PATCH 4/9] fix flake --- cinema/views.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index ee5ea057..5335186e 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -114,12 +114,8 @@ class MovieSessionViewSet( queryset = ( MovieSession.objects.all() .select_related("movie", "cinema_hall") - .annotate( - tickets_available= - F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") - ) + .annotate(tickets_available=F("cinema_hall__rows") * F("cinema_hall__seats_in_row") + - Count("tickets")) ) serializer_class = MovieSessionSerializer authentication_classes = (TokenAuthentication,) From 4b1ee3f5c5068ae9c2d1d8ec590e0baa4a6e676a Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:34:10 +0200 Subject: [PATCH 5/9] fix flake --- cinema/views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 5335186e..5fbb1529 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -114,8 +114,11 @@ class MovieSessionViewSet( queryset = ( MovieSession.objects.all() .select_related("movie", "cinema_hall") - .annotate(tickets_available=F("cinema_hall__rows") * F("cinema_hall__seats_in_row") - - Count("tickets")) + .annotate( + tickets_available=F("cinema_hall__rows") + * F("cinema_hall__seats_in_row") + - Count("tickets") + ) ) serializer_class = MovieSessionSerializer authentication_classes = (TokenAuthentication,) From 39c00e62e413b59a5fbb9a89d2cdcac570697180 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:35:24 +0200 Subject: [PATCH 6/9] fix flake --- cinema/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 5fbb1529..151bca19 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -116,8 +116,8 @@ class MovieSessionViewSet( .select_related("movie", "cinema_hall") .annotate( tickets_available=F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") + * F("cinema_hall__seats_in_row") + - Count("tickets") ) ) serializer_class = MovieSessionSerializer From 36f1271e7d2adccd263e7e78ebd0fa7c9d88d6f4 Mon Sep 17 00:00:00 2001 From: Serhii Date: Sat, 14 Dec 2024 21:36:35 +0200 Subject: [PATCH 7/9] fix flake --- cinema/views.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 151bca19..cf5e06f2 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -111,15 +111,7 @@ class MovieSessionViewSet( mixins.DestroyModelMixin, viewsets.GenericViewSet, ): - queryset = ( - MovieSession.objects.all() - .select_related("movie", "cinema_hall") - .annotate( - tickets_available=F("cinema_hall__rows") - * F("cinema_hall__seats_in_row") - - Count("tickets") - ) - ) + queryset = MovieSession.objects.all() serializer_class = MovieSessionSerializer authentication_classes = (TokenAuthentication,) permission_classes = (IsAdminOrIfAuthenticatedReadOnly,) From e9cb34b12f45cbd7c0dbfa4e4982a60ece295d47 Mon Sep 17 00:00:00 2001 From: Serhii Date: Mon, 16 Dec 2024 09:12:15 +0200 Subject: [PATCH 8/9] fix readonly --- cinema/views.py | 3 --- user/serializers.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index cf5e06f2..a41fa6cd 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,6 +1,4 @@ from datetime import datetime - -from django.db.models import F, Count from rest_framework import viewsets, mixins from rest_framework.authentication import TokenAuthentication from rest_framework.pagination import PageNumberPagination @@ -13,7 +11,6 @@ MovieSession, Order) from cinema.permissions import IsAdminOrIfAuthenticatedReadOnly - from cinema.serializers import ( GenreSerializer, ActorSerializer, diff --git a/user/serializers.py b/user/serializers.py index ee7c1289..93aae8a1 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -14,7 +14,7 @@ class Meta: "password", "is_staff", ) - read_only = ( + read_only_fields = ( "id", "is_staff", ) From b4e519b1f505d9b60e938b1dcf33bc65f32960cd Mon Sep 17 00:00:00 2001 From: Serhii Date: Tue, 17 Dec 2024 11:50:22 +0200 Subject: [PATCH 9/9] fix code look --- cinema/models.py | 10 ++++++---- cinema/serializers.py | 21 ++++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/cinema/models.py b/cinema/models.py index 9fe83356..2f9ac0f3 100644 --- a/cinema/models.py +++ b/cinema/models.py @@ -63,8 +63,10 @@ def __str__(self): class Order(models.Model): created_at = models.DateTimeField(auto_now_add=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, - on_delete=models.CASCADE) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE + ) def __str__(self): return str(self.created_at) @@ -120,8 +122,8 @@ def save( ) def __str__(self): - return f"{str(self.movie_session)} "\ - f"(row: {self.row}, seat: {self.seat})" + return (f"{str(self.movie_session)} " + f"(row: {self.row}, seat: {self.seat})") class Meta: unique_together = ("movie_session", "row", "seat") diff --git a/cinema/serializers.py b/cinema/serializers.py index eab13004..0012019c 100644 --- a/cinema/serializers.py +++ b/cinema/serializers.py @@ -37,9 +37,11 @@ class Meta: class MovieListSerializer(MovieSerializer): - genres = serializers.SlugRelatedField(many=True, - read_only=True, - slug_field="name") + genres = serializers.SlugRelatedField( + many=True, + read_only=True, + slug_field="name" + ) actors = serializers.SlugRelatedField( many=True, read_only=True, slug_field="full_name" ) @@ -96,8 +98,10 @@ class Meta: class TicketListSerializer(TicketSerializer): - movie_session = MovieSessionListSerializer(many=False, - read_only=True) + movie_session = MovieSessionListSerializer( + many=False, + read_only=True + ) class TicketSeatsSerializer(TicketSerializer): @@ -109,8 +113,11 @@ class Meta: class MovieSessionDetailSerializer(MovieSessionSerializer): movie = MovieListSerializer(many=False, read_only=True) cinema_hall = CinemaHallSerializer(many=False, read_only=True) - taken_places = TicketSeatsSerializer(source="tickets", many=True, - read_only=True) + taken_places = TicketSeatsSerializer( + source="tickets", + many=True, + read_only=True + ) class Meta: model = MovieSession