去病率兵十万深入敌后痛击匈奴,经此一役,胡虏元气甚伤,单于伊治斜弃河西平原而迁王庭至漠北。汉乃设酒泉、张掖、敦煌、武威河西四郡。众将凯旋回师,帝召大将军卫青、骠骑将军霍去病、飞将军李广、苏建、公孙敖、公孙贺等将议于宣室殿。众将皆以需涉荒漠为困而欲弃之。然武帝废胡之心愈坚,怒曰:汝皆以为不可,而胡虏亦曰寡人不可为之,然朕必举全国之力誓击之,永绝胡虏之患!
遂点精骑十万,以大将军五万兵出定襄,骠骑将军五万出代郡,辎重步卒五十万,挥师远征漠北!
一、兵马未动,辎重先行。——表结构的设计
(参照抽屉新热榜)
在论坛中文章包含的属性(数据库中的字段)有标题、简介、板块、内容、作者、发布时间、最后一次修改时间、优先级(数字权重)。
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; }
修改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>
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 %}
点赞及评论的显示:
由于点赞和评论存在同一张表中,需分开统计。自定义标签:
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