Django4

안재영·2024년 4월 12일

Users로 접근했을때 해당 유저가 가지고있는 question의 디테일로 이동시키는 방법을 알아봅시다

일단 choice의 시리얼라이저를 만들어줍시다

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

serializer에서 questions 부분을 HyperlinkedRelatedField로 수정해주고 이동할 path을 view_name으로 잡아줍니다

class UserSerializer(serializers.ModelSerializer):
    questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')

    class Meta :
        model = User
        fields = ['id','username','questions']

이제 서버를 켜서 users로 접근후 question에 주소를 누르면 해당 주소로 이동할수 있게되었습니다

question detail에서 해당 question의 choice들도 가져와봅시다

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

    def create(self, validated_data):
        return Question.objects.create(**validated_data)

    def update(self, instance, validated_data):
            instance.question_text = validated_data.get('question_text', instance.question_text)
            instance.save()
            return instance
 

choice를 가져오는 부분과 fields에 choice를 추가해줍시다

그리고 모델로 이동해서


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

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

choice의 question 부분의 related_name을 serializer에서 호출하는 이름과 맞게 추가해줍시다

서버를 확인해서 question detail로 접근하면 이제 choices에 choice들이 담겨오는것을 확인할수 있습니다

이제 투표(vote)기능을 구현해봅시다

전에 만든기능으로는 vote의 값을 누구나 횟수제한없이 올릴수 있엇기때문에 해당 행위를 막기위해 question과 user의 정보를 받아 해당값이 있으면 재투표가 불가능하게 만들어줍시다

그러면 일단 question과 user의 정보를 받아둘 vote 모델을 생성해줍니다

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

Meta에 constraints로 UniqueConstraint의 fields로 question과 voter를 넣고 네임은 unique_voter_for_questions로 만들어줍니다

이제 같은 question과 voter값이 있으면 데이터가 생성되지 못할것입니다

시리얼라이저도 수정해줍니다

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()
    

이제 vote에 만들어진 데이터의 개수를 votes_count로 넘겨줄것입니다

투표하는 기능을 만들기위해

views로 가서 vote관련기능을 추가해줍시다

class VoteList(generics.ListCreateAPIView):
    serializer_class = VoteSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_queryset(self, *arg, **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.IsAuthenticated,IsVoter]

permissions에서 IsVoter를 만들어 본인확인기능을 추가해줍니다

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

이제 접근을 위한 urls수정을 하면 작업이 완료가됩니다

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(), name='user-detail'),
    path('register/', RegisterUser.as_view()),
    path('api-auth/', include('rest_framework.urls')),
    path('vote/', VoteList.as_view()),
    path('vote/<int:pk>', VoteDetail.as_view()),
]

이제 vote로 들어가면 내가 투표한 기록과 투표기능이 구현이되있는것을 볼수있습니다

기능 구현이 완료됬으니 이제 오류들을 처리해봅시다

validation처리가 부족하여 조건에 맞지않아도 저장이되는 경우가 여럿있습니다 해당 기능들을 처리해봅시다

시리얼라이저에서 choince의 question.id와 question의 id와 일치하지 않으면 validation을 통과하지 못하게 처리해줍니다

from rest_framework.validators import UniqueTogetherValidator
class VoteSerializer(serializers.ModelSerializer):
    # voter = serializers.ReadOnlyField(source='voter.username')

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

view에서 create시 voter가 validation에 걸리면 전달되지 않아 생기는 오류와 voter가 수정되는 오류를 해결해줍니다

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

이제 기능들이 잘 동작하는지 서버에서 테스트해봅시다

하지만 이렇게 기능이 추가될 때 마다 하나하나 수정하는 것 은 굉장히 많은 시간이 필요할수 있습니다 이러한 문제를 더 간단하게 처리하는 방법을 알아봅시다

tests.py

from django.test import TestCase
from polls_api.serializers import QuestionSerializer

# Create your tests here.
class QuestionSerializerTestCase(TestCase):
    def test_a(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)

테스트 케이스를 생성할때는 test_를 method 앞에 붙여줘야지 해당 method가 테스트 케이스라는 것을 인식할수 있습니다

위 테스트 케이스는 test_a에서는 abc의 값이 question모델의 question_text로 전달됬을때 is_valid를 통과하는지 확인후 해당 데이터가 저장후 정상적으로 저장되어 id값을 가지게됬는지 확인하는 케이스

