DevCourse TIL Day5 Week4

김태준·2023년 4월 27일
0

Data Enginnering DevCourse

목록 보기
17/93
post-thumbnail

금일 학습 내용은 RelatedField, Model 구현, Validation, Testing 과정이다.

음, 갈수록 사이트 내 필요요소들을 학습하고 실제로 배포하는 과정도 학습!

주말에 무조건 복습을 해야겠다는 생각이 든다.
복습하자..✈️

✅ RelatedField

qustion과 user사이 관계를 정의하는 Field에 대해 알아보자.


# api/serializers.py
## RelatedField
from django.contrib.auth.models import User

# choice 모델 내 필드 표시 위해 생성
class ChoiceSerializer(serializers.ModelSerializer): 
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes']
        
# choiceserializer에서 정의한 내용이 questionserializer에도 적용되도록 함
class QuestionSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    # app/models.py가서 choice 모델에서 question에다가 related_name='choices' 해주어야 함.
    choices = ChoiceSerializer(many=True, read_only=True)
    
    class Meta:
        model = Question
        fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']

class UserSerializer(serializers.ModelSerializer):
    # 기존 (id 출력 됌)
    questions = serializers.PrimaryKeyRelatedField(many = True, queryset = Question.objects.all())
    # Question 모델의 str 출력
    questions = serializers.StringRelatedField(many=True, read_only=True)
    # 특정 field 지정
    questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
    # question 내용 링크 제공
    questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
    class Meta:
        model = User
        fields = ['id', 'username', 'questions']

✍️ Polls 기능 추가

지금까지 학습한 내용을 기반으로 로그인한 사용자에 한해 투표가 가능토록 기능 구현!
현재 models.py에는 selected_choice.votes = F('votes') + 1로 인해 누구든 여러번 투표가 가능한 상황. -> 사용자 단위로 투표 가능토록 기능 구현 必

# app/models.py에서 처리 필요! - django shell에서 vote 구현
## 사용자 단위로 투표 가능토록 기능 구현    
from django.contrib.auth.models import User

class Vote(models.Model):
    qustion = 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 = ['qustion', 'choice', 'voter'], name = 'unique_voter_for_questions')
        ]
        
# app_api/serializers.py - django shell
## 사용자 단위로 VOTE 기능 구현하기
class ChoiceSerializer(serializers.ModelSerializer):
	# 투표 횟수 표기해주기
    votes_count = serializers.SerializerMethodField()
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes_count']
    def get_votes_count(self, obj):
    	# Foreignkey로 관계 정의될 때 별도의 related_name없으면 _ 언더바로 갖고오기 가능
        return obj.votes_set.count()
        

✍️ < 직접 serializer로 vote 생성하기 >

# api_serializers.py
## 여기서 직접 vote 생성하기
from polls.models import Question, Choice, Vote

class VoteSerializer(serializers.ModelSerializer):
    voter = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']

# api/views.py
## Vote 기능 구현
from polls.models import Question, Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly, IsVoter
from rest_framework import generics,permissions

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    # 자신의 투표만 볼 수 있도록 쿼리셋 정의 필요
    def get_queryset(self):
        return Vote.objects.filter(voter = self.request.user)
    
    def perform_update(self, serializer):
        serializer.save(voter = self.request.user)
# 수정도 필요하므로 생성 
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated, IsVoter]

# api/permissions.py
# IsVoter 구현위해 적용
class IsVoterOrReadOnly(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.voter == request.user

# api/urls.py 
# vote기능 구현에 대해 처리 필요
    path('vote/', VoteList.as_view()),
    path('vote/<int:pk>/', VoteDetail.as_view()),

✅ Validation

앞서 만든 Vote 기능 Error 발생 동일 유저가 vote 다시 진행 시 500 error 발생
-> Validation 작업 실행 필요!

# api/serializers.py

## 동일인물 재투표 시 500 error OR question에 속하지 않은 choice 선택 시 에러 발생 필요
from polls.models import Question, Choice, Vote
class VoteSerializer(serializers.ModelSerializer):
	# choice와 question의 조합이 옳은지 판단
    def validate(self, attrs):
        if attrs['choice'].question.id != attrs['question'].id:
            raise serializers.ValidationError('해당 question과 choice의 id가 일치하지 않습니다.')
        return attrs
    class Meta:
        model = Vote
        fields = ['id', 'question', 'choice', 'voter']
        validators = [
        	# 한 사람이 하나의 질문에 대해 vote를 1개만 생성 가능
            UniqueTogetherValidator(
            queryset = Vote.objects.all(),
            fields = ['question', 'choice'],
            )
        ]

# api/views.py
## 동일인물 재투표 시 500 error OR question에 속하지 않은 choice 선택 시 에러 발생 필요
# 화면 표기해주기
from polls.models import Question, Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly, IsVoter
from rest_framework import generics,permissions

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    def queryset(self, *args, **kwargs):
        return Vote.objects.filter(voter = self.request.user)
    def create(self, request, *args, **kwargs):
        new_data = request.data.copy()
        new_data['voter'] = request.user.id
        serializer = self.get_serializer(data=new_data)
        # perform_create 이전 유효성검사 처리하기
        serializer.is_valid(raise_exception = True)
        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):
        serializer.save(voter = self.request.user)

✅ Testing

그동안 사이트에 들어가 일일이 검색하고 클릭하며 검증을 하고 테스팅을 했는데, 이는 너무 비효율적이다. 따라서 컴퓨터가 알아서 테스트를 하는 자동화된 작업을 구현해보고자 한다.
❗ 코드가 구현하고 있는 바를 최대한 표현할 수 있도록 상세히 적는 것이 핵심!

  • cmd에서 실행하는 방법 : python manage.py test
