[0620] Django Rest Framework

nikevapormax·2022년 6월 20일
0

TIL

목록 보기
53/116

DRF 특강

과제 리뷰

😠 serializer

  • 우리는 serializer를 통해 데이터를 불러오고, 이를 프론트에 보내주는 작업을 진행했다. 데이터를 불러올 때 우리가 선택할 수 있는 방법은 정참조역참조가 있다.
  • 이번 과제에서는 역참조를 사용해 게시글과 게시글에 달린 댓글의 내용을 불러오는 문항이 있었는데, 제대로 해결하지 못했다.
  • 지금까지의 공부를 통해 나는 역참조에는 _set이 붙는 것을 알게 되었다. 이번 문항은 이것을 사용해서 진행해보도록 하겠다.
  • 먼저 우리에게 주어진 요구사항은 아래와 같다.
user serializer에 추가로 로그인한 사용자의 게시글 및 댓글 리턴 기능 구현
  • 먼저 UserSerializer에 보낼 게시글 및 댓글에 대한 serializer를 만들어보도록 하겠다.
  • blog/serializers.py
from rest_framework import serializers

from .models import Article as ArticleModel
from .models import Comment as CommentModel


class CommentSerializer(serializers.ModelSerializer):
    class Meta:
        model = CommentModel
        fields = ["user", "article", "comment"]

class ArticleSerializer(serializers.ModelSerializer):
    comments = CommentSerializer(many=True, source='comment_set')
    class Meta:
        model = ArticleModel
        fields = ["title", "content", "comments"]
  • 위의 코드를 포스트맨에서 실험해보면 아래와 같은 결과가 나온다. 그러나 결과는 별로 이뻐보지이 않는다. 추후에 튜닝을 진행하도록 하겠다.

    • 1번 : 현재 로그인되어 있는 사용자인 admi가 작성한 게시글의 정보와 이에 해당하는 댓글들이 작성되어 있음
    • 2번 : 현재 로그인되어 있는 사용자인 admi가 작성한 댓글
  • 해당 부분은 원래 두 번째 코드와 같이 쓰여야 한다. 하지만 포스트맨에 찍히는 이름을 다르게 적용하고 싶어 첫 번째 코드와 같이 쓴 것이다!

    • 첫 번째 코드
      comments = CommentSerializer(many=True, source='comment_set')
    • 두 번째 코드
      comment_set = CommentSerializer(many=True)
  • 위의 코드에서 알 수 있듯이, ForeignKeyManyToManyFeild와 같이 데이터가 QuerySet으로 넘어가는 경우에는 반드시 many=True를 붙여줘야 한다.

  • 하지만 데이터가 object로 즉 단 하나만 넘어가는 OneToOneField의 경우에는 쓰지 않는다.

  • user의 아이디인 username이 보이도록 수정해 좀 더 이쁘게 만들어보도록 하겠다. 또한 게시글의 카테고리도 추가해보도록 하겠다.

    • comment를 단 사용자의 아이디를 보여주기 위해 dir(obj)를 찍어보았다.
    • 우리는 빨간 네모 안에 있는 user를 역참조로 가져올 수 있고, 모델을 살펴보면 우리는 User 모델의 username을 사용할 수 있다는 것을 직감적으로 알아채야 한다.
      class User(AbstractBaseUser):
       
       # "사용자 계정"은 admin 페이지에서 나오는 메뉴의 이름 / unique=True를 설정해 단 하나의 값만 입력할 수 있도록 함
       username = models.CharField("사용자 계정", max_length=50, unique=True)
       password = models.CharField("비밀번호", max_length=128)
       email = models.EmailField("이메일", max_length=100)
       name = models.CharField("이름", max_length=20)
       # auto_now_add : 최초 생성 시의 시간을 자동으로 입력해줌.(그 후 업데이트에 대한 시간 기록은 x) 주로 가입일, 최초 생성일 등에 사용함
       join_data = models.DateTimeField("가입일자", auto_now_add=True) 
    • 그러므로 이 정보를 사용해 username을 불러오도록 하겠다. 추가적으로 카테고리도 불러와 적용해 보자.
      • 만약 related_name을 설정했다면, source='comment_set'은 쓸 필요가 없다. 하지만 다음과 같이 쓰게 되면 역참조를 통해서 불러왔다는 것을 직관적으로 알 수 있는 것 같다.
    from rest_framework import serializers
    
     from .models import Article as ArticleModel
     from .models import Comment as CommentModel
     from .models import Category as CatogoryModel
    
    class CommentSerializer(serializers.ModelSerializer):
       username = serializers.SerializerMethodField()
       def get_username(self, obj):
           return obj.user.username
       
       class Meta:
           model = CommentModel
           fields = ["username", "comment"]
    
    class ArticleSerializer(serializers.ModelSerializer):
       category = serializers.SerializerMethodField()
       comments = CommentSerializer(many=True, source='comment_set')
       
       # 카테고리를 리스트로 받아오기 위해 다음과 같이 작성
       def get_category(self, obj):
           return [category.name for category in obj.category.all()]
       
       class Meta:
           model = ArticleModel
           fields = ["title", "content", "comments", "category"]
  • 포스트맨에서 확인해보자.

  • 만약 각 serializer에서 서로를 참조하게 되면 circular import error(순환 참조 에러)가 일어나게 된다. 어떤 의미냐면 계속 서로를 참조하게 되는 것이다(끝없이). 그러므로 참조를 잘 설계해야 한다.

