這篇博客主要完成一個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學習筆記(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))
這樣我們就可以節省代碼。
