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】基础搭建
| |
| |
| |
| INSTALLED_APPS =['...'] |
| |
| DATABASES = { |
| 'default': { |
| 'ENGINE': 'django.db.backends.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】对数据表进行分析
字段名 |
类型 |
注释 |
phone |
BigIntegerField |
电话 |
avatar |
FileField |
头像链接 |
create_time |
DateField |
创建时间 |
blog |
OneToOneField(to="Blog") |
外键字段,一对一,个人站点表 |
字段名 |
类型 |
注释 |
site_name |
CharField |
站点名称 |
site_title |
CharField |
站点标题 |
site_theme |
CharField |
站点样式 |
字段名 |
类型 |
注释 |
name |
CharField |
分类名 |
blog |
ForeignKey(to="Blog") |
外键字段,一对多,个人站点 |
字段名 |
类型 |
注释 |
name |
CharField |
标签名 |
字段名 |
类型 |
注释 |
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") |
外键字段,多对多,文章标签 |
字段名 |
类型 |
注释 |
user |
ForeignKey(to="UserInfo") |
用户主键值 |
article |
ForeignKey(to="Article") |
文章主键值 |
is_up |
BooleanField() |
是否点赞 |
字段名 |
类型 |
注释 |
user |
ForeignKey(to='UserInfo') |
用户主键值 |
article |
ForeignKey(to="Article") |
文章主键值 |
content |
CharField() |
评论内容 |
comment_time |
DateTimeField |
评论时间 |
parent |
ForeignKey(to="self",null=True) |
自关联 |

【2】建表注意事项
【2.1】基于AbstractUser
表创建UserInfo
表
注意,基于系统表创建用户表需要在执行数据迁移前,django默认用户表表名为auth_user
- 用户信息表基于【
from django.contrib.auth.models import AbstractUser
】创建
- 注意要在
settings.py
文件中配置AUTH_USER_MODEL='指定用户表'
- 这样创建的用户表可以被django管理后台识别,可以通过该表中的数据登录django后台
- 因为很多表可能需要用到创建时间和更新时间的字段,可以创建一个公共表来快捷的注册这两个字段
- 当模型表继承公共表时,如果在创建公共表时,公共表继承了
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 |
| class Table(models.Model): |
| ... |
| class Meta: |
| db_table = '指定在数据库中的表名' |
| verbose_name_plural = '为该表起别名' |
| |
| def __str__(self): |
| return '可以返回一些实例化模型表对象时的信息' |
| |
【2.4】勤使用三引号作注释

【3】将数据库表注册至管理员后台
| |
| |
| from django.contrib import admin |
| |
| |
| from .models import 表 |
| |
| @admin.register(表) |
| class 表名(admin.ModelAdmin): |
| list_display = ['注册想要显示在后台的字段'] |
- 注意事项
- 在后台中,多对多字段是没办法显示的,所以多对多的字段注册不进去
- 注册在后台的字段,显示的是
verbose_name
属性的值,所以建表时,可以指定值

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


【前置】公共功能
【1】CommonResponse : 返回json格式的响应
| 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): |
| |
| if self.all_pager <= self.pager_count: |
| pager_start = 1 |
| pager_end = self.all_pager + 1 |
| |
| else: |
| |
| if self.current_page <= self.pager_count_half: |
| pager_start = 1 |
| pager_end = self.pager_count + 1 |
| |
| |
| 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 = [] |
| |
| 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(): |
| |
| 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() |
| |
| 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)) |
| |
| img_draw = ImageDraw.Draw(img_obj) |
| |
| 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) |
| |
| |
| img_draw.text((i * 25 + 10, 0), temp, fill=rgb_number(), font=img_font) |
| |
| code += temp |
| |
| |
| |
| |
| io_obj = BytesIO() |
| |
| img_obj.save(io_obj, "png") |
| |
| img_data = io_obj.getvalue() |
| |
| return code, img_data |
【三】用户功能
【1】用户注册
【1.1】前端
- 需要渲染错误信息时,你的错误信息需要与
{{ form }}
在同一个块级标签中,如果分成两个,可能在渲染时无法找到