😠 kwargs 사용

def post(self, request):
        user = request.user
        title = request.data.get('title', '')
        content = request.data.get('content', '')
        categorys = request.data.get('category', [])
        
        if len(title) <= 5:
            return Response({"error": "제목은 5자보다 많아야 합니다."})
        if len(content) <= 20:
            return Response({"error": "게시글은 20자보다 많아야 합니다."})
        if not categorys:
            return Response({"error": "카테고리가 지정되지 않았습니다."})
        
        article = ArticleModel(author=user, title=title, content=content)
        article.save()
        article.category.add(*categorys)

        return Response({"msg": "게시글 작성 성공!!"})
  • article.category.add(*categorys)를 사용해 여러 개의 카테고리를 보여주려 리스트 형식으로 받아온 것을 언패킹해 값을 더해줄 수 있다.

😠 status code

  • 마이너한 수준까지는 알면 좋지만 굳이 외울 필요는 없다. 하지만 큰 틀에서의 의미는 알고 넘어가는 것이 좋다.
  • 종류
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
HTTP_201_CREATED = 201
HTTP_202_ACCEPTED = 202
HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203
HTTP_204_NO_CONTENT = 204
HTTP_205_RESET_CONTENT = 205
HTTP_206_PARTIAL_CONTENT = 206
HTTP_207_MULTI_STATUS = 207
HTTP_208_ALREADY_REPORTED = 208
HTTP_226_IM_USED = 226
HTTP_300_MULTIPLE_CHOICES = 300
HTTP_301_MOVED_PERMANENTLY = 301
HTTP_302_FOUND = 302
HTTP_303_SEE_OTHER = 303
HTTP_304_NOT_MODIFIED = 304
HTTP_305_USE_PROXY = 305
HTTP_306_RESERVED = 306
HTTP_307_TEMPORARY_REDIRECT = 307
HTTP_308_PERMANENT_REDIRECT = 308
HTTP_400_BAD_REQUEST = 400
HTTP_401_UNAUTHORIZED = 401
HTTP_402_PAYMENT_REQUIRED = 402
HTTP_403_FORBIDDEN = 403
HTTP_404_NOT_FOUND = 404
HTTP_405_METHOD_NOT_ALLOWED = 405
HTTP_406_NOT_ACCEPTABLE = 406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
HTTP_408_REQUEST_TIMEOUT = 408
HTTP_409_CONFLICT = 409
HTTP_410_GONE = 410
HTTP_411_LENGTH_REQUIRED = 411
HTTP_412_PRECONDITION_FAILED = 412
HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413
HTTP_414_REQUEST_URI_TOO_LONG = 414
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416
HTTP_417_EXPECTATION_FAILED = 417
HTTP_418_IM_A_TEAPOT = 418
HTTP_422_UNPROCESSABLE_ENTITY = 422
HTTP_423_LOCKED = 423
HTTP_424_FAILED_DEPENDENCY = 424
HTTP_426_UPGRADE_REQUIRED = 426
HTTP_428_PRECONDITION_REQUIRED = 428
HTTP_429_TOO_MANY_REQUESTS = 429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451
HTTP_500_INTERNAL_SERVER_ERROR = 500
HTTP_501_NOT_IMPLEMENTED = 501
HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_506_VARIANT_ALSO_NEGOTIATES = 506
HTTP_507_INSUFFICIENT_STORAGE = 507
HTTP_508_LOOP_DETECTED = 508
HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509
HTTP_510_NOT_EXTENDED = 510
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
  • 만약 status code에 대해서 코드를 작성하다 알고 싶다면 status를 ⌘ + 클릭해 알아볼 수 있다.
  • 다음과 같이 작성하면 되며, 굳이 글자를 다 적지 않고 숫자만 적어도 무방하다.
