금일 학습 내용은 RelatedField, Model 구현, Validation, Testing 과정이다.
음, 갈수록 사이트 내 필요요소들을 학습하고 실제로 배포하는 과정도 학습!
주말에 무조건 복습을 해야겠다는 생각이 든다.
복습하자..✈️
qustion과 user사이 관계를 정의하는 Field에 대해 알아보자.
# api/serializers.py
## RelatedField
from django.contrib.auth.models import User
# choice 모델 내 필드 표시 위해 생성
class ChoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Choice
fields = ['choice_text', 'votes']
# choiceserializer에서 정의한 내용이 questionserializer에도 적용되도록 함
class QuestionSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
# app/models.py가서 choice 모델에서 question에다가 related_name='choices' 해주어야 함.
choices = ChoiceSerializer(many=True, read_only=True)
class Meta:
model = Question
fields = ['id', 'question_text', 'pub_date', 'owner', 'choices']
class UserSerializer(serializers.ModelSerializer):
# 기존 (id 출력 됌)
questions = serializers.PrimaryKeyRelatedField(many = True, queryset = Question.objects.all())
# Question 모델의 str 출력
questions = serializers.StringRelatedField(many=True, read_only=True)
# 특정 field 지정
questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
# question 내용 링크 제공
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
class Meta:
model = User
fields = ['id', 'username', 'questions']
지금까지 학습한 내용을 기반으로 로그인한 사용자에 한해 투표가 가능토록 기능 구현!
현재 models.py에는 selected_choice.votes = F('votes') + 1로 인해 누구든 여러번 투표가 가능한 상황. -> 사용자 단위로 투표 가능토록 기능 구현 必
# app/models.py에서 처리 필요! - django shell에서 vote 구현
## 사용자 단위로 투표 가능토록 기능 구현
from django.contrib.auth.models import User
class Vote(models.Model):
qustion = 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 = ['qustion', 'choice', 'voter'], name = 'unique_voter_for_questions')
]
# app_api/serializers.py - django shell
## 사용자 단위로 VOTE 기능 구현하기
class ChoiceSerializer(serializers.ModelSerializer):
# 투표 횟수 표기해주기
votes_count = serializers.SerializerMethodField()
class Meta:
model = Choice
fields = ['choice_text', 'votes_count']
def get_votes_count(self, obj):
# Foreignkey로 관계 정의될 때 별도의 related_name없으면 _ 언더바로 갖고오기 가능
return obj.votes_set.count()
# api_serializers.py
## 여기서 직접 vote 생성하기
from polls.models import Question, Choice, Vote
class VoteSerializer(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Vote
fields = ['id', 'question', 'choice', 'voter']
# api/views.py
## Vote 기능 구현
from polls.models import Question, Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly, IsVoter
from rest_framework import generics,permissions
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated]
# 자신의 투표만 볼 수 있도록 쿼리셋 정의 필요
def get_queryset(self):
return Vote.objects.filter(voter = self.request.user)
def perform_update(self, serializer):
serializer.save(voter = self.request.user)
# 수정도 필요하므로 생성
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated, IsVoter]
# api/permissions.py
# IsVoter 구현위해 적용
class IsVoterOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.voter == request.user
# api/urls.py
# vote기능 구현에 대해 처리 필요
path('vote/', VoteList.as_view()),
path('vote/<int:pk>/', VoteDetail.as_view()),
앞서 만든 Vote 기능 Error 발생 동일 유저가 vote 다시 진행 시 500 error 발생
-> Validation 작업 실행 필요!
# api/serializers.py
## 동일인물 재투표 시 500 error OR question에 속하지 않은 choice 선택 시 에러 발생 필요
from polls.models import Question, Choice, Vote
class VoteSerializer(serializers.ModelSerializer):
# choice와 question의 조합이 옳은지 판단
def validate(self, attrs):
if attrs['choice'].question.id != attrs['question'].id:
raise serializers.ValidationError('해당 question과 choice의 id가 일치하지 않습니다.')
return attrs
class Meta:
model = Vote
fields = ['id', 'question', 'choice', 'voter']
validators = [
# 한 사람이 하나의 질문에 대해 vote를 1개만 생성 가능
UniqueTogetherValidator(
queryset = Vote.objects.all(),
fields = ['question', 'choice'],
)
]
# api/views.py
## 동일인물 재투표 시 500 error OR question에 속하지 않은 choice 선택 시 에러 발생 필요
# 화면 표기해주기
from polls.models import Question, Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly, IsVoter
from rest_framework import generics,permissions
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated]
def 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)
# perform_create 이전 유효성검사 처리하기
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)
그동안 사이트에 들어가 일일이 검색하고 클릭하며 검증을 하고 테스팅을 했는데, 이는 너무 비효율적이다. 따라서 컴퓨터가 알아서 테스트를 하는 자동화된 작업을 구현해보고자 한다.
❗ 코드가 구현하고 있는 바를 최대한 표현할 수 있도록 상세히 적는 것이 핵심!
- cmd에서 실행하는 방법 : python manage.py test
app_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': 'taejun')
# assertEqual(a, b) a==b면 유효성 검사 통사한 것 이후 DB에 save
self.assertEqual(serializer.is_valid(), True)
new_question = serializer.save()
# save한 new_question에 대해 id None 여부 확인
self.assertIsNotNone(new_question.id)
# 질문지가 빈칸인 경우 False인 경우 처리
def test_with_invalid_data(self):
serializer = QuestionSerializer(data = {'question_text' : ' '})
self.assertEqual(serializer.is_valid(), False)
< vote serializer testing 하기 >
## vote seritalizer testing
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):
# user, question, choice setup 함수로 처리
# test마다 독립적으로 처리
def setup(self):
self.user = User.objects.create(username = 'testuser')
self.question = Question.objects.create(
question_text = 'test question',
owner = self.user,
)
self.choice = Choice.objects.create(
question = self.question,
choice_text = 'test choice',
)
# 투표 행위에 대해 들어오는 데이터를 비교
def test_vote_serializer(self):
# setup 실행 결과이후 아래 duplicate함수와 중복 여부 확인
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)
# 투표에 대해 동일 user가 투표하는 경우
def test_vote_serializer_with_duplicate_vote(self):
# setup 실행 결과이후 위 test_vote_serializer함수와 중복 여부 확인
self.asserEqual(User.objects.all().count, 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' : self.choice.id,
'voter' : self.user.id
}
serializer = VoteSerializer(data = data)
self.assertTrue(serializer.is_valid())
# question, choice 불일치 하는 경우
def test_vote_serializer_with_unmatched_question_and_choice(self):
question2 = Question.objects.create(
question_text = 'test question',
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())
< view testing >
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': 'test question'}
# question-list에 해당하는 url 써야함,
# reverse는 reverse_lazy와 같은 역할. 메서드 내에서는 reverse 사용
self.url = reverse('question_list')
# question 생성
def text_create_question(self):
user = User.objects.create(username = 'testuser', password = 'testpass')
# APITestcase 쓰는 이유 (사용자 강제 로그인 처리)
self.client.force_authenticate(user = user)
# post 요청 아래 url로 self.question_data 요청이 post로 날라가고 결과를 response에 저장
response = self.client.post(self.url, self.question_data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
# question 잘 만들어졌는지 확인
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)
# 모든 question 목록 불러오기 (사용자 로그인 안해도 됌)
def test_list_question(self):
question = Question.objects.create(question_text = 'Question 1')
choice = Choice.objects.create(question = question, choice_text = 'Choice 1')
Question.objects.create(question_text = 'Question 2')
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)
최종적으로 터미널에서 coverage를 사용해 code를 얼마나 잘 테스트하는 지 판단.
- pip install coverage로 설치
- coverage run manage.py test를 실행
- 이후 coverage report를 통해 각 파이썬 파일 별 Miss와 Cover%를 확인할 수 있다.
이번주 장고 학습을 맛보기로 진행해보았는데, 기존 사용했던 python코드를 기반으로 진행하는 웹 프레임워크여서 아무래도 이해는 수월했다. 하지만 cmd 기반으로 명령을 내리고 django shell 작업도 진행하다보니 적응이 힘들었고, django 역시 마찬가지고 사이트와 번갈아가며 test하고 오류 찾고 하느라 오래걸렸다.
✈️ 복습완료!!