BBS项目创作流程

BBS项目创作流程

【零】完整文件

【一】项目基本配置

【1】所需模块

asgiref==3.7.2
beautifulsoup4==4.12.3
certifi==2024.2.2
charset-normalizer==3.3.2
Django==3.2.12
fake-useragent==1.5.1
idna==3.6
lxml==5.1.0
mysqlclient==2.2.4
pillow==10.2.0
pytz==2024.1
requests==2.31.0
soupsieve==2.5
sqlparse==0.4.4
typing_extensions==4.10.0
tzdata==2024.1
urllib3==2.2.1

【2】基础搭建

# settings.py

# 注册app
INSTALLED_APPS =['...']
# 配置数据库
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        # 库名需要提前在mysql中注册
        'NAME': 'blog_based_system',
        'HOST': '127.0.0.1',
        'PORT': 3306,
        'USER': 'root',
        'PASSWORD': 'xxx',
        'CHARSET': 'utf8mb4'
    }
}
# 修改时区
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
# 配置静态文件路径
STATICFILES_DIRS = [os.path.join()]

# 配置用户表
AUTH_USER_MODEL = '指定用户表'

# 配置媒体文件路径
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')

【二】建表

【1】对数据表进行分析

  • 用户表(UserInfo)
    • 继承AbstractUser
    • 拓展字段
字段名 类型 注释
phone BigIntegerField 电话
avatar FileField 头像链接
create_time DateField 创建时间
blog OneToOneField(to="Blog") 外键字段,一对一,个人站点表
  • 个人站点表(Blog)
字段名 类型 注释
site_name CharField 站点名称
site_title CharField 站点标题
site_theme CharField 站点样式
  • 文章分类表(Category)
字段名 类型 注释
name CharField 分类名
blog ForeignKey(to="Blog") 外键字段,一对多,个人站点
  • 标签表(Tag)
字段名 类型 注释
name CharField 标签名
  • 文章表(Article)
字段名 类型 注释
title CharField 文章标题
desc CharField 文章摘要/文章简介
content TextField 文章内容
create_time DateField 发布时间
up_num BigIntegerField 点赞数
down_num BigIntegerField 点踩数
comment_num BigIntegerField 评论数
blog ForeignKey(to="Blog") 外键字段,一对多,个人站点
category ForeignKey(to="Category") 外键字段,一对多,文章分类
tags ManyToManyField(to="Tag") 外键字段,多对多,文章标签
  • 点赞点踩表(UpAndDown)
    • 用来记录哪个用户对哪篇文章点了赞还是点了踩
字段名 类型 注释
user ForeignKey(to="UserInfo") 用户主键值
article ForeignKey(to="Article") 文章主键值
is_up BooleanField() 是否点赞
  • 评论表(Comment)
    • 用来记录哪个用户给哪篇文章写了哪些评论内容
字段名 类型 注释
user ForeignKey(to='UserInfo') 用户主键值
article ForeignKey(to="Article") 文章主键值
content CharField() 评论内容
comment_time DateTimeField 评论时间
parent ForeignKey(to="self",null=True) 自关联

BBS表分析

【2】建表注意事项

【2.1】基于AbstractUser表创建UserInfo

注意,基于系统表创建用户表需要在执行数据迁移前,django默认用户表表名为auth_user

  • 用户信息表基于【from django.contrib.auth.models import AbstractUser】创建
  • 注意要在settings.py文件中配置AUTH_USER_MODEL='指定用户表'
  • 这样创建的用户表可以被django管理后台识别,可以通过该表中的数据登录django后台

【2.2】公共表CommentModel

  • 因为很多表可能需要用到创建时间和更新时间的字段,可以创建一个公共表来快捷的注册这两个字段
  • 当模型表继承公共表时,如果在创建公共表时,公共表继承了models.Model那么继承公共表的可以不用继承models.Model
