這篇部落格主要完成一個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) } } }) });
結果展示
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) } }) })