[TIL 15일자] 데브코스 데이터엔지니어링

·2023년 4월 28일
0

데브코스

목록 보기
14/55
post-thumbnail
post-custom-banner

📚 오늘 공부한 내용

1. RelatedField

1) StringRelatedField

  • StringRelatedField(many=True, read_only=True)model에서 정의된 __str__ 내용을 표시한다.

2) SlugRelatedField

  • .SlugRelatedField(many=True, read_only=True, slug_field='column_name')model에서 정의된 column들 중에서 slug_field에 있는 값을 표시되게 해 준다. 만약 pub_dateslug_field로 정했다면 questions에는 pub_date의 값이 들어가게 된다.

3) HyperlinkedRelatedField

  • .HyperlinkedRelatedField(many=True, read_only=True, view_name = 'urls.py에서 입력한 url의 name')는 하이퍼링크로 이동할 수 있게 해 주며 작성한 view_name의 위치로 연결해 준다.
  • 다음과 같이 questions에 작성자가 작성한 질문의 링크로 이동할 수 있도록 나오게 된다.

2. 중첩된 serializer 구조 만들기

1) 기존 serializer 내부에 넣을 serializer을 생성

  • 기존 QuestionSerializer 내부에 Choicemodel로 두는 serializer을 표시해 주기 위해 ChoiceSerializerserializers.py에 정의해 준다.
class ChoiceSerializer(serializers.ModelSerializer):
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes']

2) serializer에 중첩된 serializer를 호출

class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source = 'owner.username')
    choices = ChoiceSerializer(many = True, read_only=True)
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']

3) 이때 models.py에서 호출할 이름을 기존 model에 추가

  • related_name으로 호출할 수 있다.
class Choice(models.Model):
    #Question의 unique id 저장 
    question = models.ForeignKey(Question, related_name ='choices', on_delete = models.CASCADE) #related_name 추가 
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return f'{self.question.question_text}{self.choice_text}'

3. 로그인 한 사용자만 투표할 수 있는 기능 구현

1) 투표를 위한 model 생성

  • 모든 정보를 기존 model이 가지고 있기 때문에 따로 field를 정의해 주는 것이 아니라 ForeignKey를 통해서만 호출한다.
  • models.py에 생성해 준다.
from django.db import models
from django.utils import timezone
import datetime
from django.contrib import admin 
from django.contrib.auth.models import User

class Vote(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
    voter = models.ForeignKey(User, on_delete=models.CASCADE)
  • 이때 한 명이 하나의 투표를 해야 하기 때문에 제약 조건을 걸어 준다. 제약 조건을 걸기 위해 models.UniqueConstraint(fields=['제약을 걸 field'], name = '지정할 이름')를 사용해 준다.
  • 이는 question과 voter 필드의 조합이 유일해야 한다는 제약 조건이다.
class Vote(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice = models.ForeignKey(Choice, on_delete=models.CASCADE)
    voter = models.ForeignKey(User, on_delete=models.CASCADE)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['question', 'voter'], name ='unique_voter_for_questions')
        ]
  • 이는 모델 수정이 일어난 것이기 때문에 migration을 해 주어야 한다.

2) vote_count를 바로 반영되도록 serializer 수정

  • 테이블에 있는 vote 수를 바로 가지고 오도록 함수를 이용해 수정하였다.
  • 이때는 serializers.SerializerMethodField()를 사용하고, obj의 vote_set의 수를 바로 가지고 오도록 구현했다.
class ChoiceSerializer(serializers.ModelSerializer):
    votes_count = serializers.SerializerMethodField()

    class Meta:
        model = Choice
        fields = ['choice_text', 'votes_count']

    def get_votes_count(self, obj):
        return obj.vote_set.count()

3) 투표를 받기 위한 Vote 모델의 Serializer을 생성

  • 이때 voter 역시 수정이 필요하지 않고 로그인 한 사용자만 읽으면 되므로 ReadOnlyField로 설정한다.
from rest_framework import serializers
from polls.models import Question, Choice, Vote

class VoteSerializer(serializers.ModelSerializer):
    voter = serializers.ReadOnlyField(source = 'voter.username')

    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']

4) views.py에서 Vote 모델과 Serializer을 통한 화면 구현

  • views.pyVoteListVoteDetail을 생성해 준다. Question과 동일한 형태이다.
  • 작성자가 작성한 vote만 보면 되기 때문에 로그인을 안 했을 때 따로 조회가 될 필요도 없어 IsAuthenticatedOrReadOnly가 아닌 IsAuthenticated를 사용해 준다.
  • 또한 Detail에도 본인의 투표만 보이도록 처리해야 하기 때문에 permissions.pyIsVoter라는 class를 추가해 주어야 한다.
