뷰(view) 파일에서 사용되는 render()
함수는 Django의 내장 함수 중 하나로, HTTP 요청을 받아 해당 요청에 대해 원하는 템플릿 파일을 렌더링하여 응답하는 기능을 가지고 있다.
첫 번째 인자로 요청(Request) 객체,
두 번째 인자로 템플릿 파일의 경로, 그리고
세 번째 인자로 Context 변수를 입력 받는다.
템플릿 파일을 사용하기 위해서는 아래와 같이 template
이름을 가진 폴더를 생성하고 그 폴더 안에 템플릿 파일을 생성해주면 된다.
from .models import *
from django.shortcuts import render
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5] # pub_date 앞에 '-'를 붙임으로써 내림차순으로 가져온다는 뜻이다.
# template파일에서 쓰일 변수들을 넣어준다
context = {'first_question': latest_question_list[0]}
# polls/template 폴더가 기본 경로이다.
return render(request, 'polls/index.html', context)
views.index에서 render
함수의 세번째 인자로 넘긴 context
에 들어있는 변수들을 {{ }}
괄호 안에 넣어서 사용할 수 있다.
<ul>
<li>{{first question}}</li>
<ul>
context
에서 iterable한 변수를 넘기게 된다면 템플릿에서 {% %}
괄호 안에 순환문을 사용하여 변수 활용이 가능하다. 또한 조건문으로 사용할 변수의 값이 없는 경우 기존과 다른 html구성을 보여줄 수 있다.
context에 question리스트 전체를 넘긴다.
from .models import *
from django.shortcuts import render
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'questions': latest_question_list}
#context = {'questions': []}
return render(request, 'polls/index.html', context)
조건문을 사용하여 리스트에 요소가 없으면 no questions
를 표시하고, 있다면 순환문을 사용하여 리스트 형태로 표시한다.
{% if questions %}
<ul>
{% for question in questions %}
<li>{{question}}</li>
{% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}
상세 페이지를 만들어 question에 대한 choice리스트를 출력한다. 그러기 위해서는 질문이 나열되어 있는 페이지(index)에서 질문을 선택하여 해당 질문의 상세페이지로 들어갈 때, 질문 id를 상세 페이지에 넘겨야 한다.
상세페이지에서는 전달받은 질문 id와 일치하는 객체를 get()
메서드로 가져와 context
에 담고, 상세페이지 템플릿에서는 이를 사용하여 질문에 대한 선택지 리스트를 출력한다.
질문에 대한 선택지 중 하나를 고르고 vote
하게 되면 ( <form>
태그를 사용하여 구현) 해당 선택지의 vote수를 올리는 기능을 추가한다.
http://127.0.0.1:8000/polls/1 와 같이 끝에 질문 id를 포함한 경로를 설정해준다. 앱 이름을 설정하여 템플릿에서 앱 이름을 통해 url설정에 접근할 수 있도록 한다.
또한 선택지 중 하나를 골랐을 그 선택지의 vote 수가 올라가는 기능을 수행하는 views.vote
로의 경로도 추가한다.
from django.urls import path
from . import views
# 앱 이름 설정 - html파일에서 name으로 url을 접근할 때 이제 앞에 'polls:'도 붙여줘야 함
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'),
]
질문에 <a>
태그를 사용하여 링크를 걸어준다. 경로는 {% url '앱 이름:view의 name' 전달할 인자 %}
형식으로 지정한다.
<!--html에서 조건문 사용하기-->
{% if questions %}
<ul>
<!--html에서 순환문 사용하기-->
{% for question in questions %}
<!--context 변수 사용 -->
<!--<li><a href="/polls/{{question.id}}">{{question.question_text}}</a></li> -->
<!-- url파일에 설정해 준 name 사용 -->
<li><a href="{% url 'polls:detail' question.id %}">{{question.question_text}}</a></li>
{% endfor %}
</ul>
{% else %}
<p> no questions </p>
{% endif %}
해당 템플릿은 아래와 같이 렌더링 된다.
detail
에서는 질문 객체를 넘김으로써 템플릿에서 질문과 그에 딸린 선택지에 접근할 수 있도록 한다.
vote
에서는 선택한 선택지의 vote수를 올리고 db에 반영한다. 아무 선택지를 선택하지 않았을 경우의 예외처리를 해준다. 수행이 끝나고는 처음 화면으로 redirect 해준다.
두 함수 모두에 get_object_or_404
모듈을 사용하여 주어진 id에 맞는 질문이 없을 경우 404 에러를 일으킨다.
from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.urls import reverse
from django.db.models import F
from .models import *
...
def detail(request, question_id):
# 주어진 question_id를 가진 Question이 없을 경우 예외처리
# 1. try~except 로 예외처리
# try:
# question = Question.objects.get(pk=question_id)
# except Question.DoesNotExist:
# raise Http404("Question does not exist!")
# 2. get_object_or_404 메소드로 예외처리
question = get_object_or_404(Question, pk=question_id)
context = {
'question': question
}
return render(request, 'polls/detail.html', context)
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']) # choice 라는 name을 가진 tag의 value(choice_id)를 가져온다.
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {'question': question, 'error_message': f"선택이 없습니다. ID={request.POST['choice']}"})
else:
# 서버에서 연산을 하게 되면 사용자들이 동시에 투표했을 때 정확한 카운트를 하지 못할 수 있음
# selected_choice.votes += 1
# F 모듈을 활용하기 - F의 인자로 주어지는 컬럼 값에 대한 처리를 하게 해준다.
selected_choice.votes = F('votes') + 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:index'))
{% csrf_token %}
은 Django가 CSRF공격에 대해 방어하기 위한 구문이다.ㅠPOST 양식을 사용하는 템플릿에서 <form>
태그 안에 사용함으로써 CSRF(Cross Site Request Forgery) 공격을 방어한다.
<form>
태그의 action
속성에 질문 id를 넘기는 경로를 지정하고 POST 방식으로 설정한다.
question.choice_set.all
을 순환하면서 선택지들을 리스트로 보여주고, {{ forloop.counter }}
로 각 선택지를 구분해준다. 각 선택지의 name
과 value
값은 vote를 한 후 작동하는 view.vote에서 해당 선택지 객체를 가져오기 위해 쓰이게 된다.
<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>
해당 템플릿은 아래와 같이 렌더링 된다.
선택지들의 각 vote 수들을 보여주는 페이지를 생성한다. 전체적인 흐름으로는 사용자가 먼저 질문을 선택하고 선택지를 하나 골라서 vote하게 되면 그 때 결과조회 페이지로 리다이렉트 된다.
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'), # 결과조회 페이지 추가
]
view.vote 에서 투표 결과를 데이터베이스에 저장하고, 이후 바로 결과 페이지로 연결될 수 있도록 질문id와 함께 리다이렉션을 설정한다.
from django.shortcuts import get_object_or_404, render
...
def vote(request, question_id):
...
else:
selected_choice.votes = F('votes') + 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:result', args=(question.id,)))
# 결과조회 - 선택지별 vote 개수 표시
def result(request, question_id):
question = get_object_or_404(Question, pk=question_id)
# 결과조회 페이지로 질문id를 넘기면서 리다이렉트
return render(request, 'polls/result.html', {'question': question})
다른 템플릿과 마찬가지고 question 객체에 접근하여 그 객체에 딸린 선택지 정보를 출력한다.
<h1>{{ question.question_text }}</h1><br>
{% for choice in question.choice_set.all %}
<label>
{{ choice.choice_text }} -- {{ choice.votes }}
</label>
<br>
{% endfor %}
아래와 같은 결과조회 페이지를 얻게 된다.
기본으로 제공되는 관리자 페이지를 admin.ModelAdmin
클래스를 상속받아 원하는대로 커스터마이징할 수 있다.
Choice 모델을 등록할 필요없이 커스텀한 Question 모델을 등록함으로써 Question을 추가/수정할 때 Choice도 같이 추가/수정할 수 있다.
from django.contrib import admin
from .models import *
#Register your models here
#admin.site.register(Question)
#admin.site.register(Choice)
# CRUD : Create Read Update Delete
# 커스터마이징
#class ChoiceInline(admin.StackedInline): # 수직정렬
class ChoiceInline(admin.TabularInline): # 수평정렬
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
('질문 섹션', {'fields': ['question_text']}),
#('생성일', {'fields': ['pub_date']}),
('생성일', {'fields': ['pub_date'], 'classes': ['hidden']}), # 'classes' 를 지정하면 hidden, collapsed 옵션으로 숨김처리 할 수 있음
]
list_display = ['question_text', 'pub_date', 'was_published_recently'] # 표시하고 싶은 필드를 나열한다
readonly_fields = ['pub_date'] # 읽기 전용 처리
inlines = [ChoiceInline] # Choice를 생성할 때 같이 만들 수 있도록 함
list_filter = ['pub_date'] # 필드의 타입을 보고 자동으로 그에 맞는 필터를 제공함
search_fields = ['question_text', 'choice__choice_text'] # 필드값을 검색하는 검색창을 생성 - choice 객체의 필드에 접근을 위해서는 'choice__choice_text' 와 같이 접근한다
admin.site.register(Question, QuestionAdmin)
필드의 verbose_name
을 지정해주면 관리자 페이지에 컬럼명으로 나타나게 된다.
@admin.display
annotation을 사용하여 메소드에도 컬럼에 어떻게 보여질 지 설정한다.
from django.db import models
from django.utils import timezone
from django.contrib import admin
from datetime import datetime, timedelta
# Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200, verbose_name='질문')
pub_date = models.DateTimeField(auto_now=True, verbose_name='생성일시')
@admin.display(boolean=True, description='최근생성(하루기준)')
def was_published_recently(self):
return self.pub_date >= timezone.now() - timedelta(days=1)
def __str__(self):
if self.was_published_recently():
new_badge = 'NEW!!!'
else:
new_badge = ''
return f'{new_badge} 제목: {self.question_text}, 날짜: {self.pub_date}'
*args
는 Python 함수에서 사용되는 파라미터로, 함수가 호출되고 여러 개의 인자(argument)를 입력받는 상황에서 유연성을 높여주는 기능을 제공한다.
기존의 방식대로 함수에 여러가지 인자들을 전달하려면, 함수를 선언하고 괄호안에 전달되는 인자의 개수에 맞춰서 여러가지 파라미터들을 작성해주어야 하는데, *args
를 사용하면 보다 간편하고 유연하게 함수에 여러가지 인자가 전달되는 상황을 처리할 수 있다.
def example(*args):
for arg in args:
print(arg)
위의 함수를 example(1, 2, 3, 4)
로 함수 호출 시 전달된 인자는 *args
로 묶여 함수 내부에서 반복문으로 출력된다.
*args
를 사용함으로써 4개의 int형 매개변수를 작성할 필요없이 단 한줄로 전달되는 입력값들을 모두 처리할 수 있게 된다.
**kwargs
는 *args
와 동작하는 방식은 같지만 인자의 데이터형이 key와 value값의 쌍으로 이루어진다는 점에서만 다르다
def example(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value}")
위의 함수를 example(a=1, b=2, c=3, d=4)
로 호출 시 4개의 key:value 쌍의 변수를 **kwargs
로 묶어 처리할 수 있다.
HTTP 404
에러는 일반적으로 웹 요청에서 찾을 수 없는 페이지나 리소스를 요청했을 때 발생한다.
HTTP 500
에러는 서버의 동작에서 발생하는 에러 중 더 정확한 에러 코드가 아닌 경우를 나타낸다. 즉 예외적인 또는 예측하지 못한 에러일 경우 나타난다.
따라서 더 정확한 에러를 내뱉게 하기 위해서는 HTTP 500
로 나타나는 에러를 처리하여 HTTP 404
로 나타나도록 수정해야 한다.
어떤 방식으로 코드를 짜야하는지는 알겠는데 정해진 문법이 많고 일관성(?)이 부족하다고 느껴져 잘 외워지지 않았다. 여러번 반복해서 코드를 짜봐야 할 것 같다고 생각했다.