feat(user): users/me and admin user-management contract fields
This commit is contained in:
@@ -1,3 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
@@ -89,7 +93,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
class CurrentUserProfileSerializer(serializers.ModelSerializer):
|
class CurrentUserProfileSerializer(serializers.ModelSerializer):
|
||||||
"""Профиль текущего пользователя в контракте `/users/me/`."""
|
"""Профиль текущего пользователя в контракте `/users/me/`."""
|
||||||
|
|
||||||
middle_name = serializers.CharField(source="mid_name", allow_null=True)
|
middle_name = serializers.SerializerMethodField()
|
||||||
full_name = serializers.ReadOnlyField()
|
full_name = serializers.ReadOnlyField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -101,6 +105,10 @@ class CurrentUserProfileSerializer(serializers.ModelSerializer):
|
|||||||
"full_name",
|
"full_name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_middle_name(obj: Profile) -> str:
|
||||||
|
return obj.mid_name or ""
|
||||||
|
|
||||||
|
|
||||||
class CurrentUserCapabilitiesSerializer(serializers.Serializer):
|
class CurrentUserCapabilitiesSerializer(serializers.Serializer):
|
||||||
"""Возможности пользователя для UI-контрактов."""
|
"""Возможности пользователя для UI-контрактов."""
|
||||||
@@ -153,6 +161,88 @@ class CurrentUserSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManagementSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for users listed in admin management endpoint."""
|
||||||
|
|
||||||
|
id = serializers.IntegerField(read_only=True)
|
||||||
|
username = serializers.CharField(read_only=True)
|
||||||
|
email = serializers.EmailField(read_only=True)
|
||||||
|
phone = serializers.CharField(read_only=True, allow_null=True)
|
||||||
|
is_active = serializers.BooleanField(read_only=True)
|
||||||
|
first_name = serializers.SerializerMethodField()
|
||||||
|
middle_name = serializers.SerializerMethodField()
|
||||||
|
last_name = serializers.SerializerMethodField()
|
||||||
|
progress_message = serializers.SerializerMethodField()
|
||||||
|
result = serializers.SerializerMethodField()
|
||||||
|
error = serializers.SerializerMethodField()
|
||||||
|
started_at = serializers.SerializerMethodField()
|
||||||
|
completed_at = serializers.SerializerMethodField()
|
||||||
|
duration = serializers.SerializerMethodField()
|
||||||
|
is_successful = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_profile_value(profile: Profile | None, field_name: str) -> str:
|
||||||
|
if profile is None:
|
||||||
|
return ""
|
||||||
|
value = getattr(profile, field_name, "")
|
||||||
|
return value or ""
|
||||||
|
|
||||||
|
def get_first_name(self, obj: User) -> str:
|
||||||
|
return self._get_profile_value(getattr(obj, "profile", None), "first_name")
|
||||||
|
|
||||||
|
def get_middle_name(self, obj: User) -> str:
|
||||||
|
return self._get_profile_value(getattr(obj, "profile", None), "mid_name")
|
||||||
|
|
||||||
|
def get_last_name(self, obj: User) -> str:
|
||||||
|
return self._get_profile_value(getattr(obj, "profile", None), "last_name")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_latest_job(obj: User) -> Any | None:
|
||||||
|
return getattr(obj, "latest_job", None)
|
||||||
|
|
||||||
|
def get_progress_message(self, obj: User) -> str | None:
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.progress_message or None
|
||||||
|
|
||||||
|
def get_result(self, obj: User):
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.result
|
||||||
|
|
||||||
|
def get_error(self, obj: User) -> str | None:
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.error or None
|
||||||
|
|
||||||
|
def get_started_at(self, obj: User):
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.started_at
|
||||||
|
|
||||||
|
def get_completed_at(self, obj: User):
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.completed_at
|
||||||
|
|
||||||
|
def get_duration(self, obj: User) -> float | None:
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.duration
|
||||||
|
|
||||||
|
def get_is_successful(self, obj: User) -> bool | None:
|
||||||
|
job = self._get_latest_job(obj)
|
||||||
|
if job is None:
|
||||||
|
return None
|
||||||
|
return job.is_successful
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateSerializer(serializers.ModelSerializer):
|
class UserUpdateSerializer(serializers.ModelSerializer):
|
||||||
"""Сериализатор для обновления данных пользователя"""
|
"""Сериализатор для обновления данных пользователя"""
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ urlpatterns = [
|
|||||||
path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
|
path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
|
||||||
# Пользовательские данные
|
# Пользовательские данные
|
||||||
path("me/", views.CurrentUserView.as_view(), name="current_user"),
|
path("me/", views.CurrentUserView.as_view(), name="current_user"),
|
||||||
|
path("admin/users/", views.AdminUsersManagementView.as_view(), name="admin_users"),
|
||||||
path("me/update/", views.UserUpdateView.as_view(), name="user_update"),
|
path("me/update/", views.UserUpdateView.as_view(), name="user_update"),
|
||||||
path("profile/", views.ProfileDetailView.as_view(), name="profile_detail"),
|
path("profile/", views.ProfileDetailView.as_view(), name="profile_detail"),
|
||||||
path("profile/full/", views.user_profile_detail, name="profile_full"),
|
path("profile/full/", views.user_profile_detail, name="profile_full"),
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from django.contrib.auth import authenticate
|
from apps.core.models import BackgroundJob
|
||||||
|
from apps.core.services import BackgroundJobService
|
||||||
|
from django.contrib.auth import authenticate, get_user_model
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
@@ -15,12 +17,15 @@ from .serializers import (
|
|||||||
PasswordChangeSerializer,
|
PasswordChangeSerializer,
|
||||||
ProfileUpdateSerializer,
|
ProfileUpdateSerializer,
|
||||||
TokenSerializer,
|
TokenSerializer,
|
||||||
|
UserManagementSerializer,
|
||||||
UserRegistrationSerializer,
|
UserRegistrationSerializer,
|
||||||
UserSerializer,
|
UserSerializer,
|
||||||
UserUpdateSerializer,
|
UserUpdateSerializer,
|
||||||
)
|
)
|
||||||
from .services import ProfileService, UserService
|
from .services import ProfileService, UserService
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
# Swagger теги для группировки
|
# Swagger теги для группировки
|
||||||
AUTH_TAG = "Аутентификация"
|
AUTH_TAG = "Аутентификация"
|
||||||
USER_TAG = "Пользователь"
|
USER_TAG = "Пользователь"
|
||||||
@@ -230,6 +235,45 @@ class PasswordChangeView(APIView):
|
|||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUsersManagementView(APIView):
|
||||||
|
"""Список пользователей для административной страницы управления."""
|
||||||
|
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _attach_latest_jobs(users: list[User]):
|
||||||
|
user_ids = [user.id for user in users]
|
||||||
|
if not user_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
latest_jobs: dict[int, BackgroundJob] = {}
|
||||||
|
jobs = BackgroundJobService.get_queryset().filter(
|
||||||
|
user_id__in=user_ids
|
||||||
|
).order_by("user_id", "-created_at")
|
||||||
|
|
||||||
|
for job in jobs:
|
||||||
|
if job.user_id not in latest_jobs:
|
||||||
|
latest_jobs[job.user_id] = job
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
user.latest_job = latest_jobs.get(user.id)
|
||||||
|
|
||||||
|
@swagger_auto_schema(
|
||||||
|
tags=[USER_TAG],
|
||||||
|
operation_summary="Список пользователей (admin)",
|
||||||
|
operation_description=(
|
||||||
|
"Возвращает пользователей для административной панели управления. "
|
||||||
|
"Включает метрики последней фоновой задачи пользователя."
|
||||||
|
),
|
||||||
|
responses={200: UserManagementSerializer(many=True)},
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
users = User.objects.all().select_related("profile").order_by("id")
|
||||||
|
self._attach_latest_jobs(list(users))
|
||||||
|
serializer = UserManagementSerializer(users, many=True)
|
||||||
|
return Response({"results": serializer.data})
|
||||||
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
method="get",
|
method="get",
|
||||||
tags=[USER_TAG],
|
tags=[USER_TAG],
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
"""Tests for user serializers"""
|
"""Tests for user serializers"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from apps.core.models import BackgroundJob, JobStatus
|
||||||
from apps.user.serializers import (
|
from apps.user.serializers import (
|
||||||
|
CurrentUserProfileSerializer,
|
||||||
|
CurrentUserSerializer,
|
||||||
LoginSerializer,
|
LoginSerializer,
|
||||||
PasswordChangeSerializer,
|
PasswordChangeSerializer,
|
||||||
ProfileUpdateSerializer,
|
ProfileUpdateSerializer,
|
||||||
TokenSerializer,
|
TokenSerializer,
|
||||||
|
UserManagementSerializer,
|
||||||
UserRegistrationSerializer,
|
UserRegistrationSerializer,
|
||||||
UserSerializer,
|
UserSerializer,
|
||||||
UserUpdateSerializer,
|
UserUpdateSerializer,
|
||||||
@@ -124,6 +130,93 @@ class UserSerializerTest(TestCase):
|
|||||||
self.assertIn(field_name, serializer.Meta.read_only_fields)
|
self.assertIn(field_name, serializer.Meta.read_only_fields)
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentUserProfileSerializerTest(TestCase):
|
||||||
|
"""Tests for current user profile serializer"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = UserFactory.create_user()
|
||||||
|
self.profile = ProfileFactory.create_profile(
|
||||||
|
user=self.user,
|
||||||
|
first_name="Ivan",
|
||||||
|
mid_name=None,
|
||||||
|
last_name="Petrov",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_middle_name_falls_back_to_empty_string(self):
|
||||||
|
serializer = CurrentUserProfileSerializer(self.profile)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data["middle_name"], "")
|
||||||
|
|
||||||
|
|
||||||
|
class CurrentUserSerializerTest(TestCase):
|
||||||
|
"""Tests for current user serializer fields."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = UserFactory.create_user()
|
||||||
|
|
||||||
|
def test_current_user_contains_is_active(self):
|
||||||
|
serializer = CurrentUserSerializer(self.user)
|
||||||
|
|
||||||
|
self.assertIn("is_active", serializer.data)
|
||||||
|
self.assertEqual(serializer.data["is_active"], self.user.is_active)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManagementSerializerTest(TestCase):
|
||||||
|
"""Tests for user management serializer."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = UserFactory.create_user()
|
||||||
|
self.user.profile.first_name = None
|
||||||
|
self.user.profile.mid_name = None
|
||||||
|
self.user.profile.last_name = None
|
||||||
|
self.user.profile.save()
|
||||||
|
|
||||||
|
def test_profile_fields_fallback_to_empty_string(self):
|
||||||
|
serializer = UserManagementSerializer(self.user)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data["first_name"], "")
|
||||||
|
self.assertEqual(serializer.data["middle_name"], "")
|
||||||
|
self.assertEqual(serializer.data["last_name"], "")
|
||||||
|
|
||||||
|
def test_metric_fields_are_derived_from_latest_job(self):
|
||||||
|
now = datetime(2026, 4, 14, 10, 0, 0)
|
||||||
|
latest_job = BackgroundJob.objects.create(
|
||||||
|
task_id="admin-management-latest",
|
||||||
|
task_name="apps.forms.process",
|
||||||
|
user_id=self.user.id,
|
||||||
|
status=JobStatus.SUCCESS,
|
||||||
|
progress=100,
|
||||||
|
progress_message="Готово",
|
||||||
|
result={"processed": 10},
|
||||||
|
started_at=now,
|
||||||
|
completed_at=now + timedelta(minutes=3),
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now + timedelta(minutes=3),
|
||||||
|
)
|
||||||
|
BackgroundJob.objects.create(
|
||||||
|
task_id="admin-management-old",
|
||||||
|
task_name="apps.forms.process",
|
||||||
|
user_id=self.user.id,
|
||||||
|
status=JobStatus.FAILURE,
|
||||||
|
progress=100,
|
||||||
|
progress_message="Ошибка",
|
||||||
|
error="old-error",
|
||||||
|
started_at=now - timedelta(minutes=15),
|
||||||
|
completed_at=now - timedelta(minutes=10),
|
||||||
|
created_at=now - timedelta(minutes=15),
|
||||||
|
updated_at=now - timedelta(minutes=10),
|
||||||
|
)
|
||||||
|
self.user.latest_job = latest_job
|
||||||
|
|
||||||
|
serializer = UserManagementSerializer(self.user)
|
||||||
|
|
||||||
|
self.assertEqual(serializer.data["progress_message"], "Готово")
|
||||||
|
self.assertEqual(serializer.data["result"], {"processed": 10})
|
||||||
|
self.assertIsNone(serializer.data["error"])
|
||||||
|
self.assertEqual(serializer.data["is_successful"], True)
|
||||||
|
self.assertEqual(serializer.data["duration"], 180.0)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateSerializerTest(TestCase):
|
class UserUpdateSerializerTest(TestCase):
|
||||||
"""Tests for UserUpdateSerializer"""
|
"""Tests for UserUpdateSerializer"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"""Tests for user DRF views"""
|
"""Tests for user DRF views"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from apps.core.models import BackgroundJob, JobStatus
|
||||||
from apps.user.models import Profile
|
from apps.user.models import Profile
|
||||||
from apps.user.services import UserService
|
from apps.user.services import UserService
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@@ -123,7 +126,7 @@ class CurrentUserViewTest(APITestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = UserFactory.create_user()
|
self.user = UserFactory.create_user()
|
||||||
ProfileFactory.create_profile(user=self.user)
|
ProfileFactory.create_profile(user=self.user, mid_name="")
|
||||||
self.current_user_url = reverse("api_v1:user:current_user")
|
self.current_user_url = reverse("api_v1:user:current_user")
|
||||||
self.tokens = UserService.get_tokens_for_user(self.user)
|
self.tokens = UserService.get_tokens_for_user(self.user)
|
||||||
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.tokens['access']}")
|
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.tokens['access']}")
|
||||||
@@ -135,7 +138,9 @@ class CurrentUserViewTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["id"], self.user.id)
|
self.assertEqual(response.data["id"], self.user.id)
|
||||||
self.assertEqual(response.data["email"], self.user.email)
|
self.assertEqual(response.data["email"], self.user.email)
|
||||||
|
self.assertEqual(response.data["is_active"], self.user.is_active)
|
||||||
self.assertIn("profile", response.data)
|
self.assertIn("profile", response.data)
|
||||||
|
self.assertEqual(response.data["profile"]["middle_name"], "")
|
||||||
self.assertEqual(response.data["role"], "user")
|
self.assertEqual(response.data["role"], "user")
|
||||||
self.assertEqual(response.data["capabilities"]["can_access_admin_page"], False)
|
self.assertEqual(response.data["capabilities"]["can_access_admin_page"], False)
|
||||||
|
|
||||||
@@ -157,6 +162,87 @@ class CurrentUserViewTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUsersManagementViewTest(APITestCase):
|
||||||
|
"""Tests for admin users endpoint."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.admin_user = UserFactory.create_user(is_staff=True)
|
||||||
|
self.regular_user = UserFactory.create_user()
|
||||||
|
self.job_user = UserFactory.create_user()
|
||||||
|
self.client.force_authenticate(self.admin_user)
|
||||||
|
self.url = reverse("api_v1:user:admin_users")
|
||||||
|
|
||||||
|
ProfileFactory.create_profile(
|
||||||
|
user=self.job_user,
|
||||||
|
first_name="Иван",
|
||||||
|
mid_name="Сергеевич",
|
||||||
|
last_name="Петров",
|
||||||
|
)
|
||||||
|
|
||||||
|
now = datetime(2026, 4, 14, 10, 0, 0)
|
||||||
|
completed_job = now + timedelta(minutes=3)
|
||||||
|
failed_job = now - timedelta(minutes=10)
|
||||||
|
BackgroundJob.objects.create(
|
||||||
|
task_id="admin-management-task",
|
||||||
|
task_name="apps.forms.process",
|
||||||
|
user_id=self.job_user.id,
|
||||||
|
status=JobStatus.SUCCESS,
|
||||||
|
progress=100,
|
||||||
|
progress_message="Готово",
|
||||||
|
result={"processed": 10},
|
||||||
|
started_at=now,
|
||||||
|
completed_at=completed_job,
|
||||||
|
created_at=completed_job,
|
||||||
|
updated_at=completed_job,
|
||||||
|
)
|
||||||
|
BackgroundJob.objects.create(
|
||||||
|
task_id="admin-management-task-failed",
|
||||||
|
task_name="apps.forms.process",
|
||||||
|
user_id=self.job_user.id,
|
||||||
|
status=JobStatus.FAILURE,
|
||||||
|
progress=100,
|
||||||
|
progress_message="Неуспешно",
|
||||||
|
error="error",
|
||||||
|
started_at=failed_job,
|
||||||
|
completed_at=now - timedelta(minutes=50),
|
||||||
|
created_at=failed_job,
|
||||||
|
updated_at=failed_job,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_admin_users_list_contains_import_metrics_and_profile_fields(self):
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("results", response.data)
|
||||||
|
results = response.data["results"]
|
||||||
|
|
||||||
|
row_map = {row["id"]: row for row in results}
|
||||||
|
job_user_row = row_map[self.job_user.id]
|
||||||
|
regular_user_row = row_map[self.regular_user.id]
|
||||||
|
|
||||||
|
self.assertEqual(job_user_row["first_name"], "Иван")
|
||||||
|
self.assertEqual(job_user_row["middle_name"], "Сергеевич")
|
||||||
|
self.assertEqual(job_user_row["last_name"], "Петров")
|
||||||
|
self.assertEqual(job_user_row["progress_message"], "Готово")
|
||||||
|
self.assertEqual(job_user_row["result"], {"processed": 10})
|
||||||
|
self.assertIsNone(job_user_row["error"])
|
||||||
|
self.assertEqual(job_user_row["is_successful"], True)
|
||||||
|
self.assertEqual(job_user_row["duration"], 180.0)
|
||||||
|
self.assertIsNotNone(job_user_row["started_at"])
|
||||||
|
self.assertIsNotNone(job_user_row["completed_at"])
|
||||||
|
|
||||||
|
self.assertIsNone(regular_user_row["progress_message"])
|
||||||
|
self.assertIsNone(regular_user_row["result"])
|
||||||
|
self.assertIsNone(regular_user_row["error"])
|
||||||
|
self.assertIsNone(regular_user_row["is_successful"])
|
||||||
|
|
||||||
|
def test_non_admin_cannot_access_endpoint(self):
|
||||||
|
self.client.force_authenticate(self.regular_user)
|
||||||
|
response = self.client.get(self.url)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateViewTest(APITestCase):
|
class UserUpdateViewTest(APITestCase):
|
||||||
"""Tests for UserUpdateView"""
|
"""Tests for UserUpdateView"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user