23.04.26 TIL - DRF 개인과제: model, view, serializer,url
오늘은...
페어프로그래밍 후 선발대 강의 정리를 하고 개인과제를 진행하였다.
개인과제에서 구현해야하는 사항은 DRF todo list CRUD를 구현하는 것, 사용자 회원가입/인증/로그아웃/탈퇴를 구현하는 것이다.
사용자의 인증은 조금 나중에 토큰을 이용해 구현해보기로 하고, 먼저 todo list 부터 구현해 보기로 했다.
1. todolists/models.py
import time
class TodoList(models.Model):
class Meta:
db_table = "todolist_table"
title = models.CharField(max_length=50)
is_complete = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
completion_at = models.DateTimeField(null=True, blank=True)
# user_id = models.ForeignKey()
def save(self, *args, **kwargs):
if self.is_complete:
self.completion_at = time.time()
else:
self.completion_at = None
super().save(*args, **kwargs)
todo list 내용(title): charField로 50자 이하의 줄바꿈 없는 문자열을 받는다.
완료 여부(is_complete): booleanField로 기본값을 False로 저장한다.
달성 일자(complation_at): null이면 미달성, Datetime값이 들어가면 달성인 것이다.
save() 메서드를 오버라이딩 하여 만약 is_complete가 참이면 conpletion_at을 현재 시간으로 갱신하고 거짓이면 None값을 넣게 했다.
오류 발생: fromisofromat
나중에 검증을 하던 중 model에서 오류가 발생하였다.
정확히는 super.save()가 실행 될 때 오류가 발생하였다.
시도: super().save() 위쪽으로 올리기
이전에 다른 개인과제에서 save()함수를 오버라이딩 할 때는 super().save()가 추가 작업들을 하기 전에 실행되었기에 이와 달라 문제가 생긴것인가 싶어 다음과 같이 수정해 보았다.
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.is_complete:
self.completion_at = timezone.now()
else:
self.completion_at = None
super().save(*args, **kwargs)
그러나 문제가 해결되지는 않았다.
해결: timezone.now()
비슷한 오류를 겪은 사례를 찾아보니, DateTimeField이 문제였다. DateTimeField는 문자열 형태의 날짜/시간을 받아 fromisoformat()을 통해 날짜/시간 객체를 만들어내는데, time.time은 부동소수점(float)형의 값을 반환한다. 따라서 자료형이 맞지 않아 오류가 발생한 것이다. 이를 해소하기위해서는 django에서 제공하는 timezone을 쓰면된다.
from django.utils import timezone
class TodoList(models.Model):
class Meta:
db_table = "todolist_table"
title = models.CharField(max_length=50)
is_completed = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
completion_at = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kwargs):
if self.is_completed:
self.completion_at = timezone.now()
else:
self.completion_at = None
super().save(*args, **kwargs)
2. serializers.py
class TodoListSerializer(serializers.ModelSerializer):
class Meta:
model = TodoList
fields = "__all__"
class TodoListCreateSerializer(serializers.ModelSerializer):
class Meta:
model = TodoList
fields = [
"title",
]
todo list의 모든 정보를 조회하기 위한 TodoListSerializer와, 사용자가 todolist를 생성할때 입력할 필요가 있는 정보(title)만 시리얼라이저 필드로 요구하는 TodoListCreatSerializer를 모델 시리얼라이저를 상속하여 생성하였다.
3. views.py
CBV로 구현하기로 하였다.
class TodoListView(APIView):
def get(self, request):
todolist = TodoList.objects.all()
serializer = TodoListSerializer(todolist, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@swagger_auto_schema(request_body=TodoListCreateSerializer)
def post(self, request):
serializer = TodoListCreateSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
먼저 CRUD 중 기존에 존재하는 데이터중 특정 데이터만 조회할 필요가 없는 CR부터 뷰를 구현하였다.
class TodoSpecificView(APIView):
@swagger_auto_schema(request_body=TodoListCreateSerializer)
def put(self, request, todo_id):
todo = get_object_or_404(TodoList, id=todo_id)
serializer = TodoListCreateSerializer(todo, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.data, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, todo_id):
todo = get_object_or_404(TodoList, id=todo_id)
todo.delete()
return Response(status=status.HTTP_200_OK)
이후 기존 존재하는 데이터중 특정 데이터만 조회해야하는 UD를 위한 뷰를 따로 구현하였다.
class TodoCompleteView(APIView):
def put(self, request, todo_id):
print("hello")
todo = get_object_or_404(TodoList, id=todo_id)
print(todo)
print(todo.is_complete)
print(type(todo.is_complete))
if todo.is_complete:
todo.is_complete = False
else:
todo.is_complete = True
todo.save()
serializer = TodoListSerializer(todo)
return Response(serializer.data, status=status.HTTP_200_OK)
마지막으로, 요청을 보낼 때 마다 완료여부를 토글하는 뷰를 만들었다.
오류발생:
문제상황: 하나의 뷰와 url에서 get/post/put/delete 모두 처리하기 위해 다음과 같이 뷰, urls.py를 작성하였다.
class TodoListView(APIView):
def get(self, request, *args):
...
def post(self, request, *args):
...
def put(self, request, todo_id):
...
def delete(self, request, todo_id):
...
# urls.py
urlpatterns = [
path("<int:todo_id>/", views.TodoListView.as_view(), name="todolist"),
...
]
이는 *args에 url의 path variable로 주어지는 todo_id가 필요없는 get과 post가 그 인자를 치워버리고 정상적으로 동작할 수 있도록 하려고 한 것이었다. 그런데 오류가 발생한 것이다.
해결1: **kwargs
오류를 읽어보면 명확하게 'get()은 todo_id라는 인자를 안받습니다' 라고 알려주고 있다. 매개변수의 갯수가 아닌 명확한 매개변수명을 언급한다는 것은 해당 함수가 호출될 때 todo_id가 keyword arguments로 전달된다는 뜻이다.
def foo(a, b):
print(a + b)
foo(10, 5)
try:
foo(19, 5, 5)
except Exception as e:
print(e) # foo() takes 2 positional arguments but 3 were given
try:
foo(19, 5, c=5)
except Exception as e:
print(e) # foo() got an unexpected keyword argument 'c'
따라서 다음과 같이 수정하면 된다.
class TodoListView(APIView): ## 후
def get(self, request, **kwargs):
...
def post(self, request, **kwargs):
...
해결 2: 그냥 뷰 분리하기
그러나 이렇게하면 host/todolist/1/ 과 같이 특정 todolist 객체의 id값을 가지고 접근했을 때 get과 post가 id와 상관없이 동작하므로 일관성이 없어 보일 수 있다.
그래서 결과적으로 위에서 한것과 같이 뷰를 분리하기로 하였다.
배운점
1. django.utils의 timezone을 이용할 수 있다. DateField 등에 값을 넣을 때는 이것을 사용하면 좋다.
2. 에러메시지에 따라 positional arguments가 주어졌는지 keyword arguments가 주어졌는지 알 수 있다.
레퍼런스
Django datetime TypeError: fromisoformat: argument must be str
Internal Server Error: /api/orders/add/ Traceback (most recent call last):**strong t[enter image description here][1]ext** File "C:\Users\kalya\OneDrive\Desktop\my app\myenv\lib\site-packages\
stackoverflow.com