[TIL] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (2)

이원진·2023년 4월 25일
0

데브코스

목록 보기
12/54
post-thumbnail
post-custom-banner

학습 내용


  1. 뷰(Views)와 템플릿(Templates)

  2. 템플릿에서 제어문 사용하기

  3. 상세(Detail) 페이지 만들기

  4. 상세 페이지로 링크 추가하기

  5. 404 에러 처리하기

  6. 폼(Forms)

  7. 에러 방어하기 1

  8. 에러 방어하기 2

  9. 결과(Result) 조회 페이지

  10. Django Admin 편집 페이지 커스터마이징

  11. Django Admin 목록 페이지 커스터마이징

1. 뷰(Views)와 템플릿(Templates)


  • 뷰의 결과를 보기 좋게 출력하기 위해 템플릿 사용

    • views.py

      ...
      from django.shortcuts import render
      from .models import *
      
      def index(request):
      	# 모델에서 데이터를 가져와 템플릿으로 전달할 context 생성
      	latest_question_list = Question.objects.order_by('-pub_date')[:5]
      	context = {"first_question": latest_question_list[0]}
      
      	# 인자로 받은 request와 생성한 context를 사용해 템플릿 호출
      	return render(request, "polls/index.html", context)
          
      ...

    • templates/polls/index.html

      <!--전달받은 context 사용-->
      <ul>
      	<li>{{first_question}}</li>
      </ul>

2. 템플릿에서 제어문 사용하기


  • 템플릿에서 iterable 객체를 인덱싱할 때는 대괄호가 아닌 '.'을 사용

    <ul>
        <li>{{questions.0}}</li>
    </ul>

  • 제어문을 사용할 때는 {% if ... %}, {% for ... %}로 블럭을 열고, {% endif %}, {% endfor %}로 블럭을 닫음

    • templates/polls/index.html

      {% if questions %}
          <ul>
              {% for question in questions %}
              	<li>{{question}}</li>
              {% endfor %}
          </ul>
      
      {% else %}
          <p>no questions</p>
      
      {% endif %}

3. 상세(Detail) 페이지 만들기


  • urls.py

    ...
    
    urlpatterns = [
        ...
    
        # int 타입의 Query String이 들어온 경우, detail 페이지 출력
        path('<int:question_id>/', views.detail, name = "detail"),
        ...
    ]

  • views.py

    ...
    
    def detail(request, question_id):
        question = Question.objects.get(pk = question_id)
    
        return render(request, "polls/detail.html", {"question" : question})
    
    ...

  • templates/polls/index.html

    <h1>{{question.question_text}}</h1>
    
    <ul>
      <!--템플릿에서는 all 메서드에 괄호를 쓰지 않는 것을 유의-->
      {% for choice in question.choice_set.all %}
          <li>{{choice.choice_text}}</li>
      {% endfor %}
    </ul>

4. 상세 페이지로 링크 추가하기


  • URL을 적어주는 방법

    • templates/polls/index.html

      ...
      
      <ul>
          {% for question in questions %}
          	<li><a href = "/polls/{{question.id}}">{{question.question_text}}</a></li>
          {% endfor %}
      </ul>
      
      ...

  • 코드를 사용해 메서드를 호출하는 방법

    • templates/polls/index.html

      ...
      
      <ul>
          {% for question in questions %}
          	<!--questions라는 app_name을 갖는 urls.py에서 detail이라는 이름의 메서드를 question.id를 인자로 사용해 호출-->
          	<li><a href = "{% url 'questions:detail' question.id %}">{{question.question_text}}</a></li>
          {% endfor %}
      </ul>

    • urls.py

      ...
      # 템플릿에서 urls.py의 메서드를 호출할 때 사용할 app_name
      app_name = "questions"
      
      urlpatterns = [
          ...
              path('<int:question_id>/', views.detail, name = "detail"),
      ]

    • URL이 복잡해질수록 이와 같은 방식이 편함


