기록 블로그

프로그래머스 데브코스-데이터 엔지니어/TIL(Today I Learned)

04/28 15일차 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (5)

usiohc 2023. 4. 28. 17:03

파이썬 장고 프레임웍을 사용해서 API 서버 만들기


주요 메모 사항


 

 

RelatedField

  • polls_api/serializers.py
class ChoiceSerializer(serializers.ModelSerializer): 
    class Meta:
        model = Choice
        fields = ['choice_text', 'votes']

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']

class UserSerializer(serializers.ModelSerializer):
    #questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
    #questions = serializers.StringRelatedField(many=True, read_only=True)
    #questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
    questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
    
    class Meta:
        model = User
        fields = ['id', 'username', 'questions']

 

  • polls_api/urls.py
from django.urls import path,include
from .views import *

urlpatterns = [
    path('question/', QuestionList.as_view(), name='question-list'),
    path('question/<int:pk>/', QuestionDetail.as_view(),name='question-detail'),
    path('users/', UserList.as_view(),name='user-list'),
    path('users/<int:pk>/', UserDetail.as_view()),
    path('register/', RegisterUser.as_view()),
    path('api-auth/', include('rest_framework.urls'))
]

 

  • polls/models.py
...


class Choice(models.Model):
    question = models.ForeignKey(Question, related_name='choices', on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text

 

  • rest/users/, HyperLinkRelatedField

 

 


투표(Votes) 기능 구현하기 1 - Models

  • polls/models.py
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)
    
    class Meta:
        constraints = [
            models.UniqueConstraint(fields=['question', 'voter'], name='unique_voter_for_questions')
        ]

 

 

  • polls_api/serializers.py
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()

 

 

  • Django Shell
>>> from polls.models import *
>>> question = Question.objects.first()
>>> choice = question.choices.first()
>>> from django.contrib.auth.models import User

>>> question
<Question: 제목: 휴가를 어디서 보내고 싶나요?, 날짜: 2023-04-24 06:48:53+00:00>

>>> choice
<Choice: 바다>

>>> user= User.objects.get(username='user4') 
>>> user
<User: user4>

>>> Vote.objects.create(voter=user,question=question,choice=choice)
<Vote: Vote object (1)>

>>> Vote.objects.first()
<Vote: Vote object (1)>

>>> question.id
1

 

 

  • rest/question/1, user4로 shell에서 투표한 결과

 

 

 


투표(Votes) 기능 구현하기 2 - Serializers & Views 

  • polls_api/serializers.py
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']

 

 

  • polls_api/views.py
from polls.models import Question,Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly , IsVoter

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_queryset(self, *args, **kwargs):
        return Vote.objects.filter(voter=self.request.user)
   
    def perform_create(self, serializer):
        serializer.save(voter=self.request.user)
  
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Vote.objects.all()
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsVoter]

 

 

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

 

 

  • polls_api/urls.py
from django.urls import path, include
from .views import VoteList, VoteDetail

urlpatterns = [
    ...
    path('vote/', VoteList.as_view()),
    path('vote/<int:pk>/', VoteDetail.as_view()),
]

 

 

  • rest/vote/ Vote 하는 api 기능 구현 화면

 

 


Validation

  • polls_api/serializers.py
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']
            )
        ]

 

 

  • polls_api/views.py
from rest_framework import status
from rest_framework.response import Response

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_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)
        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)

 

 

 

 

  • 이전의 vote 기능에 대한 문제점으로 다음 2가지 문제가 있었음
    1. 동일한 투표를 진행했을때 오류 페이지와 오류코드 (400번대 사용자 오류) 를 표현해야함
    2. question에 맞는 choice를 선택하지 않은 vote에 대해서 불가능 하게 정의해야함

 

 

  • 위 문제를 해당 코드 사용으로 해결

 


