这篇博客主要完成一个BBS+Blog项目,那么主要是模仿博客园的博客思路,使用Django框架进行练习。

准备:项目需求分析

  在做一个项目的时候,我们首先做的就是谈清楚项目需求,功能需求,然后才开始写,要是没有和产品经理聊清楚需求,到时候改的话就非常非常麻烦。

  那此次写项目的话,我会严格按着此次写的项目流程完成项目。那下面就是此次的项目流程。

1,项目流程

1.1,功能需求分析(和产品经理聊清楚需求)

  1,基于用户认证组件和AJAX实现登录验证(图片验证码)

  2,基于AJAX 和Forms组件实现注册功能

  3,设计系统首页(完成文章列表的渲染)

  4,设计个人站点页面

  5,文章详情页面

  6,实现一个点赞的功能

  7,实现文章的评论功能

  ——对文章的评论

  ——对评论的评论(就是子评论,反驳评论的评论)

  8,后台管理页面(后面新增文章的功能)——富文本编辑框

  9,防止XSS攻击框

1.2,设计表结构

1.3,按着每一个功能进行开发

1.4,功能测试阶段

1.5,项目部署上线(开发人员最难熬的阶段)

2,开发功能的主要设计思路

  那么下面我们要开发这个网站,而我此次是严格按照经典的软件开发所遵循的MVC设计模型。(如果不懂软件设计的MVC模式,请参考这篇博客:请点击我,后面有MVC的介绍。

  下面写的内容呢,就是我在review整个BBS+Blog项目,其实整体学完,我在这里梳理一遍,做个笔记,那么下面我的记录笔记肯定是按照Django网站开发的四件套Model(模型),URL(链接),View(视图)和Template(模板)完成的。其实这四个就对应着经典的MVC。分别是:

  • Django Model(模型):这个与经典MVC模式下的Model差不多。
  • Django URL+View(视图):这个合起来就与经典MVC下的Controller更像。原因就在于Django的URL和View合起来才能向Template传递正确的数据。用户输入提供的数据也需要Django的View来处理。
  • Django Template(模板):这个与经典MVC模式下的View一致。Django模板用来呈现Django View 传来的数据,也决定了用户界面的外观。Template里面也包含了表单,可以用来收集用户的输入。

一,Django model(模型)  ==  Model(MVC)

1,创建项目,迁移表

1.1,创建Django项目,然后建立url路径

1.2,在mysql建数据库,然后在settings中配置

import pymysql

pymysql.install_as_MySQLdb()

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME':'blog',      # 要连接的数据库,连接前需要创建好
'USER':'root',       # 连接数据库的用户名
'PASSWORD':'',       # 连接数据库的密码
'HOST':'127.0.0.1', # 连接主机,默认本级
'PORT':3306     # 端口 默认3306
}
}

 

1.3,设置时区和语言

  Django默认使用美国时间和英语,在项目的settings文件中,如下图所示:

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True

    我们将其改为 亚洲/上海  时间和中文

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = False

  

1.4,创建模型

  这里模型表设计多表操作,不懂的可以先学习这篇博客:Django学习笔记(7):单表操作和多表操作

1.4.1,设计表结构

  分析表结构

  跨表查询效率非常低。不建议使用。

  所以为了保证查询的效率,经常会牺牲增删改的效率。

1.4.2,完成表内容

  继承AbstractUser,对比继承user。

  每个人的个人站点,可以添加个人标签,和随笔分类:

  一个人可以创建多个分类,一个人可以拥有多个分类,人user和分类时一对多的关系

  分类和站点的关系:一个站点blog有多个分类category,一个分类只能属于一个站点,所以站点和分类是一对多。

  站点blog 和人user是一对一的关系。(跨表查询的问题)

  一个博客存的最核心的数据就是文章,所以展示文章表:

  

  关系表,联合唯一

1.5,迁移表

python manage.py  makemigrations

python manage.py migrate

  

2,Django URL+View  ==  Controller(MVC)

2.1  url的设计

  由于博客系统只有一个APP,所以我们这里不做分发路由。直接在根URL里面写即可。

