일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- dag 작성
- ETL
- snowflake
- Serializer
- 데이터마트
- SQL
- Django Rest Framework(DRF)
- Kafka
- truncate
- ELT
- selenium
- AWS
- docker hub
- 알고리즘
- airflow.cfg
- 데이터 웨어하우스
- airflow
- docker-compose
- 웹 스크래핑
- 웹 크롤링
- docker
- Hive
- 컨테이너 삭제
- dag
- spark
- 데이터레이크
- redshift
- 데이터파이프라인
- Django
- yarn
- Today
- Total
개발 기록장
05. Python Django 프레임웍을 사용해서 API 서버 만들기(5) 본문
학습 주제: Django Rest Framework(DRF), RelatedField, 투표(Vote) 기능 구현, Validation, Testing
RelatedField
: 관계 필드
: 유저와 질문간의 관계
- polls_api/serializers.py
- UserSerializer 수정
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']
class UserSerializer(serializers.ModelSerializer):
#questions = serializers.PrimaryKeyRelatedField(many=True, queryset=Question.objects.all())
#questions = serializers.StringRelatedField(many=True, read_only=True)
#questions = serializers.SlugRelatedField(many=True, read_only=True, slug_field='pub_date')
questions = serializers.HyperlinkedRelatedField(many=True, read_only=True, view_name='question-detail')
class Meta:
model = User
fields = ['id', 'username', 'questions']
- PrimaryKeyRelatedField
- 유저 리스트에서 유저가 작성한 question이 id 값으로 나타남
- StringRelatedField
- 유저 리스트에서 유저가 작성한 question이 제목으로 나타남
- SlugRelatedField
- 유저 리스트에서 유저가 작성한 question이 날짜/시간으로 나타남
- HyperlinkedRelatedField
- 유저 리스트에서 유저가 작성한 question이 Hyper링크로 연결됨
- polls_api/urls.py
- 회원 기능 어디에/어떻게 사용할 것인가 정의
- polls_api/serializers.py
- 회원 리스트: ex)http://127.0.0.1:8000/rest/users/
- 회원 상세(detail) 페이지: ex)http://127.0.0.1:8000/rest/users/1
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(), name = 'signup'),
path('register/',RegisterUser.as_view()),
path('api-auth/', include('rest_framework.urls')),
]
- polls/models.py
- question/related_name ='choices': Choice 불러올때 이름을 choices로 불러오도록 함.
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}'
Vote
: 질문에 대한 선택지 투표 기능
- Models
- django.contrib.auth에서 지원하는 UserCreation폼을 사용하여 회원가입
- polls/models.py
- UniqueConstraint: User가 하나의 질문에 한번씩만 투표할 수 있도록 함.
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)
#하나의 질문에 User가 한번 투표할 수 있도록(ex. 1번 질문에 1번유저 1번만 투표가능)
class Meta:
constraints = [
models.UniqueConstraint(fields=['question','voter'], name = 'unique_voter_for_questions')
]
- polls_api/serializers.py
- votes_count: 투표 결과 count 값 표시
class ChoiceSerializer(serializers.ModelSerializer):
votes_count = serializers.SerializerMethodField() #count 표시
class Meta:
model = Choice
fields = ['choice_text','votes_count']
def get_votes_count(self,obj):
return obj.vote_set.count()
- Django Shell
- 유저당 1질문 1투표 확인
>>> from polls.models import *
>>> question = Question.objects.first()
>>> choice = question.choices.first()
>>> 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)>
>>> question.id
1
- Serializers & Views
- Rest API로 회원가입
- polls_api/serializers.py
- 패스워드 재확인하고 회원가입
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
- class VoteList(): 로그인된 사용자의 투표 리스트
- class VoteDetail(): 로그인된 사용자의 투표 상세 내용 Read, Update, Delete
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]
#내가 작성한 vote만 보여줌
def get_queryset(self, *args, **kwargs):
return Vote.objects.filter(voter=self.request.user)
#투표한 사람(로그인된) voter로 저장
def perform_create(self, serializer):
serializer.save(voter=self.request.user)
class VoteDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Vote.objects.all()
serializer_class = VoteSerializer
#상세정보 Read, Update, Delete 내가 작성한 Vote만 가능
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsVoter]
- polls_api/permission.py
- 새로운 권한 생성
- IsVoterOrReadOnly: voter 만이 접근할 수 있는 권한
class IsVoter(permissions.BasePermission):
def has_object_permission(self, request,view, obj):
return obj.voter == request.user
- polls_api/urls.py
- 투표 리스트: http://127.0.0.1:8000/rest/vote/
- 투표 상세페이지: http://127.0.0.1:8000/rest/vote/1
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(), name = 'signup'),
path('register/',RegisterUser.as_view()),
path('api-auth/', include('rest_framework.urls')),
path('vote/',VoteList.as_view()), #추가
path('vote/<int:pk>/',VoteDetail.as_view()), #추가
]
Validation
: 검증
- polls_api/serializers.py
- 사용자가 투표시 질문과 일치하지 않는 조합의 답변 선택 가능 오류 수정
- UniqueTohetherValidator: question, voter 두 필드에서 서로 값이 유일하게 존재하는가 판단
- 투표 값 생성시, Question에 일치하지(존재X) 않는 값이면 생성 불가
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
- 1질문 1투표 가능 이때, 나오는 에러 400 Bad Request "voter": ["This field is required"] 에러 수정
->400 Bad Request "non_field_errors": "[The fields quetion, voter must make a unique set"]
- serializer가 is_valid()하기 전에 voter가 존재하는지 확인하는 절차 수행
from rest_framework import status
from rest_framework.response import Response
class VoteList(generics.ListCreateAPIView):
serializer_class = VoteSerializer
permission_classes = [permissions.IsAuthenticated] #IsAuthenticated 로그인 X일 땐 못 봄
#내가 작성한 vote만 보여줌
def get_queryset(self, *args, **kwargs):
return Vote.objects.filter(voter =self.request.user)
#투표한 사람(로그인된) 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하기 전에 voter가 있는지 확인
serializer.is_valid(raise_exception=True)
self.perform_create(serializer) #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
#상세정보 Read, Update, Delete 내가 작성한 Vote만 가능
permission_classes = [permissions.IsAuthenticated, IsVoter]
#수정 시: 투표자 로그인된 사람으로 지정
def perform_update(self, serializer):
serializer.save(vote
Testing
: Unit Test
: 작성한 메소드/ 클래스들을 테스트 코드를 작성하여 테스트 함.
: polls_api/tests.py
- 테스트 실행하기
- tests.py 전체 모듈 테스트
python manage.py test
- tests.py에서 특정 모듈만 테스트
python manage.py test polls_api.tests.QuestionListTest
- Python Code Coverage 라이브러리
- 테스트 코드 커버리지 확인
- 테스트 케이스가 요구사항에 충족된 정도를 나타내는 수치
- 코드 커버리지 라이브러리 설치
pip install coverage
- 코드 커버리지 라이브러리 실행
coverage run manage.py test
- 코드 커버리지 라이브러리 실행결과 확인
coverage report
- ex) polls_api/serializers.py에 대한 커버리지는 85% 충족됨
- 커버리지 100%이 가장 이상적인 상태
- Basic of Testing
- 테스트 시 생성된 데이터는 DB에 저장되지 않고, 테스트 종료 시 삭제된다.
- 테스트 케이스를 나타내는 함수명은 def test_***의 형태로 작성되어야 TestCase로 인지하고 python이 실행
- Method
- def setUp(): 클래스 내의 test case 함수 실행시 먼저 실행되는 설정 값이다.(해당 test 함수 종료시 내용 삭제됨)
- assertEqual(A, B): A객체와 B객체의 값이 같은지 확인(같지 않으면 에러 발생)
- assertTrue(A): A객체가 False이면 에러 발생(해당 Case가 True이어야 함)
- assertFalse(A): A객체가 True이면 에러 발생(해당 Case가 False이어야 함)
- assertLess(A, B): A가 B보다 작다면 True
- assertIsNotNone(A): A가 None이 아니라면 True
- Testing Serializers
- Serializer와 관련한 Testing
- Serializer test는 Class인자 TestCase
- class QuestionSerializerTestCase(TestCase)
- 질문 생성시 데이터 유효성 검증 Serializer Test
- test_with_valid_data(): 유효한 데이터가 존재할 때 질문 생성
- test_with_invalid_data(): 유효한 데이터가 존재하지 않을 때 질문 생성
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)
- class VoteSerializerTest(TestCase)
- 투표 Serializer Test
- test_vote_serializer(): 정상적인 투표
- test_vote_serializer_with_duplicate_vote(): 같은 질문에 중복 투표
- test_vote_serializer_with_unmatched_question(): 질문에 맞지 않는 답변 선택
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)
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.assertFalse(serializer.is_valid()) #해당 Case가 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())
- Testing Views
- Views와 관련한 Testing
- View test는 Class인자 APITestCase(해당 클래스는 generics.ListCreateAPIView를 상속받았기 때문)
- class VoteList(APITestCase)
- 로그인(인증)관련 질문 생성 Serializer Test
- test_create_question(): 로그인된 유저에 의한 질문 생성
- test_create_question_without_authentication(): 로그인되지 않은 상태로 질문 생성
- test_list_question(): 질문 목록 받아오기
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_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_data).total_seconds(),1) #앞의 인자가 뒤의 인자보다 작다면 True
#로그인되지 않은 상태로 질문 만들기 -> Error 발생(정상)
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 = '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)
공부하며 느낀점
지금까지 개발할 때 모든 것을 직접 해보면서 Unit Test를 했는데, 오늘 학습을 통해 Test 코드의 작성 방법과 필요성을 알게 되어 다음부터는 꼭 Test 코드를 작성하면서 개발을 진행해야겠다고 생각했다.
'데브코스(DE) > 장고 활용한 API 서버 제작' 카테고리의 다른 글
04. Python Django 프레임웍을 사용해서 API 서버 만들기(4) (0) | 2024.04.11 |
---|---|
03. Python Django 프레임웍을 사용해서 API 서버 만들기(3) (1) | 2024.04.10 |
02. Python Django 프레임웍을 사용해서 API 서버 만들기(2) (0) | 2024.04.09 |
01. Python Django 프레임웍을 사용해서 API 서버 만들기(1) (0) | 2024.04.08 |