[4/9] TIL - Django를 이용한 API 만들기 [2]

Sangwon Jwa·2024년 4월 9일

데브코스 TIL

목록 보기
12/54
post-thumbnail

📖 학습 주제


  1. View & Template
  2. Form & Customizing

✏️ 주요 메모 사항 소개


뷰(View) 와 템플릿(Template)

  • views.py 파일에 직접 httpresponse를 이용해 뷰를 구성할 수 있지만 보통 HTML 파일을 별도로 생성해서 구현하는 것이 대중적이다. templates/[app_name] 폴더에 html 파일을 만든 뒤 views.py 에서 render 함수를 이용해 반환을 해주면 된다. 넘겨진 값을 사용하려면 {{ item }} 중괄호 두개를 사용한다.
# polls/views.py
def index(request):
    # Question 모델의 목록들을 pub_date를 기준으로 내림차순으로 5개 까지 추출
    latest_question_list = Question.objects.all().order_by('-pub_date')[:5]
    context = {'first_question': latest_question_list[0]}
    # render 함수를 이용해 html 파일 연결
    return render(request, 'polls/index.html', context)
<!-- templates/polls/index.html -->
<ul>
  	<!-- 전달받은 인자를 사용하기 위해서는 {{}} 사용 -->
    <li>{{first_question}}</li>
</ul>

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

  • 하나의 값이 아닌 반복문 등을 이용해 여러개의 값을 가지고 오려면 템플릿 내에서 {% %} 태그를 활용하면 된다. {% for item in items %}로 시작해서 반복문이 끝나는 곳에 {% endfor %}를 입력하면 된다.
  • 조건문을 활용 하기 위해서는 {% 조건식 %}{% else %}, {% endif %} 를 사용하면 된다.
{% if question %}
<ul>
    {% for question in questions%}
    <li>{{question}}</li>    
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

상세(detail) 페이지 만들기

  • 상세 페이지를 만들기 위해서는 url에 특정 상세 페이지를 나타내는 인자가 필요하다. 따라서 polls/urls.py 에 인자를 받는 path를 추가하고 , views.pydetail함수를 추가하고 detail.html 뷰를 생성하면 된다.
  1. urls.py에 인자를 받는 url 설정 ( '<int:question_id>/')
urlpatterns = [
    path('', views.index, name='index'),
    
    # url 뒤에 인자값으로 정수값 question_id를 받고 이를 views.detail로 연결
    path('<int:question_id>/', views.detail, name='detail'),
    path('some_url', views.some_url),
]
  1. views.py에 detail 추가
def detail(request, question_id):
	# Question 객체들 중 pk(primary key)가 입력받은 question_id인 것 찾기
    question = Question.objects.get(pk=question_id)
    # render 로 뷰 생성 (question의 값들을 인자로 넘겨줌)
    return render(request, 'polls/detail.html', {'question': question})
  1. templates/polls/detail.html 추가
<!-- 넘겨받은 인자의 필드 출력 -->
<h1>{{question.question_text}}</h1>
<ul>
<!-- 넘겨받은 question으로 연결된 choice 출력, 템플릿에서는 함수뒤에 () 붙이지 않음!! -->
{% for choice in question.choice_set.all %}
    <li>{{choice.choice_text}}</li>
{% endfor %}
</ul>

링크 추가하기

  • 링크를 추가하려면 <a> 태그를 이용하면 된다. <a> 태그 안에 urls.py에서 지정한 app_name 과 url 설정의 name을 이용해서 경로를 지정하고, 넘겨줘야 할 인자가 있다면 {% url '경로' 인자 %} 방식으로 사용하면 된다.
  1. urls.py에서 app_name을 추가하고 detailname 확인하기
# app_name을 polls 로 지정
app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    # 링크로 만드려고 하는 detail의 name 값 detail을 기억
    path('<int:question_id>/', views.detail, name='detail'),
    path('some_url', views.some_url),
]
  1. templates/polls/index.html<a> 태그 추가
{% if questions %}
<ul>
    {% for question in questions%}
    <!-- urls.py 에서 지정한 detail의 name 속성을 이용해서 경로 지정, 인자로 question의 id값을 넘겨줌-->
    <li><a href="{% url 'polls:detail' question.id %}">{{question.question_text}}</a></li>    
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

404 에러 처리하기

요청한 페이지가 없음을 나타내는 404 에러는 try-except구문을 사용하거나 django.shortcuts 라이브러리의 get_object_or_404 메서드를 사용할 수 있다.

  1. try-except
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})
  1. get_object_or_404
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

폼(Forms)과 커스터마이징

  • 폼은 사용자에게 데이터를 입력받는 태그를 말한다. 입력받은 데이터를 urls.py에서 설정한 url에 따라 views.py 에서 처리한다.
  1. html 작성
<!-- form 태그 작성, action 속성에 {% url 'url이름' %}를 이용하여 경로를 설정 -->
<form action={% url 'polls:vote' question.id %} method='post'>
  	<!-- csrf token 공격 방지 -->
    {% csrf_token %}
    <h1>{{question.question_text}}</h1>
	
    <!-- input 데이터가 없을 때 error_message 출력 -->
    {% if error_message %}
    <p><strong>{{error_message}}</strong></p>
    {% endif %}
	
  	<!-- question에 따른 choice_set을 radio 형태로 출력 -->
    {% 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>
  1. urls.pyvote url 추가
# polls/urls.py
app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    # name을 vote로 해서 경로 선언
    path('<int:question_id>/vote/', views.vote, name='vote'),
]
  1. views.py에 vote 추가
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    # POST로 받은 choice를 selected_choice에 넣음
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    # 만약 POST에서 넘어온 값이 없다면 error_message를 포함하여 다시 render
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다'})
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))

에러 방어하기

  • 서로 다른 서버에서 접속한 두명의 사용자가 동시에 POST를 진행할 시 값이 제대로 저장이 안될 수 있다. 이런 상황을 방지하기 위해 DB의 값을 변화하는 경우 views.py에서 실행되게 하지 말고 DB에서 처리되게 변경해주어야 한다.
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': '선택이 없습니다'})
    
    # django.db.models의 F 를 이용하여 기존의 votes 개수를 메모리가 아닌 db에서 가져온다.
    selected_choice.votes = F('votes') + 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))

결과(result) 조회 페이지

  • 투표한 내용을 나타내는 결과 페이지를 작성
  1. urls.py에 result 경로 작성
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'),
    #result 경로 추가
    path('<int:question_id>/result/', views.result, name='result'),
]
  1. result 를 보여주는 html 작성
<h1>{{question.question_text}}</h1><br>
    {% for choice in question.choice_set.all %}
        <label>
            {{choice.choice_text}} -- {{choice.votes}}
        </label>
        <br>
    {% endfor %}
  1. views.pyresult 를 추가하고 vote의 redirect를 result로 변경
# result 추가
def result(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/result.html', {'question': question})

# 마지막 리다이렉트 부분을 polls:result로 변경, result는 question_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': '선택이 없습니다'})
    selected_choice.votes = F('votes') + 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:result', args=(question_id,)))

*args & **kwargs

*args : 파이썬 함수의 파라미터로 여러개의 값이 들어올 때 사용하는 파라미터

def example(*args):
	sum = 0
    for arg in args:
    	sum += arg
    return sum

**kwargs : 파이썬 함수의 파라미터로 여러 개의 키워드 인자를 받을 수 있도록 기능을 제공 (입력이 key-value 와 같은 형태일 때)

def example(**kwargs):
	for key, value in kwargs.items():
    	print(f"{key} = {value}")

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

Django에서는 admin.py 파일에 admin.site.register를 통해 admin 페이지를 활용해 모델들에 대한 CRUD 작업을 처리할 수 있었다. 이 작업을 더 사용자 맞춤형으로 만들기 위해 admin.py파일을 수정해 보자.

from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(Choice)

# Choice도 Question 탭에서 같이 수정하기 위해 Inline 설정
class ChoiceInline(admin.TabularInline):
    model = Choice
    # 추가옵션 개수
    extra = 3

# 커스터마이징 된 QuestionAdmin 페이지를 구성
class QuestionAdmin(admin.ModelAdmin):
	# Question의 필드를 설정, 순서를 바꾸면 바꾼 순서대로 표시됨.
    fieldsets = [
        ('질문 섹션', {'fields': ['question_text']}),
        # 생성일 숨김(collapse)
        ('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    
    # 더 깔끔하게 질문과 날짜를 출력
    list_display = ['question_text', 'pub_date', 'was_published_recently']
    
    # auto_now_add=True 때문에 오류가 생길 수 있음 따라서 pub_date 필드를 수정 불가능하게 설정
    readonly_fields = ['pub_date']
    
    # Question에 따른 Choice 변경할 때 필요한 inlines를 같이 작성
    inlines = [ChoiceInline]
    
    # 생성일을 기준으로 필터 추가
    list_filter = ['pub_date']
    
    # Question과 Choice에 대한 검색 기능 추가
    search_fields = ['question_text', 'choice__choice_text']

# Question과 함께 커스터마이징한 QuestionAdmin도 같이 등록
admin.site.register(Question, QuestionAdmin)
  • Question 페이지의 레이블을 변경하고 싶다면 models.py에서 Question 모델의 필드와 메서드에 verbose를 설정하거나 @admin.display()를 달아줘야 한다.
# models.py의 Question 클래스

# 각 필드의 속성으로 verbose_name을 지정해서 레이블 명을 바꿀 수 있다.
question_text = models.CharField(max_length=200, verbose_name='질문')
pub_date = models.DateTimeField(auto_now_add=True, verbose_name='생성일')


# 메서드 위에 admin.display() 애너테이션 작성
@admin.display(boolean=True, description='최근생성(하루기준)')
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

💦 공부하며 어려웠던 내용


django의 문법이 익숙치 않아 특수기호 여러 개가 중첩될 때 헷갈릴 때가 많았다.

0개의 댓글