from django.contrib import admin
from django.urls import path, re_path
from blog import views
from cnblog_review import settings
from django.views.static import serve urlpatterns = [
path('admin/', admin.site.urls),
path('login/', views.login),
path('get_validCode_image/', views.get_validCode_image),
re_path(r'^$', views.index),
path('register/', views.register),
path('logout/', views.logout), # 点赞
path('digg/', views.digg),
# 评论
path('comment/', views.comment),
# 树形评论
path('get_comment_tree/', views.get_comment_tree), # media配置
re_path(r'media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}), re_path(r'^(?P<username>\w+)/articles/(?P<article_id>\d+)$', views.article_detail), # 后台管理url
re_path(r'cn_backend/$', views.cn_backend),
re_path(r'cn_backend/add_article/$', views.add_article), # 关于个人站点的URL
re_path(r'^(?P<username>\w+)/$', views.home_site), # 关于个人站点的跳转
re_path(r'^(?P<username>\w+)/(?P<condition>tag|category|archive)/(?P<param>.*/$)', views.home_site), ]

  

2.2  登录页面的设计 

  在登录页面设计之前,我们可以参考我这两篇博客:

Django学习笔记(9)——开发用户注册与登录系统

Django学习笔记(16)——扩展Django自带User模型,实现用户注册与登录

 下面我就不多解释,直接完成登录页面。代码如下:

  views.py

def login(request):
'''
登录视图函数:
get请求响应页面
post(Ajax)请求响应字典
:param request:
:return:
'''
if request.method == 'POST': response = {'user': None, 'msg': None}
user = request.POST.get('user')
pwd = request.POST.get('pwd')
valid_code = request.POST.get('valid_code') valid_code_str = request.session.get('valid_code_str')
if valid_code.upper() == valid_code_str.upper():
user = auth.authenticate(username=user, password=pwd)
if user:
# request.user == 当前登录对象
auth.login(request, user)
response['user'] = user.username
else:
response['msg'] = '用户名或者密码错误!'
else:
# 校验失败了
response['msg'] = 'valid code error!' return JsonResponse(response) return render(request, 'login.html')

  login.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.min.css"> </head>
<body> <h3 class="text-center">登录页面</h3>
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-offset-3">
<form>
{% csrf_token %}
<div class="form-group">
<label for="user">用户名</label>
<input type="text" id="user" class="form-control">
</div> <div class="form-group">
<label for="pwd">密码</label>
<input type="password" id="pwd" class="form-control">
</div> <div class="form-group">
<label for="pwd">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="valid_code">
</div>
<div class="col-md-6">
<img width="270" height="36" id="valid_code_img" src="/get_validCode_image/" alt="">
</div>
</div>
</div> <input type="button" class="btn btn-default login_btn" value="submit"><span class="error"></span> <a href="/register/" class="btn btn-success pull-right">注册</a> </form>
</div>
</div>
</div> <script src="/static/JS/jquery-3.2.1.min.js"></script>
<script>
// 刷新验证码
$("#valid_code_img").click(function () {
$(this)[0].src += "?"
}); // 登录验证
$(".login_btn").click(function () {
$.ajax({
url: "",
type: "post",
data: {
user: $("#user").val(),
pwd: $("#pwd").val(),
valid_code: $("#valid_code").val(),
csrfmiddlewaretoken: $("[name='csrfmiddlewaretoken']").val(),
},
success: function (data) {
console.log(data); if (data.user) {
if (location.search){
location.href = location.search.slice(6)
}
else {
location.href = "/index/"
} }
else {
$(".error").text(data.msg).css({"color": "red", "margin-left": "10px"});
setTimeout(function(){
$(".error").text("");
},1000) }
}
})
</script> </body>
</html>

  结果展示:

2.3  基于forms组件的注册页面设计

  在注册页面设计之前,我们要学习验证码的代码。

参考这篇博客:Django学习笔记(17)——验证码功能的实现

2.3.1 注册页面总体代码展示

  views.py

def get_validCode_image(request):
"""
基于PIL模块动态生成响应状态码图片
:param request:
:return:
"""
data = get_valid_code_img(request)
return HttpResponse(data) def index(request):
"""
系统首页
:param request:
:return:
"""
article_list = models.Article.objects.all() return render(request, 'index.html', locals()) def logout(request):
"""
注销视图
:param request:
:return:
"""
auth.logout(request)
# 等同于执行了 request.session.fulsh()
return redirect('/login/') def register(request):
"""
注册视图函数:
get请求响应注册页面
post(Ajax)请求,校验字段,响应字典
:param request:
:return:
"""
if request.is_ajax():
print(request.POST)
form = UserForm(request.POST) response = {'user': None, 'msg': None}
if form.is_valid():
response['user'] = form.cleaned_data.get('user') # 生成一条用户记录
user = form.cleaned_data.get('user')
pwd = form.cleaned_data.get('pwd')
email = form.cleaned_data.get('email')
avatar_obj = request.FILES.get('avatar') extra = {}
if avatar_obj:
extra['avatar'] = avatar_obj
# 要是逻辑没有用到值,我们可以不用赋值,等用到的时候,则添加
UserInfo.objects.create_user(username=user, password=pwd, email=email, avatar=avatar_obj, **extra) else:
print(form.cleaned_data)
print(form.errors)
response['msg'] = form.errors return JsonResponse(response) form = UserForm()
return render(request, 'register.html', locals())

  register.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/CSS/register.css">
<link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
<script src="/static/JS/jquery-3.2.1.min.js"></script> </head>
<body>
<h3 class="text-center">注册页面</h3>
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-offset-3"> <form id="form">
{% csrf_token %} {% for field in form %}
<div class="form-group">
<label for="{{ field.auto_id }}">{{ field.label }}</label>
{{ field }} <span class="error pull-right"></span>
</div>
{% endfor %} <div class="form-group">
<label for="avatar">
头像
<img id="avatar_img" width="60" height="60" src="/static/img/default.png" alt="">
</label>
<input type="file" id="avatar" name="avatar">
</div> <input type="button" class="btn btn-default reg_btn" value="submit"><span class="error"></span> </form> </div>
</div>
</div> <script>
// 头像预览
$("#avatar").change(function () { // 获取用户选中的文件对象
var file_obj = $(this)[0].files[0];
// 获取文件对象的路径
var reader = new FileReader();
reader.readAsDataURL(file_obj);
// 修改img的src属性 ,src=文件对象的路径
reader.onload = function () {
$("#avatar_img").attr("src", reader.result)
}; }); // 基于Ajax提交数据 $(".reg_btn").click(function () {
//console.log($("#form").serializeArray());
var formdata = new FormData();
var request_data = $("#form").serializeArray();
$.each(request_data, function (index, data) {
formdata.append(data.name, data.value)
}); formdata.append("avatar", $("#avatar")[0].files[0]); $.ajax({
url: "",
type: "post",
contentType: false,
processData: false,
data: formdata,
success: function (data) {
//console.log(data); if (data.user) {
// 注册成功
location.href="/login/"
}
else { // 注册失败 //console.log(data.msg)
// 清空错误信息
$("span.error").html("");
$(".form-group").removeClass("has-error"); // 展此次提交的错误信息!
$.each(data.msg, function (field, error_list) {
console.log(field, error_list);
if (field=="__all__"){
$("#id_re_pwd").next().html(error_list[0]).parent().addClass("has-error");
}
$("#id_" + field).next().html(error_list[0]);
$("#id_" + field).parent().addClass("has-error"); }) }
}
}) }) </script> </body>
</html>

  index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/CSS/index.css">