app_api/tests.py
from django.test import TestCase
from polls_api.serializers import QuestionSerializer

class QuestionSerializerTestcase(Testcase):
    def test_with_valid_data(self):
        serializer = QuestionSerializer(data = 'question_text': 'taejun')
        # assertEqual(a, b) a==b면 유효성 검사 통사한 것 이후 DB에 save
        self.assertEqual(serializer.is_valid(), True)
        new_question = serializer.save()
        # save한 new_question에 대해 id None 여부 확인
        self.assertIsNotNone(new_question.id)
    # 질문지가 빈칸인 경우 False인 경우 처리
    def test_with_invalid_data(self):
        serializer = QuestionSerializer(data = {'question_text' : ' '})
        self.assertEqual(serializer.is_valid(), False)

< vote serializer testing 하기 >


## vote seritalizer testing
from django.test import TestCase
from polls_api.serializers import QuestionSerializer, VoteSerializer
from django.contrib.auth.models import User
from polls.models import Question, Choice, Vote

class VoteSerializerTestcase(Testcase):
    # user, question, choice setup 함수로 처리
    # test마다 독립적으로 처리
    def setup(self):
        self.user = User.objects.create(username = 'testuser')
        self.question = Question.objects.create(
            question_text = 'test question',
            owner = self.user,
        )
        self.choice = Choice.objects.create(
            question = self.question,
            choice_text = 'test choice',
        )
    # 투표 행위에 대해 들어오는 데이터를 비교
    def test_vote_serializer(self):
        # setup 실행 결과이후 아래 duplicate함수와 중복 여부 확인
        self.assertEqual(User.objects.all().count() + 1)
        data = {
            'question' : self.question.id,
            'choice' : self.choice_id,
            'voter' : self.user.id
        }
        serializer = VoteSerializer(data = data)
        self.assertTrue(serializer.is_valid())
        vote = serializer.save()

        self.assertEqual(vote.question, self.question)
        self.assertEqual(vote.choice, self.choice)
        self.assertEqual(vote.voter, self.user)

    # 투표에 대해 동일 user가 투표하는 경우 
    def test_vote_serializer_with_duplicate_vote(self):
        # setup 실행 결과이후 위 test_vote_serializer함수와 중복 여부 확인
        self.asserEqual(User.objects.all().count, 1)
        choice1 = Choice.objects.create(
            question = self.question,
            choice_text = '2'
        )
        Vote.objects.create(question = self.question, choice = self.choice, voter = self.user)
        data = {
            'question' : self.question.id,
            'choice' : self.choice.id,
            'voter' : self.user.id
        }
        serializer = VoteSerializer(data = data)
        self.assertTrue(serializer.is_valid())
    
    # question, choice 불일치 하는 경우 
    def test_vote_serializer_with_unmatched_question_and_choice(self):
        question2 = Question.objects.create(
            question_text = 'test question',
            owner = self.user,
        )
        choice2 = Choice.objects.create(
            question = question2,
            choice_text = '1'
        )
        data = {
            'question' : self.question.id,
            'choice' : self.choice.id,
            'voter' : self.user.id
        }
        serializer = VoteSerializer(data = data)
        self.assertTrue(serializer.is_valid())

< view testing >

from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from django.utils import timezone

class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {'question_text': 'test question'}
        # question-list에 해당하는 url 써야함, 
        # reverse는 reverse_lazy와 같은 역할. 메서드 내에서는 reverse 사용
        self.url = reverse('question_list')
    
    # question 생성
    def text_create_question(self):
        user = User.objects.create(username = 'testuser', password = 'testpass')
        # APITestcase 쓰는 이유 (사용자 강제 로그인 처리)
        self.client.force_authenticate(user = user)
        # post 요청 아래 url로 self.question_data 요청이 post로 날라가고 결과를 response에 저장
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        # question 잘 만들어졌는지 확인
        self.assertEqual(Question.objects.count(), 1)
        # 첫 인자 뺴와서 질문이 맞게 됐는지 확인
        question = Question.objects.first()
        self.assertEqual(question.question_text, self.question_data['question_text'])
        self.assertEqual((timezone.now() - question.pub_date).total_seconds(), 1)
    
    # 로그인 되지 않은 상황
    def test_create_question_without_authentication(self):
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
    
    # 모든 question 목록 불러오기 (사용자 로그인 안해도 됌)
    def test_list_question(self):
        question = Question.objects.create(question_text = 'Question 1')
        choice = Choice.objects.create(question = question, choice_text = 'Choice 1')
        Question.objects.create(question_text = 'Question 2')
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 2)
        self.assertEqual(response.data[0]['choices'][0]['choice_text'], choice.choice_text)

🎇 정리

최종적으로 터미널에서 coverage를 사용해 code를 얼마나 잘 테스트하는 지 판단.

  • pip install coverage로 설치
  • coverage run manage.py test를 실행
  • 이후 coverage report를 통해 각 파이썬 파일 별 Miss와 Cover%를 확인할 수 있다.

이번주 장고 학습을 맛보기로 진행해보았는데, 기존 사용했던 python코드를 기반으로 진행하는 웹 프레임워크여서 아무래도 이해는 수월했다. 하지만 cmd 기반으로 명령을 내리고 django shell 작업도 진행하다보니 적응이 힘들었고, django 역시 마찬가지고 사이트와 번갈아가며 test하고 오류 찾고 하느라 오래걸렸다.

✈️ 복습완료!!

profile
To be a DataScientist

0개의 댓글