五十二、BBS项目

一 项目分析

BBS项目:

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

二 项目表设计及关联

  1. 创建数据库bbs

    create database bbs
    
  2. 表分析

    一共需要创建七张表

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

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

3.1 创建项目

  1. 安装django 2.2.2版本

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

    配置setting.py

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

    配置语言环境

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

    配置数据库

    DATABASES = {
    	'default': {
    		'ENGINE':'django.db.backends.mysql',
    		'NAME': 'bbs',
    		'HOST': '127.0.0.1',
    		'PORT': 3306,
    		'USER': 'root',
    		'PASSWORD':'password'
    	}
    }
    

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)

终端执行数据库迁移命令

python38 manage.py makemigretions
python38 manage.py migrater

没有安装mysqlclient会报错

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

知识点

  • 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)
    
  • 字段类的属性(字段类型)
    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

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),
]

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})

4.4 前端模板编写

register.html

需要先配置静态文件
	-在setting.py中
		STATICFILES_DIRS = [
			os.path.join(BASE_DIR, 'static')
	] 
	-把bootstrap和jquery导入模板中
<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">
	                    <!--提交按钮不能是submit或者单独的button按钮 如果写了ajax 点击提交 就会发送两次请求-->
	                    <input type="button" value="注册" class="btn btn-success" id="id_submit">
	                    <span class="text-danger error"></span>
	                </div>
	            </form>
	        </div>
	    </div>
	</div>
</body>

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>

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) {

			}

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

现在后端可以收到数据 继续写后端
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)

前端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')
                 }
             })
         }
     }
 })

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

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

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>

后端

urls.py

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

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)

五 登录功能

5.1 登陆界面搭建

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</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>
</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>

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))

上面是自定义的图片验证码,也可以使用第三方模块,比如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())

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>

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)

六 首页

6.1 导航条和轮播图

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</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>
</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 navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">博客园</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="#">首页 <span class="sr-only">(current)</span></a></li>
                    <li><a href="#">新闻</a></li>
                </ul>
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="#">jasper</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="#">修改密码</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><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </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">
                    <!-- Indicators -->
                    <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>

                    <!-- Wrapper for slides -->
                    <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>

                    <!-- Controls -->
                    <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">Previous</span>
                    </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">Next</span>
                    </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>

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)

注意:

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

class Meta:
    verbose_name_plural = '博客表'

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

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

def __str__(self):
    return self.title

6.3 开启media访问

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

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

注意: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})

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>

七 个人站点页面搭建

7.1 路由配置

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

# 站点匹配 必须放最后
path('<str:name>/', views.site),

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())

7.3 404界面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>404</title>
</head>
<body>
<script type="text/javascript"
        src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js"
        charset="utf-8">
</script>
</body>
</html>

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>

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 navigation</span>
                        <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>

                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        <li>
                            <button type="button" class="btn btn-danger navbar-btn">管理</button>
                        </li>
                    </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </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 %}

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

urls.py

# jasper/tag/4.html 标签匹配
path('<str:name>/tag/<int:tag>.html', views.site),
# jasper/classify/3.html
path('<str:name>/classify/<int:classify>.html', views.site),
# jasper/archive/202209.html
path('<str:name>/archive/<int:time>.html', views.site),

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())

八 文章详情和点赞点踩

8.1 左侧列表组使用inclusion_tag实现

# 自定义标签
1. 在应用下创建templatetags包,必须是templatetags
2. 在templatetags中新建一个new_tag.py文件,py文件名随意。
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中使用

left.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</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>

base.html

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

<div class="col-md-2">
    {% load new_tag %}
    {% left name %}
</div>

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

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

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

8.2 点赞点踩样式

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

path('<str:name>/articles/<int:article_id>', views.article_detail),

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')

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 navigation</span>
                        <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>

                <!-- Collect the nav links, forms, and other content for toggling -->
                <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                    <ul class="nav navbar-nav">
                    </ul>
                    <ul class="nav navbar-nav navbar-right">
                        <li>
                            <button type="button" class="btn btn-danger navbar-btn">管理</button>
                        </li>
                    </ul>
                </div><!-- /.navbar-collapse -->
            </div><!-- /.container-fluid -->
        </nav>
    </div>
{% endblock %}

{% block crticle %}
    <div>
        <h3>{{ article.title }}</h3>
    </div>

    <div>
        {{ article.content }}
    </div>
    <!--点赞点踩样式 直接copy-->
    <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 %}

/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;
}

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>

设置路由

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

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'] = '没有登录点击跳转<a href="/login/">登录</a>'
        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)

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 %}

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 = `
                <li class="list-group-item" style="margin-top: 20px">
                <i class="fa fa-commenting" aria-hidden="true"></i>
                <b><span>${cur_name}:</span></b>
                <div><span>@${comment_name}</span></div>
                <div><span>${content}</span></div>


                </li>`
                        } else {
                            s = `
                <li class="list-group-item" style="margin-top: 20px">
                <i class="fa fa-commenting" aria-hidden="true"></i>
                <b><span>${cur_name}:</span></b>
                <div>
                <span>${content}</span>
                </div>
                </li>`
                        }
                    }
                    $('.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>

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)

九 后台管理

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>

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 %}

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 %}
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/')

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

# 文章图片处理
# 需要处理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}"
    })

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标签一个个删除

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 %}

9.5 退出后台

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

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 %}
# 修改头像
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('/')

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 %}
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': '原密码不一致'})

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 %}
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}')

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"])

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值