<link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css">
<script rel="stylesheet" src="/static/JS/jquery-3.2.1.js"></script>
<script rel="stylesheet" src="/static/bootstrap-3.3.7/dist/js/bootstrap.min.js"></script> </head>
<body> <nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<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="#">博客园</a>
</div> <!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">随笔<span class="sr-only">(current)</span></a></li>
<li><a href="#">新闻</a></li>
<li><a href="#">博文</a></li> </ul> <ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#"><span id="user_icon" class="glyphicon glyphicon-user"></span>{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="/logout/">注销</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a> </li>
<li><a href="/register/">注册</a> </li>
{% endif %}
</ul>
</div>
</div>
</nav> <div class="container-fluid">
<div class="row">
<div class="col-md-3">
<div class="panel panel-warning">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
</div>
<div class="col-md-6">
<div class="article_list">
{% for article in article_list %}
<div class="article-item">
<h5><a href="">{{ article.title }}</a></h5>
<div class="article-desc">
<span class="media-left">
<a href=""><img height="56" width="56" src="media/{{ article.user.avatar }}" alt=""></a>
</span>
<span class="media-right">
{{ article.desc }}
</span>
</div>
<div class="small pub_info">
<span><a href="">{{ article.user.username }}</a> </span>    
<span>发布于  {{ article.create_time|date:'Y-m-d:H:i' }}</span>   
<span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
<span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})
</div>
</div>
<hr>
{% endfor %}
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">Panel heading without title</div>
<div class="panel-body">
Panel content
</div>
</div>
</div>
</div>
</div> </body>
</html>

  

  结果展示:

2.3.2  头像的设置

 点击头像===点击input(这里使用label标签属性方法)

  首先,我们下载一个默认头像:

  注册头像的预览方法

   1,获取用户选中的问卷对象

   2,获取文件对象的路径

   3,修改img的src,src=文件路径对象

  取用户的标签,基于AJAX提交formdata数据

// 基于AJAX 提交数据
$(".reg_btn").click(function () { var formdata = new FormData();
formdata.append('user', $("#id_user").val());
formdata.append('pwd', $("#id_pwd").val());
formdata.append('re_pwd', $("#id_re_pwd").val());
formdata.append('email', $("#id_email").val());
formdata.append('avatar', $("#avatar")[0].files[0]);
formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val()); $.ajax({
url:"",
type:"post",
data: formdata,
processData:false,
contentType:false,
success:function (data) {
console.log(data)
}
})
})

  效果:

2.3.3  AJAX在注册页面显示错误信息

  views: form.errors

  Ajax.success方法  data.msg 就是上面的errors

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title> <link rel="stylesheet" href="/static/blog/bootstrap-3.3.7-dist/css/bootstrap.css">
<style>
#avatar_image{
margin-left: 20px;
}
#avatar{
display: none;
}
.error{
color: red;
}
</style> </head>
<body>
<h3 class=" text-center">注册页面</h3>
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-offset-3">
<form>
{% csrf_token %}
{% for field in form %}
<div class="form-group">
{# <label for="user">{{ field.label }}</label>#}
<label for={{ field.auto_id }}>{{ field.label }}</label>
{{ field }}<span class="error pull-right"></span>
</div>
{% endfor %} <div class="form-group">
<label for="avatar">
头像
<img id="avatar_image" src="/static/img/default.jpg" alt="" width="60" height="60">
</label> <input type="file" id="avatar">
</div> <input type="button" value="Submit" class="btn btn-default reg_btn"> </form>
</div>
</div>
</div> </body>
<script src="/static/JS/jquery-3.2.1.js"></script>
<script> // 基于AJAX 提交数据
$(".reg_btn").click(function () {
console.log($("#form").serializeArray()); var request_data = $("#form").serializeArray();
$.each(request_data, function (index, data) {
formdata.append(data.name, data.value)
}); var formdata = new FormData(); formdata.append('avatar', $("#avatar")[0].files[0]);
formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val()); $.ajax({
url:"",
type:"post",
data: formdata,
processData:false,
contentType:false,
success:function (data) {
console.log(data);
if(data.user){
// 注册成功
}else{
// 注册失败
console.log(data.msg);
$.each(data.msg, function (field, error_list){
console.log(field, error_list);
$("#id_" +field).next().html(error_list[0])
})
}
}
})
})
</script> </html>

    结果展示:

  当我们用户输入后,需要清空用户名的错误信息。

2.4  使用Admin 去录入数据

  (关于Django admin的详细内容,我们后面补充)

  这里我们基于admin 去录入文章数据。

  为了让admin界面管理我们的数据模型,我们需要先注册数据模型到admin。所以我们去 admin.py 中注册模型,代码如下:

from django.contrib import admin

# Register your models here.
from blog import models admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article)
admin.site.register(models.ArticleUpDown)
admin.site.register(models.Article2Tag)
admin.site.register(models.Comment)

  注册后,我们需要通过下面命令来创建超级用户:

