From eaf9d18065daf81ec10cdef7f28529792bd840ec Mon Sep 17 00:00:00 2001 From: Maksym Bondarevskyi Date: Thu, 21 Nov 2024 22:52:46 +0200 Subject: [PATCH 1/3] Implement token authentication --- cinema/permissions.py | 16 +++++++++++ cinema/views.py | 57 ++++++++++++++++++++++++++++++++------ cinema_service/settings.py | 23 +++++++++------ cinema_service/urls.py | 1 + requirements.txt | 4 +-- user/serializers.py | 32 ++++++++++++++++++++- user/urls.py | 12 +++++++- user/views.py | 27 +++++++++++++++++- 8 files changed, 150 insertions(+), 22 deletions(-) create mode 100644 cinema/permissions.py diff --git a/cinema/permissions.py b/cinema/permissions.py new file mode 100644 index 00000000..f77cca79 --- /dev/null +++ b/cinema/permissions.py @@ -0,0 +1,16 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS + + +class IsAdminOrIfAuthenticatedReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + ( + request.user and request.user.is_staff + + ) + or ( + request.method in SAFE_METHODS + and request.user + and request.user.is_authenticated + ) + ) diff --git a/cinema/views.py b/cinema/views.py index a191bf5f..a2332811 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,11 +1,19 @@ 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 cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order - +from rest_framework.permissions import IsAuthenticated + +from cinema.models import ( + Genre, + Actor, + CinemaHall, + Movie, + MovieSession, + Order +) from cinema.serializers import ( GenreSerializer, ActorSerializer, @@ -21,24 +29,45 @@ ) -class GenreViewSet(viewsets.ModelViewSet): +class GenreViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.CreateModelMixin +): queryset = Genre.objects.all() serializer_class = GenreSerializer + authentication_classes = (TokenAuthentication,) -class ActorViewSet(viewsets.ModelViewSet): +class ActorViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.CreateModelMixin +): queryset = Actor.objects.all() serializer_class = ActorSerializer + authentication_classes = (TokenAuthentication,) -class CinemaHallViewSet(viewsets.ModelViewSet): +class CinemaHallViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.CreateModelMixin +): queryset = CinemaHall.objects.all() serializer_class = CinemaHallSerializer + authentication_classes = (TokenAuthentication,) -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,) @staticmethod def _params_to_ints(qs): @@ -87,6 +116,7 @@ class MovieSessionViewSet(viewsets.ModelViewSet): ) ) serializer_class = MovieSessionSerializer + authentication_classes = (TokenAuthentication,) def get_queryset(self): date = self.request.query_params.get("date") @@ -113,17 +143,26 @@ def get_serializer_class(self): return MovieSessionSerializer + class OrderPagination(PageNumberPagination): page_size = 10 max_page_size = 100 + permission_classes = (IsAuthenticated,) + authentication_classes = (TokenAuthentication,) -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 + permission_classes = (IsAuthenticated,) + authentication_classes = (TokenAuthentication,) 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..906369fe 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -12,10 +12,11 @@ from pathlib import Path +from cinema.permissions import IsAdminOrIfAuthenticatedReadOnly + # 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/ @@ -80,7 +81,6 @@ WSGI_APPLICATION = "cinema_service.wsgi.application" - # Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases @@ -91,26 +91,25 @@ } } - # Password validation # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { "NAME": "django.contrib.auth.password_validation." - "UserAttributeSimilarityValidator", + "UserAttributeSimilarityValidator", }, { "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", + "MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", + "CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", + "NumericPasswordValidator", }, ] @@ -127,7 +126,6 @@ USE_TZ = False - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ @@ -137,3 +135,12 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "cinema.permissions.IsAdminOrIfAuthenticatedReadOnly", + ], +} 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/requirements.txt b/requirements.txt index 56e13554..33ddcf3b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -django==4.1 +django==4.2 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 \ No newline at end of file +djangorestframework==3.15.2 \ No newline at end of file diff --git a/user/serializers.py b/user/serializers.py index fa56336e..d3d9c68f 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1 +1,31 @@ -# 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): + 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/urls.py b/user/urls.py index fa56336e..11b251a4 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1 +1,11 @@ -# write your code here +from django.urls import path + +from user.views import (CreateUserView, ManageUserView, CreateTokenView) + +app_name = "user" + +urlpatterns = [ + path("register/", CreateUserView.as_view(), name="create"), + path("login/", CreateTokenView.as_view(), name="login"), + path("me/", ManageUserView.as_view(), name="manage"), +] diff --git a/user/views.py b/user/views.py index fa56336e..bf0e8a6d 100644 --- a/user/views.py +++ b/user/views.py @@ -1 +1,26 @@ -# 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, AllowAny +from rest_framework.settings import api_settings + +from user.serializers import UserSerializer + + +class CreateUserView(generics.CreateAPIView): + serializer_class = UserSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = (AllowAny,) + + +class CreateTokenView(ObtainAuthToken): + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + authentication_classes = (TokenAuthentication,) + + +class ManageUserView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + permission_classes = (IsAuthenticated,) + + def get_object(self): + return self.request.user From cd153365a4ebc151b23482bd48242f73d7400479 Mon Sep 17 00:00:00 2001 From: Maksym Bondarevskyi Date: Thu, 21 Nov 2024 22:56:39 +0200 Subject: [PATCH 2/3] flake8 --- cinema/permissions.py | 11 ++++------- cinema/views.py | 9 +++++---- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/cinema/permissions.py b/cinema/permissions.py index f77cca79..bbe6dd6c 100644 --- a/cinema/permissions.py +++ b/cinema/permissions.py @@ -4,13 +4,10 @@ class IsAdminOrIfAuthenticatedReadOnly(BasePermission): def has_permission(self, request, view): return bool( - ( - request.user and request.user.is_staff - - ) + (request.user and request.user.is_staff) or ( - request.method in SAFE_METHODS - and request.user - and request.user.is_authenticated + request.method in SAFE_METHODS + and request.user + and request.user.is_authenticated ) ) diff --git a/cinema/views.py b/cinema/views.py index a2332811..687b37e5 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -110,9 +110,11 @@ class MovieSessionViewSet(viewsets.ModelViewSet): 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 @@ -143,7 +145,6 @@ def get_serializer_class(self): return MovieSessionSerializer - class OrderPagination(PageNumberPagination): page_size = 10 max_page_size = 100 From e656baf613b49a98292fe4214198ae8796382869 Mon Sep 17 00:00:00 2001 From: Maksym Bondarevskyi Date: Thu, 21 Nov 2024 23:09:40 +0200 Subject: [PATCH 3/3] changes --- cinema/views.py | 9 --------- cinema_service/settings.py | 2 +- user/serializers.py | 2 +- user/views.py | 1 - 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/cinema/views.py b/cinema/views.py index 687b37e5..0f16ba2b 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -2,7 +2,6 @@ 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 from rest_framework.permissions import IsAuthenticated @@ -36,7 +35,6 @@ class GenreViewSet( ): queryset = Genre.objects.all() serializer_class = GenreSerializer - authentication_classes = (TokenAuthentication,) class ActorViewSet( @@ -46,7 +44,6 @@ class ActorViewSet( ): queryset = Actor.objects.all() serializer_class = ActorSerializer - authentication_classes = (TokenAuthentication,) class CinemaHallViewSet( @@ -56,7 +53,6 @@ class CinemaHallViewSet( ): queryset = CinemaHall.objects.all() serializer_class = CinemaHallSerializer - authentication_classes = (TokenAuthentication,) class MovieViewSet( @@ -67,7 +63,6 @@ class MovieViewSet( ): queryset = Movie.objects.prefetch_related("genres", "actors") serializer_class = MovieSerializer - authentication_classes = (TokenAuthentication,) @staticmethod def _params_to_ints(qs): @@ -118,7 +113,6 @@ class MovieSessionViewSet(viewsets.ModelViewSet): ) ) serializer_class = MovieSessionSerializer - authentication_classes = (TokenAuthentication,) def get_queryset(self): date = self.request.query_params.get("date") @@ -148,8 +142,6 @@ def get_serializer_class(self): class OrderPagination(PageNumberPagination): page_size = 10 max_page_size = 100 - permission_classes = (IsAuthenticated,) - authentication_classes = (TokenAuthentication,) class OrderViewSet( @@ -163,7 +155,6 @@ class OrderViewSet( serializer_class = OrderSerializer pagination_class = OrderPagination permission_classes = (IsAuthenticated,) - authentication_classes = (TokenAuthentication,) 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 906369fe..30decdeb 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -124,7 +124,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ diff --git a/user/serializers.py b/user/serializers.py index d3d9c68f..41beabf3 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -12,7 +12,7 @@ class Meta: "password", "is_staff", ) - read_only = ["id", "is_staff"] + read_only_fields = ["id", "is_staff"] extra_kwargs = { "password": {"write_only": True, "min_length": 5} } diff --git a/user/views.py b/user/views.py index bf0e8a6d..bda4e3dc 100644 --- a/user/views.py +++ b/user/views.py @@ -15,7 +15,6 @@ class CreateUserView(generics.CreateAPIView): class CreateTokenView(ObtainAuthToken): renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES - authentication_classes = (TokenAuthentication,) class ManageUserView(generics.RetrieveUpdateAPIView):