[Django] (점프투장고) 카테고리 기능 추가

azzurri21·2022년 1월 12일
0

Django

목록 보기
2/7

점프투장고 3-16 파이보 추가 기능 중 카테고리 기능을 추가하는 과정이다.


요약

카테고리 기능 도입에 의한 코드의 변화는 대략 아래와 같다.

  • Model
    • 카테고리 모델(Category) 생성
    • 질문 모델(Question)의 외래키로 카테고리 모델 추가
  • Template
    • 질문 목록 화면(question_list.html)에 사이드바 및 카테고리 선택 버튼 추가
  • URL
    • 질문 목록(pybo:index), 질문 생성(pybo:question_create) url의 매개변수로 문자열 형식의 category_name을 추가
  • View
    • 질문 목록 뷰(base_views.index) 함수에서 질문 추출 쿼리에 카테고리 필터를 추가 및 템플릿에 카테고리 전체와 현재 카테고리를 전달
    • 질문 생성 뷰(question_views.question_create) 함수에서 url에 포함된 카테고리 정보를 질문 인스턴스에 저장
    • 질문 생성, 수정 뷰(question_create, question.modify) 함수에서 question_form.html을 render할 때 카테고리 변수를 전달

Model

책 저자분께서 운영 중인 질문 사이트 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을 입력한다.

Template

실제 운영 중인 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는 현재 카테고리의 인스턴스이다.


완성된 화면은 위와 같다.

URL

[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에 카테고리 이름을 포함하여 새로 생성된 질문의 카테고리 속성에 해당하는 인스턴스를 할당해준다.

View

질문 목록 View

[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)

변경점은 크게 두 가지이다.

  1. view의 매개변수에 카테고리 이름을 추가한다. default값은 'qna'(질문과답변)이다. url이 question/list/로 redirect되는 경우 default값이 category 변수에 입력된다.

  2. 질문 목록 추출 시에 카테고리 정보를 포함시킨다.

  3. 템플릿에 전달하는 context에 카테고리 변수를 추가한다. category_list는 전체 카테고리 인스턴스 queryset으로, 질문 목록 화면에 모든 카테고리를 출력하기 위해 전달한다. category는 현재 카테고리 인스턴스이다.

질문 생성 View

[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>

화면의 제목을 나타내는 위 코드를 아래와 같이 수정하여 현재 생성/수정 중인 질문의 카테고리를 명시한다.

완성된 질문 생성/수정 화면은 위와 같다.

profile
파이썬 백엔드 개발자

3개의 댓글

comment-user-thumbnail
2022년 4월 28일
  1. migration 에서 기존 인스턴스의 category_id 속성값으로 1을 입력한다. 는 어디에 입력하는 것인가요,,? 감사합니다.
  2. 템플릿 HTML 의 기존내용은 어떤 내용을 말씀하시는 것인가요?
    감사합니다..
답글 달기
comment-user-thumbnail
2022년 8월 22일

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


6

7

8
9
10

11 {% for cat in category_list %}
12 {% if cat == category %}
13 <a class="rounded-0 list-group-item list-group-item-action list-group-item-light active"
14 href="{{ cat.get_absolute_url }}">{{ cat.description }}
15 {% else %}
16 <a class="rounded-0 list-group-item list-group-item-action list-group-item-light"
17 href="{{ cat.get_absolute_url }}">{{ cat.description }}
18 {% endif %}
19 {% endfor %}
20

21
22

23 <div class="col-sm-12

뭐가 문제인지 에러가 납니다.

답글 달기
comment-user-thumbnail
2022년 12월 18일

안녕하세요, 좋은 포스팅 감사합니다. 저도 같은 주제로 고생하고 있는데요
님이 작성하신 def question_create(request, category_name) 에 질문이 있어요

님과 동일하게 작성했는데, 게시판에서 질문등록하기를 클릭하면
category_name을 못 찾는 문제가 생겨요...혹시 소스 코드가 어떻게 되는지 보여주실 수 있으세요?

답글 달기