python manage.py createsuperuser

  然后登陆Django的后台(http://127.0.0.1:8000/admin/),我们输入超级用户,进去如下:

  然后我们就可以录入数据了。

2.5 个人站点页面的设计

1.我的标签,随机分类,标签列表
随机分类: /username/category/
我的标签: /username/tag/
随笔归档: /username/archive/ ​
2.模板继承
{% extends 'base.html' %}

{% block content %}
{% endblock content%}}

3.自定义标签
/blog/templatetags/my_tag.py

@register.inclusion_tag('classification.html')
def get_classification_style(username):
...
return {} # 去渲染 menu.html

4.分组查询 .annotate() / extra()应用
多表分组
tag_list = Tag.objects.filter(blog=blog).annotate(
count = Count('article')).values_list('title', 'count')

单表分组 / DATE_FORMAT() / extra()
date_list = Article.objects.filter(user=user).extra(
select={"create_ym": "DATE_FORMAT(create_time,'%%Y-%%m')"}).values('create_ym').annotate(
c = Count('nid')).values_list('create_ym', 'c')

5. 时间、区域配置
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False

2.5.1  个人站点设计的总体代码展示

  views.py

def home_site(request, username, **kwargs):
'''
个人站点视图函数
:param request:
:return:
'''
print("执行的是home_site的内容")
print('username:', username)
user = UserInfo.objects.filter(username=username).first()
# 判断用户是否存在
if not user:
return render(request, 'not_found.html') # 当用户存在的话 当前用户或者当前站点对应所有文章取出来
# 1, 查询当前站点
blog = user.blog # kwargs是为了区分访问的是站点页面还是站点下的跳转页面
article_list = models.Article.objects.filter(user=user) if kwargs:
condition = kwargs.get('condition')
param = kwargs.get('param')
print(condition)
print(param)
if condition == 'category':
print(1)
article_list = article_list.filter(category__title=param)
elif condition == 'tag':
print(2)
article_list = article_list.filter(tags__title=param)
else:
print(3)
year, month, day = param.split("-")
article_list = article_list.filter(create_time__year=year, create_time__month=month) return render(request, 'home_site.html', {'username': username, 'blog': blog, 'article_list': article_list, })

  home_site.html

{% extends 'base.html' %}

{% block content %}
<div class="article_list">
{% for article in article_list %}
<div class="article-item clearfix">
<h5><a href="/{{ article.user.username }}/articles/{{ article.pk }}">{{ article.title }}</a></h5>
<div class="article-desc">
{{ article.desc }}
</div>
<div class="small pub_info pull-right">
<span>发布于   {{ article.create_time|date:"Y-m-d H:i" }}</span>  
<span class="glyphicon glyphicon-comment"></span>评论({{ article.comment_count }})  
<span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article.up_count }})  
</div>
</div>
<hr>
{% endfor %} </div>
{% endblock %}

   结果展示:

2.5.2  个人站点页面的文章查询

  当博客园用户站点不存在的时候,我们发现,会返回一个下面页面:

  当然,我们也可以做与上面一样的页面,其代码入下:

  not_found.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Error_404_资源不存在</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/dist/css/bootstrap.css"> <style type="text/css">
body{
margin:8% auto 0;
max-width: 550px;
min-height: 200px;
padding:10px;
font-family: Verdana,Arial,Helvetica,sans-serif;
font-size:14px;
}
p{
color:#555;
margin:10px 10px;
}
img {
border:0px;
}
.d{
color:#404040;
} </style> </head>
<body> <div class="container" style="margin-top: 100px">
<div class="text-center">
<a href="">
<img src="/static/img/logo_small.gif" alt="">
</a>
<p>
<b>404.</b>
抱歉!您访问的资源不存在!
</p>
<p class="d">
请确认您输入的网址是否正确,如果问题持续存在,请发邮件至
<b>contact@qq.com</b>
与我们联系。
</p>
<p>
<a href="http://www.baidu.com/">返回百度查询</a>
</p>
</div>
</div> </body>
</html>

  

2.5.3  个人站点页面的日期查询

  如何只拿出来 年和月?

2.5.4  Extra函数的学习

  Django对一些复杂的函数不能一一对应,所以提供了一种extra函数。

2.5.5  跳转过滤功能的实现

  views.py (home_site函数)

    article_list = models.Article.objects.filter(user=user)
if kwargs:
condition = kwargs.get('condition')
param = kwargs.get('param')
print(condition)
print(param)
if condition == 'category':
print(1)
article_list = article_list.filter(category__title=param)
elif condition == 'tag':
print(2)
article_list = article_list.filter(tags__title=param)
else:
print(3)
year, month, day = param.split("-")
article_list = article_list.filter(create_time__year=year, create_time__month=month)

  home_site.html

<div class="col-md-3">
<div class="panel panel-warning">
<div class="panel-heading">我的标签</div>
<div class="panel-body">
{% for tag in tag_list %}
<p><a href="/{{ username }}/tag/{{ tag.0 }}" >{{ tag.0 }}({{ tag.1 }})</a></p>
{% endfor %}
</div>
</div> <div class="panel panel-danger">
<div class="panel-heading">随笔分类</div>
<div class="panel-body">
{% for cate in cate_list %}
<p><a href="/{{ username }}/category/{{ cate.0 }}" >{{ cate.0 }}({{ cate.1 }})</a></p>
{% endfor %}
</div>
</div> <div class="panel panel-success">
<div class="panel-heading">随笔归档</div>
<div class="panel-body">
{% for data in data_list %}
<p><a href="/{{ username }}/archive/{{ data.0 }}" >{{ data.0 }}({{ data.1 }})</a></p>
{% endfor %}
</div>
</div>
</div>

 