-
【注】使用箭头函数时this
就失效了,就无法定位到当前对象了
【1.2】后端
- 后端基本上没有到很巧妙的技术,都是基础技术,直接放上代码
- 【注】由于用户信息与个人站点绑定,所以在录入信息时,将站点信息一起录入
| def register(request): |
| register_form = RegisterForm() |
| |
| 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】用户登录
| '''使用了图片验证码,没有使用forms组件,使用的是layui表单''' |
| def login(request): |
| '''没用forms组件''' |
| |
| 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】用户注销
| @login_required |
| def logout(request): |
| auth.logout(request) |
| url = reverse('home') |
| return redirect(to=url) |
【4】修改密码 [ 使用模态框跳转 ]
| |
| |
| def editPassword(request): |
| if request.is_ajax() and request.method == 'POST': |
| data = request.POST |
| |
| |
| 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-toggle
和data-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">×</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> |
【四】站点搭建
【1】主页导航栏搭建
【1.1】根据登录状态显示不同页面
| |
| {% 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> |
| <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_search").submit(function (event) { |
| event.preventDefault() |
| func_search() |
| }) |
| } |
| ) |
| </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('inclusion_tags/adv_label.html') |
| def adv_main(): |
| |
| adv_obj = Advertisement.objects.all().order_by('pk') |
| return locals() |
| |
| |
| {% 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> |
| <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> |

【五】文章界面搭建
【1】新建文章
【1.1】文章编辑器(kindeditor)
| <body> |
| <textarea id="editor_id" name="content" style="width:700px;height:300px;"></textarea> |
| </body> |
| |
| <script> |
| KindEditor.ready(function (K) { |
| |
| window.editor = K.create('#editor_id', { |
| |
| resizeType: 1, |
| |
| themeType: 'simple', |
| |
| autoHeight: true, |
| |
| uploadJson: '{% url 'article:articleImg' %}', |
| |
| 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: |
| |
| with open(img_path, 'wb') as fp: |
| for i in file_obj.chunks(): |
| fp.write(i) |
| |
| url = f'/media/article_img/{file_obj.name}/' |
| |
| |
| |
| return CommonResponse(error=0, url=url) |
| except Exception as e: |
| message = f'发生了一些意外{e}' |
| return CommonResponse(error=1, message=message) |

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

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

【1.2.2】对文章内容进行处理
- 如果在输入的网页内容中,提交了
js动作
,而js动作可能会导致你的信息泄漏等信息安全事故,也就是xss攻击
- 为了预防xss攻击,我们需要将js动作删除或更改标签使其无法触发
- 通过
BeautifulSoup
解析网页文档,找到script
标签并将其删除或更改
| from bs4 import BeautifulSoup |
| |
| def __analysis_content(content): |
| |
| soup = BeautifulSoup(content, 'lxml') |
| desc = soup.text[:50] |
| |
| original_paragraph = soup.script |
| |
| |
| |
| |
| if not original_paragraph: |
| |
| 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版本中 使用的是重新渲染修改文章的网页 ''' |
| |
| |
| ''' |
| 通过调用指定视图函数,将指定文章id传递给后端,后端将文章数据渲染到前端 |
| 代码就是前端使用form表单 |
| ''' |
【2.2】使用两次ajax提交事件,返回渲染数据
| ''' 在修改广告时,使用了两次ajax事件 ''' |
| |
| |
| |
| <script> |
| $('.btn_edit_adv').click(function () { |
| let pk = $(this).attr('value'); |
| $.ajax({ |
| url: '{% url 'backend:editAdvertisement'%}', |
| |
| 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); |
| } |
| }) |
| }) |
| ) |
| |
| 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; |
| }); |
| }); |
| </script> |
【3】渲染上一篇文章及下一篇文章
| def article_detail(request, username, pk): |
| |
| _pk_list_all = Article.objects.filter(blog__userinfo__username=username).values('pk').all() |
| |
| _pk_list = [d1.get('pk') for d1 in _pk_list_all] |
| if pk in _pk_list: |
| |
| |
| index = _pk_list.index(pk) |
| if index == 0: |
| |
| index = 1 |
| elif index == len(_pk_list) - 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.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()) |

【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='请先登录') |
| |
| 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); |
| } |
| |
| 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> |
| |
| |
| 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) |
| |
| comment_data_list.append((father_comment, child_comment)) |
| return locals() |
| <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 }}楼 |
| {{ comment_data.0.create_time|date:'Y-m-d H:i:s' }} |
| <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' }} |
| <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】回复评论自动添加回复的用户
| |
| <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> |

【六】博客后台
【1】标签切换
| <div> |
| |
| <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> |
| |
| |
| <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) |
| |
| 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

【2】参数传递失败
- 在前端时,尽量不要直接使用变量,尽量使用一个公用的对象去反向查询获取值
- 因为在模板继承时,变量极有可能传值失败,而且变量的值有些时候你不清楚获取的是哪一个
- 例如:在站点中,需要username参数,这个username参数,在site视图函数中可以通过url中的路径获得,而其他视图函数不一定能通过url获得,所以需要考虑其他方式获得
- 基本上每一个试图函数都需要使用的参数是
blog_obj
,因为很多功能依赖于站点
- 这样的话,如果需要username,我就可以通过
blog_obj.userinfo.username
获得
- 如果依赖于登录用的功能,就可以直接通过
request.user.username
获得
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了