diff --git a/cinema/views.py b/cinema/views.py index a191bf5f..4e8090f3 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,8 +1,12 @@ from datetime import datetime from django.db.models import F, Count -from rest_framework import viewsets +from rest_framework import mixins +from rest_framework.authentication import TokenAuthentication from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.exceptions import PermissionDenied from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order @@ -17,66 +21,84 @@ MovieSessionDetailSerializer, MovieListSerializer, OrderSerializer, - OrderListSerializer, ) -class GenreViewSet(viewsets.ModelViewSet): +from user.permissions import IsAdminOrIfAuthenticatedReadOnly + + +class GenreViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericViewSet, +): queryset = Genre.objects.all() serializer_class = GenreSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] -class ActorViewSet(viewsets.ModelViewSet): +class ActorViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericViewSet, +): queryset = Actor.objects.all() serializer_class = ActorSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] -class CinemaHallViewSet(viewsets.ModelViewSet): +class CinemaHallViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericViewSet, +): queryset = CinemaHall.objects.all() serializer_class = CinemaHallSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] -class MovieViewSet(viewsets.ModelViewSet): +class MovieViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + GenericViewSet, +): queryset = Movie.objects.prefetch_related("genres", "actors") serializer_class = MovieSerializer + authentication_classes = (TokenAuthentication,) + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] @staticmethod def _params_to_ints(qs): - """Converts a list of string IDs to a list of integers""" return [int(str_id) for str_id in qs.split(",")] def get_queryset(self): - """Retrieve the movies with filters""" title = self.request.query_params.get("title") genres = self.request.query_params.get("genres") actors = self.request.query_params.get("actors") - queryset = self.queryset - if title: queryset = queryset.filter(title__icontains=title) - if genres: genres_ids = self._params_to_ints(genres) queryset = queryset.filter(genres__id__in=genres_ids) - if actors: actors_ids = self._params_to_ints(actors) queryset = queryset.filter(actors__id__in=actors_ids) - return queryset.distinct() def get_serializer_class(self): if self.action == "list": return MovieListSerializer - if self.action == "retrieve": return MovieDetailSerializer - return MovieSerializer -class MovieSessionViewSet(viewsets.ModelViewSet): +class MovieSessionViewSet(ModelViewSet): queryset = ( MovieSession.objects.all() .select_related("movie", "cinema_hall") @@ -87,6 +109,8 @@ class MovieSessionViewSet(viewsets.ModelViewSet): ) ) serializer_class = MovieSessionSerializer + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] + authentication_classes = (TokenAuthentication,) def get_queryset(self): date = self.request.query_params.get("date") @@ -112,27 +136,52 @@ def get_serializer_class(self): return MovieSessionSerializer + def perform_create(self, serializer): + if not self.request.user.is_staff: + raise PermissionDenied( + "You do not have permission to perform this action." + ) + serializer.save() + + def perform_update(self, serializer): + if not self.request.user.is_staff: + raise PermissionDenied( + "You do not have permission to perform this action." + ) + serializer.save() + + def perform_destroy(self, instance): + if not self.request.user.is_staff: + raise PermissionDenied( + "You do not have permission to perform this action." + ) + instance.delete() + class OrderPagination(PageNumberPagination): page_size = 10 max_page_size = 100 -class OrderViewSet(viewsets.ModelViewSet): +class OrderViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + GenericViewSet, +): queryset = Order.objects.prefetch_related( - "tickets__movie_session__movie", "tickets__movie_session__cinema_hall" + "tickets__movie_session__movie", + "tickets__movie_session__cinema_hall" ) serializer_class = OrderSerializer pagination_class = OrderPagination - def get_queryset(self): - return Order.objects.filter(user=self.request.user) + authentication_classes = (TokenAuthentication,) + permission_classes = [IsAdminOrIfAuthenticatedReadOnly] - def get_serializer_class(self): - if self.action == "list": - return OrderListSerializer + def get_permissions(self): + if self.action == "create": + return [IsAuthenticated()] + return super().get_permissions() - return OrderSerializer - - def perform_create(self, serializer): - serializer.save(user=self.request.user) + 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..0b58da46 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -11,6 +11,8 @@ """ from pathlib import Path +from rest_framework.views import exception_handler + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -25,7 +27,7 @@ ) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False ALLOWED_HOSTS = [] @@ -125,7 +127,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) @@ -137,3 +139,14 @@ # 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", + "rest_framework.authentication.SessionAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], + "EXCEPTION_HANDLER": "rest_framework.views.exception_handler", +} diff --git a/cinema_service/urls.py b/cinema_service/urls.py index bf903c00..a0af4e0f 100644 --- a/cinema_service/urls.py +++ b/cinema_service/urls.py @@ -1,8 +1,11 @@ from django.contrib import admin from django.urls import path, include +from rest_framework.authtoken import views urlpatterns = [ path("admin/", admin.site.urls), + path("api/token-auth/", views.obtain_auth_token, name="token-auth"), path("api/cinema/", include("cinema.urls", namespace="cinema")), + path("api/user/", include("user.urls")), path("__debug__/", include("debug_toolbar.urls")), ] diff --git a/user/permissions.py b/user/permissions.py new file mode 100644 index 00000000..8a428069 --- /dev/null +++ b/user/permissions.py @@ -0,0 +1,13 @@ +from rest_framework.permissions import BasePermission, SAFE_METHODS +from rest_framework.exceptions import NotAuthenticated + + +class IsAdminOrIfAuthenticatedReadOnly(BasePermission): + def has_permission(self, request, view): + if not request.user.is_authenticated: + raise NotAuthenticated() + + if request.method in SAFE_METHODS and request.user.is_authenticated: + return True + + return request.user.is_staff diff --git a/user/serializers.py b/user/serializers.py index fa56336e..ac522049 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1 +1,61 @@ -# write your code here +from django.contrib.auth import get_user_model +from django.contrib.auth.password_validation import validate_password +from rest_framework import serializers + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ("id", "username", "email", "password", "is_staff") + read_only_fields = ("id", "is_staff") + extra_kwargs = { + "password": {"write_only": True, "min_length": 5} + } + + def create(self, validated_data): + user = get_user_model().objects.create_user(**validated_data) + return user + + def update(self, instance, validated_data): + password = validated_data.pop("password", None) + user = super().update(instance, validated_data) + + if password: + validate_password(password, user) # Validate the new password + user.set_password(password) + user.save() + + return user + + +class AuthTokenSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField( + style={"input_type": "password"}, trim_whitespace=False + ) + + def validate(self, attrs): + from django.contrib.auth import authenticate + + username = attrs.get("username") + password = attrs.get("password") + + if username and password: + user = authenticate( + request=self.context.get("request"), + username=username, + password=password + ) + if not user: + raise serializers.ValidationError( + "Unable to log in with provided credentials.", + code="authorization", + ) + else: + raise serializers.ValidationError( + "Must include 'username' and 'password'.", + code="authorization", + ) + + attrs["user"] = user + return attrs diff --git a/user/urls.py b/user/urls.py index fa56336e..3bbcc24c 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, + CreateTokenView, + ManageUserView +) + +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..9856c215 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, AuthTokenSerializer + + +class CreateUserView(generics.CreateAPIView): + serializer_class = UserSerializer + + +class CreateTokenView(ObtainAuthToken): + serializer_class = AuthTokenSerializer + 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