2.6  文章详细页的设计

1.文章详情页的设计

2.文章详情页的数据构建

3.文章详情页点赞样式的完成(基本仿照博客园)

4.文章评论样式的添加(基本仿照博客园)

5.文章评论树的添加(支持对对评论的评论)

6.文章评论中邮件发送

2.6.1 总体的代码及其样式展示

 views.py

def get_classification_data(username):
user = UserInfo.objects.filter(username=username).first()
blog = user.blog cate_list = models.Category.objects.filter(blog=blog).values('pk').annotate(c=Count("article__title")).values_list(
"title", "c") tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", 'c') data_list = models.Article.objects.filter(user=user).extra(
select={"y_m_date": "date_format(create_time, '%%Y-%%m-%%d')"}).values(
'y_m_date').annotate(c=Count('nid')).values_list('y_m_date', 'c') return {"blog": blog, 'cate_list': cate_list, 'tag_list': tag_list, 'data_list': data_list} def article_detail(request, username, article_id):
print("执行的是article_detail的内容") user = UserInfo.objects.filter(username=username).first()
blog = user.blog article_obj = models.Article.objects.filter(pk=article_id).first() comment_list = models.Comment.objects.filter(article_id=article_id) return render(request, 'article_detail.html', locals())

  article_detail.html

{% extends "base.html" %}

{% block content %}
{% csrf_token %}
<div class="article_info">
<h3 class="text-center title">{{ article_obj.title }}</h3>
<div class="cont">
{{ article_obj.content|safe }}
</div> <div class="clearfix">
<div id="div_digg">
<div class="diggit action">
<span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
</div>
<div class="buryit action">
<span class="burynum" id="bury_count">{{ article_obj.down_count }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips" style="color: red;"></div>
</div>
</div> <div class="comments list-group">
<p class="tree_btn">评论树</p>
<div class="comment_tree"> </div> <script> $.ajax({
url: "/get_comment_tree/",
type: "get",
data: {
article_id: "{{ article_obj.pk }}"
},
success: function (comment_list) {
console.log(comment_list); $.each(comment_list, function (index, comment_object) { var pk = comment_object.pk;
var content = comment_object.content;
var parent_comment_id = comment_object.parent_comment_id;
var s = '<div class="comment_item" comment_id=' + pk + '><span>' + content + '</span></div>'; if (!parent_comment_id) { $(".comment_tree").append(s);
} else { $("[comment_id=" + parent_comment_id + "]").append(s);
}
}) }
}) </script> <p>评论列表</p> <ul class="list-group comment_list"> {% for comment in comment_list %}
<li class="list-group-item">
<div>
<a href=""># {{ forloop.counter }}楼</a>   
<span>{{ comment.create_time|date:"Y-m-d H:i" }}</span>  
<a href=""><span>{{ comment.user.username }}</span></a>
<a class="pull-right reply_btn" username="{{ comment.user.username }}"
comment_pk="{{ comment.pk }}">回复</a>
</div> {% if comment.parent_comment_id %}
<div class="pid_info well">
<p>
{{ comment.parent_comment.user.username }}: {{ comment.parent_comment.content }}
</p>
</div>
{% endif %} <div class="comment_con">
<p>{{ comment.content }}</p>
</div> </li>
{% endfor %} </ul> <p>发表评论</p>
<p>昵称:<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
value="{{ request.user.username }}">
</p>
<p>评论内容:</p>
<textarea name="" id="comment_content" cols="60" rows="10"></textarea>
<p>
<button class="btn btn-default comment_btn">提交评论</button>
</p>
</div>
<script>
// 点赞请求
$("#div_digg .action").click(function () {
var is_up = $(this).hasClass("diggit"); $obj = $(this).children("span"); $.ajax({
url: "/digg/",
type: "post",
data: {
"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
"is_up": is_up,
"article_id": "{{ article_obj.pk }}",
},
success: function (data) {
console.log(data); if (data.state) {
var val = parseInt($obj.text());
$obj.text(val + 1);
}
else {
var val = data.handled ? "您已经推荐过!" : "您已经反对过!";
$("#digg_tips").html(val); setTimeout(function () {
$("#digg_tips").html("")
}, 1000) } }
}) }); // 评论请求
var pid = ""; $(".comment_btn").click(function () { var content = $("#comment_content").val(); if (pid) {
var index = content.indexOf("\n");
content = content.slice(index + 1)
} $.ajax({
url: "/comment/",
type: "post",
data: {
"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
"article_id": "{{ article_obj.pk }}",
"content": content,
pid: pid
},
success: function (data) { console.log(data); var create_time = data.create_time;
var username = data.username;
var content = data.content; var s = `
<li class="list-group-item">
<div> <span>${create_time}</span>  
<a href=""><span>${username}</span></a> </div>
<div class="comment_con">
<p>${content}</p>
</div> </li>`; $("ul.comment_list").append(s); // 清空评论框
pid = "",
$("#comment_content").val(""); }
}) }); // 回复按钮事件 $(".reply_btn").click(function () { $('#comment_content').focus();
var val = "@" + $(this).attr("username") + "\n";
$('#comment_content').val(val); pid = $(this).attr("comment_pk"); })
</script> </div>
{% endblock %}

  结果展示:

2.6.2  点赞,评论,评论树及其发送邮件

 

根评论:对文章的评论

子评论:对评论的评论

区别:是否有父评论

评论:   1.构建样式

    2.提交根评论

    3.显示跟评论——render显示   ——AJAX显示

    4.提交子评论

    5.显示子评论——render显示   ——AJAX显示

    6.评论树的显示

  其代码展示

def digg(request):
"""
点赞功能
:param request:
:return:
"""
print(request.POST) article_id = request.POST.get("article_id")
is_up = json.loads(request.POST.get("is_up")) # "true"
# 点赞人即当前登录人
user_id = request.user.pk
obj = models.ArticleUpDown.objects.filter(user_id=user_id, article_id=article_id).first() response = {"state": True}
if not obj:
ard = models.ArticleUpDown.objects.create(user_id=user_id, article_id=article_id, is_up=is_up) queryset = models.Article.objects.filter(pk=article_id)
if is_up:
queryset.update(up_count=F("up_count") + 1)
else:
queryset.update(down_count=F("down_count") + 1)
else:
response["state"] = False
response["handled"] = obj.is_up return JsonResponse(response) def comment(request):
"""
提交评论视图函数
功能:
1 保存评论
2 创建事务
3 发送邮件
:param request:
:return:
"""
print(request.POST) article_id = request.POST.get("article_id")
pid = request.POST.get("pid")
content = request.POST.get("content")
user_id = request.user.pk article_obj = models.Article.objects.filter(pk=article_id).first() # 事务操作
with transaction.atomic():
comment_obj = models.Comment.objects.create(user_id=user_id, article_id=article_id, content=content,
parent_comment_id=pid)
models.Article.objects.filter(pk=article_id).update(comment_count=F("comment_count") + 1) response = {} response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")
response["username"] = request.user.username
response["content"] = content # 发送邮件 from django.core.mail import send_mail
from cnblog import settings # send_mail(
# "您的文章%s新增了一条评论内容"%article_obj.title,
# content,
# settings.EMAIL_HOST_USER,
# ["916852314@qq.com"]
# )
...
import threading t = threading.Thread(target=send_mail, args=("您的文章%s新增了一条评论内容" % article_obj.title,
content,
settings.EMAIL_HOST_USER,
["916852314@qq.com"])
)
t.start()
...
return JsonResponse(response) def get_comment_tree(request):
article_id = request.GET.get("article_id")
response = list(models.Comment.objects.filter(article_id=article_id).order_by("pk").values("pk", "content",
"parent_comment_id")) return JsonResponse(response, safe=False)

  点赞的jQuery代码展示:

$("#div_digg .action").click(function () {
var is_up = $(this).hasClass("diggit"); $obj = $(this).children('span');
$.ajax({
url: '/digg/',
type: 'post',
data: {
"csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
"is_up": is_up,
"article_id": "{{ article_obj.pk }}", },
success:function (data) {
//alert(is_up);
console.log(data);
if (data.state){
var val = parseInt($obj.text());
$obj.text(val+1);
{#if (is_up){#}
{# var val=parseInt($("#digg_count").text());#}
{# $("#digg_count").text(val+1);#}
//}
{#else{#}
{# var val=parseInt($("#bury_count").text());#}
{# $("#bury_count").text(val+1);#}
//}
}else {
var val = data.handled?"您已经推荐过!":"您已经反对过!";
$("#digg_tips").html(val);
{#if (data.handled){#}
{# $("#digg_tips").html("您已经推荐过!")#}
//}else {
{# $("#digg_tips").html("您已经反对过!")#}
//} setTimeout(function () {
$("#digg_tips").html()
}, 2000) }
}
})
});

  

  结果展示

  笔记:

  render显示根评论:

  效果如下:

评论树的显示

  (我们需要将评论楼改为评论树展开)

2.7  后台管理页面设计

1.支持文章编辑

2.支持富文本编辑器(支持渲染已有文章,并支持文本编辑器的上传功能)

3.支持删除文章(未添加,很简单,可自行添加)

4.防止Xss攻击(基于BS4)

    views.py

@login_required
def cn_backend(request):
article_list = models.Article.objects.filter(user=request.user) return render(request, 'backend/backend.html', locals()) from bs4 import BeautifulSoup @login_required
def add_article(request):
if request.method == 'POST':
title = request.POST.get("title")
content = request.POST.get("content") # 防止XSS攻击,过滤script
soup = BeautifulSoup(content, "html.parser")
for tag in soup.find_all(): print(tag.name)
if tag.name == 'script':
tag.decompose() # 构建摘要数据,获取标签字符串的文本前150个符号
desc = soup.text[0:150] + "..."
models.Article.objects.create(title=title, desc=desc, content=str(soup), user=request.user)
return redirect('/cn_backend/')
return render(request, "backend/add_article.html") def upload(request):
'''
编辑器上传文件接收视图函数
:param request:
:return:
'''
print(request.FILES)
img_obj = request.FILES.get('upload_img')
print(img_obj.name) path = os.path.join(settings.MEDIA_ROOT, 'add_article_img', img_obj.name) with open(path, 'wb') as f:
for line in img_obj:
f.write(line) response = {
'error': 0,
'url': '/media/add_article_img/%s' % img_obj.name
}
import json
return HttpResponse(json.dumps(response))

  add_articles.html

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

{% block content %}

    <form action="" method="post">
{% csrf_token %}
<div class="add_article">
<div class="alert-success text-center">添加文章</div> <div class="add_article_region">
<div class="title form-group">
<label for="">标题</label>
<div>
<input type="text" name="title">
</div>
</div> <div class="content form-group">
<label for="">内容(Kindeditor编辑器,不支持拖放/粘贴上传图片) </label>
<div>
<textarea name="content" id="article_content" cols="30" rows="10"></textarea>
</div>
</div> <input type="submit" class="btn btn-default"> </div> </div>
</form>
<script src="/static/JS/jquery-3.2.1.min.js"></script>
<script charset="utf-8" src="/static/kindeditor/kindeditor-all.js"></script> <script>
KindEditor.ready(function(K) {
window.editor = K.create('#article_content',{
width:"800",
height:"600",
resizeType:0,
uploadJson:"/upload/",
extraFileUploadParams:{
csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()
},
filePostName:"upload_img" });
});
</script> {% endblock %}

  结果展示:

  添加文章

3,Django Template  ==  View(MVC)

   Django的模板与经典MVC模式下的View一致。Django模板用来呈现Django View传来的数据,也决定了用户界面的外观。Template里面也包含了表单,可以用来收集用户的输入。

  那这部分内容,我决定单独写一篇博客记录内容。请参考:

4,代码优化

4.1  优化思路

  1,导入包的时候,需要先导入python标准库的包,再导入第三方插件的包,最后导入我们自己定义的包。

  2,冗余代码可以优化的话,自己优化

  3,开发中所有的 print得去掉,我们测试的时候可以加。

           # if avatar_obj:
# user_obj = UserInfo.objects.create_user(username=user, password=pwd,
email=email, avatar=avatar_obj)
# else:
# user_obj = UserInfo.objects.create_user(username=user, password=pwd,
email=email) # 代码优化
extra = {}
if avatar_obj:
extra['avatar'] = avatar_obj
# 要是逻辑没有用到值,我们可以不用赋值,等用到的时候,则添加
UserInfo.objects.create_user(username=user, password=pwd, email=email, avatar=avatar_obj, **extra)

  

4.2 注意问题

  注意问题1:由于我们使用django自带的AbstractUser扩展之后,需要进行更改AUTH_USER_MODEL的配置。

  我们在settings.py中设置:

AUTH_USER_MODEL = 'blog.UserInfo'

  AUTH_USER_MODEL是等于APP blog下面的UserInfo表。因为UserInfo表集成的是自带的AbstractUser表。

  然后进行迁移。

  注意问题2:对于最新版的Django2.0,在使用一对一(OneToOneField)和外键(ForeignKey)时,需要加上on_delete 参数,不然就会报错。

on_delete=models.CASCADE,
# 删除关联数据,与之关联也删除

  如果直接执行上述代码,遇到的报错如下:

TypeError: __init__() missing 1 required positional argument: 'on_delete'

  因为 on_delete 在最新版的Django中已经是位置参数了。

4.3  使用inclution_tag 优化代码

  base.html

 <div class="col-md-3 menu">
{% load my_tags %}
{% get_classification_style username %} </div>

  my_tags.py

from django import template

from blog import models
from django.db.models import Count
register = template.Library() @register.simple_tag
def multi_tag(x, y):
return x*y @register.inclusion_tag('classification.html')
def get_classification_style(username):
user = models.UserInfo.objects.filter(username=username).first()
blog = user.blog cate_list = models.Category.objects.filter(blog=blog).values('pk').annotate(c=Count("article__title")).values_list(
"title", "c") tag_list = models.Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article")).values_list("title", 'c') data_list = models.Article.objects.filter(user=user).extra(
select={"y_m_date": "date_format(create_time, '%%Y-%%m-%%d')"}).values(
'y_m_date').annotate(c=Count('nid')).values_list('y_m_date', 'c') return {"blog": blog, 'cate_list': cate_list, 'tag_list': tag_list, 'data_list': data_list}

 

4.4  头像设置的代码优化

  取用户的标签,基于AJAX提交formdata数据

// 基于AJAX 提交数据
$(".reg_btn").click(function () { var formdata = new FormData();
formdata.append('user', $("#id_user").val());
formdata.append('pwd', $("#id_pwd").val());
formdata.append('re_pwd', $("#id_re_pwd").val());
formdata.append('email', $("#id_email").val());
formdata.append('avatar', $("#avatar")[0].files[0]);
formdata.append('csrfmiddlewaretoken', $('[name= "csrfmiddlewaretoken"]').val()); $.ajax({
url:"",
type:"post",
data: formdata,
processData:false,
contentType:false,
success:function (data) {
console.log(data)
}
})
})

  代码优化:

  // 基于AJAX 提交数据
$(".reg_btn").click(function () {
console.log($("#form").serializeArray()); var request_data = $("#form").serializeArray();
$.each(request_data, function (index, data) {
formdata.append(data.name, data.value)
}); formdata.append('avatar', $("#avatar")[0].files[0]); $.ajax({
url:"",
type:"post",
data: formdata,
processData:false,
contentType:false,
success:function (data) {
console.log(data)
}
})
})

  

4.5  admin的代码优化

  我们查看之前写的admin.py的代码:

from django.contrib import admin

# Register your models here.
from blog import models admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article)
admin.site.register(models.ArticleUpDown)
admin.site.register(models.Article2Tag)
admin.site.register(models.Comment)

  那如果要是存在100个表,我们需要写100个注册吗?当然不可能,为了简化代码,我们这样写。

  首先在model.py中,把所有的表名称写入一个列表中,如下:

from django.db import models

# Create your models here.
__all__ = ['UserInfo', 'Blog', 'Category', 'Tag', 'Article', 'Article2Tag',
'ArticleUpDown', 'Comment']

  然后我们在admin.py中使用一个for循环进行注册。代码如下:

from django.contrib import admin

# Register your models here.
from blog import models for table in models.__all__:
admin.site.register(getattr(models, table))

  这样我们就可以节省代码。

Django学习笔记(18)——BBS+Blog项目开发(2)主体思路及流程的更多相关文章

  1. Django学习笔记(19)——BBS+Blog项目开发(3)细节知识点补充

    本文将BBS+Blog项目开发中所需要的细节知识点进行补充,其中内容包括KindEditor编辑器的使用,BeautifulSoup 模块及其防XSS攻击,Django中admin管理工具的使用,me ...

  2. Django学习笔记(20)——BBS+Blog项目开发(4)Django如何使用Bootstrap

    本文学习如何通过Django使用Bootstrap.其实在之前好几个Django项目中已经尝试使用过了Bootstrap,而且都留有学习记录,我已经大概有了一个大的框架,那么本文就从头再走一遍流程,其 ...

  3. Django学习笔记(17)——BBS+Blog项目开发(1)验证码功能的实现

    本文主要学习验证码功能的实现,为了项目BBS+Blog项目打下基础. 为了防止机器人频繁登陆网站或者破坏分子恶意登陆,很多用户登录和注册系统都提供了图形验证码功能. 验证码(CAPTCHA)是“Com ...

  4. 项目实战:BBS+Blog项目开发

    01-博客系统之功能需求 02-博客系统之表结构设计1 03-博客系统之表结构设计2 04-博客系统之表结构设计3 05-博客系统之表结构设计4 06-博客系统之表机构设计5 07-博客系统之创建系统 ...

  5. IDEA 学习笔记之 Spark/SBT项目开发

    Spark/SBT项目开发: 下载Scala SDK 下载SBT 配置IDEA SBT:(如果不配置,就会重新下载SBT, 非常慢,因为以前我已经下过了,所以要配置为过去使用的SBT) 新建立SBT项 ...

  6. BBS+Blog项目开发

    BBS+Blog项目开发 目前本项目已经上线,可以直接在GEEK浏览本项目效果:GEEK 1.项目需求 基于ajax和用户认证组件实现登录验证 基于ajax和form组件实现注册功能 系统首页文章列表 ...

  7. Django学习笔记(9)—— 开发用户注册与登录系统

    一,项目题目: 开发用户注册与登录系统 该项目主要练习使用Django开发一个用户注册与登录的系统,通过这个项目然后巩固自己这段时间所学习的Django知识. 二,项目需求: 开发一个简单的用户登录与 ...

  8. Django学习笔记(一)——安装,创建项目,配置

    疯狂的暑假学习之 Django学习笔记(一) 教材  书<The Django Book> 视频:csvt Django视频 1.创建项目 django‐admin.py startpro ...

  9. Django学习笔记(11)——开发图书管理页面

    一,项目题目: 开发图书管理页面 该项目主要练习Django对多个数据库进行增删改查的操作. 二,项目需求: 基础需求:75% 1. 列出图书列表.出版社列表.作者列表 2. 点击作者,会列出其出版的 ...

随机推荐

  1. Linux 内存释放

    简介 linux 内存释放通过如下命令,将cache与buff根据环境进行释放操作,避免重启释放内存. 操作 1.将内存中buff数据保存磁盘 sync 2.清理cache与buff缓存 echo 3 ...

  2. Elasticsearch Query DSL 语言介绍

    目录 0. 引言 1. 组合查询 2. 全文搜索 2.1 Match 2.2 Match Phase 2.3 Multi Match 2.4 Query String 2.5 Simple Query ...

  3. kafka的主题与消费

    同一个消费者组不能同时消费同一个分区的数据 不同分区可以消费同一组不同消费者 同一个消费者可以同时消费多个topicA的数据 Topic和consumer依赖zookeeper,producer不依赖

  4. python基础(1):python介绍、python发展史

    1. python介绍 1.1 python是什么样的语言 编程语⾔主要从以下⼏个⻆度为进⾏分类,编译型和解释型.静态语⾔和动态语⾔.强类型定义语⾔和弱类型定义语⾔,我们先看编译型语⾔和解释型语⾔.稍 ...

  5. java基础(9):类、封装

    1. 面向对象 1.1 理解什么是面向过程.面向对象 面向过程与面向对象都是我们编程中,编写程序的一种思维方式. 面向过程的程序设计方式,是遇到一件事时,思考“我该怎么做”,然后一步步实现的过程. 例 ...

  6. tinyriscv---一个从零开始写的极简、易懂的开源RISC-V处理器核

    本项目实现的是一个微riscv处理器核(tinyriscv),用verilog语言编写,只求以最简单.最通俗易懂的方式实现riscv指令的功能,因此没有特意去对代码做任何的优化,因此你会看到里面写的代 ...

  7. Python【day 13】内置函数02

    一.作用域相关-2个 1.locals() 参数是空 返回当前位置作用域的所有变量,返回的是字典 当前位置:函数内,返回局部变量 当前位置:函数外,返回全局变量 2.globals() 参数是空 返回 ...

  8. vue学习笔记(三): 启动说明

    1.启动页面:index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" ...

  9. STM32 掉电检测程序

    当VDD下降到PVD阀值以下或当VDD上升到PVD阀值之上时,根据外部中断第16线的上升/下降边沿触发设置,就会产生PVD中断 void PVD_IRQHandler(void) { led_ctrl ...

  10. 软工个人设计(Java)

    一.GitHub的网络地址:https://github.com/qiannai/WC.git 二.PSP图表: PSP2.1 Personal Software Process Stages 预估耗 ...