from rest_framework.response import Response
from rest_framework import status

def post(self, request):
        user = request.user
        title = request.data.get('title', '')
        content = request.data.get('content', '')
        categorys = request.data.get('category', [])
        
        if len(title) <= 5:
            return Response({"error": "제목은 5자보다 많아야 합니다."})
        if len(content) <= 20:
            return Response({"error": "게시글은 20자보다 많아야 합니다."})
        if not categorys:
            return Response({"error": "카테고리가 지정되지 않았습니다."})
        
        article = ArticleModel(author=user, title=title, content=content)
        article.save()
        article.category.add(*categorys)

        return Response({"msg": "게시글 작성 성공!!"}, status=status.HTTP_200_OK)

custom user model 생성 보완

😠 user admin 추가

  • 현재의 User 모델로 비밀번호를 저장하게 되면 해싱이 되지 않아 로그인이 되지 않는 문제가 발생하게 된다. 이를 해결하고자 admin.py에 user admin을 추가해보도록 하겠다.
from django.contrib import admin
from .models import User as UserModel, UserProfile as UserProfileModel, Hobby as HobbyModel
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

class UserAdmin(BaseUserAdmin):
    list_display = ('id', 'username', 'name', 'email')
    list_display_links = ('username', )
    list_filter = ('username', )
    search_fields = ('username', 'email', )
    
    fieldsets = (
        ("info", {"fields": ("username", "password", "email", "name", "join_data", )}),
        ("Permissions", {"fields": ("is_admin", "is_active", )}),)
    
    filter_horizontal = []
    
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('username', 'join_data',)
        else:
            return ('join_data', )
        

# Register your models here.
admin.site.register(UserModel, UserAdmin)
admin.site.register(UserProfileModel)
admin.site.register(HobbyModel)
  • 이제 서버를 돌리고 어드민 페이지로 들어가보았더니, 아래와 같이 변화가 생겼다.

  • admi를 클릭해 들어가보도록 하자.

  • 그리고 빨간 박스를 눌러보자. 빨간 박스가 쳐져있는 부분의 말은 비밀번호를 바꾸려면 여기를 눌러라다. 진짜 사이트와 같이 비밀번호를 바꿀 수 있는 것을 볼 수 있다.

  • 잘못 입력된 비밀번호는 아래와 같이 나온다.

😠 실행되는 SQL 쿼리

  • 데이터가 현재는 별로 없어 문제가 없지만, 몇십만개 이상으로 치닺게되면 속도 제한이 생기게 된다. 그때 아래의 코드가 있다면 디버깅이 많이 편리해진다.
  • settings.py에 아래 코드를 붙여넣어주면 된다.
# https://docs.djangoproject.com/en/1.11/topics/logging/
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        }
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    }
}
  • 터미널에도 아래와 같은 부분이 추가된다.
    • 괄호 안의 숫자가 실행시간이고 옆에 있는 구문이 실행된 쿼리문이다.

custom permissions

😠 method별 권한 설정

  • 우리가 이전에는 가입한 날짜 차이에 다라서 혹은 시간 차이에 따라서 권한을 부여했었다.
  • 오늘은 우리가 받는 메소드별로 권한을 설정해보도록 하겠다.
from rest_framework.permissions import BasePermission
from datetime import timedelta
from django.utils import timezone
from rest_framework.exceptions import APIException
from rest_framework import status


class MyAuthenticateOver3(BasePermission):
    
    message = "가입 후 3일이 지나지 않아 글 작성이 불가능합니다."
    
    def has_permission(self, request, view):
        user = request.user
        if not user or not user.is_authenticated:
            return False
        
        # DateField : 2022-06-18
        # DataTimeField : 2022-06-18 10:50:55
        # datetime.now().date() 를 통해서 시간까지는 안나오게 설정함
        
        print(f'user join date : {user.join_data}')
        print(f'now date : {timezone.now()}')
        print(f'a week ago date : {timezone.now() - timedelta(minutes=3)}')
        
        return bool(user.join_data < (timezone.now() - timedelta(minutes=3)))
    
    
