开发一个博客园系统
最近在学django框架,准备用django写一个博客园的系统,并且在写的过程中也遇到一些问题,实践出真知,对django开发web应用方面也有了进一步的了解。很多操作实现都是以我所认知的技术完成的,可能存在不合理的地方(毕竟实现的方法多种多样),基本完成后会将源码上传到git,也欢迎各位大神指正。
首先,要写未登录主站(index)。这里需要注意文章的分类:
文章的分类切换,网站本身有定义的文章类型:
type_choices = [ (1, "Python"), (2, "Linux"), (3, "OpenStack"), (4, "GoLang"), ]
要实现主页的分类(分类标签样式要突出)需要使用一个前端与后端都有的id来显示分类。
if request.method=='GET': type_id = int(kwargs.get('type_id')) if kwargs.get('type_id') else None #后台都是get传参 if type_id: article_list = models.Article.objects.filter(article_type_id=type_id).extra(select={'c': "strftime('%%Y-%%m',create_time)"}) else: article_list = models.Article.objects.all().extra(select={'c': "strftime('%%Y-%%m',create_time)"}) type_choice_list = models.Article.type_choices#分类的 # print(type_choice_list)#[(1, 'Python'), (2, 'Linux'), (3, 'OpenStack'), (4, 'GoLang')]
{% if type_id %} <li><a href="/">全部</a></li> {% else %} <li class="active"><a href="/">全部</a></li> {% endif %} {% for item in type_choice_list %} {% if item.0 == type_id %} <li class="active"><a href="/all/{{ item.0 }}/">{{ item.1 }}</a></li> {% else %} <li><a href="/all/{{ item.0 }}/">{{ item.1 }}</a></li> {% endif %} {% endfor %}
登陆与注册页面
登陆与注册的验证使用form表单功能完成,除此之外我们还需要有一个图片验证码用于认证。
在前端设置一个图片,图片src属性指向后端(获取图片时向后端发生get请求方式,后端返回的),验证码图片由后端生成图片在上面显示,点击更换我们使用每次点击在src属性后面加一个?,这样url改变了前端向后台发送一个get请求,那么就会获得一个新的验证码图片了。
<img style="width: 120px;height: 30px;" src="/check_code/" title="点击更换" id="change_img"> $(function(){ change_img(); }); function change_img() {//get方式在url上加?刷新图片 $('#change_img').click(function () { $(this)[0].src=$(this)[0].src+'?'; }) }
from PIL import Image,ImageDraw,ImageFont,ImageFilter import random def rd_check_code(width=120, height=30, char_length=4, font_file='kumo.ttf', font_size=28): code = [] img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') def rndChar(): """ 生成随机字母 :return: """ return chr(random.randint(65, 90)) def rndColor(): """ 生成随机颜色 :return: """ return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255)) # 写文字 font = ImageFont.truetype(font_file, font_size) for i in range(char_length): char = rndChar() code.append(char) h = random.randint(0, 4) draw.text([i * width / char_length, h], char, font=font, fill=rndColor()) # 写干扰点 for i in range(40): draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor()) # 写干扰圆圈 for i in range(40): draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor()) x = random.randint(0, width) y = random.randint(0, height) draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor()) # 画干扰线 for i in range(5): x1 = random.randint(0, width) y1 = random.randint(0, height) x2 = random.randint(0, width) y2 = random.randint(0, height) draw.line((x1, y1, x2, y2), fill=rndColor()) img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) return img,''.join(code)
def check_code(request): from io import BytesIO from utils.random_check_code import rd_check_code img,code = rd_check_code() stream = BytesIO()#开辟一个内存空间,类似于文件句柄 print(stream)#io空间,<_io.BytesIO object at 0x06D6EAE0> print(img)#pillow生成的图片对象 img.save(stream,'png') print(stream) print(stream.getvalue())#bytes类型的图片信息,返回前端生成图片 print(img) request.session['code'] = code#将生成的随机字符串存到session用于验证 return HttpResponse(stream.getvalue())
登录后将session信息写入浏览器cookie,可以完成两周免登陆效果。注销我使用的时Ajax,后台需要清理session。这个过程中要注意,Ajax需要向后台发送自己的csrf码,否则后端默认是伪造的跨站请求,不给予服务。
$(function () { $(".take_off").click(function () { {# $(".take_off").click(function () {#} {# $.ajaxSetup({#} {# data:{csrfmiddlewaretoken:'{{ csrf_token }}'}#} {# });#} $.ajax({ url:'/', type:'POST', {#data:{ 'csrftoken':{{ csrf_token}} },#} data:{csrfmiddlewaretoken:'{{ csrf_token }}'}, dataType:"JSON", success:function(arg){ console.log(arg); if(arg.status){ location.href='/' }else{ }} }) }); });
注册也有一个地方需要注意,就是图片上传的问题,我使用的是硬解码的方式存放图片:
with open(os.path.join('/static/imgs/', obj.cleaned_data.get('avatar').name), 'wb') as file: all = obj.cleaned_data.get('avatar').chunks() # 拿到整个文件 for trunk in all: file.write(trunk) file.close() obj.cleaned_data['avatar'] = os.path.join('/static/imgs/', obj.cleaned_data.get('avatar').name) models.UserInfo.objects.create(**obj.cleaned_data)
这种方法还是比较笨重的解决方法,在创建数据库的时候有一个upload_to字段可以直接指定文件存放路径。
avatar = models.ImageField(verbose_name='头像',upload_to='static/imgs')
不过这两种方法都不够灵活,不能防止图片名重复的问题,这里有一篇博客对存储路径进行优化的方式。这已经解决了很多一部分命名问题了。http://blog.csdn.net/alxandral_brother/article/details/53415551。
用Ajax完成图片预览功能
首先文件上传的丑陋的接口我们是没有办法修改的(点击上传那个),所以我们使用默认图片遮住这个文件框。
<div class="col-sm-10" style="position: relative;height:80px;width: 80px;"> <img id="previewImg" style="position: absolute;height:80px;width: 80px;" src="/static/imgs/default.png"> {{ obj.avatar }}<span>{{ obj.errors.avatar.0 }}</span> </div>
接下来是关于上传预览的部分,最早我们使用Ajax把前端获取的图片发给后端,后端接收后保存再发送回前端显示预览,但是这样做会导致用户上传了图片但是没有注册成功,那么后端保存的图片信息就是垃圾数据,那么我们必须要进行定期的数据清理工作。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.5-dist/css/bootstrap.css" /> <style> .login{ width: 600px; margin: 0 auto; padding: 20px; margin-top: 80px; } .f1{ position: absolute;height:80px;width: 80px;top:0;left: 0;opacity: 0; } </style> </head> <body> <div class="login"> <div style="position: relative;height:80px;width: 80px; left:260px;top: -10px "> <img id="previewImg" style="height:80px;width: 80px;" src="/static/image/default.png"> <input id="imgSelect" style="height: 80px;width: 80px; position: absolute; top:0;left: 0; opacity: 0" type="file"> {{ obj.avatar }} </div> </div> <script src="/static/jquery-3.2.1.js"></script> <script> $(function () { bindAvartar1(); }); function bindAvartar1() { $("#imgSelect").change(function () { //$(this)[0] #jquery变成DOM对象 //$(this)[0].files #获取上传当前文件的上传对象 //$(this)[0].files[0] #获取上传当前文件的上传对象的某个对象 var obj = $(this)[0].files[0]; console.log(obj); //ajax 发送后台获取头像路径 //img src 重新定义新的路径 var formdata = new FormData(); //创建一个对象 formdata.append("file",obj); var xhr = new XMLHttpRequest(); xhr.open("POST","/register/"); xhr.send(formdata); xhr.onreadystatechange = function () { if(xhr.readyState ==4){ var file_path = xhr.responseText; console.log(file_path); $("#previewImg").attr("src","/" + file_path) } }; }) } </script> </body> </html>
import os def register(request): if request.method == "GET": return render(request,"register.html") else: print(request.POST) print(request.FILES) file_obj = request.FILES.get("file") print(file_obj) file_path = os.path.join("static", file_obj.name) with open(file_path, "wb") as f: for chunk in file_obj.chunks(): f.write(chunk) return HttpResponse(file_path)
当然,我们还可以使用本地预览的方式。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.5-dist/css/bootstrap.css" /> <style> .login{ width: 600px; margin: 0 auto; padding: 20px; margin-top: 80px; } .f1{ position: absolute;height:80px;width: 80px;top:0;left: 0;opacity: 0; } </style> </head> <body> <div class="login"> <div style="position: relative;height:80px;width: 80px; left:260px;top: -10px "> <img id="previewImg" style="height:80px;width: 80px;" src="/static/image/default.png"> <input id="imgSelect" style="height: 80px;width: 80px; position: absolute; top:0;left: 0; opacity: 0" type="file"> {{ obj.avatar }} </div> </div> <script src="/static/jquery-3.2.1.js"></script> <script> $(function () { bindAvartar2(); }); function bindAvartar2() { $("#imgSelect").change(function () { var obj = $(this)[0].files[0]; console.log(obj); //将文件对象上传到浏览器 //IE10 以下不支持 var v = window.URL.createObjectURL(obj); $("#previewImg").attr("src",v); //不会自动释放内存 //当加载完图片后,释放内存 document.getElementById("previewImg").onload= function () { window.URL.revokeObjectURL(v); }; }) } function bindAvartar3() { $("#imgSelect").change(function () { var obj = $(this)[0].files[0]; console.log(obj); var reader = new FileReader(); reader.onload = function (e) { $("#previewImg").attr("src",this.result); }; reader.readAsDataURL(obj) }) } </script> </body> </html>
因为用户的浏览器版本限制,我们可以采用多重手段给不同的用户使用预览功能:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.5-dist/css/bootstrap.css" /> <style> .login{ width: 600px; margin: 0 auto; padding: 20px; margin-top: 80px; } .f1{ position: absolute;height:80px;width: 80px;top:0;left: 0;opacity: 0; } </style> </head> <body> <div class="login"> <div style="position: relative;height:80px;width: 80px; left:260px;top: -10px "> <img id="previewImg" style="height:80px;width: 80px;" src="/static/image/default.png"> <input id="imgSelect" style="height: 80px;width: 80px; position: absolute; top:0;left: 0; opacity: 0" type="file"> </div> </div> <script src="/static/jquery-3.2.1.js"></script> <script> $(function(){ bindAvatar(); }); function bindAvatar(){ if(window.URL.createObjectURL){ bindAvatar2(); }else if(window.FileReader){ bindAvatar3() }else{ bindAvatar1(); } } function bindAvatar1() { $("#imgSelect").change(function () { //$(this)[0] #jquery变成DOM对象 //$(this)[0].files #获取上传当前文件的上传对象 //$(this)[0].files[0] #获取上传当前文件的上传对象的某个对象 var obj = $(this)[0].files[0]; console.log(obj); //ajax 发送后台获取头像路径 //img src 重新定义新的路径 var formdata = new FormData(); //创建一个对象 formdata.append("file",obj); var xhr = new XMLHttpRequest(); xhr.open("POST","/register/"); xhr.send(formdata); xhr.onreadystatechange = function () { if(xhr.readyState ==4){ var file_path = xhr.responseText; {# console.log(file_path);#} $("#previewImg").attr("src","/" + file_path) } }; }) } function bindAvatar2() { $("#imgSelect").change(function () { var obj = $(this)[0].files[0]; console.log(obj); //将文件对象上传到浏览器 //IE10 以下不支持 //不会自动释放内存 //当加载完图片后,释放内存 document.getElementById("previewImg").onload= function () { window.URL.revokeObjectURL(v); }; var v = window.URL.createObjectURL(obj); $("#previewImg").attr("src",v); }) } function bindAvatar3() { $("#imgSelect").change(function () { var obj = $(this)[0].files[0]; console.log(obj); var reader = new FileReader(); reader.onload = function (e) { $("#previewImg").attr("src",this.result); }; reader.readAsDataURL(obj) }) } </script> </body> </html>
主页部分,主页部分的主要操作就是各项分类,你可以将标签,随笔和时间分开写,其实我一开始也是这么做的,但实际上重复代码有很多,这些按分类展现的页面,唯一的不同就是根据不同类型分类的文章也不同。根据这一点,我们可以将分类写到一个视图函数里面,这样代码更为精简。
url(r'^(?P<site>\w+)/(?P<key>((tag)|(date)|(category)))/(?P<val>\w+-*\w*)/', views.filter)
而分类的过程中主要涉及的就是ORM的操作,并且也没有十分难的数据表操作。
文章页
文章页的部分主要是点赞与评论部分,先说一下评论部分,评论可以做成缩进的多级评论,但是需要将数据库获得的数据进行数据结构改造,快速索引。
msg_list = [ {'id':1,'content':'写的太好了','parent_id':None}, {'id':2,'content':'你说得对','parent_id':None}, {'id':3,'content':'顶楼上','parent_id':None}, {'id':4,'content':'你眼瞎吗','parent_id':1}, {'id':5,'content':'我看是','parent_id':4}, {'id':6,'content':'鸡毛','parent_id':2}, {'id':7,'content':'你是没呀','parent_id':5}, {'id':8,'content':'惺惺惜惺惺想寻','parent_id':3}, ] msg_list_dict = {} for item in msg_list: item['child'] = []#每一行加一个空列表child,存放子数据 msg_list_dict[item['id']] = item#每个行加一个索引的序列改造成[1;{},2:{}] # #### msg_list_dict用于查找,msg_list result = [] for item in msg_list: pid = item['parent_id'] if pid:#如果有父id msg_list_dict[pid]['child'].append(item)#加到刚才的child列表中 else: result.append(item)#列表里都是第一级的评论 # ########################### 打印 ################### from utils.comment import comment_tree comment_str = comment_tree(result)#自定义把所有的评论一级一级递归的拨开,解析成HTML格式
def comment_tree(comment_list): """ :param result: [ {id,:child:[xxx]},{}] :return: """ comment_str = "<div class='comment'>" for row in comment_list: tpl = "<div class='content'>%s</div>" %(row['content']) comment_str += tpl if row['child']: # child_str = comment_tree(row['child']) comment_str += child_str comment_str += "</div>" return comment_str
个人觉得也可以写成博客园的@的方式,@的回复可跨表取到。
{% for re in reply %} <div style="background-color: #e0e0e0">{{ re.comment__create_time }}  [发言人]{{ re.comment__user__username }}</div> {% if re.comment__reply__user__username %} <p>@{{ re.comment__reply__user__username }}</p> {% else %} <p></p> {% endif %} <div style="width: 100%;border-bottom: #00b3ee 1px solid ;margin-top: 5px"> {{ re.comment__content }} </div> {% endfor %}
点赞要给赞绑定点击事件,定义1为赞,0为踩,
onclick="updown(this,{{ content.nid }},1);//传给绑定事件触发的函数
function updown(ths,nid,val){ $.ajax({ url: '/updown.html', data:{'val':val,'nid':nid,'csrfmiddlewaretoken':'{{ csrf_token }}'}, type: "POST", dataType:'JSON', success:function(arg){ if(arg.status){ // 点赞成功刷新页面 location.reload(); }else{ alert(arg.msg) } } }) }
后台管理可以使用xadmin来做,当然也可以写一个后台管理,我这个后台管理暂时使用管理员管理界面。
这两天把后台搭起来再把源码上传。我发现这个xadmin功能很强大啊,待我修习几日直接用它来做后台管理。