• 五十二、BBS项目


    一 项目分析

    BBS项目:

    • 多人博客系统
    • 技术栈 Django、MySQL
    • 功能
      • 注册 (forms校验,页面渲染,上传头像)
      • 登录 (自定义图片验证码)
      • 首页:文章展示、侧边栏过滤(分类,标签,时间)
      • 文章详情:点赞点踩、评论(父评论和子评论)
      • 后台管理:当前用户文章展示(文章增删改查)
      • 发布文章
    • 项目版本信息:python3.8、django2.2.2、mysql:5.7、jquery2.x、bootstrap3

    二 项目表设计及关联

    1. 创建数据库bbs

      create database bbs
      
      • 1
    2. 表分析

      一共需要创建七张表

      -用户表(基于auth模块的user表扩写)
      -博客表(跟用户表一对一关系)
      -分类表(和博客表一对多、和文章表一对多)
      -标签表(和博客表一对多、和文章表多对多)
      -点赞点踩表(和用户表一对多、和文章表一对多)
      -评论表(和用户表一对多,和文章表一对多)
      -文章表(和博客表一对多)
      在这里插入图片描述

    三 项目表字段编写和数据库迁移

    3.1 创建项目

    1. 安装django 2.2.2版本

      pip3 install django==2.2.2
      
      • 1
    2. 使用pycharm创建django项目

      配置setting.py

      TEMPLATES = {
      "DIRS":[os.path.joi(BASE_DIR, "templates")]
      }
      
      • 1
      • 2
      • 3

      配置语言环境

      LANGUAGE = 'zh-hans'  # 语言汉化
      TIME-ZONE = 'Asia/Shanghai'  # 时区使用上海时区
      USE_I18N = True
      USE_L10N = True
      USE_TZ = False
      
      • 1
      • 2
      • 3
      • 4
      • 5

      配置数据库

      DATABASES = {
      	'default': {
      		'ENGINE':'django.db.backends.mysql',
      		'NAME': 'bbs',
      		'HOST': '127.0.0.1',
      		'PORT': 3306,
      		'USER': 'root',
      		'PASSWORD':'password'
      	}
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10

    3.2 在models中写表模型

    from django.db import models
    
    # Create your models here.
    from django.contrib.auth.models import AbstractUser
    
    
    class UserInfo(AbstractUser):  # 继承AbstractUser表 只用写auth表中没有的字段
        phone = models.CharField(max_length=32, null=True, verbose_name='用户手机号')
        # upload_to是文件保存在什么路径
        icon = models.FileField(upload_to='icon/', default='icon/default.png', null=True, verbose_name='用户头像')
        # 用户表和博客表一对一
        blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True)
    
    
    class Blog(models.Model):
        title = models.CharField(max_length=32, null=True, verbose_name='主标题')
        site_title = models.CharField(max_length=32, null=True, verbose_name='副标题')
        site_style = models.CharField(max_length=64, null=True, verbose_name='站点样式')
    
    
    class Tag(models.Model):
        name = models.CharField(max_length=32, verbose_name='标签名', null=True)
        # 标签和博客是一对多 一个博客有多个标签
        blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
    
    
    class Classify(models.Model):
        name = models.CharField(max_length=32, verbose_name='分类名')
        # 分类和博客是一对多关系 一个博客有多个分类
        blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
    
    
    class Article(models.Model):
        title = models.CharField(max_length=32, verbose_name='文章标题')
        desc = models.CharField(max_length=255, verbose_name='文章摘要')
        content = models.TextField(verbose_name='文章内容')
        create_time = models.DateTimeField(auto_now_add=True)  # 第一次创建时自动添加时间
        # 文章和分类表是一对多 一个分类有多篇文章
        classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE)
        # 文章和标签是多对多关系 自动创建第三张表
        tag = models.ManyToManyField(to='Tag')
        # 文章和博客是一对多关系 一个博客对应多篇文章
        blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
    
    
    class UpAndDown(models.Model):
        create_time = models.DateTimeField(auto_now_add=True, verbose_name='点赞点踩时间')
        # 和用户表是一对多关系 一个用户可以有多条点赞点踩记录
        user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
        # 和文章也是一对多
        article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
        # 1代表点赞 0代表点踩
        is_up = models.BooleanField(verbose_name='是否点赞')
    
    
    class Comment(models.Model):
        content = models.CharField(max_length=64, verbose_name='评论内容')
        create_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
        user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
        article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
        # 自关联字段 只能存已有评论的主键值
        parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
        # 自关联的其他方式
        # parent = models.ForeignKey(to='Comment', on_delete=models.CASCADE)
        # parent = models.IntegerField(null=Ture)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    终端执行数据库迁移命令

    python38 manage.py makemigretions
    python38 manage.py migrater
    
    • 1
    • 2

    没有安装mysqlclient会报错

    • 解决方案一:
      在任意的双下init文件中编写以下代码:
      import pymysql
      pymysql.install_as_MySQLdb()
      
      • 1
      • 2
      在django2.0.7及以后版本,需要改源码才能使用,operations.py中的146行,改成query = query.encode(errors=‘replace’)。
    • 解决方法二:
      pip3 instasll mysqlclient
      
      • 1

    知识点

    • on_delete
      当删除关联表中的数据时,当前表与其关联的行的行为。

      models.CASCADE
        删除关联数据,与之关联也删除

      models.DO_NOTHING
        删除关联数据,引发错误IntegrityError

      models.PROTECT
        删除关联数据,引发错误ProtectedError

      models.SET_NULL
        删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)

      models.SET_DEFAULT
        删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)

    • OneToOneField就是ForeignKey + unique

      def __init__(self, to, on_delete, to_field=None, **kwargs):
         kwargs['unique'] = True  # 继承ForeignKey 自动加上unique=True
         super().__init__(to, on_delete, to_field=to_field, **kwargs)
      
      • 1
      • 2
      • 3
    • 字段类的属性(字段类型)
      max_length (最大长度)
      null=True (可以为空)
      default=‘’ (设置默认值)
      unique=True (数据值必须唯一)
      db_index=True (设置索引)
      verbose_name=‘’ (注释)
      db_constraint=False (数据约束 放在ForeignKey中,不建立外键关联 可以使用正反向查询 可能存在脏数据 可在代码层面进行限制)

    • ManyToManyField

      • 自动创建第三张表
      • 手动创建第三张表(当中间表除了关联字段外还需其他字段)
        手动创建第三章关系表
        字段类属性中增加:through= 通过哪张表进行关联 througu_fields= 设置关联的字段
    • OneToOneField,ForeignKey,ManyToManyField

      • related_name:反向操作时,使用的字段名,用于代替原反向查询的’表名_set‘。
      • related_query_name:反向操作时,使用的连接的前缀,用户替换表名。

    四 注册功能

    4.1 注册forms编写

    在根目录下创建blog_forms.py文件

    from django import forms
    from django.forms import widgets
    from blog.models import UserInfo
    from django.core.exceptions import ValidationError  # 合法性错误
    
    
    class User(forms.Form):
        # 用户名 密码 确认密码 邮箱
        username = forms.CharField(max_length=8, min_length=3, label='用户名', required=True,
                                   error_messages={'max_length': '用户名最多只能输入8位',
                                                   'min_length': '用户名最少输入3位',
                                                   'required': '用户名必须填'
                                                   },
                                   # 添加bootstr样式
                                   widget=widgets.TextInput(attrs={'class': 'form-control'})
                                   )
        password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                                   error_messages={
                                       'max_length': '密码最长16位',
                                       'min_length': '密码最短8位',
                                       'required': '密码不能为空',
                                   },
                                   widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                                   )
        re_password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
                                      error_messages={
                                          'max_length': '密码最长16位',
                                          'min_length': '密码最短8位',
                                          'required': '密码不能为空',
                                      },
                                      widget=widgets.PasswordInput(attrs={'class': 'form-control'})
                                      )
        email = forms.EmailField(label='邮箱地址', widget=widgets.EmailInput(attrs={'class': 'form-control'}))
    
        # 局部钩子 校验用户名是否存在
        def clean_username(self):
            name = self.cleaned_data.get('username')
            if UserInfo.objects.filter(username=name).first():
                # 用户已存在
                raise ValidationError('用户名已存在')  # 校验错误抛出异常
            else:
                return name
    
        # 局部钩子 校验用户名是否存在
        # def clean_username(self):
        #     username = self.cleaned_data.get('username')
        #     try:
        #         UserInfo.objects.get(username=username)
        #         print(UserInfo.objects.get(username=username), type(UserInfo.objects.get(username=username)))
        #         raise ValidationError('用户名已存在')
        #     except Exception:
        #         return username
    
        # 全局钩子 校验两次输入密码是否一致
        def clean(self):
            pwd = self.cleaned_data.get('password')
            re_pwd = self.cleaned_data.get('re_password')
            if pwd != re_pwd:
                raise ValidationError('两次密码不一致')  # 主动抛出合法性错误
            else:
                return self.cleaned_data
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    4.2 路由配置

    在项目同名文件夹下的urls.py中配置路由

    from django.contrib import admin
    from django.urls import path
    from blog import views
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('register/', views.register),
    ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.3 编写视图函数

    views.py

    from django.shortcuts import render
    from blog.blog_forms import User
    
    
    def register(request):
        form_obj = User()
        if request.method == 'GET':  # 当请求为get时返回注册界面,并返回forms组件对象进行数据校验
            return render(request, 'register.html', {'form_obj': form_obj})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.4 前端模板编写

    register.html

    需要先配置静态文件
    	-在setting.py中
    		STATICFILES_DIRS = [
    			os.path.join(BASE_DIR, 'static')
    	] 
    	-把bootstrap和jquery导入模板中
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    <body>
    	<div class="container-fluid">
    	    <div class="row">
    	        <div class="col-md-6 col-md-offset-3">
    	            <h1 class="text-center text-info">注册功能h1>
    	            <form action="" id="id_form">
    	                {% csrf_token %}
    	                {% for foo in form_obj %}
    	                    <div class="form-group">
    	                        <label for="{{ foo.id_for_label }}">{{ foo.label }}label>
    	                        {{ foo }}
    	                        <span class="pull-right text-danger">span>
    	                    div>
    	                {% endfor %}
    	                <div class="form-group">
    	                    <label for="id_file">头像
    	                        <img src="/static/default.png" alt="" height="100px" width="100px" style="margin-right: 20px"
    	                             id="id_img">
    	                        <input type="file" id="id_file" accept="image/*" style="display: none">
    	                    label>
    	                div>
    	                <div class="form-group text-center">
    	                    
    	                    <input type="button" value="注册" class="btn btn-success" id="id_submit">
    	                    <span class="text-danger error">span>
    	                div>
    	            form>
    	        div>
    	    div>
    	div>
    body>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    4.5 头像动态显示

    <script>
        // 头像动态显示
        $('#id_file').change(function () {
            // 将上传的头像展示到img标签内 修改img标签内的src参数
    
            // 读出图片文件 借助于文件阅读器
            let reader = new FileReader()
    
            // 拿到文件对象
            let file = $('#id_file')[0].files[0]
    
            // 将文件对象读到文件阅读器中
            reader.readAsDataURL(file)
    
            // 文件加载完后修改img标签的src参数
            reader.onload = function () {
                // $('#id_img')[0].src=reader.result
                $('#id_img').attr('src', reader.result)  # jquery对象方法
            }
        })
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4.6 发送ajax请求

    // 发送ajax请求
        $('#id_submit').click(function () {
            let data = new FormData  // 可以传递文件数据
    
            // 方式一:根据id获取标签数据添加至data中
            
            // data.append('username', $('#id_username').val())
            // data.append('password', $('#id_password').val())
            // data.append('re_password', $('#id_re_password').val())
            // data.append('email', $('#id_email').val())
            // data.append('icon', $('#id_file')[0].files[0])
            // data.append('csrfmiddlewaretoken', $("[name='csrfmiddlewaretoken']").val())
            // ...发送ajax请求
    
    
            // 方式二:利用form组件批量处理
            let data_arr = $('#id_form').serializeArray()  // 序列化数组
            console.log(data_arr)  // 是一个数组套对象 对象中k是name v是value 自动添加csrf
    
            // 使用for循环把数据添加到data对象中
            $.each(data_arr, function (i, v) {
                console.log("index:",i)
                console.log("value:", v)
                console.log("-----------------------")
                data.append(v.name, v.value)
            })
    
            // 文件需要单独放入
            data.append('icon', $('#id_file')[0].files[0])
    
            // 使用ajax发送请求
            $.ajax({
                url: '/register/',
                type: 'post',
                data: data,
                processData: false,
                contentType: false,
                success: function (data) {
    
    			}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    在这里插入图片描述
    打印结果
    在这里插入图片描述

    现在后端可以收到数据 继续写后端
    views.py

    def register(request):
        form_obj = User()
        if request.method == 'GET':
            return render(request, 'register.html', {'form_obj': form_obj})
        else:  # 当发送post请求
            res = {'code': 100, 'msg': '注册成功'}
            forms_obj = User(data=request.POST)  # forms组件检验
            if forms_obj.is_valid():  # 如果数据全部合法
                register_data = forms_obj.cleaned_data  # 拿出所有的合法数据
                register_data.pop('re_password')  # 弹出二次输入密码 因为用户表中不需要改字段
                if request.FILES.get('icon'):  # 判断是否上传了图片文件
                    register_data['icon'] = request.FILES.get('icon')  # 上传了的话就添加进去
                # 一定要用create_user 密码是密文 后面才可以使用auth模块的功能
                UserInfo.objects.create_user(**register_data)  # 将register_data打散保存至数据库
                return JsonResponse(res)  # 注册成功返回信息
            else:  # 弱国数据不是全部合法
                res['code'] = 101
                res['msg'] = '注册失败'
                res['errors'] = forms_obj.errors  # 返回错误信息
                return JsonResponse(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    前端ajax可以接受到后端返回的json字符串

    $.ajax({
         url: '/register/',
         type: 'post',
         data: data,
         processData: false,
         contentType: false,
         success: function (data) {
             console.log(data)
             if (data.code === 100) {
                 // 注册成功跳转至登录界面
                 location.href = '/login/'
             } else {
                 // 在前端渲染出错误信息
                 console.log(data)
                 $.each(data.errors, function (k, v) {// for循环错误字典
                     if (k === '__all__') {
                         // 全局钩子错误 两次密码不一致
                         $('.error').html(data.errors['__all__'][0])
                     } else {
                         // 其他错误找到相应的input框后的span标签渲染 父类标签加上has-error属性变红
                         $('#id_' + k).next().html(v[0]).parent().addClass('has-error')
                     }
                 })
             }
         }
     })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    打印结果
    在这里插入图片描述
    此时的错误提示信息不会消失 需要绑定一个定时任务

    // 定时任务 渲染的错误信息三秒后清除
    setTimeout(function () {
        // 把所有的span标签的内容清除 父类中的属性has-error去除
        $('.text-danger').html('').parent().removeClass('has-error')
    }, 3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.7 校验用户是否存在

    需求:当用户输入用户名后鼠标离开用户名框,校验用户名是否存在且不能刷新页面

    前端

    <script>
    	// 后端ajax校验用户名是否存在
        // 前端使用get请求传入用户名
        
        // 绑定一个失去焦点事件
        $('#id_username').blur(function () {
            $.ajax({
                url: '/check_name/?name=' + $('#id_username').val(),
                type: 'get',
                success: function (data) {
                    if (data.code === 110) {// 当用户名存在 添加提示信息
                        $('#id_username').next().html(data.msg)
                    }else {// 当用户不存在时清除提示信息
                        $('#id_username').next().html('')
                    }
                }
            })
        })
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    后端

    urls.py

    path('check_name/', views.check_name),
    
    • 1

    views.py

    def check_name(request):
        # print(request.GET)
        res = {'msg': '用户已存在', 'code': 110}
        name = request.GET.get('name')
        obj = UserInfo.objects.filter(username=name).first()
        if obj:
            return JsonResponse(res)
        else:
            res['code'] = 100
            res['msg'] = '用户不存在'
            return JsonResponse(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    五 登录功能

    5.1 登陆界面搭建

    注册成功跳转至/login/,创建login.html。

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
        <script src="/static/jQuery.js">script>
        <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css">
        <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js">script>
    head>
    <body>
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-6 col-md-offset-3">
                <h1 class="text-center text-info">登录功能h1>
                <form action="" id="id_form" method="post">
                    {% csrf_token %}  
                    <div class="form-group">
                        <label for="id_username">用户名label>
                        <input type="text" id="id_username" name="username" class="form-control">
                    div>
                    <div class="form-group">
                        <label for="id_password">密码label>
                        <input type="password" id="id_password" name="password" class="form-control">
                    div>
                    <div class="row">
                        <div class="col-md-6 form-group">
                            <label for="id_code">验证码label>
                            <input type="text" id="id_code" class="form-control" name="code">
                        div>
                        <div class="col-md-6">
                            <img src="/get_code/" alt="" id="id_img" width="350px" height="50px">  
                        div>
                    div>
                    <div class="form-group">
                        <input type="button" value="登录" class="btn btn-block btn-danger" id="id_submit">
                        <div class="text-center">
                            <span  class="text-danger error">span>
                        div>
                    div>
                form>
    
                <script>
                    // 点击验证码图片刷新验证码
                    $('#id_img').click(function () {
                        let time = new Date().getTime()
                        console.log(time)
                        // 再次获取随机验证码图片
                        $('#id_img')[0].src = '/get_code/?t=' + time
                    })
    
                    // 提交ajax
                    $('#id_submit').click(function () {
                    	// 将form表单的input标签数据序列化成数组套对象 name value
                        dataArray = $('#id_form').serializeArray()
                        $.ajax({
                            url: '/login/',
                            type: 'post',
                            data: dataArray,
                            success: function (data) {
                                console.log(data)
                                if(data.code===100){
                                    location.href = '/'
                                }else {
                                    $('.error').html(data.msg)
                                }
                            }
                        })
                    })
    
    
                    // 定时器任务 自动关闭错误提示信息
                    let test = function () {
                        $('.error').html('')
                    }
                    // 可重复关闭
                    timer = setInterval(test, 2000)
    
    				//60秒后关闭循环定时任务
                    setTimeout(function () {
                        clearTimeout(timer)
                    },60*1000)
                script>
            div>
        div>
    div>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    5.2 自定义图片验证码

    验证码:字母数字共五位

    views.py

    from PIL import Image, ImageDraw, ImageFont
    from io import BytesIO
    import random
    
    def get_code(request):
    	# 1 生成一张图片 pillow模块
    	img = Image.new('RGB', (350, 50), color=(255, 255, 255))
    	# 2 生成一个画图对象 将img传入
    	draw = ImageDraw.Draw(img)
    	# 3 生成字体对象
    	font = ImageFont.truetype(font='./static/font/1641263938811335.ttf', size=50)
    	# 4 生成随机字符串
    	ran_str = ''
    	for i in range(5):
    		ran_num = str(random.randint(0, 9))
    		ran_upper = chr(random.randint(65,90))
    		# 去除I和L
    		while (ran_upper == 'L' or ran_upper == 'I'):
    			ran_upper = chr(random.randint(65,90))
    		ran_lower = chr(random.randint(97, 122))
    		# 去除i和l
    		while (ran_lower == i or ran_lower == l):
    			ran_lower = chr(random.randint(97, 122))
    		res = random.choice([ran_num , ran_upper, ran_lower])
    		# 将生成的随机字符画到图片中
    		# fill=get_color 字体颜色也随机
    		draw.text(xy=(10 + i * 60, 0), text=res, font=font, fill=get_color())
    	# 5 画线
    	    for i in range(10):
            draw.line([(random.randint(0, 350), random.randint(0, 50)), (random.randint(0, 350), random.randint(0, 50))],
                      fill=get_color())  # 起点和终点
        # 6 画点
        for i in range(100):
            draw.point((random.randint(0, 350), random.randint(0, 50)), fill=get_color())
    	# 7 将图片保存在内存中 BytesIo模块 并返回给前端
    	byte_io = BytesIo()
    	img.save(fp=byte_io, format='png')
    	# 怎样校验前端传过来的验证码?
    	# 可以存在session表中 前端访问返回给前端 前端再次访问携带session 后端取出data进行校验
    	request.session['code'] = res
    	return HttpResponse(byte_io.getvalue())
    	
    
    
    def get_color():
    	x, y = 0, 255	
    	return (random.randint(x, y), random.randint(x, y), random.randint(x, y))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    上面是自定义的图片验证码,也可以使用第三方模块,比如gvcode模块

    from gvcode import VFCode
    
    """
    使用方法:
    vc = VFCode(
            width=200,                       # 图片宽度
            height=80,                       # 图片高度
            fontsize=50,                     # 字体尺寸
            font_color_values=[
                '#ffffff',
                '#000000',
                '#3e3e3e',
                '#ff1107',
                '#1bff46',
                '#ffbf13',
                '#235aff'
            ],                                # 字体颜色值
            font_background_value='#ffffff',  # 背景颜色值
            draw_dots=False,                  # 是否画干扰点
            dots_width=1,                     # 干扰点宽度
            draw_lines=True,                  # 是否画干扰线
            lines_width=3,                    # 干扰线宽度
            mask=False,                       # 是否使用磨砂效果
            font='arial.ttf'                  # 字体 内置可选字体 arial.ttf calibri.ttf simsun.ttc
        )
        # 验证码类型
        # 自定义验证码
        # vc.generate('abcd')
    
        # 数字验证码(默认5位)
        # vc.generate_digit()
        # vc.generate_digit(4)
    
        # 字母验证码(默认5位)
        # vc.generate_alpha()
        # vc.generate_alpha(5)
    
        # 数字字母混合验证码(默认5位)
        # vc.generate_mix()
        # vc.generate_mix(6)
    
        # 数字加减验证码(默认加法)
        vc.generate_op()
        # 数字加减验证码(加法)
        # vc.generate_op('+')
        # 数字加减验证码(减法)
        # vc.generate_op('-')
    
        # 图片字节码
        # print(vc.get_img_bytes())
        # 图片base64编码
        print(vc.get_img_base64())
        # 保存图片
        vc.save()
    """
    def get_code(request):
        vc = VFCode(width=350, height=50)
        vc.generate_mix()
        # vc.generate_op()
        print(vc.get_img_base64()[0])
        byte_io = BytesIO()
        vc.save(byte_io, fm='png')
        request.session['code'] = vc.get_img_base64()[0]
        return HttpResponse(byte_io.getvalue())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    5.3 登陆界面前端发送数据

    login.html

    <script>
    
    // 提交ajax
    $('#id_submit').click(function () {
         let dataArray = $('#id_form').serializeArray()
         console.log(dataArray)
         $.ajax({
             url: '/login/',
             type: 'post',
             data: dataArray,
             success: function (data) {
                 console.log(data)
                 if(data.code===100){
                 	 // 登陆成功 去首页
                     location.href = '/'
                 }else {
                 	 // 登陆失败 显示错误信息
                     $('.error').html(data.msg)
                 }
             }
         })
     })
    
    // 计时器 关闭错误提示
    let test = function () {
    	$('.error').html('')
    }
    // 循环执行
    timer = setInterval(test, 2000)
    
    //60秒后关闭循环定时任务
    setTimeout(function () {
    	clearTimeout(timer)
    },60*1000)
    														            
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    5.4 登录后端

    def login(request):
        if request.method == 'GET':
            return render(request, 'login.html')
        res = {'code': 100, 'msg': '登陆成功'}
        code = request.POST.get('code')
        # 校验验证码
        if request.session.get('code').lower() == code.lower():
            username = request.POST.get('username')
            password = request.POST.get('password')
            
            # 如果认证成功(用户名和密码正确有效),便会返回一个 User 对象。
            obj = authenticate(username=username, password=password)
            if obj:
                return JsonResponse(res)
            res['code'] = 110
            res['msg'] = '用户名或密码错误'
            return JsonResponse(res)
        res['code'] = '120'
        res['msg'] = '验证码错误'
        return JsonResponse(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    六 首页

    6.1 导航条和轮播图

    创建index.html,添加路由,get请求时返回index.html

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>indextitle>
        <script src="/static/jQuery.js">script>
        <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css">
        <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js">script>
    head>
    <body>
    <div class="my_nav">
        <nav class="navbar navbar-inverse">
            <div class="container-fluid">
                <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 navigationspan>
                        <span class="icon-bar">span>
                        <span class="icon-bar">span>
                        <span class="icon-bar">span>
                    button>
                    <a class="navbar-brand" href="#">博客园a>
                div>
    
                
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">
                        <li class="active"><a href="#">首页 <span class="sr-only">(current)span>a>li>
                        <li><a href="#">新闻a>li>
                    ul>
                    <ul class="nav navbar-nav navbar-right">
                        <li><a href="#">jaspera>li>
                        <li class="dropdown">
                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                               aria-expanded="false">更多 <span class="caret">span>a>
                            <ul class="dropdown-menu">
                                <li><a href="#">修改密码a>li>
                                <li><a href="#">后台管理a>li>
                                <li><a href="#">修改头像a>li>
                                <li role="separator" class="divider">li>
                                <li><a href="#">退出a>li>
                            ul>
                        li>
                    ul>
                div>
            div>
        nav>
    div>
    <div class="container-fluid">
        <div class="row">
            <div class="view_left">
                <div class="col-md-2">
                    <div class="list-group">
                        <a href="#" class="list-group-item active">
                            头条
                        a>
                        <a href="#" class="list-group-item">286 亿元!败了的 Google 是否会为 Android 交最贵罚单?a>
                        <a href="#" class="list-group-item">苹果灵动岛华而不实?网友整活改进,竟可以“一键抢大米”a>
                        <a href="#" class="list-group-item">“AI 终有可能消灭人类!”a>
                        <a href="#" class="list-group-item">Python 3.14 将比 C++ 更快a>
                    div>
                    <div class="list-group">
                        <a href="#" class="list-group-item active">
                            热点
                        a>
                        <a href="#" class="list-group-item">《羊了个羊》否认抄袭;安卓反垄断案再次败诉,罚款金额下降至286亿元a>
                        <a href="#" class="list-group-item">聊聊Redis的数据热点问题a>
                        <a href="#" class="list-group-item">抖音开放平台,究竟开放了什么?a>
                        <a href="#" class="list-group-item">谷歌CEO皮查伊暗示要裁员;华为研发投入位居首位;Android 13首个安全更新|极客头条a>
                    div>
                div>
            div>
            <div class="view_mid">
                <div class="col-md-7">
                    <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
                        
                        <ol class="carousel-indicators">
                            <li data-target="#carousel-example-generic" data-slide-to="0" class="active">li>
                            <li data-target="#carousel-example-generic" data-slide-to="1">li>
                            <li data-target="#carousel-example-generic" data-slide-to="2">li>
                        ol>
    
                        
                        <div class="carousel-inner" role="listbox">
                            <div class="item active">
                                <img src="../media/slideshow/1.png" alt="...">
                                <div class="carousel-caption">
                                    ...
                                div>
                            div>
                            <div class="item">
                                <img src="../media/slideshow/2.png" alt="...">
                                <div class="carousel-caption">
                                    ...
                                div>
                            div>
                            <div class="item">
                                <img src="../media/slideshow/3.png" alt="...">
                                <div class="carousel-caption">
                                    ...
                                div>
                            div>
                        div>
    
                        
                        <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
                            <span class="glyphicon glyphicon-chevron-left" aria-hidden="true">span>
                            <span class="sr-only">Previousspan>
                        a>
                        <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
                            <span class="glyphicon glyphicon-chevron-right" aria-hidden="true">span>
                            <span class="sr-only">Nextspan>
                        a>
                    div>
                div>
            div>
            <div class="view_right">
                <div class="col-md-3">
                    <div class="panel panel-primary">
                        <div class="panel-heading">
                            <h3 class="panel-title">广告招租h3>
                        div>
                        <div class="panel-body">
                            vx:xxx
                        div>
                    div>
                    <div class="panel panel-danger">
                        <div class="panel-heading">
                            <h3 class="panel-title">广告招租h3>
                        div>
                        <div class="panel-body">
                            vx:xxx
                        div>
                    div>
                    <div class="panel panel-info">
                        <div class="panel-heading">
                            <h3 class="panel-title">广告招租h3>
                        div>
                        <div class="panel-body">
                            vx:xxx
                        div>
                    div>
                    <div class="panel panel-success">
                        <div class="panel-heading">
                            <h3 class="panel-title">广告招租h3>
                        div>
                        <div class="panel-body">
                            vx:xxx
                        div>
                    div>
                    <div class="panel panel-warning">
                        <div class="panel-heading">
                            <h3 class="panel-title">广告招租h3>
                        div>
                        <div class="panel-body">
                            vx:xxx
                        div>
                    div>
    
                div>
            div>
        div>
    div>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165

    6.2 首页文章列表

    登陆超级管理员录入数据。

    先在admin.py中将表注册后 可以看到表名

    from django.contrib import admin
    from .models import *
    
    # Register your models here.
    admin.site.register(UserInfo)
    admin.site.register(Blog)
    admin.site.register(Tag)
    admin.site.register(Classify)
    admin.site.register(Article)
    admin.site.register(UpAndDown)
    admin.site.register(Comment)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    注意:

    显示表名需在models.py中创建表时添加一个Meta类

    class Meta:
        verbose_name_plural = '博客表'
    
    • 1
    • 2

    定义字段时添加属性verbose_name在添加数据时显示

    在创建表时定义双下str方法可以自定义显示的创建对象名字

    def __str__(self):
        return self.title
    
    • 1
    • 2

    6.3 开启media访问

    Django中的media文件夹一般用来存放文件,图片等不重要的数据,想在前端通过路径访问media中的数据,是不可以的,需要开启media访问。

    1. 在settings.py中添加
      MEDIA_ROOT = os.path.join(BASEDIR, 'media')
      
      • 1
    2. 在urls中添加
      from django.views.static import serve
      from django.conf import settings
      
      path('media/', serve, {'document_root':settings.MEDIA_ROOT})
      
      • 1
      • 2
      • 3
      • 4

    注意:static文件夹已经默认开启,可以从浏览器进行访问,所以static和media文件夹下不能放重要文件。

    6.4 图片防盗链

    有的网站有上传图片功能,可以上传到该网站,然后再自己的网站使用,这样就不会消耗自己的带宽。

    图片防盗链就是抑制这种行为,本质原理是:浏览器发送http请求,请求头中会携带referer参数,是一个url地址,表示上一次访问的地址,图片防盗链可以跟据这个地址判断是不是自己的网址发的请求,如果不是直接拒绝响应。

    6.5 首页文章渲染

    views.py

    后端返回所有文章

    def index(request):
        article_query_set = Article.objects.all()
        return render(request, 'index.html', context={'article_query_set': article_query_set})
    
    • 1
    • 2
    • 3

    index.html

    <div class="article" style="margin-top: 20px">
        {% for foo in article_query_set %}
            <div style="margin-top: 20px">
                <h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
                <hr>
                <div class="media">
                    <div class="media-left">
                        <a href="#">
                            <img class="media-object" src="/media/{{ foo.blog.userinfo.icon }}" alt="..."
                                 width="60px" height="60px">
                        a>
                    div>
                    <div class="media-body">
                        <h4 class="media-heading">{{ foo.desc }}h4>
                    div>
                div>
    
                <div class="" style="margin-top: 20px">
                    <a href="{{ foo.blog.userinfo.username }}"><span
                            style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>a>
                    <span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
                    <span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
                                                                  aria-hidden="true">i>{{ foo.up_num }}span>
                    <span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
                                                                   aria-hidden="true">i>{{ foo.down_num }}span>
                    <span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
                                                                   aria-hidden="true">i>{{ foo.comment_num }}span>
                div>
            div>
    
        {% endfor %}
    
    div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    七 个人站点页面搭建

    7.1 路由配置

    当点击用户名 则跳转到用户对应的站点(如果存在)不存在就返回404界面

    # 站点匹配 必须放最后
    path('/', views.site),
    
    • 1
    • 2

    7.2 后端

    def site(request, name, **kwargs):
        user = UserInfo.objects.filter(username=name).first()
        if not user:
            return render(request, 'error.html')
        article_set = user.blog.article_set.all()
        return render(request, 'site.html', locals())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    7.3 404界面

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>404title>
    head>
    <body>
    <script type="text/javascript"
            src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js"
            charset="utf-8">
    script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    7.5 个人站点前端

    使用模板的继承,变得只是中间文章的展示,分类标签随笔部分不变。

    base.html

    <head>
        <meta charset="UTF-8">
        <title>
            {% block title %}
    
            {% endblock %}
        title>
        <script src="/static/jQuery.js">script>
        <link rel="stylesheet" href="/static/bootstrap-3.4.1-dist/css/bootstrap.min.css">
        <script src="/static/bootstrap-3.4.1-dist/js/bootstrap.min.js">script>
        <link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
        {% block link %}
    
        {% endblock %}
    head>
    <body>
    <div class="main">
        <div class="header">
            {% block handle %}
    
            {% endblock %}
        div>
    
        <div class="container-fluid">
            <div class="row">
                <div class="col-md-2">
                    <div class="list-group">
                        <a href="#" class="list-group-item active">
                            我的标签
                        a>
                        {% for foo in tag_res %}
                            <div class="list-group">
                            <a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
                               class="list-group-item"><span>{{ foo.1 }}span>
                                <span>({{ foo.2 }})span>a>
                        {% endfor %}
                        div>
    
                    div>
                    <div class="list-group">
                        <a href="#" class="list-group-item active">
                            我的分类
                        a>
                        {% for foo in classify_res %}
                            <div class="list-group">
                            <a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
                               class="list-group-item"><span>{{ foo.1 }}span>
                                <span>({{ foo.2 }})span>a>
                        {% endfor %}
                        div>
                    div>
                    <div class="list-group">
                        <a href="#" class="list-group-item active">
                            随笔分类
                        a>
                        {% for foo in date_res %}
                            <div class="list-group">
                            <a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
                               class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
                                <span>({{ foo.1 }})span>a>
                        {% endfor %}
                        div>
                    div>
                div>
                <div class="col-md-10">
                    {% block crticle %}
    
                    {% endblock %}
                div>
            div>
        div>
    div>
    body>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    site.html

    {% extends 'base.html' %}
    
    {% block title %}
        {{ user.username }}
    {% endblock %}
    
    {% block link %}
    
    {% endblock %}
    
    {% block handle %}
        <div class="my_nav">
            <nav class="navbar navbar-inverse">
                <div class="container-fluid">
                    <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 navigationspan>
                            <span class="icon-bar">span>
                            <span class="icon-bar">span>
                            <span class="icon-bar">span>
                        button>
                        <a class="navbar-brand" href="#">{{ user.username }}a>
                    div>
    
                    
                    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                        <ul class="nav navbar-nav">
                        ul>
                        <ul class="nav navbar-nav navbar-right">
                            <li>
                                <button type="button" class="btn btn-danger navbar-btn">管理button>
                            li>
                        ul>
                    div>
                div>
            nav>
        div>
    {% endblock %}
    
    
    
    
    {% block crticle %}
        <div class="article">
            {% for foo in article_set %}
                <div style="margin-top: 20px">
                    <h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
                    <hr>
                    <div class="media">
                        <div class="media-body">
                            <h4 class="media-heading">{{ foo.desc }}h4>
                        div>
                    div>
    
                    <div class="" style="margin-top: 20px">
                        <span
                                style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>
                        <span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
                        <span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
                                                                      aria-hidden="true">i>{{ foo.up_num }}span>
                        <span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
                                                                       aria-hidden="true">i>{{ foo.down_num }}span>
                        <span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
                                                                       aria-hidden="true">i>{{ foo.comment_num }}span>
                    div>
                div>
    
            {% endfor %}
    
        div>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    7.6 标签、分类、随便档案过滤

    urls.py

    # jasper/tag/4.html 标签匹配
    path('/tag/.html', views.site),
    # jasper/classify/3.html
    path('/classify/.html', views.site),
    # jasper/archive/202209.html
    path('/archive/.html', views.site),
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    views.py

    def site(request, name, **kwargs):
    	# name是传过来的站点对应的用户名
        user = UserInfo.objects.filter(username=name).first()
        if not user:
        	# 博主不存在 返回错误界面
            return render(request, 'error.html')
        # 查询该博主的所有文章
        article_set = user.blog.article_set.all()
        # 取名字后的路由后缀
        tag = kwargs.get('tag')
        classify = kwargs.get('classify')
        time = kwargs.get('time')
        # 有tag后缀
        if tag:
        	# 返回当前标签的所有文章
            article_set = article_set.filter(tag__id=tag)
        elif classify:
            article_set = article_set.filter(classify__id=classify)
        # 按时间分类
        elif time:
            year = str(time)[:4]
            month = str(time)[4:]
            article_set = article_set.filter(create_time__year=year, create_time__month=month)
        # # 需要标签名和统计标签内文章数
        classify_res = Classify.objects.all().filter(blog=user.blog).values('id').annotate(
            c=Count('article__id')).values_list('id', 'name', 'c')
        tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
            'id', 'name', 'c')
        date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
            'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
        return render(request, 'site.html', locals())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    八 文章详情和点赞点踩

    8.1 左侧列表组使用inclusion_tag实现

    # 自定义标签
    1. 在应用下创建templatetags包,必须是templatetags
    2. 在templatetags中新建一个new_tag.py文件,py文件名随意。
    
    • 1
    • 2
    • 3
    from django import template
    from blog.models import Classify, Tag, Article, UserInfo
    from django.db.models import Count
    from django.db.models.functions import TruncMonth
    
    register = template.Library()  # 生成一个Library对象 名字必须叫register 
    
    
    # 装饰函数
    @register.inclusion_tag(filename='left.html', name='left') # 返回html片段,第一个参数是html文件
    def left(name):
    	# user 当前根据用户名查到的用户,需要传入用户名,一定会有user
        user = UserInfo.objects.filter(username=name).first()
        # 需要标签名和统计标签内文章数
        classify_res = Classify.objects.all().filter(blog=user.blog).values('id').annotate(
            c=Count('article__id')).values_list('id', 'name', 'c')
        tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
            'id', 'name', 'c')
        date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
            'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
        return {'classify_res': classify_res, 'tag_res': tag_res, 'date_res': date_res, 'user':user}  # 字典中的数据可以在left中使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    left.html

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Titletitle>
    head>
    <body>
    <div class="list-group">
        <a href="#" class="list-group-item active">
            我的标签
        a>
        {% for foo in tag_res %}
            <div class="list-group">
                <a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
                   class="list-group-item"><span>{{ foo.1 }}span>
                    <span>({{ foo.2 }})span>a>
            div>
        {% endfor %}
    
    div>
    <div class="list-group">
        <a href="#" class="list-group-item active">
            我的分类
        a>
        {% for foo in classify_res %}
            <div class="list-group">
                <a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
                   class="list-group-item"><span>{{ foo.1 }}span>
                    <span>({{ foo.2 }})span>a>
            div>
        {% endfor %}
    div>
    <div class="list-group">
        <a href="#" class="list-group-item active">
            随笔分类
        a>
        {% for foo in date_res %}
            <div class="list-group">
                <a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
                   class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
                    <span>({{ foo.1 }})span>a>
            div>
        {% endfor %}
    
    div>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    base.html

    在base中的左侧栅格使用inclusion_tag
    需要先将自定义标签load过来,在使用标签并传入参数。

    <div class="col-md-2">
        {% load new_tag %}
        {% left name %}
    div>
    
    • 1
    • 2
    • 3
    • 4

    渲染site.html页面时,返回的locals(),所以可以用到site函数的所有变量,site函数的name是它的形参,是点击首页博主用户名跳转过来的,name参数就是用文章取到的博主用户名,所以base可以用到name属性。

    将那么属性传到new_tag文件中的left函数中,执行该函数。进行标签等数据的过滤,然后返回参数供left.html文件使用,left.html文件渲染完后,贴在base.html的相应位置。views.py中的

    注意:添加templatetags模块后 需要重启服务器 才可以使用标签

    8.2 点赞点踩样式

    点击首页文章和个人站点中的文章跳转到文章详情页面去。

    path('/articles/', views.article_detail),
    
    • 1

    views.py

    def article_detail(request, name, article_id):
    	# 文章博主
        user = UserInfo.objects.filter(username=name).first()
        # 文章
        article = Article.objects.filter(id=article_id).first()
        if user and article:
            return render(request, 'article.html', context={'user': user, 'article': article, 'name': name})
        return render(request, 'error.html')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    article.html

    {% extends 'base.html' %}
    
    {% block title %}
        {{ article.title }}
    {% endblock %}
    
    {% block link %}
        <link rel="stylesheet" href="/static/css/up.css">
    {% endblock %}
    
    {% block handle %}
        <div class="my_nav">
            <nav class="navbar navbar-inverse">
                <div class="container-fluid">
                    <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 navigationspan>
                            <span class="icon-bar">span>
                            <span class="icon-bar">span>
                            <span class="icon-bar">span>
                        button>
                        <a class="navbar-brand" href="#">{{ user.username }}a>
                    div>
    
                    
                    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                        <ul class="nav navbar-nav">
                        ul>
                        <ul class="nav navbar-nav navbar-right">
                            <li>
                                <button type="button" class="btn btn-danger navbar-btn">管理button>
                            li>
                        ul>
                    div>
                div>
            nav>
        div>
    {% endblock %}
    
    {% block crticle %}
        <div>
            <h3>{{ article.title }}h3>
        div>
    
        <div>
            {{ article.content }}
        div>
        
        <div id="div_digg" class="pull-right">
            <div class="diggit is_up">
                <span class="diggnum" id="digg_count">{{ article.up_num }}span>
            div>
            <div class="buryit is_up">
                <span class="burynum" id="bury_count">{{ article.down_num }}span>
            div>
            <div class="clear">div>
            <div class="diggword" id="digg_tips">
            div>
        div>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    /static/css/up.css

    .diggit {
        float: left;
        width: 46px;
        height: 52px;
        background: url(/static/upup.gif) no-repeat;
        text-align: center;
        cursor: pointer;
        margin-top: 2px;
        padding-top: 5px;
    }
    
    
    .buryit {
        float: right;
        margin-left: 20px;
        width: 46px;
        height: 52px;
        background: url(/static/downdown.gif) no-repeat;
        text-align: center;
        cursor: pointer;
        margin-top: 2px;
        padding-top: 5px;
    }
    
    .clear {
        clear: both;
    }
    
    .diggword {
        margin-top: 5px;
        margin-left: 0;
        font-size: 12px;
        color: #808080;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    8.3 点赞点踩前端js

    <script>
    	// 将点赞点踩设置成一个点击事件
        $('.is_up').click(function () {
            let is_up = ($(this).hasClass('diggit'))//根据类属性来判断是点赞还是点踩
            $.ajax({
                url: '/is_up/',  //处理点赞点踩的接口
                type: 'post',
                // 需要传谁给哪篇文章点赞还是点踩了 谁点赞可以不传 只要后端登陆了就可以查到
                data: {
                    article_id:{{ article.id }},
                    is_up: is_up,
                    csrfmiddlewaretoken: '{{ csrf_token }}'
                },
                success: function (data) {
                    if (data.code == 100) {
                    	// 如果成功,点赞数+1
                        $('#digg_count').html({{ article.up_num }} +1)
                    } else if (data.code == 103) {
                    	// 如果失败,点踩数+1
                        $('#bury_count').html({{ article.down_num }} +1)
                    }
                    // 每次打印提示信息
                    $('.diggword').html(data.msg)
                }
            })
        })
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    设置路由

    # is_up 处理点赞相关路由
    path('is_up/', views.is_up),
    
    • 1
    • 2

    8.4 点赞点踩后端

    def is_up(request):
        article_id = request.POST.get('article_id')
        is_up = json.loads(request.POST.get('is_up'))  # 直接取出来是字符串 需要转成bool值
        res = {'code': 100, 'msg': '点赞成功了'}
        # 1. 判断当前用户是否登录
        if not request.user.is_authenticated:  # 只要用户登录就是当前用户 没有登陆就是匿名用户
            res['code'] = 101
            res['msg'] = '没有登录点击跳转登录'
            return JsonResponse(res)
        # 2. 判断当前用户是否已经给这篇文章点过赞或踩了
        if UpAndDown.objects.filter(user=request.user, article_id=article_id).first():
            res['code'] = 102
            res['msg'] = '已经点赞或点踩了'
            return JsonResponse(res)
        # 3. 用户是点赞还是点踩 存入点赞点踩表和文章表 并将点赞点踩数返回bbs
        # 开启事务
        with transaction.atomic():
            UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
            if is_up:
                # 文章表点赞数加1
                Article.objects.filter(id=article_id).update(up_num=F('up_num') + 1)
            else:
                Article.objects.filter(id=article_id).update(down_num=F('down_num') + 1)
                res['code'] = 103
                res['msg'] = '点踩成功了'
        return JsonResponse(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    8.5 评论前端页面

        <div class="comment-show">
            <div style="margin-top: 60px">
                <b>评论列表b>
            div>
    
    
            <ul class="list-group comment-ajax">
                {% for foo in comment %}
                    <li class="list-group-item">
                        <div>
                            <span># {{ forloop.counter }} 楼span> <span
                                style="margin-left: 20px">{{ foo.create_time|date:'Y-m-d H:i' }}span>
                            <a href="/{{ foo.user.username }}/"><span
                                    style="margin-left: 20px">{{ foo.user.username }}span>a>
                            <div class="fa-pull-right">
                                <a class="reply" parent_id="{{ foo.article.id }}" username="{{ foo.user.username }}">回复a>
                            div>
    
                        div>
                        {% if foo.parent_id %}
                            <p style="margin-top: 10px">@ {{ foo.parent.user.username }}p>
                            <p>{{ foo.content|safe }}p>
                        {% else %}
                            <p style="margin-top: 10px">{{ foo.content }}p>
                        {% endif %}
                    li>
                {% endfor %}
            ul>
    
    
        div>
        
        <div>
            <a href="">刷新页面a>
        div>
        <div style="margin-top: 60px">
            <i class="fa fa-commenting-o" aria-hidden="true">i>
            <b>发表评论b>
        div>
        {% if request.user.is_authenticated %} 
            <div>
                <label for="content">label>
                <textarea name="" id="content" cols="170" rows="10">textarea>
            div>
            <div class="pull-right">
                <button class="btn btn-info" id="comment" style="margin-bottom: 50px">提交评论button>
            div>
        {% else %}
            <div>
                <i class="fa fa-commenting-o" aria-hidden="true">i> <span style="margin-left: 10px">登录后才能发表评论,立即 <a
                    href="/login/">登录a> 或者 <a
                    href="/">逛逛a> 首页span>
            div>
        {% endif %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    js代码

    	</script>
            // 评论按钮点击事件
            var parent_id = ''
            $('#comment').click(function () {
                // 取出评价内容 包括子评论和跟评论
                var content = $('#content').val()
                // 判断 如果是子评论要删除 @ 名字 换行
                if (parent_id) {
                    console.log(content)
                    var i = content.indexOf('\n')//取到换行的索引
                    content = content.slice(i)//从索引位置往后切
                }
                $.ajax({
                    url: '/comment/',
                    type: 'post',
                    data: {// 谁给哪篇文章评论了什么 父评论的id
                        parent_id: parent_id,
                        article_id: {{ article.id }},
                        content: content,
                        csrfmiddlewaretoken: '{{ csrf_token }}'  // 坑!!! 一定要加引号
                    },
                    success: function (data) {
                        console.log(data)
                        if (data.code == 100) {
                            $('#content').val('')  //评论成功将评论区文字清空
                            var cur_name = data.people // 当前评论人
                            var content = data.content // 评论内容
                            var s = ''  // 将评论拼接到评论列表中
                            if (data.comment_name) {//如果是子评论
                                var comment_name = data.comment_name
                                s = `
                    
  • ${cur_name}:
    @${comment_name}
    ${content}
  • `
    } else { s = `
  • ${cur_name}:
    ${content}
  • `
    } } $('.comment-ajax').append(s)//追加到评论组的最后边 ajax提交跟评论和子评论 } }) }) // 回复事件 $('.reply').click(function () { parent_id = $(this).attr('parent_id') console.log(parent_id) var name = $(this).attr('username') // 将 @ 名字 换行 加到输入框中 $('#content').val(`@${name}\n`).focus()//光标聚焦 }) </script>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    8.6 评论后端

    def comment(request):
        res = {'code': 100, 'msg': '评论成功'}
        if request.user.is_authenticated:
            article_id = request.POST.get('article_id')
            content = request.POST.get('content')
            parent_id = request.POST.get('parent_id')
            # 保存评论
            # 开启事务
            with transaction.atomic():
                res_comment = Comment.objects.create(content=content, user=request.user, article_id=article_id,
                                                     parent_id=parent_id)
                # 文章表中评论数加1
                Article.objects.filter(id=article_id).update(comment_num=F('comment_num') + 1)
            # 评论成功发送邮件
            # 使用多线程
            article_title = Article.objects.filter(pk=article_id).first().title
            send = Article.objects.filter(pk=article_id).first().blog.userinfo.email
            t = Thread(target=send_mail,
                       args=(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, [send]))
            t.start()
            # send_mail(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, ['xuxiaoxu152@163.com'])  # subject, message, from_email, recipient_list,
            # 返回给前端当前评论人 和评论内容
            res['people'] = request.user.username
            res['content'] = content
            if parent_id:  # 如果这是一条子评论 将他评论的这条评论的博主名返回
                res['comment_name'] = res_comment.parent.user.username
            return JsonResponse(res)
        res['code'] = 101
        res['msg'] = '未登录 不能评论'
        return JsonResponse(res)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    九 后台管理

    9.1 后台管理模板

    base.html

        <div class="container-fluid">
            <div class="row">
                <div class="col-md-2">
                    <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
                        <div class="panel panel-default">
                            <div class="panel-heading" role="tab" id="headingOne">
                                <h4 class="panel-title">
                                    <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
                                       aria-expanded="false" aria-controls="collapseOne" class="collapsed">
                                        博客后台
                                    a>
                                h4>
                            div>
                            <div id="collapseOne" class="panel-collapse collapse" role="tabpanel"
                                 aria-labelledby="headingOne" aria-expanded="false" style="height: 0px;">
                                <div class="panel-body">
                                    <a href="/add/">新建随笔a>
                                div>
                                <div class="panel-body">
                                    <a href="">草稿箱a>
                                div>
                                <div class="panel-body">
                                    <a href="">回收站a>
                                div>
                            div>
                        div>
                        <div class="panel panel-default">
                            <div class="panel-heading" role="tab" id="headingTwo">
                                <h4 class="panel-title">
                                    <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
                                       href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
                                        分类
                                    a>
                                h4>
                            div>
                            <div id="collapseTwo" class="panel-collapse collapse" role="tabpanel"
                                 aria-labelledby="headingTwo" aria-expanded="false" style="height: 0px;">
                                <div class="panel-body">
                                    <a href="">新增分类a>
                                div>
                                <div class="panel-body">
                                    <a href="">分类列表<a>
                                div>
                            div>
                        div>
                    div>
                div>
                <div class="col-md-10">
                    <div class="is-show">
                        <h4 class="is-show">文章展示h4>
                        <ul class="nav nav-tabs">
                            <li role="presentation" class="active"><a href="#">文章a>li>
                            <li role="presentation"><a href="#">新闻a>li>
                            <li role="presentation"><a href="#">标签a>li>
                        ul>
    
                        <div class="tab-content">
                            <div role="tabpanel" class="tab-pane fade in active" id="home">
                                {% block crticle %}
    
                                {% endblock %}
                            div>
    
                        div>
                    div>
    
    
                    {% block add %}
    
                    {% endblock %}
                div>
    
            div>
        div>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    index.html

    {% extends 'backend/base.html' %}
    {% block title %}
        后台管理
    {% endblock %}
    
    {% block crticle %}
        <div class="bs-example" data-example-id="hoverable-table">
            <table class="table table-hover">
                <thead>
                <tr>
                    <th>编号th>
                    <th>标题th>
                    <th>发布时间th>
                    <th>评论数th>
                    <th>操作th>
                    <th>操作th>
                tr>
                thead>
                <tbody>
                    {% for article in article_list %}
                        <tr>
                            <td>{{ forloop.counter }}td>
                            <td><a href="/{{ article.blog.userinfo.username }}/articles/{{ article.id }}">{{ article.title }}/a>td>
                            <td>{{ article.create_time|date:'Y-m-d H:i' }}td>
                            <td>{{ article.comment_num }}td>
                            <td><a href="/delete/?pk={{ article.id }}">删除a>td>
                            <td><a href="/alter_article/?pk={{ article.id }}">修改a>td>
                        tr>
                    {% endfor %}
    
                tbody>
            table>
        div>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    9.2 新建文章

    前端模板

    {% extends 'backend/base.html' %}
    
    {% block link %}
        <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js">script>
        <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js">script>
    {% endblock %}
    
    {% block title %}
        添加文章
    {% endblock %}
    
    {% block add %}
        <div class="text-center" style="background: #2aabd2">
            <h3>添加随笔h3>
        div>
        <form action="" method="post">
            {% csrf_token %}
            <div class="form-group">
                <label for="add-title">标题label>
                <input type="text" id="add-title" name="title" class="form-control">
            div>
            <div class="form-group">
                <label for="add-content">内容label>
                <div>
                    <textarea name="content" id="editor_id" cols="300" rows="20">textarea>
                div>
    
            div>
    
            <div class="form-group">
                <label for="add-classify">分类label>
                <select class="form-control" name="category" id="add-classify">
                    {% for classify in classify_list %}
                        <option value="{{ classify.id }}">{{ classify.name }}option>
                    {% endfor %}
                select>
            div>
    
            <div class="form-group">
                <label for="add-tag">标签label>
                <select class="form-control" name="tag" id="add-tag" multiple>
                    {% for tag in tag_list %}
                        <option value="{{ tag.id }}">{{ tag.name }}option>
                    {% endfor %}
                select>
            div>
            <button class="btn btn-success form-control">上传文章button>
        form>
    
    {% endblock %}
    
    {% block js %}
    	// 使用富文本编辑器
        <script>
            KindEditor.ready(function (K) {
                window.editor = K.create('#editor_id', {
                    width: '100%',
                    height: '300px',
                    resizeType: '1',
                    // 上传图片相关
                    uploadJson: '/put_img/',
                    //filePostName: 'myfile',  //默认imgFile
                    //extraFileUploadParams: {
                       // 'csrfmiddlewaretoken': '{{ csrf_token }}'
                   // }  后端没有取消校验 需要传csrf
                });
            });
        script>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    def add(request):
        if request.method == 'GET':
            tag_list = Tag.objects.filter(blog=request.user.blog)
            classify_list = Classify.objects.filter(blog=request.user.blog)
            return render(request, 'backend/add.html', context={'tag_list': tag_list, 'classify_list': classify_list})
        title = request.POST.get('title')
        content = request.POST.get('content')
        # BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
        bs = BeautifulSoup(content, features='html.parser')
        # 截取html文本,将空格和换行替换成空,并截取70个字符
        desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
        # 剔除script标签
        script_list = bs.findAll('script')
        for i in script_list:
            i.decompose()  # 将每个script标签删除
        classify = request.POST.get('category')
        tag = request.POST.getlist('tag')  # 这是多对多的
        res = Article.objects.create(title=title, content=str(bs), desc=desc, classify_id=classify, blog=request.user.blog)
        # 多对多添加外键关系
        res.tag.add(*tag)
        return redirect('/backend/')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    富文本编辑器图片处理,查看官方文档。

    # 文章图片处理
    # 需要处理csrf 可已经用掉这个接口的csrf
    
    @csrf_exempt  # 免除校验
    def put_img(request):
        img = request.FILES.get('imgFile')
        path = os.path.join(settings.MEDIA_ROOT, 'upload', img.name)
        with open(path, 'wb') as f:
            for i in img:
                f.write(i)
        return JsonResponse({
            "error": 0,
            "url": f"http://127.0.0.1:8000/media/upload/{img.name}"
        })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    9.3 处理xss攻击

    xss跨站脚本,在内容中存script脚本,前端渲染时使用了safe,如果存在script脚本,就会执行。解决方案。富文本编辑器在输入代码块时会自动将尖括号转换成对应的字符,只需在后端将恶意的script清除即可。

    需要使用beautifulsoup4模块。

    -pip3 install beautifulsoup4
    -删除script标签
     soup = BeautifulSoup(content, 'html.parser')
     script_list=soup.findAll('script') # 搜索到html中所有的script标签
     for script in script_list:
         script.decompose() # 把搜到的script标签一个个删除
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    9.4 首页用户信息展示

    用户登陆后展示用户名和和管理选项按钮

    
                        {% if request.user.is_authenticated %}
                            <li><a href="{{ request.user.username }}">{{ request.user.username }}a>li>
                            <li class="dropdown">
                                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                                   aria-haspopup="true"
                                   aria-expanded="false">更多 <span class="caret">span>a>
                                <ul class="dropdown-menu">
                                    <li><a href="/set_pwd/">修改密码a>li>
                                    <li><a href="/backend/">后台管理a>li>
                                    <li><a href="/alter_icon/">修改头像a>li>
                                    <li role="separator" class="divider">li>
                                    <li><a href="/login_out/">退出登录a>li>
                                ul>
                            li>
                        {% else %}
                            <a href="/login/">登录a>
                            <a href="/register/">注册a>
                        {% endif %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    9.5 退出后台

    # 退出登录
    def login_out(request):
        logout(request)  # request.session.flush() 清除掉session和cookie
        return redirect('/')
    
    • 1
    • 2
    • 3
    • 4

    9.6 修改头像

    {% extends 'backend/base.html' %}
    
    {% block title %}
        修改头像
    {% endblock %}
    
    {% block add %}
        <form action="" method="post" enctype="multipart/form-data">
        {% csrf_token %}
            <div style="margin-top: 100px">
                <h3 style="color: darkslateblue">修改头像h3>
                <label for="icon">
                    <img src="/media/{{ icon }}" alt="" width="100px" height="100px" id="img">
                label>
                <input type="file" id="icon" style="display: none" name="icon">
                <button class="btn btn-success">确认修改button>
            div>
        form>
    
    
    {% endblock %}
    
    {% block js %}
        <script>
            $('.is-show').toggle()
    
            // 头像动态显示 给文件标签绑定一个变化事件
            $('#icon').change(function () {
                var reader = new FileReader()
    
                // 获取文件内容
                var file = $('#icon')[0].files[0]
    
                reader.readAsDataURL(file)
    
                reader.onload = (function () {
                    $('#img').attr('src', reader.result)
                })
    
            })
        script>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    # 修改头像
    def alter_icon(request):
        if request.method == "GET":
            # 需要当前用户头像
            icon = request.user.icon
            return render(request, 'backend/alter_icon.html', context={'icon': icon})
        icon = request.FILES.get('icon')
        request.user.icon = icon
        request.user.save()
        return redirect('/')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    9.7 修改密码

    {% extends 'backend/base.html' %}
    
    {% block title %}
        修改密码
    {% endblock %}
    
    {% block add %}
        <form action="" method="post">
        {% csrf_token %}
            <div class="form-group">
                <label for="pwd1">原密码label>
                <input type="password" id="pwd1" name="old_password" class="form-control">
            div>
            <div class="form-group">
                <label for="pwd2">新密码label>
                <input type="password" id="pwd2" name="new_password" class="form-control">
            div>
            <div class="form-group">
                <label for="pwd3">确认密码label>
                <input type="password" id="pwd3" name="re_password" class="form-control">
            div>
            <button class="form-control btn-success">提交button> <span style="color: red">{{ error }}span>
        form>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    def set_pwd(request):
        if request.method == 'GET':
            return render(request, 'backend/set_pwd.html')
        old_password = request.POST.get('old_password')
        new_password = request.POST.get('new_password')
        re_password = request.POST.get('re_password')
        if request.user.check_password(old_password):
            if new_password == re_password:
                request.user.set_password(new_password)
                request.user.save()
                # 退出当前登录 跳转至登录
                login_out(request)
                return redirect(to='/login/')
            return render(request, 'backend/set_pwd.html', context={'error': '两次密码不一致'})
        return render(request, 'backend/set_pwd.html', context={'error': '原密码不一致'})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    9.8 修改文章

    {% extends 'backend/base.html' %}
    
    {% block link %}
        <script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js">script>
        <script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js">script>
    {% endblock %}
    
    {% block title %}
        修改文章
    {% endblock %}
    
    {% block add %}
        <div class="text-center" style="background: #2aabd2">
            <h3>修改文章h3>
        div>
        <form action="" method="post">
            {% csrf_token %}
            <div class="form-group">
                <label for="add-title">标题label>
                <input type="text" id="add-title" name="title" class="form-control" value="{{ article.title }}">
            div>
            <div class="form-group">
                <label for="add-content">内容label>
                <div>
                    <textarea name="content" id="editor_id" cols="300" rows="20">{{ article.content }}textarea>
                div>
    
            div>
    
            <div class="form-group">
                <label for="add-classify">分类label>
                <select class="form-control" name="category" id="add-classify">
                    {% for classify in classify_list %}
                        {% if classify == article.classify %}
                            <option value="{{ classify.id }}" selected>{{ classify.name }}option>
                        {% else %}
                            <option value="{{ classify.id }}">{{ classify.name }}option>
                        {% endif %}
    
    
                    {% endfor %}
                select>
            div>
    
            <div class="form-group">
                <label for="add-tag">标签label>
                <select class="form-control" name="tag" id="add-tag" multiple>
                    {% for tag in tag_list %}
                        {% if tag in tag_list %}
                            <option value="{{ tag.id }}" selected>{{ tag.name }}option>
                        {% else %}
                            <option value="{{ tag.id }}">{{ tag.name }}option>
                        {% endif %}
    
                    {% endfor %}
                select>
            div>
            <button class="btn btn-success form-control">上传文章button>
        form>
    
    {% endblock %}
    
    {% block js %}
        <script>
            KindEditor.ready(function (K) {
                window.editor = K.create('#editor_id', {
                    width: '100%',
                    height: '300px',
                    resizeType: '1',
                    // 上传图片相关
                    uploadJson: '/put_img/',
                    //filePostName: 'myfile',  //默认imgFile
                    //extraFileUploadParams: {
                       // 'csrfmiddlewaretoken': '{{ csrf_token }}'
                   // }  后端没有取消校验 需要传csrf
                });
            });
        script>
    {% endblock %}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    def alter_article(request):
        pk = request.GET.get('pk')
        # 需要当前文章 当前用户的分类和标签
        if request.method == 'GET':
            article = Article.objects.filter(pk=pk).first()
            classify_list = Classify.objects.filter(blog=request.user.blog)
            tag_list = Tag.objects.filter(blog=request.user.blog)
            return render(request, 'backend/alter_article.html',
                          context={'article': article, 'classify_list': classify_list, 'tag_list': tag_list})
        # post请求 修改文章
        title = request.POST.get('title')
        content = request.POST.get('content')
        # BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
        bs = BeautifulSoup(content, features='html.parser')
        # 截取html文本,将空格和换行替换成空,并截取70个字符
        desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
        # 剔除script标签
        script_list = bs.findAll('script')
        for i in script_list:
            i.decompose()  # 将每个script标签删除
        classify = request.POST.get('category')
        tag = request.POST.getlist('tag')  # 这是多对多的
        article = Article.objects.filter(pk=request.GET.get('pk'))  # 必须是一个queryset
        # 还需要将该文章的评论点赞点踩一起更新
        up_num = Article.objects.filter(pk=pk).first().up_num
        down_num = Article.objects.filter(pk=pk).first().down_num
        comment_num = Article.objects.filter(pk=pk).first().comment_num
        with transaction.atomic():
            article.update(title=title, desc=desc, classify_id=classify, content=str(bs), blog=request.user.blog,
                           up_num=up_num, down_num=down_num, comment_num=comment_num)
            article.first().save()
            # 多对多关系添加
            article.first().tag.set(tag)
        return redirect(f'/{request.user.username}/articles/{pk}')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    9.9 django发送邮件

    from django.core.mail import send_mail
    # (subject, message, from_email, recipient_list,)
    res1 = send_mail('邮件标题', '邮件内容', settings.EMAIL_HOST_USER, ["@qq.com"])
    
    • 1
    • 2
    • 3

    settings.py配置

    # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    EMAIL_HOST = 'smtp.qq.com'  # 如果是 163 改成 smtp.163.com
    EMAIL_PORT = 465
    EMAIL_HOST_USER = '@qq.com'  # 帐号
    EMAIL_HOST_PASSWORD = '***'  # 密码
    DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
    #这样收到的邮件,收件人处就会这样显示
    #DEFAULT_FROM_EMAIL = ''
    EMAIL_USE_SSL = True   #使用ssl
    #EMAIL_USE_TLS = False # 使用tls
    
    #EMAIL_USE_SSL 和 EMAIL_USE_TLS 是互斥的,即只能有一个为 True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
  • 相关阅读:
    Nginx部署history路由模式的vue项⽬
    鸿蒙开发工程师面试-架构篇
    线性代数学习笔记11-1:总复习Part1(CR分解、LU分解、QR分解)
    ES 集群常用排查命令
    PWMADC重要参数
    echarts折线图流动特效的实现(非平滑曲线)
    一次k8s docker下.net程序的异常行为dump诊断
    VMWare的安装,创建虚拟机及安装CentOS图文详细步骤(内含VMWare安装包,CentOS安装包)
    前端周刊第十二期
    ARM系列 -- 虚拟化(三)
  • 原文地址:https://blog.csdn.net/weixin_68531269/article/details/126859280