class GenericAPIException(APIException):
    def __init__(self, status_code, detail=None, code=None):
        self.status_code=status_code
        super().__init__(detail=detail, code=code)

class IsAdminOrIsAuthenticatedReadOnly(BasePermission):
    """
    admin 사용자는 모두 가능, 로그인 사용자는 조회만 가능
    """
    SAFE_METHODS = ('GET', )
    message = '접근 권한이 없습니다.'

    def has_permission(self, request, view):
        user = request.user

        if not user.is_authenticated:
            response ={
                    "detail": "서비스를 이용하기 위해 로그인 해주세요.",
                }
            raise GenericAPIException(status_code=status.HTTP_401_UNAUTHORIZED, detail=response)
            
		# 유저가 인증되어있고 어드민이라면 전부다 할 수 있다.
        if user.is_authenticated and user.is_admin:
            return True
            
        # 유저가 인증되어있고, 메서드가 세이프 메서드 안에 있다면 트루(어드민 아닌 사람들)
        if user.is_authenticated and request.method in self.SAFE_METHODS:
            return True
        
        return False
  • 한 번 테스트해보도록 하겠다.
  • 먼저 우리가 만든 permission을 사용자 정보를 조회하는 데에 적용해보자. 어떤 유저가 적용되는지와 그 유저의 admin 권한이 있는지에 대해 프린트를 찍어볼 것이다.
from django_rest_framework.permissions import MyAuthenticateOver3, IsAdminOrIsAuthenticatedReadOnly

# 사용자 정보 
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):        
        return Response(UserSerializer(request.user).data)
   
    
    # 회원가입
    def post(self, request):
        user = request.user
        print(user)
        print(user.is_admin)
        
        return Response({"msg": "post method!!"})
  • 먼저 포스트맨에 가 확인해보면 성공했다.

  • 이제 터미널창을 통해 결과를 보도록 하겠다.

    • 현재 로그인해 있는 admi는 admin 권한을 가지고 있는 것을 알 수 있다.
  • 로그아웃을 진행한 후 다시 사용자 조회를 해보았다. 로그인이 필요하다는 메세지를 주는 것을 알 수 있다.

  • 이제 admi 말고 다른 유저로 로그인해 사용자 조회를 진행하도록 하겠다.

  • 사용자 조회에 실패한 것을 알 수 있다. 하지만 우리는 여기서 에러 메세지에 집중해야 한다. 현 사용자는 user5로 어드민 권한이 없다.

  • 여기서 우리가 알 수 있는 점은 사용자의 권한에 따라 에러 메세지를 다르게 설정할 수 있는 것이다.

    • 1번 : 로그인에 성공해 인증은 되어 있지만 admin 권한이 없는 user
    • 2번 : 로그인을 하지 않아 인증도 되어 있지 않은 비인가된 사용자
  • 현재 우리는 POST에 대해서 실험을 해본 것이다. 그렇다면 SAFE_METHOD에 들어있는 GET 메서드로 실험을 한다면 어떻게 될 것인지 알아보도록 하겠다.

    • 조회가 잘 되는 것을 볼 수 있다.
  • 이를 통해 우리가 부여한 권한에 따라 사용자들이 이용할 수 있는 서비스를 제한시킬 수 있는 permission을 적용할 수 있다는 것을 알 수 있었다.


admin 심화

  • 아까 우리는 admin.py를 업그레이드 했었다. 업그레이드를 한 것들이 어디에 적용되었는지 알아보도록 하겠다.
  • user/admin.py
class UserAdmin(BaseUserAdmin):
    list_display = ('id', 'username', 'name', 'email')
    list_display_links = ('username', )
    list_filter = ('username', )
    search_fields = ('username', 'email', )
    
    fieldsets = (
        ("info", {"fields": ("username", "password", "email", "name", "join_data", )}),
        ("Permissions", {"fields": ("is_admin", "is_active", )}),)
    
    filter_horizontal = []
    
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('username', 'join_data',)
        else:
            return ('join_data', )

😠 list_display

  • object 목록에 띄워줄 필드 지정
  • object 클릭 시 상세 페이지로 들어갈 수 있는 필드를 지정

