Computer Science/BackEnd

점프 투 장고 #3-3 | 파이보 서비스 개발하기(views.py 분리, 추천, 앵커, 마크다운, 검색, 파이보 추가 기능)

토마토. 2022. 8. 23. 14:41
views.py 파일 분리하기

 

지금까지는 장고에 페이지가 추가될 때마다 이에 맞는 view 함수를 views.py에 추가해주었다. 

views.py에 너무 함수가 많아지면 관리가 어렵기 때문에 views.py 파일을 분리해보자. 

 

views 폴더를 만들어, base_views.py, question_views.py, answer_views.py로 나누어 담는 방식

 

<base_views.py>

from webbrowser import get
from django.http import HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404, redirect
from ..models import Question, Answer
from django.utils import timezone
from ..forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.contrib import messages
# Create your views here.


def index(request):
    page = request.GET.get('page','1') # 페이지
    question_list = Question.objects.order_by('-create_date')
    paginator = Paginator(question_list, 10) # 페이지당 10개씩 보여주기
    page_obj = paginator.get_page(page)
    context = {'question_list' : page_obj}
    return render(request, 'pybo/question_list.html', context)

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    context = {'question': question}
    return render(request, 'pybo/question_detail.html', context)

 

<question_views.py>

from webbrowser import get
from django.http import HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404, redirect
from ..models import Question, Answer
from django.utils import timezone
from ..forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.contrib import messages

@login_required(login_url='common:login')
def question_create(request):
    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')
    else:
        form = QuestionForm()
    context = {'form':form}
    return render(request, 'pybo/question_form.html', context)

@login_required(login_url='common:login')
def question_modify(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '수정 권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    if request.method == 'POST':
        form = QuestionForm(request.POST, instance=question)
        if form.is_valid():
            question = form.save(commit=False)
            question.modify_date = timezone.now()
            question.save()
            return redirect('pybo:detail',question_id=question.id)
    else:
        form = QuestionForm(instance=question)
    context = {'form':form}
    return render(request, 'pybo/question_form.html', context)

@login_required(login_url='common:login')
def question_delete(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    question.delete()
    return redirect('pybo:index')

 

<answer_views.py>

from webbrowser import get
from django.http import HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404, redirect
from ..models import Question, Answer
from django.utils import timezone
from ..forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.contrib import messages


@login_required(login_url='common:login')
def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.method=="POST":
        form = AnswerForm(request.POST)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect('pybo:detail', question_id=question_id)

    else:
        return HttpResponseNotAllowed('Only POST is possible.')
    context = {'question':question, 'form':form}
    return render(request, 'pybo/question_detail.html', context)    
    
    

@login_required(login_url='common:login')
def answer_modify(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail',question_id=answer.question.id)
    if request.method == "POST":
        form = AnswerForm(request.POST, instance=answer)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.modify_date = timezone.now()
            answer.save()
            return redirect('pybo:detail', question_id=answer.question.id)
    else:
        form = AnswerForm(instance=answer)
    context = {'answer':answer, 'form':form}
    return render(request, 'pybo/answer_form.html', context)

@login_required(login_url='common:login')
def answer_delete(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '삭제권한이 없습니다')
    else:
        answer.delete()
    return redirect('pybo:detail', question_id=answer.question.id)

 

 

이 파일명에 맞게 urls.py 파일을 수정해준다.

 

<urls.py>

from django.urls import path
from .views import base_views, question_views, answer_views
app_name = 'pybo'

urlpatterns = [
    #base_views.py
    path('', base_views.index, name='index'),
    path('<int:question_id>/', base_views.detail, name='detail'),
    
    #question_views.py
    path('question/create/', question_views.question_create, name='question_create'),
    path('question/modify/<int:question_id>/', question_views.question_modify, name='question_modify'),
    path('question/delete/<int:question_id>/', question_views.question_delete, name='question_delete'),
    
    #answer_views.py
    path('answer/create/<int:question_id>/', answer_views.answer_create, name='answer_create'),
    path('answer/modify/<int:answer_id>/', answer_views.answer_modify, name='answer_modify'),
    path('answer/delete/<int:answer_id>/', answer_views.answer_delete, name='answer_delete'),

 

<config/urls.py>

이제 config/urls.py에 있는 index URL 매핑도 base_views를 사용하게 바꿔준다.

 

 

추천

질문과 답변에 달 수 있는 좋아요 기능을 추가해보자. 

- 모델 변경

<models.py>

voter 항목을 Answer와 Question에 추가해준다. 

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_question')
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_question')
    def __str__(self):
        return self.subject

class Answer(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='author_answer')
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    voter = models.ManyToManyField(User, related_name='voter_answer')

이때 처음으로 ManyToManyField를 사용했다. 

 

<makemigrations -> migrate>

$ python manage.py makemigrations
Migrations for 'pybo':
  pybo/migrations/0005_answer_voter_question_voter_alter_answer_author_and_more.py
    - Add field voter to answer
    - Add field voter to question
    - Alter field author on answer
    - Alter field author on question
    
$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, pybo, sessions      
Running migrations:
  Applying pybo.0005_answer_voter_question_voter_alter_answer_author_and_more... OK

 

- 질문 추천

                <a href="javascript:void(0)" data-uri="{%url 'pybo:question_vote' question.id %}" 
                    class="recommend btn btn-sm btn-outline-secondary">추천
                    <span class="badge rounded-pill bg-success">{{question.voter.count}}</span>
            </a>

 

같은 파일 아랫단 자바스크립트 코드 파트에 '정말로 추천하시겠습니까' 이벤트를 추가해준다. 

const recommend_elements = document.getElementsByClassName("recommend");
Array.from(recommend_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        if (confirm("정말로 추천하시겠습니까?")){
            location.href = this.dataset.uri;
        };
    });
});

 

질문 추천 urls.py

이제 pybo:question_vote URL을 추가해주었으므로

URL 매핑 규칙을 추가해주자.

    #질문 추천 기능
    path('question/vote/<int:question_id>/', question_views.question_vote, name='question_vote'),
]

 

