diff --git a/cinema/models.py b/cinema/models.py index e9b22ecd..8cdb7cb4 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) @@ -76,11 +75,11 @@ class Meta: 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" + MovieSession, 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 +121,10 @@ 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..84ab6b14 --- /dev/null +++ b/cinema/permissions.py @@ -0,0 +1,8 @@ +from rest_framework import permissions + + +class IsAdminOrIfAuthenticatedReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.method in permissions.SAFE_METHODS: + return True + return request.user and request.user.is_staff diff --git a/cinema/serializers.py b/cinema/serializers.py index 7a6ae20c..d9e1fe7e 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" ) @@ -62,9 +62,8 @@ 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 - ) + 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 +84,9 @@ 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: @@ -108,9 +107,9 @@ 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..adc0e005 100644 --- a/cinema/views.py +++ b/cinema/views.py @@ -1,10 +1,13 @@ from datetime import datetime +import pytz from django.db.models import F, Count -from rest_framework import viewsets +from rest_framework import viewsets, mixins from rest_framework.pagination import PageNumberPagination +from rest_framework.permissions import IsAuthenticated, IsAdminUser from cinema.models import Genre, Actor, CinemaHall, Movie, MovieSession, Order +from cinema.permissions import IsAdminOrIfAuthenticatedReadOnly from cinema.serializers import ( GenreSerializer, @@ -21,32 +24,70 @@ ) -class GenreViewSet(viewsets.ModelViewSet): +class GenreViewSet( + mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet +): queryset = Genre.objects.all() serializer_class = GenreSerializer + def get_permissions(self): + if self.action == "create": + self.permission_classes = [IsAdminUser] + else: + self.permission_classes = [IsAuthenticated] + return super().get_permissions() -class ActorViewSet(viewsets.ModelViewSet): + +class ActorViewSet( + mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet +): queryset = Actor.objects.all() serializer_class = ActorSerializer + def get_permissions(self): + if self.action == "create": + self.permission_classes = [IsAdminUser] + else: + self.permission_classes = [IsAuthenticated] + return super().get_permissions() + -class CinemaHallViewSet(viewsets.ModelViewSet): +class CinemaHallViewSet( + mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet +): queryset = CinemaHall.objects.all() serializer_class = CinemaHallSerializer + def get_permissions(self): + if self.action == "create": + self.permission_classes = [IsAdminUser] + else: + self.permission_classes = [IsAuthenticated] + return super().get_permissions() + -class MovieViewSet(viewsets.ModelViewSet): +class MovieViewSet( + mixins.ListModelMixin, + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): queryset = Movie.objects.prefetch_related("genres", "actors") serializer_class = MovieSerializer + def get_permissions(self): + if self.action == "create": + self.permission_classes = [IsAdminUser] + else: + self.permission_classes = [IsAuthenticated] + return super().get_permissions() + @staticmethod def _params_to_ints(qs): - """Converts a list of string IDs to a list of integers""" + """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") @@ -69,10 +110,8 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return MovieListSerializer - if self.action == "retrieve": return MovieDetailSerializer - return MovieSerializer @@ -88,6 +127,13 @@ class MovieSessionViewSet(viewsets.ModelViewSet): ) serializer_class = MovieSessionSerializer + def get_permissions(self): + if self.action in ["create", "update", "partial_update", "destroy"]: + self.permission_classes = [IsAdminUser] + else: + self.permission_classes = [IsAuthenticated] + return super().get_permissions() + def get_queryset(self): date = self.request.query_params.get("date") movie_id_str = self.request.query_params.get("movie") @@ -95,7 +141,7 @@ def get_queryset(self): queryset = self.queryset if date: - date = datetime.strptime(date, "%Y-%m-%d").date() + date = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=pytz.UTC) queryset = queryset.filter(show_time__date=date) if movie_id_str: @@ -106,10 +152,8 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return MovieSessionListSerializer - if self.action == "retrieve": return MovieSessionDetailSerializer - return MovieSessionSerializer @@ -118,12 +162,15 @@ class OrderPagination(PageNumberPagination): max_page_size = 100 -class OrderViewSet(viewsets.ModelViewSet): +class OrderViewSet( + mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet +): queryset = Order.objects.prefetch_related( "tickets__movie_session__movie", "tickets__movie_session__cinema_hall" ) serializer_class = OrderSerializer pagination_class = OrderPagination + permission_classes = [IsAuthenticated] def get_queryset(self): return Order.objects.filter(user=self.request.user) @@ -131,7 +178,6 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return OrderListSerializer - return OrderSerializer def perform_create(self, serializer): diff --git a/cinema_service/settings.py b/cinema_service/settings.py index 29ea7dea..1779cf48 100644 --- a/cinema_service/settings.py +++ b/cinema_service/settings.py @@ -20,9 +20,8 @@ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = ( +SECRET_KEY = \ "django-insecure-6vubhk2$++agnctay_4pxy_8cq)mosmn(*-#2b^v4cgsh-^!i3" -) # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -102,15 +101,15 @@ }, { "NAME": "django.contrib.auth.password_validation." - "MinimumLengthValidator", + "MinimumLengthValidator", }, { "NAME": "django.contrib.auth.password_validation." - "CommonPasswordValidator", + "CommonPasswordValidator", }, { "NAME": "django.contrib.auth.password_validation." - "NumericPasswordValidator", + "NumericPasswordValidator", }, ] @@ -125,7 +124,7 @@ USE_I18N = True -USE_TZ = False +USE_TZ = True # Static files (CSS, JavaScript, Images) @@ -137,3 +136,13 @@ # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + "rest_framework.authentication.SessionAuthentication", + ], +} 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..be9e14e2 100644 --- a/user/serializers.py +++ b/user/serializers.py @@ -1 +1,47 @@ -# write your code here +from django.contrib.auth import get_user_model, authenticate +from django.utils.translation import gettext as _ +from rest_framework import serializers + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = get_user_model() + fields = ("id", "username", "email", "password", "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 + + +class AuthTokenSerializer(serializers.Serializer): + username = serializers.CharField() + password = serializers.CharField( + style={"input_type": "password"}, trim_whitespace=False + ) + + def validate(self, attrs): + username = attrs.get("username") + password = attrs.get("password") + + user = authenticate( + request=self.context.get("request"), + username=username, + password=password + ) + + if not user: + raise serializers.ValidationError( + _("Unable to authenticate with provided credentials."), + code="authentication", + ) + + attrs["user"] = user + return attrs 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..84fcfe0a 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1 +1,10 @@ -# write your code here +from django.urls import path +from .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..23943709 100644 --- a/user/views.py +++ b/user/views.py @@ -1 +1,24 @@ -# write your code here +from rest_framework import generics, permissions +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.authtoken.models import Token +from rest_framework.response import Response + +from .serializers import UserSerializer, AuthTokenSerializer + + +class CreateUserView(generics.CreateAPIView): + serializer_class = UserSerializer + permission_classes = [permissions.AllowAny] + + +class CreateTokenView(ObtainAuthToken): + serializer_class = AuthTokenSerializer + permission_classes = [permissions.AllowAny] + + +class ManageUserView(generics.RetrieveUpdateAPIView): + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_object(self): + return self.request.user