編輯功能使用到了ckeditor的MathJax組件。ajax提交評論能夠不用刷新瀏覽器。javascript
一、變化的部分css
二、上代碼:html
ul.blog-types,ul.blog-dates { list-style-type: none; } div.blog:not(:last-child) { margin-bottom: 2em; padding-bottom: 1em; border-bottom: 1px solid #eee; } div.blog h3 { margin-top: 0.5em; } div.blog-info p { margin-bottom: 0; } div.blog-info p span{ margin-right: 10px; } div.blog-info-description { list-style-type: none; margin-bottom: 1em; } ul.blog-info-description li { display: inline-block; margin-right: 1em; } div.paginator { text-align: center; } div.container { max-width: 80%; } div.comment-area{ margin-top: 2em; } h3.comment-area-title{ border-bottom: 1px solid #ccc; padding-bottom: 0.4em; } div.django-ckeditor-widget { width: 100%; }
{# 引用模板 #} {% extends 'base.html' %} {% load staticfiles %} {% block header_extends %} <link rel="stylesheet" href="{% static 'blog/blog.css' %}"> {# 處理公式 #} <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script> {% endblock %} {# 標題 #} {% block title %} {{ blog.title }} {% endblock %} {# 內容#} {% block content %} <div class="container"> <div class="row"> <div class="col-10 offset-1"> <ul class="blog-info-description"> <h3>{{ blog.title }}</h3> <li>做者:{{ blog.author }}</li> {# 時間過濾器讓時間按照本身須要的格式過濾 #} <li>發佈日期:{{ blog.created_time|date:"Y-m-d H:i:s" }}</li> <li>分類: <a href="{% url 'blogs_with_type' blog.blog_type.pk %}"> {{ blog.blog_type }} </a> </li> <li>閱讀({{ blog.get_read_num }})</li> </ul> <p class="blog-content">{{ blog.content|safe }}</p> <p>上一篇: {% if previous_blog %} <a href="{% url 'blog_detail' previous_blog.pk %}">{{ previous_blog.title }}</a> {% else %} <span>沒有了</span> {% endif %} </p> <p>下一篇: {% if next_blog %} <a href="{% url 'blog_detail' next_blog.pk %}">{{ next_blog.title }}</a> {% else %} <span>沒有了</span> {% endif %} </p> </div> </div> <div class="row"> <div class="col-10 offset-1"> <div class="comment-area"> <h3 class="comment-area-title">提交評論</h3> {% if user.is_authenticated %} <form id="comment-form" action="{% url 'update_comment' %}" method="post" style="overflow: hidden"> {% csrf_token %} <label for="form-control">{{ user.username }},歡迎評論~</label> {% for field in comment_form %} {{ field }} {% endfor %} <span id="comment-error" class="text-danger float-left"></span> <input type="submit" value="評論" class="btn btn-primary float-right"> </form> {% else %} 您還沒有登陸,登陸以後方可評論 {# 提交登陸的時候帶上從哪裏訪問的路徑 #} <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登陸</a> <span> or </span> <a class="btn-danger btn" href="{% url 'register' %}?from={{ request.get_full_path }}">註冊</a> {% endif %} </div> <div class="-comment-area"> <h3 class="comment-area-title">評論列表</h3> <div id="comment-list"> {% for comment in comments %} <div> {{ comment.user.username }} {{ comment.comment_time|date:"Y-m-d H:i:s" }} {{ comment.text|safe }} </div> {% empty %} {% endfor %} </div> </div> </div> </div> </div> {% endblock %} {% block js %} <script> $('#comment-form').submit(function () { // 獲取錯誤框 let comment_error = $('#comment-error'); comment_error.text(''); // 更新數據到textarea CKEDITOR.instances['id_text'].updateElement(); let comment_text = CKEDITOR.instances['id_text'].document.getBody().getText().trim(); // 判斷是否爲空 if (!(CKEDITOR.instances['id_text'].document.getBody().find('img')['$'].length !== 0 || comment_text !== '')) { // 顯示錯誤信息 comment_error.text('評論內容不能爲空'); return false; } //異步提交 $.ajax({ url: "{% url 'update_comment' %}", type: 'POST', data: $(this).serialize(),// 序列化表單值 cache: false, // 關閉緩存 success: function (data) { if (data['status'] === 'SUCCESS') { console.log(data); // 插入數據 // es6寫法 let comment_html = `<div>${data["username"]} (${data["comment_time"]}): ${data["text"]}</div>`; $('#comment-list').prepend(comment_html); // 清空編輯框的內容 CKEDITOR.instances['id_text'].setData(''); } else { // 顯示錯誤信息 comment_error.text(data['message']) } }, error: function (xhr) { console.log(xhr); } }); return false; }) </script> <script> $(".nav-blog").addClass("active").siblings().removeClass("active"); </script> {% endblock %}
{# 引用模板 #} {% extends 'base.html' %} {% load staticfiles %} {% block header_extends %} <link rel="stylesheet" href="{% static 'blog/blog.css' %}"> {# 處理公式 #} <script src='https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML' async></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor-init.js" %}"></script> <script type="text/javascript" src="{% static "ckeditor/ckeditor/ckeditor.js" %}"></script> {% endblock %} {# 標題 #} {% block title %} {{ blog.title }} {% endblock %} {# 內容#} {% block content %} <div class="container"> <div class="row"> <div class="col-10 offset-1"> <ul class="blog-info-description"> <h3>{{ blog.title }}</h3> <li>做者:{{ blog.author }}</li> {# 時間過濾器讓時間按照本身須要的格式過濾 #} <li>發佈日期:{{ blog.created_time|date:"Y-m-d H:i:s" }}</li> <li>分類: <a href="{% url 'blogs_with_type' blog.blog_type.pk %}"> {{ blog.blog_type }} </a> </li> <li>閱讀({{ blog.get_read_num }})</li> </ul> <p class="blog-content">{{ blog.content|safe }}</p> <p>上一篇: {% if previous_blog %} <a href="{% url 'blog_detail' previous_blog.pk %}">{{ previous_blog.title }}</a> {% else %} <span>沒有了</span> {% endif %} </p> <p>下一篇: {% if next_blog %} <a href="{% url 'blog_detail' next_blog.pk %}">{{ next_blog.title }}</a> {% else %} <span>沒有了</span> {% endif %} </p> </div> </div> <div class="row"> <div class="col-10 offset-1"> <div class="comment-area"> <h3 class="comment-area-title">提交評論</h3> {% if user.is_authenticated %} <form id="comment-form" action="{% url 'update_comment' %}" method="post" style="overflow: hidden"> {% csrf_token %} <label for="form-control">{{ user.username }},歡迎評論~</label> {% for field in comment_form %} {{ field }} {% endfor %} <span id="comment-error" class="text-danger float-left"></span> <input type="submit" value="評論" class="btn btn-primary float-right"> </form> {% else %} 您還沒有登陸,登陸以後方可評論 {# 提交登陸的時候帶上從哪裏訪問的路徑 #} <a class="btn btn-primary" href="{% url 'login' %}?from={{ request.get_full_path }}">登陸</a> <span> or </span> <a class="btn-danger btn" href="{% url 'register' %}?from={{ request.get_full_path }}">註冊</a> {% endif %} </div> <div class="-comment-area"> <h3 class="comment-area-title">評論列表</h3> <div id="comment-list"> {% for comment in comments %} <div> {{ comment.user.username }} {{ comment.comment_time|date:"Y-m-d H:i:s" }} {{ comment.text|safe }} </div> {% empty %} {% endfor %} </div> </div> </div> </div> </div> {% endblock %} {% block js %} <script> $('#comment-form').submit(function () { // 獲取錯誤框 let comment_error = $('#comment-error'); comment_error.text(''); // 更新數據到textarea CKEDITOR.instances['id_text'].updateElement(); let comment_text = CKEDITOR.instances['id_text'].document.getBody().getText().trim(); // 判斷是否爲空 if (!(CKEDITOR.instances['id_text'].document.getBody().find('img')['$'].length !== 0 || comment_text !== '')) { // 顯示錯誤信息 comment_error.text('評論內容不能爲空'); return false; } //異步提交 $.ajax({ url: "{% url 'update_comment' %}", type: 'POST', data: $(this).serialize(),// 序列化表單值 cache: false, // 關閉緩存 success: function (data) { if (data['status'] === 'SUCCESS') { console.log(data); // 插入數據 // es6寫法 let comment_html = `<div>${data["username"]} (${data["comment_time"]}): ${data["text"]}</div>`; $('#comment-list').prepend(comment_html); // 清空編輯框的內容 CKEDITOR.instances['id_text'].setData(''); } else { // 顯示錯誤信息 comment_error.text(data['message']) } }, error: function (xhr) { console.log(xhr); } }); return false; }) </script> <script> $(".nav-blog").addClass("active").siblings().removeClass("active"); </script> {% endblock %}
# -*- coding: utf-8 -*- # @Time : 18-11-20 下午10:47 # @Author : Felix Wang from django import forms from django.contrib.contenttypes.models import ContentType from django.db.models import ObjectDoesNotExist from ckeditor.widgets import CKEditorWidget class CommentForm(forms.Form): content_type = forms.CharField(widget=forms.HiddenInput) object_id = forms.IntegerField(widget=forms.HiddenInput) text = forms.CharField(widget=CKEditorWidget(config_name='comment_ckeditor'), error_messages={'required': '評論內容不能爲空'}) def __init__(self, *args, **kwargs): if 'user' in kwargs: self.user = kwargs.pop('user') super().__init__(*args, **kwargs) # 表單驗證 def clean(self): # 判斷用戶是否登陸 if self.user.is_authenticated: self.cleaned_data['user'] = self.user else: raise forms.ValidationError('用戶還沒有登陸') content_type = self.cleaned_data['content_type'] object_id = self.cleaned_data['object_id'] try: model_class = ContentType.objects.get(model=content_type).model_class() model_obj = model_class.objects.get(pk=object_id) self.cleaned_data['content_object'] = model_obj except ObjectDoesNotExist as e: raise forms.ValidationError('評論對象不存在') return self.cleaned_data
from django.shortcuts import render, reverse, redirect from django.http import JsonResponse from .models import Comment from django.contrib.contenttypes.models import ContentType from .forms import CommentForm import re import copy def update_commit(requests): comment_form = CommentForm(requests.POST, user=requests.user) if comment_form.is_valid(): comment = Comment() comment.user = comment_form.cleaned_data['user'] comment.text = comment_form.cleaned_data['text'] comment.content_object = comment_form.cleaned_data['content_object'] comment.save() # 返回數據 data = { 'status': 'SUCCESS', 'username': comment.user.username, 'comment_time': comment.comment_time.strftime('%Y-%m-%d %H:%M:%S'), 'text': comment.text.strip(), } else: data = { 'status': 'ERROR', 'message': list(comment_form.errors.values())[0][0], } return JsonResponse(data)
# -*- coding: utf-8 -*- # @Time : 18-11-20 下午8:10 # @Author : Felix Wang from django import forms from django.contrib import auth from django.contrib.auth.models import User class LoginForm(forms.Form): username = forms.CharField(label='用戶名', required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '請輸入用戶名'})) # widget指定input標籤類型 password = forms.CharField(label='密碼', widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '請輸入密碼'})) def clean(self): # 驗證數據 username = self.cleaned_data['username'] password = self.cleaned_data['password'] user = auth.authenticate(username=username, password=password) if user is None: raise forms.ValidationError('用戶名或密碼錯誤') self.cleaned_data['user'] = user # 將驗證過的user放入clean_data return self.cleaned_data class RegisterForm(forms.Form): # 用戶名字段 username = forms.CharField(label='用戶名', max_length=30, min_length=3, required=True, widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '請輸入用戶名'})) # 郵箱字段 email = forms.EmailField(label='郵箱', min_length=3, required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': '請輸入郵箱'})) # 密碼字段 password = forms.CharField(label='密碼', min_length=6, required=True, widget=forms.PasswordInput( attrs={'class': 'form-control', 'placeholder': '請輸入密碼'})) # 再次輸入密碼 password_again = forms.CharField(label='確認密碼', min_length=6, required=True, widget=forms.PasswordInput( attrs={'class': 'form-control', 'placeholder': '請再輸入一次密碼'})) def clean_username(self): username = self.cleaned_data['username'] if User.objects.filter(username=username).exists(): raise forms.ValidationError('用戶名已存在') return username def clean_email(self): email = self.cleaned_data['email'] if User.objects.filter(email=email).exists(): raise forms.ValidationError('郵箱已存在') return email def clean_password_again(self): password = self.cleaned_data['password'] password_again = self.cleaned_data['password_again'] if password != password_again: raise forms.ValidationError('兩次輸入的密碼不一致') return password_again
""" Django settings for myblog project. Generated by 'django-admin startproject' using Django 2.1.3. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'ea+kzo_5k^6r7micfg@lar1(rfdc08@b4*+w5d11=0mp1p5ngr' # SECURITY WARNING: don't run with debug turned on in production!2. DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'ckeditor', 'ckeditor_uploader', 'blog.apps.BlogConfig', # 將本身建立的app添加到設置中 'read_statistics.apps.ReadStatisticsConfig', # 註冊閱讀統計app 'comment.apps.CommentConfig', # 註冊評論 ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'blog.middleware.mymiddleware.My404', # 添加本身的中間件 ] ROOT_URLCONF = 'myblog.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ os.path.join(BASE_DIR, 'templates'), ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'myblog.wsgi.application' # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'myblogs', # 要鏈接的數據庫,鏈接前須要建立好 'USER': 'root', # 鏈接數據庫的用戶名 'PASSWORD': 'felixwang', # 鏈接數據庫的密碼 'HOST': '127.0.0.1', # 鏈接主機,默認本級 'PORT': 3306 # 端口 默認3306 } } # Password validation # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/2.1/topics/i18n/ # LANGUAGE_CODE = 'en-us' # 語言 LANGUAGE_CODE = 'zh-hans' # TIME_ZONE = 'UTC' # 時區 TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True # 不考慮時區 USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] # media MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # 配置ckeditor CKEDITOR_UPLOAD_PATH = 'upload/' # 自定義參數 EACH_PAGE_BLOGS_NUMBER = 7 # 設置數據庫緩存 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'my_read_num_cache_table', } } # ckeditor 代碼高亮,以及公式 CKEDITOR_CONFIGS = { 'default': { 'skin': 'moono', 'tabSpaces': 4, 'mathJaxLib': 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML', 'toolbar': ( ['div', 'Source', '-', 'Save', 'NewPage', 'Preview', '-', 'Templates'], ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Print', 'SpellChecker', 'Scayt'], ['Undo', 'Redo', '-', 'Find', 'Replace', '-', 'SelectAll', 'RemoveFormat', '-', 'Maximize', 'ShowBlocks', '-', "CodeSnippet", 'Mathjax', 'Subscript', 'Superscript'], ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'], ['Bold', 'Italic', 'Underline', 'Strike', '-'], ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', 'Blockquote'], ['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'], ['Link', 'Unlink', 'Anchor'], ['Image', 'Flash', 'Table', 'HorizontalRule', 'Smiley', 'SpecialChar', 'PageBreak'], ['Styles', 'Format', 'Font', 'FontSize'], ['TextColor', 'BGColor'],), 'extraPlugins': ','.join([ 'codesnippet', 'mathjax', 'dialog', 'dialogui', 'lineutils', ]), }, 'comment_ckeditor': { 'toolbar': 'custom', 'toolbar_custom': [ ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript'], ['TextColor', 'BGColor', 'RemoveFormat'], ['NumberedList', 'BulletedList'], ['Link', 'Unlink'], ['Smiley', 'SpecialChar', 'Blockquote'], ], 'width': 'auto', 'height': '180', 'tabSpace': 4, 'removePlugins': 'elementspath', 'resize_enabled': False, } }