TIL

23.04.26 TIL - DRF 개인과제: model, view, serializer,url

best_spear_man 2023. 4. 27. 03:52

오늘은...

페어프로그래밍 후 선발대 강의 정리를 하고 개인과제를 진행하였다.

개인과제에서 구현해야하는 사항은 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가 주어졌는지 알 수 있다.

 

레퍼런스

https://stackoverflow.com/questions/72627164/django-datetime-typeerror-fromisoformat-argument-must-be-str

 

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