😠 list_filter

  • filter를 걸 수 있는 필드를 생성
    • 현재는 필터를 걸만한 것이 없지만, 사용자 그룹같은 것이 있다면 그룹별 사용자를 확인할 수 있다.

😠 search_fields

  • 검색에 사용될 필드를 지정
    • 다음과 같이 사용하면 되며, 내가 설정한 값인 username과 email만을 가지고 검색할 수 있다.

😠 fieldsets

  • 필드셋을 설정해 info에는 어떤 것들이 들어가고 permissions에는 어떤 것들이 들어가는지 정해주어 깔끔하게 보여질 수 있도록 할 수 있음

😠 readonly_fields

  • 상세페이지에서 읽기 전용 필드를 설정할 때 사용
    • 리드온리인 것들은 무조건 보기만 할 수 있다. 변하지 않을 값들을 지정하면 좋다.(유저아이디나 가입일 등)
    • 다음 코드를 한 번 보면서 이해해보자.
      def get_readonly_fields(self, request, obj=None):
           if obj:
               return ('username', 'join_data',)
           else:
               return ('join_data', )
    • if 조건의 경우는 이미 가입한 사용자의 정보를 볼 때 적용되는 부분이다. 아래의 사진과 같이 변경이 불가능한 것을 볼 수 있다.
    • else 조건의 경우는 사용자가 회원가입을 할 때 보이는 읽기만 되는 필드이다. 가입일같은 경우는 auto_now_add=True로 인해 자동으로 생성되며 이것은 readonly_fields의 필수값이다. 하지만 사용자의 아이디는 회원가입을 하면서 입력해야 된다. 따라서 username은 아래의 경우 빠져있다.

😠 StackedInline

  • 클래스를 선언해주고 아래있는 UserAdmin 안에 inlines를 선언해주어야 한다.
  • 아래의 사진과 같이 User메뉴에서 사용자를 지정해 들어가면 그 사용자의 UserProfile 내용까지 볼 수 있다.
  • StackedInline역참조 관계에서만 활용할 수 있다. User와 UserProfile은 역참조를 할 수 있는 OneToOne 관계이다.
from django.contrib import admin
from .models import User as UserModel, UserProfile as UserProfileModel, Hobby as HobbyModel
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin


cfrom django.contrib import admin
from .models import User as UserModel, UserProfile as UserProfileModel, Hobby as HobbyModel
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

# Stackedinline
class UserProfileInline(admin.StackedInline):
    model = UserProfileModel
    
    # 헷갈릴 수 있어 일단 사용 안함
    # def formfield_for_manytomany(self, db_field, request, **kwargs):
    #     if db_field.name == 'hobby':
    #         kwargs['queryset'] = HobbyModel.objects.filter(id__lte=7)

    #     return super().formfield_for_foreignkey(db_field, request, **kwargs)

class UserAdmin(BaseUserAdmin):
    list_display = ('id', 'username', 'name', 'email')
    list_display_links = ('username', )
    list_filter = ('username', )
    search_fields = ('username', 'email', )
    
    fieldsets = (
        ("info", {"fields": ("username", "password", "email", "name", "join_data", )}),
        ("Permissions", {"fields": ("is_admin", "is_active", )}),)
    
    filter_horizontal = []
    
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('username', 'join_data',)
        else:
            return ('join_data', )
        
    # 여기에 인라인을 지정해준다.
    inlines = (
            UserProfileInline,
        )

😠 TabularInline

  • StackedInline과 완전히 똑같지만 다른 점은 UserProfile의 내용이 세로로 나오냐 가로로 나오냐이다.
  • TabulrInline은 가로로 나오는 것을 볼 수 있다.
# TabularInline 
class UserProfileInline(admin.TabularInline):
    model = UserProfileModel
    
    # 헷갈릴 수 있어 일단 사용 안함
    # def formfield_for_manytomany(self, db_field, request, **kwargs):
    #     if db_field.name == 'hobby':
    #         kwargs['queryset'] = HobbyModel.objects.filter(id__lte=7)

    #     return super().formfield_for_foreignkey(db_field, request, **kwargs)

