본문 바로가기

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('<','&lt;').replace('>','&gt;')
  • 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/

76.11 댓글 등록 테스트 및 확인

등록을 테스트 해봅니다. 잘 등록 됩니다.

다음글은 수정 / 삭제 기능 구현입니다.

현재글 : 76 Django CBV 댓글 기능 - Ajax 등록
Comments
Login:

Copyright © PythonBlog 2021 - 2022 All rights reserved
Mail : PYTHONBLOG