5. 404 에러 처리하기


  • 404: 요청한 URL의 웹페이지가 존재하지 않는 경우 발생하는 에러

  • 400: 서버에 문제가 있어서 발생하는 에러

  • Obj.object.get()은 요청한 객체가 없는 경우 Obj.DoesNotExist 에러 반환

    • 이 때문에 브라우저가 존재하지 않는 URL을 요청해도 404가 아닌 500에러를 반환
      -> 이를 404로 변경하기 위해 views.py의 detail() 메서드 수정

  • 직접 반환

    ...
    from django.http import Http404
    ...
    
    def detail(request, question_id):
        try:
            question = Question.objects.get(pk = question_id)
    
        except Question.DoesNotExist:
            raise Http404("Question does not exist")
    
        return render(request, "polls/detail.html", {"question" : question})
    
    ...

  • shortcut 사용해 간단하게 반환

    ...
    from django.shortcuts import render, get_object_or_404
    ...
    
    def detail(request, question_id):
        # 찾는 객체가 있으면 객체를 찾아오고, 없으면 404에러 반환
        question = get_object_or_404(Question, pk = question_id)
    
        return render(request, "polls/detail.html", {"question" : question})
    
    ...

6. 폼(Forms)


  • <form>을 사용해 사용자가 choice를 투표하도록 구현

  • CSRF(Cross-Site Request Forgery) 토큰

    • 서버에 들어온 요청이 정상적인 요청인지를 확인하는 토큰

    • 사용할 <form>{% csrf_token %}과 같이 추가해 405에러 방지

  • urls.py

    ...
    
    urlpatterns = [
        ...
        path('<int:question_id>/vote/', views.vote, name = "vote"),
    ]

  • views.py

    ...
    from django.urls import reverse
    ...
    
    def vote(request, question_id):
        question = get_object_or_404(Question, pk = question_id)
    
        try:
            # 사용자가 form에 입력한 값 가져오기
            selected_choice = question.choice_set.get(pk = request.POST["choice"])
    
        # 사용자가 아무런 선택도 하지 않고 form을 제출할 경우, 에러 메시지 출력
        except (KeyError, Choice.DoesNotExist) : 
            return render(request, "polls/detail.html", {"question": question, "error_message" : "선택이 없습니다."})
    
        else:
            # 사용자가 선택한 답안의 투표 수 1 증가
            selected_choice.votes += 1
            selected_choice.save()
    
            # index 페이지로 redirect
            return HttpResponseRedirect(reverse("polls:index"))

  • templates/polls/detail.html

    <form action = {% url "polls:vote" question.id %} method = "post">
        <!--CSRF 토큰 추가-->
        {% csrf_token %}
        
        <h1>{{question.question_text}}</h1>
    
        <!--에러 메시지 존재할 경우 출력-->
        {% if error_message %}
            <p><strong>{{error_message}}</strong></p>
        {% endif %}
    
        <ul>
        <!--라디오 버튼을 사용해 투표 기능 구현-->
        {% for choice in question.choice_set.all %}
            <input type = "radio" name = "choice" id = "choice{{forloop.counter}}" value = "{{choice.id}}">
            <label for = "choice{{forloop.counter}}">
                {{choice.choice_text}}
            </label>
            <br>
        {% endfor %}
        </ul>
    
        <input type = "submit" value = "Vote">
    </form>

7. 에러 방어하기 1


  • 발생할 수 있는 에러를 예상하기 위해서는 상상력이 필요

  • 사용자가 form을 제출하는 시점에 모델이 변경되어 존재하지 않는 객체가 선택될 수 있음
    -> 이러한 문제를 방지하기 위해 Choice.DoesNotExist 에러를 처리


  • 정합성이 맞지 않는 데이터로 요청이 들어올 경우 에러 메시지로 해당 데이터 출력

  • views.py

    ...
    
    def vote(request, question_id):
            ...
    
        # KeyError: 사용자가 아무런 선택도 하지 않고 form을 제출할 경우 발생
        # Choice.DoesNotExist: 존재하지 않는 Choice 객체를 get()할 경우 발생
        except (KeyError, Choice.DoesNotExist) : 
            return render(request, "polls/detail.html", {"question": question, "error_message" : f'선택이 없습니다. id = {request.POST["choice"]}'})
    
        ...

8. 에러 방어하기 2


  • 동시에 두 명의 사용자가 form을 제출한 경우, 혹은 두 사용자가 서로 다른 서버에 접근하는 경우, 동시에 같은 값을 변경하면 제대로 반영되지 않을 수 있음

    • 값을 변경하는 연산을 서버가 아닌 DB에서 실행

  • views.py

    from django.db.models import F
    ...
    
    def vote(request, question_id):
        ...
    
        else:
            # DB에서 votes의 값을 불러와 1 증가
            # 여러 명의 사용자가 동시에 연산하는 경우도 반영 가능
            selected_choice.votes = F("votes") + 1
            selected_choice.save()
    
    	...

9. 결과(Result) 조회 페이지


  • 302(= 302 Redirect): 요청한 리소스가 다른 URL로 이동했음을 알림

  • urls.py

    ...
    
    urlpatterns = [
        ...
        path('<int:question_id>/result', views.result, name = "result"),
    ]

  • views.py

    def vote(request, question_id):
        ...
    
        else:
            ...
    
            # result 페이지로 redirect
            return HttpResponseRedirect(reverse("polls:result", args = (question_id, )))
    
    def result(request, question_id):
        question = get_object_or_404(Question, pk = question_id)
    
        return render(request, "polls/result.html", {"question": question})
    • *args: 함수가 호출될 때 여러 개의 인자를 받을 수 있는 기능

    • **kwargs: 함수가 호출될 때 여러 개의 키워드 인자를 받을 수 있는 기능

      • 키워드 인자: key와 value로 이루어진 인자

  • templates/polls/result.html

    <h1>{{question.question_text}}</h1>
    <br>
    
    {% for choice in question.choice_set.all %}
        <label>
            {{choice.choice_text}} -- {{choice.votes}}
        </label>
        <br>
    
    {% endfor %}

10. Django Admin 편집 페이지 커스터마이징


  • admin.py

    ...
    
    # Choice 객체를 Question 객체 페이지에서 관리
    class ChoiceInline(admin.TabularInline):
        model = Choice
        extra = 3
    
    # Question 객체 커스터마이징
    class QuestionAdmin(admin.ModelAdmin):
        fieldsets = [
            ("질문 섹션", {"fields" : ["question_text"]}),
    
            # collapse: 숨김 기능
            ("생성일", {"fields" : ["pub_date"], "classes" : ["collapse"]})
        ]
    
        # 생성일은 관리자가 수정하는 것이 아니고, 글이 생성될 때의 시각이 고정
        readonly_fields = ["pub_date"]
    
        inlines = [ChoiceInline]
    
    admin.site.register(Question, QuestionAdmin)
    ...


11. Django Admin 목록 페이지 커스터마이징


  • Question 페이지에서 Choice 객체를 관리할 수 있기 때문에 등록 해제

  • models.py

    ...
    
    class Question(models.Model):
        # verbose_name: admin 목록 페이지에서 출력될 이름
        question_text = models.CharField(max_length = 200, verbose_name = "질문")
        pub_date = models.DateTimeField(auto_now_add = True, verbose_name = "생성일")
    
        # 생성한 지 하루 이내인지 확인하는 메서드
        @admin.display(boolean = True, description = "최근 생성(하루 기준)")
        def was_published_recently(self):
            return self.pub_date >= timezone.now() - datetime.timedelta(days = 1)
    
        ...

  • admin.py

    ...
    
    class QuestionAdmin(admin.ModelAdmin):
        ...
    
        # 목록 페이지 출력 형식
        list_display = ("question_text", "pub_date", "was_published_recently")
    
        # 생성일 기준 정렬 필터
        list_filter = ["pub_date"]
    
        # Question과 Choice의 텍스트로 검색
        search_fields = ["question_text", "choice__choice_text"]
    
    ...


메모


  • URL에는 공백 포함하면 안 됨
post-custom-banner

0개의 댓글