python2.0_day21_bbs系统评论自动加载+文章创建
day20中我们已经实现了bbs系统的前端展示,后台admin管理,以及前端动态显示顶部\登录和评论的分级展示功能.其中评论的分级展示功能最为复杂.
上一节中我们只是在文章明细页面中加了一个button按钮,能够获得该文章的评论内容.但是我们知道正常的论坛网站,在打开某一篇文章的时候就会显示该文章的评论内容.
本节我们主要实现的几个功能:1.评论自动加载 2.可以添加子评论. 3.评论框的动态显示 4.新文章的建立要求使用富文本编辑框 5.Django框架通过前端页面上传文件的用法.
一\首先我们看上节中我们实现获取评论的代码,就是templates/bbs/article_detail.html页面中js;
有这么一个标签:
<button type="button" onclick="GetComments()">测试获取评论</button> 然后定义了一个js: function GetComments(){ $.get("{% url 'get_comments' article_obj.id %}",function(callback){ console.log(callback); $(".comment-list").html(callback); }); } 那么我们想让他自动加载,无非就是在页面加载后调用一下,也就是加载到那个$(document).ready(function(){})中,等加载好文档树后在function中执行 $(document).ready(function(){ GetComments(); //页面加载后,调用此函数获取当前文章的的评论 $(".comment-box .btn").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 5){ alert("评论不能少于5个字sb") }else{ //post $.post("{% url 'post_comment' %}", { 'comment_type':1, 'article_id':"{{ article_obj.id }}", parent_commet_id:null, 'comment':comment_text.trim(), 'csrfmiddlewaretoken':getCsrf() },//end post args function(callback){ console.log(callback) if (callback == 'post-comment-success'){ alert('successful') } });//end post }; });//end button click });
还有当我们新创建一个评论时也要把新评论的内容加载出来.所以我们应该在评论添加成功后,再次调用GetComments()函数.所以最后是在回调函数中调用.
于是上面的代码改成:
$(document).ready(function(){ GetComments(); //页面加载后,调用此函数获取当前文章的的评论 $(".comment-box .btn").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 5){ alert("评论不能少于5个字sb") }else{ //post $.post("{% url 'post_comment' %}", { 'comment_type':1, 'article_id':"{{ article_obj.id }}", parent_commet_id:null, 'comment':comment_text.trim(), 'csrfmiddlewaretoken':getCsrf() },//end post args function(callback){ console.log(callback) if (callback == 'post-comment-success'){ //alert('successful'); GetComments();//新的评论添加成功后,再次刷新评论. } });//end post }; });//end button click });
完成上面的代码.我们就可以实现自动加载页面和提交评论后自动加载页面了.紧接着问题来了.我们这里只能添加一级评论,不能添加子评论.接下来我们就来实现如何添加子评论.
我们先为每一个评论加一个评论按钮吧.因为评论内容都是在后台生成后直接返回给前端页面是html文本内容.所以我们要为每一个评论再加一个"回复"按钮,也要在后台的函数中修改添加.
这里又一个问题 ,我们要为每一条评论添加一个onclick事件.同时又要考虑,当你新加评论时使用的是ajax方式,而不是form方式,所以js是不会再次执行的.也就是新加的评论不会绑定onclick事件.
所以此时就要用到day14节中提到的知识委托.委托的核心概念就是监控上级标签下的指定标签类型,一旦子标签有变化就会重新绑定.
理论上我们就可以这么做.但是我们先从普通的绑定方法下手.看看究竟会发生什么?
查看我们更改的后台的函数的更改的部分
和
按照常理来说,这里应该是绑定了click事件,但是通过代码来看却没有绑定,这是什么问题呢?
原因是这样的,关于评论的获取也是通过调用js获取的.而我们绑定事件也是通过js实现的.那么问题来了.html语言解释器(浏览器)虽然是自上而下解释代码的,但是对于获取评论的js函数是非阻塞的,也就是调用GetComments()函数后,就立马之行绑定函数.
问题就在于javascript里的函数都是非足赛的,那么在前面一个函数还没执行完成,即还没有返回评论相关的html代码,后面就开始绑定,当然是找不到任何关于.add-commnet的标签了.所以上图显示无任何click事件.
紧接着问题来了,那么我们就应该在GetComments()函数返回结果后,在进行绑定函数的调用.那么怎么判断之行完成了呢.最简单的就是不判断,把绑定这部分代码直接放到GetComments()函数中去.放到回调函数中不就行了.
紧接着我们测试下结果:
我们随便点点,看到结果打印了.
二\使用js里的委托方法实现上述功能
那么问题来了,我们如果用委托,并且不写在GetComments()函数里面是不是也能实现,必定委托是有监控作用的.
我们来测试下,把代码改成如下:
访问测试结果:
我们看使用委托绑定事件的方法也是可以实现的.都可以,那么我们就用委托把.
三/ 绑定事件完成,我们把事件的内容换成把评论框拉到该评论的下方.
我们看在代码中我们在.comment-box类下就写了一个评论框,我们考虑能不能在点击回复图标的时候,就在这个标签的下面克隆一个评论框.前提的评论框和提交按钮这两个标签应该在一个div下才好克隆,
于是我们在这两个标签上层加一个div标签,class='new-comment-box'类.代码如下:
接下来我们就要在绑定事件的地方,实现克隆这个new-comment-box标签了
代码如下:
我们看看测试结果
我们看到克隆是成功了,但是存在两个问题,问题1.是之前的那个在comment-box里的应该删掉, 问题2 另一个是多个评论框既然能同时存在.
我们先解决问题1
我们可以在克隆后,把前面一个new-comment-box删除.于是代码改成如下:
访问测试:
这就实现了同时只有1个评论输入框.
但是还是有问题,当我们想对文章进行评论,而不是对评论进行评论.怎么办.现在看来是不行.
其实我们可以在评论处也加一个按钮,实现文章旁边的图标,再把评论输入框移过来.代码如下
这下我们看效果如图
其实现在还存在三个问题:
1.我们使用clone()虽然把标签克隆到新的位置了,但是button标签的绑定事件是没有克隆的,也就是说只要clone一次评论输入框后,提交就失效了.
那么如何解决把click事件也克隆呢?
2.我们之前做提交评论时,使用的是ajax异步提交.提交时父级评论的id也就是parent_commet_id:null,默认都是没有的,现在要根据实际情况加上.
3.我们知道当我们点击评论图标时候会有评论输入框,点击输入提交后,在点击任何评论图标都是没有反应的.这个是为什么,怎么解决?
我们先解决问题1
三种方案,方案一直接在button中加一个onclick事件属性.方案二:每次克隆后,在绑定一次. 方案三.clone()函数直接加一个参数就可以把click事件也克隆了.
我们使用方案三 .clone(true) 加一个true即可.
代码如下:
下面我们来解决问题2 ,为评论添加子评论.
此问题解决不难,只要在用$.post提交时获取到父级评论的id即可.js 代码如下:
解决问题3 评论提交后为什么评论框就消失了?
因为提交评论后我们在JS代码中又再次之行获取评论的方法.而获取评论的方法会把comment-list里的html代码覆盖掉.从而导致new-comment-box这个评论输入框和提交评论的按钮div消失了.
所以你在点也没有用.解决办法就是在提交评论后把.new-comment-box 这个类的标签插入到comment-box标签的内部的前面(之所以是内部的前面是因为之前就放这).
代码如下
至此评论这边就没啥问题了.
四\bbs创建新贴页面开发
首先我们要在现有的页面中添加一个创建新帖的入口 a标签.所以我们要修改base.html,放在右上角
<ul class="nav navbar-nav navbar-right"> {% if request.user.is_authenticated %} <li><a href="#">{{request.user.userprofile.name}}</a></li> <li><a href="{% url 'logout' %}">logout</a></li> {% else %} <li><a href="{% url 'login' %}">登录/注册</a></li> {% endif %} <li class=""><a href="{% url 'new-article' %}">发贴</a></li> </ul>
然后在bbs/urls.py文件中创建这个路由条目且别名为new-article
如:
urlpatterns = [ url(r'^$', views.index), url(r'^category/(\d+)/$',views.category,name='category_detail'), url(r'^detail/(\d+)/$',views.ariticle_detail,name='article_detail'), url(r'^comment/$',views.comment,name='post_comment'), url(r'^comment_list/(\d+)/$',views.get_comments,name='get_comments'), url(r'^new_article/$',views.new_article,name='new-article'), ]
其次在视图文件bbs/viewspy中创建new_article函数.如:
def new_article(request): if request.method=="GET": return render(request,'bbs/new_article.html') #如果是GET就是浏览创建新贴的页面,如果是POST才是提交.
然后我们在创建这个页面.内容如下:
{% extends 'base.html' %} {% load custom_tags %} {% block page-container %} <div class="wrap-left"> dddd </div> <div class="wrap-right"> sss </div> <div class="clear-both"></div> {% endblock %}
我们访问首页和点击"发帖"查看跳转页面如下:
和
接下来我们来使用modelform来实现新帖的创建,和后端数据的验证.
modelform 的使用可以参考 之前的章节 python2.0_day19_充分使用Django_form实现前端操作后台数据库
1.首先创建bbs/forms.py文件,并添加如下内容:
#_*_coding:utf-8_*_ __author__ = 'ZhouMing' from django.forms import Form,ModelForm from bbs import models class ArticleModelForm(ModelForm): class Meta: model = models.Article exclude = ()
2.在bbs/views.py文件中引用ArticleModelForm类,实例化对象并返回给前端
def new_article(request): if request.method=="GET": form_obj = forms.ArticleModelForm() #实例化form类 return render(request,'bbs/new_article.html',{'form_obj':form_obj,})
3.接着我们更改templates/bbs/new_article.html文件内容如下:
{% extends 'base.html' %} {% load custom_tags %} {% block page-container %} <div class="wrap-left"> {{ form_obj }} #这里我们前端模版语言取到这个form_obj </div> <div class="wrap-right"> sss </div> <div class="clear-both"></div> {% endblock %}
4.访问测试如图:
我们看前端已经获取到了form类,接下来还是先美化下.
还是form的知识点,我们知道form不仅提供字段数据还提供html代码,所以改样式也要在form类中改,于是更改bbs/forms.py文件如下:
#_*_coding:utf-8_*_ __author__ = 'ZhouMing' from django.forms import Form,ModelForm from bbs import models class ArticleModelForm(ModelForm): class Meta: model = models.Article exclude = () # 先继承,再重写(想重新定义某些字段的属性,必须先继承,在重写,就这样记着) def __init__(self,*args,**kwargs): super(ArticleModelForm,self).__init__(*args,**kwargs) # self.fields['qq'].widget.attrs["class"] = "form-control" # for循环每一个字段,修改字段的class属性 for field_name in self.base_fields: #self.base_fields说白了就是把所有字段取出来但是是字典的形势 field = self.base_fields[field_name] #这里可以是上面的例子field.widget.attrs["class"] = "form-control",也可以用update,用update的好处就是可以同时修改多个属性了 field.widget.attrs.update({'class':"form-control"}) # class = form-control 这是bootstrap里的样式.
5.我们再次访问测试结果如下:
我们看样式变得好看了.但是这里要引入一个新知识点.就是 帖子的正文,不能单纯的是输入框,而是富文本编辑框.
富文本编辑框我们可以使用百度的富文本编辑器.
有几个比较出名的:国内的有一个百度出的叫做 ueditor,但是它默认不支持Django,支持java和PHP 和c++,如果想支持Django,但是很麻烦.有时间还是要研究下,因为它很强大,做得就跟word似的.
现在用的比较多的是国外的一款ckeditor,基本功能够用,它也有很强大的功能,但是是付费的.有时间还是去研究下ueditor
我们这里就使用ckeditor了,访问http://ckeditor.com/download,下载ckeditor.
这个安装使用非常简单,直接把下载下来的包,拷贝到statics目录下,创建一个新目录为plugins.然后把整个ckeditor目录放到statics/plugins/ckeditor
我们来看应该在html代码中如何调用:
我们按照上面的配置方法来实现看看效果先.
我们看到效果了,我们让这个富文本替换我们modelform里帖子文本字段,就好了.
我们看form里返回给前端的文章内容输入框的标签是有id的我们只需要把上面替换的内容改成这个id就好了
访问页面结果如图:
然后把不必要的字段都给屏蔽掉.比如:作者\发布时间\优先级等
修改bbs/forms.py文件如下:
我们来看看结果:
上图中我们看到可以传图片.这是一个新知识点!你会说后台不就可以传图片嘛!这怎么会是新知识点呢?问题就在于后台可以传,用form后在前端传图片就有问题了.
具体问题如图:
首先我们要在前端加一个提交的按钮commit
如图
加一个form标签.
后端还要在视图函数中添加POST请求类型的处理方法.
def new_article(request): if request.method=="GET": article_form = forms.ArticleModelForm() #实例化form类 return render(request,'bbs/new_article.html',{'article_form':article_form,}) elif request.method == 'POST': print(request.POST) article_form = forms.ArticleModelForm(request.POST) if article_form.is_valid(): article_form.save() else: return render(request,'bbs/new_article.html',{'article_form':article_form,}) 这里返回的article_form,就是进行验证后的内容了所以错误会在相应的标签中显示 return HttpResponse('创建成功')
我们在页面上填写信息,并且上传图片.结果在提交的时候图片虽然已经上传但是还是提示要上传,情况如下:
这是为什么呢?
我们先来看看后台打印出来的request.POST里有没有上传的图片信息:
我们看其实后台是接收到前端上传文件的信息了,但为什么还提示错误呢?
注意了,原因是如果在Django中从前端上传图片或者文件到后台,是要单独的使用request.FILES获得的,而不是request.POST里获取的.
甭管前端是通过FORM方式传还是通过ajax方式上传,都是使用request.FILES获取,这一点要彻底记住.
更改视图如下:
我们把视图函数改下在看看结果如何:
我们在看看后端视图的打印内容:
为空,此时后端是没有问题的,已经通过request.POST获得都 了,那么为什么为空?原因是前端没有传过来.
这又有一点要记住的:如果你在前端需要提交文件或者图片到后端,form的格式要改,改成如下:
这是一个前端的知识点
现在我们再次测试提交图片看看结果:
看到上面的错误信息,最起码说明图片我们上传成功了,并且后端也接收到了,这个错误的原因知识在使用form保存数据到models.Article类时有必填的值没填.
在之前的章节中我们学过 form对象.cleaned_data 以字典的方式显示form表单里提交过来的数据.
记住form对象.cleaned_data 是一个属性,你不能对它进行更改和新加元素.
另外 cleaned_data属性只能在form实例调用过.is_valid() 方法后才能使用,否则不显示字典.
那么既然我们不能对form对象里的元素进行更改.也就意味着 前端传过来啥子form就是啥子,后面不能加元素,那么就不能使用 form对象.save()进行添加models对象了.
于是我们想添加元素的思路只能是先获取form里的字典,然后添加元素,在调用models生成对象,使用models对象.save()方法.
再次提交,点击创建就成功了.前提是你登录过了,如果你没登录过是没办法使用request.user.userprifile.id获得用户id的.
当然我们要为此视图函数加上用户验证的装饰器.
这里又多了一个知识点:装饰器@login_required(login_url='/login/') 直接指定如果没登录会跳转到哪个url
这个low,因为如果有多个函数要做验证,你就要在每一个上面写这个.
有没有办法,只写一次,其它函数调用的时候还是像以前一样调用装饰器@login_required
有,就是在settings.py文件里设置.
这里我们之所以url写成'/login/',是因为我们前面在提交评论时写了登录页面的url是这个,
而使用@login_required装饰器对视图函数验证时Django框架里默认的url是'/accounts/login/'
我们上图改的就是为@login_required装饰器更改验证不通过时跳转的登录url.
下图中表示我们自己写的前端创建新贴成功,但是还是存在一定的问题.就是我们的文档内容却是标签,如图
那么为什么是标签呢?因为富文本编辑框存通过form提交到post到后台,后台存的字段内的值就是标签.
那么我们在前端就需要把这些带有标签的字段渲染成html页面内容.怎么渲染呢,这里又有一个前端知识点:使用内置的filter标签|safe
我们要在article_detail.html页面展示文章内容,所以我们就要把|safe 添加到article_detail.html页面中,具体位置如下图:
我们再次刷新页面看结果如图:
前端显示列表时应该反转,让最新发布的帖子放在前面.于是我们修改index.hmlt的页面代码如下:
结果:
四\详细介绍前端创建文件
上面的我们用form类实现了上传文件.但是实际开发中我们可能有不用form类的时候.也就是需要我们写上传文件的前端和后端.
首先对于上传文件,官网上有那么一段英文解释:
Note that request.FILES will only contain data if the request method was POST and the <form> that posted the request has the attribute enctype="multipart/form-data". Otherwise, request.FILES will be empty.
意思就是:使用request.FILES获得上传文件的前提条件两个: 1.前端POST方式提交的 2.FORM表单要有enctype="multipart/form-data"参数
但是你知道文件是怎么从浏览器中读到后台的吗?当上传的文件很大的时候.如果通过form表单传文件的时候,你会发现,一点summit按钮就白屏了.
为什么会白屏呢?因为浏览器要花很长时间把文件读进去并且发送给服务端.我们知道大白屏很不友好.那么用什么方式提交比较好呢?当然是ajax异步提交了.现实中的例子,百度的云盘如果提交上传一个2G文件,它就不是白屏,不然2小时白屏,用户会疯的.
百度云盘会有一个进度条,然后在那不停的上传,这就是采用ajax异步上传实现的.进度条的实现和我们之前学习过的socket上传实现原理是一样的.就是前端上传多少纪录下来,然后显示百分比.
关键是后端怎么处理,后端如果用form,form对象.save()就一次性保存了.另外一个问题,如果文件是2M后台可以先把接收到的文件放到内存中,但如果是2G,有10个用户都在上传,那服务器还不爆炸.
所以后台要自己写解决上面的问题.
首先Django考虑到大文件的问题,并且Django框架内部有规则:如果你上传的文件小于2.5M,是可以保存在内存中的.直至文件接收完成后保存在upload文件夹下.所以此时速度非常快
如果文件大于2.5M,Django默认会把接收到的文件存在一个临时目录里.这个临时目录在unix系统中的/tmp/目录下
我们看下例子:
实现在new_article.html页面中添加一个form表单用于上传,上传的URL可以新定义一个,这样就可以提供给所有需要上传的页面来调用了.
new_article.html页面添加内容如下:
bbs/urls.py文件内容添加如下:
bbs/views.py文件视图函数添加如下:
我们分别上传一个小于2.5M的文件,和一个大于2.5M的文件,看看前端和后端的反应
前端反应:当文件较大时,上传时卡住了. 这个用户体验很差
后端:
当文件小于2.5M时打印print(request.FILES)结果如图
当文件大于2.5M时打印print(request.FILES)结果如图
了解到Django对大小文件上传的处理方式不一样后,下面我们要解决的是怎么把这些文件真正存下来.
Django官方上实现了方法,如图:
于是我们的视图函数参照官网,更改成如下:
这时候我们在做上传就会发现upload目录下有文件了.并且我们看下图知道,这是前端边上传,后端边保存的.
至此上传文件的后端操作就结束了.接下来想实现进度条的效果,就需要将form改成ajax的方式了.具体怎么做,当作作业回去研究.
五\在页面顶部如何提示有"n条新消息".
我个人在使用Django框架,开发系统时,有一点不清楚的:比如一个新功能涉及到urls.py文件views.py文件还有html文件.
那对于一个功能的开发,到底从哪一个开始入手,能不能总结出一套方法呢.
能,我觉得我找到方法了.
1.主要看request请求的出发点.
浏览器输入网址 -> 更改urls.py文件 -> 更改视图函数文件views.py -> 请求的返回返回json数据或者返回新的模版页面*.html
页面中a标签的点击 -> 更改urls.py文件 -> 更改视图函数文件views.py -> 请求的返回返回json数据或者返回新的模版页面*.html
js中$.post和$.get 方式的提交 -> 更改urls.py文件 -> 更改视图函数文件views.py -> 请求的返回返回json数据
form 的post和get的action提交地址 -> 更改urls.py文件 -> 更改视图函数文件views.py -> 模版页面*.html
所以无论哪中方式都要从请求的出发点入手.
总结:无论是什么请求,都可以归纳成上面四步:
1.请求的起点
2.编辑urls.py路由
3.后台views.py中的函数编辑,处理后返回给请求源.
4.前端页面获得数据进行渲染展示.
所以这里我们的思路是:
在js中定义一个定时任务,在定时任务里使用$.getJSON方法获取后视图函数查找到的新帖子的数量.
1.首先是先要写的是这个定时任务,以及定时人中中$.getJSON方法访问的url和传递的参数.
2.然后更改urls.py文件,添加$.getJSON请求的URL
3.写后台的视图函数.由于使用的$getJSON方法获取的,所以返回json的数据即可
4.既然没有新页面,请求的源页面拿到数据后进行展示即可.在改$.getJSON中的回调函数即可.
所以实现上述四步的实际代码是:
首先在index.html中写定时任务如图:
然后我们要在urls.py中添加上图中$.getJSON()请求的地址
然后我们在写后台处理的函数.
最后就是编辑回调函数,实现有新消息显示在顶部,没有则不显示.在这之前我们先看看后台返回的数据前端能接收到吗?
如图:
我们从图中可以看到,js的定时任务执行成功,并且后台以json格式返回新帖的数量.
接着我们就可以更改回调函数,把得到的新贴数量的消息展现在前端了.
这里为什么要在$(“.wrap-left”).children()[1].attr(‘article_id’)的外层在加一个$()呢?
因为你用切片[1]获得的是一个标签,而不是一个document 的obj了。就不能调用
其它方法了,在外面加一个$()是把一个标签实例化成一个obj了,在次能够使用方法了.
这时候我们看看访问效果如图:
至此我们的bbs的一些基本页面算是ok了,接下来就是实现WEB聊天了.
码农小明