#views.py
from django.shortcuts import render, get_object_or_404
from polls.models import Question, Vote
from polls_api.serializers import QuestionSerializer, UserSerializer, VoteSerializer
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework import status, mixins, generics, permissions
from rest_framework.views import APIView
from django.contrib.auth.models import User
from polls_api.serializers import *
from .permissions import IsOwnerOrReadOnly, IsVoter

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated] #나만 봐야 해서 로그인 안 했을 때는 조회를 할 필요도 없음 그래서 ReadOnly가 아닌 IsAuthenticated로 설정

    def get_queryset(self, *arg, **kwargs):
        return Vote.objects.filter(voter=self.request.user) #로그인한 user의 vote만 보이도록

    def perform_create(self, serializer): #overriding
        serializer.save(voter=self.request.user)  

class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsVoter]
#permissions.py
from rest_framework import permissions

class IsVoter(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.voter == request.user  

  • 이 과정을 다 거치면 question에 따라 choice를 선택해 다음과 같이 투표를 할 수 있는 창이 나오게 된다.

4. Validation

1) 투표한 건을 다시 투표할 때 발생하는 오류

  • 투표를 한 건에 대해 다시 투표를 할 때 다음과 같은 오류가 발생한다.
  • 이 오류의 status code는 500인데 투표를 한 내용에 대해서 한 번 더 투표를 한 것은 사용자의 오류이기 때문에 status code가 400대여야 한다.
  • validator를 이용해 question과 voter의 관계가 유일한지(Unique) 확인한다. UniqueTogeterValidator를 사용하기 위해서는 from rest_framework.validators import UniqueTogetherValidator 다음과 같이 import 해 준다.
from rest_framework import serializers
from polls.models import Question, Choice, Vote
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from rest_framework.validators import UniqueTogetherValidator

class VoteSerializer(serializers.ModelSerializer):
    voter = serializers.ReadOnlyField(source = 'voter.username')

    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
            UniqueTogetherValidator(
                queryset = Vote.objects.all(),
                fields = ['question', 'voter'] #unique한지 체크
            )
        ]

  • 다음과 같이 400의 Bad Request 오류가 뜨는 것을 확인할 수 있다.

2) voter 명이 제대로 저장되지 않는 오류

  • 이 문제는 현재 views.py에서 VoteList를 보면 다음과 같이 perform_create를 mixin의 perform_create를 overriding 해서 사용하고 있는데 유효성 검사인 validation이 perform_create가 아닌 그 전 단계의 create 단계에서 발생해 유효성 검사를 할 때는 user를 가지고 올 수 없기 때문이다.
def perform_create(self, serializer): #overriding
        serializer.save(voter=self.request.user)  
  • 이 문제를 해결해 주기 위해 perform_create가 아닌 create를 overriding 해 주어야 한다.
from rest_framework import status
from rest_framework.response import Response

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated] #나만 봐야 해서 로그인 안 했을 때는 조회를 할 필요도 없음 그래서 ReadOnly가 아닌 IsAuthenticated로 설정

    def get_queryset(self, *arg, **kwargs):
        return Vote.objects.filter(voter=self.request.user) #로그인한 user의 vote만 보이도록
        
    def create(self, request, *args, **kwargs):
        new_data = request.data.copy()
        new_data['voter'] = request.user.id #voter를 가지고 와 준다.
        serializer = self.get_serializer(data=new_data)
        serializer.is_valid(raise_exception=True) #이 단계 전에 voter가 있는지 보기 때문에 voter를 이 단계 전에 넣어 주어야 한다.
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
        
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsVoter]
    
    def perform_update(self, serializer):  #update를 통해 voter가 동일하면 새로 create가 아닌 update가 되도록 만들어 준다
        serializer.save(voter=self.request.user)        

3) 전혀 관련 없는 Question과 Choice를 매칭해서 투표를 해도 오류가 발생하지 않고 데이터가 생성되는 오류

  • serializers.py를 수정해 주어야 한다.
  • VoteSerializer에서 validate 함수를 추가해 주어 choicequestion_idquestionquestion_id와 다를 때 ValidationError가 발생하도록 해 준다.
from rest_framework.validators import UniqueTogetherValidator

class VoteSerializer(serializers.ModelSerializer):
    def validate(self, attrs):
        if attrs['choice'].question.id != attrs['question'].id:
            raise serializers.ValidationError("Question과 Choice가 조합이 맞지 않습니다.")
        return attrs
        
    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
            UniqueTogetherValidator(
                queryset = Vote.objects.all(),
                fields = ['question', 'voter'] #unique한지 체크
            )
        ]

5. Testing

  • tests.py에 Test 내용을 작성한다.
  • 이때 Test를 위한 하나의 class를 만들어 주고 TestCase를 상속받도록 한다.
  • TestCase에서는 test_로 시작하는 함수를 테스트 케이스로 판단하여 Terminal에서 테스트를 돌릴 때 인식해 준다.
  • Terminal에서 테스트 케이스를 실행해 보는 방법은 python manage.py test를 입력해 주면 된다.
  • 테스트가 무사히 끝난 경우 '.'이나 그 내용이 출력되며 실패한 경우 'FAIL: test_'가 뜬다.
  • 또한 테스트 케이스를 작성할 때는 self.assertEqual(내 코드가 출력한 값, 테스트 결과로 나와야 하는 값)을 통해 제대로 동작하는지 여부를 확인하게 해 준다.
