diff --git a/questions/migrations/__init__.py b/learn_docs/__init__.py similarity index 100% rename from questions/migrations/__init__.py rename to learn_docs/__init__.py diff --git a/learn_docs/__pycache__/__init__.cpython-38.pyc b/learn_docs/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..d3bc689 Binary files /dev/null and b/learn_docs/__pycache__/__init__.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/admin.cpython-38.pyc b/learn_docs/__pycache__/admin.cpython-38.pyc new file mode 100644 index 0000000..3a85eae Binary files /dev/null and b/learn_docs/__pycache__/admin.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/apps.cpython-38.pyc b/learn_docs/__pycache__/apps.cpython-38.pyc new file mode 100644 index 0000000..e8c7a0b Binary files /dev/null and b/learn_docs/__pycache__/apps.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/mixins.cpython-38.pyc b/learn_docs/__pycache__/mixins.cpython-38.pyc new file mode 100644 index 0000000..1978705 Binary files /dev/null and b/learn_docs/__pycache__/mixins.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/models.cpython-38.pyc b/learn_docs/__pycache__/models.cpython-38.pyc new file mode 100644 index 0000000..c75a04a Binary files /dev/null and b/learn_docs/__pycache__/models.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/serializers.cpython-38.pyc b/learn_docs/__pycache__/serializers.cpython-38.pyc new file mode 100644 index 0000000..6c5e14e Binary files /dev/null and b/learn_docs/__pycache__/serializers.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/urls.cpython-38.pyc b/learn_docs/__pycache__/urls.cpython-38.pyc new file mode 100644 index 0000000..417776b Binary files /dev/null and b/learn_docs/__pycache__/urls.cpython-38.pyc differ diff --git a/learn_docs/__pycache__/views.cpython-38.pyc b/learn_docs/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000..3cfc11d Binary files /dev/null and b/learn_docs/__pycache__/views.cpython-38.pyc differ diff --git a/learn_docs/admin.py b/learn_docs/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/learn_docs/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/learn_docs/apps.py b/learn_docs/apps.py new file mode 100644 index 0000000..fddcb45 --- /dev/null +++ b/learn_docs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LearnDocsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'learn_docs' diff --git a/learn_docs/migrations/__init__.py b/learn_docs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/learn_docs/migrations/__pycache__/__init__.cpython-38.pyc b/learn_docs/migrations/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..d9faff7 Binary files /dev/null and b/learn_docs/migrations/__pycache__/__init__.cpython-38.pyc differ diff --git a/learn_docs/mixins.py b/learn_docs/mixins.py new file mode 100644 index 0000000..0bfd532 --- /dev/null +++ b/learn_docs/mixins.py @@ -0,0 +1,15 @@ +from rest_framework.generics import get_object_or_404 + + +class MultipleFieldLookupMixin: + + def get_object(self): + queryset = self.get_queryset() # соглашаемся с queryset + queryset = self.filter_queryset() # также соглашаемся с фильтрами + filter = {} + for field in self.lookup_fields: + if self.kwargs.get(field): ## так достаем из lookup + filter[field] = self.kwargs.get(field) + obj = get_object_or_404(queryset, **filter) # передаем фильтр для поиска + self.check_object_permissions(self.request, obj) + return obj \ No newline at end of file diff --git a/learn_docs/models.py b/learn_docs/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/learn_docs/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/learn_docs/serializers.py b/learn_docs/serializers.py new file mode 100644 index 0000000..c9929ba --- /dev/null +++ b/learn_docs/serializers.py @@ -0,0 +1,62 @@ +from datetime import datetime + +from rest_framework import serializers + + +class Comment: + + def __init__(self, email, content, created=None): + self.email = email + self.content = content + self.created = created or datetime.now() + + +class CommentSerializer(serializers.Serializer): + email = serializers.EmailField() + content = serializers.CharField(max_length=20) + created = serializers.DateTimeField() + + # если мы хотим сразу возвращать готовые обькты по валидированным данным данным + def create(self, validated_data): + print('created') + # Comment.objects.create(**validated_data) ## если хотим сразу в бд + return Comment(**validated_data) + + def update(self, instance, validated_data): + print('update') + instance.email = validated_data.get('email', instance.email) + instance.content = validated_data.get('content', instance.email) + instance.created = validated_data.get('created', instance.email) + # instance.save() + return instance + + # # если нам не нужно создавать новый обьект, и например сделать какую нибудь логику(например, отправка сообщения на почту, можно переопределить save напрямую) + # def save(self, **kwargs): + # email = self.validated_data.get('email') + # message = self.validated_data.get('message') + # # send_email(from=email, message=message) + + # для валидации поля мы создаем метод с названием validate_ + # при неудачной валидации кидаем validation error + # только для полей required true + def validate_content(self, value): + if 'django' not in value.lower(): + raise serializers.ValidationError('This comment is not about Django') + return value + + # валидации на уровне обьекто + def validate(self, attrs): + # if attrs['start'] > attrs['finish']: + # raise serializers.ValidationError('finish ust occur after start') + return attrs + + # можно написать валидатор, который будет использоваться для нескольких полей + + def multiple_of_ten(self, value): + if value % 10 != 0: + raise serializers.ValidationError('Not a muliply of ten') + + class GameRecord(serializers.Serializer): + score = serializers.IntegerField( ) + + diff --git a/learn_docs/tests.py b/learn_docs/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/learn_docs/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/learn_docs/urls.py b/learn_docs/urls.py new file mode 100644 index 0000000..6c07443 --- /dev/null +++ b/learn_docs/urls.py @@ -0,0 +1,22 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from .views import TestRequestAPIView, TestList, UserViewSet, TestSerializers + +## 1 вариант +urlpatterns = [ + path('request/', TestRequestAPIView.as_view()), + path('list/', TestList.as_view()), + path('user/', UserViewSet.as_view({'get': 'list'})), + path('user//', UserViewSet.as_view({'get': 'retrieve'})), + path('serializer/', TestSerializers.as_view()) + + + + +] +## тк юрл самому писать тяжело и запарно, необходим роутер +## 2 вариант для вьюсетов +router = DefaultRouter() +router.register(r'user-set', UserViewSet, basename='user') +urlpatterns += router.urls diff --git a/learn_docs/views.py b/learn_docs/views.py new file mode 100644 index 0000000..74bae93 --- /dev/null +++ b/learn_docs/views.py @@ -0,0 +1,189 @@ +import datetime + +from django.contrib.auth import get_user_model +from django.shortcuts import render +from rest_framework import generics, permissions, status, viewsets + +# Create your views here. +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.renderers import JSONRenderer +from rest_framework.response import Response +from rest_framework_simplejwt.authentication import JWTAuthentication + +from learn_docs.mixins import MultipleFieldLookupMixin +from learn_docs.serializers import Comment, CommentSerializer +from users.serializers import UserProfileSerializer, UserSerializer + +User = get_user_model() + +# добавляем миксин и все ок в целом + +class TestSerializers(generics.RetrieveUpdateDestroyAPIView): + serializer_class = CommentSerializer + authentication_classes = (JWTAuthentication, ) + + def get(self, request, *args, **kwargs): + comment = Comment(email='leila@example.com', content='foo bar') + # serializing + serializer = self.get_serializer(comment) + print(serializer.data) # здесь мы транслируем лишь в нативный питоновский словарь. Нужны рендеры + # json = JSONRenderer().render(serializer.data) + # print(json) + return Response(serializer.data) ## рендер вызывается автоматически + + def patch(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) ## нужно указывать дата + comment = None + if serializer.is_valid(raise_exception=True): + comment = serializer.save() # create + # comment = serializer.save(owner=request.user) # можно добавлять информацию при save, которая не находится в request.data + ## вызов create или update в методе save зависит от того, как мы обьявили сериализатор + # print(comment) + # serializer = self.get_serializer(data=request.data, instance=comment) + # if serializer.is_valid(): + # comment = serializer.save() # update + # print(comment) + return Response(serializer.validated_data) + + # def get_permissions(self): + # if self.request.method == 'GET': + # self.permission_classes = (AllowAny,) + # else: + # self.permission_classes = (IsAuthenticated, ) + # return super().get_permissions() + + # def get_serializer(self, *args, **kwargs): + # if self.request.method == 'GET': + # self.serializer_class = CommentSerializer + # else: + # self.serializer_class = UserSerializer + # return super().get_serializer(*args, **kwargs) + + +class TestList(generics.ListCreateAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = self.serializer_class(queryset, many=True) ## флаг того, что много инстансов + return Response(serializer.data) + +class TestRequestAPIView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView): + queryset = User.objects.all() + permission_classes = (permissions.AllowAny, ) + authentication_classes = (JWTAuthentication, ) + lookup_fields = ('id', 'username', ) + + def get(self, request, *args, **kwargs): + print(request.query_params) + return Response('1', status=status.HTTP_200_OK) + + def post(self, request, *args, **kwargs): + queryset = self.get_queryset() ## очень важно вызывать данный метод, чем обращаться к проперте напрямую, тк при обращении напрямую, он кэшируется и получается грустно невкусно + print(request.data) + print(request.accepted_renderer) + print(request.user) + print(request.auth) # тут крч содержится токен(или может что то другое), что является параметром для аутентификации + response = Response() + request.data['1'] = '2' + print(response.set_cookie('COKA', '12345', max_age=5)) + return response + + def handle_exception(self, exc): # вот так можно хэндлить ошибки любые(даже ерроры)(мб и не все) + print(type(exc)) + return Response(data={'detail': 'pizda', }) + + #def filter_queryset(self, queryset): фильтрует queryset (там нужны фильтры бэкэнды) + + # если нужно определить гибко сериализоатор, то как вариант. Если переопределяем, обращаемся только через метод(да и вообще всегда) + + def get_serializer_class(self): + if self.request.user.is_staff: + return UserProfileSerializer + return UserSerializer + + # хуки + # нужны для того, чтобы можно было переопределить сохранение или удаление внутри миксина(CreateModelMixin и тд вызывают их) + + # в данном случае очень удобно добавлять при сохранении информацию, которая передается вместе с request, но не с request.data + def perform_create(self, serializer): + serializer.save(user=self.request.user) + + # в данном случае можно вставлять определенную логику до или после сохранение + def perform_update(self, serializer): + #logger.log('update instance') + instance = serializer.save() ## сохранение изменненных данных + ##send_email_confirmation(user=self.request.user, modified=instance) + + ## также можно внедрять валидацию + # def perform_create(self, serializer): + # queryset = SignupRequest.objects.filter(user=self.request.user) + # if queryset.exists(): + # raise ValidationError('You have already signed up') + # serializer.save(user=self.request.user) + + +# если придется часто использовать данный миксин, можно создать базовый класс +class BaseRetrieveView(MultipleFieldLookupMixin, generics.RetrieveAPIView): + pass + +class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView): + pass + +### тесты классов +### В целом если мы переопределяем поведения методов, то настраивать некоторые параметры необязательно(если они нам в коде не нужны) +## но в целом тенденция такая, что для retrieve нам нужен полюбому queryset и model +### для create,update достаточно модели (и для delete тоже), однако в целом, иногда очень полезно вызывать queryset в коде через класс представления, а не через модель напрямую(например ели queryset отфильтрован) + + + +## view set +## обычный класс - не содежит действий +## GenericViewSet добавляет стандартную логику для вьюшек(действий также не содержит) +## ReadOnlyViewSet - list() и retrieve() +## ModelViewSet - содержит все методы(использовать можно, но для себя в крайне редких случаях) +## для всех них (кроме ModelViewSet) удобно использовать роутер +class UserViewSet(viewsets.ReadOnlyModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer + authentication_classes = (JWTAuthentication, ) + ## так мы можем устанавливать permissions для конкретного юрл + + def get_permissions(self): + if self.action == 'list': + permission_classes = (IsAuthenticated,) + else: + permission_classes = (AllowAny, ) + return (permission() for permission in permission_classes) + + # благодаря данному декоратору можем создавать свои action. Можно настроить хоть пермиссион хоть что + @action(detail=True, methods=['post']) + def set_password(self, request, pk=None): + user = self.get_object() + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + user.set_password(serializer.validated_data['password']) + user.save() + return Response({'status': 'password set'}) + else: + return Response(serializer.errors, + status=status.HTTP_400_BAD_REQUEST) + + # def list(self, request, *args, **kwargs): + # queryset = User.objects.all() + # serializer = UserSerializer(queryset, many=True) + # return Response(serializer.data) + # + # def retrieve(self, request, pk=None): + # queryset = User.objects.all() + # user = get_object_or_404(queryset, pk=pk) + # serializer = UserSerializer(user) + # return Response(serializer.data) + + # у вью сета нету методов хендлеров вроде get post и тд + + + diff --git a/questions/migrations/0001_initial.py b/questions/migrations/0001_initial.py deleted file mode 100644 index 825efb2..0000000 --- a/questions/migrations/0001_initial.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 3.2.5 on 2021-08-10 12:45 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Question', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50)), - ('description', models.TextField()), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('views', models.IntegerField(default=1)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField()), - ('like', models.IntegerField(default=0)), - ('is_useful', models.BooleanField(default=False)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('question', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='questions.question')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/questions/migrations/__pycache__/0001_initial.cpython-38.pyc b/questions/migrations/__pycache__/0001_initial.cpython-38.pyc deleted file mode 100644 index c19465a..0000000 Binary files a/questions/migrations/__pycache__/0001_initial.cpython-38.pyc and /dev/null differ diff --git a/questions/migrations/__pycache__/__init__.cpython-38.pyc b/questions/migrations/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 6c602e8..0000000 Binary files a/questions/migrations/__pycache__/__init__.cpython-38.pyc and /dev/null differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c365563 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +asgiref==3.4.1 +Django==3.2.6 +django-ckeditor==6.1.0 +django-js-asset==1.2.2 +djangorestframework==3.12.4 +djangorestframework-simplejwt==4.7.2 +Pillow==8.3.1 +PyJWT==2.1.0 +pytz==2021.1 +sqlparse==0.4.1 +typing==3.7.4.3 diff --git a/stackoverflow_api/__pycache__/jwt_authorization.cpython-38.pyc b/stackoverflow_api/__pycache__/jwt_authorization.cpython-38.pyc new file mode 100644 index 0000000..53ef483 Binary files /dev/null and b/stackoverflow_api/__pycache__/jwt_authorization.cpython-38.pyc differ diff --git a/stackoverflow_api/__pycache__/settings.cpython-38.pyc b/stackoverflow_api/__pycache__/settings.cpython-38.pyc index cf15b77..97ca6d6 100644 Binary files a/stackoverflow_api/__pycache__/settings.cpython-38.pyc and b/stackoverflow_api/__pycache__/settings.cpython-38.pyc differ diff --git a/stackoverflow_api/__pycache__/urls.cpython-38.pyc b/stackoverflow_api/__pycache__/urls.cpython-38.pyc index b3689ea..e3db1a7 100644 Binary files a/stackoverflow_api/__pycache__/urls.cpython-38.pyc and b/stackoverflow_api/__pycache__/urls.cpython-38.pyc differ diff --git a/stackoverflow_api/jwt_authorization.py b/stackoverflow_api/jwt_authorization.py new file mode 100644 index 0000000..7b5fefe --- /dev/null +++ b/stackoverflow_api/jwt_authorization.py @@ -0,0 +1,87 @@ +from django.conf import settings +from rest_framework import exceptions, authentication +from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed +import jwt + +from users.models import User + + +class JWTAuthentication(BaseAuthentication): + authentication_header_prefix = 'Bearer' + + def authenticate(self, request): + """ + Метод authenticate вызывается каждый раз, независимо от того, требует + ли того эндпоинт аутентификации. 'authenticate' имеет два возможных + возвращаемых значения: + 1) None - мы возвращаем None если не хотим аутентифицироваться. + Обычно это означает, что мы значем, что аутентификация не удастся. + Примером этого является, например, случай, когда токен не включен в + заголовок. + 2) (user, token) - мы возвращаем комбинацию пользователь/токен + тогда, когда аутентификация пройдена успешно. Если ни один из + случаев не соблюден, это означает, что произошла ошибка, и мы + ничего не возвращаем. В таком случае мы просто вызовем исключение + AuthenticationFailed и позволим DRF сделать все остальное. + """ + request.user = None + + # 'auth_header' должен быть массивом с двумя элементами: + # 1) именем заголовка аутентификации (Token в нашем случае) + # 2) сам JWT, по которому мы должны пройти аутентифкацию + auth_header = authentication.get_authorization_header(request).split() + auth_header_prefix = self.authentication_header_prefix.lower() + + if not auth_header: + return None + + if len(auth_header) == 1: + # Некорректный заголовок токена, в заголовке передан один элемент + return None + + elif len(auth_header) > 2: + # Некорректный заголовок токена, какие-то лишние пробельные символы + return None + + # JWT библиотека которую мы используем, обычно некорректно обрабатывает + # тип bytes, который обычно используется стандартными библиотеками + # Python3 (HINT: использовать PyJWT). Чтобы точно решить это, нам нужно + # декодировать prefix и token. Это не самый чистый код, но это хорошее + # решение, потому что возможна ошибка, не сделай мы этого. + prefix = auth_header[0].decode('utf-8') + token = auth_header[1].decode('utf-8') + + if prefix.lower() != auth_header_prefix: + # Префикс заголовка не тот, который мы ожидали - отказ. + return None + + # К настоящему моменту есть "шанс", что аутентификация пройдет успешно. + # Мы делегируем фактическую аутентификацию учетных данных методу ниже. + return self._authenticate_credentials(request, token) + + def _authenticate_credentials(self, request, token): + """ + Попытка аутентификации с предоставленными данными. Если успешно - + вернуть пользователя и токен, иначе - сгенерировать исключение. + """ + try: + payload = jwt.decode(token, settings.SECRET_KEY) + except Exception: + msg = 'Ошибка аутентификации. Невозможно декодировать токеню' + raise exceptions.AuthenticationFailed(msg) + + try: + user = User.objects.get(pk=payload['id']) + except User.DoesNotExist: + msg = 'Пользователь соответствующий данному токену не найден.' + raise exceptions.AuthenticationFailed(msg) + + if not user.is_active: + msg = 'Данный пользователь деактивирован.' + raise exceptions.AuthenticationFailed(msg) + + return (user, token) + + + diff --git a/stackoverflow_api/settings.py b/stackoverflow_api/settings.py index 7582658..8fa2443 100644 --- a/stackoverflow_api/settings.py +++ b/stackoverflow_api/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os +from datetime import timedelta from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. @@ -25,7 +26,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = ['*'] AUTH_USER_MODEL = 'users.User' @@ -35,6 +36,10 @@ 'ckeditor', 'questions.apps.QuestionsConfig', 'users', + 'learn_docs', + 'rest_framework', + 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -86,23 +91,29 @@ } } +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSESS': [ + 'custom_authorization.CustomAuthorization', + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ], +} # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.users.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.users.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.users.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.users.password_validation.NumericPasswordValidator', - }, + # { + # 'NAME': 'django.contrib.users.password_validation.UserAttributeSimilarityValidator', + # }, + # { + # 'NAME': 'django.contrib.users.password_validation.MinimumLengthValidator', + # }, + # { + # 'NAME': 'django.contrib.users.password_validation.CommonPasswordValidator', + # }, + # { + # 'NAME': 'django.contrib.users.password_validation.NumericPasswordValidator', + # }, ] @@ -111,7 +122,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Europe/Moscow' USE_I18N = True @@ -123,13 +134,61 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, 'static'), -] +if DEBUG: + STATIC_URL = '/static/' + MEDIA_URL = '/media/' + + STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'static'), + ] + MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +else: + STATIC_ROOT = '/var/www/static/' + + STATICFILES_DIRS = [ + Path(BASE_DIR, '/var/www/static/') + ] + MEDIA_URL = '/media/' + MEDIA_ROOT = Path(BASE_DIR, '/var/www/media/') + +DEFAULT_USER_PROFILE_LOGO = '/media/users/default/logo.png' +CUSTOM_USER_PROFILE_LOGO = 'media/default/{}/logo/' +GITHUB_LINK_PATTERN = 'https://github.com/{}' +TWITTER_LINK_PATTERN = 'https://twitter.com/{}' # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# SIMPLE JWT SETTINGS +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=5), ## вставляется в exp при генерации токенов + 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), + 'ROTATE_REFRESH_TOKENS': True, # при рефрешу токенов выдает новую пару + 'BLACKLIST_AFTER_ROTATION': True, # для этого используется специальноое приложение blacklist app. ('rest_framework_simplejwt.token_blacklist' в installed_apps) + 'UPDATE_LAST_LOGIN': True, + + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'VERIFYING_KEY': None, + 'AUDIENCE': None, + 'ISSUER': None, + 'JWK_URL': None, + + 'AUTH_HEADER_TYPES': ('Bearer',), + 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', + + 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), + 'TOKEN_TYPE_CLAIM': 'token_type', + + 'JTI_CLAIM': 'jti', + + 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', + 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), +} diff --git a/stackoverflow_api/urls.py b/stackoverflow_api/urls.py index 5a15d96..1653f4e 100644 --- a/stackoverflow_api/urls.py +++ b/stackoverflow_api/urls.py @@ -21,5 +21,6 @@ urlpatterns = [ path('admin/', admin.site.urls), path('admin-questions/', question_admin_site.urls), - path('', include('questions.urls')) + path('', include('users.urls')), + path('test/', include('learn_docs.urls')), ] diff --git a/users/__pycache__/__init__.cpython-38.pyc b/users/__pycache__/__init__.cpython-38.pyc index 282fee0..00fd82d 100644 Binary files a/users/__pycache__/__init__.cpython-38.pyc and b/users/__pycache__/__init__.cpython-38.pyc differ diff --git a/users/__pycache__/models.cpython-38.pyc b/users/__pycache__/models.cpython-38.pyc index d48c8f2..cbb3ab9 100644 Binary files a/users/__pycache__/models.cpython-38.pyc and b/users/__pycache__/models.cpython-38.pyc differ diff --git a/users/__pycache__/permissions.cpython-38.pyc b/users/__pycache__/permissions.cpython-38.pyc new file mode 100644 index 0000000..e912830 Binary files /dev/null and b/users/__pycache__/permissions.cpython-38.pyc differ diff --git a/users/__pycache__/serializers.cpython-38.pyc b/users/__pycache__/serializers.cpython-38.pyc new file mode 100644 index 0000000..1e5c416 Binary files /dev/null and b/users/__pycache__/serializers.cpython-38.pyc differ diff --git a/users/__pycache__/urls.cpython-38.pyc b/users/__pycache__/urls.cpython-38.pyc new file mode 100644 index 0000000..9fc424c Binary files /dev/null and b/users/__pycache__/urls.cpython-38.pyc differ diff --git a/users/__pycache__/views.cpython-38.pyc b/users/__pycache__/views.cpython-38.pyc new file mode 100644 index 0000000..36c335e Binary files /dev/null and b/users/__pycache__/views.cpython-38.pyc differ diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py index 355b468..488560a 100644 --- a/users/migrations/0001_initial.py +++ b/users/migrations/0001_initial.py @@ -1,9 +1,9 @@ -# Generated by Django 3.2.5 on 2021-08-10 12:44 +# Generated by Django 3.2.6 on 2021-08-17 15:56 -import django.contrib.auth.models -import django.contrib.auth.validators from django.db import migrations, models -import django.utils.timezone +import django.db.models.deletion +import django.db.models.manager +import uuid class Migration(migrations.Migration): @@ -18,28 +18,47 @@ class Migration(migrations.Migration): migrations.CreateModel( name='User', fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('login', models.CharField(blank=True, max_length=150)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('username', models.CharField(db_index=True, max_length=255, unique=True)), + ('email', models.EmailField(db_index=True, max_length=254, unique=True)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, + 'unique_together': {('username', 'email')}, }, + ), + migrations.CreateModel( + name='UserProfileContactInfo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('website_link', models.URLField(blank=True, null=True)), + ('github_link', models.URLField(blank=True, null=True)), + ('twitter_link', models.URLField(blank=True, null=True)), + ('website_link_active', models.BooleanField(default=False)), + ('github_link_active', models.BooleanField(default=False)), + ('twitter_link_active', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='UserProfile', + fields=[ + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='users.user')), + ('logo', models.ImageField(default='/media/users/default/logo.png', upload_to='media/default//logo/')), + ('location', models.CharField(blank=True, max_length=200, null=True)), + ('about', models.TextField(blank=True, max_length=255, null=True)), + ('title', models.CharField(blank=True, max_length=100, null=True)), + ('user_profile', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='users.userprofilecontactinfo')), + ], managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ('test', django.db.models.manager.Manager()), ], ), ] diff --git a/users/migrations/0002_rename_user_profile_userprofile_contacts.py b/users/migrations/0002_rename_user_profile_userprofile_contacts.py new file mode 100644 index 0000000..1b4923c --- /dev/null +++ b/users/migrations/0002_rename_user_profile_userprofile_contacts.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-08-17 16:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='userprofile', + old_name='user_profile', + new_name='contacts', + ), + ] diff --git a/users/migrations/__pycache__/0001_initial.cpython-38.pyc b/users/migrations/__pycache__/0001_initial.cpython-38.pyc index f694720..7b0fb70 100644 Binary files a/users/migrations/__pycache__/0001_initial.cpython-38.pyc and b/users/migrations/__pycache__/0001_initial.cpython-38.pyc differ diff --git a/users/migrations/__pycache__/0002_rename_user_profile_userprofile_contacts.cpython-38.pyc b/users/migrations/__pycache__/0002_rename_user_profile_userprofile_contacts.cpython-38.pyc new file mode 100644 index 0000000..cfc2b61 Binary files /dev/null and b/users/migrations/__pycache__/0002_rename_user_profile_userprofile_contacts.cpython-38.pyc differ diff --git a/users/migrations/__pycache__/__init__.cpython-38.pyc b/users/migrations/__pycache__/__init__.cpython-38.pyc index 2c6608e..9b2bff9 100644 Binary files a/users/migrations/__pycache__/__init__.cpython-38.pyc and b/users/migrations/__pycache__/__init__.cpython-38.pyc differ diff --git a/users/models.py b/users/models.py index dd4e152..16f24d5 100644 --- a/users/models.py +++ b/users/models.py @@ -1,9 +1,184 @@ -from django.contrib.auth.models import AbstractUser +import uuid +from datetime import datetime, timedelta + +from django.contrib.auth.base_user import BaseUserManager, AbstractBaseUser +from django.contrib.auth.models import AbstractUser, PermissionsMixin from django.db import models # Create your models here. +from django.db.models import Manager +from rest_framework_simplejwt.tokens import AccessToken, RefreshToken + +from stackoverflow_api import settings + + +class UserManager(BaseUserManager): + """ + Django требует, чтобы кастомные пользователи определяли свой собственный + класс Manager. Унаследовавшись от BaseUserManager, мы получаем много того + же самого кода, который Django использовал для создания User (для демонстрации). + """ + + def create_user(self, username, email, password=None): + """ Создает и возвращает пользователя с имэйлом, паролем и именем. """ + if username is None: + raise TypeError('Users must have a username.') + # + if email is None: + raise TypeError('Users must have an email address.') + + user = self.model(username=username, email=self.normalize_email(email)) + user.set_password(password) + user.save() + print(user.pk) + self._create_user_profile(user) + return user + + def create_superuser(self, username, email, password): + """ Создает и возввращет пользователя с привилегиями суперадмина. """ + if password is None: + raise TypeError('Superusers must have a password.') + user = self.create_user(username, email, password) + user.is_superuser = True + user.is_staff = True + user.save() + + return user + + def get(self, *args, **kwargs): + try: + user = super().get(*args, **kwargs) + return user + except User.DoesNotExist: + return None + + +class User(AbstractBaseUser, PermissionsMixin): + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + # Каждому пользователю нужен понятный человеку уникальный идентификатор, + # который мы можем использовать для предоставления User в пользовательском + # интерфейсе. Мы так же проиндексируем этот столбец в базе данных для + # повышения скорости поиска в дальнейшем. + username = models.CharField(db_index=True, max_length=255, unique=True) + + # Так же мы нуждаемся в поле, с помощью которого будем иметь возможность + # связаться с пользователем и идентифицировать его при входе в систему. + # Поскольку адрес почты нам нужен в любом случае, мы также будем + # использовать его для входы в систему, так как это наиболее + # распространенная форма учетных данных на данный момент (ну еще телефон). + email = models.EmailField(db_index=True, unique=True) + + # Когда пользователь более не желает пользоваться нашей системой, он может + # захотеть удалить свой аккаунт. Для нас это проблема, так как собираемые + # нами данные очень ценны, и мы не хотим их удалять :) Мы просто предложим + # пользователям способ деактивировать учетку вместо ее полного удаления. + # Таким образом, они не будут отображаться на сайте, но мы все еще сможем + # далее анализировать информацию. + is_active = models.BooleanField(default=True) + + # Этот флаг определяет, кто может войти в административную часть нашего + # сайта. Для большинства пользователей это флаг будет ложным. + is_staff = models.BooleanField(default=False) + + # Временная метка создания объекта. + created_at = models.DateTimeField(auto_now_add=True) + + # Временная метка показывающая время последнего обновления объекта. + updated_at = models.DateTimeField(auto_now=True) + + # Дополнительный поля, необходимые Django + # при указании кастомной модели пользователя. + + # Свойство USERNAME_FIELD сообщает нам, какое поле мы будем использовать + # для входа в систему. + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email', 'password'] + + # Сообщает Django, что определенный выше класс UserManager + # должен управлять объектами этого типа. + objects = UserManager() + + def __str__(self): + """ Строковое представление модели (отображается в консоли) """ + return self.username + + def get_full_name(self): + """ + Этот метод требуется Django для таких вещей, как обработка электронной + почты. Обычно это имя фамилия пользователя, но поскольку мы не + используем их, будем возвращать username. + """ + return self.username + + def get_short_name(self): + """ Аналогично методу get_full_name(). """ + return self.username + + def _create_user_profile(self): + """ Создает и инициализирует профиль юзера вместе с контактной информацией""" + user_profile = UserProfile() + user_profile.user = self + user_contact_info = UserProfileContactInfo() + user_profile.contacts = user_contact_info + user_contact_info.save() + user_profile.save() + + def save(self, *args, **kwargs): + """ Переопределил данный метод для включения логики по созданию дефолтного юзерпрофайл для пользователя""" + super().save(*args, **kwargs) + self._create_user_profile() + + class Meta: + unique_together = ('username', 'email', ) + +class UserProfileManager(models.Manager): + + def get_queryset(self): + return super().get_queryset().select_related('userprofilecontactinfo__user_profile') + +# TODO 1. Сделать уникальными все линки и добавить транзакцию в создание юзера +""" отдельная таблица нужна для того, чтобы потом создать отдельный сервис который будет проверять все это""" +class UserProfileContactInfo(models.Model): + """ Для того, чтобы сделать select_related при запросе на профиль""" + + website_link = models.URLField(blank=True, null=True) + github_link = models.URLField(blank=True, null=True) + twitter_link = models.URLField(blank=True, null=True) + + website_link_active = models.BooleanField(default=False) + github_link_active = models.BooleanField(default=False) + twitter_link_active = models.BooleanField(default=False) + + +class UserProfile(models.Model): + user = models.OneToOneField(User, primary_key=True, related_name='user_profile', to_field='id', on_delete=models.CASCADE) + contacts = models.OneToOneField(UserProfileContactInfo, blank=False, + on_delete=models.CASCADE) + logo = models.ImageField(blank=False, null=False, upload_to=settings.CUSTOM_USER_PROFILE_LOGO.format(id), + default=settings.DEFAULT_USER_PROFILE_LOGO) + location = models.CharField(blank=True, null=True, max_length=200) + about = models.TextField(blank=True, null=True, max_length=255) + title = models.CharField(blank=True, null=True, max_length=100) + + test = UserProfileManager() + objects = Manager() + + + + + + + + + + + + + + + + -class User(AbstractUser): - login = models.CharField(max_length=150, blank=True, unique=False) diff --git a/users/permissions.py b/users/permissions.py new file mode 100644 index 0000000..134a3b3 --- /dev/null +++ b/users/permissions.py @@ -0,0 +1,19 @@ +from rest_framework import permissions + + +class IsResourceOwner(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + return bool( + request.method in permissions.SAFE_METHODS + or request.user == obj.user + ) + + +class IsResourceOwnerAccount(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + return bool( + request.method in permissions.SAFE_METHODS + or request.user == obj + ) diff --git a/users/serializers.py b/users/serializers.py new file mode 100644 index 0000000..629056b --- /dev/null +++ b/users/serializers.py @@ -0,0 +1,109 @@ +from django.contrib.auth import get_user_model +from django.contrib.auth.hashers import make_password +from rest_framework import serializers +from rest_framework.validators import UniqueTogetherValidator + +from users.models import UserProfile, UserProfileContactInfo + +User = get_user_model() + + + + # def create(self, validated_data): + # return super().create(validated_data) + + # def create(self, validated_data): + # print(validated_data) + # return User.objects.create_user(**validated_data) + + # def validate(self, data): + # print(data) + # return data +# +# class MyTokenObtainPairSerializer(TokenObtainPairSerializer): +# @classmethod +# def get_token(cls, user): +# token = super().get_token(user) +# # Add custom claims +# token['username'] = user.name +# # ... +# print(token) +# return token + +class ChangePasswordSerializer(serializers.Serializer): + new_password = serializers.CharField() + repeat_new_password = serializers.CharField() + old_password = serializers.CharField() + + """ Здесь мы будем хранить логику по изменению пароля""" + def save(self, **kwargs): + user = kwargs.get('user') + user.set_password(self.validated_data.get('new_password')) + user.save() + return user + + def validate(self, attrs): + new_password = attrs.get('new_password') + + + # def save(self, **kwargs): + # user = kwargs.get('user') + # print(user) + # old_password = kwargs.get('password') + # print(old_password) + # new_password = kwargs.get('new_password') + # user.set_password(new_password) + # user.save() + + # def validate_old_password(self, value: str) -> str: + # if value == self.old_password: + # raise serializers.ValidationError('Пароли совпадают') + # return super().validate_password(value) + + +class UserProfileContactInfoSerializer(serializers.ModelSerializer): + class Meta: + model = UserProfileContactInfo + fields = ['website_link', 'twitter_link', 'github_link'] + + +class UserProfileSerializer(serializers.ModelSerializer): + contacts = UserProfileContactInfoSerializer(many=False, read_only=True) # так мы добавляем сериализованный обьект в другой обьект + + class Meta: + model = UserProfile + fields = ('logo', 'location', 'about', 'title', 'contacts') + + # def create(self, validated_data): + # return super().create(validated_data) + +class UserSerializer(serializers.ModelSerializer): + # username = serializers.CharField(validators=[UniqueValidator(queryset=User.objects.all())]) + user_profile = UserProfileSerializer(many=False, read_only=True) + + class Meta: + model = User + fields = ('id', 'username', 'password', 'email', 'user_profile') + extra_kwargs = { + 'password': { + 'write_only': True + } + } + depth = 2 + # validators = [ + # UniqueTogetherValidator( + # queryset=User.objects.all(), + # fields=('username', 'email') + # ) + # ] + + + def validate_password(self, value: str) -> str: + """ + Hash value passed by user. + + :param value: password of a user + :return: a hashed version of the password + """ + return make_password(value) + diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..c95464f --- /dev/null +++ b/users/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, include +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + +from .views import GetUserAPIView, UserCreateAPIView, UserProfileDetailAPIView, ChangePasswordAPIView + +urlpatterns = [ + path('hello/', GetUserAPIView.as_view(), name='hello'), + path('auth/', TokenObtainPairView.as_view(), name='auth'), + path('refresh/', TokenRefreshView.as_view(), name='auth'), + path('register/', UserCreateAPIView.as_view(), name='reg'), + path('/profile/', UserProfileDetailAPIView.as_view(), name='profile'), + path('/profile/change/password/', ChangePasswordAPIView.as_view(),) + # path('test///', TestLookup.as_view()), +] \ No newline at end of file diff --git a/users/utils.py b/users/utils.py new file mode 100644 index 0000000..b444a14 --- /dev/null +++ b/users/utils.py @@ -0,0 +1,11 @@ + +from rest_framework.views import exception_handler + +def user_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + if response is not None: + response.data['status_code'] = response.status_code + return response diff --git a/users/views.py b/users/views.py index 91ea44a..3e2c249 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,104 @@ -from django.shortcuts import render - +from django.contrib.auth import get_user_model +from rest_framework import generics, status, permissions # Create your views here. +from rest_framework.generics import get_object_or_404 + +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework_simplejwt.authentication import JWTAuthentication + +from users.models import UserProfile +from users.permissions import IsResourceOwner, IsResourceOwnerAccount +from users.serializers import UserSerializer, UserProfileSerializer, ChangePasswordSerializer + +User = get_user_model() + +class GetUserAPIView(generics.RetrieveAPIView): + permission_classes = (permissions.IsAuthenticated, ) + authentication_classes = (JWTAuthentication, ) + + def get(self, request, *args, **kwargs): + return Response('1') + +## чтобы уж совсем не быть ленивым, я напишу свою регистрацию + +class UserCreateAPIView(generics.CreateAPIView): + permission_classes = (permissions.AllowAny, ) + model = get_user_model() + serializer_class = UserSerializer + + ## этого можно было и не делать, но чисто для себя я написал + def post(self, request, *args, **kwargs): + user = request.data + serializer = self.serializer_class(data=user) + if serializer.is_valid(raise_exception=True): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +# class TestLookup(generics.RetrieveAPIView): +# multiple_lookup_fields = ('username', 'email', ) +# # пример как сделать поиск по нескольким лукапам +# # данный метод вызывается тогда, когда достается обьект по лукапам +# # тут недоделанный вариант, пока нет в этом необходимости +# def get_object(self): +# queryset = self.get_queryset() +# filter = {} +# for field in self.multiple_lookup_fields: +# filter[field] = self.kwargs[field] +# obj = get_object_or_404(queryset, **filter) +# self.check_object_permissions(self.request, obj) +# return obj + +class UserProfileDetailAPIView(generics.RetrieveUpdateAPIView): + queryset = UserProfile.objects.all() + lookup_field = 'username' + permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsResourceOwner) ## данный пермиссионс требует авторизации только в тех методах запросов, которые изменяют данные + authentication_classes = (JWTAuthentication, ) + serializer_class = UserProfileSerializer + + def get(self, request, *args, **kwargs): + data = self.get_object() + serializer = self.serializer_class(data) + return Response(serializer.data, status=status.HTTP_200_OK) + + # def patch(self, request, *args, **kwargs): + # return super().patch(request, *args, **kwargs) + + def get_object(self) -> UserProfile: + user = User.objects.get(username=self.kwargs.get(self.lookup_field)) ## вот так достаем из lookup + obj = get_object_or_404(self.get_queryset(), user=user) + self.check_object_permissions(self.request, obj) + return obj + + + + +class ChangePasswordAPIView(generics.UpdateAPIView): + queryset = User.objects.all() + permission_classes = (permissions.IsAuthenticated, IsResourceOwnerAccount) + authentication_classes = (JWTAuthentication, ) + serializer_class = ChangePasswordSerializer + lookup_field = 'username' + model = get_user_model() + + def post(self, request, **kwargs): + serializer = self.get_serializer(data=request.data) + if serializer.is_valid(raise_exception=True): + serializer.save(user=self.get_object()) + return Response(serializer.validated_data) + + def get_object(self): + obj = get_object_or_404(self.get_queryset(), username=self.kwargs.get(self.lookup_field)) + self.check_object_permissions(self.request, obj) + return obj + + + + + + + + + +