class UserAdmin(BaseUserAdmin):
    list_display = ('id', 'username', 'name', 'email')
    list_display_links = ('username', )
    list_filter = ('username', )
    search_fields = ('username', 'email', )
    
    fieldsets = (
        ("info", {"fields": ("username", "password", "email", "name", "join_data", )}),
        ("Permissions", {"fields": ("is_admin", "is_active", )}),)
    
    filter_horizontal = []
    
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('username', 'join_data',)
        else:
            return ('join_data', )
        
    # 여기에 인라인을 지정해준다.
    inlines = (
            UserProfileInline,
        )

  • 그러므로 UserProfile에 어떤 내용이 얼마나 들어가는지에 따라 선택해 사용해주면 된다.
  • 그리고 이렇게 지정해 놓으면 내가 유저를 생성할 때도 한 번에 UserProfile의 내용까지 적을 수 있다.

😠 filter_horizontal

  • hobby를 값으로 넣어 확인해보도록 하자.
  • 보이는 것과 같이 훨씬 페이지가 이뻐지는 것을 볼 수 있다.

😠 추가 / 삭제 / 수정 권한 설정

class UserProfileInline(admin.StackedInline):
    model = UserProfileModel
    filter_horizontal = ["hobby"]

class UserAdmin(BaseUserAdmin):
    list_display = ('id', 'username', 'name', 'email')
    list_display_links = ('username', )
    list_filter = ('username', )
    search_fields = ('username', 'email', )
    
    fieldsets = (
        ("info", {"fields": ("username", "password", "email", "name", "join_data", )}),
        ("Permissions", {"fields": ("is_admin", "is_active", )}),)
    
    filter_horizontal = []
    
    def get_readonly_fields(self, request, obj=None):
        if obj:
            return ('username', 'join_data',)
        else:
            return ('join_data', )
        
    # 여기에 인라인을 지정해준다.
    inlines = (
            UserProfileInline,
        )
    
    def has_add_permission(self, request, obj=None): # 추가 권한
        print(request.user)
        return False

    def has_delete_permission(self, request, obj=None): # 삭제 권한
        print(request.user)
        return False

    def has_change_permission(self, request, obj=None): # 수정 권한
        print(request.user)
        return False
  • 로그인해 있는 사용자를 찍어보았다. 권한을 모두 False로 수정해 아예 모든 사용자가 추가, 삭제 및 수정을 할 수 있는 것을 볼 수 있다.

  • 현재는 모두 False로 적용되어 있어 모든 페이지에서 할 수 있던 추가, 삭제, 수정이 모두 막힌 것을 볼 수 있다.


django orm 심화

😠 get, filter, exclude 검색 시 사용하는 Field lookups 문법

  • get
    • get은 다음과 같이 사용한다.
      #### 상단 생략 ####
      class ArticleView(APIView):
       permission_classes = [MyAuthenticateOver3]
       
       def get(self, request):
           user = request.user
           articles = ArticleModel.objects.get(author=user)
           print(articles)
           return Response({})
           
        #### 하단 생략 ####
    • 프린트가 찍힌 값을 확인해보자.
    • get의 경우, 단 하나의 object 만을 반환하며, 여러 개의 object가 있어 쿼리셋으로 반환될 시 MultiValueDictKeyError를 반환해준다.
  • filter
    • 다음과 같이 사용하면 된다.
      #### 상단 생략 ####
      class ArticleView(APIView):
       permission_classes = [MyAuthenticateOver3]
       
       def get(self, request):
           user = request.user
           articles = ArticleModel.objects.filter(author=user)
           print(articles)
           return Response({})
           
        #### 하단 생략 ####
    • 프린트가 찍힌 값을 확인해보자.
    • filter의 경우, 여러 개의 object가 포함되어 있는 QuerySet으로 결과값을 반환해준다.
  • exclude
    • 다음과 같이 사용한다.
      #### 상단 생략 ####
      class ArticleView(APIView):
       permission_classes = [MyAuthenticateOver3]
       
       def get(self, request):
           user = request.user
           articles = ArticleModel.objects.exclude(author=user)
           print(articles)
           return Response({})
           
        #### 하단 생략 ####
    • 프린트가 된 내용을 확인해보자.
    • 프린트의 결과에서 알 수 있듯이, exclude는 현재 로그인한 사용자의 글을 제외한 모든 글을 쿼리셋의 형태로 불러와주는 것을 알 수 있다.

😠 exclude를 사용해 프로젝트 수정

  • 현재 나의 프로젝트에서는 나와 같은 hobby를 가진 사용자를 출력할 때 현재 로그인되어 있는 사용자의 username로 같이 나오게 된다. 이는 불필요한 부분이라 생각해 제외해보도록 하겠다.
  • user/views.py
