一个论坛引发的血案

      去病率兵十万深入敌后痛击匈奴,经此一役,胡虏元气甚伤,单于伊治斜弃河西平原而迁王庭至漠北。汉乃设酒泉、张掖、敦煌、武威河西四郡。众将凯旋回师,帝召大将军卫青、骠骑将军霍去病、飞将军李广、苏建、公孙敖、公孙贺等将议于宣室殿。众将皆以需涉荒漠为困而欲弃之。然武帝废胡之心愈坚,怒曰:汝皆以为不可,而胡虏亦曰寡人不可为之,然朕必举全国之力誓击之,永绝胡虏之患!

      遂点精骑十万,以大将军五万兵出定襄,骠骑将军五万出代郡,辎重步卒五十万,挥师远征漠北!

一、兵马未动,辎重先行。——表结构的设计

(参照抽屉新热榜)

在论坛中文章包含的属性(数据库中的字段)有标题、简介、板块、内容、作者、发布时间、最后一次修改时间、优先级(数字权重)。

model里的字段类型

auto_now和auto_now_add区别:

auto_now:记录字段每次被修改的时间

auto_now_add:记录字段被创建时的时间

若字段被设置为这两种类型的一种,则字段不可再更改。

models.py

#! _*_ coding:utf-8 _*_

from
__future__ import unicode_literals from django.db import models from django.contrib.auth.models import User #倒入django-admin自带的User表 from django.core.exceptions import ValidationError #页面提示错误信息 import datetime # Create your models here. #文章 class Article(models.Model): title = models.CharField(max_length=255) #标题 brief = models.CharField(blank=True,null=True,max_length=255)#文章简介,可以为空 category = models.ForeignKey('Category')#板块,需要关联Cotegory类(若要关联尚未定义的model,需要使用引号) content = models.TextField(u'文章内容')#文章内容 author = models.ForeignKey('UserInfo')#作者 pub_date = models.DateTimeField(blank=True,null=True)#发布日期,若是草稿,则没有发布时间 last_modify = models.DateTimeField(auto_now=True)#最后一此修改时间 priority = models.IntegerField(u'优先级',default=1000)#优先级,默认1000
head_img = models.ImageField('文章标题图片',upload_to='uploads')#存储标题图片,图片上传到uploads目录下,该目录在app下会自动创建
    status_choices = (
        ('draft','草稿'),
        ('published','已发布'),
        ('hidden','隐藏'),
    )
    status = models.CharField(choices=status_choices,default='published',max_length=32)#文章状态

    #发布时间的判断
    def clean(self):
        if self.status == 'draft' and self.pub_date is not None:
            raise ValidationError('草稿没有发布时间')
        #如果已经发布,则发布时间设为当前时间
        if self.status == 'published' and self.pub_date is None:
            self.pub_date = datetime.date.today()

    def __unicode__(self):
        return self.title

    class Meta:
        verbose_name_plural = '文章'


#评论
class Comment(models.Model):
    article = models.ForeignKey(Article,verbose_name='所属文章')#评论所属文章
    parent_comment = models.ForeignKey('self',related_name='my_children',blank=True,null=True)#存储父评论,与自己关联,relate_name获取自己的子评论
    comment_choices = (
        (1,'评论'),
        (2,'点赞'),
    )
    comment_type = models.IntegerField(choices=comment_choices,default=1)#选择评论还是点赞,默认是评论
    user = models.ForeignKey('UserInfo')#发表评论或点赞的用户
    date = models.DateTimeField(auto_now_add=True)#评论时间
    comment = models.TextField(blank=True,null=True)#评论内容

    #判断评论是否为空
    def clean(self):
        if self.comment_type == 1 and len(self.comment) == 0:
            raise ValidationError('评论不能为空')

    def __unicode__(self):
        return '%s,P:%s,%s' %(self.article,self.parent_comment,self.comment)
    class Meta:
        verbose_name_plural = '评论'

#板块
class Category(models.Model):
    name = models.CharField(max_length=64)
    brief = models.CharField(blank=True,null=True,max_length=255)#板块简介
    set_as_top_menu = models.BooleanField(default=False)#是否将该模块添加到顶部菜单栏
    position = models.SmallIntegerField()#若在顶部菜单栏显示,则需要设置显示的顺序
    admin = models.ManyToManyField('UserInfo',blank=True)#板块管理员

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name_plural = '板块'
#用户
class UserInfo(models.Model):
    user = models.OneToOneField(User)#关联到django-admin自带的user表
    name = models.CharField(max_length=32)
    signature = models.CharField(max_length=255,blank=True,null=True)#个性签名
    head_img = models.ImageField(height_field=150,width_field=150,blank=True,null=True)#头像

    def __unicode__(self):
        return self.name

    class Meta:
        verbose_name_plural = '用户'

