WIL 2022.06.20 DRF/ Custom User, login/out, 역참조, Serializer
Custom user 생성
# user/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
# custom user model 사용 시 UserManager 클래스와 create_user, create_superuser 함수가 정의되어 있어야 함
class UserManager(BaseUserManager):
def create_user(self, username, password=None):
if not username:
raise ValueError('Users must have an username')
user = self.model(
username=username,
)
user.set_password(password)
user.save(using=self._db)
return user
# python manage.py createsuperuser 사용 시 해당 함수가 사용됨
def create_superuser(self, username, password=None):
user = self.create_user(
username=username,
password=password
)
user.is_admin = True
user.save(using=self._db)
return user
class User(AbstractBaseUser):
username = models.CharField("사용자계정", max_length=50, unique=True)
password = models.CharField("비밀번호", max_length=128)
email = models.EmailField("이메일", max_length=100)
fullname = models.CharField("이름", max_length=20)
join_date = models.DateTimeField("가입일", auto_now_add=True)
is_active = models.BooleanField(default=True) # 계정활성화 여부
is_admin = models.BooleanField(default=False) # 관리자 계정 여부
USERNAME_FIELD = 'username' # 로그인 시 사용할 필드 지정
REQUIRED_FIELDS = [] # createsuperuser 할 때 추가로 요구할 필드 지정
objects = UserManager() # custom user 생성 시 필요
def __str__(self):
return self.username
# 로그인 사용자의 특정 테이블의 crud 권한을 설정, perm table의 crud 권한이 들어간다.
# admin일 경우 항상 True, 비활성 사용자(is_active=False)의 경우 항상 False
def has_perm(self, perm, obj=None):
return True
# 로그인 사용자의 특정 app에 접근 가능 여부를 설정, app_label에는 app 이름이 들어간다.
# admin일 경우 항상 True, 비활성 사용자(is_active=False)의 경우 항상 False
def has_module_perms(self, app_label):
return True
# admin 권한 설정
@property
def is_staff(self):
return self.is_admin
class Hobbies(models.Model):
name = models.CharField("취미", max_length=20)
def __str__(self):
return self.name
class UserProfile(models.Model):
user = models.OneToOneField(User, verbose_name="유저",on_delete=models.CASCADE)
discription = models.TextField("자기소개", null=True, blank=True)
hobby = models.ManyToManyField(Hobbies, verbose_name="취미")
birthday = models.DateField("생일")
def __str__(self):
return self.user.username
모델 생성 후 settings.py 에 아래의 코드를 추가 함으로써 위에 정의한 모델을 Authenticate 사용할 때 적용되도록 꼭 설정해줘야만한다!
AUTH_USER_MODEL = 'user.User' # app.model
admin 페이지에서 출력할 __str__ 메서드 함수 정의 시 외래키로 지정할 경우 아래와 같이 타입에러가 발생된다.
class UserProfile(models.Model):
user = models.OneToOneField(User, verbose_name="유저",on_delete=models.CASCADE)
discription = models.TextField("자기소개", null=True, blank=True)
hobby = models.ManyToManyField(Hobbies, verbose_name="취미")
birthday = models.DateField("생일")
def __str__(self):
# return self.user -> TypeError 발생!
return self.user.username
OneToOneField 반환값으로는 object가 반환 되기 때문에 __str__ 함수에서 받았을 때 string이 아닌 object를 return 할 수 없다고 오류가 나기 때문에 return 값을 self.user.username 의 형태로 확실하게 명시해주면 오류가 해결되었다.
url 연결!
# DRF_study/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('user/', include('user.urls')),
path('blog/', include('blog.urls')),
]
기존 처럼 연결하되 CBV로 작성하였기때문에 as_view() 를 달아주기만하면 알아서 http method 따라 가도록 설정된다.
# user/urls.py
from django.urls import path
from user import views
urlpatterns = [
# user/
path('', views.UserAPIView.as_view()),
]
User views.py 로그인, 로그아웃
UserAPIView를 만들때 APIView를 상속 받아야하기 때문에 import를 꼭 받아와야한다.
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from django.contrib.auth import login, authenticate, logout
from user.models import User
# Create your views here.
class UserAPIView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
username = request.data.get('username', '')
password = request.data.get('password', '')
user = authenticate(request, username=username, password=password)
if not user:
return Response({'error': '아이디와 패스워드를 확인해주세요!'})
login(request, user)
return Response({'success': '로그인 성공!'})
def delete(self, request):
logout(request)
return Response({'success': '로그아웃 성공!'})
역참조
외래 키 지정 시 related_name 옵션을 사용해 역참조 시 사용될 이름을 지정할 수 있다.
releated_name을 지정하지 않는다면 기본적으로 tablename_set 형태로 지정된다.
ex1) user_profile.hobby → 정참조
ex2) hobby.userprofile_set → hobby를 참조하고 있는 UserProfile 테이블의 object를 져옴
models.py에서 releated_name을 user_hobby 로 지정했다면 hobby.user_hobby와 같이 사용
foreignkey, many-to-many ( _set 이 붙음)
# Hobby model에서 무작위 object를 지정
hobby = HobbyModel.objects.all().order_by("?").first()
# userprofile_set은 many to many기 때문에 queryset 형태
# 아래와 같이 사용 할 경우 hobby object를 참조하는 모든 userprofile을 return
# .all()을 붙여주지 않으면 user.UserProfile.None와 같은 형태로 return됨
hobby_users = hobby.userprofile_set.all()
# queryset 형태기 때문에 필요에 따라 특정 filter를 걸어 줄 수도 있다.
hobby_users = hobby.userprofile_set.filter(field=value)
one-to-one ( _set이 없음)
# User model에서 무작위 object를 지정
user = UserModel.objects.all().order_by("?").first()
# userprofile은 역참조지만 one-to-one 관계이기 때문에 _set이 붙지 않음
# 아래와 같이 사용 할 경우 user를 바라보는 userprofile object를 return
user_profile = user.userprofile
# object를 return 받았기 때문에 user_profile의 field들을 확인 할 수 있다.
print(user_profile.hobby)
print(user_profile.introduction)
print(user_profile.birthday)
print(user_profile.age)
역참조를 활용해 나와 같은 취미를 가진 사람을 찾는 코드
def get(self, request):
user = request.user
hobbys = user.userprofile.hobby.all()
for hobby in hobbys:
# exclde : 매칭 된 쿼리만 제외, filter와 반대
# annotate : 필드 이름을 변경해주기 위해 사용, 이외에도 원하는 필드를 추가하는 등 다양하게 활용 가능
# values / values_list : 지정한 필드만 리턴 할 수 있음. values는 dict로 return, values_list는 tuple로 ruturn
# F() : 객체에 해당되는 쿼리를 생성함
hobby_members = hobby.userprofile_set.exclude(user=user).annotate(username=F('user__username')).values_list('username', flat=True)
hobby_members = list(hobby_members)
print(f"hobby : {hobby.name} / hobby members : {hobby_members}")
# result print
"""
hobby : 산책 / hobby members : ['user1']
hobby : 음악감상 / hobby members : ['user1', 'user2']
hobby : 스쿠버다이빙 / hobby members : ['user2']
hobby : 여행 / hobby members : ['user2']
"""
Serializers
# blog/serializers.py
from rest_framework import serializers
from user.models import *
from blog.models import *
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["user", "article", "comment"]
class ArticleSerializer(serializers.ModelSerializer):
comment = CommentSerializer(many=True, source="comment_set")
class Meta:
model = Article
fields = ["user", "title", "category", "content", "comment"]
# user/serializers.py
class HobbySerializer(serializers.ModelSerializer):
# SerializerMethodField() 이후 get_변수명 형태로 오버라이딩해서 원하는 값 return받아 fields에 추가
same_hobby_users = serializers.SerializerMethodField()
def get_same_hobby_users(self, obj):
user_list = []
for user_profile in obj.userprofile_set.all():
user_list.append(user_profile.user.username)
return user_list
class Meta:
model = Hobbies
fields = ["name", "same_hobby_users"]
class UserProfileSerializer(serializers.ModelSerializer):
hobby = HobbySerializer(many=True)
class Meta:
model = UserProfile
fields = ["discription", "birthday", "hobby"]
class UserSerializer(serializers.ModelSerializer):
# userprofile 과는 OneToOne 관계
userprofile = UserProfileSerializer()
# article과는 ManyToMany관계 source="article_set"으로 변수명으로 출력, MTM인걸 확실히 알 수 있음
article = ArticleSerializer(many=True, source="article_set")
class Meta:
# serializer에 사용될 model, field지정
model = User
# 모든 필드를 사용하고 싶을 경우 fields = "__all__"로 사용
fields = ["username", "fullname", "email", "userprofile", "article"]
# user/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import permissions
from rest_framework import status
from DRF_study.serializers import UserSerializer
# Create your views here.
class UserAPIView(APIView):
permission_classes = [permissions.AllowAny]
# 요청을 보낼 method의 이름으로 함수명을 지어 오버라이딩 해서 사용해야함
def get(self, request):
user = request.user
#serializer에 queryset을 인자로 줄 경우(ManyToMany관계일 때) many=True 옵션을 줘야한다.
serialized_user_data = UserSerializer(user).data
# context= 를 통해 원하는 데이터를 serializer에 넘겨주고, self.context를 사용해 호출 가능하다.
# serialized_user_data = UserSerializer(user, context={"some_key": "some_value"}).data
return Response(serialized_user_data, status=status.HTTP_200_OK)