장고(Django) - 뷰(Views)와 템플릿(Templates)
뷰(Views)와 템플릿(Templates)
polls/urls.py
이전 강의에서 urls.py의 path를 다음과 같이 작성하였다. 여기에서 views.py의 index에 Question을 받아오는 작업을 하려고 한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('some_url', views.some_url)
]
polls/views.py
index 메서드를 다음과 같이 설정하여 질문 내용을 받아오도록 하였다.
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = ' ,'.join([q.question_text for q in latest_question_list])
return HttpResponse(context)
latest_question_list를 받아오는 과정을 쿼리로 출력해 보았다. 내림차순으로 정렬하여 5개를 받아오는 것이다.
print(Question.objects.order_by('-pub_date')[:5].query)
# result
# SELECT "polls_question"."id", "polls_question"."question_text", "polls_question"."pub_date"
# FROM "polls_question" ORDER BY "polls_question"."pub_date" DESC LIMIT 5
템플릿(Templates)
views.py의 index를 실행할 때, 단지 텍스트를 출력하는 게 아니라 html을 적용하려고 한다. 그러기 위해서는 polls 내에 templates 폴더를 생성하고, templates 안에 polls를 생성하고, polls 안에 index.html를 생성한다. 그리고 간단히 확인하기 위해 index.html을 작성하고, index 메서드에서 render를 사용하여 받아오도록 한다. 그러면 index.html을 정상적으로 받아올 수 있다.
<!-- index.html -->
<ul>
<li>text</li>
</ul>
# views.py
from django.shortcuts import render
def index(request):
return render(request, 'polls/index.html')
또한 아래처럼 views.py에서 변수를 전달해 index.html에서 활용할 수도 있다. {{변수}}처럼 중괄호로 두 번 묶어주면 해당 변수를 활용하여 페이지에 표시할 수 있다.
<!-- index.html -->
<ul>
<li>{{first_question}}</li>
</ul>
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)
템플릿에서 제어문 사용하기
템플릿 내에서 반복문과 조건문을 사용하는 법을 알아보자.
반복문
템플릿 내에서 반복문을 사용하기 위해서는 {% %} 형식으로 묶어 사용하고, 그 안에는 python처럼 for 문을 사용한다. 끝낼 때는 사이에 endfor을 넣어주면 된다. 템플릿의 반복문으로 모든 question_text를 출력하였다.
<ul>
{% for q in questions %}
<li>{{q}}</li>
{% endfor %}
</ul>
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
context = {'questions' : latest_question_list}
return render(request, 'polls/index.html', context)
조건문
반복문과 사용법은 똑같다. 아래의 코드는 questions가 존재하지 않으면, 'no question'이라는 문구를 출력하도록 하였다.
{% if questions %}
<ul>
{% for q in questions %}
<li>{{q}}</li>
{% endfor %}
</ul>
{% else %}
<p>no question</p>
{% endif %}
상세(Detail) 페이지 만들기
polls/<id> 형태의 url을 접속할 때, 질문과 답변을 볼 수 있도록 하는 실습을 진행한다.
polls/urls.py
urls.py에 새로운 path를 생성한다. '<int:question_id>'의 형식으로 int 형의 인자를 활용할 수 있다는 것을 알아두자. url에 적힌 이 인자를 views.py에 넘겨 활용한다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<int:question_id>/', views.detail, name='detail'),
path('some_url', views.some_url)
]
polls/views.py
views.py에 detail 메서드를 생성하고, question_id를 활용하여 render의 인자로 넘겨 템플릿에서 사용할 수 있도록 한다.
def detail(request, question_id):
question = Question.objects.get(id=question_id)
return render(request, 'polls/detail.html', {'question' : question})
templates/polls/detail.html
detail.html에서는 question_text와 질문에 대한 답변을 출력하도록 작성하였다. templates에서는 all 뒤에 '()' 괄호를 붙혀주지 않고 실행해야 정상적으로 작동한다는 것을 알아두자.
<h1>{{question.question_text}}</h1>
<ul>
{% for c in question.choice_set.all %}
<li>{{c.choice_text}}</li>
{% endfor %}
</ul>
상세(Detail) 페이지로 링크 추가하기
원래 질문들이 표시됐던 페이지에서 해당 질문의 id에 맞는 페이지에 접속할 수 있도록 링크를 추가하는 실습을 진행한다.
polls/urls.py
여기서 app_name은 polls로 한정된 것이 아니라 마음대로 정의하여 지정할 수 있다.
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('some_url', views.some_url)
]
templates/polls/index.html
<a> 태그를 활용해 링크를 달고, 주소를 설정해주어야 한다. 주석 처리한 것처럼 전체 경로를 작성해도 되지만, {% url 'path의 name' 인자 %}와 같은 형식으로도 접근이 가능하다. 만약 name이 같은 경우가 존재하므로 app_name 변수를 활용한다. app_name이 존재하지 않을 경우 'detail'이라고만 적어도 되지만, app_name이 존재할 경우 'polls'와 같이 적어주지 않으면 에러가 발생하니 주의해야 한다.
{% if questions %}
<ul>
{% for q in questions %}
<!-- <a href="/polls/q.id">{{q.question_text}}</a>와 같다. -->
<li><a href="{% url 'polls:detail' q.id %}">{{q.question_text}}</a></li>
{% endfor %}
</ul>
{% else %}
<p>no question</p>
{% endif %}
404 에러 처리하기
404 메시지는 사용자가 잘못된 요청을 보내는 경우 서버에서 보내는 것이다. 그러나 polls/<id> url에서 id에 올바르지 않은 값을 넣을 경우 404가 아닌 500 에러 상태 코드가 발생한다. 그 이유는 detail의 get 메서드는 존재하지 않은 id를 찾으려고 하면 에러를 발생해 500 코드를 발생시키기 때문이다.
try-except / Http404
404 코드로 바꾸기 위해 try-except 구문을 활용하였다.
from django.http import Http404
def detail(request, question_id):
try:
question = Question.objects.get(id=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question' : question})
get_object_or_404
해당 부분은 Django의 shortcuts에서 제공하고 있다. 그래서 위의 try-except 구문 말고도, get_object_or_404를 활용하여 더 짧은 코드로 올바른 결과를 얻을 수 있다.
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})
장고(Django) - 폼(Forms)과 커스터마이징(Customizing)
폼(Forms)
Question과 Choice를 활용해서 사용자에게 투표를 받는 기능을 만드는 실습을 진행한다.
polls/views.py
detail 메서드와 똑같이 question을 받아오고, POST 메서드로 이름이 'choice'인 value를 받아온다. detail.html을 확인하면 알겠지만, value는 choice의 id이므로 selected_choice는 사용자가 선택한 답변이 저장된다. 만약 아무 선택도 하지 않을 경우를 대비해 try-except 구문으로 예외 처리를 실시하였다. 그렇지 않은 경우에는 투표수를 하나 증가시키고 저장하며, index 페이지로 이동시킨다.
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()
return HttpResponseRedirect(reverse('polls:index'))
polls/urls.py
vote의 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')
]
templates/polls/detail.html
<form> 태그를 활용하여 투표를 진행할 페이지를 만든다. 만약 error_message가 존재할 경우(선택하지 않았을 경우) error_message를 출력한다. 반복문에서는 모든 질문을 radio type으로 생성하고, forloop.counter를 사용하여 id 지정 및 label의 for를 지정한다. 제출을 하면, views.py의 vote에 c.id가 전달된다.
<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 c in question.choice_set.all %}
<input type="radio" name="choice" id="choice{{forloop.counter}}" value="{{c.id}}">
<label for="choice{{forloop.counter}}">{{c.choice_text}}</label>
<br>
{% endfor %}
<input type="submit" value="Vote">
</form>
에러 방어하기 (1)
위의 실습에서 except 구문에 DoesNotExist 에러를 추가해 준 이유는 무엇인지 알아보자. 입력이 없을 경우에는 KeyError가 발생한다. 그러나 DoesNotExist는 해당 답변이 없을 때 발생하게 된다. 이 경우는 해당 페이지에 접속해서 답변을 제출할 수 있지만, 그 사이에 데이터베이스에서 해당 답변이 사라진 경우이다. 그러면 DoesNotExist가 발생하게 된다. 따라서 DoesNotExist도 같이 예외 처리를 해준 것이다.
에러 방어하기 (2)
만약 두 사람이 동시에 같은 답변을 선택한다고 하자. 그러면 같은 votes 값을 가진 choice가 동시에 증가 및 저장되어 두 명이 투표를 했지만, 1이 증가하게 된다. 이 경우를 막기 위해 서버가 아닌 데이터베이스에서 직접 값을 증가시키는 것이다.
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()
return HttpResponseRedirect(reverse('polls:index'))
F
F 메서드를 사용하면 서버가 아닌 데이터베이스에서 값을 가져온다. 동시에 접근하는 것은 많은 사용자가 사용할 때 발생할 수 있으므로 지금 살펴볼 수는 없다. 그러나 이러한 상황이 발생할 수도 있다는 것을 기억해 두자.
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': '선택이 없습니다'})
else:
selected_choice.votes = F('votes') + 1
selected_choice.save()
return HttpResponseRedirect(reverse('polls:index'))
결과(result) 조회 페이지
투표 후에 결과를 확인할 수 있는 페이지로 이동하도록 작성하는 실습을 진행한다.
polls/urls.py
result 페이지를 조회하기 위해 페이지 하나를 생성한다.
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/views.py
result 페이지를 조회하기 위해 페이지 하나를 생성한다. 또한 투표 후에 결과 페이지를 확인하도록 vote의 반환 페이지를 result로 변경하였다. 인자로 넘기는 args는 result에 필요한 인자를 넘겨준 것으로 ','를 같이 넣어줘야 한다.
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 = F('votes') + 1
selected_choice.save()
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})
templates/polls/result.html
결과를 확인할 수 있도록 질문 제목과 답변, 투표수를 출력한다.
<h1>{{question.question_text}}</h1>
{% for c in question.choice_set.all %}
<label>{{c.choice_text}} -- {{c.votes}}</label>
<br>
{% endfor %}
Django Admin 편집 페이지 커스터마이징
Class QuestionAdmin 생성(polls/admin.py)
- fieldsets : 편집 화면에서 표시되는 순서/이름 정의
- "'classes' : ['collapse']" : 해당 섹션 숨김/표시가 가능
- readonly_fields : 읽기 전용으로 변경(수정 불가)
- inlines : Question과 Choice를 함께 수정할 수 있게 함
- TabularInline : 필드가 한 줄에 펼쳐서 나옴
- StackedInline : 필드가 아래로 이어짐
- extra : 각 Question마다 보여줄 Choice의 수 지정
from django.contrib import admin
from .models import *
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 1
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
('질문', {'fields': ['question_text']}),
('생성일', {'fields' : ['pub_date'], 'classes' : ['collapse']}),
]
readonly_fields = ['pub_date']
inlines = [ChoiceInline]
# Register your models here.
admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice)
Django Admin의 목록 페이지 커스터마이징
polls/admin.py
- list_display : 목록에 표시되는 이름을 정의
- list_filter, search_fields : Question을 검색 및 정렬할 수 있도록 도와줌
from django.contrib import admin
from .models import *
class ChoiceInline(admin.TabularInline):
model = Choice
extra = 1
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']
# Register your models here.
admin.site.register(Question, QuestionAdmin)
polls/models.py
- verbose_name : 목록에 표시되는 이름을 정의
- @admin_display : boolean과 description으로 type과 이름 정의
from django.db import models
from django.utils import timezone
import datetime
from django.contrib import admin
# Create your models here.
class Question(models.Model):
question_text = models.CharField(max_length=200, verbose_name='질문')
pub_date = models.DateTimeField(auto_now_add=True, verbose_name='생성일')
def __str__(self):
if self.was_published_recently():
new_badge = 'NEW!!!'
else:
new_badge = ''
return f'{new_badge} 제목: {self.question_text}, 날짜: {self.pub_date}'
@admin.display(boolean=True, description='최근 생성(하루 기준)')
def was_published_recently(self):
return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=2000)
votes = models.IntegerField(default=0)
def __str__(self):
return f'질문 : {self.question.question_text}, 답 : {self.choice_text}'
결론
느낀 점
거의 모든 것을 라이브러리와 메서드로 처리할 수 있다는 것을 느꼈고, 기능을 많이 알면 알수록 더 좋은 웹을 구현할 수 있을 것 같다는 생각이 들었다. 그리고 데이터베이스의 값을 페이지에서 변경하는 작업을 처음 해봤는데, 직접 값을 변경할 수 있다는 게 신기했다.
어려웠던 점
html에 아직 익숙지 않아서 form을 구현하는 강의를 들을 때 이해가 100% 된 것 같지 않다. 다음에 form 관련 내용을 더 공부해야 할 것 같다.
'[프로그래머스] 데이터 엔지니어링 데브코스 3기 > TIL(Today I Learn)' 카테고리의 다른 글
[TIL - 14일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (4) (0) | 2024.04.11 |
---|---|
[TIL - 13일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (3) (0) | 2024.04.10 |
[TIL - 11일 차] 파이썬 장고 프레임웍을 사용해서 API 서버 만들기 (1) (0) | 2024.04.08 |
[TIL - 10일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (5) (0) | 2024.04.05 |
[TIL - 9일 차] 데이터 엔지니어링 : 파이썬으로 웹 데이터를 크롤하고 분석하기 (4) (0) | 2024.04.04 |