设置settings.py 

#注册app
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',
]

#设置数据库
DATABASES = {
    'default': {
        #'ENGINE': 'django.db.backends.sqlite3',
        #'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
        'ENGINE': 'django.db.backends.mysql',
        'NAME':'blog',
        'HOST':'',
        'PORT':'',
        'USER':'root',
        'PASSWORD':'0711'
    }
}

#模版路径
'DIRS': [
            os.path.join(BASE_DIR,'templates'),
        ],

#静态文件
STATICFILES_DIRS=(
    os.path.join(BASE_DIR,'statics'),     #该定义要求statics目录在project目录下
    )

一切前期准备就绪,在数据库中创建blog库(注意字符编码):

mysql> create database blog character set utf8;

同步数据库,创建表:

python manage.py makemigrations

python manage.py migrate

注册admin后台

admin.py

from django.contrib import admin

from blog import models

# Register your models here.

class ArticleAdmin(admin.ModelAdmin):
    list_display = ('title','category','author','pub_date','last_modify','status')  #每个表在页面上展示的字段


class CommentAdmin(admin.ModelAdmin):
    list_display = ('article','parent_comment','comment_type','comment','user','date')

class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name','set_as_top_menu','position')

admin.site.register(models.Article,ArticleAdmin)
admin.site.register(models.Category,CategoryAdmin)
admin.site.register(models.Comment,CommentAdmin)
admin.site.register(models.UserInfo)

二、华丽铠甲,折戟断箭。——前端展示模版的选择

 Bootstrap中挑选合适的页面模版,全部下载到本地。

设置模版和静态文件:

在project目录下创建templates和statics目录分别存放模版文件和静态文件

settings.py

#templates

    'DIRS': [
        os.path.join(BASE_DIR,'templates'),
    ],


#statics STATIC_URL
= '/static/' #该设置对static目录在app目录下有效 STATICFILES_DIRS =( os.path.join(BASE_DIR,'statics'), #该设置对statics目录在project目录下有效 )

在statics目录中添加静态文件:

在templates目录中添加模版文件:

将下载的html文件修改为base.html作为父模版,和下载的目录一起放在templates下创建的blog目录下,然后创建index.html作为项目首页。

index.html:

{% extends 'blog/base.html' %}    #继承父模版base.html

将base.html中js和css部分的url修改为本地的文件资源,如:

    <!-- Bootstrap core CSS -->
    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="/static/bootstrap/css/navbar-fixed-top.css" rel="stylesheet">

urls.py

url采用根据不同项目做分发的规则,project目录下的urls.py:

from django.conf.urls import url,include
from django.contrib import admin


urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^blog/$',include('blog.urls')),
]

在app中新创建urls.py,然后设置如下:

from blog import views


urlpatterns = [
    url(r'^$',views.index),
]

url设置完毕,创建试图函数index,views.py:

def index(request):
    return render(request,'blog/index.html')

 一个简单的页面套用完毕。

 

三、临阵布局,指挥若定。——动态导航栏及登录

对页面进行改造,将导航栏设为动态,并添加登陆与退出功能。

1、动态导航

根据表结构的设计,在category表中,若set_as_top_menu字段为True,则该板块名称即可显示在导航栏中。即每个现实在导航栏中的category,其set_as_top_menu字段必须为Ture。并且,每个category都有一个position标示,这样就可以根据前端请求的url中的id来现实对应的目录名称。

blog/urls.py:

from blog import views


urlpatterns = [
    url(r'category/(d+)',views.category),
]

views中,根据url中的id,查询category表,并将查询到的category名字返回给前端。

views.py:

category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')      #将其定义为全局变量,便于多个函数引用

def index(request):
    return render(request,'blog/index.html',{'category_list':category_list})

def category(request,id):
    category_obj = models.Category.objects.get(id = id)
    return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj})

前端页面base.html中,循环所有在导航栏中显示的category,并和当前请求的category比较(根据id值),若两者相等,则跳转url并加底色。

base.html:

{% for category in category_list %}
{% if category.id == category_obj.id %} #当前请求id等于导航栏中某一个category的id值
<li class="active"><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
{% else %}
<li class=""><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
{% endif %}
{% endfor %}

这样,若点击导航栏中id为3的category,则url跳转至 http://localhost:8000/blog/category/3

2、登录及退出

若未登录,主页面右上角显示登录/注册按钮,可跳转至登录或注册页面;若已登录,则在该处显示用户名。

全局urls.py的设置:

from blog import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^blog/',include('blog.urls')),
    url(r'^login/$',views.user_login,name='login'),
    url(r'^logout/$',views.user_logout,name='logout'),
]

 分别对登录和退出添加别名,方便前端页面中调用。

views.py中,引用django框架中的用户登录验证及退出功能。

from django.shortcuts import render,HttpResponseRedirect

# Create your views here.

from blog import models
from django.contrib.auth import login,logout,authenticate
from django.contrib.auth.decorators import login_required

category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')

def index(request):
    return render(request,'blog/index.html',{'category_list':category_list})

def category(request,id):
    category_obj = models.Category.objects.get(id = id)
    return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj})

def user_login(request):
    if request.method == 'POST':
        user = authenticate(username = request.POST.get('username'),password = request.POST.get('password'))
        if user is not None:     #登录成功
            login(request,user)
            return HttpResponseRedirect('/blog')  
        else:
            login_error = 'wrong username or password!'
            return render(request,'blog/login.html',{'login_error':login_error})
    return render(request,'blog/login.html')

def user_logout(request):
    logout(request)
    return HttpResponseRedirect('/blog')

登录页面login.html:

{% extends 'blog/base.html' %}

{% block page-container %}
    <form action="" method="post"> {% csrf_token %}
        <div>
            <input type="text" name="username">
        </div>
        <div>
            <input type="password" name="password">
        </div>
        <div>
            <input type="submit" value="login">
        </div>
    </form>
    <div>
        {% if login_error %}
            <p style="color: red">{{ login_error }}</p>
        {% endif %}
    </div>
{% endblock %}

继承base.html 父模版,自定内容主题。

base.html中,在右上角添加登陆/注册按钮


<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li class="active"><a href="#">{{ request.user.userinfo.name }}</a></li> #反向查询,查询 UserInfo表中的name字段,这里需要将表名小写
<li class="active"><a href="{% url 'logout' %}">注销</a></li>
{% else %}
<li class=""><a href="{% url 'login' %}">登陆/注册</a></li>
{% endif %}
</ul>
 

效果:

 

三、左迂回,右包抄,稳坐中军帐。——内容排版展示

显示文章及图片

主页及各板块下显示相应文章及图片

views.py

category_list = models.Category.objects.filter(set_as_top_menu=True).order_by('position')

def index(request):
    #获取position为1的category,category_obj.id即position为1的categoryid值
    category_obj = models.Category.objects.get(position = 1)
    '''
    返回position为1的category
    前端请求主页时,首先从position=1开始循环所有category列表,此时'全部'板块的position为1,将该category加底色
    '''
    article_list = models.Article.objects.all()   #所有文章
    return render(request,'blog/index.html',{'category_list':category_list,'article_list':article_list,'category_obj':category_obj})

def category(request,id):
    category_obj = models.Category.objects.get(id = id)
    if  category_obj.position == 1:      #如果是全部,则显示全部文章
        article_list = models.Article.objects.all()
    else:
        article_list = models.Article.objects.filter(category_id=category_obj.id)   #article表中category_id字段
    return render(request,'blog/index.html',{'category_list':category_list,'category_obj':category_obj,'article_list':article_list})

前端页面展示设置中关于图片展示的问题:

'''
图片上传到uploads目录,前端请求的url为'/static/uploads/3_DDWq8O3.jpg',
uploads目录不在django静态文件的配置中,前端不能访问,因此要在setting中添加该目录,
这样实际图片位置为'/static/3_DDWq8O3.jpg',
自定义模版,将'/static/uploads/3_DDWq8O3.jpg'变为/static/3_DDWq8O3.jpg'
前端中图片src为'<img src="/static/{{ article.head_img }}">',
这样,只需要将'uploads/3_DDWq8O3.jpg'变为'3_DDWq8O3.jpg'即可
'''

settings.py:

STATICFILES_DIRS =(
    os.path.join(BASE_DIR,'statics'),
    os.path.join(BASE_DIR,'uploads'),
    )