class CommonModel(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    update_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')

    class Meta():
        # 将公共表定义成抽象表
        # 只能继承,无法实例化
        abstract = True

【2.3】在建表时使用class Metadef __str__(self)

class Table(models.Model):
    ...
    class Meta:
        db_table = '指定在数据库中的表名'  # 数据库建表时默认使用【应用程序名+表名小写】
        verbose_name_plural = '为该表起别名'
        
    def __str__(self):
        return '可以返回一些实例化模型表对象时的信息'
   		# 比如用户对象时,可以返回【self.name】,这样在后端查看时,可以直接显示用户姓名

【2.4】勤使用三引号作注释

image-20240314161549840

【3】将数据库表注册至管理员后台

  • 在django管理员后台可以查看并操作数据库
# admin.py  # 每一个应用下都有一个admin文件  # 根据分表创建注册即可

from django.contrib import admin

# Register your models here.
from .models import 表

@admin.register(表)
class 表名(admin.ModelAdmin):
    list_display = ['注册想要显示在后台的字段']
  • 注意事项
    • 在后台中,多对多字段是没办法显示的,所以多对多的字段注册不进去
    • 注册在后台的字段,显示的是verbose_name属性的值,所以建表时,可以指定值

image-20240314221620914

【4】使用Navicat逆向数据库至模型

  • 在Navicat中【右键数据库】---- 【 逆向数据库至模型】

image-20240314170001145

  • 模型参考

Diagram 1

【前置】公共功能

【1】CommonResponse : 返回json格式的响应

  • 通用的返回jsonresponse格式数据
from django.http import JsonResponse

def CommonResponse(code=200, msg=None, **kwargs):
    '''
    为了在ajax请求时,方便的返回响应数据
    :param code: 响应状态码
    :param msg: 响应信息
    :param kwargs: 其他可能会有的关键字参数
    :return: JsonResponse
    '''
    if msg:
        data = {'code': code, 'msg': msg}
    else:
        data = {'code': code}
    if kwargs:
        data.update(kwargs)
    return JsonResponse(data)

【2】CommonPaper : 分页器

class Pagination(object):
    def __init__(self, current_page, all_count, per_page_num=2, pager_count=11):
        """
        封装分页相关数据
        :param current_page: 当前页
        :param all_count:    数据库中的数据总条数
        :param per_page_num: 每页显示的数据条数
        :param pager_count:  最多显示的页码个数
        """
        try:
            current_page = int(current_page)
        except Exception as e:
            current_page = 1

        if current_page < 1:
            current_page = 1

        self.all_count = all_count
        self.per_page_num = per_page_num

        # 总页码
        all_pager, tmp = divmod(all_count, per_page_num)
        if tmp:
            all_pager += 1
        self.all_pager = all_pager
        if current_page > all_pager:
            current_page = all_pager
        self.current_page = current_page
        self.pager_count = pager_count
        self.pager_count_half = int((pager_count - 1) / 2)

    @property
    def start(self):
        return (self.current_page - 1) * self.per_page_num

    @property
    def end(self):
        return self.current_page * self.per_page_num

    def page_html(self):
        # 如果总页码 < 11个:
        if self.all_pager <= self.pager_count:
            pager_start = 1
            pager_end = self.all_pager + 1
        # 总页码  > 11
        else:
            # 当前页如果<=页面上最多显示11/2个页码
            if self.current_page <= self.pager_count_half:
                pager_start = 1
                pager_end = self.pager_count + 1

            # 当前页大于5
            else:
                # 页码翻到最后
                if (self.current_page + self.pager_count_half) > self.all_pager:
                    pager_end = self.all_pager + 1
                    pager_start = self.all_pager - self.pager_count + 1
                else:
                    pager_start = self.current_page - self.pager_count_half
                    pager_end = self.current_page + self.pager_count_half + 1

        page_html_list = []
        # 添加前面的nav和ul标签
        page_html_list.append('''
                    <nav aria-label='Page navigation>'
                    <ul class='pagination'>
                ''')
        first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
        page_html_list.append(first_page)

        if self.current_page <= 1:
            prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
        else:
            prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)

        page_html_list.append(prev_page)

        for i in range(pager_start, pager_end):
            if i == self.current_page:
                temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
            else:
                temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
            page_html_list.append(temp)

        if self.current_page >= self.all_pager:
            next_page = '<li class="disabled"><a href="#">下一页</a></li>'
        else:
            next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
        page_html_list.append(next_page)

        last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
        page_html_list.append(last_page)
        # 尾部添加标签
        page_html_list.append('''
                                           </nav>
                                           </ul>
                                       ''')
        return ''.join(page_html_list)

【3】CommonVerifyCode : 图形验证码

from PIL import Image, ImageDraw, ImageFont, ImageFilter
import random
from io import BytesIO


def rgb_number():
    # rgb_tuple = [random.randint(0, 255) for i in range(3)]
    return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)


# 创建图片验证码
def create_verify_code(img_type="RGB", img_size: tuple = (115, 34), img_rgb_number: tuple = None):
    if not img_rgb_number:
        img_rgb_number = rgb_number()
    # 利用 Image 对象定义一个图片文件参数
    img_obj = Image.new(mode=img_type, size=img_size, color=img_rgb_number)
    img_obj.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
    # 利用 ImageDraw 对象产生一个画笔对象
    img_draw = ImageDraw.Draw(img_obj)
    # 利用 ImageFont 对象定义验证码字体参数(字体样式,字体大小)
    img_font = ImageFont.truetype('static/font/汉仪晴空体简.ttf', 30)

    # 创建随机验证码(数字 + 字母)
    code = ''

    for i in range(4):
        string_list = [
            str(random.randint(0, 9)),
            chr(random.randint(65, 90)),
            chr(random.randint(97, 122))
        ]
        temp = random.choice(string_list)

        # 借助画笔对象依次写入图片数据((x偏移值,y偏移值),写入内容,字体颜色,字体样式)
        img_draw.text((i * 25 + 10, 0), temp, fill=rgb_number(), font=img_font)
        # 拼接随机字符串
        code += temp

    # 随机验证码在登录的视图函数中需要比对,所以需要找地方存起来,便于其他函数调用

    # 生成 IO 对象 操作字节流数据
    io_obj = BytesIO()
    # 利用 Image 对象保存图片数据成指定格式
    img_obj.save(io_obj, "png")
    # 利用 IO 对象读取保存的图片字节数据
    img_data = io_obj.getvalue()
    # 返回生成的随机验证码和验证码的图片二进制数据
    return code, img_data