Testing

  • polls_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': 'abc'})
        self.assertEqual(serializer.is_valid(), True)
        new_question = serializer.save()
        self.assertIsNotNone(new_question.id)
        
    def test_with_invalid_data(self):
        serializer = QuestionSerializer(data={'question_text': ''})
        self.assertEqual(serializer.is_valid(), False)

 

 

  • terminal 테스트 실행하기
python manage.py test


# 결과
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.003s

OK
Destroying test database for alias 'default'...

 


Testing Serializers 

  • polls_api/tests.py
class VoteSerializerTest(TestCase):
    def setUp(self):
        self.user = User.objects.create(username='testuser')
        self.question = Question.objects.create(
            question_text='abc',
            owner=self.user,
        )
        self.choice = Choice.objects.create(
            question=self.question
            choice_text='1'
        )    
    
   def test_vote_serializer(self):  
        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)
    
    def test_vote_serializer_with_duplicate_vote(self):
        self.assertEqual(User.objects.all().count, 1)
        choice1 = Choice.objects.create(
            quetsion=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())
    
    def test_vote_serilaizer_with_unmatched_question_and_choice(self):
        question2 = Question.objects.create(
            question_text='abc',
            owner=self.user,
        )
    
        choice2 = Choice.objects.create(
            quetsion=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())

 

  • 테스트 실행
(django-venv) C:\_code\de-data_eng\week3_Django\mysite>python manage.py test
Found 3 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.FE
======================================================================
ERROR: test_vote_serilaizer_with_unmatched_question_and_choice (polls_api.tests.VoteSerializerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\_code\de-data_eng\week3_Django\mysite\polls_api\tests.py", line 56, in test_vote_serilaizer_with_unmatched_question_and_choice
    choice2 = Choice.objects.create(
  File "C:\_code\de-data_eng\week3_Django\django-venv\lib\site-packages\django\db\models\manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "C:\_code\de-data_eng\week3_Django\django-venv\lib\site-packages\django\db\models\query.py", line 669, in create
    obj = self.model(**kwargs)
  File "C:\_code\de-data_eng\week3_Django\django-venv\lib\site-packages\django\db\models\base.py", line 585, in __init__
    raise TypeError(
TypeError: Choice() got unexpected keyword arguments: 'quetsion'

======================================================================
FAIL: test_vote_serializer_with_duplicate_vote (polls_api.tests.VoteSerializerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\_code\de-data_eng\week3_Django\mysite\polls_api\tests.py", line 35, in test_vote_serializer_with_duplicate_vote
    self.assertEqual(User.objects.all().count, 1)
AssertionError: <bound method QuerySet.count of <QuerySet [<User: testuser>]>> != 1

----------------------------------------------------------------------
Ran 3 tests in 0.012s

FAILED (failures=1, errors=1)
Destroying test database for alias 'default'...

 


Testing Views

  • polls_api/tests.py
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': 'some question'}
        self.url = reverse('queston-list')
    
    def test_create_question(self):
        user =User.objects.create(username='testuser', password='testpass')
        self.client.force_authenticate(user=user)
        response = self.client.post(self.url, self.question_data)
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        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)
 
    def test_list_question(self):
        question = Question.objects.create(question_text='Question1')
        choice = Choice.objects.create(question=question, choice_text='Question1')
        Question.objects.create(question_text='Question2')
        response = self.client.post(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)

 

 

  • Python 코드 커버리지 라이브러리 설치하기
pip install coverage

 

  • Python 코드 커버리지 라이브러리 실행하기
coverage run manage.py test

 

 

공부하며 어려웠던 내용

tests.py를 작성하는게 가장 어려웠다. 약간 shell에 입력할 tmp 데이터들을 코드에서 선언하는 느낌으로 받아들였는데, 내가 짠 코드의 구조를 완벽하게 이해하고 있어야만 작성이 가능할 것 같다. -> 다시 코드 리뷰를 해봐야겠음

 

이외의 코드 수정이나, 유효성 검사 코드를 구현하는것은 어렵지 않게 해결했다.