[DEV] 4주차. 장고 활용한 API서버 만들기(5)
1. RelatedField
polls_api/serializers.py
UserSerializer
수정
1) StringRelatedField
- model의 str 값으로 표현
class UserSerializer(serializers.ModelSerializer):
questions = serializers.StringRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ['id', 'username', 'questions']
2) SlugRelatedField
- 지정한 모델의 필드로 표현
class UserSerializer(serializers.ModelSerializer):
questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
class Meta:
model = User
fields = ['id', 'username', 'questions']
3) HyperlinkedRelatedField
- 지정한 뷰로 이동하는 하이퍼링크로 표현
class UserSerializer(serializers.ModelSerializer):
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail)
class Meta:
model = User
fields = ['id', 'username', 'questions']
4) question_detail에서 choice의 필드들도 함께 표시하기
polls_api/serializers.py
ChoiceSerializer
생성
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']
polls/models.py
’Choice
의 related_name 지정
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 f'[{self.question.question_text}] {self.choice_text}'
2. Vote 기능 구현하기 - Models
- 로그인한 사용자만 투표할 수 있도록
polls/models.py
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/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에서 실행
- migration
python manage.py makemigrations
python manage.py migrate
- shell
from polls.models import *
question = Question.objects.first()
question
#<Question: 제목: 휴가를 어디서 보내고 싶으세요?, 날짜: 2023-11-01 07:37:20+00:00>
choice = question.choices.first()
choice
# <Choice: [휴가를 어디서 보내고 싶으세요?] 바다>
from django.contrib.auth.models import User
user = User.objects.get(username='user1')
user
# <User: user1>
Vote.objects.create(voter=user, question=question, choice=choice)
#<Vote: Vote object (1)>
Vote.objects.first()
# <Vote: Vote object (1)>
3. Vote 기능 구현하기 - Serializers & Views
polls_api/serializers.py
class VoteSerializer(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='voter.username')
class Meta:
model = Vote
fields = ['id', 'question', 'choice', 'voter']
polls_api/views.py
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated] # 로그인 안하면 조회 X
def get_queryset(self, *args, **kwargs):
return Vote.objects.filter(voter=self.request.user)
class VoteDetail(generics.ListCreateAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated, IsVoter]
polls_api/permissions.py
class IsVoter(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.voter == request.user
polls_api/usrls.py
path('vote/', VoteList.as_view()),
path('vote/<int:pk>/', VoteDetail.as_view()),
추가!
4. Validation
- 중복 투표 방지, 다른 질문에 대한 choice 선택 방지
polls_api/serilalizers.py
class VoteSerializer(serializers.ModelSerializer):
def validate(self, attrs):
if attrs['choice'].quesiton.id != attrs['question'].id:
raise serializers.ValidationError("Questions과 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
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated] # 로그인 안하면 조회 X
def get_queryset(self, *args, **kwargs):
return Vote.objects.filter(voter=self.request.user)
# generics.ListCreateAPIView - mixins.CreateModelMixin - create 메소드 오버라이드
## is_valid 전에 voter를 넣어주어야 함
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.ListCreateAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated, IsVoter]
def perform_update(self, serializer):
serializer.save(voter=self.request.user)
5. Serializer Testing
- 자동으로 테스트를 진행하도록!
example1
polls_api/tests.py
from django.test import TestCase
class QuestionSerializerTestCase(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")
- django shell
python manage.py test
결과
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
This is test a
.This is test b
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Destroying test database for alias 'default'...
some_method
는 시작하지 않음- TestCase 클래스에서
test_
로 시작하는 메소드만 자동으로 test라고 판단하여 실행하기 때문
- TestCase 클래스에서
.
- test가 별 문제없이 실행되었다는 뜻
example2
polls_api/tests.py
from django.test import TestCase
class QuestionSerializerTestCase(TestCase):
def test_a(self):
self.assertEqual(1, 2)
def test_b(self):
print("This is test b")
- django shell
python manage.py test
결과
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
FThis is test b
.
======================================================================
FAIL: test_a (polls_api.tests.QuestionSerializerTestCase.test_a)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/bokyung/django-projects/mysite/polls_api/tests.py", line 5, in test_a
self.assertEqual(1, 2)
AssertionError: 1 != 2
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
QuestionSerializer
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)
- django shell
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.001s
OK
Destroying test database for alias 'default'...
VoteSerializer
정상적 경우
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):
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)
결과
Found 3 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.004s
OK
Destroying test database for alias 'default'...
비정상적 경우
1) 같은 유저가 같은 질문에 대해 다시 투표하는 경우
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())
결과
Found 4 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 4 tests in 0.006s
OK
Destroying test database for alias 'default'...
- VoteSerializer의
UniqueTogetherValidator
기 잘 동작함을 알 수 있음!
2) 조합이 맞지 않는 question과 choice 요청
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):
choice = Choice.objects.create(
question = self.question,
choice_text='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' : 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())
결과
Found 5 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.008s
OK
Destroying test database for alias 'default'...
- VoteSerializer의
validate
메소드가 잘 실행됨을 알 수 있음
serUp 메소드
- 각 테스트가 실행되기 전 한 번 실행되는 메소드
- 테스트마다 독립적!
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'
)
print("--setUp 실행--")
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):
choice = Choice.objects.create(
question = self.question,
choice_text='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' : choice1.id,
'voter' : self.user.id,
}
serializer = VoteSerializer(data=data)
self.assertFalse(serializer.is_valid())
결과
Found 4 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..--setUp 실행--
.--setUp 실행--
.
----------------------------------------------------------------------
Ran 4 tests in 0.006s
OK
Destroying test database for alias 'default'...
6. View Testing
정상
from rest_framework.test import APITestCase
from django.urls import reverse # 메소드 안에서는 reverse_lazy 대신 reverse
from rest_framework import status
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) # 사용자 강제로 로그인! (APITestCase 쓴 이유)
response = self.client.post(self.url, self.question_data) # url로 data가 POST로 날아감
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)
python manage.py test polls_api.tests.QuestionListTest
결과
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.008s
OK
Destroying test database for alias 'default'...
비정상
- 로그인하지 않고 요청
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) # 사용자 강제로 로그인! (APITestCase 쓴 이유)
response = self.client.post(self.url, self.question_data) # url로 data가 POST로 날아감
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.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)
print(f'$$$$$$${response.data}\n')
결과
Found 3 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..$$$$$$$[OrderedDict([('id', 1), ('question_text', 'Question1'), ('pub_date', '2023-11-04T22:03:05.149758Z'), ('choices', [OrderedDict([('choice_text', 'choice1'), ('votes_count', 0)])])]), OrderedDict([('id', 2), ('question_text', 'Question2'), ('pub_date', '2023-11-04T22:03:05.150064Z'), ('choices', [])])]
.
----------------------------------------------------------------------
Ran 3 tests in 0.012s
OK
Destroying test database for alias 'default'...
질문 리스트
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) # 사용자 강제로 로그인! (APITestCase 쓴 이유)
response = self.client.post(self.url, self.question_data) # url로 data가 POST로 날아감
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)
결과
Found 3 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.012s
OK
Destroying test database for alias 'default'...
7. converage
- test가 잘 진행되고 있는지 확인
pip install coverage
coverage run manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.032s
OK
Destroying test database for alias 'default'...
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 29 6 79%
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/admin.py 1 0 100%
polls_api/apps.py 4 0 100%
polls_api/migrations/__init__.py 0 0 100%
polls_api/models.py 1 0 100%
polls_api/permissions.py 9 4 56%
polls_api/serializers.py 48 7 85%
polls_api/tests.py 67 0 100%
polls_api/urls.py 3 0 100%
polls_api/views.py 43 9 79%
----------------------------------------------------------------------------------------------
TOTAL 313 43 86%