自定义django模版:

blog目录下创建templatetags/custom.py文件,并在该目录下创建__init__.py文件。

customer.py:

#! _*_coding:utf-8 _*_
from django import template

register = template.Library()

@register.filter
def truncate_url(img_obj):
    return img_obj.name.split('/',1)[-1]   
  #
return img_obj.name.split('/')[1]
  #截取结果:['uploads','xxx.jpg']

#即将uploads/xxx.jpg 截为xxx.jpg,传给前端之后,就是static/xx.jpg #split(
'/',1)表示只截取一次,如'1/2/3'截取后为['1','2/3']

index.html页面:

{% extends 'blog/base.html' %}
{% load custom %}

{% block page-container %}
    {% for article in article_list %}
        <div class="article-box">
            <div class="article-head-img">
                <img src="/static/{{ article.head_img | truncate_url}}">   #自定义模版,将articel.head_img交给truncate_url函数处理
 </div> <div class="article-bruff"> {{ article.title }} </div> </div> {% endfor %} {% endblock %}

显示评论及点赞数

(样式参考虎嗅)

statics/bootstrap/css/目录下创建custom.css,用来定义图片及标题显示样式

custom.css

.page-container{
    border: 1px dashed darkcyan;
    padding-right: 150px;
    padding-left: 150px;
}


/*去除漂浮*/
.clear-both{
    clear: both;
}


/*左漂浮*/
.wrap-left{
    width: 75%;
    float: left;

}


/*右漂浮*/
.wrap-right{
    width: 25%;
    float: right;
    background-color: #9d9d9d;
}

.footer{
    height: 300px;
    background-color: #ac2925;
}

/*压缩图片*/
.article-head-img img{
    height: 150px;
    width: 200px;
}

/*每条新闻间距*/
.article-box{
    padding-bottom: 10px;
}

/*图片和标题之间距离*/
.article-brief{
    margin-left: -10px;
}


/*文章标题*/
.article-title{
    font-size: 20px;
}


/*a标签去掉下划线*/
a:hover{
    text-decoration: none;
}


/*字体样式*/
.body{
    color: rgb(51, 51, 51);
    font-family: Arial, 微软雅黑, "Microsoft yahei", "Hiragino Sans GB", "冬青黑体简体中文 w3", "Microsoft Yahei", "Hiragino Sans GB", "冬青黑体简体中文 w3", STXihei, 华文细黑, SimSun, 宋体, Heiti, 黑体, sans-serif;
}


/*简介样式*/
.article-brief-text{
    margin-top: 8px;
    color: #999;
}
View Code

修改base.html主题内容,引用custom.css中定义的样式

base.html

<!DOCTYPE html>
<!-- saved from url=(0048)http://v3.bootcss.com/examples/navbar-fixed-top/ -->
<html lang="zh-CN"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <meta name="description" content="">
    <meta name="author" content="">
    <link rel="icon" href="http://v3.bootcss.com/favicon.ico">

    <title>文艺社区</title>

    <!-- Bootstrap core CSS -->
    <link href="/static/bootstrap/css/bootstrap.min.css" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link href="/static/bootstrap/css/navbar-fixed-top.css" rel="stylesheet">
    <link href="/static/bootstrap/css/custom.css" rel="stylesheet">   <!--引用自定义的css样式文件-->
  </head>

  <body>

    <!-- Fixed navbar -->
    <nav class="navbar navbar-default navbar-fixed-top">
      <div class="container">
        <div class="navbar-header">
          <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
            <span class="sr-only">Toggle navigation</span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
            <span class="icon-bar"></span>
          </button>
          <a class="navbar-brand" href="http://v3.bootcss.com/examples/navbar-fixed-top/#">文艺社区</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
          {% block page-menu %}
          <ul class="nav navbar-nav">
            {% for category in category_list %}    <!--循环所有category-->
                {% if category.id == category_obj.id %}  <!--前端点击的一个category与所有category列表中某一个相等-->
                    <li class="active"><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
                {% else %}
                    <li class=""><a href="/blog/category/{{ category.id }}">{{ category.name }}</a></li>
                {% endif %}
            {% endfor %}
          </ul>
          {% endblock %}
          <ul class="nav navbar-nav navbar-right">
            {% if request.user.is_authenticated %}
                <li class="active"><a href="#">{{ request.user.userinfo.name }}</a></li>
                <li class="active"><a href="{% url 'logout' %}">注销</a></li>
            {% else %}
                <li class=""><a href="{% url 'login' %}">登陆/注册</a></li>
            {% endif %}
          </ul>
        </div><!--/.nav-collapse -->
      </div>
    </nav>

    <div class="page-container">  <!--内容部分-->
      {% block page-container %}
      <!-- Main component for a primary marketing message or call to action -->
      <div class="jumbotron">
        <h1>Welcome</h1>
        <p>
          <a class="btn btn-lg btn-primary" href="http://v3.bootcss.com/components/#navbar" role="button">View navbar docs »</a>
        </p>
      </div>
      {% endblock %}
    </div> <!-- /container -->

    <footer class="footer"></footer>
    <!-- Bootstrap core JavaScript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="/static/bootstrap/js/jquery-2.2.3.js"></script>
    <script src="/static/bootstrap/js/bootstrap.min.js"></script>

