利用django搭建一個簡單的博客系統,記錄下整個過程。
建立項目blog,項目文件如下:
首先建立數據模型,包括用戶,博客,文章,文章內容,評論,點贊,分類,標簽八張表,代碼如下:

#coding:utf-8 from __future__ import unicode_literals from django.db import models from django.contrib.auth.models import AbstractUser # Create your models here. class UserInfo(AbstractUser): phone = models.CharField(max_length=11,null=True,unique=True) avatar = models.ImageField(upload_to='avatars/',default='avatars/default.png', verbose_name='頭像')#儲存在設置的MEDIA_ROOT下的avatars文件夾下 blog = models.OneToOneField(to='Blog', to_field='nid',null=True) def __str__(self): return self.username class Meta: verbose_name = '用戶' verbose_name_plural = verbose_name class Article(models.Model): nid = models.AutoField(primary_key=True) title = models.CharField(max_length=50,verbose_name='文章標題') summary = models.CharField(max_length=255) create_date = models.DateTimeField(auto_now_add=True) comment_count = models.IntegerField(verbose_name='評論數', default=0) up_count = models.IntegerField(verbose_name='點贊數', default=0) down_count = models.IntegerField(verbose_name='踩數', default=0) author = models.ForeignKey(to='UserInfo') category = models.ForeignKey(to='Category', to_field='nid', null=True) tag = models.ManyToManyField(to='Tag') def __str__(self): return self.title class Meta: verbose_name = "文章" verbose_name_plural = verbose_name class ArticleContent(models.Model): nid = models.AutoField(primary_key=True) content = models.TextField() article = models.OneToOneField(to='Article',to_field='nid') class Meta: verbose_name = "文章詳情" verbose_name_plural = verbose_name class Comment(models.Model): nid = models.AutoField(primary_key=True) content = models.CharField(max_length=255) article = models.ForeignKey(to='Article',to_field='nid') user = models.ForeignKey(to='UserInfo') create_date = models.DateTimeField(auto_now_add=True) parent_comment = models.ForeignKey('self',null=True,blank=True) #blank=True設置后 django admin后台可以不填 # 一條父評論對應多條子評論,設置為自己的外鍵 def __str__(self): return self.content class Meta: verbose_name = '評論' verbose_name_plural = verbose_name class ArticleUpDown(models.Model): nid = models.AutoField(primary_key=True) user = models.ForeignKey(to='UserInfo',null=True) article = models.ForeignKey(to='Article', to_field='nid',null=True) is_up = models.BooleanField(default=True) class Meta: unique_together=(('article','user')) #對於一篇文章,一個用戶只能有一個記錄,up或down,不能同時出現 verbose_name = '文章點贊' verbose_name_plural = verbose_name class Category(models.Model): nid = models.AutoField(primary_key=True) name = models.CharField(max_length=32) blog = models.ForeignKey(to='Blog',to_field='nid') #一個博客站點包括多個分類 def __str__(self): return self.name class Meta: verbose_name = "文章分類" verbose_name_plural = verbose_name class Tag(models.Model): nid = models.AutoField(primary_key=True) name = models.CharField(max_length=32) blog = models.ForeignKey(to='Blog',to_field='nid') def __str__(self): return self.name class Meta: verbose_name = '標簽' verbose_name_plural = verbose_name class Blog(models.Model): nid = models.AutoField(primary_key=True) desc = models.CharField(max_length=64) site = models.CharField(max_length=32, unique=True) #個人博客站點url唯一 theme = models.CharField(max_length=32) #個人博客主題樣式 def __str__(self): return self.desc class Meta: verbose_name = '個人博客站點' verbose_name_plural = verbose_name
url設計如下:
from django.conf.urls import url, include
from django.contrib import admin
from django.views.static import serve
#總的url路由 urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^login/', views.login), #登陸
url(r'^get_valid_img.png/', views.get_valid_img), #獲取登陸驗證碼圖片
url(r'^logout/', views.logout), #登出 url(r'^register/', views.register), #注冊 url(r'^index/', views.index), #主頁 url(r'^blog/', include('blogHome.urls')), #訪問個人博客 url(r'^article/up_down/',views.addUpdown), #點贊或踩 url(r'^article/comment/',views.addComment), #添加評論 url(r'^article/comment_tree/(\d+)/',views.getCommentTree), #獲取評論 url(r'^media/(?P<path>.*)$', serve, {"document_root": settings.MEDIA_ROOT}), url(r'^upload/', views.upload), #上傳文件 url(r'(\w+)/article/(\d+)/',views.getArticle), #獲取文章內容 ] #blogHome.urls路由分支 urlpatterns = [ url(r'backend/',views.addArticle), url(r'(\w+)/(category|tag|archive)/(.+)',views.getBlog), url(r'(\w+)/',views.getBlog), ]
3.實現用戶登陸和注冊
3.1 用戶注冊
注冊后端視圖函數如下:利用了forms.Form模塊和auth模塊(creat_user)

