점프투장고 3-16 파이보 추가 기능 중 카테고리 기능을 추가하는 과정이다.
카테고리 기능 도입에 의한 코드의 변화는 대략 아래와 같다.
category_name
을 추가책 저자분께서 운영 중인 질문 사이트 pybo의 답변을 참고하여 Category 모델을 구현했다. 질문 목록을 출력할 때는 반드시 카테고리 인스턴스가 필요하므로 get_absolute_url 메서드를 추가로 정의했다.
[mysite\pybo\models.py]
class Category(models.Model):
name = models.CharField(max_length=20, unique=True)
description = models.CharField(max_length=200, null=True, blank=True)
has_answer = models.BooleanField(default=True) # 답변가능 여부
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('pybo:index', args=[self.name])
class Question(models.Model):
(... 생략 ...)
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='category_question')
Django Shell에 아래와 같이 입력하여 두 개의 카테고리 인스턴스를 생성한다.
>>> from pybo.models import Category
>>> c = Category(name='qna', description='질문과답변')
>>> c.save()
>>> c = Category(name='free', description='자유게시판')
>>> c.save
우선 Category 클래스를 정의하고 migration한 뒤에 인스턴스를 아래와 같이 생성한다. Question 모델의 속성을 추가하고서 기존 인스턴스의 category_id 속성값으로 1을 입력한다.
실제 운영 중인 pybo 사이트의 소스를 참고하여 카테고리 선택 사이드바와 버튼을 추가했다.
[mysite\templates\pybo\question_list.html]
<div class="container my-3">
<div class="row">
<div class="col-sm-12 col-md-3 col-lg-2 p-2">
<!-- Sidebar -->
<nav id="sidebar" class="border-top border-secondary">
<div class="list-group">
{% for cat in category_list %}
{% if cat == category %}
<a class="rounded-0 list-group-item list-group-item-action list-group-item-light active"
href="{{ cat.get_absolute_url }}">{{ cat.description }}</a>
{% else %}
<a class="rounded-0 list-group-item list-group-item-action list-group-item-light"
href="{{ cat.get_absolute_url }}">{{ cat.description }}</a>
{% endif %}
{% endfor %}
</div>
</nav>
</div>
<div class="col-sm-12 col-md-9 col-lg-10 p-2">
<!-- Content -->
<div id="content" class="border-top border-secondary">
<div class="content_block">
<h5 class="border-bottom pl-2 pb-3 my-2">{{ category.description }}</h5>
<div class="row justify-content-between my-3">
(... 기존 내용 ...)
</div>
(... 기존 내용 ...)
</div>
</div>
</div>
</div>
</div>
category_list와 category는 view에서 템플릿에 전달하는 값이다. category_list는 모든 카테고리 인스턴스의 QuerySet이고, category는 현재 카테고리의 인스턴스이다.
완성된 화면은 위와 같다.
[mysite\pybo\urls.py]
(... 생략 ...)
urlpatterns = [
# base_views.py
path('question/list/', base_views.index, name='index'),
path('question/list/<str:category_name>/', base_views.index, name='index'),
path('question/detail/<int:question_id>/', base_views.detail, name='detail'),
# question_views.py
path('question/craete/<str:category_name>/', question_views.question_create, name='question_create'),
(... 생략 ...)
기능 차이는 없지만, url의 일관성을 위해 index와 detail의 앞에
question/
을 추가했다.
url에 카테고리 이름을 포함하여 질문 목록 출력 시 카테고리를 반영하도록 했다.
카테고리 이름이 포함되지 않은 url을 유지한다. 그리하여 매개변수로 카테고리 이름이 전달되지 않은 경우(ex. redirect('pybo:index')
, <a href="{% url 'index' %}"> ... </a>
)에도 코드수정 없이 정상 작동하게 된다. 이후 view 파트에서 category_name의 default 값으로 'qna'를 지정한다.
카테고리 이름 전달 없이 질문 목록으로 redirect되는 경우는 네비게이션 바의 'Pybo' 버튼을 클릭할 때와 회원가입 이후 연결되는 때이다.
질문 생성 시에도 url에 카테고리 이름을 포함하여 새로 생성된 질문의 카테고리 속성에 해당하는 인스턴스를 할당해준다.
[mysite\pybo\views\base_views.py]
def index(request, category_name='qna'):
'''
pybo 목록 출력
'''
# 입력 파라미터
page = request.GET.get('page', '1') # 페이지
kw = request.GET.get('kw', '') # 검색어
so = request.GET.get('so', 'recent') # 정렬기준
category_list = Category.objects.all()
category = get_object_or_404(Category, name=category_name)
question_list = Question.objects.filter(category=category)
# 정렬
if so == 'recommend':
# aggretation, annotation에는 relationship에 대한 역방향 참조도 가능 (ex. Count('voter'))
question_list = question_list.annotate(num_voter=Count('voter')).order_by('-num_voter', '-create_date')
elif so == 'popular':
question_list = question_list.annotate(num_answer=Count('answer')).order_by('-num_answer', '-create_date')
else:
question_list = question_list.order_by('-create_date')
# 검색
if kw:
question_list = question_list.filter(
Q(subject__icontains=kw) | # 질문 제목검색
Q(content__icontains=kw) | # 질문 내용검색
Q(answer__content__icontains=kw) | # 답변 내용검색
Q(author__username__icontains=kw) | # 질문 작성자검색
Q(answer__author__username__icontains=kw) # 답변 작성자검색
).distinct()
# 페이징처리
paginator = Paginator(question_list, 10) # 페이지당 10개식 보여주기
page_obj = paginator.get_page(page)
max_index = len(paginator.page_range)
context = {'question_list': page_obj, 'max_index': max_index, 'page': page, 'kw': kw, 'so': so,
'category_list': category_list, 'category': category}
return render(request, 'pybo/question_list.html', context)
변경점은 크게 두 가지이다.
view의 매개변수에 카테고리 이름을 추가한다. default값은 'qna'(질문과답변)이다. url이 question/list/
로 redirect되는 경우 default값이 category 변수에 입력된다.
질문 목록 추출 시에 카테고리 정보를 포함시킨다.
템플릿에 전달하는 context에 카테고리 변수를 추가한다. category_list는 전체 카테고리 인스턴스 queryset으로, 질문 목록 화면에 모든 카테고리를 출력하기 위해 전달한다. category는 현재 카테고리 인스턴스이다.
[mysite\pybo\views\question_views.py]
@login_required(login_url='common:login')
def question_create(request, category_name):
"""
pybo 질문등록
"""
category = Category.objects.get(name=category_name)
if request.method == 'POST':
form = QuestionForm(request.POST)
if form.is_valid():
question = form.save(commit=False)
question.author = request.user # author 속성에 로그인 계정 저장
question.create_date = timezone.now()
question.category = category
question.save()
return redirect(category)
else: # request.method == 'GET'
form = QuestionForm()
context = {'form': form, 'category': category}
return render(request, 'pybo/question_form.html', context)
@login_required(login_url='common:login')
def question_modify(request, question_id):
(... 생략 ...)
context = {'form': form, 'category': question.category}
return render(request, 'pybo/question_form.html', context)
question.category = category
를 통해 질문 인스턴스에 카테고리 인스턴스를 연결해준다. 질문 생성, 수정 시에 화면에 카테고리 종류를 출력하기 위해 question_form 템플릿에 카테고리 인스턴스를 전달했다.
[mysite\templates\pybo\question_form.html]
<h5 class="my-3 border-bottom pb-2">질문등록</h5>
<h5 class="my-3 border-bottom pb-2">[{{ category.description }}] 질문 등록</h5>
화면의 제목을 나타내는 위 코드를 아래와 같이 수정하여 현재 생성/수정 중인 질문의 카테고리를 명시한다.
완성된 질문 생성/수정 화면은 위와 같다.
NoReverseMatch at /pybo/
Reverse for 'index' with arguments '('qna',)' not found. 1 pattern(s) tried: ['pybo/\Z']
Request Method: GET
Request URL: http://127.0.0.1:8000/pybo/
Django Version: 4.1
Exception Type: NoReverseMatch
Exception Value:
Reverse for 'index' with arguments '('qna',)' not found. 1 pattern(s) tried: ['pybo/\Z']
Exception Location: C:\Users\Dongs\anaconda3\envs\mysite\lib\site-packages\django\urls\resolvers.py, line 803, in _reverse_with_prefix
Raised during: pybo.views.base_views.index
Python Executable: C:\Users\Dongs\anaconda3\envs\mysite\python.exe
Python Version: 3.10.4
Python Path:
['C:\projects\mysite',
'C:\Users\Dongs\anaconda3\envs\mysite\python310.zip',
'C:\Users\Dongs\anaconda3\envs\mysite\DLLs',
'C:\Users\Dongs\anaconda3\envs\mysite\lib',
'C:\Users\Dongs\anaconda3\envs\mysite',
'C:\Users\Dongs\anaconda3\envs\mysite\lib\site-packages']
Server time: Mon, 22 Aug 2022 14:16:34 +0000
Error during template rendering
In template C:\projects\mysite\templates\pybo\question_list.html, error at line 14
Reverse for 'index' with arguments '('qna',)' not found. 1 pattern(s) tried: ['pybo/\Z']
4
5
뭐가 문제인지 에러가 납니다.
감사합니다..