이전에 유저 목록을 조회할 때,
사진에서 처럼 1번 유저의 questions가 id로 출력되고 있는 것을 확인할 수 있습니다. 이를 question의 str로 출력되도록 바꿔 보겠습니다.
class UserSerializer(serializers.ModelSerializer):
# StringRelatedField로 변경.
questions = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'questions']
수정 후 페이지를 다시 확인하면,
Question 모델의 __str__
로 출력되는 것을 확인할 수 있습니다.
위 코드처럼 여러 방식으로 응용할 수 있습니다.
class UserSerializer(serializers.ModelSerializer):
# Slug를 이용하여 pub_date로 표시.
questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
class Meta:
model = User
fields = ['id', 'username', 'questions']
class UserSerializer(serializers.ModelSerializer):
# 하이퍼링크로 표시.
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
class Meta:
model = User
fields = ['id', 'username', 'questions']
다음으로 question에 대해서 Choice를 출력하도록 해 봅니다.
# Choice에 대한 시리얼라이저 추가
class ChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['choice_text', 'votes']
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
# choice_set 이라는 Choice시리얼라이저.
choice_set = ChoiceSerializer(many=True, read_only=True)
class Meta:
model = Question
# fields에 choice_set 추가
fields = ['id', 'question_text', 'pub_date', 'owner', 'choice_set']
choice_set 이라는 이름 대신에 choices로 수정해 봅니다.
polls/models.py
class Choice(models.Model):
# choices로 realted_name 설정.
question = models.ForeignKey(Question, related_name='choices', on_delete=models.CASCADE) # 외래키 Question을 CASCADE로 참조한다.
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
def __str__(self):
return self.choice_text
```
polls_api/serializers.py
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
# choices 이라는 Choice시리얼라이저.
choices = ChoiceSerializer(many=True, read_only=True)
class Meta:
model = Question
# fields에 choices 추가
fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']
코드를 다음과 같이 수정합니다.
from django.db import models
from django.utils import timezone
import datetime
from django.contrib.auth.models import 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')
]
class ChoiceSerializer(serializers.ModelSerializer):
# method에 의해서 값이 결정되는 votes_count
votes_count = serializers.SerializerMethodField()
class Meta:
model = Choice
fields = ['choice_text', 'votes_count']
# method 정의
def get_votes_count(self, obj):
return obj.vote_set.count()
>>> from polls.models import *
# Question, Choice 객체 입력
>>> question = Question.objects.first()
>>> question
<Question: 휴가를 보내고 싶은 장소는?>
>>> choice = Choice.objects.first()
>>> choice
<Choice: 바다>
# User 객체 입력
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='admin')
>>> user
<User: admin>
# 위 객체들을 인수로 갖는 Vote 객체 생성.
>>> Vote.objects.create(voter=user, question=question, choice=choice)
<Vote: Vote object (1)>
>>> Vote.objects.first()
<Vote: Vote object (1)>
>>> question.id
1
위 이미지에서 특정 질문의 choices에 대한 votes_count를 확인할 수가 있습니다.
이제 직접 투표를 할 수 있도록(CRUD가 가능하도록) django rest framework에서 활용해 봅니다.
# Vote 모델 시리얼라이저 정의
class VoteSerializer(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='voter.username')
class Meta:
model = Vote
fields = ['id', 'question', 'choice', 'voter']
# Vote 뷰 정의
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.IsAuthenticated, IsVoter]
from rest_framework import permissions
class IsVoter(permissions.BasePermission):
# 오직 해당 유저가 voter인 경우에만 가능하도록 권한 부여
def has_object_permission(self, request, view, obj):
return obj.voter == request.user
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')),
path('vote/', VoteList.as_view()),
path('vote/<int:pk>', VoteDetail.as_view()),
]
http://127.0.0.1:8000/rest/vote/ 에서 현재 admin 유저의 투표 내역을 확인할 수가 있으며 특정 question에 대한 choice를 추가할 수도 있습니다.
http://127.0.0.1:8000/rest/vote/1 에서 admin 계정의 특정 Vote에 대한 정보를 확인할 수 있습니다. 그리고 해당 vote를 PUT하여 수정하는 기능도 확인할 수가 있습니다.
현재 상황에서 다음과 같은 문제점들이 존재합니다.
이미 투표한 질문에 대해서 중복 투표를 하는 경우, 500 error(서버 에러)가 발생합니다. 하지만, 이는 서버 에러가 아닌 사용자의 잘못된 선택으로 인한 에러이기에 400번 대 error가 발생해야 합니다.
특정 질문에 대해서 이상한 Choice를 선택할 수가 있습니다. 특정 질문에만 해당되는 Choice만 고르도록 수정해야 합니다.
from rest_framework import serializers
from polls.models import *
from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
# validator 임포트
from rest_framework.validators import UniqueTogetherValidator
# Vote 모델 시리얼라이저 정의
class VoteSerializer(serializers.ModelSerializer):
# 특정 질문에만 가능한 Choice인지 판단하는 메서드
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']
# valid한지 체크하는 validator 정의. is_valid를 정의하는 것과 유사(?)
# 고유한 question과 voter인지 확인. (중복 투표 방어)
validotrs = [
UniqueTogetherValidator(
queryset=Vote.objects.all(),
fields = ['question', 'voter']
)
]
from polls.models import *
from polls_api.serializers import *
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from django.contrib.auth.models import User
from .permissions import *
# Vote 뷰 정의
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)
# 조상 클래스를 보면 perform_create 이전에 is_valid 메서드가 호출됨.
# 따라서, create 메서드를 오버라이딩 함.
def create(self, request, *args, **kwargs):
new_data = request.data.copy()
new_data['voter'] = request.user.id
serializer = self.get_serializer(data=request.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]
# Update 시 해당 유저의 계정에 대해서만 가능하도록 정의.
def perform_update(self, serializer):
serializer.save(voter=self.request.user)
여러 기능들을 수정하면 자동으로 Testing할 수 있도록 자동화된 testing을 만들어 봅니다.
from django.test import TestCase
from polls_api.serializers import QuestionSerializer
# Create your tests here.
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)
# test_ 로 시작되어야만 테스트 메서드로 인식함.
# def some_method(self):
# print("This is some method")
(django) django\mysite>python manage.py test
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
# 여기서 ..이 나오는데, 2 개의 test를 통과하였다는 의미입니다.
# 만약 실패가 된다면, .이 F 로 출력이 됩니다.
..
----------------------------------------------------------------------
Ran 2 tests in 0.002s
OK
Destroying test database for alias 'default'...
Vote시리얼라이저를 테스트하는 방법은 다음과 같습니다.
from django.test import TestCase
from polls_api.serializers import *
from django.contrib.auth.models import User
from polls.models import *
class VoteSerilaizerTest(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()) # ==True?
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()) # ==False?
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()) # ==False?
(django) django\mysite>python manage.py test
Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...F.
======================================================================
FAIL: test_vote_serializer_with_duplicate_vote (polls_api.tests.VoteSerilaizerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\lb948\Desktop\Programmers\django\mysite\polls_api\tests.py", line 47, in test_vote_serializer_with_duplicate_vote
self.assertFalse(serializer.is_valid()) # ==False?
AssertionError: True is not false
----------------------------------------------------------------------
Ran 5 tests in 0.011s
FAILED (failures=1)
Destroying test database for alias 'default'...
원래는 .....으로 출력이 되는 것이 맞으나, 어떠한 원인으로 인해 중복 테스트에서 에러가 발생했습니다. 아직까지 에러를 해결하지 못하여서 계속해서 자료를 찾아 보는 중입니다.
이번에는 View를 테스트하는 방법을 알아 봅니다.
from django.test import TestCase
from polls_api.serializers import *
from django.contrib.auth.models import User
from polls.models import *
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('question-list')
def test_create_qeustion(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_questions(self):
question = Question.objects.create(question_text = 'Question1')
choice = Choice.objects.create(question=question, choice_text='Choice1')
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 )
class VoteSerilaizerTest(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()) # ==True?
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()) # ==False?
def test_vote_serilaizer_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': self.choice.id,
'voter': self.user.id,
}
serializer = VoteSerializer(data=data)
self.assertTrue(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)
# test_ 로 시작되어야만 테스트 메서드로 인식함.
# def some_method(self):
# print("This is some method")
터미널에서 coverage 라이브러리를 인스톨합니다.
pip install coverage
터미널에서 결과를 확인합니다.
(django) C:\Users\lb948\Desktop\Programmers\django\mysite>coverage run manage.py test
Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......F.
======================================================================
FAIL: test_vote_serializer_with_duplicate_vote (polls_api.tests.VoteSerilaizerTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\lb948\Desktop\Programmers\django\mysite\polls_api\tests.py", line 79, in test_vote_serializer_with_duplicate_vote
self.assertFalse(serializer.is_valid()) # ==False?
AssertionError: True is not false
----------------------------------------------------------------------
Ran 8 tests in 0.051s
FAILED (failures=1)
Destroying test database for alias 'default'...
(django) C:\Users\lb948\Desktop\Programmers\django\mysite>coverage report
Name Stmts Miss Cover
----------------------------------------------------------------------------------------------
manage.py 12 2 83%
mysite\__init__.py 0 0 100%
mysite\settings.py 21 0 100%
mysite\urls.py 3 0 100%
polls\__init__.py 0 0 100%
polls\admin.py 13 0 100%
polls\apps.py 4 0 100%
polls\migrations\0001_initial.py 6 0 100%
polls\migrations\0002_question_owner_alter_question_pub_date_and_more.py 6 0 100%
polls\migrations\0003_alter_choice_question_vote_and_more.py 6 0 100%
polls\migrations\__init__.py 0 0 100%
polls\models.py 25 4 84%
polls\tests.py 1 0 100%
polls\urls.py 5 0 100%
polls\views.py 31 15 52%
polls_api\__init__.py 0 0 100%
polls_api\migrations\__init__.py 0 0 100%
polls_api\permissions.py 9 4 56%
polls_api\serializers.py 47 8 83%
polls_api\tests.py 66 0 100%
polls_api\urls.py 3 0 100%
polls_api\views.py 43 9 79%
----------------------------------------------------------------------------------------------
TOTAL 301 42 86%