지난 9월 중순부터 부산 연합 동아리 PROJECT 웹사이트 백엔드를 맡아서 홈페이지를 개설하게 되었다.
아직 미완성이고 프론트와 협의를 거칠 부분이 많이 존재해서 수정작업이 더 필요하지만, 수정될 때마다 이 글도 수정하기로 하고 일단 지금까지 어떤식으로 작업했는지 기록을 해두려고 한다.
우선 반드시 필요한 기능으로는
- 회원가입/로그인
- 공지사항
- 활동기록
이 정도를 꼽을 수 있었다.
django 파일의 현재 파일 tree 구조이다.
.
├── FAQs
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── apis.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── models.py
│ ├── tests.py
│ └── urls.py
├── README.md
├── activity
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── apis.py
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── models.py
│ ├── serializers.py
│ ├── tests.py
│ └── urls.py
├── api
│ ├── __init__.py
│ ├── __pycache__
│ ├── migrations
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── mixins.py
│ └── urls.py
├── auth
│ ├── __init__.py
│ ├── __pycache__
│ ├── apis.py
│ ├── apps.py
│ ├── authenticate.py
│ ├── googleapi.py
│ ├── kakaoapi.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── __pycache__
│ ├── naverapi.py
│ ├── services.py
│ ├── test.py
│ └── urls.py
├── boards
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── api_category.py
│ ├── api_comment.py
│ ├── api_post.py
│ ├── api_reply.py
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── test.py
│ └── urls.py
├── config
│ ├── __init__.py
│ ├── __pycache__
│ ├── asgi.py
│ ├── settings
│ │ ├── __pycache__
│ │ ├── base.py
│ │ ├── local.py
│ │ └── prod.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
├── docker-compose.yml
├── dummydata.py
├── local.Dockerfile
├── manage.py
├── media
│ ├── activity_detail
│ ├── activity_detail_file
│ ├── activity_thumbnail
│ ├── django-summernote
│ ├── post_thumbnail
│ ├── profile_image
│ └── upload_file_post
├── nginx.conf
├── project.crt
├── project.key
├── requirements.txt
├── reservations
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── apis.py
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── tests.py
│ └── urls.py
├── server.Dockerfile
├── sotongapp
│ ├── __init__.py
│ ├── __pycache__
│ ├── admin.py
│ ├── apis.py
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ ├── models.py
│ ├── serializer.py
│ ├── urls.py
│ └── utils.py
├── superuser.py
├── swagger
│ ├── __init__.py
│ ├── __pycache__
│ ├── apps.py
│ ├── migrations
│ │ ├── __init__.py
│ ├── urls.py
│ └── views.py
├── templates
│ └── recovery_email.html
└── users
├── __init__.py
├── __pycache__
├── admin.py
├── apis.py
├── apps.py
├── migrations
│ ├── 0001_initial.py
│ ├── __init__.py
│ └── __pycache__
├── models.py
├── serializers.py
├── services.py
├── test.py
├── urls.py
└── utils.py
폴더 구성 설계
홈페이지 상에서 다양한 기능을 추가할 가능성이 매우 높기 때문에 나중 유지보수를 생각한다면 백엔드의 엔드포인트 구성과 폴더 구조가 정말 중요할것이다.
그림이 약간 징그럽고 문어같다.
홈페이지를 구성하는 백엔드의 엔드포인트를 트리구조로 나타내 보았다.
api/
api서버를 구축했기 때문에 브라우저의 모든 요청은 api/로 시작하는 엔드포인트로 보내지도록 했다.
v1/
카카오 오픈 api나 구글 오픈 api를 보니까 v1, v2 이런식으로 api의 버전을 구분하는 것을 볼 수 있었다.
좋은 방법같아서 바로 따라했다.
서버를 만들고 아직까지 별다른 업데이트가 없기 때문에 v1만 사용중이다.
sotong/
7회 부산 소통고리 공모전에서 사용했던 웹 사이트를 홈페이지와 합치면서 생겨난 엔드포인트 시작점이다.
swagger/
api url을 프론트엔드 개발자들과 공유하기 위해서 swagger를 사용하려고 만든 엔드포인트 시작점이다.
회원가입/로그인 기능을 위한 User모델
## users/models.py
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractUser
from django.core import validators
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from django.utils.deconstruct import deconstructible
from boards.models import Post, Category, Comment, Reply
@deconstructible
class UnicodeUsernameValidator(validators.RegexValidator):
regex = r'^[\w.@+-]+\Z'
message = _(
'Enter a valid username. This value may contain only letters, '
'numbers, and @/./+/-/_ characters.'
)
flags = 0
class UserManager(BaseUserManager):
@transaction.atomic
def create_user(self, username, email, password=None, **extra_fields):
print("Create User by manager")
if not username:
raise ValueError('아이디는 필수 항목입니다.')
if not email:
raise ValueError('이메일은 필수 항목입니다.')
if not password:
raise ValueError('패드워드는 필수 항목입니다.')
user = self.model(
username=username,
email = self.normalize_email(email)
)
user.set_password(password)
user.full_clean()
user.save()
profile = Profile(user=user, nickname=username)
profile.save()
return user
def create_superuser(self, username, email=None, password=None):
user = self.create_user(
email=self.normalize_email(email),
username=username,
password=password
)
user.is_admin = True
user.is_superuser = True
user.is_staff = True
user.save()
return user
class User(AbstractUser):
username_validator = UnicodeUsernameValidator()
username = models.CharField(
_('username'),
max_length=150,
unique=True,
help_text=_('Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.'),
validators=[username_validator],
error_messages={
'unique': _("A user with that username already exists."),
},
)
email = models.EmailField(
_('email address'),
unique=True,
blank=True
)
objects = UserManager()
class Meta:
swappable = 'AUTH_USER_MODEL'
def __str__(self):
return self.username
@property
def name(self):
if not self.last_name:
return self.first_name.capitalize()
return f'{self.first_name.capitalize()} {self.last_name.capitalize()}'
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
auth = models.CharField(max_length=128, null=True, blank=True)
realname = models.CharField(max_length=64, null=True, blank=True)
nickname = models.CharField(max_length=64, unique=True, null=True, blank=True)
image = models.ImageField(
default='profile_image/basic_profile.png',
upload_to='profile_image/',
null=True, blank=True
)
introduce = models.TextField(null=True, blank=True)
is_project = models.BooleanField(default=False)
signup_path = models.CharField(max_length=64, default='basic')
favorite_category = models.ManyToManyField(Category, blank=True, related_name='favorite_user')
favorite_post = models.ManyToManyField(Post, blank=True, related_name='favorite_user')
favorite_comment = models.ManyToManyField(Comment, blank=True, related_name='favorite_user')
favorite_reply = models.ManyToManyField(Reply, blank=True, related_name='favorite_user')
def __str__(self):
return self.user.username
홈페이지 자체의 회원 모델이 될 User Model이다.
class User(AbstractUser) 설명
기본적인 User의 필드는 AbstractUser클래스가 가지고 있는 필드를 상속받아 사용한다.
username과 username_validator 필드는 오버라이딩 할 필요없었지만 공부목적으로 한 것이다. (없어도 상관없음)
email필드는 unique=True설정을 위해서 오버라이딩 하였다.
objects 필드를 내가 임의로 생성한 UserManager클래스로 오버라이딩 시켜주었다.
Meta클래스의 swappable 을 'AUTH_USER_MODEL'로 설정해서, 새로 만든 User모델을 Django기본 User 모델과 스왑이 가능하도록 설정해주었다.
settings.py에
AUTH_USER_MODEL = "users.User"
을 적어주면 된다.
@property 를 사용해서 User.name을 가져올 때 사용할 getter를 지정해 주었다.
따라서 앞으로 user의 이름을 호출할 때는 항상 name 메서드가 사용되어 호출될 것이고 모두 대문자로 정규화 되어서 나타날 것이다.
class Profile(models.Model) 설명
기본 User모델과 1:1 연결시켜주었다. related_name="profile"로 지정해줌으로서 user에서 profile을 접근할 때 user.profile로 접근할 수 있게 되었다.
auth 필드는 이메일 인증을 위한 필드이다.
비밀번호 찾기 기능을 사용할 때 등록했던 이메일로 인증을 하게 된다.
이때 이메일로 발송되는 문장이 auth필드에 저장되고, 유저가 해당 코드를 똑같이 입력한다면 인증이 완료되어 비밀번호를 변경할 수 있게 해주는 역할이다.
실명과 닉네임을 위한 realname, nickname필드이다.
nickname은 중복을 허용하지 않기로 했으므로 unique=True 로 설정해주었다.
프로필 사진을 위한 image 필드이다.
기본 이미지와 업로드 되면 사진을 저장해 둘 공간을 지정해 주었다.
is_project 필드는 회원가입을 할 때 처음에 PROJECT 동아리 회원인지, 일반 회원인지를 구분하는 체크박스를 선택하게 할건데 이를 구분해 주기위한 필드이다.
signup_path 필드는 가입자가 어떤 경로로 가입을 했는지 기록하는 필드이다. 소셜로그인 기능이 kakao, naver, google 이렇게 3가지가 있기 때문에 소셜로그인인지, 웹사이트 자체 회원가입인지 구별하는 역할이다.
favorite_... 해당 필드들은 게시판과 관련된 외래키 필드들인데, 자신이 좋아요한 카테고리, 글, 댓글, 대댓글을 기록하는 역할이다.
class UserManager(BaseUserManager) 설명
user와 superuser의 create를 담당한다.
BaseUserManager를 상속받아 사용하기 때문에 이외의 기능은 모두 기본 기능과 동일하다.
create_user는 transaction.atomic 데코레이터를 사용해서 User를 생성할 때 원자성을 보장해준다.
transaction이 필요한 이유로는 다음과 같다.
create_user에는 User모델을 만들고 Profile모델도 같이 만들기 때문에 중간에 오류가 생겨서 User모델만 생긴채로 남아있을 수 있다. 이렇게 되면 계속해서 dummy데이터가 쌓이게 될 것이고, 관리자의 불편을 초래할 수도 있을 것이다.
class UnicodeUsernameValidator(validators.RegexValidator)
username필드의 유효성을 검사해주는 필드이다.
@deconstructible데코레이터가 붙어있다.
As long as all of the arguments to your class’ constructor are themselves serializable, you can use the @deconstructible class decorator from django.utils.deconstruct to add the deconstruct() method:
공식문서에 deconstructible에 대해서 위와 같이 적혀있다.
@deconstructible이 없다면 makemigrations를 수행할 때 오류가 발생할 수 있다.
urls.py
## users/urls.py
from django.urls import path
from users.apis import \
UserMeApi, FindIDApi, SendPasswordEmailApi, \
ResetPasswordApi, ConfirmPasswordEmailApi, UserCreateApi
app_name = 'users'
urlpatterns = [
path('me', UserMeApi.as_view(), name="me"),
path('me/', UserCreateApi.as_view(), name="createuser"),
path('me/id/', FindIDApi.as_view(), name="findid"),
path('password/code', SendPasswordEmailApi.as_view(), name="sendpw"),
path('password/verifycode', ConfirmPasswordEmailApi.as_view(), name="confirmpw"),
path('password/reset', ResetPasswordApi.as_view(), name="resetpw"),
]
user와 관련된 기능을 수행하는 엔드포인트들이다.
아래는 urlpatterns에서 위에서부터 차례대로 각 엔드포인트들과 연결된 기능들이다
- me : 유저 정보 가져오기, 비밀번호 수정하기, 삭제하기
- me/ : 유저 생성하기
- me/id/ : 유저 아이디 찾기
- password/code : 유저 비밀번호 변경을 위한 인증코드 발송
- password/verifycode : 유저 비밀번호 변경을 위한 인증코드 확인
- password/reset : 유저 비밀번호 변경
여기서 정말 개선하고 싶은 구조가 있다..
현재 유저 정보 가져오기, 비밀번호 수정하기, 삭제하기와 유저 생성하기 기능이 서로 분리된 엔드포인트를 사용하고 있다. (me, me/)
/ (슬래시) 하나로 구분되는 api라니.. postman에서도 post는 반드시 끝에 /가 붙어야 한다고 해서 저렇게 구분했지만 HTTP 메소드도 겹치지 않고, 모두 유저의 CRUD에 관련되었기 때문에 하나로 통합 할 수도 있다고 생각한다.
하지만 내가 저렇게 나눈 이유는 '인증' 때문이다..
현재 유저정보 가져오기, 비밀번호 수정, 유저 삭제 기능들은 반드시 유저가 로그인이 되어있는 상태에서 호출이 되어야 한다. 하지만 유저 회원가입 같은 경우에는 당연히 모든 사용자가 회원가입 기능에 접근이 가능해야 할 것이다. 그래서 서로 다른 인증 방법을 가진 2개의 view를 만들게 되었다..
방법을 찾게 되면 바로 여기에 정리해보겠다.
>>
REST 방식을 따르기 위해서는
엔드포인트의 끝에 '/' 슬래시를 붙이는 것을 `지양`해야 한다.
path('new', UserCreateApi.as_view(), name="createuser") -> path('new', UserCreateApi.as_view(), name="createuser")
회원가입/로그인을 위한 api 코드
## users/apis.py
from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView
from django.db import transaction
from django.db.models import Q
from django.conf import settings
from django.core import exceptions
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password
from django.core.management.utils import get_random_secret_key
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from api.mixins import ApiAuthMixin, PublicApiMixin
from auth.authenticate import jwt_login
from users.models import Profile
from users.serializers import RegisterSerializer, UserSerializer,\
PasswordChangeSerializer, validate_password12
from users.services import send_mail, email_auth_string
User = get_user_model()
class UserMeApi(ApiAuthMixin, APIView):
def get(self, request, *args, **kwargs):
"""
현재 로그인 된 유저의 모든 정보 반환
username, email, last_login, profile etc...
"""
if request.user is None:
raise exceptions.PermissionDenied('PermissionDenied')
username = request.user.username
user_query = User.objects\
.filter(Q(username=username))\
.prefetch_related(
'profile__favorite_post',
'profile__favorite_post__favorite_user',
'profile__favorite_post__creator',
'profile__favorite_post__category',
'profile__favorite_category',
'profile__favorite_category__favorite_user',
)
return Response(UserSerializer(user_query, many=True, context={'request':request}).data)
def put(self, request, *args, **kwargs):
"""
현재 로그인 된 유저의 비밀번호 변경
"""
user = request.user
if not check_password(request.data.get("oldpassword"), user.password):
raise serializers.ValidationError(
_("passwords do not match")
)
serializer = PasswordChangeSerializer(data=request.data, partial=True)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_409_CONFLICT)
validated_data = serializer.validated_data
print(validated_data)
serializer.update(user=user, validated_data=validated_data)
return Response({
"message": "Change password success"
}, status=status.HTTP_200_OK)
def delete(self, request, *args, **kwargs):
"""
현재 로그인 된 유저 삭제
소셜 로그인 유저는 바로 삭제.
일반 회원가입 유저는 비밀번호 입력 후 삭제.
"""
user = request.user
signup_path = user.profile.signup_path
if signup_path == "kakao" or signup_path == "google":
user.delete()
return Response({
"message": "Delete user success"
}, status=status.HTTP_204_NO_CONTENT)
if not check_password(request.data.get("password"), user.password):
raise serializers.ValidationError(
_("passwords do not match")
)
user.delete()
return Response({
"message": "Delete user success"
}, status=status.HTTP_204_NO_CONTENT)
class UserCreateApi(PublicApiMixin, APIView):
@transaction.atomic
def post(self, request, *args, **kwargs):
"""
회원가입 api
user 모델과 profile 모델이 반드시 같이 생성되어야 하기 때문에
transaction 적용
"""
serializer = RegisterSerializer(data=request.data)
if not serializer.is_valid(raise_exception=True):
return Response({
"message": "Request Body Error"
}, status=status.HTTP_409_CONFLICT)
user = serializer.save()
profile = Profile(user=user, nickname=user.username, introduce="소개를 작성해주세요.")
profile.save()
response = Response(status=status.HTTP_200_OK)
response = jwt_login(response=response, user=user)
return response
class FindIDApi(PublicApiMixin, APIView):
def post(self, request, *args, **kwargs):
"""
유저 아이디 찾기
email 입력을 통해 아이디를 찾을 수 있다.
email 필드가 unique인 이유.
"""
target_email = request.data.get('email', '')
user = User.objects.filter(email=target_email)
if not user.exists():
return Response({
"message": "user not found"
}, status=status.HTTP_404_NOT_FOUND)
data = {
"username": user.first().username
}
return Response(data, status=status.HTTP_200_OK)
class SendPasswordEmailApi(PublicApiMixin, APIView):
def post(self, request, *args, **kwargs):
"""
비밀번호 변경 인증 코드 발송
"""
target_username = request.data.get('username', '')
target_email = request.data.get('email', '')
target_user = User.objects.filter(
username=target_username,
email=target_email
)
if target_user.exists():
auth_string = email_auth_string()
target_user.first().profile.auth = auth_string
target_user.first().profile.save()
try:
send_mail(
'[PROJECT:HOME] 비밀번호 찾기 인증 메일입니다.',
recipient_list=[target_email],
html=render_to_string('recovery_email.html', {
'auth_string': auth_string,
})
)
except:
return Response({
"message": "email error",
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
"message": "Verification code sent"
}, status=status.HTTP_200_OK)
else:
return Response({
"message": "User does not exist"
}, status=status.HTTP_404_NOT_FOUND)
class ConfirmPasswordEmailApi(PublicApiMixin, APIView):
def post(self, request, *args, **kwagrs):
"""
1. 인증 코드 확인
2. 해당 username으로 로그인
"""
target_username = request.data.get('username', '')
target_code = request.data.get('code', '')
user = User.objects.get(username=target_username)
profile = user.profile
if profile.auth == target_code:
profile.auth = get_random_secret_key()
profile.save()
response = Response({
"message": "Verification success",
"user": target_username,
}, status=status.HTTP_202_ACCEPTED)
response = jwt_login(response=response, user=user)
return response
else:
return Response({
"message": "Verification Failed"
}, status=status.HTTP_401_UNAUTHORIZED)
class ResetPasswordApi(ApiAuthMixin, APIView):
def post(self, request, *args, **kwargs):
"""
비밀번호 찾기 이메일 인증 이후
비밀번호 변경 api
"""
password1 = request.data.get('password1', '')
password2 = request.data.get('password2', '')
user = request.user
newpassword = validate_password12(password1, password2)
user.set_password(newpassword)
user.save()
response = Response({
"message": "Reset password success! Go to login page"
}, status=status.HTTP_202_ACCEPTED)
response.delete_cookie(settings.JWT_AUTH['JWT_AUTH_COOKIE'])
return response
UserMeApi
def get()
현재 로그인 되어있는 유저의 정보들을 반환한다.
기본 User 정보들과 Profile 모델의 정보들, 자신이 좋아요한 글과 카테고리정보를 보여준다.
User에서 모든 정보를 역참조해서 가져오고 최대 3번까지 역참조를 하기 때문에 그냥 Serializer를 사용해서 json을 반환하도록 하면 쿼리수가 기하급수적으로 늘어나서 성능이 매우 나빠진다. 한 유저의 좋아요 한 포스팅의 수가 많아질 수록 성능은 급격하게 나빠질 것이다.
따라서 prefetch_related를 사용해서 serializer가 필요한 정보들을 미리 db에서 fetch해 오는 방법으로 쿼리수를 확 줄이게 되었다.
아래 그림은 admin계정이 좋아요 한 포스팅의 수를 100개 가량으로 설정 한 후 쿼리문 테스트를 한 결과이다.(prefetch_related X)
중첩된 외래키가 많아서 440개의 쿼리가 발생함을 볼 수 있다.
이렇게 쿼리가 많아지면 local에서 테스트 하는데도 약간의 딜레이가 생긴다. 실제 서비스에서 이런 쿼리를 방치한다면 해당 기능을 사용하는 사람이 많아지면 사실상 사용이 불가능해지지 않을까 싶다.
아래 그림은 prefetch_related를 사용해서 쿼리 수를 확연히 줄이고 중복 쿼리도 없앤 결과이다.
매우 깔끔해졌고 테스트 하는데도 딜레이가 발생하지 않았다!
유저 관리 serializer
## users/serializers.py
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from users.models import Profile
from boards.serializers import PostListSerializer, CategorySerializer
User = get_user_model()
def validate_password12(password1, password2):
validate_condition = [
lambda s: all(x.islower() or x.isupper() or x.isdigit() or (x in ['!', '@', '#', '$', '%', '^', '&', '*', '_']) for x in s), ## 영문자 대소문자, 숫자, 특수문자(리스트)만 허용
lambda s: any(x.islower() or x.isupper() for x in s), ## 영어 대소문자 필수
lambda s: any((x in ['!', '@', '#', '$', '%', '^', '&', '*', '_']) for x in s), ## 특수문자 필수
lambda s: len(s) == len(s.replace(" ","")),
lambda s: len(s) >= 6, ## 글자수 제한
lambda s: len(s) <= 20, ## 글자수 제한
]
for validator in validate_condition:
if not validator(password1):
raise serializers.ValidationError(
_("password ValidationError")
)
if not password1 or not password2:
raise serializers.ValidationError(
_("need two password fields")
)
if password1 != password2:
raise serializers.ValidationError(
_("password fields didn't match!"))
return password1
class ProfileSerializer(serializers.ModelSerializer):
favorite_category = CategorySerializer(read_only=True, many=True)
favorite_post = PostListSerializer(read_only=True, many=True)
class Meta:
model = Profile
fields = [
'nickname',
'realname',
'image',
'introduce',
'is_project',
'signup_path',
'favorite_category',
'favorite_post',
]
class UserSerializer(serializers.ModelSerializer):
profile = ProfileSerializer(read_only=True)
class Meta:
model = User
fields = [
'id',
'username',
'is_superuser',
'email',
'profile',
'last_login',
'date_joined',
]
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(max_length=150, write_only=True)
email = serializers.EmailField(write_only=True)
password1 = serializers.CharField(write_only=True)
password2 = serializers.CharField(write_only=True)
def validate_email(self, email):
if not email:
raise serializers.ValidationError(
_("email field not allowed empty")
)
used = User.objects.filter(email__iexact=email)
if used.count() > 0:
raise serializers.ValidationError(
_("A user is already registered with this e-mail address."))
return email
def validate_username(self, username):
validate_condition = [
lambda s: all(x.islower() or x.isdigit() or '_' for x in s), ## 영문자 대소문자, 숫자, 언더바(_)만 허용
lambda s: any(x.islower() for x in s), ## 영어 소문자 필수
lambda s: len(s) == len(s.replace(" ","")),
lambda s: len(s) >= 3, ## 글자수 제한
lambda s: len(s) <= 20, ## 글자수 제한
]
for validator in validate_condition:
if not validator(username):
raise serializers.ValidationError(
_("username ValidationError")
)
if not username:
raise serializers.ValidationError(
_("username field not allowed empty")
)
used = User.objects.filter(username__iexact=username).first()
if used:
raise serializers.ValidationError(
_("A user is already registered with this username."))
return username
def validate(self, data):
data['password1'] = validate_password12(data['password1'], data['password2'])
data['email'] = self.validate_email(data['email'])
data['username'] = self.validate_username(data['username'])
print("check validate ALL")
return data
def get_cleaned_data(self):
return {
'username': self.validated_data.get('username', ''),
'password1': self.validated_data.get('password1', ''),
'email': self.validated_data.get('email', '')
}
def create(self, validated_data):
user = User.objects.create_user(
username=validated_data["username"],
email=validated_data["email"],
password=validated_data["password1"]
)
return user
class PasswordChangeSerializer(serializers.Serializer):
oldpassword = serializers.CharField(write_only=True)
newpassword1 = serializers.CharField(write_only=True)
newpassword2 = serializers.CharField(write_only=True)
def validate_password(self, oldpassword, newpassword1, newpassword2):
if oldpassword == newpassword1 or oldpassword == newpassword2:
raise serializers.ValidationError(
_("oldpw and newpw are same either"))
newpassword = validate_password12(newpassword1, newpassword2)
return newpassword
def validate(self, data):
data['newpassword1'] = self.validate_password(
data['oldpassword'], data['newpassword1'], data['newpassword2'])
return data
def update(self, user, validated_data):
user.set_password(validated_data.get('newpassword1'))
user.save()
return user
Serializer를 사용하니까 serializing 작업을 api view와 분리시켜서 작업할 수 있다는 장점을 확실히 느낄 수 있었다.
특히 validation 과정을 하나의 serializer에서 처리해주도록 설정이 가능하기 때문에 쓰면 쓸 수록 좋은 기능인 것 같다.
'Back-End > Django' 카테고리의 다른 글
[Djnago] Django 이메일 인증하기(Thread) (0) | 2021.10.27 |
---|---|
[Django] Django Api 인증, 권한 설정 (0) | 2021.10.27 |
[Django] ORM 쿼리 최적화 (select_related, annotate, aggregates) (0) | 2021.10.11 |
[Security] XSS(Cross Site Scripting) 취약점 Django (0) | 2021.09.02 |
[Django] Google 소셜 로그인 (OAuth2.0) (13) | 2021.09.02 |