</body></html>
View Code

index.html 

{% extends 'blog/base.html' %}
{% load custom %}
    {% block page-container %}
    <div class="wrap-left">
        {% for article in article_list %}
            <div class="article-box row">
                <div class="article-head-img col-lg-4">  <!--图片在左-->
                    <img src="/static/{{ article.head_img | truncate_url}}">  <!--图片展示地址-->
                </div>
                <div class="article-brief col-lg-8">    <!--标题在右-->
                    <a class="article-title" href="#">{{ article.title }}</a>  <!--文章标题-->
                    <div class="article-title-info">
                        <span>{{ article.author.name }}</span>
                        <span>{{ article.pub_date }}</span>
                        <span>{% filter_comment article as comments %}</span>
                        <span class="glyphicon glyphicon-comment" aria-hidden="true"></span> <!--评论图表-->
                        <span>{{ comments.comment_count }}</span>  <!--评论数-->
                        <span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> <!--点赞图标-->
                        <span>{{ comments.thumb_count }}</span>   <!--点赞数-->
                        <div class="article-brief-text">
                            {{ article.brief }}
                        </div>
                    </div>
                </div>
            </div>
            <hr>
        {% endfor %}
    </div>
    <div class="wrap-right">
            rightfd
    </div>
    <div class="clear-both"></div>
{% endblock %}
View Code

点赞及评论的显示:

由于点赞和评论存在同一张表中,需分开统计。自定义标签:

custom.py

@register.simple_tag
def filter_comment(article_obj): #传递一个模型名字作为参数
    query_set = article_obj.comment_set.select_related()  #反向查询:comment关联了article,通过article查询comment,字段名_set.select_related()优化查询性能
    comments = {
        'comment_count':query_set.filter(comment_type=1).count(),#统计type为1的数量
        'thumb_count':query_set.filter(comment_type=2).count(),#统计type为2的数量
    }
    return comments

单个页面显示文章详细信息: 

新建页面article_detail.html来展示文章详细信息(包括标题,作者,发布时间,评论数量,图片等)

配置blog/urls.py:传入文章id

urlpatterns = [
    url(r'^$',views.index),
    url(r'category/(d+)',views.category),
    url(r'article_detail/(d+)',views.article_detail,name='article_detail'),
]

views.py:

def article_detail(request,article_id):
    article_obj = models.Article.objects.get(id=article_id)
    return render(request,'blog/article_detail.html',{'article_obj':article_obj,'category_list':category_list})  #category_list导航栏

article_detail.html页面依然继承base.html,并加载custom自定义的标签,

article_detail.html:

{% extends 'blog/base.html' %}
{% load custom %}
    {% block page-container %}
    <div class="wrap-left">
        <div class="article-title-bg">
            {{ article_obj.title }}
        </div>
        <div class="article-title-brief">
            <span>作者:{{ article_obj.author.name }}</span>
            <span>{{ article_obj.pub_date }}</span>
            {% filter_comment article_obj as comments %}
            <span class="glyphicon glyphicon-comment" aria-hidden="true"></span>
            <span>{{ comments.comment_count }}</span>
        </div>
        <div class="article-content">
            <img class="article-detail-head-img" src="/static/{{ article_obj.head_img | truncate_url }}">
            {{ article_obj.content }}
        </div>
    </div>
    <div class="wrap-right">
            rightfd
    </div>
    <div class="clear-both"></div>
{% endblock %}

在custom.css中添加文章页面中标题,图片及文章正文等样式:

custome.css

