项目:开发一个简单的BBS论坛
需求:
- 整体参考“抽屉新热榜” + “虎嗅网”
- 实现不同论坛版块
- 帖子列表展示
- 帖子评论数、点赞数展示
- 在线用户展示
- 允许登录用户发贴、评论、点赞
- 允许上传文件
- 帖子可被置顶
- 可进行多级评论
- 就先这些吧。。。
知识必备:
- Django
- HTMLCSSJS
- BootStrap
- Jquery
1.设计表结构
在做项目之前只要涉及到数据库,就一定要把表结构想清楚,表结构没有做出来,就不要动手写代码,这是做开发最起码要坚持的一个原则。
表结构可以帮你理清思路,表结构是体现了你业务逻辑关系的。
from django.db import models from django.contrib.auth.models import User from django.core.exceptions import ValidationError import datetime # Create your models here. class Article(models.Model): # 文章标题可以重名,不同的用户id就可以分别 title = models.CharField(max_length=255) # 简介可以为空 brief = models.CharField(null=True,blank=True,max_length=255) # 所属版块 Category类位于Article下面时,调用需要加上引号 category = models.ForeignKey("Category") content = models.TextField(u"文章内容") author = models.ForeignKey("UserProfile") # auto_now 和 auto_now_add 区别? # 每次对象修改了,保存都会更新auto_now的最新时间 # 每次对象创建的时候,会生成auto_now_add 时间 pub_date = models.DateTimeField(blank=True, null=True) # 为什么不写auto_now_add? last_modify = models.DateTimeField(auto_now=True) # 文章置顶功能 priority = models.IntegerField(u"优先级",default=1000) head_img = models.ImageField(u"文章标题图片",upload_to="uploads") status_choices = (('draft',u"草稿"), ('published',u"已发布"), ('hidden',u"隐藏"), ) status = models.CharField(choices=status_choices,default='published',max_length=32) def __str__(self): return self.title def clean(self): # 自定义model验证(除django提供的外required max_length 。。。),验证model字段的值是否合法 # Don't allow draft entries to have a pub_date. if self.status == 'draft' and self.pub_date is not None: raise ValidationError(('Draft entries may not have a publication date.')) # Set the pub_date for published items if it hasn't been set already. if self.status == 'published' and self.pub_date is None: self.pub_date = datetime.date.today() class Comment(models.Model): article = models.ForeignKey(Article,verbose_name=u"所属文章") # 关联到同一张表的时候需要关联自己用self,当关联自己以后 想反向查找需要通过related_name来查, # 顶级评论不用包含父评论 parent_comment = models.ForeignKey('self',related_name='my_children',blank=True,null=True) comment_choices = ((1,u'评论'), (2,u"点赞")) comment_type = models.IntegerField(choices=comment_choices,default=1) user = models.ForeignKey("UserProfile") comment = models.TextField(blank=True,null=True)# 问题来了? 点赞不用内容,但是评论要内容啊!!! date = models.DateTimeField(auto_now_add=True) # django clean方法可以实现表字段验证 #Model.clean()[source] #This method should be used to provide custom model validation, and to modify attributes on your model if desired. # For instance, you could use it to automatically provide a value for a field, # or to do validation that requires access to more than a single field: def clean(self): if self.comment_type == 1 and len(self.comment) ==0: raise ValidationError(u'评论内容不能为空,sb') def __str__(self): return "C:%s" %(self.comment) class Category(models.Model): name = models.CharField(max_length=64,unique=True) brief = models.CharField(null=True,blank=True,max_length=255) # 一般页面的版块是固定死的,但是我们想动态生成版块的时候,我们需要定义一个位置字段和是否显示字段 # 一般常规的网站首页都是固定的 set_as_top_menu = models.BooleanField(default=False) position_index = models.SmallIntegerField()
# 可以有多个管理员 admins = models.ManyToManyField("UserProfile",blank=True) def __str__(self): return self.name class UserProfile(models.Model): """ 在用户表中定义一个friends 字段,关联自己 """ user = models.OneToOneField(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) #for web qq friends = models.ManyToManyField('self',related_name="my_friends",blank=True) def __str__(self): return self.name
配置Django Admin
from django.contrib import admin from bbs import models # Register your models here. class ArticleAdmin(admin.ModelAdmin): list_display = ('title','category','author','pub_date','last_modify','status','priority') 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_index') admin.site.register(models.Article,ArticleAdmin) admin.site.register(models.Comment, CommentAdmin) admin.site.register(models.UserProfile) admin.site.register(models.Category,CategoryAdmin)
选择合适的前端模板
....
前端实现动态菜单
首页菜单实现动态展示,首先将版块取出来,排序(在数据库中放两个字段,一个是是否可以展示在前端首页,另一个是它放置的位置),展示不同版块的内容,当前版块高亮显示。有两种实现方式:通过js获取当前url对比;从数据库返回当前模块
{% block top-menu %} <ul class="nav navbar-nav"> {% for category in category_list %} //1.循环后端返回的版块列表 2.后端需要返回当前版块 3.当前版块id同循环版块列表对比 4.给当前版块加上active {% if category.id == category_obj.id %} <li class="active"><a href="{% url 'category_list' category.id %}">{{ category.name }}</a></li> {% else %} <li class=""><a href="{% url 'category_list' category.id %}">{{ category.name }}</a></li> {% endif %} {% endfor %} </ul> {% endblock %}
category_list = models.Category.objects.filter(set_as_top_menu=True,).order_by('position_index') def index(request): category_obj = models.Category.objects.get(position_index=1) #首页需要acitve cls,手动定义返回 article_list = models.Article.objects.filter(status='published') return render(request, 'bbs/index.html', {'category_list': category_list, 'article_list': article_list, 'category_obj': category_obj, }) def category(request, id): #获取板块对象 category_obj = models.Category.objects.get(position_index=id) if category_obj.id == 1: #判断首页 article_list = models.Article.objects.filter(status='published') #获取已发布的文章 else:#获取当前板块下已发布的文章 article_list = models.Article.objects.filter(category_id=category_obj.id, status='published') return render(request, 'bbs/index.html', {'category_list': category_list, 'article_list': article_list, 'category_obj': category_obj, })
文章评论功能
当我们浏览完文章,想评论的时候,提示登录,当登录成功后返回到当前页面,这个要怎么实现呢?
{% if request.user.is_authenticated %} //验证用户是否登录 <textarea class="form-control" rows="3" placeholder="客官8字起评,不讲价哟"></textarea> <button type="button" style="margin-top: 10px" class="btn btn-success pull-right">评论</button> <div style="clear:both;" ></div> {% else %}//登录成功 返回的a标签 href=当前url后面加上?next跟当前路径 在登录后才会返回当前页面! <div class="jumbotron"> <h4 class="text-center"><a class="btn-link" href="{% url 'login' %}?next={{ request.path }}">登录</a>后评论</h4> </div> {% endif %}
跨站请求伪造保护
CSRF 中间件和模板标签提供对跨站请求伪造简单易用的防护。某些恶意网站上包含链接、表单按钮或者JavaScript ,它们会利用登录过的用户在浏览器中的认证信息试图在你的网站上完成某些操作,这就是跨站攻击。还有另外一种相关的攻击叫做“登录CSRF”,攻击站点触发用户浏览器用其它人的认证信息登录到其它站点。
全局:
中间件 django.middleware.csrf.CsrfViewMiddleware
局部:
- @csrf_protect,为当前函数强制设置防跨站请求伪造功能,即便settings中没有设置全局中间件。
- @csrf_exempt,取消当前函数防跨站请求伪造功能,即便settings中设置了全局中间件。
注:from django.views.decorators.csrf import csrf_exempt,csrf_protect
使用csrf:
1.普通表单
veiw中设置返回值: return render_to_response('Account/Login.html',data,context=RequestContext(request))
或者 return render(request, 'xxx.html', data) html中设置Token: <form action="." method="post"> {% csrf_token %}
2.ajax
虽然上面的方法可以用于AJAX POST 请求,但是它不太方便:你必须记住在每个POST 请求的数据中传递CSRF token。
由于这个原因,还有另外一种方法:在每个XMLHttpRequest 上设置一个自定义的X-CSRFToken 头部,其值为CSRF token。
注:
CSRF token 的Cookie 默认叫做csrftoken,你可以通过CSRF_COOKIE_NAME 设置自定义它的名字。
CSRF header 默认叫做 HTTP_X_CSRFTOKEN
,你可以通过
CSRF_HEADER_NAME
设置自定义它的名字。
// using jQuery function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken');
以上代码可以使用JavaScript Cookie library来替换getCookie:
var csrftoken = Cookies.get('csrftoken');
实例:提交评论
#全局配置csrftoken
function getCookie(name) { //获取页面cookie,需要再页面埋下{% csrftoken%} var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break;} } } return cookieValue; } var csrftoken = getCookie('csrftoken');//获取cookie中的csrftoken function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({//全局设置ajax的post请求头中添加csrf beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } } }); ==================================以下是局部csrf配置====================================== function getCsrf(){ var csrftoken = Cookies.get('csrftoken'); //利用js插件获取 return csrftoken } $(document).ready(function(){ $(".comment-box button").click(function(){ //获取评论内容 var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length < 8){ alert("评论不能少于8个字"); } else{ $.post("{% url 'post_comment' %}", { comment_type: 1, article_id: "{{ article_obj.id }}", parent_comment_id: null, comment: comment_text.trim(), csrfmiddlewaretoken: getCsrf() },//end post args function (callback){ console.log(callback); var callback_dict = $.parseJSON(callback);//这里把字符串转换为对象 if (callback){ alert("post-comment-success"); window.location.reload(); }else{ alert(callback_dict.error); } }) }//end post }); //end button click });
views
def comment(request): #定义返回内容 ret = {'status': True,'error':''} try:#添加评论 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'), comment=request.POST.get('comment'), user_id=request.user.userprofile.id, ) new_comment_obj.save() except Exception as e: ret['status'] = False ret['error'] = str(e) return HttpResponse(json.dumps(ret)) #返回结果
多级评论展示
用户可以对文章进行评论,用户也可以对评论进行评论。实现一个层叠关系。如下图:
数据库存放形式
文章ID 评论父ID 评论ID
1 null 1 1 1 2 1 2 3 1 1 4 1 null 5 1 2 6 1 2 10 1 3 11 1 11 12
1 5 7
转换成字典树结构
{ 1:{ 2:{ 3:{ 11:{ 12:{} } } 6:{} 10:{} } 4:{} }, 5:{ 7:{} } }
思路:
后端生成字典树格式的数据返回给前端,前端递归字典树生成html_tree;其实这是没法操作的,因为返回的字典中仅仅是一个ID值,我与需要的不仅仅是一个ID而是一个评论对象(需要包含评论的其他信息),所以可以通过后端生成 dict_tree 和 html_tree返回给前端 或者前端返回查询对(parment_coment,comment), 使用自定义标签直接生成html_tree 两种方式
方式一:
#comment_handle.py
def add_node(tree_dic,comment): if comment.parent_comment is None: #如果我是顶层,那我放在这 tree_dic[comment] = {} else: #评论的父元素肯定存在,循环当前整个dict,直到找到为止 for k,v in tree_dic.items(): if k == comment.parent_comment: #找到父元素 # print("find dad.", k) tree_dic[comment.parent_comment][comment] = {} #将父元素下的子元素为key构建一个字典 { f:{ s:{} } } else: #进入下一层继续找 # print("keep going deeper....") add_node(v,comment) #广度查找,循环第一层字典key下的所有子字典(value) def build_tree(comment_set): # print(comment_set) # 首先定义一个空字典 tree_dic = {} # 循环接收到的值是一个列表(parent_comment,comment...) for comment in comment_set: add_node(tree_dic,comment) # 构建字典 传入空字典和评论 print('----------------') for k,v in tree_dic.items(): print(k,v) return tree_dic
字典树生成了,这时我们不能将字典直接交给前端处理,一个字典返回给前端,前端拿到字典的key只是一个ID没有任何其他内容。所以我们接着再后端返回。
def s_comment_tree_html(tree_dic,margin_val):
//拼接子级评论 html = " " for k,v in tree_dic.items(): html += "<div style='margin-left:%spx' class='s-comment'>" % margin_val +
"<img src=/static/%s class='comment-list-author-face'>" % custom.truncate_url(k.user.head_img) + "<span style='margin-left:20px'>%s</span>" % k.user.name + "<span style='margin-left:20px'>%s </span>" %k.comment + "<span style='margin-left:20px'>%s</span>" % custom.filter_date(k.date) + "</div>" html += s_comment_tree_html(v, margin_val+20) return html def p_comment_tree_html(tree_dic):
//拼接父级评论 html = " " for k,v in tree_dic.items(): html += '<div class="comment-list">' html += "<div class='p-comment'><img src=/static/%s class='comment-list-author-face'>" % custom.truncate_url(k.user.head_img) + "<span style='margin-left:20px'>%s</span>" % k.user.name + "<span style='margin-left:20px'>%s </span>" %k.comment + "<span style='margin-left:20px'>%s</span>" % custom.filter_date(k.date) + "</div>" html += s_comment_tree_html(v,20) //每个子级评论需要缩进,默认定义20px return html
views
#views.py
from bbs import comment_handle
def article_detail(request, article_id): #获取当前文章对象 article_obj = models.Article.objects.get(id=article_id) #获取文章关联评论对象,利用build_tree构建多级评论字典树 comment_tree = comment_handle.build_tree(article_obj.comment_set.select_related()) #通过递归字典树生成html树,返回给前端 tree_html = comment_hander.p_comment_tree_html(comment_tree) return render(request, 'bbs/article_detail.html', { "comment_list": tree_html, 'article_obj': article_obj, 'category_list': category_list, }) def comment(request): #定义返回内容 ret = {'status': True,'error':''} try:#添加评论 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'), comment=request.POST.get('comment'), user_id=request.user.userprofile.id, ) new_comment_obj.save() except Exception as e: ret['status'] = False ret['error'] = str(e) return HttpResponse(json.dumps(ret)) #返回结果
html
<div style="border: 0">{{ comment_list| safe }}</div>
效果如下:
另一种方式:
在循环的过程中不断的创建字典,先建立最顶级的,然后在一层一层的建立。
先通过一个简单的例子看下:
data = [ (None,'A'), ('A','A1'), ('A','A1-1'), ('A1','A2'), ('A1-1','A2-3'), ('A2-3','A3-4'), ('A1','A2-2'), ('A2','A3'), ('A2-2','A3-3'), ('A3','A4'), (None,'B'), ('B','B1'), ('B1','B2'), ('B1','B2-2'), ('B2','B3'), (None,'C'), ('C','C1'), ] def tree_search(d_dic,parent,son): #一层一层找,先拨第一层,一层一层往下找 for k,v in d_dic.items(): #举例来说我先遇到A,我就把A来个深度查询,A没有了在找B if k == parent:#如果等于就找到了parent,就把son加入到他下面 d_dic[k][son] = {} #son下面可能还有儿子 #这里找到就直接return了,你找到就直接退出就行了 return else: #如果没有找到,有可能还有更深的地方,得需要剥掉一层 tree_search(d_dic[k],parent,son)
data_dic = {} for item in data: # 每一个item代表两个值一个父亲一个儿子 parent,son = item #先判断parent是否为空,如果为空他就是顶级的,直接吧他加到data_dic if parent is None: data_dic[son] = {} #这里如果为空,那么key就是他自己,他儿子就是一个空字典 else: ''' 如果不为空他是谁的儿子呢?举例来说A3他是A2的儿子,但是你能直接判断A3的父亲是A2你能直接判断他是否在A里面吗?你只能到第一层.key 所以咱们就得一层一层的找,我们知道A3他爹肯定在字典里了,所以就得一层一层的找,但是不能循环找,因为你不知道他有多少层,所以通过递归去找 直到找到位置 ''' tree_search(data_dic,parent,son) #因为你要一层一层找,你得把data_dic传进去,还得把parent和son传进去 for k,v in data_dic.items(): print(k,v)
执行结果:(完美)
('A', {'A1': {'A2': {'A3': {'A4': {}}}, 'A2-2': {'A3-3': {}}}, 'A1-1': {'A2-3': {'A3-4': {}}}}) ('C', {'C1': {}}) ('B', {'B1': {'B2-2': {}, 'B2': {'B3': {}}}})
前端返回
当咱们把这个字典往前端返回的时候,前端模板里是没有一个语法递归的功能的,虽然咱们的层级关系已经出来了!所以咱们需要自定义一个模板语言然后拼成一个html然后返回给前端展示!
配置前端把数据传给simple_tag
{% load custom_tags %} {% build_comment_tree article_obj.comment_set.select_related %}
simple_tag获取数据然后把用户传过来的数据进行转换为字典,生成html标签
from django import template from django.utils.safestring import mark_safe register = template.Library() def tree_search(d_dic,comment_obj):#这里不用传父亲和儿子了因为他是一个对象,可以直接找到父亲和儿子 for k,v_dic in d_dic.items(): if k == comment_obj.parent_comment:#如果找到了 d_dic[k][comment_obj] = {} #如果找到父亲了,你得把自己存放在父亲下面,并把自己当做key,value为一个空字典 return else:#如果找不到递归查找 tree_search(d_dic[k],comment_obj) def generate_comment_html(sub_comment_dic,margin_left_val): #先创建一个html默认为空 html = "" for k,v_dic in sub_comment_dic.items():#循环传过来的字典 html += "<div style='margin-left:%spx' class='comment-node'>" % margin_left_val + k.comment + "</div>" #上面的只是把第一层加了他可能还有儿子,所以通过递归继续加 if v_dic: html += generate_comment_html(v_dic,margin_left_val+15) return html @register.simple_tag def build_comment_tree(comment_list): ''' 把评论传过来只是一个列表格式(如下),要把列别转换为字典,在把字典拼接为html
''' comment_dic = {} #print(comment_list) for comment_obj in comment_list: #每一个元素都是一个对象 if comment_obj.parent_comment is None: #如果没有父亲 comment_dic[comment_obj] = {} else: #通过递归找 tree_search(comment_dic,comment_obj) # #测试: # for k,v in comment_dic.items(): # print(k,v) # 上面完成之后开始递归拼接字符串 #div框架 html = "<div class='comment-box'>" margin_left = 0 for k,v in comment_dic.items(): #第一层的html html += "<div class='comment-node'>" + k.comment + "</div>" #通过递归把他儿子加上 html += generate_comment_html(v,margin_left+15) html += "</div>" return mark_safe(html)
来自罗天帅:http://www.cnblogs.com/luotianshuai/p/5331982.html
首页定时刷新文章
我们可以在前端写一个死循环(定时器)查询是否有新文章;
如何查看有新的文章
1.查找所有比当前首页文章更新的时间
2.查找所有比当前首页文章ID大的时间
问题:文章通过优先级被置顶了怎么处理?
可以通过设立一个单独的div存放置顶文章。
html
<div class="wrap-left"> <div class="new-article-notify hide" > <a href="{{ request.path }}">有<span></span>条新消息</a> </div> {% for article in article_list reversed %} <div article_id="{{ article.id }}" class="article-box row"> <div class="article-head-img col-md-4"> <img src="/static/{{ article.head_img|truncate_url }}" > </div> <div class="article-brief col-md-8"> <a class="article-title" href="{% url 'article_detail' article.id %}">{{ article.title }}</a> <div class="article-brief-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> {{ comments.comment_count }} <span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> {{ comments.thumb_count }} </div> <div class="article-brief-text"> <p>{{ article.brief }}</p> </div> </div> </div> <hr> {% endfor %} </div>
$(document).ready(function(){ var new_article_refresh = setInterval(function(){ //定义定时器 var latest_article_id = $( $(".wrap-left").children()[1] ).attr("article_id"); //获取最新文章ID $.getJSON("{% url 'get_latest_article_count' %}",{latest_id:latest_article_id},function(callback){//获取新文章 console.log(callback); if (callback.new_article_count >0){ //判断是否有新消息 if ($(".new-article-notify").hasClass("hide")){ //判断消息div是否隐藏,代表有之前没有新消息刚来的 $(".new-article-notify").removeClass("hide");//将样式去掉,显示新消息提示 } $(".new-article-notify span").html(callback.new_article_count );//否则 代表之前有新消息未读,只需要修改条数就可以啦 }//end if callback.new_article_count >0 });//end get //console.log(latest_article_id); },3000);//end setInterval });//end doc ready
views
def get_latest_article_count(request): latest_article_id = request.GET.get("latest_id") if latest_article_id: new_article_count = models.Article.objects.filter(id__gt = latest_article_id).count() print("new article count:",new_article_count) else: new_article_count = 0 return HttpResponse(json.dumps({'new_article_count':new_article_count}))
回复评论
$(document).ready(function(){ GetComments();//页面加载后先把评论加载出来 $(".comment-box button").click(function(){ var comment_text = $(".comment-box textarea").val(); if (comment_text.trim().length <5){ alert("评论不能少于5个字sb"); }else{ //post var parent_comment_id = $(this).parent().prev().attr('comment-id'); if(typeof parent_comment_id == "undefined"){ parent_comment_id = null } console.log(parent_comment_id); $.post("{% url 'post_comment' %}", { 'comment_type':1, article_id:"{{ article_obj.id }}", parent_comment_id: parent_comment_id, 'comment':comment_text.trim(), 'csrfmiddlewaretoken':getCsrf() },//end post args function(callback){ //console.log(callback); if (callback == 'post-comment-success'){ if(parent_comment_id){ var new_comment_box_div = $(".new-comment-box").clone(true); $(".comment-list").before(new_comment_box_div); } $(".new-comment-box textarea").val(""); //在刷新评论之前,把评论框再放回文章 底部 GetComments(); //alert("post-comment-success"); } })//end post } });//end button click });
<div article_id="{{ article.id }}" class="article-box row"> ... </div>
def comment(request):
print(request.POST)
if request.method == 'POST':
# user_id = request.user.userprofile.id,
print(request.user.userprofile.id)
# print("user_id:", user_id)
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.userprofile.id,
comment = request.POST.get('comment')
)
new_comment_obj.save()
return HttpResponse('post-comment-success')