def register(request): if request.method == 'POST': ret = {'status':'0','message':''} form_obj = RegisterForm(request.POST) if form_obj.is_valid(): ret['status']=1 ret['message']='/index/' avatar = request.FILES.get('avatar') form_obj.cleaned_data.pop('confirmPassword') #去掉提交的表單中確認密碼數據 print form_obj.cleaned_data, avatar models.UserInfo.objects.create_user(avatar=avatar, **form_obj.cleaned_data) #必須creat_user創建普通用戶,密碼會進行hash加密,能利用auth模塊的認證系統 # user = models.UserInfo(avatar=avatar, **form_obj.cleaned_data) #這樣創建的用戶,密碼在數據庫中為明文 # user.save() return redirect('/index/') else: ret['message'] = form_obj.errors return render(request, 'register.html', {'form_obj': form_obj}) form_obj = RegisterForm() return render(request,'register.html',{'form_obj':form_obj})
RegisterForm表單的代碼如下:(重寫了兩個局部鈎子函數和一個全局鈎子函數來檢查提交數據的合法性)

#coding:utf-8 from django import forms from django.core.exceptions import ValidationError from blogHome import models class RegisterForm(forms.Form): username = forms.CharField( max_length=16, label='用戶名', error_messages={ 'max_length':'用戶名最長16位', 'required':'用戶名不能為空', }, widget=forms.widgets.TextInput( attrs={'class':'form-control'} ), ) password = forms.CharField( min_length=6, label='密碼', error_messages={ 'min_length':'密碼至少6位', 'required':'密碼不能為空', }, widget=forms.widgets.PasswordInput( attrs={'class':'form-control'}, ), ) confirmPassword = forms.CharField( min_length=6, label='確認密碼', error_messages={ 'min_length':'確認密碼至少6位', 'required':'確認密碼不能為空', }, widget=forms.widgets.PasswordInput( attrs={'class':'form-control'}, ), ) email = forms.EmailField( label='郵箱', widget=forms.widgets.EmailInput( attrs={'class':'form-control'}, ), error_messages={ 'invalid':'郵箱格式不正確', 'required':'郵箱不能為空', }, ) #重寫用戶名鈎子函數,驗證用戶名是否已經存在 def clean_username(self): username = self.cleaned_data.get('username') is_exist = models.UserInfo.objects.filter(username=username) if is_exist: self.add_error('username',ValidationError('用戶名已注冊')) else: return username #重寫郵箱鈎子函數,驗證郵箱是否已經存在 def clean_email(self): email = self.cleaned_data.get('email') is_exist = models.UserInfo.objects.filter(email=email) if is_exist: self.add_error('email',ValidationError('郵箱已被注冊')) else: return email #重寫form全局鈎子函數,判斷兩次密碼一致 def clean(self): password = self.cleaned_data.get('password') confirmPassword = self.cleaned_data.get('confirmPassword') if confirmPassword and password != confirmPassword: self.add_error('confirmPassword', ValidationError('兩次密碼不一致')) else: return self.cleaned_data #重寫后必須返回cleaned_data數據
前端注冊頁面,使用bootstrap框架,代碼如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>注冊 </title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/myCSS.css"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-6 col-md-offset-3 "> <form class="form-horizontal" action="/register/" method="post" enctype="multipart/form-data"> {% csrf_token %} <div class="form-group"> <label for="{{ form_obj.username.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.username.label }}</label> <div class="col-sm-8"> {{ form_obj.username }} <div class="has-error"> <span class="help-block">{{ form_obj.username.errors.0 }}</span> </div> </div> </div> <div class="form-group"> <label for="{{ form_obj.password.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.password.label }}</label> <div class="col-sm-8"> {{ form_obj.password }} <div class="has-error"> <span class="help-block">{{ form_obj.password.errors.0 }}</span> </div> </div> </div> <div class="form-group"> <label for="{{ form_obj.confirmPassword.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.confirmPassword.label }}</label> <div class="col-sm-8"> {{ form_obj.confirmPassword }} <div class="has-error"> <span class="help-block">{{ form_obj.confirmPassword.errors.0 }}</span> </div> </div> </div> <div class="form-group"> <label for="{{ form_obj.email.id_for_label }}" class="col-sm-2 control-label">{{ form_obj.email.label }}</label> <div class="col-sm-8"> {{ form_obj.email }} <div class="has-error"> <span class="help-block">{{ form_obj.email.errors.0 }}</span> </div> </div> </div> <div class="form-group"> <label for="InputFile" class="col-sm-2 control-label">頭像</label> <div class="col-sm-8"> <input type="file" id="InputFile" name="avatar"> </div> </div> <div class="form-group"> <div class="col-sm-offset-4 col-sm-8"> <button type="submit" class="btn btn-info">提交</button> </div> </div> </form> </div> </div> </div> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> </body> </html>
3.2 用戶登陸和注銷
登陸和注銷主要利用了auth模塊的authenticate(), login()和logout()函數,代碼如下:

