
- 연관 필드 처리
- 투표(Votes) 기능 구현하기
- Validation
- Testing
저번에 작성한 코드대로 사용자 목록을 불러왔을 때는 user 내의 questions 에 question id 가 들어있었다. 이 항목을 id 가 아닌 다른 값으로 변경해서 가독성을 올리려 한다.
serializers.py의 UserSerializer의 questions를 받아오는 처리에 serializers.StringRelatedField()를 사용하면 Question 모델의 ''__str__'' 메서드 값을 가져올 수 있다.class UserSerializer(serializers.ModelSerializer):
# question들을 가져오는 필드의 정보는 User 테이블에 있는게 아니기 때문에 따로 불러오는 처리가 필요
# StringRelatedField 이용하여 Question 모델의 __str__ 메서드를 호출하여 question_text를 가져온다.
questions = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ('id', 'username', 'questions')
[result]

serializers.SlugrelatedField() 를 사용하면 된다. class UserSerializer(serializers.ModelSerializer):
# question들을 가져오는 필드의 정보는 User 테이블에 있는게 아니기 때문에 따로 불러오는 처리가 필요
# SlugRelatedField를 사용하여 question_text가 아닌 pub_date를 가져옴
questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
class Meta:
model = User
fields = ('id', 'username', 'questions')
[result]

serializers.HyperlinkedRelatedField() 를 사용하면 된다. class UserSerializer(serializers.ModelSerializer):
# question들을 가져오는 필드의 정보는 User 테이블에 있는게 아니기 때문에 따로 불러오는 처리가 필요
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
class Meta:
model = User
fields = ('id', 'username', 'questions')
[result]

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
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
# choice_set 변수 추가
choices = ChoiceSerializer(many=True, read_only=True)
class Meta:
model = Question
fields = ('id', 'question_text', 'pub_date', 'owner', 'choices')
[result]

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)
# 하나의 question에 대해 사용자는 하나의 vote만 가질 수 있게 설정.
class Meta:
constraints =[
models.UniqueConstraint(fields=['question', 'voter'], name='unique_voter_for_questions'),
]
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()

VoteSerializer 구현 class VoteSeriazlier(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='voter.username')
class Meta:
model = Vote
fields = ('id', 'question', 'choice', 'voter')
VoteList, VoteDetail) # polls_api/views.py
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSeriazlier
permissions_classes = [permissions.IsAuthenticated]
# 아무나 모든 vote를 보면 안되기 때문에 내 vote만 보도록 설정
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 = VoteSeriazlier
permissions_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
from django.urls import path, include
from .views import *
urlpatterns = [
# ...
path('vote/', VoteList.as_view()),
path('vote/<int:pk>/', VoteDetail.as_view()),
]
- 현재까지 진행한 Vote 서비스의 문제점은 사용자가 같은 질문에 한번 더 Vote를 했을 때 에러를 잘 처리하지 않았고, 질문에 엉뚱한 대답을 해도 그대로 저장이 된다. 이를 고쳐보자.
Serializers.py에 유효성 검사 코드 추가
class VoteSeriazlier(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='voter.username')
class Meta:
model = Vote
fields = ('id', 'question', 'choice', 'voter')
validators = [
# question 과 voter가 Unique한 지를 유효성 검사 진행
UniqueTogetherValidator(
queryset = Vote.objects.all(),
fields=['question', 'voter']
)
]
create 하기 이전에 유효성 검사를 진행하게 되므로 view 에서 perform_create를 변경해야 한다.views.py 수정 class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSeriazlier
permissions_classes = [permissions.IsAuthenticated]
# 아무나 모든 vote를 보면 안되기 때문에 내 vote만 보도록 설정
def get_queryset(self, *args, **kwargs):
return Vote.objects.filter(voter=self.request.user)
# perform_create 가 아닌 create 메서드를 바로 오버라이드
def create(self, request, *args, **kwargs):
현재 접속한 id를 voter로 설정
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 VoteSeriazlier(serializers.ModelSerializer):
class Meta:
model = Vote
fields = ('id', 'question', 'choice', 'voter')
validators = [
# question 과 voter가 Unique한 지를 유효성 검사 진행
UniqueTogetherValidator(
queryset = Vote.objects.all(),
fields=['question', 'voter']
)
]
# polls_api/views.py
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSeriazlier
permissions_classes = [permissions.IsAuthenticated, IsVoter]
# 접속한 사용자를 voter로 강제적으로 지정
def perfrom_update(self, serializer):
serializer.save(voter=self.request.user)
class VoteSeriazlier(serializers.ModelSerializer):
def validate(self, attrs):
if attrs['choice'].question.id != attrs['question'].id:
raise serializers.ValidationError({'Question과 Chocie가 조합이 맞지 않습니다.'})
return attrs
기능을 추가할 때 마다 직접 테스트를 해보는 것은 매우 어려워진다. 또한 수정 시 전혀 예상치 못한 곳에서 버그가 발생하기도 한다. 그 때 마다 전체를 테스트하는 것은 비효율적이다. Django 프로젝트는 test.py 파일에 테스트 케이스를 작성해서 테스트를 수행한다.
test.py에 test 작성from django.test import TestCase
# Create your tests here.
class QuestionSeriazlierTestCase(TestCase):
def test_a(self):
print("This is test a")
def test_b(self):
print("This is test b")
def some_method(self):
print("This is some method")
python manage.py test
some_method는 실행이 되지 않는데 이는 django에서 이름에 test가 들어있지 않는 함수는 test로 판단하지 않기 때문이다.self.assertEqual( obj, obj )를 사용한다. 이는 인자로 들어온 두 값이 다를 경우 Fail이 발생한다.from django.test import TestCase
# Create your tests here.
class QuestionSeriazlierTestCase(TestCase):
def test_a(self):
self.assertEqual(1, 2)
def test_b(self):
pass
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 VoteSerializerTest(TestCase):
# setUp 메서드는 각 테스트에 공통부분을 작성해서 일괄적으로 적용시키는 함수이다.
# setUp을 이용한 결과는 다음 테스트에 반영되지 않는다.
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_with_valid_data(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())
def test_vote_serializer_with_unmatched_question_and_choice(self):
question2 = Question.objects.create(
question_text = 'abc',
owner = self.user
)
choice2 = Choice.objects.create(
question = question2,
choice_text = '1',
)
data = {
'question' : self.question.id,
'choice' : choice2.id,
'voter' : self.user.id,
}
serializer = VoteSerializer(data=data)
self.assertFalse(serializer.is_valid())
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)

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
from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status
from django.utils import timezone
#Serializer Test
#...
# View Test
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='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.assertLess((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.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)

역시 테스트 케이스를 만들고 테스트를 실행하는 데 꼼꼼하게 따져봐야 한다는 것을 다시 한번 깨달았다. 테스트에서 에러가 났을 때 이게 내가 테스트를 잘못 만든건지, 아니면 소스코드에 문제가 있는건지 계속 왔다 갔다 하면서 어디에서 오류가 생겼는지를 찾는 것이 제일 어려웠던 것 같다. 찾고나면 변수이름을 살짝 다르게 설정했다던지... , () 괄호를 빼먹엇다던지 ... 사소한 부분에서 오류가 생겨서 참 작성할 때부터 정확하게 타이핑 할 껄 이라는 생각이 들었다.