
파이썬 장고 프레임웍을 사용해서 API 서버 만들기(2)
뷰 : 모델로 읽어온 정보를 활용한다.
템플릿 : html을 활용해 데이터를 잘 표시해준다.
from .models import *
from django.shortcuts import render
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'first_question': latest_question_list[0]}
return render(request, 'polls/index.html', context)
위 코드에서는 가장 최근에 생성된 5개의 Question을 가져와서 "latest_question_list" 변수에 저장하고, 그 중 첫번째 Question을 딕셔너리 형태로 context 변수에 저장한다.
render(request, 템플릿의 경로, context) : Django의 내장 함수 중 하나로, HTTP 요청을 받아 해당 요청에 대해 원하는 템플릿 파일을 렌더링하여 응답하는 기능을 가지고 있음
<ul>
<li>{{first question}}</li>
<ul>
{% 제어문 %}, {{변수}} 와 같은 형식으로 템플릿에서 제어문, 변수를 사용할 수 있다.
{% if questions %}
<ul>
{% for question in questions %}
<li>{{question}}</li>
{% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}
위의 코드는 questions 변수가 비어있지 않은 경우에는 questions 변수에 저장된 오브젝트들을 출력하고, 반대로 비어있다면 no questions" 문구를 출력하게 한다.
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)
views.py에 작성된 index 메서드의 코드는 위와 같다.
detail : 특정 클래스의 세부 정보를 표시하는 뷰
def detail(request, question_id):
question = Question.objects.get(pk=question_id)
return render(request, 'polls/detail.html', {'question': question})
위의 코드로 Question 모델 클래스에서 id 필드의 값이 detail 뷰의 인자로 전달된 question_id와 일치하는 객체를 get() 메서드로 가져와서 question 변수에 저장할 수 있다.
이렇게 저장된 question 변수는 해당 Question에 대한 정보를 렌더링하는 템플릿에 전달된다.
웹 브라우저 상에서 사용자의 요청에 따라 detail 뷰의 내용이 실행될 수 있도록 URL 경로를 설정하자
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('some_url', views.some_url),
path('<int:question_id>/', views.detail, name='detail'),
]
http://127.0.0.1:8000/polls/ 뒤에 숫자(정수)를 입력하면, 그 값을 question_id 변수에 전달하고 detail 뷰를 호출하도록 path를 지정하도록 했다.
detail 템플릿(Template)은 특정 클래스에 대한 상세 정보를 보여준다.
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>
위의 코드는 먼저 해당 질문의 question_text를 제목으로 출력한다.
그리고 해당 질문과 연결된 모든 선택지들을 가져오고, 반복문을 사용하여 각 선택지의 choice_text를 출력한다.
코드에서 사용하는 question은 detail 뷰에서 전달받은 question 오브젝트를 나타낸다.
{% if questions %}
<ul>
{% for question in questions %}
<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
{% endfor %}
<ul>
{% else %}
<p>no questions</p>
{% endif %}
위의 코드는 질문 목록에 질문이 하나 이상 존재하는 경우에만 해당 목록에서 질문을 하나씩 출력하는 index 템플릿 코드이다. 여기에 <a> 태그를 활용하여 각 질문을 클릭하였을 때, 해당 질문의 상세페이지로 이동하도록 했다. 그 방식은 해당 질문의 question.id를 입력받고, 그 값을 questions:question_detail에 반영하여 고유한 URL을 생성하는 구조이다.
path를 추가하자.
from django.urls import path
from . import views
app_name = 'polls'
urlpatterns = [
path('', views.index, name='index'),
path('some_url', views.some_url),
path('<int:question_id>/', views.detail, name='detail'),
]
위의 코드는 urls.py파일의 코드이다. app_name이 polls로 되어있음을 알 수 있는데 url을 불러올 때는 이 app_name과 name의 값과 같이 써야한다. 따라서 템플릿에 url 'polls:detail' 라고 되어 있음을 볼 수 있다.
get_object_or_404 메서드를 활용하면 조건을 만족하는 오브젝트를 가져오고, 만약 해당 객체가 없다면 Http404 예외를 발생시킬 수 있다.
from models.py import *
from django.http import HttpResponse
from django.http import Http404
from django.shortcuts import render , 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})
html의 form과 input을 이용해 기능을 추가할 수 있다. 아래의 코드로 Question과 Choice로 투표를 받는 기능을 추가한다.
# {% 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 %}
# forloop.counter : for 문이 돌 때마다 1씩 증가함
# value : 선택값
{% 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>
from django.urls import reverse
# 투표 결과를 제출할 페이지 생성
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 없을 때
except (KeyError, Choice.DoesNotExist):
return render(request, 'polls/detail.html', {'question': question, 'error_message': '선택이 없습니다.'})
else:
selected_choice.votes += 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:index'))
path를 추가하자.
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'),
]
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,)))
브라우저에서는 에러가 발생하여 존재하지 않는 엉뚱한 choice_id 값이 전달되는 경우도 발생할 수 있다.
이런 경우를 대비하여 error_message인 "선택이 없습니다." 에 추가로 잘못 전달된 choice_id의 값을 표시하도록 했다.
실제로 사용하는 서버에서는 하나의 데이터베이스에 접속하는 여러가지 서버를 운영하는데 따라서 여러 사람이 서로 다른 서버에서 동시에 하나의 Choice를 투표하는 상황도 발생할 수 있다. (A라는 서버와 B라는 서버에서 동시에 selected_choice.votes값을 1 증가시키라는 명령을 전달하는 경우)
이 때 votes의 값에는 원래대로라면 들어가야할 투표의 합계인 2가 아니라 1이 들어가는 에러가 발생할 수 있다.
이를 방지하기 위해서 selected_choice.votes값을 1 증가시키는 연산을 서버가 아닌 데이터베이스에서 수행하도록 코드를 변경하자.
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'))
from django.shortcuts import get_object_or_404, render
def result(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/result.html', {'question': question})
<h1>{{ question.question_text }}</h1><br>
{% for choice in question.choice_set.all %}
<label>
{{ choice.choice_text }} -- {{ choice.votes }}
</label>
<br>
{% endfor %}
result 뷰와 템플릿을 이용해 투표 결과를 조회하는 페이지를 만들었다.
추가적으로 vote 뷰에서 투표 결과를 데이터베이스에 저장하고, 이후 바로 결과 페이지로 연결될 수 있도록 리다이렉션을 설정하자. vote의 return을 HttpResponseRedirect(reverse('polls:result', args=(question.id,)))으로 변경한다.
아래의 코드는 Django Admin 페이지에서 Question이나 Choice의 내용을 편집할 때, 그 기능을 보완하기 위해서 몇가지 클래스들을 추가하여 커스터마이징한 코드이다.
from django.contrib import admin
from .models import Choice, Question
admin.site.register(Choice)
# Question 모델을 편집하면서 Choice 모델도 함께 편집할 수 있음
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3 # 각 Question마다 보여줄 Choice의 수
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
('질문 섹션', {'fields': ['question_text']}),
('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
# collapse : 필드의 내용을 감추거나 보일 수 있는 옵션이 존재
readonly_fields = ['pub_date'] #pub_date를 읽기 전용으로
inlines = [ChoiceInline]
admin.site.register(Question, QuestionAdmin) #등록
다음은 @admin.display 데코레이터를 사용하여, was_published_recently 메서드를 Django 어드민 페이지에서 사용할 수 있도록 커스터마이징 한 코드이다.
import datetime
from django.db import models
from django.utils import timezone
from django.contrib import admin
class Question(models.Model):
question_text = models.CharField(max_length=200, verbose='질문')
pub_date = models.DateTimeField(auto_now_add=True, verbose='생성일')
# @admin.display : 모델 필드가 표시되는 방식을 사용자가 지정할 수 있도록 하는 기능을 제공함
# boolean=True : 해당 필드를 boolean 값으로 표시
# description : 메서드의 레이블을 지정
@admin.display(boolean=True, description='최근생성(하루기준)')
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
def __str__(self):
return f'제목: {self.question_text}, 날짜: {self.pub_date}
from django.contrib import admin
from .models import Choice, Question
admin.site.register(Choice)
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 3
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
('질문 섹션', {'fields': ['question_text']}),
('생성일', {'fields': ['pub_date'], 'classes': ['collapse']}),
]
readonly_fields = ['pub_date']
inlines = [ChoiceInline]
list_filter = ['pub_date']
# list_filter : 리스트를 분류하는 필터를 만들어 줌
search_fields = ['question_text', 'choice__choice_text']
# search_fields : 검색옵션
admin.site.register(Question, QuestionAdmin)