def login(request): ret = {'status': 0, 'msg': ''} if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') valid_code = request.POST.get("validCode") if valid_code and valid_code.upper() == request.session['valid_code'].upper(): user = auth.authenticate(username=username,password=password) if user: auth.login(request,user) return redirect('/index') else: ret['msg'] = '用戶名或密碼錯誤!' ret['status'] = 1 return render(request, 'login.html',{'result':ret}) else: ret['msg'] = '驗證碼錯誤!' ret['status'] = 1 return render(request, 'login.html', {'result': ret}) else: return render(request, 'login.html',{'result': ret}) def logout(request): auth.logout(request) return redirect('/index/')
前端登陸頁面如下,其中驗證碼圖片來自於后端,且每次點擊刷新驗證碼時,驗證碼圖片的src會變化,后端響應而產生不同的驗證碼圖片,前端代碼如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>登陸</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/myCSS.css"> </head> <body> <form class="form-horizontal" action="/login/" method="post"> {% csrf_token %} <div class="form-group"> <label for="inputUsername3" class="col-sm-4 control-label">用戶名</label> <div class="col-sm-4"> <input type="text" class="form-control" id="inputUsername3" placeholder="Username" name="username"> </div> </div> <div class="form-group"> <label for="inputPassword3" class="col-sm-4 control-label">密碼</label> <div class="col-sm-4"> <input type="password" class="form-control" id="inputPassword3" placeholder="Password" name="password"> </div> </div> <div class="form-group"> <label for="validCode" class="col-sm-4 control-label">驗證碼</label> <div class="col-sm-4"> <input type="password" class="form-control" id="validCode" placeholder="驗證碼" name="validCode"> <img id="valid-img" class="valid-img" src="/get_valid_img.png/" alt=""> <span style="margin-left: 10px;color: green;">看不清,點擊驗證碼刷新</span> </div> </div> <div class="form-group"> <div class="col-sm-offset-4 col-sm-8"> <div class="checkbox"> <label> <input type="checkbox"> 記住我 </label> </div> </div> </div> <div class="form-group has-error"> <div class="col-sm-offset-4 col-sm-8"> {% if result.status %} <span class="help-block">{{ result.msg }}</span> {% endif %} </div> </div> <div class="form-group"> <div class="col-sm-offset-4 col-sm-8"> <button type="submit" class="btn btn-info">登陸</button> </div> </div> </form> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script> $('#valid-img').click(function () { //console.log($(this)); //console.log($(this)[0]); $(this)[0].src+='?'; //變化src屬性值不一樣即可以重復提交url }); </script> </body> </html>
后端驗證碼圖片產生代碼如下:

def get_valid_img(request): from PIL import Image, ImageDraw, ImageFont import random #隨機背景顏色 def get_random_bgcolor(): return random.randint(0,255), random.randint(0,255), random.randint(0,255) image_size = (220,35) # 圖片大小 image = Image.new('RGB',image_size,get_random_bgcolor()) # 生成圖片對象 draw = ImageDraw.Draw(image) # 生成畫筆 font = ImageFont.truetype('static/font/kumo.ttf',35) # 字體類型和大小 temp_list = [] for i in range(5): u = chr(random.randint(65, 90)) # 生成大寫字母 l = chr(random.randint(97, 122)) # 生成小寫字母 n = str(random.randint(0, 9)) # 生成數字,注意要轉換成字符串類型 temp = random.choice([u,l,n]) temp_list.append(temp) text = ''.join(temp_list) request.session['valid_code'] = text font_width, font_height = font.getsize(text) draw.text(((220-font_width)/4,(35-font_height)/4),text,font=font,fill=get_random_bgcolor()) #繪制干擾線 # for i in range(5): # x1 = random.randint(0, 220) # x2 = random.randint(0, 220) # y1 = random.randint(0, 35) # y2 = random.randint(0, 35) # draw.line((x1, y1, x2, y2), fill=get_random_bgcolor()) # # #加干擾點 # for i in range(40): # draw.point((random.randint(0, 220), random.randint(0, 35)), fill=get_random_bgcolor()) # x = random.randint(0, 220) # y = random.randint(0, 35) # draw.arc((x, y, x+4, y+4), 0, 90, fill=get_random_bgcolor()) # 不需要在硬盤上保存文件,直接在內存中加載就可以 from cStringIO import StringIO from io import BytesIO io_object = StringIO() image.save(io_object,'png') # 將生成的圖片數據保存在io對象中 data = io_object.getvalue() # 從io對象里面取上一步保存的數據 return HttpResponse(data)
4.博客文章首頁
4.1 首頁顯示
根據分頁設置,每次返回一頁的數據,后端視圖函數代碼如下:

def index(request): articles_list = models.Article.objects.all() current_page = int(request.GET.get('page', 1)) #當前頁碼數 params = request.GET #get提交的參數 base_url = request.path # url路徑 all_count = articles_list.count() # 文章總數 pageination = Pagination(current_page, all_count, base_url, params, per_page_num=3, pager_count=3 ) artilce_list = articles_list[pageination.start:pageination.end] return render(request,'index.html',{'article':artilce_list,'page':pageination})
分頁器的實現代碼如下:

#coding:utf-8 class Pagination(object): def __init__(self, current_page, all_count, base_url,params, per_page_num=8, pager_count=11, ): """ 封裝分頁相關數據 :param current_page: 當前頁 :param all_count: 數據庫中的數據總條數 :param per_page_num: 每頁顯示的數據條數 :param base_url: 分頁中顯示的URL前綴 :param params: url中提交過來的數據 :param pager_count: 最多顯示的頁碼個數 """ try: current_page = int(current_page) except Exception as e: current_page = 1 if current_page < 1: current_page = 1 self.current_page = current_page self.all_count = all_count self.per_page_num = per_page_num self.base_url = base_url # 總頁碼 all_pager, tmp = divmod(all_count, per_page_num) if tmp: all_pager += 1 self.all_pager = all_pager self.pager_count = pager_count # 最多顯示頁碼數 self.pager_count_half = int((pager_count - 1) / 2) import copy params = copy.deepcopy(params) params._mutable = True self.params = params # self.params : {"page":77,"title":"python","nid":1} @property def start(self): return (self.current_page - 1) * self.per_page_num @property def end(self): return self.current_page * self.per_page_num def page_html(self): # 如果總頁碼 < 11個: if self.all_pager <= self.pager_count: pager_start = 1 pager_end = self.all_pager + 1 # 總頁碼 > 11 else: # 當前頁如果<=頁面上最多顯示(11-1)/2個頁碼 if self.current_page <= self.pager_count_half: pager_start = 1 pager_end = self.pager_count + 1 # 當前頁大於5 else: # 頁碼翻到最后 if (self.current_page + self.pager_count_half) > self.all_pager: pager_start = self.all_pager - self.pager_count + 1 pager_end = self.all_pager + 1 else: pager_start = self.current_page - self.pager_count_half pager_end = self.current_page + self.pager_count_half + 1 page_html_list = [] self.params["page"] = 1 first_page = '<li><a href="%s?%s">首頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(first_page) if self.current_page <= 1: prev_page = '<li class="disabled"><a href="#">上一頁</a></li>' else: self.params["page"] = self.current_page - 1 prev_page = '<li><a href="%s?%s">上一頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(prev_page) for i in range(pager_start, pager_end): # self.params : {"page":77,"title":"python","nid":1} self.params["page"] = i # {"page":72,"title":"python","nid":1} if i == self.current_page: temp = '<li class="active"><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) else: temp = '<li><a href="%s?%s">%s</a></li>' % (self.base_url, self.params.urlencode(), i,) page_html_list.append(temp) if self.current_page >= self.all_pager: next_page = '<li class="disabled"><a href="#">下一頁</a></li>' else: self.params["page"] = self.current_page + 1 next_page = '<li><a href="%s?%s">下一頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(next_page) self.params["page"] = self.all_pager last_page = '<li><a href="%s?%s">尾頁</a></li>' % (self.base_url, self.params.urlencode(),) page_html_list.append(last_page) return ''.join(page_html_list)
前端利用bootstrap的柵格系統將一行分為了左中右三塊,代碼如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>主頁</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="/static/myCSS.css"> </head> <body> <nav class="navbar navbar-inverse"> <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="#">First Blog</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="#">Link <span class="sr-only">(current)</span></a></li> <li><a href="#">Link</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="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="#">Separated link</a></li> <li role="separator" class="divider"></li> <li><a href="#">One more separated link</a></li> </ul> </li> </ul> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" placeholder="Search"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> <ul class="nav navbar-nav navbar-right"> {% if request.user.username %} <li><a href="#">{{ 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="#">Action</a></li> <li><a href="#">Another action</a></li> <li><a href="#">Something else here</a></li> <li role="separator" class="divider"></li> <li><a href="/logout/">注銷</a></li> </ul> </li> {% else %} <li><a href="/register/">注冊</a></li> <li><a href="/login/">登陸</a></li> {% endif %} </ul> </div><!-- /.navbar-collapse --> </div><!-- /.container-fluid --> </nav> <div class="container"> <div class="row"> <div class="col-md-3"> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> </div> <div class="col-md-6"> {% for item in article %} <div class="media"> <a href="/{{ item.author.username }}/article/{{ item.nid }}"><h4 class="media-heading">{{ item.title }}</h4></a> <div class="media-left media-middle"> <a href="#"> <img class="author-img media-object " width="80px" height="80px" src="/media/{{item.author.avatar }}" alt="..."> </a> </div> <div class="media-body"> <p>{{ item.summary }}</p> </div> <div class="footer"> <span><a href="/blog/{{ item.author.username }}">{{ item.author.username }}</a></span>發布於 <span>{{ item.create_date|date:'Y-m-d H:i:s' }}</span> <span><i class="fa fa-commenting"></i>評論({{ item.comment_count }})</span> <span><i class="fa fa-thumbs-up"></i>點贊({{ item.up_count }})</span> <!--<span class="glyphicon glyphicon-comment">評論({{ item.comment_count }})</span>--> <!--<span class="glyphicon glyphicon-hand-right">點贊({{ item.up_count }})</span>--> </div> </div> <hr> {% endfor %} </div> <div class="col-md-3"> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">Panel title</h3> </div> <div class="panel-body"> Panel content </div> </div> </div> </div> <div class="row"> <div class="col-md-offset-5"> <!--分頁標簽--> <nav aria-label="Page navigation"> <ul class="pagination">{{ page.page_html|safe }}</ul> </nav> </div> </div> </div> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> </body> </html>
在頁面代碼中有兩處設置了超鏈接,一是文章標題,點擊可以跳轉到文章內容;二是作者名字,點擊跳轉到作者博客主頁,如下所示:
<!--文章標題--> <a href="/{{ item.author.username }}/article/{{ item.nid }}"><h4 class="media-heading">{{ item.title }}</h4></a> <!--作者姓名--> <span><a href="/blog/{{ item.author.username }}">{{ item.author.username }}</a></span>
4.2 跳轉到文章內容
點擊標題跳轉到文章內容的處理函數如下:

def getArticle(request,username,id): nid = id username = username # print nid blog = models.UserInfo.objects.get(username=username).blog content_obj = models.ArticleContent.objects.get(article__nid = nid) comment_list = models.Comment.objects.filter(article__nid = nid) #print content_obj return render(request,'article.html',{'username':username,'content_obj':content_obj, 'blog_obj':blog,'comment_list':comment_list,})
顯示文章詳細內容的前端代碼如下,主框架繼承了base.html。

{% extends 'base.html' %} {% block head %} {% endblock %} {% block body %} <!-- 文章內容部分--> <div id="info" article_id='{{ content_obj.article.nid }}'> <p><h3>{{ content_obj.article.title }}</h3></p> {{ content_obj.content|safe }} <!--不對html標簽轉義,否則可能亂碼--> </div> <!-- 點贊和反對部分,注意clearfix--> <div class="poll clearfix "> <div id="div_digg"> <div class="diggit"> <span id="digg_count">{{ content_obj.article.up_count }}</span> </div> <div class="buryit"> <span id="bury_count">{{ content_obj.article.down_count }}</span> </div> <div class="clear"></div> <div class="diggword" id="digg_tips"> </div> </div> </div> <!-- 以評論樓層的形式顯示評論內容--> <div class="comment_floor"> <ul class="list-group" id="comment_list"> {% for comment in comment_list %} <li class="list-group-item"> <div> <span><a>#{{ forloop.counter }}樓</a> {{ comment.create_date|date:'Y-m-d H:i' }} {{ comment.user.username }}</span> {% if request.user.username %} <span class=" pull-right"> <a id="reply" user="{{ comment.user.username }}" comment_id="{{ comment.nid }}">回復</a> </span> {% endif %} </div> {% if comment.parent_comment %} <div class="well parent_con"> <p>@{{ comment.parent_comment.user.username }}:{{ comment.parent_comment.content }}</p> </div> {% endif %} <p class="comment_content">{{ comment.content }}</p> </li> {% endfor %} </ul> </div> <!-- 寫和提交部分--> <div> {% if request.user.username %} <div id="current_user" current_user="{{ request.user.username }}"> <p><i class="fa fa-commenting"></i>發表評論</p> <label>昵稱:</label> <input class='author' disabled='disabled' type="text" size="50" value="{{ request.user.username }}"> <p>評論內容:</p> <textarea id='comment_content' cols="60" rows="10"></textarea> <p> <button id="comment">提交評論</button> </p> </div> {% else %} <a href="/login/">登陸</a> {% endif %} </div> <!-- 以評論樹的形式顯示評論內容--> <div class="comment_tree"> <ul class="list-group" id="comment_list"> </ul> </div> {% endblock %} {% block js %} <script> //通過ajax方式獲得評論樹,未寫回復評論 {#$.ajax({#} {# url: '/article/comment_tree/' + '{{ content_obj.article.nid }}/', //采用get方式獲取#} {# success: function (data) {#} {# //console.log(data);#} {# var count = 0;#} {# $.each(data, function (index, comment) {#} {# var s = '<li class="list-group-item" comment_tree_id="' + comment.nid + '"><span>' + comment.create_date + ' ' + comment.user + '</span>' + '{% if request.user.username %}'+ '<span class=" pull-right"><a id="reply" user="'+comment.user+'"comment_id="'+comment.nid+'">回復</a></span>'+'{% endif %}' + '<p class="comment_content">' + comment.content + '</p></li>';#} {# if (comment.parent_comment) {#} {# pid = comment.parent_comment;#} {# $("[comment_tree_id=" + pid + "]").append(s)#} {# }#} {# else {#} {# count = count + 1;#} {# var s1 = '<li class="list-group-item" comment_tree_id="' + comment.nid + '"><span><a>#' + count + '樓</a> ' + comment.create_date + ' ' + comment.user + '</span>' + '{% if request.user.username %}'+ '<span class=" pull-right"><a id="reply" user="' +comment.user+'" comment_id="'+comment.nid+'">回復</a></span>'+'{% endif %}' + '<p class="comment_content">' + comment.content + '</p></li>';#} {# $(".comment_tree ul").append(s1);#} {# }#} {# })#} {# }#} {##} {#});#} //為提交評論按鈕綁定點擊事件 var pid = '' $('#comment').click(function () { var comment_content = $('#comment_content').val(); var article_id = $('#info').attr('article_id'); //console.log(article_id,comment_content); if (pid) { index = comment_content.indexOf('\n'); comment_content = comment_content.slice(index + 1); //去掉評論中的@uer部分 } $.ajax({ url: '/article/comment/', type: 'post', data: { article_id: article_id, comment_content: comment_content, pid: pid, csrfmiddlewaretoken: '{{ csrf_token }}', }, success: function (data) { //console.log(data); var username = data.username; var create_date = data.create_date; comment = '<li class="list-group-item"><div><span>' + create_date + ' ' + username + '</span></div><p class="comment_content">' + comment_content + '</p></li>'; $('#comment_list').append(comment); $('#comment_content').val(''); } }); }); //為回復評論綁定點擊事件 $('span #reply').click(function () { var current_user = $('#current_user').attr('current_user'); var reply_user = $(this).attr('user') if (current_user != reply_user) { var reply = '@' + reply_user + ':\n'; $('#comment_content').focus().val(reply); pid = $(this).attr('comment_id'); //console.log(pid); } }); //為點贊和反對綁定點擊事件 $('.diggit,.buryit').click(function () { var current_user = $('#current_user').attr('current_user'); if (current_user) { var is_up = $(this).hasClass('diggit'); //console.log(is_up); var article_id = $('#info').attr('article_id'); $.ajax({ url: '/article/up_down/', method: 'post', data: { is_up: is_up, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}', }, success: function (data) { //console.log(data); if (data.status) { //更新顯示數值 if (is_up) { var up = $('#digg_count').text(); up = parseInt(up) + 1; $('#digg_count').text(up); } else { var down = $('#bury_count').text(); down = parseInt(down) + 1; $('#bury_count').text(down); } } else { //重復提交處理 if (data.first_action) { $('#digg_tips').text('你已經點贊過了'); } else { $('#digg_tips').text('你已經反對過了'); } setTimeout(function () { $('#digg_tips').text(''); }, 1000) } } }); } else { location.href = '/login/'; } }); </script> {% endblock %}

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>主頁</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="/static/myCSS.css"> <link rel="stylesheet" href=/static/theme/{{ blog_obj.theme }}> {% block head %} {% endblock %} </head> <body> <nav class="navbar header"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <a class="navbar-brand" href="#">{{ blog_obj.desc }}</a> </div> <a class="pull-right" href="/blog/backend/">后台管理</a> </nav> <div class="container"> <div class="row"> <div class="col-md-3"> {% load mytags %} {% get_left_menu username %} </div> <div class="col-md-9"> {% block body %} {% endblock %} </div> </div> </div> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> {% block js %} {% endblock %} </body> </html>
4.3 跳轉到個人博客主頁
點擊作者跳轉到個人博客主頁的處理函數如下:

def getBlog(request,username,*args): # print user user = models.UserInfo.objects.filter(username=username).first() if not user: return HttpResponse('404') else: if not args: article_list = models.Article.objects.filter(author=user) else: if args[0]=='category': article_list = models.Article.objects.filter(author=user).filter(category__name=args[1]) elif args[0]=='tag': article_list = models.Article.objects.filter(author=user,tag__name=args[1]) else: year, month = args[1].split('-') print year,month month=int(month) article_list = models.Article.objects.filter(author=user).filter( create_date__year=year,) #article_list = models.Article.objects.filter(author=user).filter( # create_date__year=year, create_date__month=month) #錯在哪里? # article_list1 = models.Article.objects.filter(author=user).filter( # create_date__month=month) # for item in article_list: # print item.create_date.month # month2 = models.Article.objects.first().create_date.month # print month2== month,month,month2,type(month2),type(month) #(month得到的值為‘09’,而數據庫中month為9) blog = user.blog return render(request,'blog.html',{'username':username,'blog_obj':blog,'article': article_list,})
個人博客主頁的前端代碼如下,主框架也繼承了上面的base.html。

{% extends 'base.html' %} {% block head %} {% endblock %} {% block body %} {% for item in article %} <div class="media"> <a href="/{{ item.author.username }}/article/{{ item.nid }}"><h4 class="media-heading">{{ item.title }}</h4></a> <div class="media-left media-middle"> <a href="#"> <img class="author-img media-object " width="80px" height="80px" src="/media/{{ item.author.avatar }}" alt="..."> </a> </div> <div class="media-body"> <p>{{ item.summary }}</p> </div> <div class="footer" style="margin-top: 8px"> <span>發布於{{ item.create_date|date:'Y-m-d H:i:s' }}</span> <span><i class="fa fa-commenting"></i>評論({{ item.comment_count }})</span> <span><i class="fa fa-thumbs-up"></i>點贊({{ item.up_count }})</span> <!--<span class="glyphicon glyphicon-comment">評論({{ item.comment_count }})</span>--> <!--<span class="glyphicon glyphicon-hand-right">點贊({{ item.up_count }})</span>--> </div> </div> <hr> {% endfor %} {% endblock %} {% block js %} {% endblock %}
5. 評論和點贊
在上面article.html頁面代碼中,可以采用兩種方式顯示評論內容,一是采用評論樓的形式(這里采用這種),前端進行構造,二是采用評論樹的形式顯示,通過ajax動態構建內容並顯示(代碼注釋掉了)。另外為點贊和評論分別添加了監聽事件,通過ajax向后端傳送數據,對應的處理函數如下:
點贊事件處理函數:

def addUpdown(request): if request.method=='POST': is_up = json.loads(request.POST.get('is_up')) #傳過來的is_up為小寫的字符竄‘false’,json解析為python支持的False article_id = request.POST.get('article_id') user = request.user # print is_up,user,article_id result = {'status':True} # ArticleUpDown表里面設置了unique_together=(('article','user')),一條記錄中article和user必須是唯一值,即一個用戶點贊后不能再反對,也不能再點贊,否則會報錯 try: models.ArticleUpDown.objects.create(user=user,is_up=is_up,article_id=article_id) models.Article.objects.filter(nid=article_id).update(up_count=F('up_count')+1) except Exception as e: result['status']=False result['first_action']=models.ArticleUpDown.objects.filter(user=user,article__nid=article_id).first().is_up return JsonResponse(result)

