장고(Django) - 사용자(Users)와 인증(Authentication)
RelatedField
UserSerializer를 정의할 때, PrimaryKeyRelatedField를 사용하여 해당 유저의 questions를 id로 가져오도록 작성했었다. 이것 말고도 다양한 RelatedField를 사용해 보는 실습을 진행하다.
StringRelatedField
id로만 표시되어 어떤 question인지 알지 못했기 때문에 StringRelatedField를 사용하여 질문을 표시하여 확인할 수 있다.
class UserSerializer(serializers.ModelSerializer):
questions = serializers.StringRelatedField(many=True, read_only=True)
...
SlugRelatedField
String 말고도 원하는 데이터를 표시할 수도 있다. 아래의 코드는 pub_date를 나타낸 것이다.
class UserSerializer(serializers.ModelSerializer):
questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
...
HyperlinkedRelatedField
HyperlinkedRelatedField는 해당 질문의 정보가 있는 링크로 이동할 수 있게 할 수 있다.
class UserSerializer(serializers.ModelSerializer):
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
...
장고(Django) - 투표(Votes)와 테스팅(Testing)
투표(Votes) 기능 구현하기 1 - Models
로그인한 사용자만 투표가 가능하도록 하는 실습을 진행한다.
polls/models.py
Vote model을 만들어 사용자 별로 한 번만 투표를 할 수 있게 지정한다.
from django.contrib.auth.models import User
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_api/serializers.py
ChoiceSerializer를 투표수를 셀 수 있도록 변경한다.
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()
shell
from polls.models import *
from django.contrib.auth.models import User
question = Question.objects.first()
choice = question.choices.first()
user= User.objects.get(username='luke')
Vote.objects.create(voter=user,question=question,choice=choice)
# <Vote: Vote object (1)>
question.id
# 1
투표(Votes) 기능 구현하기 2 - Serializers & Views
Vote Models를 바탕으로 Serializers와 Views를 작성하여 웹페이지에서 투표를 할 수 있도록 하는 실습을 진행한다.
polls_api/serializers.py
VoteSerializer를 다음과 같이 작성한다. QuestionSerializer를 작성했을 때, owner를 ReadOnlyField로 작성했던 것처럼 VoteSerializer의 voter도 이와 같이 작성한다.
from polls.models import Question,Choice, Vote
class VoteSerializer(serializers.ModelSerializer):
voter = serializers.ReadOnlyField(source='voter.username')
class Meta:
model = Vote
fields = ['id', 'question', 'choice', 'voter']
polls_api/views.py
VoteList와 VoteDetail은 다음과 같이 작성한다. permission_classes에는 권한이 있는 사용자만 접근할 수 있도록 설정하였고, VoteDetail의 경우 IsVoter permission으로 권한이 있는 사용자만 투표할 수 있도록 작성하였다.
from polls.models import Question,Choice, Vote
from polls_api.serializers import VoteSerializer
from .permissions import IsOwnerOrReadOnly , IsVoter
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.IsAuthenticatedOrReadOnly, IsVoter]
polls_api/permissions.py
class IsVoter(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.voter == request.user
polls_api/urls.py
from django.urls import path, include
from .views import VoteList, VoteDetail
urlpatterns = [
...
path('vote/', VoteList.as_view()),
path('vote/<int:pk>/', VoteDetail.as_view()),
]
Validation
만약 투표를 두 번 진행할 경우 한 번만 진행할 수 있도록 설정했기에 에러가 발생한다. 그러나 500 에러가 발생하게 되는데, 이를 4XX로 변경하는 실습을 진행한다. 또한 질문에 다른 Choice를 제출했을 때에도 올바르게 제출이 된다. 이러한 상황을 방어하는 실습도 함께 진행한다.
polls_api/serializers.py
validate 메서드를 오버라이드하여 조합이 맞을 경우에만 투표가 진행되도록 작성하였다.
from rest_framework.validators import UniqueTogetherValidator
class VoteSerializer(serializers.ModelSerializer):
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']
)
]
polls_api/views.py
500 에러가 발생했던 이유는 create 메서드 과정에서 is_valid가 perform_create보다 먼저 실행되어 ReadOnly인 voter를 읽지 못했기 때문이다. 그래서 create 메서드를 오버라이드하여 new_data를 생성하고, 해당 데이터를 넘겨주었다. 추가로 VoteSerializer의 voter 또한 지워서 new_data의 정보가 모두 전달되도록 하였다.
from rest_framework import status
from rest_framework.response import Response
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 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)
Testing
장고 라이브러리를 사용해서 Question이 올바르게 생성되는지 확인하는 TestCase를 생성하는 실습을 진행한다.
polls_api/test.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)
shell
python manage.py test
# result
# Found 2 test(s).
# Creating test database for alias 'default'...
# System check identified no issues (0 silenced).
# ..
# ----------------------------------------------------------------------
# Ran 2 tests in 0.003s
# OK
# Destroying test database for alias 'default'...
Testing Serializers
이번엔 Vote가 올바르게 작동하는지 TestCase를 만드는 실습을 진행한다.
polls_api/tests.py
- setUp : 테스트케이스가 실행되기 전에 먼저 실행됨
- test_vote_serializer : 올바르게 데이터가 들어왔을 경우 테스트
- test_vote_serializer_with_duplicate_vote : 같은 유저가 두 번 투표했을 경우 테스트
- test_vote_serializer_with_unmatched_question_and_choice : 해당 질문에 다른 답을 했을 경우 테스트
class VoteSerializerTest(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):
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)
def test_vote_serializer_with_duplicate_vote(self):
self.assertEqual(User.objects.all().count(), 1)
choice2 = 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.assertFalse(serializer.is_valid())
def test_vote_serializer_with_unmatched_question_and_choice(self):
question2 = Question.objects.create(
question_text = 'edf',
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())
Testing Views
이전 실습에 이어 Views가 올바르게 작동하는지 테스트케이스를 만드는 실습을 진행한다.
polls_api/tests.py
- setUp : 테스트케이스가 실행되기 전에 먼저 실행됨
- test_create_question : 질문이 올바르게 생성되는지 테스트
- test_create_question_without_authentication : 로그인 없이 질문을 생성했을 경우 테스트
- test_list_questions : 목록을 올바르게 불러오는지 테스트
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':"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_questions(self):
question = Question.objects.create(question_text='Question1')
choice = Choice.objects.create(question=question, choice_text='choice')
question2 = 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)
'[프로그래머스] 데이터 엔지니어링 데브코스 3기 > TIL(Today I Learn)' 카테고리의 다른 글
[TIL - 22일 차] 데이터 웨어하우스와 SQL과 데이터 분석 (2) (2) | 2024.04.23 |
---|---|
[TIL - 21일 차] 데이터 웨어하우스와 SQL과 데이터 분석 (1) (0) | 2024.04.22 |
[TIL - 14일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (4) (0) | 2024.04.11 |
[TIL - 13일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (3) (0) | 2024.04.10 |
[TIL - 12일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (2) (0) | 2024.04.09 |