#### 상단 생략 ####

 class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        # 다수의 사용자의 값을 불러올 때 사용!
        # all_user = UserModel.objects.all()
        # return Response(UserSerializer(all_user, many=True).data)
        user_serializer = UserSerializer(request.user, context={"request": request}).data
        return Response(user_serializer)
        
#### 하단 생략 ####
  • 일단 context로 request의 데이터를 보냈으니, seriallizer.py로 가서 request에서 현재 로그인한 유저의 이름을 찾을 수 있는지 확인해보도록 하겠다.
class UserSerializer(serializers.ModelSerializer):
    user_detail = UserProfileSerializer(source="userprofile") # object (OneToOne 관계)
    articles = ArticleSerializer(many=True, source="article_set")
    comments = CommentSerializer(many=True, source="comment_set")
    
    login_name = serializers.SerializerMethodField()
    def get_login_name(self, obj):
        return self.context["request"].user.name
    
    class Meta:
    
        # 내가 serializer에서 어떤 모델을 쓰겠다는 것을 선언하는 것!(중요)
        model = UserModel
        fields = ["username", "email", "name", "join_data", "user_detail", "articles", "comments", "login_name"]
  • 포스트맨에서 확인해보도록 하겠다.
  • 이름이 아주 잘 담겨왔으니 이제 이 이름을 제외시켜 보자.
  • 하지만 해당 작업은 UserSerializer가 아닌 HobbySerializer에서 진행하도록 한다. 왜냐하면 HobbySerialzer에서도 해당 context 값을 받아서 사용할 수 있기 때문에 굳이 다른 곳에서 받아다 두 번 사용할 필요가 없기 때문이다.
  • user/serializers.py
class HobbySerializer(serializers.ModelSerializer):
    same_hobby_user = serializers.SerializerMethodField()
    def get_same_hobby_user(self, obj):
        user_list = []
        user = self.context["request"].user
        
        for user_profile in obj.userprofile_set.exclude(user=user):
            user_list.append(user_profile.user.username)
        return user_list
    
        # 위의 for문을 리스트 컴프리헨션을 통해 줄일 수 있음!
        # return [user_profile.user.username for user_profile in obj.userprofile_set.all()]
    
    class Meta:
        model = HobbyModel
        fields = ["name", "same_hobby_user"]
  • 포스트맨에서 잘 적용되었는지 확인해보도록 하겠다. admi가 선택한 hobby를 동시에 좋아하는 유저 목록에서 admi가 빠진 것을 볼 수 있다.

😠 contains 활용

  • contains는 쿼리문에서 사용자가 입력한 값을 포함한 데이터 내역들을 찾아주는 기능을 한다. 즉, 특정 string이 포함된 object를 찾는 것이다.
  • 다음과 같이 작성해 테스트해보도록 하겠다.
#### 상단 생략 ####
   class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):
        user = request.user
        articles = ArticleModel.objects.filter(author__name__contains=user)
        print(articles)
        return Response({})
        
     #### 하단 생략 ####
  • 프린트가 된 내용을 확인해보자.

😠 startswith / endswith 활용

  • 특정 string으로 시작하는 또는 끝나는 object를 찾는 것이다.
  • 먼저 startswith를 작성해보자.
#### 상단 생략 ####
   class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):
        user = request.user
        articles = ArticleModel.objects.filter(author__name__startswith='u')
        print(articles)
        return Response({})
        
     #### 하단 생략 ####
  • 프린트된 결과이다.
  • 그 다음으로 endswith를 확인해보도록 하겠다.
#### 상단 생략 ####
   class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):
        user = request.user
        articles = ArticleModel.objects.filter(author__name__endswith='1')
        print(articles)
        return Response({})
        
     #### 하단 생략 ####
  • 프린트된 결과이다. 현재 1로 끝나는 이름을(아이디 아니다.) 가진 유저는 단 한 명 뿐이라 결과가 하나밖에 없다.