【三】用户功能

【1】用户注册

【1.1】前端

  • 需要渲染错误信息时,你的错误信息需要与{{ form }}在同一个块级标签中,如果分成两个,可能在渲染时无法找到

image-20240314215043335

  • 渲染头像小技巧

image-20240314215822988

  • 实时渲染

image-20240314220207326

  • 使用form.serializeArray()获得form表单中所有的数据

image-20240314220503512

  • 渲染错误信息

image-20240314221103786

  • 报错信息一直显示着比较难看,所以当用户将鼠标回到input框进行修改时,将报错信息以及错误样式取消

image-20240314221242392

  • 需要注意,以上操作均需要在事件监控的前提下进行

image-20240314221358863

  • 【注】使用箭头函数时this就失效了,就无法定位到当前对象了

【1.2】后端

  • 后端基本上没有到很巧妙的技术,都是基础技术,直接放上代码
  • 【注】由于用户信息与个人站点绑定,所以在录入信息时,将站点信息一起录入
def register(request):
    register_form = RegisterForm()
    # 判断是否时post且时ajax传入的
    if request.method == 'POST' and request.is_ajax():
        kv_data = request.POST
        # 得到普通的键值对数据
        register_form = RegisterForm(kv_data)
        # 对数据进行清洗
        if not register_form.is_valid():
            # 如果有错误,将错误信息传回前端
            return CommonResponse(code=300, errors=register_form.errors)
        kv_data = register_form.cleaned_data
        # 获取到清洗过的数据
        # 并讲在数据库中没有的字段删除掉
        kv_data.pop('confirm_pwd')
        blog_title = kv_data.pop('blog_title')
        # 获取文件对象
        avatar_data = request.FILES.get('avatar')
        theme_data = request.FILES.get('theme')
        if not theme_data:
            blog_obj = Blog.objects.create(name=kv_data.get('username'), title=blog_title)
        else:
            blog_obj = Blog.objects.create(name=kv_data.get('username'), title=blog_title, theme=theme_data)
        # 创建用户,可以通过解包将字典中的键值对传回数据库
        if avatar_data:
            # 如何上传了头像,使用对应的头像
            user_obj = UserInfo.objects.create_user(**kv_data, blog=blog_obj, avatar=avatar_data)
        else:
            # 如果没有上传头像,使用默认头像
            user_obj = UserInfo.objects.create_user(**kv_data, blog=blog_obj)
        # 注册成功,向前端返回信息
        return CommonResponse(code=200, msg=f"{kv_data.get('username')} 注册成功")
    return render(request, 'register.html', locals())

【2】用户登录

  • 登录就是基于auth模块对用户名和密码进行校验
'''使用了图片验证码,没有使用forms组件,使用的是layui表单'''
def login(request):
    '''没用forms组件'''
    # login1_obj = LoginForm()
    if request.is_ajax() and request.method == 'POST':
        data = request.POST
        username = data.get('username')
        password = data.get('password')
        captcha = data.get('captcha')
        if captcha.upper() != request.session.get('captcha').upper():
            return CommonResponse(code=300, errors='验证码错误')
        user_obj = auth.authenticate(username=username, password=password)
        if not user_obj:
            return CommonResponse(code=300, errors='密码输入错误')
        auth.login(request, user_obj)
        return CommonResponse(code=200, msg=f'{username}登录成功')
    return render(request, 'Login_test.html', locals())

【3】用户注销

  • 基于auth模块的注销功能
@login_required
def logout(request):
    auth.logout(request)
    url = reverse('home')
    return redirect(to=url)

【4】修改密码 [ 使用模态框跳转 ]

# views.py

def editPassword(request):
    if request.is_ajax() and request.method == 'POST':
        data = request.POST
        # username = request.user.username
        # 获取数据
        old_password = data.get('old_password')
        new_password = data.get('new_password')
        captcha = data.get('captcha')
        # 条件判断
        if not all([old_password, new_password, captcha]):
            return CommonResponse(code=100, errors='请补全参数')
        if captcha.upper() != request.session.get('captcha').upper():
            return CommonResponse(code=300, errors='验证码输入错误')
        is_pwd = request.user.check_password(old_password)
        if not is_pwd:
            return CommonResponse(code=400, errors='原密码输入错误')
        if len(new_password) < 5:
            return CommonResponse(code=401, errors='密码至少5位')
        # 保存修改数据
        request.user.set_password(new_password)
        request.user.save()
        # 将登录状态退出
        auth.logout(request)
        return CommonResponse(code=200, msg='密码修改成功')
  • 导航栏菜单
    • 关键参数:data-toggledata-target,执行模态框
