博客系统实现
利用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. 实现效果
下图为博客个人主页界面展示: