[Django] Part 4: 폼(Forms)과 커스터마이징(Customizing)

김서연·2024년 4월 12일

Django

목록 보기
5/8

1. 폼(Forms)

  • 질문별 detail 페이지를 답변을 제출할 수 있는 폼 형태로 수정한다

폼 형태로 수정하기

  • polls/templates/polls/detail.html
    <form action="'#" method="'post">
        <h1>{{question.question_text}}</h1>
        
        {% 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 %}
        
    <input type="submit" value="Vote">
    </form>

토큰 추가

  • {% csrf_token %}
<form action="'#" method="'post">
    {% csrf_token %}
    <h1>{{question.question_text}}</h1>
    
    {% 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 %}
    
<input type="submit" value="Vote">
</form>

submit 결과를 admin에서 확인하기

  • 커피 선택 후 submit
  • admin에서 Votes 수가 올라간 것을 확인

선택지를 선택하지 않고 submit하는 경우 예외 처리하기

  • 선택하지 않고 submit할 경우 error message 출력

  • {% csrf_token %}

    <form action= {% url 'polls:vote' question.id %} method="post">
        {% csrf_token %}
        <h1>{{question.question_text}}</h1>
        {% if error_message %}
        <p><strong>{{ error_message }}</strong></p>
        {% endif %}
        
        {% 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 %}
        
    <input type="submit" value="Vote">
    </form>
  • polls/views.py

    • try-except 문을 통해 선택하지 않았을 경우 템플릿으로 error_message 전달

      def vote(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          try:
              selected_choice = question.choice_set.get(pk=request.POST['choice'])
          except (KeyError, Choice.DoesNotExist):
              return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다.'})
          else:
              selected_choice.votes += 1
              selected_choice.save() # 테이블에 값 저장
      
              # index로 돌아가기
              return HttpResponseRedirect(reverse('polls:index'))
  • 에러 메시지 화면


2. 에러 방어하기 1

  • 에러는 다양한 상황에서 발생할 수 있기 때문에 상상력이 필요하다
  • 만약 선택 후 vote를 누르는 사이에 선택지 일부가 사라진다면?
    • DoesNotExist 가 발생
  • 브라우저가 잘못 동작하며, 서버와 정확성이 맞지 않는 데이터가 올라올 수도 있다
  • polls/views.py
    • 'error_message': f"선택이 없습니다. id={request.POST['choice']}" 에서 id까지 함께 에러 메시지에 출력하게 한다

      def vote(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          try:
              selected_choice = question.choice_set.get(pk=request.POST['choice'])
          except (KeyError, Choice.DoesNotExist):
              return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. id={request.POST['choice']}"})
          else:
              selected_choice.votes += 1
              selected_choice.save()
              return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

3. 에러 방어하기 2

  • A, B가 서로 다른 장고 서버 A_server, B_server에 접속하고, 이 서버는 모두 하나의 데이터베이스와 연결되어 있다

  • 이때 A, B가 동시에 같은 선택지를 선택 후 submit한다면?

  • 똑같은 명령, Votes = 1 이라는 명령을 실행하게 되는데 어쩔 수 없이 우열이 생기므로 실제로 Votes = 2가 되어야 하지만 결과적으로 Votes = 1이 된다

  • 기존의 코드에서는 로컬 메모리에 저장한 뒤 서버 DB에 save하기 때문에 이러한 문제가 생긴다

  • F() 활용

    • F()는 DB에서 바로 값을 가져와 업데이트 하라는 의미

      selected_choice.votes = F('votes') + 1
  • polls/views.py

    • HttpResponseRedirect(reverse('polls:index')) : submit하면 다른 페이지로 이동하도록 한다.
      - 이때, reverse()는 함수의 url 패턴의 이름을 사용해 해당 url을 가져온다

      from django.urls import reverse
      from django.db.models import F
      
      def vote(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          try:
              selected_choice = question.choice_set.get(pk=request.POST['choice'])
          except (KeyError, Choice.DoesNotExist):
              return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. id={request.POST['choice']}"})
          else:
              # A서버에서도 Votes = 1
              # B서버에서도 Votes = 1 
              selected_choice.votes = F('votes') + 1
              selected_choice.save()
              return HttpResponseRedirect(reverse('polls:index'))

4. 결과(result) 조회 페이지

  • submit 후 현재까지의 vote 결과를 보여주는 페이지를 구현한다

  • polls/urls.py

    from django.urls import path
    from . import views
    
    app_name = 'polls'
    urlpatterns = [
        path('', views.index, name='index'),
        path('<int:question_id>/', views.detail, name='detail'),
        path('<int:question_id>/vote/', views.vote, name='vote'),
        path('<int:question_id>/result/', views.result, name='result'),
    ]
  • polls/templates/polls/result.html

    • choice를 순회하며 choice - votes 형태로 출력될 수 있도록 구현한다

      <h1>{{question.question_text}}</h1>
      {% for choice in question.choice_set.all %}
          <label for="choice{{forloop.counter}}">
              {{choice.choice_text}} -- {{choice.votes}}
          </label>
          <br>
      {% endfor %}
  • polls/views.py

    • submit 후 result 페이지로 이동하도록 구현

      from django.shortcuts import get_object_or_404, render
      
      def vote(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          try:
              selected_choice = question.choice_set.get(pk=request.POST['choice'])
          except (KeyError, Choice.DoesNotExist):
              return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. id={request.POST['choice']}"})
          else:
              selected_choice.votes = F('votes') + 1
              selected_choice.save() # 테이블에 값 저장
      
              # result 보여주기
              return HttpResponseRedirect(reverse('polls:result', args = (question_id,)))
          
      # vote 결과
      def result(request, question_id):
          question = get_object_or_404(Question, pk=question_id)
          return render(request, 'polls/result.html', {'question':question})
  • 결과 화면


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

  • admin는 커스터마이징을 통해 보완할 수 있다
  • django admin은 모델을 추가하기만 하면 모델의 내용에 대해 CRUD가 가능한 페이지를 만들 수 있다는 장점이 있다
    • CRUD: Create Read Update Delete

Question Admin 커스텀

  • 원하는 필드만 고르기
    • 이때, 필드명에 None을 입력하면 admin에서 보이지 않는다

  • 순서 설정하기
  • 객체 생성 시, 요소의 기본값을 자동으로 넣어주는데 admin 편집 페이지에서 보고 싶을 때
    • pub_date = models.DateTimeField(*auto_now_add=True*) : 자동으로 현재 시간을 추가해준다
    • 자동으로 현재 시간이 등록되는 기능때문에 직접 pub date를 입력하는 fieldsets 에 있으면 오류가 난다 → readonly field로 설정

Question 안에서 Choice 관리하기

  • Question에 종속 관계인 Choice를 서로 다른 편집기를 사용하지 않고 Question 편집기 안에서 관리할 수 있다.
  • polls/admin.py
    • extra는 몇 개의 choice를 “추가로” 등록할 것인지 나타낸다

    • 이미 2개의 choice 객체가 있을 경우 3개를 추가로 띄운다

      from django.contrib import admin
      from .models import *
      
      class ChoiceInline(admin.StackedInline):
          model = Choice
          extra = 3 # 몇 개의 choice를 "추가로" 등록할 것인지
      
      class QuestionAdmin(admin.ModelAdmin):
          fieldsets = [
              ('질문 섹션', {'fields' : ['question_text']}),
              ('생성일', {'fields' : ['pub_date']}),
          ]
          readonly_fields = ['pub_date']
          inlines = [ChoiceInline]
      
      # Register your models here.
      admin.site.register(Question, QuestionAdmin)
      admin.site.register(Choice)
      

  • Choice의 요소를 가로 / 세로로 나열하기
    • Inline 객체는 다양하게 상속받을 수 있다
    • admin.StackedInline : 요소를 세로로 나열한다
    • admin.TabularInline : 요소를 가로로 나열한다
      	class ChoiceInline(admin.TabularInline):
          model = Choice
          extra = 3

  • 옵션 숨김 처리하기
    • 숨기고 싶을 경우 inlines 에 포함시킨다

      class QuestionAdmin(admin.ModelAdmin):
          fieldsets = [
              ('질문 섹션', {'fields' : ['question_text']}),
              ('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
          ]
          readonly_fields = ['pub_date']
          inlines = [ChoiceInline]
      

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

목록에 원하는 요소 표시하기

  • polls/admin.py
    • list_display: 목록에서 표시할 요소

      from django.contrib import admin
      from .models import Choice, Question
      
      class QuestionAdmin(admin.ModelAdmin):
          fieldsets = [
              ('질문 섹션', {'fields' : ['question_text']}),
              ('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
          ]
          list_display = ('question_text', 'pub_date', 'was_published_recently')
          readonly_fields = ['pub_date']
          inlines = [ChoiceInline]

column label 표기 바꾸기

  • models.py
    • verbose_name : label 표기 설정

    • @admin.display(boolean=True, description='최근생성(하루기준)')
      - boolean : True로 할 경우, 아이콘으로 보인다. False로 할 경우 True, False로 보인다.
      - escription : label 표기 설정

      import datetime
      from django.db import models
      from django.utils import timezone
      from django.contrib import admin
      
      class Question(models.Model):
          # 질문 (최대 길이 200)
          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):
              # pub date가 어제보다 크면, 어제보다 더 최근에 만들어졌는지
              return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

필터 설정

  • models.py
    • list_filter = ['pub_date'] : 필터로 사용할 요소

      class QuestionAdmin(admin.ModelAdmin):
          fieldsets = [
              ('질문 섹션', {'fields' : ['question_text']}),
              ('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
          ]
          list_display = ('question_text', 'pub_date', 'was_published_recently')
          readonly_fields = ['pub_date']
          inlines = [ChoiceInline]
          list_filter = ['pub_date']

검색 기능 구현하기

  • models.py
    • search_fields = ['question_text'] : 검색할 때 탐색할 요소

      class QuestionAdmin(admin.ModelAdmin):
          fieldsets = [
              ('질문 섹션', {'fields' : ['question_text']}),
              ('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
          ]
          list_display = ('question_text', 'pub_date', 'was_published_recently')
          readonly_fields = ['pub_date']
          inlines = [ChoiceInline]
          list_filter = ['pub_date']
          search_fields = ['question_text']

Choice에 대해 검색 기능 구현하기

  • 이전에 Choice에 대해 탐색했을 때 쿼리를 생각해보면 Question과 Choice를 join하여 가능했다
  • 그것처럼, 단순히 search_fields 에 추가하면 choice에 대해서도 검색할 수 있다.
    class QuestionAdmin(admin.ModelAdmin):
        fieldsets = [
            ('질문 섹션', {'fields' : ['question_text']}),
            ('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
        ]
        list_display = ('question_text', 'pub_date', 'was_published_recently')
        readonly_fields = ['pub_date']
        inlines = [ChoiceInline]
        list_filter = ['pub_date']
        search_fields = ['question_text', 'choice__choice_text']
profile
가보자고! 🔥

0개의 댓글