• 阶段总结之BBS


    目录

    项目流程

    1.1功能需求分析

    1.2 技术栈

    1.3 表关系梳理

    1.4 表的关联关系

    创建项目

    2.1 创建django项目, 配置设置信息, 连接数据库等

    2.2 基于auth的user表扩写字段

    2.3 表关系建立

    2.4 迁移表

    2.5 注册forms编写

    2.6 路由配置

    2.7 基于forms组件写的注册页面

    2.8 头像动态显示

    2.9  此时的错误提示信息不会消失 需要绑定一个定时任务

    2.10 校验用户是否存在

    登录功能

    3.1 登陆界面搭建

     3.2 自定义图片验证码

    3.3 登陆界面前端发送数据

    3.4 登录后端

    首页

    4.1 导航条和轮播图

    4.2 首页文章列表

    4.3  开启media访问

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

     4.4 图片防盗链

     4.5 首页文章渲染

    个人站点页面搭建

    5.1 路由配置

    5.2 后端

    5.3  404界面

     5.4 个人站点前端

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

    5.6  文章详情和点赞点踩

    左侧列表组使用inclusion_tag实现

    left.html

    base.html

    5.7 点赞点踩样式

    5.8 article.html

    5.9 点赞点踩前端js

    5.10 点赞点踩后端

    5.11 评论前端页面

    5.12 评论后端

     后台管理

    6.1 后台管理模板

    6.2 新建文章

    前端模板

    6.3 处理xss攻击

    6.4 首页用户信息展示

    6.5 退出后台

    6.6 修改头像

    6.7 修改密码

    6.8 修改文章

    6.9 django发送邮件

    小知识点补充

    字段类的属性(字段类型)

    ManyToManyField

    没有安装mysqlclient会报错


    学习完MySQL数据库和Django框架之后, 我们结合前后端来写一个项目.此项目主要是模仿博客园.

    项目流程

    1.1功能需求分析

            1.注册功能

            2.登录功能

            3.个人主页: 文章展示,侧边栏过滤>> 按照时间,标签, 分类

            4. 文章详情 : 点赞点踩. 评论

            5.后台管理; 个人文案行展示[增删改查]

            6.发布文章>> 富文本编辑器, xss攻击处理

    1.2 技术栈

            python 3.8            django 2.2.2         mysql 5.6.4         jquery 2.x         bootstrap3

    1.3 表关系梳理

    • 用户表
    • 博客表
    • 标签表
    • 分类表
    • 文章表
    • 点赞点踩表
    • 评论表

    1.4 表的关联关系

    • 用户表[基于auth的user表扩写字段]
    • 博客表[ 与用户表一对一]
    • 标签表[跟博客表一对多, 跟文章表多对多]
    • 分类表[跟博客表一对多,跟文章表一对多]
    • 文章表[跟博客表一对多]
    • 点赞点踩表[跟用户一对多, 跟文章表一对多]
    • 评论表[跟用户一对多, 跟文章表一对多]

    创建项目

    2.1 创建django项目, 配置设置信息, 连接数据库等

    1. # 时间国际化
    2. LANGUAGE_CODE = 'Asia-Shanghai'
    3. TIME_ZONE = 'UTC'
    4. USE_I18N = True
    5. USE_L10N = True
    6. USE_TZ = False
    7. # 连接数据库
    8. DATABASES = {
    9. 'default': {
    10. 'ENGINE': 'django.db.backends.mysql',
    11. 'NAME': 'bbs',
    12. 'HOST': '127.0.0.1',
    13. 'PORT': 3306,
    14. 'USER': 'root',
    15. 'PASSWORD':'123',
    16. 'CHARSET': 'UTF8'
    17. }
    18. }
    19. # 静态资源配置
    20. STATIC_URL = '/static/'
    21. STATICFILES_DIRS = [
    22. os.path.join(BASE_DIR,'static')
    23. ]

    2.2 基于auth的user表扩写字段

    1. class UserInfo(AbstractUser):
    2. phone = models.CharField(max_length=32,null=True,)
    3. avatar = models.FileField(upload_to='avatar',default='avatar/default.jpg')
    4. blog = models.OneToOneField(to='Blog',on_delete=models.CASCADE,null=True)

    2.3 表关系建立

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

    2.4 迁移表

    1. python manage.py makemigrations
    2. python manage.py migrate

    2.5 注册forms编写

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

    2.6 路由配置

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

    2.7 基于forms组件写的注册页面

    views.py

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

     register.html

    1. class="container-fluid">
    2. class="row">
    3. class="col-md-6 col-md-offset-3">
    4. class="text-center text-info">注册功能

    5. "" id="id_form">
    6. {% csrf_token %}
    7. {% for foo in form_obj %}
    8. class="form-group">
    9. {{ foo }}
    10. class="pull-right text-danger">
  • {% endfor %}
  • class="form-group">
  • "/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">
  • class="form-group text-center">
  • <input type="button" value="注册" class="btn btn-success" id="id_submit">
  • class="text-danger error">
  • 2.8 头像动态显示

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

    发送Ajax请求

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

     

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

    views.py

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

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

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

     

    2.9  此时的错误提示信息不会消失 需要绑定一个定时任务

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

    2.10 校验用户是否存在

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

    前端

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

    后端

    urls.py

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

    views.py

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

    登录功能

    3.1 登陆界面搭建

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

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

     3.2 自定义图片验证码

    验证码:字母数字共五位

    views.py

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

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

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

    3.3 登陆界面前端发送数据

    login.html

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

    3.4 登录后端

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

    首页

    4.1 导航条和轮播图

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

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

    4.2 首页文章列表

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

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

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

    注意:

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

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

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

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

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

    4.3  开启media访问

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

    在settings.py中添加

    MEDIA_ROOT = os.path.join(BASEDIR, 'media')
    

    在urls中添加

    1. from django.views.static import serve
    2. from django.conf import settings
    3. path('media/', serve, {'document_root':settings.MEDIA_ROOT})

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

     4.4 图片防盗链

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

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

     4.5 首页文章渲染

    views.py

    后端返回所有文章

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

    index.html

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

    个人站点页面搭建

    5.1 路由配置

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

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

    5.2 后端

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

    5.3  404界面

    1. "en">
    2. "UTF-8">
    3. <span class="hljs-number">404</span>

     5.4 个人站点前端

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

    base.html

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

    site.html

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

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

    urls.py

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

    views.py

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

    5.6  文章详情和点赞点踩

    左侧列表组使用inclusion_tag实现

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

    left.html

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

    base.html

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

    1. class="col-md-2">
    2. {% load new_tag %}
    3. {% left name %}

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

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

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

    5.7 点赞点踩样式

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

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

    views.py

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

    5.8 article.html

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

    /static/css/up.css

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

    5.9 点赞点踩前端js

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

    设置路由

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

    5.10 点赞点踩后端

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

    5.11 评论前端页面

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

    js代码

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

    5.12 评论后端

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

     后台管理

    6.1 后台管理模板

    base.html

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

    index.html

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

    6.2 新建文章

    前端模板

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

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

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

    6.3 处理xss攻击

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

    需要使用beautifulsoup4模块。

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

    6.4 首页用户信息展示

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

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

    6.5 退出后台

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

    6.6 修改头像

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

    6.7 修改密码

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

    6.8 修改文章

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

    6.9 django发送邮件

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

    settings.py配置

    1. # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
    2. EMAIL_HOST = 'smtp.qq.com' # 如果是 163 改成 smtp.163.com
    3. EMAIL_PORT = 465
    4. EMAIL_HOST_USER = '@qq.com' # 帐号
    5. EMAIL_HOST_PASSWORD = '***' # 密码
    6. DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
    7. #这样收到的邮件,收件人处就会这样显示
    8. #DEFAULT_FROM_EMAIL = ''
    9. EMAIL_USE_SSL = True #使用ssl
    10. #EMAIL_USE_TLS = False # 使用tls
    11. #EMAIL_USE_SSL 和 EMAIL_USE_TLS 是互斥的,即只能有一个为 True

    小知识点补充

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

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

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

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

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

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

    OneToOneField就是ForeignKey + unique
     

    字段类的属性(字段类型)


    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:反向操作时,使用的连接的前缀,用户替换表名。
     

    终端执行数据库迁移命令

    python38 manage.py makemigretions
    python38 manage.py migrater

    没有安装mysqlclient会报错

    解决方案一:
    在任意的双下init文件中编写以下代码:

    1. import pymysql
    2. pymysql.install_as_MySQLdb()

    在django2.0.7及以后版本,需要改源码才能使用,operations.py中的146行,改成query = query.encode(errors=‘replace’)。

    解决方法二:

    pip3 instasll mysqlclient
    

  • 相关阅读:
    全网最全!!Qt实现图片旋转及图片旋转动画的几种方式
    家长杂志家长杂志社家长编辑部2022年第30期目录
    IOday1
    【LeetCode】每一轮都要把输入数组看一遍的二分
    MATLAB | 全网唯一 MATLAB双向弦图(有向弦图)绘制
    探索ChatGPT的前沿科技:解锁其在地理信息系统、气候预测、农作物生长等关键领域的创新应用
    leetcode 1208.尽可能使字符串相等 滑动窗口
    linux线程创建等待及退出总结
    30-Spark入门之Spark技术栈讲解、分区、系统架构、算子和任务提交方式
    从电商到超市,美团的零售之变
  • 原文地址:https://blog.csdn.net/weixin_67531112/article/details/126906389