diff --git a/djoser/urls/authtoken.py b/djoser/urls/authtoken.py index ccfc1d82..18ada330 100644 --- a/djoser/urls/authtoken.py +++ b/djoser/urls/authtoken.py @@ -1,8 +1,9 @@ -from django.urls import re_path +from django.urls import path -from djoser import views +from djoser.views.token.create import TokenCreateView +from djoser.views.token.destroy import TokenDestroyView urlpatterns = [ - re_path(r"^token/login/?$", views.TokenCreateView.as_view(), name="login"), - re_path(r"^token/logout/?$", views.TokenDestroyView.as_view(), name="logout"), + path("token/login/", TokenCreateView.as_view(), name="login"), + path("token/logout/", TokenDestroyView.as_view(), name="logout"), ] diff --git a/djoser/urls/base.py b/djoser/urls/base.py index 44afa38e..347212eb 100644 --- a/djoser/urls/base.py +++ b/djoser/urls/base.py @@ -1,11 +1,108 @@ from django.contrib.auth import get_user_model -from rest_framework.routers import DefaultRouter +from django.urls import path -from djoser import views - -router = DefaultRouter() -router.register("users", views.UserViewSet) +from djoser.views.activation import ( + UserActivationAPIView, + UserResendActivationAPIView, +) +from djoser.views.me import UserMeAPIView +from djoser.views.password import ( + ResetPasswordConfirmViewAPIView, + ResetPasswordViewAPIView, + SetPasswordViewAPIView, +) +from djoser.views.user import UserViewSet +from djoser.views.username import ( + ResetUsernameAPIView, + ResetUsernameConfirmAPIView, + SetUsernameAPIView, +) User = get_user_model() -urlpatterns = router.urls +# user +user_list = path( + "users/", UserViewSet.as_view({"get": "list", "post": "create"}), name="user-list" +) +user_detail = path( + f"users/<{UserViewSet.lookup_field}>/", + UserViewSet.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="user-detail", +) + +# me +me_list = path( + "users/me/", + UserMeAPIView.as_view( + { + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="user-me", +) + +# activation +user_activation = path( + "users/activation/", UserActivationAPIView.as_view(), name="user-activation" +) +user_resend_activation = path( + "users/resend-activation/", + UserResendActivationAPIView.as_view(), + name="user-resend-activation", +) + +# password +user_password_reset_confirm = path( + "users/reset_password_confirm/", + ResetPasswordConfirmViewAPIView.as_view(), + name="user-reset-password-confirm", +) +user_reset_password = path( + "users/reset_password", + ResetPasswordViewAPIView.as_view(), + name="user-reset-password", +) +user_set_password = path( + "users/set_password", SetPasswordViewAPIView.as_view(), name="user-set-password" +) + +# username +user_reset_username = path( + f"users/reset_{User.USERNAME_FIELD}", + ResetUsernameAPIView.as_view(), + name="user-reset-username", +) +user_reset_username_confirm = path( + f"users/reset_{User.USERNAME_FIELD}_confirm", + ResetUsernameConfirmAPIView.as_view(), + name="user-reset-username-confirm", +) +user_set_username = path( + f"users/set_{User.USERNAME_FIELD}", + SetUsernameAPIView.as_view(), + name="user-set-username", +) + +urlpatterns = [ + user_resend_activation, + user_activation, + user_password_reset_confirm, + user_reset_username_confirm, + user_reset_password, + user_set_password, + user_set_username, + user_reset_username, + me_list, + user_detail, + user_list, +] diff --git a/djoser/urls/jwt.py b/djoser/urls/jwt.py index 474411f1..f43728a3 100644 --- a/djoser/urls/jwt.py +++ b/djoser/urls/jwt.py @@ -1,8 +1,8 @@ -from django.urls import re_path +from django.urls import path from rest_framework_simplejwt import views urlpatterns = [ - re_path(r"^jwt/create/?", views.TokenObtainPairView.as_view(), name="jwt-create"), - re_path(r"^jwt/refresh/?", views.TokenRefreshView.as_view(), name="jwt-refresh"), - re_path(r"^jwt/verify/?", views.TokenVerifyView.as_view(), name="jwt-verify"), + path("jwt/create/", views.TokenObtainPairView.as_view(), name="jwt-create"), + path("jwt/refresh/", views.TokenRefreshView.as_view(), name="jwt-refresh"), + path("jwt/verify/", views.TokenVerifyView.as_view(), name="jwt-verify"), ] diff --git a/djoser/views.py b/djoser/views.py deleted file mode 100644 index 09b9e46a..00000000 --- a/djoser/views.py +++ /dev/null @@ -1,307 +0,0 @@ -from django.contrib.auth import get_user_model, update_session_auth_hash -from django.contrib.auth.tokens import default_token_generator -from django.utils.timezone import now -from rest_framework import generics, status, views, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import NotFound -from rest_framework.response import Response -from rest_framework.serializers import Serializer - -from djoser import signals, utils -from djoser.compat import get_user_email -from djoser.conf import settings - -User = get_user_model() - - -class TokenCreateView(utils.ActionViewMixin, generics.GenericAPIView): - """Use this endpoint to obtain user authentication token.""" - - serializer_class = settings.SERIALIZERS.token_create - permission_classes = settings.PERMISSIONS.token_create - - def _action(self, serializer): - token = utils.login_user(self.request, serializer.user) - token_serializer_class = settings.SERIALIZERS.token - return Response( - data=token_serializer_class(token).data, status=status.HTTP_200_OK - ) - - -class TokenDestroyView(views.APIView): - """Use this endpoint to logout user (remove user authentication token).""" - - serializer_class = Serializer - permission_classes = settings.PERMISSIONS.token_destroy - - def post(self, request): - utils.logout_user(request) - return Response(status=status.HTTP_204_NO_CONTENT) - - -class UserViewSet(viewsets.ModelViewSet): - serializer_class = settings.SERIALIZERS.user - queryset = User.objects.all() - permission_classes = settings.PERMISSIONS.user - token_generator = default_token_generator - lookup_field = settings.USER_ID_FIELD - - def permission_denied(self, request, **kwargs): - if ( - settings.HIDE_USERS - and request.user.is_authenticated - and self.action in ["update", "partial_update", "list", "retrieve"] - ): - raise NotFound() - super().permission_denied(request, **kwargs) - - def get_queryset(self): - user = self.request.user - queryset = super().get_queryset() - if settings.HIDE_USERS and self.action == "list" and not user.is_staff: - queryset = queryset.filter(pk=user.pk) - return queryset - - def get_permissions(self): - if self.action == "create": - self.permission_classes = settings.PERMISSIONS.user_create - elif self.action == "activation": - self.permission_classes = settings.PERMISSIONS.activation - elif self.action == "resend_activation": - self.permission_classes = settings.PERMISSIONS.password_reset - elif self.action == "list": - self.permission_classes = settings.PERMISSIONS.user_list - elif self.action == "reset_password": - self.permission_classes = settings.PERMISSIONS.password_reset - elif self.action == "reset_password_confirm": - self.permission_classes = settings.PERMISSIONS.password_reset_confirm - elif self.action == "set_password": - self.permission_classes = settings.PERMISSIONS.set_password - elif self.action == "set_username": - self.permission_classes = settings.PERMISSIONS.set_username - elif self.action == "reset_username": - self.permission_classes = settings.PERMISSIONS.username_reset - elif self.action == "reset_username_confirm": - self.permission_classes = settings.PERMISSIONS.username_reset_confirm - elif self.action == "destroy" or ( - self.action == "me" and self.request and self.request.method == "DELETE" - ): - self.permission_classes = settings.PERMISSIONS.user_delete - return super().get_permissions() - - def get_serializer_class(self): - if self.action == "create": - if settings.USER_CREATE_PASSWORD_RETYPE: - return settings.SERIALIZERS.user_create_password_retype - return settings.SERIALIZERS.user_create - elif self.action == "destroy" or ( - self.action == "me" and self.request and self.request.method == "DELETE" - ): - return settings.SERIALIZERS.user_delete - elif self.action == "activation": - return settings.SERIALIZERS.activation - elif self.action == "resend_activation": - return settings.SERIALIZERS.password_reset - elif self.action == "reset_password": - return settings.SERIALIZERS.password_reset - elif self.action == "reset_password_confirm": - if settings.PASSWORD_RESET_CONFIRM_RETYPE: - return settings.SERIALIZERS.password_reset_confirm_retype - return settings.SERIALIZERS.password_reset_confirm - elif self.action == "set_password": - if settings.SET_PASSWORD_RETYPE: - return settings.SERIALIZERS.set_password_retype - return settings.SERIALIZERS.set_password - elif self.action == "set_username": - if settings.SET_USERNAME_RETYPE: - return settings.SERIALIZERS.set_username_retype - return settings.SERIALIZERS.set_username - elif self.action == "reset_username": - return settings.SERIALIZERS.username_reset - elif self.action == "reset_username_confirm": - if settings.USERNAME_RESET_CONFIRM_RETYPE: - return settings.SERIALIZERS.username_reset_confirm_retype - return settings.SERIALIZERS.username_reset_confirm - elif self.action == "me": - return settings.SERIALIZERS.current_user - - return self.serializer_class - - def get_instance(self): - return self.request.user - - def perform_create(self, serializer, *args, **kwargs): - user = serializer.save(*args, **kwargs) - signals.user_registered.send( - sender=self.__class__, user=user, request=self.request - ) - - context = {"user": user} - to = [get_user_email(user)] - if settings.SEND_ACTIVATION_EMAIL: - settings.EMAIL.activation(self.request, context).send(to) - elif settings.SEND_CONFIRMATION_EMAIL: - settings.EMAIL.confirmation(self.request, context).send(to) - - def perform_update(self, serializer, *args, **kwargs): - super().perform_update(serializer, *args, **kwargs) - user = serializer.instance - signals.user_updated.send( - sender=self.__class__, user=user, request=self.request - ) - - # should we send activation email after update? - if settings.SEND_ACTIVATION_EMAIL and not user.is_active: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.activation(self.request, context).send(to) - - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data) - serializer.is_valid(raise_exception=True) - - if instance == request.user: - utils.logout_user(self.request) - self.perform_destroy(instance) - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["get", "put", "patch", "delete"], detail=False) - def me(self, request, *args, **kwargs): - self.get_object = self.get_instance - if request.method == "GET": - return self.retrieve(request, *args, **kwargs) - elif request.method == "PUT": - return self.update(request, *args, **kwargs) - elif request.method == "PATCH": - return self.partial_update(request, *args, **kwargs) - elif request.method == "DELETE": - return self.destroy(request, *args, **kwargs) - - @action(["post"], detail=False) - def activation(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.user - user.is_active = True - user.save() - - signals.user_activated.send( - sender=self.__class__, user=user, request=self.request - ) - - if settings.SEND_CONFIRMATION_EMAIL: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.confirmation(self.request, context).send(to) - - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False) - def resend_activation(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.get_user(is_active=False) - - if not settings.SEND_ACTIVATION_EMAIL: - return Response(status=status.HTTP_400_BAD_REQUEST) - - if user: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.activation(self.request, context).send(to) - - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False) - def set_password(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - self.request.user.set_password(serializer.data["new_password"]) - self.request.user.save() - - if settings.PASSWORD_CHANGED_EMAIL_CONFIRMATION: - context = {"user": self.request.user} - to = [get_user_email(self.request.user)] - settings.EMAIL.password_changed_confirmation(self.request, context).send(to) - - if settings.LOGOUT_ON_PASSWORD_CHANGE: - utils.logout_user(self.request) - elif settings.CREATE_SESSION_ON_LOGIN: - update_session_auth_hash(self.request, self.request.user) - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False) - def reset_password(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.get_user() - - if user: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.password_reset(self.request, context).send(to) - - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False) - def reset_password_confirm(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - serializer.user.set_password(serializer.data["new_password"]) - if hasattr(serializer.user, "last_login"): - serializer.user.last_login = now() - serializer.user.save() - - if settings.PASSWORD_CHANGED_EMAIL_CONFIRMATION: - context = {"user": serializer.user} - to = [get_user_email(serializer.user)] - settings.EMAIL.password_changed_confirmation(self.request, context).send(to) - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False, url_path=f"set_{User.USERNAME_FIELD}") - def set_username(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = self.request.user - new_username = serializer.data["new_" + User.USERNAME_FIELD] - - setattr(user, User.USERNAME_FIELD, new_username) - user.save() - if settings.USERNAME_CHANGED_EMAIL_CONFIRMATION: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.username_changed_confirmation(self.request, context).send(to) - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False, url_path=f"reset_{User.USERNAME_FIELD}") - def reset_username(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = serializer.get_user() - - if user: - context = {"user": user} - to = [get_user_email(user)] - settings.EMAIL.username_reset(self.request, context).send(to) - - return Response(status=status.HTTP_204_NO_CONTENT) - - @action(["post"], detail=False, url_path=f"reset_{User.USERNAME_FIELD}_confirm") - def reset_username_confirm(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - new_username = serializer.data["new_" + User.USERNAME_FIELD] - - setattr(serializer.user, User.USERNAME_FIELD, new_username) - if hasattr(serializer.user, "last_login"): - serializer.user.last_login = now() - serializer.user.save() - - if settings.USERNAME_CHANGED_EMAIL_CONFIRMATION: - context = {"user": serializer.user} - to = [get_user_email(serializer.user)] - settings.EMAIL.username_changed_confirmation(self.request, context).send(to) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djoser/views/__init__.py b/djoser/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djoser/views/activation.py b/djoser/views/activation.py new file mode 100644 index 00000000..dc2e3638 --- /dev/null +++ b/djoser/views/activation.py @@ -0,0 +1,55 @@ +from django.contrib.auth import get_user_model +from rest_framework import status +from rest_framework.response import Response + +from djoser import signals +from djoser.conf import settings +from djoser.compat import get_user_email +from djoser.views.base import GenericUserAPIView + +User = get_user_model() + + +class UserActivationAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.activation + serializer_class = settings.SERIALIZERS.activation + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.user + user.is_active = True + user.save() + + signals.user_activated.send( + sender=self.__class__, user=user, request=self.request + ) + + if settings.SEND_CONFIRMATION_EMAIL: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.confirmation(self.request, context).send(to) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class UserResendActivationAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.password_reset + serializer_class = settings.SERIALIZERS.password_reset + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.get_user(is_active=False) + + if not settings.SEND_ACTIVATION_EMAIL: + return Response(status=status.HTTP_400_BAD_REQUEST) + + if user: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.activation(self.request, context).send(to) + + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djoser/views/base.py b/djoser/views/base.py new file mode 100644 index 00000000..bb37d72c --- /dev/null +++ b/djoser/views/base.py @@ -0,0 +1,14 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.tokens import default_token_generator +from rest_framework import generics +from djoser.conf import settings + + +User = get_user_model() + + +class GenericUserAPIView(generics.GenericAPIView): + queryset = User.objects.all() + lookup_field = settings.USER_ID_FIELD + token_generator = default_token_generator # used in serializers + http_method_names = [] diff --git a/djoser/views/me.py b/djoser/views/me.py new file mode 100644 index 00000000..e8f93eb5 --- /dev/null +++ b/djoser/views/me.py @@ -0,0 +1,66 @@ +from django.contrib.auth import get_user_model +from rest_framework import status, mixins +from rest_framework.exceptions import NotFound +from rest_framework.response import Response +from rest_framework.viewsets import ViewSetMixin + +from djoser import signals, utils +from djoser.conf import settings +from djoser.compat import get_user_email +from djoser.views.base import GenericUserAPIView + +User = get_user_model() + + +class UserMeAPIView( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + ViewSetMixin, + GenericUserAPIView, +): + http_method_names = ["get", "put", "patch", "delete"] + permission_classes = settings.PERMISSIONS.user + lookup_field = None + + def get_queryset(self): + # probably redundant but better safe than sorry + queryset = self.queryset.objects.all() + return queryset.filter(pk=self.request.user.pk) + + def get_serializer_class(self): + if self.request.method == "DELETE": + return settings.SERIALIZERS.user_delete + return settings.SERIALIZERS.current_user + + def get_object(self): + if settings.HIDE_USERS and not self.request.user.is_authenticated: + raise NotFound() + return self.request.user + + def perform_update(self, serializer): + super().perform_update(serializer) + user = serializer.instance + signals.user_updated.send( + sender=self.__class__, user=user, request=self.request + ) + + if settings.SEND_ACTIVATION_EMAIL and not user.is_active: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.activation(self.request, context).send(to) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + + if instance == request.user: + utils.logout_user(self.request) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + def permission_denied(self, request, **kwargs): + if settings.HIDE_USERS and request.user.is_authenticated: + raise NotFound() + super().permission_denied(request, **kwargs) diff --git a/djoser/views/password.py b/djoser/views/password.py new file mode 100644 index 00000000..5ac0bf68 --- /dev/null +++ b/djoser/views/password.py @@ -0,0 +1,82 @@ +from django.contrib.auth import get_user_model, update_session_auth_hash +from django.utils.timezone import now +from rest_framework import status +from rest_framework.response import Response + +from djoser import utils +from djoser.conf import settings +from djoser.compat import get_user_email +from djoser.views.base import GenericUserAPIView + +User = get_user_model() + + +class SetPasswordViewAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.set_password + http_method_names = ["post"] + + def get_serializer_class(self): + if settings.SET_PASSWORD_RETYPE: + return settings.SERIALIZERS.set_password_retype + return settings.SERIALIZERS.set_password + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + self.request.user.set_password(serializer.data["new_password"]) + self.request.user.save() + + if settings.PASSWORD_CHANGED_EMAIL_CONFIRMATION: + context = {"user": self.request.user} + to = [get_user_email(self.request.user)] + settings.EMAIL.password_changed_confirmation(self.request, context).send(to) + + if settings.LOGOUT_ON_PASSWORD_CHANGE: + utils.logout_user(self.request) + elif settings.CREATE_SESSION_ON_LOGIN: + update_session_auth_hash(self.request, self.request.user) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ResetPasswordViewAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.password_reset + serializer_class = settings.SERIALIZERS.password_reset + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.get_user() + + if user: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.password_reset(self.request, context).send(to) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ResetPasswordConfirmViewAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.password_reset_confirm + http_method_names = ["post"] + + def get_serializer_class(self): + if settings.PASSWORD_RESET_CONFIRM_RETYPE: + return settings.SERIALIZERS.password_reset_confirm_retype + return settings.SERIALIZERS.password_reset_confirm + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + serializer.user.set_password(serializer.data["new_password"]) + if hasattr(serializer.user, "last_login"): + serializer.user.last_login = now() + serializer.user.save() + + if settings.PASSWORD_CHANGED_EMAIL_CONFIRMATION: + context = {"user": serializer.user} + to = [get_user_email(serializer.user)] + settings.EMAIL.password_changed_confirmation(self.request, context).send(to) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djoser/views/token/__init__.py b/djoser/views/token/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djoser/views/token/create.py b/djoser/views/token/create.py new file mode 100644 index 00000000..3cbc63e5 --- /dev/null +++ b/djoser/views/token/create.py @@ -0,0 +1,20 @@ +from rest_framework import generics, status +from rest_framework.response import Response + +from djoser import utils +from djoser.conf import settings + + +class TokenCreateView(utils.ActionViewMixin, generics.GenericAPIView): + """Use this endpoint to obtain user authentication token.""" + + serializer_class = settings.SERIALIZERS.token_create + permission_classes = settings.PERMISSIONS.token_create + queryset = settings.TOKEN_MODEL.objects.none() + + def _action(self, serializer): + token = utils.login_user(self.request, serializer.user) + token_serializer_class = settings.SERIALIZERS.token + return Response( + data=token_serializer_class(token).data, status=status.HTTP_200_OK + ) diff --git a/djoser/views/token/destroy.py b/djoser/views/token/destroy.py new file mode 100644 index 00000000..82481ca2 --- /dev/null +++ b/djoser/views/token/destroy.py @@ -0,0 +1,16 @@ +from rest_framework import status, generics, serializers +from rest_framework.response import Response + +from djoser import utils +from djoser.conf import settings + + +class TokenDestroyView(generics.GenericAPIView): + """Use this endpoint to logout user (remove user authentication token).""" + + serializer_class = serializers.Serializer + permission_classes = settings.PERMISSIONS.token_destroy + + def post(self, request): + utils.logout_user(request) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/djoser/views/user.py b/djoser/views/user.py new file mode 100644 index 00000000..90e6bcc6 --- /dev/null +++ b/djoser/views/user.py @@ -0,0 +1,227 @@ +from django.contrib.auth import get_user_model +from rest_framework import status, mixins, generics, viewsets +from rest_framework.exceptions import NotFound +from rest_framework.response import Response +from rest_framework.viewsets import ViewSetMixin + +from djoser import signals, utils +from djoser.conf import settings +from djoser.compat import get_user_email + +from djoser.views.base import GenericUserAPIView + +User = get_user_model() + + +class UserViewSet( + mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + ViewSetMixin, + GenericUserAPIView, +): + serializer_class = settings.SERIALIZERS.user + permission_classes = settings.PERMISSIONS.user + http_method_names = ["get", "post", "path", "put", "delete"] + + def permission_denied(self, request, **kwargs): + if ( + settings.HIDE_USERS + and request.user.is_authenticated + and self.action in ["update", "partial_update", "list", "retrieve"] + ): + raise NotFound() + super().permission_denied(request, **kwargs) + + def get_queryset(self): + user = self.request.user + queryset = super().get_queryset() + if settings.HIDE_USERS and self.action == "list" and not user.is_staff: + queryset = queryset.filter(pk=user.pk) + return queryset + + def get_permissions(self): + if self.action == "create": + self.permission_classes = settings.PERMISSIONS.user_create + elif self.action == "list": + self.permission_classes = settings.PERMISSIONS.user_list + elif self.action == "destroy": + self.permission_classes = settings.PERMISSIONS.user_delete + return super().get_permissions() + + def get_serializer_class(self): + if self.action == "create": + if settings.USER_CREATE_PASSWORD_RETYPE: + serializer_class = settings.SERIALIZERS.user_create_password_retype + else: + serializer_class = settings.SERIALIZERS.user_create + elif self.action == "destroy": + serializer_class = settings.SERIALIZERS.user_delete + else: + serializer_class = self.serializer_class + return serializer_class + + def perform_create(self, serializer, *args, **kwargs): + user = serializer.save(*args, **kwargs) + signals.user_registered.send( + sender=self.__class__, user=user, request=self.request + ) + + context = {"user": user} + to = [get_user_email(user)] + if settings.SEND_ACTIVATION_EMAIL: + settings.EMAIL.activation(self.request, context).send(to) + elif settings.SEND_CONFIRMATION_EMAIL: + settings.EMAIL.confirmation(self.request, context).send(to) + + def perform_update(self, serializer, *args, **kwargs): + super().perform_update(serializer, *args, **kwargs) + user = serializer.instance + signals.user_updated.send( + sender=self.__class__, user=user, request=self.request + ) + + # should we send activation email after update? + if settings.SEND_ACTIVATION_EMAIL and not user.is_active: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.activation(self.request, context).send(to) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + + if instance == request.user: + utils.logout_user(self.request) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class UserBaseView(GenericUserAPIView): + """Base view for user views with common methods.""" + serializer_class = settings.SERIALIZERS.user + + def permission_denied(self, request, **kwargs): + action = getattr(self, 'action', None) + if ( + settings.HIDE_USERS + and request.user.is_authenticated + and action in ["update", "partial_update", "list", "retrieve"] + ): + raise NotFound() + super().permission_denied(request, **kwargs) + + def get_queryset(self): + user = self.request.user + queryset = super().get_queryset() + action = getattr(self, 'action', None) + if settings.HIDE_USERS and action == "list" and not user.is_staff: + queryset = queryset.filter(pk=user.pk) + return queryset + + +class UserListView(UserBaseView, generics.ListAPIView): + """View for listing users.""" + permission_classes = settings.PERMISSIONS.user_list + http_method_names = ["get"] + + +class UserCreateView(UserBaseView, generics.CreateAPIView): + """View for creating a user.""" + permission_classes = settings.PERMISSIONS.user_create + http_method_names = ["post"] + + def get_serializer_class(self): + if settings.USER_CREATE_PASSWORD_RETYPE: + return settings.SERIALIZERS.user_create_password_retype + return settings.SERIALIZERS.user_create + + def perform_create(self, serializer, *args, **kwargs): + user = serializer.save(*args, **kwargs) + signals.user_registered.send( + sender=self.__class__, user=user, request=self.request + ) + + context = {"user": user} + to = [get_user_email(user)] + if settings.SEND_ACTIVATION_EMAIL: + settings.EMAIL.activation(self.request, context).send(to) + elif settings.SEND_CONFIRMATION_EMAIL: + settings.EMAIL.confirmation(self.request, context).send(to) + + +class UserListCreateView(UserBaseView, generics.ListCreateAPIView): + """View for listing and creating users.""" + + def get_permissions(self): + if self.request.method == 'POST': + self.permission_classes = settings.PERMISSIONS.user_create + else: + self.permission_classes = settings.PERMISSIONS.user_list + return super().get_permissions() + + def get_serializer_class(self): + if self.request.method == 'POST': + if settings.USER_CREATE_PASSWORD_RETYPE: + return settings.SERIALIZERS.user_create_password_retype + return settings.SERIALIZERS.user_create + return self.serializer_class + + def perform_create(self, serializer, *args, **kwargs): + user = serializer.save(*args, **kwargs) + signals.user_registered.send( + sender=self.__class__, user=user, request=self.request + ) + + context = {"user": user} + to = [get_user_email(user)] + if settings.SEND_ACTIVATION_EMAIL: + settings.EMAIL.activation(self.request, context).send(to) + elif settings.SEND_CONFIRMATION_EMAIL: + settings.EMAIL.confirmation(self.request, context).send(to) + + +class UserRetrieveView(UserBaseView, generics.RetrieveAPIView): + """View for retrieving a user.""" + permission_classes = settings.PERMISSIONS.user + http_method_names = ["get"] + + +class UserUpdateView(UserBaseView, generics.UpdateAPIView): + """View for updating a user.""" + permission_classes = settings.PERMISSIONS.user + http_method_names = ["put", "patch"] + + def perform_update(self, serializer, *args, **kwargs): + super().perform_update(serializer, *args, **kwargs) + user = serializer.instance + signals.user_updated.send( + sender=self.__class__, user=user, request=self.request + ) + + # should we send activation email after update? + if settings.SEND_ACTIVATION_EMAIL and not user.is_active: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.activation(self.request, context).send(to) + + +class UserDeleteView(UserBaseView, generics.DestroyAPIView): + """View for deleting a user.""" + permission_classes = settings.PERMISSIONS.user_delete + http_method_names = ["delete"] + + def get_serializer_class(self): + return settings.SERIALIZERS.user_delete + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data) + serializer.is_valid(raise_exception=True) + + if instance == request.user: + utils.logout_user(self.request) + self.perform_destroy(instance) diff --git a/djoser/views/username.py b/djoser/views/username.py new file mode 100644 index 00000000..b8372493 --- /dev/null +++ b/djoser/views/username.py @@ -0,0 +1,79 @@ +from django.contrib.auth import get_user_model +from django.utils.timezone import now +from rest_framework import status +from rest_framework.response import Response + +from djoser.conf import settings +from djoser.compat import get_user_email +from djoser.views.base import GenericUserAPIView + +User = get_user_model() + + +class SetUsernameAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.set_username + http_method_names = ["post"] + + def get_serializer_class(self): + if settings.SET_USERNAME_RETYPE: + return settings.SERIALIZERS.set_username_retype + return settings.SERIALIZERS.set_username + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.request.user + new_username = serializer.data["new_" + User.USERNAME_FIELD] + + setattr(user, User.USERNAME_FIELD, new_username) + user.save() + + if settings.USERNAME_CHANGED_EMAIL_CONFIRMATION: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.username_changed_confirmation(self.request, context).send(to) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ResetUsernameAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.username_reset + serializer_class = settings.SERIALIZERS.username_reset + http_method_names = ["post"] + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = serializer.get_user() + + if user: + context = {"user": user} + to = [get_user_email(user)] + settings.EMAIL.username_reset(self.request, context).send(to) + + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ResetUsernameConfirmAPIView(GenericUserAPIView): + permission_classes = settings.PERMISSIONS.username_reset_confirm + http_method_names = ["post"] + + def get_serializer_class(self): + if settings.USERNAME_RESET_CONFIRM_RETYPE: + return settings.SERIALIZERS.username_reset_confirm_retype + return settings.SERIALIZERS.username_reset_confirm + + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + new_username = serializer.data["new_" + User.USERNAME_FIELD] + + setattr(serializer.user, User.USERNAME_FIELD, new_username) + if hasattr(serializer.user, "last_login"): + serializer.user.last_login = now() + serializer.user.save() + + if settings.USERNAME_CHANGED_EMAIL_CONFIRMATION: + context = {"user": serializer.user} + to = [get_user_email(serializer.user)] + settings.EMAIL.username_changed_confirmation(self.request, context).send(to) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/docs/source/index.rst b/docs/source/index.rst index 46fcb5ee..36bf05ad 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ Welcome to djoser's documentation! .. note:: - djoser 2.x is not backward compatible with djoser 1.x + djoser 3.x is not backward compatible with djoser 2.x .. toctree:: :maxdepth: 2 diff --git a/pyproject.toml b/pyproject.toml index c6da5853..11f5d5f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,6 @@ in-place = true [tool.pytest.ini_options] minversion = "7.0" DJANGO_SETTINGS_MODULE = "testproject.settings" -python_paths = "testproject" [tool.coverage.run] source = ["djoser"] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 67a0f55c..00000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -DJANGO_SETTINGS_MODULE = testproject.settings -python_paths = testproject diff --git a/testproject/testapp/tests/test_password_reset.py b/testproject/testapp/tests/test_password_reset.py index ccf6817f..c024f111 100644 --- a/testproject/testapp/tests/test_password_reset.py +++ b/testproject/testapp/tests/test_password_reset.py @@ -69,7 +69,6 @@ def test_post_should_return_bad_request_if_user_does_not_exist(self): ) @mock.patch("djoser.serializers.User", CustomUser) - @mock.patch("djoser.views.User", CustomUser) @override_settings(AUTH_USER_MODEL="testapp.CustomUser") def test_post_should_send_email_to_custom_user_with_password_reset_link( self, @@ -88,7 +87,6 @@ def test_post_should_send_email_to_custom_user_with_password_reset_link( self.assertIn(site.name, mail.outbox[0].body) @mock.patch("djoser.serializers.User", CustomUser) - @mock.patch("djoser.views.User", CustomUser) @override_settings( AUTH_USER_MODEL="testapp.CustomUser", DJOSER=dict(settings.DJOSER, **{"PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND": True}), diff --git a/testproject/testapp/tests/test_resend_activation.py b/testproject/testapp/tests/test_resend_activation.py index c451b916..da2ba32e 100644 --- a/testproject/testapp/tests/test_resend_activation.py +++ b/testproject/testapp/tests/test_resend_activation.py @@ -53,7 +53,6 @@ def test_dont_resend_activation_when_no_password(self): self.assert_status_equal(response, status.HTTP_204_NO_CONTENT) @mock.patch("djoser.serializers.User", CustomUser) - @mock.patch("djoser.views.User", CustomUser) @override_settings( AUTH_USER_MODEL="testapp.CustomUser", DJOSER=dict(settings.DJOSER, **{"SEND_ACTIVATION_EMAIL": True}), diff --git a/testproject/testapp/tests/test_reset_username.py b/testproject/testapp/tests/test_reset_username.py index 5245a898..80680f3b 100644 --- a/testproject/testapp/tests/test_reset_username.py +++ b/testproject/testapp/tests/test_reset_username.py @@ -72,7 +72,6 @@ def test_post_should_return_bad_request_if_user_does_not_exist(self): ) @mock.patch("djoser.serializers.User", CustomUser) - @mock.patch("djoser.views.User", CustomUser) @override_settings(AUTH_USER_MODEL="testapp.CustomUser") def test_post_should_send_email_to_custom_user_with_username_reset_link( self, @@ -91,7 +90,6 @@ def test_post_should_send_email_to_custom_user_with_username_reset_link( self.assertIn(site.name, mail.outbox[0].body) @mock.patch("djoser.serializers.User", CustomUser) - @mock.patch("djoser.views.User", CustomUser) @override_settings( AUTH_USER_MODEL="testapp.CustomUser", DJOSER=dict(settings.DJOSER, **{"USERNAME_RESET_SHOW_EMAIL_NOT_FOUND": True}), diff --git a/testproject/testapp/tests/test_set_username.py b/testproject/testapp/tests/test_set_username.py index d2165e98..dffd94f3 100644 --- a/testproject/testapp/tests/test_set_username.py +++ b/testproject/testapp/tests/test_set_username.py @@ -110,7 +110,7 @@ def test_post_not_set_new_username_if_same(self): "djoser.serializers.SetUsernameSerializer.Meta.fields", (CustomUser.USERNAME_FIELD, "custom_username"), ) - @mock.patch("djoser.views.User", CustomUser) + @mock.patch("djoser.views.user.set_username.User", CustomUser) @override_settings( AUTH_USER_MODEL="testapp.CustomUser", DJOSER=dict(settings.DJOSER, **{"LOGIN_FIELD": CustomUser.USERNAME_FIELD}), @@ -132,7 +132,7 @@ def test_post_set_new_custom_username(self): "djoser.serializers.SetUsernameSerializer.Meta.fields", (CustomUser.USERNAME_FIELD, "custom_username"), ) - @mock.patch("djoser.views.User", CustomUser) + @mock.patch("djoser.views.user.set_username.User", CustomUser) @override_settings( AUTH_USER_MODEL="testapp.CustomUser", DJOSER=dict( diff --git a/testproject/testapp/tests/test_urls/test_urls.py b/testproject/testapp/tests/test_urls/test_urls.py index b619a187..395653b4 100644 --- a/testproject/testapp/tests/test_urls/test_urls.py +++ b/testproject/testapp/tests/test_urls/test_urls.py @@ -7,6 +7,9 @@ from django.urls import get_resolver +ALLOW_RECREATE = False + + @pytest.mark.django_db def test_urls_have_not_changed(settings): BASE_DIR = settings.BASE_DIR @@ -72,8 +75,9 @@ def get_all_urls(patterns, prefix=""): diff = DeepDiff(current_urls, saved_urls) if diff: - with open(FILE_PATH, "w") as f: - json.dump(current_urls, f, indent=2) + if ALLOW_RECREATE: + with open(FILE_PATH, "w") as f: + json.dump(current_urls, f, indent=2) pytest.fail( f"URL structure has changed. Updated snapshot with new URLs and names. Diff:\n\n{diff}" # noqa: E501 ) diff --git a/testproject/testapp/tests/test_urls/urls_snapshot.json b/testproject/testapp/tests/test_urls/urls_snapshot.json index b52be00f..4e70a2ae 100644 --- a/testproject/testapp/tests/test_urls/urls_snapshot.json +++ b/testproject/testapp/tests/test_urls/urls_snapshot.json @@ -1,6 +1,6 @@ [ { - "pattern": "^auth/^jwt/create/?", + "pattern": "^auth/^jwt/create/\\Z", "name": "jwt-create", "allowed_methods": [ "get", @@ -13,7 +13,7 @@ ] }, { - "pattern": "^auth/^jwt/refresh/?", + "pattern": "^auth/^jwt/refresh/\\Z", "name": "jwt-refresh", "allowed_methods": [ "get", @@ -26,7 +26,7 @@ ] }, { - "pattern": "^auth/^jwt/verify/?", + "pattern": "^auth/^jwt/verify/\\Z", "name": "jwt-verify", "allowed_methods": [ "get", @@ -52,7 +52,7 @@ ] }, { - "pattern": "^auth/^token/login/?$", + "pattern": "^auth/^token/login/\\Z", "name": "login", "allowed_methods": [ "get", @@ -65,7 +65,7 @@ ] }, { - "pattern": "^auth/^token/logout/?$", + "pattern": "^auth/^token/logout/\\Z", "name": "logout", "allowed_methods": [ "get", @@ -78,15 +78,7 @@ ] }, { - "pattern": "^auth/^users/$", - "name": "user-list", - "allowed_methods": [ - "get", - "post" - ] - }, - { - "pattern": "^auth/^users/(?P[^/.]+)/$", + "pattern": "^auth/^users/(?P[^/]+)/\\Z", "name": "user-detail", "allowed_methods": [ "get", @@ -96,41 +88,22 @@ ] }, { - "pattern": "^auth/^users/(?P[^/.]+)\\.(?P[a-z0-9]+)/?$", - "name": "user-detail", + "pattern": "^auth/^users/\\Z", + "name": "user-list", "allowed_methods": [ "get", - "put", - "patch", - "delete" - ] - }, - { - "pattern": "^auth/^users/activation/$", - "name": "user-activation", - "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/activation\\.(?P[a-z0-9]+)/?$", + "pattern": "^auth/^users/activation/\\Z", "name": "user-activation", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/me/$", - "name": "user-me", - "allowed_methods": [ - "get", - "put", - "patch", - "delete" - ] - }, - { - "pattern": "^auth/^users/me\\.(?P[a-z0-9]+)/?$", + "pattern": "^auth/^users/me/\\Z", "name": "user-me", "allowed_methods": [ "get", @@ -140,111 +113,54 @@ ] }, { - "pattern": "^auth/^users/resend_activation/$", + "pattern": "^auth/^users/resend\\-activation/\\Z", "name": "user-resend-activation", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/resend_activation\\.(?P[a-z0-9]+)/?$", - "name": "user-resend-activation", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/reset_password/$", - "name": "user-reset-password", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/reset_password\\.(?P[a-z0-9]+)/?$", + "pattern": "^auth/^users/reset_password\\Z", "name": "user-reset-password", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/reset_password_confirm/$", + "pattern": "^auth/^users/reset_password_confirm/\\Z", "name": "user-reset-password-confirm", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/reset_password_confirm\\.(?P[a-z0-9]+)/?$", - "name": "user-reset-password-confirm", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/reset_username/$", + "pattern": "^auth/^users/reset_username\\Z", "name": "user-reset-username", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/reset_username\\.(?P[a-z0-9]+)/?$", - "name": "user-reset-username", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/reset_username_confirm/$", + "pattern": "^auth/^users/reset_username_confirm\\Z", "name": "user-reset-username-confirm", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/reset_username_confirm\\.(?P[a-z0-9]+)/?$", - "name": "user-reset-username-confirm", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/set_password/$", + "pattern": "^auth/^users/set_password\\Z", "name": "user-set-password", "allowed_methods": [ "post" ] }, { - "pattern": "^auth/^users/set_password\\.(?P[a-z0-9]+)/?$", - "name": "user-set-password", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/set_username/$", - "name": "user-set-username", - "allowed_methods": [ - "post" - ] - }, - { - "pattern": "^auth/^users/set_username\\.(?P[a-z0-9]+)/?$", + "pattern": "^auth/^users/set_username\\Z", "name": "user-set-username", "allowed_methods": [ "post" ] }, - { - "pattern": "^auth/^users\\.(?P[a-z0-9]+)/?$", - "name": "user-list", - "allowed_methods": [ - "get", - "post" - ] - }, { "pattern": "^webauthn-example/$", "name": null, diff --git a/testproject/testapp/tests/test_user_create.py b/testproject/testapp/tests/test_user_create.py index 3bebc943..f1122eef 100644 --- a/testproject/testapp/tests/test_user_create.py +++ b/testproject/testapp/tests/test_user_create.py @@ -123,7 +123,7 @@ def test_post_return_400_for_integrity_error(self, perform_create): tuple(CustomUser.REQUIRED_FIELDS) + (CustomUser.USERNAME_FIELD, CustomUser._meta.pk.name, "password"), ) - @mock.patch("djoser.views.User", CustomUser) + @mock.patch("djoser.views.user.User", CustomUser) @override_settings(AUTH_USER_MODEL="testapp.CustomUser") def test_post_create_custom_user_with_all_required_fields(self): data = { @@ -151,7 +151,7 @@ def test_post_create_custom_user_with_all_required_fields(self): tuple(CustomUser.REQUIRED_FIELDS) + (CustomUser.USERNAME_FIELD, CustomUser._meta.pk.name, "password"), ) - @mock.patch("djoser.views.User", CustomUser) + @mock.patch("djoser.views.user.User", CustomUser) @override_settings(AUTH_USER_MODEL="testapp.CustomUser") def test_post_not_create_custom_user_with_missing_required_fields(self): data = {"custom_username": "john", "password": "secret"} @@ -168,7 +168,7 @@ def test_post_not_create_custom_user_with_missing_required_fields(self): tuple(ExampleUser.REQUIRED_FIELDS) + (ExampleUser.USERNAME_FIELD, ExampleUser._meta.pk.name, "password"), ) - @mock.patch("djoser.views.User", ExampleUser) + @mock.patch("djoser.views.user.User", ExampleUser) @override_settings(AUTH_USER_MODEL="testapp.ExampleUser") def test_post_create_custom_user_without_username(self): data = {"password": "secret", "email": "test@user1.com"} @@ -187,7 +187,7 @@ def test_post_create_custom_user_without_username(self): tuple(ExampleUser.REQUIRED_FIELDS) + (ExampleUser.USERNAME_FIELD, ExampleUser._meta.pk.name, "password"), ) - @mock.patch("djoser.views.User", ExampleUser) + @mock.patch("djoser.views.user.User", ExampleUser) @override_settings(AUTH_USER_MODEL="testapp.ExampleUser") def test_post_create_custom_user_missing_required_fields(self): data = {"password": "secret"} diff --git a/testproject/testapp/tests/test_user_delete.py b/testproject/testapp/tests/test_user_delete.py index 5240a2cd..763632b8 100644 --- a/testproject/testapp/tests/test_user_delete.py +++ b/testproject/testapp/tests/test_user_delete.py @@ -7,7 +7,6 @@ from rest_framework.reverse import reverse from rest_framework.test import APITestCase -import djoser.views from djoser.conf import settings as djoser_settings from .common import PermCheckClass, RunCheck, SerializerCheckClass, create_user @@ -21,8 +20,6 @@ class UserMeDeleteViewTest( assertions.EmailAssertionsMixin, assertions.InstanceAssertionsMixin, ): - viewset = djoser.views.UserViewSet - def test_delete_user_if_logged_in(self): user = create_user() self.assert_instance_exists(User, username="john") diff --git a/testproject/testapp/tests/test_user_detail.py b/testproject/testapp/tests/test_user_detail.py index 837f3644..67ece6c0 100644 --- a/testproject/testapp/tests/test_user_detail.py +++ b/testproject/testapp/tests/test_user_detail.py @@ -5,7 +5,8 @@ from testapp.tests.common import create_user, login_user import djoser.permissions -import djoser.views + +from djoser.views.user import UserViewSet class BaseUserViewSetListTest(APITestCase, assertions.StatusCodeAssertionsMixin): @@ -26,14 +27,14 @@ class ModifiedPermissionsTest(APITestCase): def setUp(self): super().setUp() - self.previous_permissions = djoser.views.UserViewSet.permission_classes - djoser.views.UserViewSet.permission_classes = [ + self.previous_permissions = UserViewSet.permission_classes + UserViewSet.permission_classes = [ djoser.permissions.CurrentUserOrAdminOrReadOnly ] def tearDown(self): super().tearDown() - djoser.views.UserViewSet.permission_classes = self.previous_permissions + UserViewSet.permission_classes = self.previous_permissions class UserViewSetListTest(BaseUserViewSetListTest): @@ -72,8 +73,11 @@ def test_user_can_get_other_user_detail(self): def test_user_cant_set_other_user_detail(self): login_user(self.client, self.user) - response = self.client.get(reverse("user-detail", args=[self.superuser.pk])) - self.assert_status_equal(response, status.HTTP_200_OK) + response = self.client.patch( + reverse("user-detail", args=[self.superuser.pk]), + data={"email": "eggs@example.com"}, + ) + self.assert_status_equal(response, status.HTTP_404_NOT_FOUND) class UserViewSetEditTest(APITestCase, assertions.StatusCodeAssertionsMixin):