//為點贊和反對綁定點擊事件 $('.diggit,.buryit').click(function () { var current_user = $('#current_user').attr('current_user'); if (current_user) { var is_up = $(this).hasClass('diggit'); //console.log(is_up); var article_id = $('#info').attr('article_id'); $.ajax({ url: '/article/up_down/', method: 'post', data: { is_up: is_up, article_id: article_id, csrfmiddlewaretoken: '{{ csrf_token }}', }, success: function (data) { //console.log(data); if (data.status) { //更新顯示數值 if (is_up) { var up = $('#digg_count').text(); up = parseInt(up) + 1; $('#digg_count').text(up); } else { var down = $('#bury_count').text(); down = parseInt(down) + 1; $('#bury_count').text(down); } } else { //重復提交處理 if (data.first_action) { $('#digg_tips').text('你已經點贊過了'); } else { $('#digg_tips').text('你已經反對過了'); } setTimeout(function () { $('#digg_tips').text(''); }, 1000) } } }); } else { location.href = '/login/'; } });
評論事件處理函數:

def addComment(request): if request.method == 'POST': article_id = request.POST.get('article_id') comment_content = request.POST.get('comment_content') user_id = request.user.id pid = request.POST.get('pid') if not pid : #判斷是否存在父評論 comment = models.Comment.objects.create(content=comment_content,user_id=user_id,article_id=article_id,) else: comment = models.Comment.objects.create(content=comment_content, user_id=user_id, article_id=article_id,parent_comment_id=pid ) result = {'username':comment.user.username,'create_date':comment.create_date.strftime('%Y-%m-%d %H:%M:%S')} #print result return JsonResponse(result)

//為提交評論按鈕綁定點擊事件 var pid = '' $('#comment').click(function () { var comment_content = $('#comment_content').val(); var article_id = $('#info').attr('article_id'); //console.log(article_id,comment_content); if (pid) { index = comment_content.indexOf('\n'); comment_content = comment_content.slice(index + 1); //去掉評論中的@uer部分 } $.ajax({ url: '/article/comment/', type: 'post', data: { article_id: article_id, comment_content: comment_content, pid: pid, csrfmiddlewaretoken: '{{ csrf_token }}', }, success: function (data) { //console.log(data); var username = data.username; var create_date = data.create_date; comment = '<li class="list-group-item"><div><span>' + create_date + ' ' + username + '</span></div><p class="comment_content">' + comment_content + '</p></li>'; $('#comment_list').append(comment); $('#comment_content').val(''); } }); });
6. 個人博客主頁
個人博客的主頁顯示如上面blog.html所示,其中對於文章分類,標簽,歸檔一部分采用了自定義的tag(在base.html中),其實現細節如下:

<div class="col-md-3"> {% load mytags %} {% get_left_menu username %} </div>

#coding:utf-8 from django import template from blogHome import models from django.db.models import Count register = template.Library() ''' 1,自定義tag,必須放在app下的templatetags模塊文件(自己新建的)下。 2,register = template.Library() 3,@register.inclusion_tag('left_menu.html'),其中left_menu.html存放要載入的html代碼 4, 在前端html中,{% load mytags %} {% get_left_menu username%}, 定義了username參數,在views函數的render代碼中得傳入username參數 ''' @register.inclusion_tag('left_menu.html') def get_left_menu(username): blog = models.UserInfo.objects.get(username=username).blog category = models.Article.objects.filter(author__username=username).values( 'category__name').annotate(c=Count('nid')).values('category__name', 'c') #category2 = models.Category.objects.filter(blog=blog).annotate(c=Count('article')).values('name', 'c') tag = models.Article.objects.filter(author__username=username).values( 'tag__name').annotate(c=Count('nid')).values('tag__name', 'c') #tag2 = models.Tag.objects.filter(blog=blog).annotate(c=Count('article')).values('name', 'c') archive = models.Article.objects.filter(author__username=username).extra( select={'archive_date': 'date_format(create_date,"%%Y-%%m")'} ).values('archive_date').annotate(c=Count('nid')).values('archive_date', 'c') # extra執行原生的sql語句 #print category,tag,archive return { 'username':username, 'category': category, 'tag': tag, 'archive':archive } # annotate()查詢每個分類對應的文章數量,相當於下面代碼 # category2 = models.Category.objects.filter(blog=blog) # for obj in category2: # print obj.article_set.all().count() ''' #category1 = models.Article.objects.filter(author__username=username).values( 'category__name').annotate(c=Count('nid')).values('category__name', 'c') #category2 = models.Category.objects.filter(blog=blog).annotate(c=Count('article')).values('name', 'c') category2計算出來的是每個分類的文章數量,category1計算出來的是一個作者每個分類下的文章數量, tag1 = models.Article.objects.filter(author__username=username).values( 'tag__name').annotate(c=Count('nid')).values('tag__name', 'c') #tag2 = models.Tag.objects.filter(blog=blog).annotate(c=Count('article')).values('name', 'c') '''

