76 Django CBV 댓글 기능 - Ajax 등록
76.01 장고 댓글 디자인 - bootstrap
pythonblog.co.kr은 대메뉴를 3개의 App으로 분리되어 개발했습니다.
3개의 App을 하나의 댓글모델(테이블)에서 관리하고 등록할 수 있도록 작업을 할예정입니다.
그래서 댓글폼은 각각의 App에서 호출하고 실제 기능 추가/수정/삭제는 공통 view에서 화면 전환없이 처리할 생각입니다.(Ajax)
우선 댓글 등록 기능만들기 부터 진행합니다.
댓글 디자인 가져온 곳 : https://startbootstrap.com/template/blog-post
76.02 모델 만들기 - models.py
댓글모델 부터 만들겠습니다.
#myapp/common/common_models.py
from django.db import models
class PyComment(models.Model):
id = models.AutoField(primary_key=True)
app_id = models.IntegerField( blank=False, null=False)
app_name = models.CharField(max_length=15, blank=False, null=False)
host = models.CharField(max_length=20, blank=True, null=True)
host_id = models.CharField(max_length=30, blank=True, null=True)
comment_body = models.TextField()
comment_img_url = models.CharField(max_length=100, blank=True, null=True)
use_yn = models.CharField(max_length=1, default='Y')
regist_usnm = models.CharField(max_length=30)
regist_dt = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-regist_dt']
db_table = 'py_comment'
- app_id, app_name : 댓글이 등록되는 위치(메뉴)를 찾기 위해 저장, 각 페이지의 PK id
- host, host_id : 차후에 로그인 기능에서 네이버 로그인, 카카오로그인을 넣을때 사용하려고 일단 만듭니다.
- comment_body : 댓글 내용 들어가는 부분
- comment_img_url : 기본 이미지 와 차후에 로그인한 host 이미지?
- use_yn : 댓글 삭제 여부
- regist_usnm, regist_dt : 등록자 / 등록일자
모델을 정의하신 후 실제로 DB에 테이블을 만듭니다.
python manage.py makemigrations
python manage.py migrate
76.03 관리페이지 등록 - admin.py
admin.py에 76.1에서 만든 댓글 모델(PyComment)을 등록합니다.
#myapp/common_models/admin.py
from django.contrib import admin
from .common_models import PyComment
from markdownx.admin import MarkdownxModelAdmin
from markdownx.widgets import AdminMarkdownxWidget
from markdownx.models import MarkdownxField
class CommentAdmin(admin.ModelAdmin):
list_display = ('app_name', 'app_id', 'comment_body', 'use_yn', 'regist_dt')
list_filter = ('use_yn', 'regist_dt')
search_fields = ('app_id', 'host', 'comment_body')
admin.site.register(PyComment, CommentAdmin)
list_display: 화면 표시
list_filter : 필터 선택으로 조회
search_fields : 검색으로 조회할 컬럼
localhost:8000/admin으로 접속하여
로그인 후 (계정이 없으면 생성합니다. python manage.py createsuperuser )
관리자 페이지에서 테스트 댓글 데이터를 미리 넣습니다.
76.04 댓글 폼 만들기 - form.py
HTML에서
부분을 장고에서는 form.py에서 정의할 수 있습니다.장고는 기본적으로 모델의 모든 필드를 생성하지만
fields , wedgets 등을 이용하여 포함하려는 필드를 명시적으로 정의할 수 있습니다.
생성한 comment. 76.1에서 생성한 model을 기반으로 form을 생성합니다.
#myapp/common/common_forms.py
from django import forms
from .common_models import PyComment
class CommentForm(forms.ModelForm):
class Meta:
model = PyComment
fields = ('app_name', 'regist_usnm','comment_body','comment_img_url')
labels = {"comment_body": ""}
widgets={
'app_name': forms.TextInput(attrs={'type':'hidden'}),
'regist_usnm': forms.TextInput(attrs={'type':'hidden'}),
'comment_body': forms.Textarea(attrs={'class':'ml-2 mr-5','style':'width:100%','rows':'2','cols':'90','oninput':'auto_height(this)','placeholder':'Join the discussion and leave a comment!'}),
'comment_img_url': forms.TextInput(attrs={'type':'hidden'}),
}
- fields : 화면에 어떤 필드를 노출 또는 입력 받을 건지 필드 정의
- labels: 화면에 표시는 되는 fields는 기본적으로 라벨이 나옵니다. 라벨을 변경하거나 없애기 위해 사용
- widgets: 위젯으로 각 필드의 속성, 클래스, 스타일 등을 지정, 제어 할 수 있습니다.
forms | 설명 |
---|---|
forms.forms | 사용자정의 form 생성 |
forms.mlodel | model 기반(테이블 기반) form 으로 자동으로 폼필드 생성 |
forms.FormSet | 동일한 페이지에서 여러 form을 제어 |
76.05 폼 및 댓글 가져오기-view.py
detail view에서는 빈폼에 초기값을 셋팅 후에
templates(화면)에 보내는 작업을 진행합니다.
from django.views.generic.edit import FormMixin
from myapp.common.common_forms import CommentForm
from myapp.common.common_models import PyComment
- CommentForm, PyComment 모델 과 FormMinxin을 import 합니다.
class BlogDetail(MenuMixin, JsonLdContextMixin, FormMixin, generic.DetailView):
form_class = CommentForm
- FormMixin을 상속 받고 form_class 에 CommentForm에 할당합니다.
def __init__(self):
self.app_name = 'blog'
#form init
def get_initial(self):
return {'app_name':self.app_name,'regist_usnm':'Anonymous','comment_img_url':'https://res.cloudinary.com/dtfub5xym/image/upload/v1669638871/pb_comment_logo.png'}
def get_context_data(self, **kwargs):
...
...
...
#댓글
py_comment_object = PyComment.objects.filter(app_id = py_blog.id, app_name = self.app_name, use_yn = 'Y')
context['comments'] = py_comment_object
...
...
...
return context
-
def init:
1) app_name 값을 넣어줍니다.
2) 댓글을 3개의 app에서 공통으로 사용하기 때문에 app_name으로 테이블을 조회하여 가져오기 위함. -
def get_initial:
1) CommentForm에서 type:hidden 속성을 지정한 filed의 초기값을 셋팅하며, 셋팅된 내용으로 렌더링 됩니다. -
def get_context_data:
1) PyComment 테이블에서 blog로 등록된 데이터를 가져와 context에 셋팅합니다.
아래는 myapp/blog/views.py 의 전체 소스입니다.
#myapp/blog/views.py
from django.views import generic
from django_json_ld.views import JsonLdContextMixin
from myapp.common.common_views import MenuMixin
from django.views.generic.edit import FormMixin
from myapp.common.common_forms import CommentForm
from myapp.common.common_models import PyComment
from .models import PyBlog
from .models import PyBlogDetail
def googleHowToJson():
return {"@type": "HowTo",
"estimatedCost": {"@type": "MonetaryAmount","currency": "USD","value": "0"},
"supply": [{"@type": "HowToSupply","name": "server"}, ],
"tool": [{"@type": "HowToTool","name": "visual studio code"}, {"@type": "HowToTool","name": "python"}, ],
"totalTime": "P1D",
}
class BlogDetail(MenuMixin, JsonLdContextMixin, FormMixin, generic.DetailView):
model = PyBlogDetail
form_class = CommentForm
template_name = "blog/blogDetail.html"
structured_data = googleHowToJson()
def __init__(self):
self.app_name = 'blog'
def get_object(self):
return PyBlog.objects.get(id=self.kwargs.get('pk'), use_yn = 'Y')
#form init
def get_initial(self):
return {'app_name':self.app_name,'regist_usnm':'Anonymous','comment_img_url':'https://res.cloudinary.com/dtfub5xym/image/upload/v1669638871/pb_comment_logo.png'}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
py_blog = self.get_object()
#page 상세정보
pb_detail_querySet = py_blog.pb_detail.all()
context['dataList'] = pb_detail_querySet
#댓글
py_comment_object = PyComment.objects.filter(app_id = py_blog.id, app_name = self.app_name, use_yn = 'Y')
context['comments'] = py_comment_object
#page meta info
context['pageInfo'] = py_blog.get_page_info()
context['pageInfo'].update({ "title": py_blog.title, "id":py_blog.id,
"get_descript":' '.join(pb_detail_querySet[0].content_body.replace('```','').replace('"','')[:140].split()),
"img_url":pb_detail_querySet[0].img_url})
#menu info
context.update(self.getMenuList)
#json-ld create
self.makeJsonLD(pb_detail_querySet, py_blog.title, context)
return context
def replaceImg(self, new_img, org_img):
if new_img != None and new_img !='':
return new_img.replace('/image/upload/','/image/upload/h_306,w_406,q_auto:best/')
if org_img == None or org_img =='':
return ''
return org_img
def replaceText(self, text):
return text.replace("```"," ").replace("<br/>"," ").replace("<br>"," ").replace("\r","").replace("\t","").replace("\n"," ").replace("<","").replace(">","")
def makeJsonLD(self, query_set, page_title, context):
structured_data = super().get_structured_data()
structured_data.update({'name':page_title, 'image':{ "@type": "ImageObject","height": "306","width": "406", "url": self.replaceImg(query_set[0].new_img_url,query_set[0].img_url),},})
stepList = []
for i in query_set:
stepList.append({
"@type": "HowToStep",
"url": f"{structured_data['url']}#{i.id}",
"name": f"{i.sub_title}",
"itemListElement":[{"@type": "HowToDirection", "text": f"{self.replaceText(i.content_body[:400])}"}],
"image": {"@type": "ImageObject",
"url": f"{self.replaceImg(i.new_img_url,i.img_url)}",
"height": "306",
"width": "406"}
})
structured_data.update({'step':stepList})
context.update({'sd':structured_data})
76.06 댓글폼 및 내용- html
76.5에서 context에 넣어준 comments, form을 이용하여 댓글페이지를 만듭니다.
기본 base.html에 comment.html을 include하여 모든 app(blog, coding, edu) 메뉴에서
댓글이 표시되도록 합니다.
comment.html 파일을 새로 생성합니다.
myapp>templates/base>comment.html 소스
<!--myapp>templates/base>comment.html -->
<div class="row">
<div class="col-lg-8 mb-1">
<div class="card bg-light">
<div class="card-body">
<div id="comment_header" class="card-header h4 ">Comments
<button type="button" class="btn text-secondary float-right d-inline-block">Login</button>
</div>
<form id="commentForm" class="form-inline" method="POST">
{% csrf_token %}
{{form.as_p}}
<div class="form-group row">
<button id="comment_submit_btn" class="btn btn-primary ml-4 " type="button" >등록</button>
</div>
</form>
<div id="singleComment">
{% for comment in comments %}
<div class="d-flex mb-4">
<div class=" flex-sm-shrink-0">
<img class="rounded-circle" src="{{comment.comment_img_url}}" alt="{{comment.host}}" />
</div>
<div class="ms-3">
<div class="fw-bold">
<span class="text-gray-900 font-weight-bolder text-lg">{{ comment.regist_usnm }}</span> <span class="text-xs"> {{comment.regist_dt | date:'Y.m.d H:i:s' }}</span>
</div>
{{ comment.comment_body | linebreaks }}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
종류 | 설명 |
---|---|
form.as_div | div태그 로 래핑되어 렌더링됩니다 . |
form.as_p | p 태그로 렌더링 |
form.as_table | table로 렌더링 |
form.as_ul | li 태그로 렌더링 |
76.07 댓글입력폼 확인 - 로컬반영
76.08 댓글등록 url 설정 - url.py
화면에서 댓글등록시 ajax에서 호출할 url을 설정합니다.
urlpatterns = [
...
...
path('comment/<int:pk>/add', views.AddComment.as_view(), name='add_comment'),
]
76.09 Add comment - view.py
Form에서 전송된 데이터를 DB에 등록하는 기능과 유효성 체크를 진행합니다.
from django.http import JsonResponse
from .common_forms import CommentForm
from .common_models import PyComment
...
...
...
# https://docs.djangoproject.com/en/4.1/ref/forms/api/
class AddComment(generic.FormView):
form_class = CommentForm
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
comment_body = self.unScript(form.cleaned_data['comment_body'])
n_comment = form.save(commit=False)
n_comment.comment_body = comment_body
n_comment.app_id = self.kwargs.get('pk')
comment = n_comment.save()
return JsonResponse( {'error_msg':'','id':n_comment.id, 'comment_body':comment_body,'comment_img_url':n_comment.comment_img_url,
'regist_usnm':n_comment.regist_usnm,'regist_dt':n_comment.regist_dt.strftime('%Y.%m.%d %H:%M:%S')})
else:
error_msg = ''
for key in form.errors.as_data():
error_msg = f'{self.getKor(key)} {form.errors[key][0]} '
break
return JsonResponse({'error_msg':error_msg})
def getKor(self, text):
key_kor = {'comment_body':'댓글내용','regist_usnm':'작성자'}
rtn = key_kor.get(text)
if rtn == None:
return text
return rtn
def unScript(self, text):
return text.replace('<','<').replace('>','>')
-
76.5 에는 DetailView를 사용하고 있어, FormMix를 상속 받았지만 여기서는 generic.FormView를 상속 받았습니다.
-
self.get_form() 으로 form 을 가져온후 is_valid()로 폼 내용의 유효성을 검사합니다.
- cleaned_data는 이미 유효성 체크가 끝난 데이터 입니다.
- XSS공격 대응차원에서 unScript 함수를 통해 script가 실행되지 못하게 변경해줍니다.
- n_comment 인스턴스를 만들고 DB에 등록될 값을 셋팅 후 save() 하시면 됩니다.
-
form.errors.as_data(), as_json() 으로 유효성 검사에 실패한(ValidationError) 데이터를 확인 할 수 있습니다.
-
Ajax 로 호출 하기 때문에 return JsonResponse 을 사용했습니다.
아래 문서를 보시면 다양한 사용법을 하실 수 있습니다.
Django Form 문서 : https://docs.djangoproject.com/en/4.1/ref/forms/api/
76.10 등록 스크립트 - Ajax 요청
76.6에서 생성한 comment.html 파일을 열어 스크립트를 추가합니다.
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<script>
$(function() {
var header_lenght = $(".card-header").length
$("#comment_header").addClass('card-header-idx'+header_lenght) //우측 메뉴에서 사용
$('#comment_submit_btn').click(function() {
if($('#id_comment_body').val() == ''){
$.notify('...');
noticefyAlert("댓글을 입력하세요.",'warning')
return "";
}
$.ajax({
url: '{% url "common:add_comment" pageInfo.id %}',
method:"POST",
data:$('#commentForm').serialize()
}).done(function(data) {
if(data != null){
if(data.error_msg == ''){
new_comment = '<div id="new_comment_'+data.id+'"class="d-flex mb-4">';
new_comment += ' <div class=" flex-sm-shrink-0"><img class="rounded-circle" src="'+data.comment_img_url+'" alt="..." /></div>';
new_comment += ' <div class="ms-3">';
new_comment += ' <div class="fw-bold"><span class="text-gray-900 font-weight-bolder text-lg">'+data.regist_usnm+'</span> <span class="text-xs"> '+data.regist_dt+'</span></div>';
new_comment += '<pre>'+data.comment_body+'</pre>';
new_comment += ' </div>';
new_comment += '</div>';
$('#id_comment_body').val('')
$('#singleComment').prepend(new_comment); // new comment 끼워넣기
$("#new_comment_"+data.id).fadeOut(100).fadeIn(2000);
noticefyAlert("success","success");
}else{
noticefyAlert(data.error_msg,'danger');
}
}else{
successClick("transaction fail",'danger');
}
});
return;
});
});
</script>
</script>
{% endblock js %}
- ajax 요청에 들어갈 url, data 를 데이터 셋팅 등록을 요청합니다.
- 등록 성공시 prepend로 맨 앞에 방금 등록한 댓글 을 추가
- 실패시 view에서 넘겨준 error message 표시
noticefyAlert 알림창은 Bootstrap Notify을 사용했습니다.
https://grotesquegentleadvance--samkhaled.repl.co/