😠 gt, gte, lt, lte 활용

  • 특정 값보다 크거나 / 작거나 / 크거나같거나 / 작거나같은 object를 찾는 것이다.
  • 사용자의 나이를 비교할 수도 있고, 가입일과 같은 날짜를 비교할 수도 있다.
  • 위의 기능들은 유효기간을 측정할 때 많이 쓰인다.
  • gt
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        
        users = UserProfileModel.objects.filter(age__gt=20)
        print(users)
        return Response({})
  • 프린트된 내용을 확인해보자. 20살보다 나이가 많은 사람은 2명인 것을 알 수 있다.
  • lt
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        
        users = UserProfileModel.objects.filter(age__lt=20)
        print(users)
        return Response({})
  • 프린트된 내용을 보자. 20살보다 나이가 어린 사람은 3명인 것을 알 수 있다.
  • 프린트된 내용들을 보면 나이가 딱 20살인 user3만 프린트가 되지 않은 것을 볼 수 있다. 이제 user3도 프린트해보자.
  • gte
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        
        users = UserProfileModel.objects.filter(age__gte=20)
        print(users)
        return Response({})
  • 프린트된 내용을 확인해보자. 20살 이상이기 때문에 user3이 프린트된 것을 볼 수 있다.
  • lte
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        
        users = UserProfileModel.objects.filter(age__lte=20)
        print(users)
        return Response({})
  • 프린트된 내용을 확인해보자. user3이 잘 찍힌 것을 볼 수 있다.

😠 in 활용

  • 우리가 일반 파이썬의 for문에서 사용하듯이 장고에서도 비슷하게 쓰인다.
  • 특정 list에 포함된 object를 찾을 때 사용한다.
class UserView(APIView):
    # permission_classes = [MyAuthenticateOver3]
    permission_classes = [IsAdminOrIsAuthenticatedReadOnly]
    
    # 사용자 정보 조회
    def get(self, request):
        # 다수의 사용자의 값을 불러올 때 사용!
        # all_user = UserModel.objects.all()
        # return Response(UserSerializer(all_user, many=True).data)
        
        # return Response(UserSerializer(request.user).data)
        
        users = UserProfileModel.objects.filter(hobby__in=['축구', '농구'])
        print(users)
        return Response({})
  • 프린트된 내용을 확인해보자.

😠 kwargs 활용

  • 우리가 원래는 프론트를 통해 데이터를 받아와 백앤드에서 데이터를 저장하던 요리를 하던 한다.
  • 다음과 같은 회원가입의 예시 코드가 있다.
 def get(self, request):
        username = request.data.get('username', '')
        password = request.data.get('username', '')
        email = request.data.get('username', '')
        name = request.data.get('username', '')
        
        user = UserModel.objects.create(
            username=username,
            password=password, # 이건 이렇게 하면 안된다. set_password 사용해야 함! 예시임!
            email=email,
            name=name
        )
  • 하지만 kwargs를 배운 우리는 이렇게 하지 않고 한줄로 코드를 잘 수 있다.
def get(self, request):
               
        UserModel.objects.create(**request.data)
  • 이제 password를 따로 처리하려면 어떻게 해야할까??
  • pop을 사용해 password 값만 따로 빼서 처리해보자.
def get(self, request):
        password = request.data.pop("password")
        user = UserModel(**request.data)
        user.set_password(password)
        user.save()

😠 order_by를 통한 정렬

  • QuerySet을 정렬시킬 수 있다.
  • 먼저 가입일 순으로 게시글을 정렬해보도록 하겠다.
class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):      
        user = request.user
        articles = ArticleModel.objects.all().order_by("author__join_data")
        print(articles)
        return Response({})
  • 프린트를 확인해보도록 하자.
  • 이번에는 가입일의 역순으로 정렬을 해보자.
class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):      
        user = request.user
        articles = ArticleModel.objects.all().order_by("-author__join_data")
        print(articles)
        return Response({})
  • 프린트를 확인해보자. 역순으로 잘 나온 것을 확인할 수 있다.
  • 이번에는 랜덤 셔플을 해보도록 하겠다.
class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):      
        user = request.user
        articles = ArticleModel.objects.all().order_by("?")
        print(articles)
        return Response({})
  • 프린트를 확인해보자. 결과는 할 때마다 달라진다.
  • 랜덤에서 하나만 뽑고 싶다면 아래와 같이 하면 된다.
class ArticleView(APIView):
    permission_classes = [MyAuthenticateOver3]
    
    def get(self, request):      
        user = request.user
        articles = ArticleModel.objects.all().order_by("?").first()
        print(articles)
        return Response({})
  • 결과로 하나가 나온 것을 볼 수 있다. 이것도 할 때마다 값이 달라진다.
profile
https://github.com/nikevapormax

0개의 댓글