해당 게시물은 점프 투 장고를 참고하여 작성하였습니다.
https://wikidocs.net/72281
내비게이션바란 메인페이지로 돌아갈 수 있는 장치를 의미한다. 내비게이션바는 모든 페이지에서 공통적으로 보여야 하므로 base.html 템플릿에 추가해야 한다.
(... 생략 ...)
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.min.js' %}"></script>
</body>
</html>
(생략)
<body>
<!-- 네비게이션바 -->
<nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'pybo:index' %}">Pybo</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">로그인</a>
</li>
</ul>
</div>
</div>
</nav>
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}
<!-- 기본 템플릿 안에 삽입될 내용 End -->
</body>
</html>
장고에는 템플릿의 특정 위치에 다른 템플릿을 삽입할 수 있는 include 태그가 있다. include 태그는 보통 템플릿에서 특정 영역이 반복적으로 사용될 경우 중복을 없애기 위해 사용한다. include를 이용하여 네비게이션바를 base.html 템플릿에 포함시킬 수 있다.
navbar.html 파일은 다른 템플릿들에서 중복되어 사용되지는 않지만 독립된 하나의 템플릿으로 관리하는 것이 유지 보수에 유리하므로 분리하였다.
projects\mysite\templates 하위에 navbar.html이라는 파일을 생성한 후, 위에서 사용했던 내비게이션바 코드를 삽입해준다. 이후 base.html을 다음과 같이 수정해준다.
(생략)
<body>
<!-- 네비게이션바 -->
{% include "navbar.html" %}
<!-- 기본 템플릿 안에 삽입될 내용 Start -->
{% block content %}
{% endblock %}
<!-- 기본 템플릿 안에 삽입될 내용 End -->
<!-- Bootstrap JS -->
<script src="{% static 'bootstrap.min.js' %}"></script>
</body>
</html>
우리가 구현한 질문 목록은 현재 페이징 처리가 안되기 때문에 게시물 300개를 작성하면 한 페이지에 300개의 게시물이 모두 표시된다.
페이징을 구현하기 전에 페이징을 테스트할 수 있을 정도로 충분한 데이터를 생성해야 한다. 대량의 테스트 데이터를 만드는 가장 좋은 방법은 장고셸을 이용하는 것이다.
1. 장고 셸 실행
python manage.py shell
>>> from pybo.models import Question
>>> from django.utils import timezone
>>> for i in range(300):
... q = Question(subject='테스트 데이터입니다:[%03d]' % i, content='내용무', create_date=timezone.now())
... q.save()
...
>>>
장고에서 페이징을 위해 사용하는 클래스는 Paginator이다. Paginator 클래스를 사용하여 다음과 같이 index 함수에 페이징 기능을 적용할 수 있다.
views.py를 다음과 같이 수정해준다.
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
def index(request):
page = request.GET.get('page', '1') # 페이지
question_list = Question.objects.order_by('-create_date')
paginator = Paginator(question_list, 10) # 페이지당 10개씩 보여주기 question_list는 게시물 전체를 의미
page_obj = paginator.get_page(page) # 요청된 페이지(page)에 해당되는 페이징 객체(page_obj)를 생성
context = {'question_list': page_obj}
return render(request, 'pybo/question_list.html', context)
(... 생략 ...)
위에서 질문 목록 템플릿(pybo/question_list.html)에 전달한 데이터는 question_list이다. 페이징 처리는 다음과 같이 할 수 있다.
(... 생략 ...)
</table>
<!-- 페이징처리 시작 -->
<ul class="pagination justify-content-center">
<!-- 이전페이지 -->
{% if question_list.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ question_list.previous_page_number }}">이전</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" tabindex="-1" aria-disabled="true" href="#">이전</a>
</li>
{% endif %}
<!-- 페이지리스트 -->
{% for page_number in question_list.paginator.page_range %}
{% if page_number == question_list.number %}
<li class="page-item active" aria-current="page">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% endif %}
{% endfor %}
<!-- 다음페이지 -->
{% if question_list.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ question_list.next_page_number }}">다음</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" tabindex="-1" aria-disabled="true" href="#">다음</a>
</li>
{% endif %}
</ul>
<!-- 페이징처리 끝 -->
<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
{% endblock %}
이전 페이지가 있는 경우에는 "이전" 링크가 활성화되게 하였고 이전 페이지가 없는 경우에는 "이전" 링크가 비활성화되도록 하였다. (다음페이지의 경우도 마찬가지 방법으로 적용되었다.) 그리고 페이지 리스트를 루프 돌면서 해당 페이지로 이동할 수 있는 링크를 생성하였다. 이때 현재 페이지와 같을 경우에는 active클래스를 적용하여 강조표시도 해 주었다.
페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만들 것이다. question_list.html 파일을 다음과 같이 수정한다.
(... 생략 ...)
<!-- 페이지리스트 -->
{% for page_number in question_list.paginator.page_range %}
{% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
{% if page_number == question_list.number %}
<li class="page-item active" aria-current="page">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a>
</li>
{% endif %}
{% endif %}
{% endfor %}
(... 생략 ...)
{% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
위 코드는 페이지 리스트가 현재 페이지 기준으로 좌우 5개씩 보이도록 만든다. |add:-5, |add:5 는 템플릿 필터로, |add:-5는 5만큼 빼라는 의미이고 |add:5는 5만큼 더하라는 의미이다.
템플릿 필터란 템플릿 태그에서 | 문자 뒤에 사용하는 필터를 말한다.
{{ form.subject.value|default_if_none:'' }}
위처럼 default_if_none과 같은 것들을 템플릿 필터라고 한다. 이와 같은 템플릿 필터를 직접 만들어 볼 것이다.
파이보 질문 목록 화면에는 모든 페이지에서 게시물 번호가 항상 1부터 시작된다는 오류가 있다.
만약 게시물 전체 건수가 12개라면 첫 번째 페이지는 번호가 12부터 3까지 역순으로 보여지고 두번째 페이지에는 2부터 1까지 보여야 한다.
번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1
시작 인덱스는 페이지당 시작되는 게시물의 시작 번호를 의미
예) 페이지당 게시물을 10건씩 보여준다 -> 1페이지의 시작 인덱스는 1, 2페이지의 시작 인덱스는 11
현재 인덱스는 페이지에 보여지는 게시물 개수만큼 0부터 1씩 증가되는 번호
예) 전체 게시물 개수가 12개이고 페이지당 10건씩 게시물을 보여 준다면 공식에 의해 1페이지의 번호는 12 - 1 - (0~9 반복) + 1 이 되어 12~3까지 표시됨
2페이지의 경우에는 12 - 11 - (0~1 반복) + 1 이 되어 2~1이 표시될 것이다.
템플릿에서 이 공식을 적용하려면 빼기 기능이 필요하다. 앞에서 더하기 필터(|add:5)를 사용한 것처럼 빼기 필터(|sub:3)가 있으면 좋겠지만 장고에는 빼기 필터가 없으므로 직접 만들어 줄 것이다.
템플릿 필터 파일을 저장할 templatetags 디렉터리를 다음과 같은 경로에 생성해준다.
projects\mysite\pybo\templatetags
(mysite) c:\projects\mysite>cd pybo
(mysite) c:\projects\mysite\pybo>mkdir templatetags
templatetags 디렉터리에 pybo_filter.py 파일을 생성하여 다음과 같이 작성한다.
from django import template
register = template.library()
@register.filter
def sub(value, arg):
return value - arg
템플릿 필터 함수를 만드는 방법은 무척 간단하다. 위처럼 sub 함수에 @register.filter 애너테이션을 적용하면 템플릿에서 해당 함수를 필터로 사용할 수 있게 된다. sub 함수는 기존 값 value에서 입력으로 받은 값 arg를 빼서 리턴하는 함수이다.
작성한 sub 필터를 템플릿에서 사용하기 위해서는 템플릿 상단에 다음처럼 {% load pybo_filter %}로 sub 필터를 저장한 파일(pybo_filter.py)을 먼저 로드해야 한다
템플릿 상단에 extends 문이 있을 경우 load문은 extends 문 다음에 위치해야 한다.
question_list.html 파일을 다음처럼 변경한다.
{% extends 'base.html' %}
{% load pybo_filter %}
{% block content %}
<div class="container my-3">
<table class="table">
<thead>
<tr class="table-dark">
<th>번호</th>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
{% if question_list %}
{% for question in question_list %}
<tr>
<td>
<!-- 번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1 -->
{{ question_list.paginator.count|sub:question_list.start_index|sub:forloop.counter0|add:1 }}
</td>
<td>
<a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a>
</td>
<td>{{ question.create_date }}</td>
</tr>
(... 생략 ...)
{% load pybo_filter %} 문을 상단에 적어주고 번호 부분을 위처럼 바꾸어 주었다. 상당히 복잡해 보이지만 번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1 의 공식이 적용된 모습이다.
질문 목록에 "해당 질문에 달린 답변 개수"를 표시할 수 있는 기능을 추가해 볼 것이다.
question_list.html 파일을 다음과 같이 수정한다.
(... 생략 ...)
<td>
<a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a>
{% if question.answer_set.count > 0 %}
<span class="text-danger small mx-2">{{ question.answer_set.count }}</span>
{% endif %}
</td>
<...>
{% if question.answer_set.count > 0 %}로 답변이 있는 경우를 검사하고, {{ question.answer_set.count }}로 답변 개수를 표시했다. 이제 답변이 있는 질문은 제목 오른쪽에 빨간색 숫자가 표시된다.
장고의 로그인, 로그아웃을 도와주는 앱은 django.contrib.auth 이다. 이 앱은 장고 프로젝트 생성시 config/settings.py에 다음처럼 자동으로 추가된다.
INSTALLED_APPS = [
(... 생략 ...)
'django.contrib.auth',
(... 생략 ...)
]
django.contrib.auth 앱을 이용하면 로그인과 로그아웃 기능을 정말 쉽게 구현할 수 있다.
하나의 웹 사이트에는 파이보와 같은 게시판 서비스 외에도 블로그나 쇼핑몰과 같은 굵직한 단위의 앱들이 함께 있을 수 있기 때문에 공통으로 사용되는 기능인 로그인이나 로그아웃 기능은
common 앱에 구현하는 것이 좋다.
(mysite) c:\projects\mysite>django-admin startapp common
(... 생략 ...)
INSTALLED_APPS = [
'common.apps.CommonConfig',
'pybo.apps.PyboConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
(... 생략 ...)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('pybo/', include('pybo.urls')),
path('common/', include('common.urls')),
]
http://localhost:8000/common/으로 시작하는 URL은 모두 common/urls.py 파일을 참조하게 된다.
app_name = 'common'
urlpatterns = []
로그인 화면으로 진입할 수 있도록 templates/navbar.html 파일의 '로그인' 링크를 다음처럼 수정한다.
(... 생략 ...)
<ul class="navbar-nav">
<li class="nav-item ">
<a class="nav-link" href="{% url 'common:login' %}">로그인</a>
</li>
</ul>
(... 생략 ...)
navbar.html 파일에서 템플릿 태그로 {% url 'common:login' %}를 사용했으므로 common/urls.py 파일에 다음과 같은 URL 매핑 규칙을 추가하자.
from django.urls import path
from django.contrib.auth import views as auth_views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
]
로그인 뷰는 따로 만들 필요없이 위 코드처럼 django.contrib.auth 앱의 LoginView를 사용하도록 설정했다.
LoginView가 common 디렉터리의 템플릿을 참조할 수 있도록 common/urls.py 파일을 다음과 같이 수정한다.
from django.urls import path
from django.contrib.auth import views as auth_views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login'),
]
common/login.html 파일을 생성하기 위해 common 템플릿 디렉터리를 다음과 같이 생성한다.
(mysite) c:\projects\mysite>cd templates
(mysite) c:\projects\mysite\templates>mkdir common
login.html 파일을 생성한 후 다음과 같이 작성한다.
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" action="{% url 'common:login' %}">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="mb-3">
<label for="username">사용자ID</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="password">비밀번호</label>
<input type="password" class="form-control" name="password" id="password"
value="{{ form.password.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">로그인</button>
</form>
</div>
{% endblock %}
사용자ID와 비밀번호를 입력으로 받아 로그인하는 템플릿이다. 로그인에 사용되는 사용자ID를 의미하는 username과 비밀번호를 의미하는 password 항목은 django.contrib.auth 앱이 요구하는 하는 필수항목이다.
그리고 {% csrf_token %} 바로 밑에 include 태그로 포함된 form_errors.html 템플릿 파일은 다음과 같이 작성한다. form_errors.html 템플릿은 로그인 실패시 로그인이 왜 실패했는지 알려주는 역할을 한다.
<!-- 필드 오류와 넌필드 오류를 출력한다. -->
{% if form.errors %}
<div class="alert alert-danger">
{% for field in form %}
<!-- 필드 오류 -->
{% if field.errors %}
<div>
<strong>{{ field.label }}</strong>
{{ field.errors }}
</div>
{% endif %}
{% endfor %}
<!-- 넌필드 오류 -->
{% for error in form.non_field_errors %}
<div>
<strong>{{ error }}</strong>
</div>
{% endfor %}
</div>
{% endif %}
폼 오류에는 다음과 같이 두 가지 종류의 오류가 있다.
필드 오류: 사용자가 입력한 필드 값에 대한 오류, 값이 누락되었거나 필드의 형식이 일치하지 않는 경우에 발생
넌필드 오류: 값과는 상관없는 이유로 발생하는 오류
아이디:admin, 비밀번호:1111을 입력하고 로그인을 수행하면 오류가 발생하는데, 오류가 발생한 이유는 django.contrib.auth 패키지는 로그인이 성공하면 디폴트로 /accounts/profile/ 이라는 URL로 이동시키기 때문이다.
로그인 성공 시 /accounts/profile/가 아닌 / 페이지로 이동할 수 있도록 config/settings.py 파일을 수정해야 한다. 마지막 줄에 다음 코드를 추가한다.
# 로그인 성공후 이동하는 URL
LOGIN_REDIRECT_URL = '/'
이제 http://localhost:8000/ 페이지에 대한 URL 매핑 규칙을 config/urls.py 파일에 작성해준다.
from django.contrib import admin
from django.urls import path, include
from pybo import views
urlpatterns = [
path('admin/', admin.site.urls),
path('pybo/', include('pybo.urls')),
path('common/', include('common.urls')),
path('', views.index, name='index'), # '/' 에 해당되는 path
]
로그인에 성공했지만, 내비게이션바에는 여전히 "로그인" 링크가 보인다. 로그인 후에는 '로그아웃'이 보이도록 변경할 것이다.
navbar.html 템플릿 파일에서 로그인 링크 부분을 다음과 같이 수정하자.
(... 생략 ...)
<li class="nav-item">
{% if user.is_authenticated %}
<a class="nav-link" href="{% url 'common:logout' %}">{{ user.username }} (로그아웃)</a>
{% else %}
<a class="nav-link" href="{% url 'common:login' %}">로그인</a>
{% endif %}
</li>
(... 생략 ...)
{% if user.is_authenticated %} 은 현재 사용자가 로그인 되었는지를 판별한다. 따라서 로그인이 되어 있으면(True) "로그아웃" 링크를 표시하고 로그인이 되어 있지 않다면 "로그인" 링크를 표시할 것이다.
로그아웃 링크가 추가되었으므로 {% url 'common:logout' %}에 대응하는 URL 매핑을 common/urls.py 파일에 추가해야 한다.
from django.urls import path
from django.contrib.auth import views as auth_views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]
로그아웃 시 리다이렉트할 위치도 config/settings.py 파일에 추가한다.
# 로그아웃시 이동하는 URL
LOGOUT_REDIRECT_URL = '/'
회원가입 기능 역시 장고의 django.contrib.auth 앱을 이용하면 쉽게 구현할 수 있다.
회원가입을 위한 링크를 navbar.html 템플릿에 추가한다.
<li>
{% if not user.is_authenticated %}
<a class="nav-link" href="{% url 'common:signup' %}">회원가입</a>
{% endif %}
</li>
로그인/로그아웃 바로 우측에 "회원가입" 링크를 추가했다. 회원가입은 로그아웃 상태에서만 보일수 있도록 했다.
navbar.html 템플릿에 {% url 'common:signup' %} 태그를 추가했으므로 이에 대응하는 URL 매핑 규칙을 추가해야 한다. common/urls.py 파일에 회원가입을 위한 URL 매핑 규칙을 추가한다.
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
app_name = 'common'
urlpatterns = [
path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('signup/', views.signup, name='signup'),
]
뷰 함수를 만들기 전에 계정생성시 사용할 UserForm을 common/forms.py 파일에 작성한다.
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
class UserForm(UserCreationForm):
email = forms.EmailField(label="이메일")
class Meta:
model = User
fields = ("username", "password1", "password2", "email")
UserForm은 django.contrib.auth.forms 모듈의 UserCreationForm 클래스를 상속하여 만들었다. 그리고 email 속성을 추가했다. UserForm을 따로 만들지 않고 UserCreationForm을 그대로 사용해도 되지만 위처럼 이메일 등의 속성을 추가하기 위해서는 UserCreationForm 클래스를 상속하여 만들어야 한다.
common/views.py 파일에 회원가입을 위한 signup 함수를 다음과 같이 작성한다.
from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect
from common.forms import UserForm
def signup(request):
if request.method == "POST":
form = UserForm(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
raw_password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password) # 사용자 인증
login(request, user) # 로그인
return redirect('index')
else:
form = UserForm()
return render(request, 'common/signup.html', {'form': form})
회원가입 화면을 구성하는 common/signup.html 템플릿을 다음과 같이 작성한다.
{% extends "base.html" %}
{% block content %}
<div class="container my-3">
<form method="post" action="{% url 'common:signup' %}">
{% csrf_token %}
{% include "form_errors.html" %}
<div class="mb-3">
<label for="username">사용자 이름</label>
<input type="text" class="form-control" name="username" id="username"
value="{{ form.username.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="password1">비밀번호</label>
<input type="password" class="form-control" name="password1" id="password1"
value="{{ form.password1.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="password2">비밀번호 확인</label>
<input type="password" class="form-control" name="password2" id="password2"
value="{{ form.password2.value|default_if_none:'' }}">
</div>
<div class="mb-3">
<label for="email">이메일</label>
<input type="text" class="form-control" name="email" id="email"
value="{{ form.email.value|default_if_none:'' }}">
</div>
<button type="submit" class="btn btn-primary">생성하기</button>
</form>
</div>
{% endblock %}
form 태그 밑에는 오류를 표시하기 위해 form_errors.html 템플릿을 include 했다. 그리고 UserForm의 속성인 사용자이름, 비밀번호1, 비밀번호2, 이메일에 해당되는 필드들을 form 항목으로 추가했다.
게시판의 글에 누가 글을 작성했는지 알 수 있도록 '글쓴이'를 추가해 볼 것이다.
Question과 Answer 모델이 '글쓴이'에 해당되는 author 속성을 추가한다.
pybo\models.py로 들어가 Question 모델에 author 속성을 추가한다.
from django.db import models
from django.contrib.auth.models import User
class Question(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
# User 모델은 django.contrib.auth 앱이 제공하는 사용자 모델로 회원 가입시 데이터 저장에 사용했던 모델이다.
(... 생략 ...)
앞서 살펴봤듯이 모델을 변경한 후에는 반드시 makemigrations와 migrate를 통해 데이터베이스를 변경해 주어야 한다.
python manage.py makemigrations
위 코드를 실행하면 option을 선택하라는 메세지가 뜨는데, 이는 Question 모델에 author를 추가하면 이미 등록되어 있던 게시물에 author에 해당되는 값이 저장되어야 하는데, 장고는 author에 어떤 값을 넣어야 하는지 모르기 때문에 메세지가 뜨는 것이다.
이후 1을 입력하여 기존 게시물에 추가될 author에 강제로 임의 계정 정보를 추가하는 방법을 사용한다.
python manage.py migrate
Question 모델과 같은 방법으로 Answer 모델에 author 속성을 추가해준다.
(... 생략 ...)
class Answer(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
(... 생략 ...)
python manage.py makemigrations
python manage.py migrate
Question, Answer 모델에 author 속성이 추가되었으므로 질문과 답변 저장시에 author도 함께 저장해야 한다.
pybo\views.py의 answer_create 함수를 다음과 같이 수정해준다.
def answer_create(request, question_id):
(... 생략 ...)
if form.is_valid():
answer = form.save(commit=False)
answer.author = request.user # author 속성에 로그인 계정 저장
(... 생략 ...)
(... 생략 ...)
request.user는 현재 로그인한 계정의 User 모델 객체이다.
question_create 함수도 동일하게 수정해준다.
def question_create(request):
(... 생략 ...)
if form.is_valid():
question = form.save(commit=False)
question.author = request.user # author 속성에 로그인 계정 저장
(... 생략 ...)
로그아웃 상태에서 질문 또는 답변을 등록하면 ValueError가 발생한다.
request.user에는 로그아웃 상태이면 AnonymousUser 객체가, 로그인 상태이면 User 객체가 들어있는데, 앞에서 author 속성을 정의할 때 User를 이용하도록 했다. 그래서 answer.author = request.user에서 User 대신 AnonymousUser가 대입되어 오류가 발생한 것이다.
request.user를 사용하는 함수에 @login_required 애너테이션을 사용해야 한다. 애너테이션이 붙은 함수는 로그인이 필요한 함수를 의미한다. answer_create 함수와 question_create 함수는 함수내에서 request.user를 사용하므로 로그인이 필요한 함수이므로 다음과 같이 @login_required 어노테이션을 사용해야 한다.
from django.shortcuts import render, get_object_or_404, redirect
from django.utils import timezone
from .models import Question
from .forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
(... 생략 ...)
@login_required(login_url='common:login')
def answer_create(request, question_id):
(... 생략 ...)
@login_required(login_url='common:login')
def question_create(request):
(... 생략 ...)
로그아웃 상태에서 '질문 등록하기'를 눌러 로그인 화면으로 전환된 상태에서 웹 브라우저 주소창의 URL을 보면 next 파라미터가 있을 것이다. 이는 로그인 성공 후 next 파라미터에 있는 URL로 페이지를 이동하겠다는 의미인데, 지금은 그렇게 되고 있지 않다. 따라서 로그인 후 next 파라미터에 있는 URL로 페이지를 이동하려면 로그인 템플릿에 다음과 같이 hidden 타입의 next 항목을 추가해야 한다.
templates\common\login.html 파일을 다음과 같이 수정해준다.
(... 생략 ...)
<form method="post" action="{% url 'common:login' %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ next }}"> <!-- 로그인 성공후 이동되는 URL -->
{% include "form_errors.html" %}
(... 생략 ...)
게시판의 게시물에는 "글쓴이"를 표시하는 것이 일반적이다. 질문 목록, 질문 상세 화면에 author 속성을 이용하여 글쓴이를 표시해 보자.
질문 목록 템플릿에 글쓴이를 표시하기 위해 다음과 같이 templates\pybo\question_list.html 파일에 글쓴이 항목을 추가한다.
(... 생략 ...)
<tr class="text-center table-dark">
<th>번호</th>
<th style="width:50%">제목</th>
<th>글쓴이</th>
<th>작성일시</th>
</tr>
(... 생략 ...)
글쓴이 항목을 추가했고, th 엘리먼트를 가운데 정렬하도록 tr 엘리먼트에 text-center 클래스를 추가하고 제목의 너비가 전체에서 50%를 차지하도록 style="width:50%"도 지정해 주었다.
이어서 for 문에도 다음처럼 글쓴이를 적용한다.
(... 생략 ...)
{% for question in question_list %}
<tr class="text-center">
<td>
<!-- 번호 = 전체건수 - 시작인덱스 - 현재인덱스 + 1 -->
{{ question_list.paginator.count|sub:question_list.start_index|sub:forloop.counter0|add:1 }}
</td>
<td class="text-start">
<a href="{% url 'pybo:detail' question.id %}">{{ question.subject }}</a>
{% if question.answer_set.count > 0 %}
<span class="text-danger small mx-2">{{ question.answer_set.count }}</span>
{% endif %}
</td>
<td>{{ question.author.username }}</td> <!-- 글쓴이 추가 -->
<td>{{ question.create_date }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4">질문이 없습니다.</td>
</tr>
(... 생략 ...)
{{ question.user.username }}를 삽입하여 질문의 글쓴이를 표시했고, 테이블 내용을 가운데 정렬하도록 tr 엘리먼트에 text-center 클래스를 추가하고, 제목을 왼쪽 정렬하도록 text-start 클래스를 추가했다. 테이블 항목도 3개에서 4개로 늘었으므로 colspan도 3에서 4로 수정했다.
질문 상세 템플릿(question_detail.html)도 다음과 같이 글쓴이를 추가한다.
(... 생략 ...)
<!-- 질문 -->
<h2 class="border-bottom py-2">{{ question.subject }}</h2>
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ question.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">{{ question.author.username }}</div>
<div>{{ question.create_date }}</div>
</div>
</div>
</div>
(... 생략 ...)
글쓴이와 작성일시가 함께 보이도록 수정했다. 그리고 부트스트랩을 이용하여 여백과 정렬 등의 디자인도 살짝 변경했다.
답변 부분도 글쓴이를 다음처럼 추가한다.
(... 생략 ...)
<!-- 답변 -->
<h5 class="border-bottom my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다.</h5>
{% for answer in question.answer_set.all %}
<div class="card-body">
<div class="card-text" style="white-space: pre-line;">{{ answer.content }}</div>
<div class="d-flex justify-content-end">
<div class="badge bg-light text-dark p-2 text-start">
<div class="mb-2">{{ answer.author.username }}</div>
<div>{{ answer.create_date }}</div>
</div>
</div>
</div>
(... 생략 ...)