질문 추천 views.py 

@login_required(login_url='common:login')
def question_vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user == question.author:
        messages.error(request, '본인이 작성한 글은 추천할 수 없습니다.')
    else:
        question.voter.add(request.user)
    return redirect('pybo:detail', question_id=question.id)

 

- 답변 추천

 

<question_detail.html>에 답변 추천 버튼을 추가해주자. 

                <a href="javascript:void(0)" data-uri="{%url 'pybo:answer_vote' answer.id %}" 
                class="recommend btn btn-sm btn-outline-secondary">추천
                <span class="badge rounded-pill bg-success">{{answer.voter.count}}</span>
       			 </a>

 

<urls.py>

답변 추천에 연결한 pybo:answer_vote를 URL 매핑 규칙으로 추가해준다. 

    path('answer/vote/<int:answer_id>/', answer_views.answer_vote, name='answer_vote')

 

<views.py>

answer_vote 함수를 answer_views.py에 추가해준다. 

@login_required(login_url='common:login')
def answer_vote(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user == answer.author:
        messages.error(request, '본인이 작성한 글은 추천할 수 없습니다')
    else:
        answer.voter.add(request.user)
    return redirect('pybo:detail', question_id=answer.question.id)

 

앵커

답글을 작성하거나 수정한 뒤에, 스크롤 위치가 위로 올라간다는 문제점이 있다. 

이를 해결하기 위해 HTML anchor 태그를 이용한다. 

 

<답변 앵커>

a가 앵커 태그인건가? 

    <a id="answer_{{answer.id}}"></a>

 

<답변 redirect>

from webbrowser import get
from django.http import HttpResponseNotAllowed
from django.shortcuts import render, get_object_or_404, redirect, resolve_url
from ..models import Question, Answer
from django.utils import timezone
from ..forms import QuestionForm, AnswerForm
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.contrib import messages


@login_required(login_url='common:login')
def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.method=="POST":
        form = AnswerForm(request.POST)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect('{}#answer_{}'.format(
                resolve_url('pybo:detail', question_id=question.id), answer.id
            ))

    else:
        return HttpResponseNotAllowed('Only POST is possible.')
    context = {'question':question, 'form':form}
    return render(request, 'pybo/question_detail.html', context)    
    
    

@login_required(login_url='common:login')
def answer_modify(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail',question_id=answer.question.id)
    if request.method == "POST":
        form = AnswerForm(request.POST, instance=answer)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.modify_date = timezone.now()
            answer.save()
            return redirect('{}#answer_{}'.format(
                resolve_url('pybo:detail', question_id=answer.question.id), answer.id
            ))
    else:
        form = AnswerForm(instance=answer)
    context = {'answer':answer, 'form':form}
    return render(request, 'pybo/answer_form.html', context)

@login_required(login_url='common:login')
def answer_delete(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '삭제권한이 없습니다')
    else:
        answer.delete()
    return redirect('pybo:detail', question_id=answer.question.id)

@login_required(login_url='common:login')
def answer_vote(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user == answer.author:
        messages.error(request, '본인이 작성한 글은 추천할 수 없습니다')
    else:
        answer.voter.add(request.user)
        return redirect('{}#answer_{}'.format(
                resolve_url('pybo:detail', question_id=answer.question.id), answer.id
        ))

 

마크다운

마크다운으로 질문을 작성해보자! 

 

<pybo_filter.py>

import markdown
from django import template
from django.utils.safestring import mark_safe

register = template.Library()

@register.filter
def sub(value, arg):
    return value-arg

@register.filter()
def mark(value):
    extensions = ["nl2br", "fenced_code"]
    return mark_safe(markdown.markdown(value, extensions=extensions))

mark 함수는 입력된 문자열을 HTML로 변환해준다. 

 

<마크다운 적용>

<question_detail.html>

            <div class="card-text">{{answer.content|mark}}</div>

 

 

검색

- 검색 기능

- GET : GET http://localhost:8000/?kw=어쩌구&page=1 처럼 전달된다

 

- 검색 화면

    <div class="row my-3">
        <div class="col-6">
            <a href="{% url 'pybo:question_create'%}" class="btn btn-primary">질문 등록하기</a>
        </div>
        <div class="col-6">
            <div class="input-group">
                <input type="text" id="search_kw" class="form-control" value="{{kw|default_if_none:''}}">
                <div class="input-group-append">
                    <button class="btn btn-outline-secondary" type="button" id="btn_search">찾기</button>
                </div>
            </div>
        </div>
    </div>

 

<form id="searchForm" method="get" action="{%url 'index'%}">
    <input type="hidden" id="kw" name="kw" value="{{kw|deafult_if_none:''}}">
    <input type="hidden" id="page" name="page" value="{{page}}">
</form>