<li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
       aria-expanded="false">{{ request.user.username }} <span class="caret"></span></a>
    <ul class="dropdown-menu">
        <li><a href="#" data-toggle="modal" data-target="#editPwdModal">修改密码</a></li>
        <li><a href="#">修改头像</a></li>
        <li><a href="#">修改信息</a></li>
        <li role="separator" class="divider"></li>
        <li><a href="{% url 'user:logout' %}">退出登录</a></li>
    </ul>
</li>
  • 模态框
<!-- 修改密码模态框 -->
<div class="modal fade" id="editPwdModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
                </button>
                <h4 class="modal-title" id="myModalLabel">修改密码</h4>
            </div>
            <form id="editPwdForm">
                {% csrf_token %}
                <div class="modal-body">
                    <div class="form-group">  {# 用户名显示 #}
                        <label for="username">用户名 :</label>
                        <input type="text" id="username" name="username" value="{{ request.user.username }}" disabled
                               class="form-control">
                    </div>
                    <div class="form-group">  {# 旧密码校验 #}
                        <label for="old_password">原密码 :</label>
                        <input type="password" id="old_password" name="old_password" class="form-control">
                    </div>
                    <div class="form-group">  {# 新密码校验 #}
                        <label for="new_password">新密码 :</label>
                        <input type="password" id="new_password" name="new_password" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="id_captcha">验证码</label>
                        <div class="row">
                            <div class="col-md-9"><input type="text" id="id_captcha" class="form-control"
                                                         name="captcha"></div>
                            <div class="col-md-3"><img src="{% url 'user:verify_code' %}" alt="图片验证码"
                                                       id="captcha_img"
                                                       onclick="this.src='{% url 'user:verify_code' %}'+'?t='+ new Date().getTime();">
                            </div>
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
                    <input type="button" class="btn btn-primary" id="editPwdSave" value="保存修改">
                </div>
            </form>
        </div>
    </div>
</div><!-- 修改密码模态框结束 -->

【四】站点搭建

  • 前端框架主要使用bootstrap V3

【1】主页导航栏搭建

【1.1】根据登录状态显示不同页面

  • 通过后端locals()传递上下文数据,可以将request对象传递给前端

  • 可以通过request.use等操作,判断属性和获取属性

<!-- 用户登录页面 -->
{% if request.user.is_authenticated %}  <!-- 判断用户登录状态 -->
<!-- 用户登录显示用户头像,用户名 -->
<li><img src="/media/{{ request.user.avatar }}/" alt="头像走丢了"
         style="height: 50px;width: 50px;align-items: center" class="img-circle"></li>
<li class="dropdown">
    <!-- 用户名可以设置下拉菜单提供用户操作 -->
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
       aria-expanded="false">{{ request.user.username }} <span class="caret"></span></a>
    <ul class="dropdown-menu">
        <li><a href="#">Action</a></li>
        <li><a href="#">Another action</a></li>
        <li><a href="#">Something else here</a></li>
        <li role="separator" class="divider"></li>
        <li><a href="{% url 'user:logout' %}">退出登录</a></li>
    </ul>
</li>
{% else %}  <!-- 用户未登录页面 -->
<li><a href="{% url 'user:register' %}">注册</a></li>
<li><a href="{% url 'user:login' %}">登录</a></li>
{% endif %}

【1.2】搜索栏携带数据去百度搜索

  • 表单
 <form class="navbar-form navbar-left" id="form_search">  <!-- 搜索表单 -->
     <div class="form-group">
         <input type="text" class="form-control" placeholder="Search" id="input_search">
     </div>
     <button type="submit" class="btn btn-default">百度一下</button>
</form>  <!-- 搜索表单结束 -->
  • js代码
<script>
    $(document).ready(function () {
            // 声明搜索函数
            let func_search = function search() {
                let txt = $("#input_search").val();
                // 拼接地址
                let next_url = 'https://www.baidu.com/s' + '?wd=' + txt
                window.open(next_url, '_blank')
            };  // 声明函数结束
            // 阻止搜索的form表单提交 执行函数
            $("#form_search").submit(function (event) {
                event.preventDefault()
                func_search()
            })  // 执行函数结束
        } // ready(){} end
    ) // ready end
</script>

【2】使用自定义标签生成广告

  • templatetags:存放自定义标签函数的文件夹
  • blog_inclusion_tags.py:自定义标签函数
  • template/inclusion_tags:存放生成网页标签的内容
  • home.html:使用自定义标签的页面
'''blog_inclusion_tags.py'''

# 注册自定义标签
from django import template

register = template.Library()

# .register.inclusion_tag('生成网页标签的网页')
@register.inclusion_tag('inclusion_tags/adv_label.html')
def adv_main():
    # 网页标签需要使用的数据
    adv_obj = Advertisement.objects.all().order_by('pk')
    return locals()
<!-- template/inclusion_tags/adv_label.html -->

{% load static %}

{% for adv in adv_obj %}
    <div class="adv_block" style="margin-bottom: 30px">
        <div class="pull-right" style="fill: none">
            <a class="btn_close_adv"><span class="glyphicon glyphicon-remove"></span></a>
        </div>
        <div class="thumbnail">
            <img src="/static/{{ adv.img }}/" alt="" style="height: 300px;width: 200px">
            <div class="caption">
                <h3>{{ adv.title }}</h3>
                <p>{{ adv.content|truncatechars:10 }}...</p>  <!-- 正常英文时区下使用truncate省略部分可以自动显示...,中文时区无法显示,所以手动加 -->
                <p><a href="#" class="btn btn-danger" role="button">查看详细</a>
            </div>
        </div>
    </div>
{% endfor %}

<script>
    $('.btn_close_adv').click(function () {
        $(this).closest('.adv_block').hide()
    })
</script>
<body>
    <div class="col-md-2">
    	<!-- 加载标签函数文件 -->
    {% load blog_inclusion_tags %}
        <!-- 使用标签函数 -->
    {% adv_main [参数] %}
</div> <!-- 左侧广告栏 -->
</body>

image-20240327190910035

【五】文章界面搭建

【1】新建文章

【1.1】文章编辑器(kindeditor)

<body>
    <textarea id="editor_id" name="content" style="width:700px;height:300px;"></textarea>
</body>

<script>
    KindEditor.ready(function (K) {
        // K.create('指定区域',{参数})
        window.editor = K.create('#editor_id', {
            // resizeType : 可改变模式 2时可以拖动改变宽度和高度,1时只能改变高度,0时不能拖动
            resizeType: 1,
            // themeType : 编辑器主题 【需要导入对应的css文件】
            themeType: 'simple',
            // 自动调整高度
            autoHeight: true,
            // 上传文件时,上传的url地址
            uploadJson: '{% url 'article:articleImg' %}',
            // 可以携带的额外的参数 为了通过csrf验证
            extraFileUploadParams: {
            'csrfmiddlewaretoken': '{{ csrf_token }}'
        }
      });
    });
</script>

【1.2】文章编辑器上传文件

  • 通过编辑器上传文件时需要在后端对图片文件进行处理
def articleImg(request):
    file_obj = request.FILES.get('imgFile')
    # 设置保存到的文件指定位置
    img_folder_path = os.path.join(settings.BASE_DIR, 'media', 'article_img')
    # 确保文件夹存在
    os.makedirs(img_folder_path, exist_ok=True)
    # 拼接图片存放路径
    img_path = os.path.join(img_folder_path, file_obj.name)
    try:
        # 将图片保存至指定路径 # 一般是media文件夹,保证网页可以通过url获取图片
        with open(img_path, 'wb') as fp:
            for i in file_obj.chunks():
                fp.write(i)
        # 拼接并返回图片url
        url = f'/media/article_img/{file_obj.name}/'
        # 返回的参数格式有讲究的
        # 需要是json格式  # 参数0代表上传成功,参数1代表失败
        # 成功返回url  # 失败返回message  # 参数名称必须是指定的两个
        return CommonResponse(error=0, url=url)
    except Exception as e:
        message = f'发生了一些意外{e}'
        return CommonResponse(error=1, message=message)

image-20240327195955265

【1.2.1】iframe验证错误,导致上传卡住
  • 如果函数正确返回了url,但是卡在一直上传,可能是因为django的django.middleware.clickjacking.XFrameOptionsMiddleware中间价,对iframe进行了限制

image-20240327200326511

  • iframe中可能会设置恶意代码,所以django做了一层验证
  • 我们可以先将中间件注释掉,我们手动对文章内容去除恶意代码

image-20240327200202961

【1.2.2】对文章内容进行处理
  • 如果在输入的网页内容中,提交了js动作,而js动作可能会导致你的信息泄漏等信息安全事故,也就是xss攻击
  • 为了预防xss攻击,我们需要将js动作删除或更改标签使其无法触发
  • 通过BeautifulSoup解析网页文档,找到script标签并将其删除或更改
from bs4 import BeautifulSoup

def __analysis_content(content):
    # 解析HTML
    soup = BeautifulSoup(content, 'lxml')
    desc = soup.text[:50]
    # 找到js标签防止xss攻击
    original_paragraph = soup.script
	
    # 直接删除script标签块 或 使用下面的替换
    # original_paragraph.replace_with('')
    
    if not original_paragraph:
        # 如果文章中没有写js代码就可以直接返回
        return desc, content
    # 创建一个新的段落
    new_paragraph = soup.new_tag("p")
    new_paragraph.string = str(soup.script)

    # 用新的段落替换原始段落
    original_paragraph.replace_with(new_paragraph)
    
    content = soup.prettify()

    return desc, content

【2】修改文章

【2.1】使用get+post方法重新渲染修改文章网页

''' 在gitee中的bbs1.0版本中 使用的是重新渲染修改文章的网页 '''
# 会比较麻烦,个人认为两次提交ajax效果可能会好一些

'''
通过调用指定视图函数,将指定文章id传递给后端,后端将文章数据渲染到前端
代码就是前端使用form表单
'''

【2.2】使用两次ajax提交事件,返回渲染数据

''' 在修改广告时,使用了两次ajax事件 '''
# 第一次ajax请求通过get请求将文章id传到后端,后端将数据返回,前端渲染数据
# 第二次ajax请求将修改后的数据返回
<!-- 使用了layui的form表单,所以提交语法不一致,不过思路是一致的 -->
<script>
$('.btn_edit_adv').click(function () {
                let pk = $(this).attr('value');
                $.ajax({
                    url: '{% url 'backend:editAdvertisement'%}',
                    // 通过get请求将pk数据传给后端
                    type: 'get',
                    contentType: 'application/json',
                    data: {'pk': pk},
                       // 通过回调函数将返回的指定数据渲染到指定位置
                    success: function (res) {
                        $('#oldTitle').attr('value', res.title);
                        $('#newTitle').attr('value', res.title);
                        $('#oldTitleInput').attr('value', res.title);
                        $('#adv_content').text(res.content);
                        $('#newPhone').attr('value', res.phone);
                        $('#old_adv').attr('src', '/media/' + res.img);
                        $('#img_new_adv').attr('src', '/media/' + res.img);
                    }
                })
            })
        )
        // 第二次ajax提交
        layui.use(['form'], function () {
            let form = layui.form;
            // 提交事件
            form.on('submit(btn_edit_adv)', function () {
                let formData = new FormData;
                $.each($('#editAdvForm').serializeArray(), function (index, data_dict) {
                    console.log(data_dict)
                    formData.append(data_dict.name, data_dict.value)
                })
                formData.append('new_adv_img', $('#new_img_adv')[0].files[0])
                $.ajax({
                    url: '{% url 'backend:editAdvertisement' %}',
                    type: 'post',
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function (res) {
                        if (res.code===200){
                            window.location.href='{% url 'backend:manage' 'advertisement' %}'
                        }
                    }
                })
                return false; // 阻止默认 form 跳转
            });
        });
</script>

【3】渲染上一篇文章及下一篇文章

def article_detail(request, username, pk):
    # 过滤当前用户的所有文章
    _pk_list_all = Article.objects.filter(blog__userinfo__username=username).values('pk').all()
    # 使用列表生成式 获得所有的文章id
    _pk_list = [d1.get('pk') for d1 in _pk_list_all]
    if pk in _pk_list:
        # 如果文章在id列表中执行以下代码
        # 获得当前文章的id在所有id列表中索引位置
        index = _pk_list.index(pk)
        if index == 0:
            # index =0 表示显示的是当前用户的第一篇文章
            index = 1
        elif index == len(_pk_list) - 1:
            # 如果索引 = 总长度减1 也就意味着显示的是最后一篇文章
            # 将索引-1 用来展示最后一篇文章
            index -= 1
        # 显示上一篇和下一篇文章
        last_article_obj = Article.objects.get(pk=_pk_list[index - 1])
        if len(_pk_list) == 1:
            # 如果总共只有一篇文章,那么下一篇还是第一篇
            next_article_obj = Article.objects.get(pk=_pk_list[index - 1])
        else:
            next_article_obj = Article.objects.get(pk=_pk_list[index + 1])
    article_now = Article.objects.get(pk=pk)
    # blog_obj为了保留样式
    blog_obj = Blog.objects.get(userinfo__username=username)
    # 文章总数
    article_data_all = Article.objects.filter(blog=blog_obj)
    # 点赞总数
    up_gross = article_data_all.aggregate(up_gross=Sum('up_count'))['up_gross']
    # 点踩总数
    down_gross = article_data_all.aggregate(down_gross=Sum('down_count'))['down_gross']
    # 评论总数
    comment_gross = article_data_all.aggregate(comment_gross=Sum('comment_count'))['comment_gross']
    return render(request, 'article_detail.html', locals())

image-20240327212423731

【4】文章点赞和点踩逻辑

@login_required
def edit_article_like(request):
    if request.is_ajax() and request.method == 'POST':
        user_obj = request.user
        if not user_obj.is_authenticated:
            return CommonResponse(code=400, errors='请先登录')
        # 获取ajax发送的数据
        data = request.POST
        article_id = int(data.get('article_id'))
        is_up = True if data.get('tag') == 'true' else False
        state = '点赞' if is_up else '点踩'

        article_obj = Article.objects.get(pk=article_id)  # 文章对象
        up_down_obj = UpOrDown.objects.filter(user=user_obj, article=article_obj).first()  # 点赞或点踩对象
        if article_obj.blog.userinfo.username == user_obj.username:
            return CommonResponse(code=402, errors='不能给自己点赞或点踩!')
        # 进行条件判断
        if up_down_obj:
            # 原表中有则进行是否一致判断
            if up_down_obj.is_up == is_up:
                # 一致返回不可重复提交
                return CommonResponse(code=403, errors=f'当前用户已{state},不可重复{state}')
            else:
                # 不一致进行更新
                up_down_obj.is_up = is_up
                up_down_obj.save()
                if is_up:
                    article_obj.up_count += 1
                    article_obj.down_count -= 1
                else:
                    article_obj.up_count -= 1
                    article_obj.down_count += 1
        else:
            # 如果原表中没有值,新增值
            UpOrDown.objects.create(user=user_obj, article=article_obj, is_up=is_up)
            if is_up:
                article_obj.up_count += 1
            else:
                article_obj.down_count += 1
        # 更新文章对象的修改操作
        article_obj.save()
        return CommonResponse(code=200, msg=f'{state}成功')
<body>
    <!-- 如果用户已登陆允许点赞 -->
    {% if request.user.is_authenticated %}
         <div id="div_digg" style="margin-top: 50px;">
        <div class="diggit" onclick="votePost({{ article_now.pk }},'Digg')">
            <span class="diggnum" id="digg_count">{{ article_now.up_count }}</span>
        </div>
        <div class="buryit" onclick="votePost({{ article_now.pk }},'Bury')">
            <span class="burynum" id="bury_count">{{ article_now.down_count }}</span>
        </div>
        <div class="clear"></div>
        <div class="diggword" id="diggit_tips"></div>
    </div>
    <!-- 没登陆渲染让其先登录 -->
        {% else %}
        <div>请先<a href="{% url 'article:edit_article_like' %}">登录</a>,登陆后后即可点赞或点踩</div>
    {% endif %}
</body>

<script><!-- 点赞点踩动作 -->
    function votePost(pk, tag) {
        let data = {
            'article_id': pk, 'tag': 1 ? tag === 'Digg' : 0, 'csrfmiddlewaretoken': '{{ csrf_token }}'
        };
        $.ajax({
            url: '{% url 'article:edit_article_like' %}',
            type: 'post',
            data: data,
            success: (res) => {
                if (res.code === 200) {
                    $('#diggit_tips').text(res.msg);
                } else {
                    $('#diggit_tips').text(res.errors);
                }
            // 设置1秒后刷新状态
                setTimeout(function () {
                    // 局部刷新
                    $('#div_digg').load(location.href + ' #div_digg');
                    $('#data_gross').load(location.href + ' #data_gross');
                }, 1000)
            }
        });
    }
    </script>

【5】渲染文章评论

  • 可以使用inclusiontag渲染评论的内容
  • 主要逻辑其实就是将主评论和子评论通过元组放置在一起
  • (主评论的queryset对象,该主评论对应的子评论queryset对象)
  • 在前端通过tuple.索引,分别获取到主评论和子评论
<body>
    {% if request.user.is_authenticated %}
    <!-- 渲染自定义标签 -->
        {% load article_inclusion_tags %}
        {% comment_flat blog_obj article_now %}
    <!-- 未登录让其登录 -->
        {% else %}
        <div>请先<a href="{% url 'article:comment' %}">登录</a>,登录后查看评论、发表评论</div>
    {% endif %}
</body>
  • 自定义标签.py文件
# article_inclusion_tags.py

from django import template

@register.inclusion_tag('article_inclusion_tags/comment_inclusion.html')
def comment_flat(blog_obj, article_now):
    # 获取所有主评论
    _main_comments = Comment.objects.filter(article=article_now).filter(parent=None)
    comment_data_list = []
    for father_comment in _main_comments:
        # 获取所有主评论及其对应的子评论
        child_comment = Comment.objects.filter(parent=father_comment.id)
        # 将主评论和子评论作为元组传入 (主评论对象,子评论queryset)
        comment_data_list.append((father_comment, child_comment))
    return locals()
  • 生成网页标签.html文件
<body>
    {% for comment_data in comment_data_list %}
    <div class="panel">
        <div class="panel-heading" role="tab" id="headingOne">
            <h6 class="panel-title">
                #{{ forloop.counter }}楼&nbsp;
                {{ comment_data.0.create_time|date:'Y-m-d H:i:s' }}&nbsp;
                <a href="{% url 'blog:site' comment_data.0.user.username %}">{{ comment_data.0.user.username }}</a>
                <p class="pull-right opacity50"><a class="btn_reply" href="#input_comment"
                                                   reply_user="{{ comment_data.0.user.username }}"
                                                   parent_id="{{ comment_data.0.id }}">回复</a>
                </p>
                <p class="margin_L30">
                    {{ comment_data.0.content }}
                </p>  <!-- 主评论内容 -->
                {% if comment_data.1.count %}
                <a class="collapsed pull-right opacity50" role="button" data-toggle="collapse"
                   data-parent="#accordion"
                   href="#comment_collapse_{{ comment_data.0.pk }}"
                   aria-expanded="false" aria-controls="comment_collapse_{{ comment_data.0.pk }}">
                    其他评论【{{ comment_data.1.count }}】
                </a>
                {% endif %}
            </h6>
        </div>
        <div id="comment_collapse_{{ comment_data.0.pk }}" class="panel-collapse collapse"
             role="tabpanel"
             aria-labelledby="headingOne">
            <div class="panel-body">
                <div class="media-body">
                    {% if comment_data.1.exists %}
                    {% for child_comment in comment_data.1 %}
                    <p class="margin_L30">
                        {{ child_comment.create_time|date:'Y-m-d H:i:s' }}&nbsp;
                        <a href="{% url 'blog:site'  child_comment.user.username %}">{{ child_comment.user.username }}</a>
                        {% if child_comment.user.username == comment_data.0.user.username %}
                        【楼主】
                        {% endif %}
                    </p>
                    <p class="pull-right opacity50"><a class="btn_reply" href="#input_comment"
                                                       reply_user="{{ child_comment.user.username }}"
                                                       parent_id="{{ comment_data.0.id }}">回复</a>
                    </p>
                    <p class="margin_L30">{{ child_comment.content }}</p>  <!-- 子评论内容 -->
                    <hr>
                    {% endfor %}
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
    {% endfor %}
</body>

【6】回复评论自动添加回复的用户

  • 思考逻辑就是,为回复按钮添加属性
<!-- 评论区域的form表单代码就不贴了,比较简单 -->
<script>
    // 提前声明一个主评论id,如果有主评论,那么设置值,如果没有默认就是null
    let parentId = null
    $(document).ready(
        $('#btn_comment').click(function () {
            let content = $('#input_comment').val();
            $.ajax({
                url: '{% url 'article:comment' %}',
                type: 'post',
                data: {
                    'article_id':{{ article_now.pk }},
                    'content': content,
                    // 将主评论id带入到ajax数据中
                    'parent_id': parentId,
                    'csrfmiddlewaretoken': '{{ csrf_token }}'
                },
                success: function (res) {
                    if (res.code === 200) {
                       alert(res.msg);
                    // 局部刷新评论区
                    {#$('#comment_flat').load(location.href + ' #comment_flat');#}
                    // 局部刷新不能多次提交,直接整个页面刷新
                    location.reload()
                    }
                }
            })
        }),
        $('.btn_reply').click(function () {
            let reply_user = $(this).attr('reply_user');
            let parent_id = $(this).attr('parent_Id');
            $('#input_comment').text('@' + reply_user + '\n');
            // 回复时设置主评论id
            parentId = parent_id
        })
    )
</script>

image-20240328093403253

【六】博客后台

【1】标签切换

<div>
  <!-- Nav tabs -->
  <ul class="nav nav-tabs" role="tablist">
    <li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">Home</a></li>
  </ul>

  <!-- Tab panes -->
  <div class="tab-content">
    <div role="tabpanel" class="tab-pane active" id="home">...</div>
  </div>
</div>
  • 后端将数据传入,前端根据数据渲染即可,具体代码可以在gitee看,当前展示一下效果

博客后台

【2】回收站

  • 实现逻辑就是在数据库中增加了一个是否删除的字段

def recycleBin(request):
    '''回收站'''
    blog_obj = Blog.objects.get(userinfo=request.user)
    # 过滤出字段is_delete为true的文章
    article_all = Article.objects.filter(blog__userinfo=request.user).filter(is_delete=True)
    return render(request, 'recycle_bin.html', locals())


def revoke(request, pk):
    '''撤销删除'''
    Article.objects.filter(pk=pk).update(is_delete=False)
    return redirect('backend:recycle')

博客后台-回收站

【七】排错技巧

【1】forbidden

  • 排查是否有{% csrf_token% }

  • 排查前端是否成功将数据传回后端

    • 一个是查看获取表单数据时,是否正确的获取到了csrf的值
    • 另一个是查看数据类型是什么?是否可以被django读取
  • 将csrf中间件注释掉,看看后端有没有收到数据

  • 使用layui的form组件传值的时候,传回的值好像是obj对象,而不是字典,所以会forbidden,因为django没办法搜索到

image-20240328114523488

【2】参数传递失败

  • 在前端时,尽量不要直接使用变量,尽量使用一个公用的对象去反向查询获取值
  • 因为在模板继承时,变量极有可能传值失败,而且变量的值有些时候你不清楚获取的是哪一个
  • 例如:在站点中,需要username参数,这个username参数,在site视图函数中可以通过url中的路径获得,而其他视图函数不一定能通过url获得,所以需要考虑其他方式获得
    • 基本上每一个试图函数都需要使用的参数是 blog_obj,因为很多功能依赖于站点
    • 这样的话,如果需要username,我就可以通过blog_obj.userinfo.username获得
    • 如果依赖于登录用的功能,就可以直接通过request.user.username获得
posted @ 2024-03-28 12:12  Lea4ning  阅读(9)  评论(0编辑  收藏  举报