<div class="panel panel-warning"> <div class="panel-heading"> <h3 class="panel-title">分類</h3> </div> <div class="panel-body"> <ul class="list-group"> {% for item in category %} <a href="/blog/{{ username }}/category/{{ item.category__name }}"> <p>{{ item.category__name }}({{ item.c }})</p> </a> {% endfor %} </ul> </div> </div> <div class="panel panel-success"> <div class="panel-heading"> <h3 class="panel-title">標簽</h3> </div> <div class="panel-body"> <ul class="list-group"> {% for item in tag %} <a href="/blog/{{ username }}/tag/{{ item.tag__name }}"> <p>{{ item.tag__name }}({{ item.c }})</p> </a> {% endfor %} </ul> </div> </div> <div class="panel panel-info"> <div class="panel-heading"> <h3 class="panel-title">日期歸檔</h3> </div> <div class="panel-body"> <ul class="list-group"> {% for item in archive %} <a href="/blog/{{ username }}/archive/{{ item.archive_date }}"> <p>{{ item.archive_date }}({{ item.c }})</p> </a> {% endfor %} </ul> </div> </div>
在left_menu.html中為分類,標簽,歸檔都添加了超鏈接,值得注意的一個技巧:對於這些超鏈接采用了統一的url路由和處理視圖函數。
url路由:url(r'(\w+)/(category|tag|archive)/(.+)',views.getBlog)
處理函數:getBlog()函數見上面第四部分4.3
7. 添加文章和上傳圖片
在上面的個人博客主頁,點擊后台管理可以跳轉到后台添加文章頁面,這里采用了KindEditor插件,前端代碼如下:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>后台管理</title> <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> <link rel="stylesheet" href="/static/myCSS.css"> <link rel="stylesheet" href=/static/theme/{{ blog_obj.theme }}> </head> <body> <nav class="navbar header"> <!-- Brand and toggle get grouped for better mobile display --> <div class="navbar-header"> <a class="navbar-brand" href="#">{{ blog_obj.desc }}</a> </div> </nav> <div class="container"> <div class="row"> <div class="col-md-3"></div> <div class="col-md-9"> <form action="" method="post"> {% csrf_token %} <div> <label>文章標題:</label> <input type="text" name="title"/> </div> <div> <label>文章分類:</label> <input type="text" name="category"/> </div> <div> <label>文章標簽:</label> <input type="text" name="tag"/> </div> <div> <p>文章內容:</p> <textarea rows="20" id="article_editor" name="content"></textarea> </div> <input class="btn-default" type="submit" value="保存"/> </form> </div> </div> </div> </body> <script src="/static/jquery-3.3.1.min.js"></script> <script src="/static/bootstrap/js/bootstrap.min.js"></script> <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script> <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script> <script> KindEditor.ready(function (K) { window.editor = K.create('#article_editor', { uploadJson : '/upload/', extraFileUploadParams:{ csrfmiddlewaretoken:'{{ csrf_token }}' }, filePostName:"upload_img", }); }); </script> </html>
添加文章標題,內容等放在了form表單中,當點擊保存時,會向當前路徑提交post請求,處理函數如下:

def addArticle(request): user = request.user # print user.username blog = user.blog if request.method=='POST': title = request.POST.get('title') content = request.POST.get('content') category = request.POST.get('category') tag = request.POST.get('tag') category_obj = models.Category.objects.create(name=category, blog=blog) tag_obj = models.Tag.objects.create(name=tag, blog=blog) #print tag_obj,category_obj from bs4 import BeautifulSoup bs = BeautifulSoup(content,'html.parser') summary =bs.text[0:150]+'....' #利用bs模塊對內容進行解析,去掉HTML標簽,拿到文本 for tag_item in bs.find_all(): if tag_item in ['script','link']: #防止xss攻擊,對存入內容中的script,link標簽進行查詢刪除,還可以加入其他自定義的過濾規則 tag_item.decompose() article_obj = models.Article.objects.create(title=title,summary=summary,author=user,category=category_obj) article_obj.tag.add(tag_obj) #多對多關系必須先建立了article_obj,再通過add方法添加,不能直接在create方法中tag=tag_obj article_obj.save() #print article_obj models.ArticleContent.objects.create(content=str(bs),article=article_obj) return redirect('/blog/'+user.username) else: return render(request,'backend.html',{'blog_obj':blog})
KindEditor插件中提交給后台的文章內容為html格式的數據,所以在上面代碼中需要利用beautiful模塊進行一定的處理。另外該插件可以在文章內容中插入圖片,對應的前后端處理代碼如下:

# 處理添加文章中kindeditor編輯器中上傳的文件 def upload(request): if request.method=='POST': file = request.FILES.get('upload_img') import os import json from Blog import settings path = os.path.join(settings.MEDIA_ROOT,"add_article_img",file.name) with open(path,'wb') as f: for line in file: f.write(line) result={ "error": 0, "url": "/media/add_article_img/"+file.name } return HttpResponse(json.dumps(result))

<script> KindEditor.ready(function (K) { window.editor = K.create('#article_editor', { uploadJson : '/upload/', extraFileUploadParams:{ csrfmiddlewaretoken:'{{ csrf_token }}' }, filePostName:"upload_img", }); }); </script>
8. 實現效果
下圖為博客個人主頁界面展示: