BBS项目创作流程

news/发布时间2024/5/22 9:24:31

BBS项目创作流程

【零】完整文件

  • gitee仓库
    • BBS/BBS1.0/BlogBasedSystem · Lea4ning/DjangoObject - 码云 - 开源中国 (gitee.com)

【一】项目基本配置

【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 JsonResponsedef 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 = 1if current_page < 1:current_page = 1self.all_count = all_countself.per_page_num = per_page_num# 总页码all_pager, tmp = divmod(all_count, per_page_num)if tmp:all_pager += 1self.all_pager = all_pagerif current_page > all_pager:current_page = all_pagerself.current_page = current_pageself.pager_count = pager_countself.pager_count_half = int((pager_count - 1) / 2)@propertydef start(self):return (self.current_page - 1) * self.per_page_num@propertydef end(self):return self.current_page * self.per_page_numdef page_html(self):# 如果总页码 < 11个:if self.all_pager <= self.pager_count:pager_start = 1pager_end = self.all_pager + 1# 总页码  > 11else:# 当前页如果<=页面上最多显示11/2个页码if self.current_page <= self.pager_count_half:pager_start = 1pager_end = self.pager_count + 1# 当前页大于5else:# 页码翻到最后if (self.current_page + self.pager_count_half) > self.all_pager:pager_end = self.all_pager + 1pager_start = self.all_pager - self.pager_count + 1else:pager_start = self.current_page - self.pager_count_halfpager_end = self.current_page + self.pager_count_half + 1page_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 BytesIOdef 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.POSTusername = 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.pydef 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 }}" disabledclass="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=' + txtwindow.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 templateregister = 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)

  • 文档 - KindEditor - 在线HTML编辑器
<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)# 拼接并返回图片urlurl = 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 BeautifulSoupdef __analysis_content(content):# 解析HTMLsoup = 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 = 1elif 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.userif not user_obj.is_authenticated:return CommonResponse(code=400, errors='请先登录')# 获取ajax发送的数据data = request.POSTarticle_id = int(data.get('article_id'))is_up = True if data.get('tag') == 'true' else Falsestate = '点赞' 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_upup_down_obj.save()if is_up:article_obj.up_count += 1article_obj.down_count -= 1else:article_obj.up_count -= 1article_obj.down_count += 1else:# 如果原表中没有值,新增值UpOrDown.objects.create(user=user_obj, article=article_obj, is_up=is_up)if is_up:article_obj.up_count += 1else: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.pyfrom 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,如果有主评论,那么设置值,如果没有默认就是nulllet 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');// 回复时设置主评论idparentId = parent_id}))
</script>

image-20240328093403253

【六】博客后台

【1】标签切换

  • 使用的是bootstrap中的Togglable tabs
  • JavaScript 插件 · Bootstrap v3 中文文档 | Bootstrap 中文网 (bootcss.com)
<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获得

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/20878822.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

Alfred使用AppleScript来实现一键隐藏功能(老板键)

set appNames to {"WeChat","QQ"} -- 将要隐藏的进程名称放入数组中tell application "System Events"repeat with appName in appNamesset appProcess to first process whose name is appNameset appId to id of appProcesstell process appNa…

[转帖]Nginx+Keepalived实现简单的服务高可用

https://www.cnblogs.com/xiexun/p/14604650.html 一般情况下,如果我们做小型项目,前端用一个nginx做反向代理即可,大概是这样的 image.png 但是,作为互联网项目,纯2C的话必然需要做高可用,不仅后端的Server有N个,Nginx同样需要有N个,一主N备,当有一个服务器挂掉的时…

红米Redmi Note 8 拆机进深度刷机模式短接图,刷机、解锁进高通9008模式

首先将手机关机,打开电池盖,用镊子短接下图中的两个触点然后通过数据线连接上电脑,计算机-管理-设备管理器中可以看到手机进入深度刷机模式的端口(高通9008)松开镊子。最后打开刷机工具,选好刷机包即可刷机,短接点位置如图所示

五种方案图文并茂教你使用DBeaver,SQL文件导入数据库,插入数据,备份恢复mysql,postgres数据

备份导出数据 方案一:支持可以整个库导出、部分表导出、多个库导出(可选格式较少) 使用连接数据库 鼠标右键选择需要导出备份的数据库-工具-备份 此步骤对于不同类型数据库来说,有的可以一次选择多个表,有的可以一次选择多个库,下面是两个截图案例勾选需要导出的表-点击下…

想分组聚合各省的条数、总额,及其平均数或者占比的话,Python方便还是slq方便?

大家好,我是Python进阶者。 一、前言 前几天在Python最强王者交流群【斌】问了一个数据处理的问题。问题如下: 求教大佬:我有全国的明细5000条,其中一个字段是省(直辖市), 如果我想分组聚合各省的条数、总额,及其平均数或者占比的话,Python方便还是sql方便? 二、实现…

linux安装/切换不同版本c/c++

查看ubuntu系统上g++的版本:ls /usr/bin/g++*安装指定版本gcc和g++# 以version == 4.9为例 sudo apt-get install gcc-4.9 g++-4.9切换不同版本 当ubuntu系统上安装了不同版本的gcc和g++,可以使用update-alternatives命令设置默认使用哪个版本,典型的如在Ubuntu 16.04里安装…