# TestCase의 기본 틀 
from django.test import TestCase

class QuestionSerializerTestCase(TestCase):
    def test_a(self):
        self.assertEqual(1, 2)
        #False
    
    def test_b(self):
        pass
        #True

1) QuestionSerializer Test

  • 지금까지 만든 QuestionSerializer을 테스트 해 보는 코드를 만들면 다음과 같다.
from django.test import TestCase
from polls_api.serializers import QuestionSerializer

class QuestionSerializerTestCase(TestCase): 
    def test_with_valid_data(self): #question_text를 abc로 넘겼을 때 유효한 값이므로 True가 나와야 한다.
        serializer = QuestionSerializer(data={'question_text': abc})
        self.assertEqual(serializer.is_valid(), True)
        new_question = serializer.save()
        self.assertIsNotNone(new_question.id) #Question 객체의 id 필드가 None이 아닌지 확인
    
    def test_with_invalid_data(self): #question_text를 null로 넘겼을 때 유효하지 않은 값이므로 false가 나와야 테스트 성공
        serializer = QuestionSerializer(data={'question_text': ''})
        self.assertEqual(serializer.is_valid(), False)
  • 명령창을 통해 이 내용을 실행했을 때 다음과 같이 두 Test 모두 OK로 통과한 것을 확인할 수 있다.


✔ [특강] Git/Github 익히기

  • 내가 쓰기 권한이 없을 때 REPO를 하면 folk로 처리하는 것이 맞다.

쓰기 권한이 없는 경우

  • 코드가 있는 상태에서 *git init을 실행해 로컬 repo 생성
  • 코드 중 repo에 업로드 하고 싶은 것을 선택 git add file_name(여러 개도 가능)
  • 이 repo를 내 git에 올리기 위해서는 git commit -m "...." file_name(파일명을 쓰지 않은 경우 git add 한 파일이 그대로 반영됨.)
  • github에 저장할 (비어 있는) repo 만들기
  • 로컬 repo의 remote repo 위치를 지정 git remote add origin (github 에서 만든 repo의 url)
  • 브랜치를 리모트(Github 서버)로 Push (먼저 로컬 브랜치 이름을 main으로 변경) git branch -M main git push -u origin main
  • Github repo 확인

Git Commit(커밋)이란?
커밋은 관련 있는 변경 파일들을 하나로 묶는데 사용한다. (관련 있는 파일들을 변경하거나 코드 추가를 하는데 사용)
이걸 잘하면 특정 버그를 고치기 위해 이 파일들을 묶어서 수정했구나라는 것을 알 수 있음.
이를 통해 나를 포함한 팀원들이 나중에 이 버그를 어떤 파일들을 수정해서 고쳤는지 알 수 있음.

쓰기 권한이 있는 경우

  • 내 로컬 repo로 서버에 있는 repo를 가지고 온다. git clone (github의 repo 주소 url)
  • 최신 버전의 상태를 유지하기 위해 작업 전에는 항상 pull 해 준다. git pull
  • main이 아닌 작업 브랜치를 만들어 주기 위해 브랜치를 생성해 준다. git branch modify_greeting_message(new_branch_name)
  • 만든 작업 브랜치로 브랜치를 변경한다. *git checkout modify_greeting_message(new_branch_name)
  • 다음과 같이 브랜치가 변경된 것을 볼 수 있다.
  • 이후 해당 브랜치를 이용해서 commit 해 준다. git commit -m "modified greeting message(commit message)" hangman.py(commit file)
  • git push 명령을 사용해 로컬 repo 브랜치를 서버의 repo로 복사한다. git push -u origin modify_greeting_message
  • github에 접속해 복사해 준 브랜치가 반영되었는지 확인한다.
  • pull request로 들어가 코드가 수정된 것을 확인 후 PR을 수행한다.

  • Reviewers를 추가한 후 어제 강의에서 들었던 좋은 PR 포맷을 바탕으로 작성한다. 코드 리뷰를 모두 받은 후 반영해야 할 때는 merge 해 준다.
  • 이후 로컬 repo로 돌아와 작업을 main으로 다시 변경한 후 git pull을 진행한다.

🔎 어려웠던 내용 & 새로 알게 된 내용

1. git과 관련된 오류

  • git branch (branch)명 입력 시 fatal: not a valid object name: 'master' 다음과 같은 오류가 발생하는 이유는 한 번도 commit을 하지 않은 repository이기 때문이다. 그래서 이 문제를 해결하기 위해서는 한 번 commit만 진행해 주면 된다.
    git commit -m "commit message"

✍ 회고

다음 주부터 본격적으로 프로젝트가 시작되면 분명 Django를 사용하여 서버를 구현하게 될 거고 github을 통해 서로 공유하게 될 텐데 앞으로의 프로젝트에 응용할 수 있도록 반복해서 복습해야 하지 않을까 싶다.

profile
송의 개발 LOG
post-custom-banner

0개의 댓글