/*文章标题样式*/
.article-title-bg{
    font-size: 28px;
    margin: 0;
    position: inherit;
    line-height: 1.5;
    color: #333;
}


/*文章内容样式*/
.article-content{
    font-size: 16px;
    line-height: 30px;
}


/*文章标题下的简介样式*/
.article-title-brief{
    margin: 0 15px 0 10px;
    color: #999;
}


/*文章中图片样式*/
.article-detail-head-img{
    width: 100%;
    margin-top: 20px;
}

如何避免csrf攻击?

关于csrf攻击,请见<关于CSRF的攻击>

1、页面中加入token:

base.html:

<body> {% csrf_token %}
,,,
</body>

这时,浏览器请求该页面后,在<body>标签中会收到如下信息:

<input type="hidden" name="csrfmiddlewaretoken" value="Ire076FEoI50ntbU9qJHQZe0xEV1Et37">

该信息即服务端发给浏览器的token,浏览器再次请求服务端时,需将该token一并发送给服务端,以便服务端来验证浏览器身份:

article_detail.html:

<script>
    {#获取token#}
    function getCsrf(){
        return $("input[name='csrfmiddlewaretoken']").val();
    }

    $(document).ready(function(){
       $('.comment-box button').click(function(){
           var comment_text = $('.comment-box textarea').val();
           if (comment_text.trim().length<5){
               alert('评论内容不能少于5个字');
           }else {
               $.post('{% url 'post_comment' %}', {# 提交的url #}
                       {
                           'comment_type':1,
                           article_id:'{{ article_obj.id }}',
                           parent_comment_id:null,
                            'comment':comment_text.trim(),
                           'csrfmiddlewaretoken':getCsrf() {# 提交到服务端 #}
                       },

                       function(callback){
                           console.log(callback);
                       }
               )
           }
       });
    });
</script>

关于登陆后方可评论的逻辑:

1)默认用户在不登陆的情况下不能对文章进行评论,评论框提示'登陆后评论',并将'评论'连接到login_url。

2)用户登陆成功后需跳转到评论页面。

如何实现?

在'登陆'超链接中,拼接url,加入'next',其参数为当前request.path。

这样跳转到登陆页面时,url中会带有上篇文章的path。

在login视图函数的'HttpResponseRedirect' 方法加入该path参数,使其实现:若从评论登陆而来,则登陆成功后跳转回评论页面;若直接请求的主页,则跳转至'blog'主页面。

article_detail.html:

<div class="comment-box">
            {% if request.user.is_authenticated %} <!--如果已登陆-->
                <textarea class="comment-box" rows="3"></textarea>  <!--评论文本框,3行的高度-->
                <button type="button" style="margin-top: 10px" class="btn btn-success pull-right">评论</button>
            {% else %}
                <div class="jumbotron">
                    <h4 class="text-center"><a class="btn-link" href="{% url 'login' %}?next={{ request.path }}">登陆</a>后评论</h4>
                </div>
            {% endif %}
        </div>

views.py中login函数的修改:

def user_login(request):
    if request.method == 'POST':
        user = authenticate(username = request.POST.get('username'),password = request.POST.get('password'))
        if user is not None:
            login(request,user)
            return HttpResponseRedirect(request.GET.get('next') or '/blog')
        else:
            login_error = 'wrong username or password!'
            return render(request,'blog/login.html',{'login_error':login_error})
    return render(request,'blog/login.html')

提交评论到后端数据库:

1、在视图函数add_comment中,初始化一个models.Comment对象,用来存放前端提交过来的数据(按照comment表的格式定义),然后调用.save()方法写入数据库。

2、成功写入数据库后,向前端返回字符串表示提交评论成功,前端提示用户。

以上过程,在前端和后端函数交互过程(通过ajax提交(post)或者获取(get))中传递的信息存放在callback()函数中。

views.py:

def add_comment(request):
    if request.method=='POST':
        new_comment_obj = models.Comment(
            article_id = request.POST.get('article_id'),
            parent_comment_id = request.POST.get('parent_comment_id') or None,     #父评论可以为空
            comment_type = request.POST.get('comment_type'),
            user_id = request.user.userinfo.id,
            comment = request.POST.get('comment')
        )
        new_comment_obj.save()

    return HttpResponse('add comment success')

前端页面article_detail.html:

<script>
    {#获取token#}
    function getCsrf(){
        return $("input[name='csrfmiddlewaretoken']").val();
    }

    $(document).ready(function(){
       $('.comment-box button').click(function(){
           var comment_text = $('.comment-box textarea').val();
           if (comment_text.trim().length<5){
               alert('评论内容不能少于5个字');
           }else {
               $.post('{% url 'post_comment' %}', {# 提交的url #}
                       {
                           'comment_type':1,
                           article_id:'{{ article_obj.id }}',
                           parent_comment_id:null,
                           'comment':comment_text.trim(),
                           'csrfmiddlewaretoken':getCsrf() {# 提交到服务端 #}
                       },

                       function(callback){
                           console.log(callback);
                           if (callback == 'add comment success'){      #后端返回的数据
                               alert('添加评论成功!')
                           }
                       }
               )
           }
       });
    });
</script>

前端页面展示评论的主要逻辑流程:

1、当页面加载时,在页面底部展示该文章所有的评论信息。

2、对文章提交评论后,ajax重新从后台查询最新的评论在前端页面展示。

3、对评论进行评论,点击评论图标时,会将comment-box(包含评论框和提交按钮)复制一份放在被评论的下方,同时原comment-box将被删除。

4、提交对评论的评论时,前端页面重新获取comment-list,并将原来的comment-box放回原来位置。

在展示父评论与子评论时,需要拼接字符串,父子评论之间使用margin-left拼接,以显示对应关系。

设置提交评论和获取评论url:

urlpatterns = [
    url(r'comment/$',views.add_comment,name='post_comment'),
    url(r'comment_list/(d+)/$',views.get_comments,name='get_comments'),
]

 视图函数views.py:

def add_comment(request):
    if request.method=='POST':
        new_comment_obj = models.Comment(
            article_id = request.POST.get('article_id'),
            parent_comment_id = request.POST.get('parent_comment_id') or None,
            comment_type = request.POST.get('comment_type'),
            user_id = request.user.id,
            comment = request.POST.get('comment')
        )
        new_comment_obj.save()

    return HttpResponse('add comment success')


#返回给前端之前,首先生成评论树,然后拼接成字符串
def get_comments(request,article_id):
    article_obj = models.Article.objects.get(id=article_id)
    comment_tree = comment_hander.build_tree(article_obj.comment_set.select_related())#生成树
    tree_html = comment_hander.render_comment_tree(comment_tree)   #拼接字符串
    return HttpResponse(tree_html)

拼接字符串在后端函数中处理:

新建comment_hander.py,用于处理评论字符串的拼接:

#! _*_ coding:utf-8 _*_

'''
生成评论树
'''

def add_node(tree_dic,comment):
    if comment.parent_comment is None:   #如果该评论没有父评论,则将自己放在顶层(父评论)位置
        tree_dic[comment]={}
    else: #如果有父评论,则循环该字典,找到父评论,将自己放父评论后面
        for k,v in tree_dic.items():
            if k == comment.parent_comment: #找到父评论
                tree_dic[comment.parent_comment][comment]={}
            else:
                add_node(v,comment) #继续递归查找



def build_tree(comment_set):
    tree_dic = {}
    for comment in comment_set:
        add_node(tree_dic,comment)
    return tree_dic



#用于递归字典
#遍历字典,取完第一层递归取第二层,<div>父</div>与<div>子</div>之间使用margin-left连接
def render_tree_node(tree_dic,margin_val):
    html = ''
    for k,v in tree_dic.items():
        ele = "<div class='comment-node' style='margin-left:%spx'>" % margin_val + k.comment 
              + "<span style='margin-left:10px'>%s</span>" % k.user.name 
              + "<span comment-id= '%s'" % k.id + "style='margin-left:10px' class='glyphicon glyphicon-comment add-comment' aria-hidden='true'></span>" 
              +"</div>"  #子评论
        html += ele
        html += render_tree_node(v,margin_val+10)
    return html


#拼接字符串
def render_comment_tree(tree_dic):
    html=''
    for k,v in tree_dic.items():
        ele = "<div class='root-comment'>" + k.comment 
              + '<span style="margin-left:10px">'+ k.user.name +'</span>' 
              +'<span comment-id= "%s"' % k.id + 'style="margin-left:10px" class="glyphicon glyphicon-comment add-comment" aria-hidden="true"></span>'
              + "</div>"  #只对父评论生效
        html += ele
        html += render_tree_node(v,10)
    return html
原文地址:https://www.cnblogs.com/ahaii/p/5761628.html