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