开发一个博客园系统

  最近在学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)
PIL生成随机码模块
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{
                    }}
            })
            });
       });
Ajax注销

  注册也有一个地方需要注意,就是图片上传的问题,我使用的是硬解码的方式存放图片:

            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>
Ajax上传预览
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
util.comment

  个人觉得也可以写成博客园的@的方式,@的回复可跨表取到。

{% for re in reply %}
    <div style="background-color: #e0e0e0">{{ re.comment__create_time }}&nbsp [发言人]{{ 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使用方法

  这两天把后台搭起来再把源码上传。我发现这个xadmin功能很强大啊,待我修习几日直接用它来做后台管理。

posted @ 2017-12-23 21:46  JeffD  阅读(2072)  评论(2编辑  收藏  举报