뷰(Views)와 템플릿(Templates)
템플릿에서 제어문 사용하기
상세(Detail) 페이지 만들기
상세 페이지로 링크 추가하기
404 에러 처리하기
폼(Forms)
에러 방어하기 1
에러 방어하기 2
결과(Result) 조회 페이지
Django Admin 편집 페이지 커스터마이징
Django Admin 목록 페이지 커스터마이징
뷰의 결과를 보기 좋게 출력하기 위해 템플릿 사용
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>
템플릿에서 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 %}
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>
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이 복잡해질수록 이와 같은 방식이 편함
404: 요청한 URL의 웹페이지가 존재하지 않는 경우 발생하는 에러
400: 서버에 문제가 있어서 발생하는 에러
Obj.object.get()
은 요청한 객체가 없는 경우 Obj.DoesNotExist 에러 반환
직접 반환
...
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})
...
<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>
발생할 수 있는 에러를 예상하기 위해서는 상상력이 필요
사용자가 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"]}'})
...
동시에 두 명의 사용자가 form을 제출한 경우, 혹은 두 사용자가 서로 다른 서버에 접근하는 경우, 동시에 같은 값을 변경하면 제대로 반영되지 않을 수 있음
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()
...
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
: 함수가 호출될 때 여러 개의 키워드 인자를 받을 수 있는 기능
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 %}
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)
...
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"]
...