회고록(TIL&WIL)

WIL 2022.06.20 DRF/ Custom User, login/out, 역참조, Serializer

만들어나가자 2022. 6. 21. 01:14

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)