test_with_invalid_data는 question_text값이 “”가 들어갔을때 is__valid() 을 통과에 실패하는지 확인하는 케이스입니다

해당 케이스를 cmd에서 python manage.py test으로돌려보면 통과 여부를 알수있습니다

이번에는 시리얼라이저를 테스트해봅시다

from django.test import TestCase
from polls_api.serializers import *
from django.contrib.auth.models import User
from polls.models import Question, Choice, Vote

# Create your tests here.
class VoteSerializerTestCase(TestCase):
    def test_vote_serializer(self):
        user = User.objects.create(username='testUser')
        question = Question.objects.create(
            question_text = 'abc',
            owner = user,
        )
        choice = Choice.objects.create(
            question = question,
            choice_text = '1'
        )
        data = {
            'question': question.id,
            'choice':choice.id,
            'voter': user.id
        }
        serializer = VoteSerializer(data=data)
        self.assertTrue(serializer.is_valid())
        vote = serializer.save()

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

    def test_vote_serializer_with_duplicate_vote(self):
        
        user = User.objects.create(username='testUser')
        question = Question.objects.create(
            question_text = 'abc',
            owner = user,
        )
        choice = Choice.objects.create(
            question = question,
            choice_text = '1'
        )
        choice1 = Choice.objects.create(
            question = question,
            choice_text = '2'
        )
        
        Vote.objects.create(question = question, choice=choice, voter=user)
        data = {
            'question': question.id,
            'choice':choice1.id,
            'voter': user.id
        }

        serializer = VoteSerializer(data=data)
        self.assertFalse(serializer.is_valid())

해당 부분은 voteSerializer가 잘 작동하는지 확인하는 케이스와 중복투표 가능여부를 확인하는 케이스입니다.

케이스는 잘 작동하지만 두 케이스를보면 같은 코드가 반복되는 부분이 있습니다 이부분을 setup을 이용하면 반복되는 부분을 처리할수 있습니다

class VoteSerializerTestCase(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):
        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):
        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':choice1.id,
            'voter': self.user.id
        }

        serializer = VoteSerializer(data=data)
        self.assertFalse(serializer.is_valid())

셋업은 각 테스트의 앞에서 먼저 한번씩 실행이 되며 해당 테스트가 끝난뒤에는 테스트를 진행됬던 DB는 초기화가 됩니다

그러면 이번엔 views를 테스트해봅시다

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

class QuestionListTest(APITestCase):
    def setUp(self):
        self.question_data = {'question_text':'some question'}
        self.url = reverse('question-list')

    def test_create_question(self):
        user = User.objects.create(username='testuser', password='testpassword')
        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'])
        
    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_questions(self):
        question = Question.objects.create(question_text='question1')
        choice = Choice.objects.create(choice_text="choice1", question=question)
        Question.objects.create(question_text='question2')
        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)

첫번째 테스트는 유저정보를 생성하고 해당 유저를 로그인시킨뒤 question_data를 post시켜 생성했을때 201코드가 오는지 question.objects.all()의 count가 1인지 question.objects.first()가 방금 생성한 question_text가 일치하는지를 확인하는 코드입니다

간단하게 정상적으로 create를 성공했는지를 보는 케이스입니다

두번째는 로그인하지 않고 create를 시도했을때 403에러가 발생하는지를 확인하는 코드입니다.

로그인이 되지않았기 때문에 생성이되면 안되기때문이죠

세번째는 두개의 question을 생성하고 하나의 choice를 생성한뒤 해당 2개의 question과 choice가 잘 생성됬는지 확인하는 코드입니다

test를 이용하면 해당 코드가 잘 작동하고있는지 확인하는데 큰 도움을 받을수 있습니다 이는 코드를 수정했을때 그전에 작성된 코드에 이상이생겻는지 확인하는데 굉장히 유용하기때문에 test케이스를 잘 만들어두는게 좋습니다

또한 내가 기능에 대하여 test를 잘 진행하고있는지 궁금한경우 사용할수있는 tool이 있습니다

pip install coverage

coverage를 설치한뒤

coverage run manage.py test

coverage로 테스트를 진행한뒤

coverage report

리포트를 확인하면 내가 만든 test가 기능에 대하여 몇퍼센트를 커버하고있는지 한눈에 볼수있게 나옵니다

해당기능을 참고하면 내가 어디기능의 테스트가 부족한지 더 쉽게알수있습니다

0개의 댓글