BBS项目
BBS
项目开发基本流程
- 需求分析
- 架构设计
- 分组开发
- 提交测试
- 交付上线
项目分析(表)
bbs项目主要是仿照博客园的页面来设计的。核心就是文章的增删改查
-
表分析
首先我们得先确定几个表(表的数量),其次确定表得基础字段,最后确定表的外键字段
1.用户表 2.个人站点表 3.文章表 4.文章分类表 5.文章标签表 6.点赞点踩表 7.文章评论表
-
基本字段分析
用户表
我们基于auth_user表并扩展额外的字段
auth_user 用户名、密码、邮箱、是否是超级管理员 基于auth_user表并扩展额外的字段 电话表、头像、注册时间
个人站点表
1.站点名称(kimi/kiki) 2.站点标题(我博客的The Road of Learning kimi) 3.站点样式(CSS样式)
文章表
文章标题、文章简介、文章内容、发布时间
文章分类表
分类名称
文章标签表
标签名称
点赞点踩表
此表是记录哪个用户给哪篇文章点了推荐(赞)反对(踩) 用户字段(用户主键):外键字段 文章字段(文章主键):外键字段 点赞点踩
文章评论表
此表是记录哪个用户给哪篇文章评论了什么内容 用户字段(用户主键):外键字段 文章字段(文章主键):外键字段 评论内容 评论时间 外键字段(自关联) """ id user_id article_id content parent_id 1 1 1 哈哈哈 null 2 2 1 你笑傻 1 3 3 1 他疯了 2 """
-
外键字段
用户表 用户与个人站点时一对一外键关系 个人站点表 文章表 文章表与个人站点表时一对多外键关系 文章表与文章分类表时一对多外键关系 文章与文章标签表时多对多外键关系 文章评论数 文章点赞数 文章点踩数 """ 数据库字段优化设计:我们想统计文章的评论数、点赞数 通过文章数据跨表查询到文章评论表中对应的数据统计,但是文章需要频繁的展示,每次都跨表查询的话效率极低,基于上述的表述,我们优化处理,只需要在文章表中再创建三个普通字段,之后只需要确保每次操作评论表或者点赞点踩表时同时修改上述的三个普通字段即可""" 文章分类表 文章分类与个人站点时一对多外键关系 文章标签表 文章标签与个人站点是一对多外键关系
表关系图如下
项目注册功能
用户注册:利用form组件、modelform组件、ajax组件等编写
1.渲染前端标签
2.校验用户数据
3.展示错误提示
注意:form组件单独开设py文件编写 解耦合!!!
项目登录功能
img标签的src属性
1.直接填写图片地址
2.填写路由,会自动朝该路由发送get请求
如果结果是图片的二进制数据,那么会自动渲染图片
终端下载pillow模块
pip install pillow -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
项目实操
项目名称:BBS
项目数据库:bbs
1.现在settings配置环境,创建数据库bbs,,编写模型表(继承auth_user表,要先配置声明AUTH_USER_MODEL='app01.UserInfo
),头像设置默认值(编写正则暴露接口给前端),迁移数据库,连接数据库
2.搭建简单的首页
3.注册功能 --编写form组件校验、注册头像的获取,提交ajax数据(区分普通数据和文件数据),后端逻辑校验,前端错误信息
渲染展示
4.登录功能 --验证码的获取
首页搭建
1.开路由、建函数、返回一个首页搭建页,利用bootstrap查找合适的导航条、列表简单展示。
2.本次使用后台管理的auth_user表,并创建新的注册表
1.在模型层引入模块后要继承auth_user做扩展,引入AbstractUser
2.要在settings配置一下AUTH_USER_MODEL = 'app01.UserInfo
3.创建模型表,本次多对多关系使用的是半自动创建方法
models.py
from django.db import models
from django.contrib.auth.models import AbstractUser # 要继承auth_user做扩展,引入AbstractUser
""" 要在settings配置一下AUTH_USER_MODEL = 'app01.UserInfo'
"""
class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True)
avatar = models.FileField(upload_to='avatar/', default='avatar/default.jpg', verbose_name='用户头像')
register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True)
site = models.OneToOneField(to='Site', on_delete=models.CASCADE, null=True)
class Site(models.Model):
"""个人站点表"""
site_name = models.CharField(verbose_name='站点名称', max_length=32)
site_title = models.CharField(verbose_name='站点标题', max_length=32)
site_theme = models.CharField(verbose_name='站点样式', max_length=32, null=True) # 简单模拟样式文件
class Article(models.Model):
"""文章表"""
title = models.CharField(verbose_name='文章标题', max_length=32)
desc = models.CharField(verbose_name='文章简介', max_length=255)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创建时间', auto_now_add=True)
# 三个优化字段
comment_num = models.IntegerField(verbose_name='评论数', default=0)
up_num = models.IntegerField(verbose_name='点赞数', default=0)
down_num = models.IntegerField(verbose_name='点踩数', default=0)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
category = models.ForeignKey(to='Category', on_delete=models.CASCADE, null=True)
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag'),
null=True
)
class Category(models.Model):
"""文章分类表"""
name = models.CharField(verbose_name='分类名称', max_length=32)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
class Tag(models.Model):
"""文章标签表"""
name = models.CharField(verbose_name='标签名称', max_length=32)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE, null=True)
class Article2Tag(models.Model):
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
tag = models.ForeignKey(to='Tag', on_delete=models.CASCADE, null=True)
class UpAndDown(models.Model):
"""文章点赞点踩表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
is_up = models.BooleanField(verbose_name='点赞点踩') # 传布尔值存 0或者1
class Comment(models.Model):
"""文章评论表"""
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
content = models.TextField(verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True)
注册功能
校验数据利用的是form组件校验
myform.py
from django import forms
from app01 import models
class RegisterForm(forms.Form):
"""用户注册form表"""
username = forms.CharField(max_length=8, min_length=3, label='用户名',
error_messages={
'min_length': '用户名最短三位',
'max_length': '用户名最长八位',
'required': '用户名不能为空'
},
widget=forms.widgets.TextInput(attrs={'class': 'form-control'}))
password = forms.CharField(min_length=3, max_length=8, label='密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空'
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(min_length=3, max_length=8, label='密码',
error_messages={
'min_length': '密码最短三位',
'max_length': '密码最长八位',
'required': '密码不能为空'
},
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱',
error_messages={
'required': '邮箱不能为空',
'invalid': '邮箱不正确'
},
widget=forms.widgets.EmailInput(attrs={'class': 'form-control'}))
# 用户头像单独校验 不使用校验类 其他字段自己根据需求自定义添加
# 钩子函数
# 局部钩子校验用户名是否已存在
def clean_username(self):
username= self.cleaned_data.get('username')
res = models.UserInfo.objects.filter(username=username)
if res:
self.add_error('username','用户名已存在')
return username
# 全局钩子校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password== confirm_password:
self.add_error('confirm_password','两次密码不一致')
return self.cleaned_data
views.py
from app01 import myforms # 引入myforms
from app01 import models # 引入models
from django.http import JsonResponse # 引入JsonResponse做数据序列化
# 注册功能
def register_func(request):
# 前后端ajax交互,通常采用字段作为交互对象
back_dict = {'code': 10000, 'msg': ''}
# 1.先产生一个空的form_obj
form_obj = myform.RegisterForm()
if request.method == 'POST':
form_obj = myform.RegisterForm(
request.POST) # username password confirm_password email csrfmiddlewaretoken四个键值对
if form_obj.is_valid(): # 判断是否符合
clean_data = form_obj.cleaned_data # 存储符合校验的数据 # username password confirm_password email
# 将confirm_password键值对移除
clean_data.pop('confirm_password') # {# username password email}
# 获取用户上传的对象文件
avatar_obj = request.FILES.get('avatar') # 用户有可能没有上次,下面的判断必须要的
if avatar_obj:
clean_data['avatar'] = avatar_obj # {username password email avatar }
# 创建用户数据
models.UserInfo.objects.create_user(**clean_data) # 上述处理字典的目的就是为了创建数据省事
back_dict['msg'] = '注册成功'
back_dict['url'] = '/login/'
""" 正常业务一般是先写正确的逻辑,后面慢慢添加错误的逻辑"""
else:
back_dict['code'] = 10001
back_dict['msg'] = form_obj.errors
return JsonResponse(back_dict)
return render(request, 'registerPage.html', locals())
register.html
本次虽用了form表单,是为了后面前端的数据反序化,前端提交数据是使用了ajax组件提交数据,本次还给input标签绑定获取焦点事件,移除错误样式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户注册</h2>
<form id = 'form'> <!--不使用form表单提交数据,但是用一下form表单,他有一个序列化功能-->
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group"> <!-- 目的是让多个获取用户数据的标签上下间距更大一点-->
<label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id自动获取渲染的标签id值-->
{{ form }}
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<!--用户头像自己编写相关标签获取-->
<div class="form-group">
<label for="myfile">头像
<img src="/static/img.jpg" alt="" width="120" id="myimg"> <!-- 把img放在lable里面,因为label和input框已经绑定了-->
</label>
<input type="file" id ='myfile' style="display: none" >
</div>
<input type="button" id="subBtn" class="btn btn-primary btn-block" value="注册">
</form>
</div>
</div>
<script>
// 1.用户头像的实时展示 >>>这是前端的固定代码
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象读取
myFileReaderObj.readAsDataURL(fileObj)
//3.等待文件阅读器对象加载完毕之后再修改src
myFileReaderObj.onload = function (){
// 4.修改img标签的src属性展示图片
$('#myimg').attr('src',myFileReaderObj.result)
}
})
// 2.给注册按钮绑定点击事件 发送ajax 携带了文件数据
$('#subBtn').click(function (){
// 1.先产生一个内置对象
let myFormDataObj = new FormData();
// 2.添加普通数据(单个单个的编写效率极低)
{#console.log($('#form').serializeArray()) // 可以一次性获取form标签内所有的普通字段数据[{},{},{}]#}
$.each($('#form').serializeArray(),function (index,dataObj){ //对结果for循环,然后交给后面的函处理
myFormDataObj.append(dataObj.name,dataObj.value) // {'name':'','value':''}
})
// 3.添加文件数据
myFormDataObj.append('avatar',$('#myfile')[0].files[0]) //query对象转为标签对象
//4.发送ajax请求
$.ajax({
url:'',
type:'post',
data:myFormDataObj,
contentType:false,
processData:false,
success:function (args){
// 如果code=10000,直接跳转到登录页面,back_dict['url'] = '/login/'
if(args.code===10000){
window.location.href = args.url
} else{
{#console.log(args.msg)#}
let dataObj =args.msg;
//如何针对性的渲染错误提示 {'username'} id_username
$.each(dataObj,function(k,msgArray){
//拼接标签的id值
let eleId='#id_'+ k
//根据id查找标签 修改下面的span标签的内容 并给父标签添加错误样式
$(eleId).next().text(msgArray[0]).parent().addClass('has-error')
})
}
}
})
})
//3.给所有的input标签绑定获取焦点事件,移除错误样式
$('input').focus(function (){
$(this).next().text('').parent().removeClass('has-error')})
</script>
</body>
</html>
注册用户头像获取
1.src修改头像动态获取,需要开设一个avatar路由存放头像。
我们针对头像设置了一个avatar路径,如果我们需要将用户上次的图片固定存在一个地方,我们需要在settings配置添加media的文件路径
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
2.添加用户自动创建一个media文件夹,media里面有avatar文件,avatar里面是一个注册添加的头像。
3.头像还没展示,我们需要引入一个模块和media的配置路径,添加路由
注意:media是支持暴露给用户的方法,但是不能乱用
from django.views.static import serve
from django.conf import settings
路由
# 自定义暴露接口
re_path('media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}),
在头像设置路径添加/media/并设置大小
<a href="#">
<img class="media-object" src="/media/{{ page_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
登录功能
验证码
-
校验组件一般只在创建的时候用到的
-
编写随机字符串,需要字体样式的化,直接百度下载相应的ttf文件;放置在static文件下面font文件夹,
- text() 参数解释,验证码有划线,是为了防爬虫
问题:我们点击随机字符串就会随机变化,如何设计?
给图片参加点击事件,src=路由('/get_code/??'),只要路由不一样就会自动发送一个get请求
绑定点击事件
<img src="/get_code/" alt="" width="350" height="35" id="imgSrc">
# 点击事件
<script>
$('#imgSrc').click(function (){
let oldSrc=$(this).attr('src'); // 拿到原来的src的路由
$(this).attr('src',oldSrc+ '?') //让原来的src加问号
})
</script>
1.使用ajax发送数据,前端书写ajax携带数据,后端写代码逻辑
// 2.登录按钮发送ajax请求
$('#loginBtn').click(function (){
//可以再次使用form标签序列化功能,也可以自己挨个获取
// console.log($('myform').serializeArray()) //拿到的[{},{},{}] ,但是ajax是大字典包着小字典
$.ajax({
url:'',
type:'post',
date: {'username':$('#name').val(),'password':$('#password').val(),'code':$('#code').val(),'csrfmiddlewaretoken':'{{ csrf_token }}',
success:function (args){
if(args.code===10000){
window.location.href =args.url
}else{
//可以使用sweetalert插件美好展示
alert(args.msg)
swal(args.msg,'error')
}
}
}
})
})
- 弹框提示
引入js,可以使用sweetalert插件美好展示
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
错误信息提示
//可以使用sweetalert插件美好展示
swal(args.msg,'error')
3.后端逻辑完成记得保存用户登录的状态,之后就可以通过request.user获取登录用户对象
auth.login(request, user_obj)
views.py
# 校验组件一般只在创建的时候用到的
def login_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 拿前端的数据和后端做比较
if code.upper() == request.session.get('code').upper():
# 用户名和密码做对比
user_obj = auth.authenticate(request, username=username, password=password)
if user_obj:
# 用户存在就保存用户登录状态
auth.login(request, user_obj) # 执行之后就可以使用request.user获取登录用户对象
back_dict['msg'] = '登录成功'
back_dict['url'] = '/home/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '用户名或密码错误'
return JsonResponse(back_dict)
return render(request, 'loginPage.html')
from PIL import Image,ImageFont,ImageDraw
"""
Image 产生图片
ImageFont 字体样式
ImageDraw 画笔对象
"""
from io import BytesIO,StringIO
"""
BytesIO 在内存中临时存储 读取的时候以bytes格式为准
StringIO 在内存中临时存储 读取的时候以字符串格式为准
"""
import random
def get_random():
return random.randint(0.255),random.randint(0.255),random.randint(0.255)
# 验证码后端逻辑判断
def get_code_func(request):
# 1.推导步骤1:直接读取图片文件返回
# with open(r'D:\PycharmProjects\BBS\avatar\头像2.png','rb')as f:
# data=f.read()
# return HttpResponse(data)
# 2.推导步骤2:随机产生图片动态返回 第三方pillow模块
# img_obj = Image.new('RGB',(350.35),'green')
# with open(r'xxx.png', 'wb') as f:
# img_obj.save(f, 'png')
# with open(r'xxx.png', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 3.推导步骤3:针对图片的保存与读取做优化 内存管理器
# img_obj = Image.new('RGB', (350, 35), 'yellow')
# io_obj = BytesIo()
# img_obj.save(io_obj,'png')
# return HttpResponse(io_obj.getvalue())
# 4.推导步骤4:图片颜色是可以随机变换的
img_obj = Image.new('RGB', (350, 35), get_random())
io_obj = BytesIO()
img_obj.save(io_obj,'png')
return HttpResponse(io_obj.getvalue())
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
</head>
<body>
<div class="container">
<div class="col-md-8 col-md-offset-2">
<h2 class="text-center">用户登录</h2>
{% csrf_token %}
<div class="form-group">
<label for="name">用户名</label>
<input type="text" id="name" class="form-control" name="username">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="text" id="password" class="form-control" name="password">
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="code" class="form-control" name="code">
</div>
<div class="col-md-6">
<img src="/get_code/" alt="" width="350" height="35" id="imgSrc" >
</div>
</div>
<input type="button" class="btn btn-success btn-block" id="loginBtn" value="登录">
</div>
</div>
</div>
<script>
// 1.验证码动态分析
$('#imgSrc').click(function (){
let oldSrc=$(this).attr('src'); // 拿到原来的src的路由
$(this).attr('src',oldSrc+ '?') //让原来的src加问号
})
// 2.登录按钮发送ajax请求
$('#loginBtn').click(function (){
//可以再次使用form标签序列化功能,也可以自己挨个获取
// console.log($('myform').serializeArray()) //拿到的[{},{},{}] ,但是ajax是大字典包着小字典
$.ajax({
url:'',
type:'post',
data: {'username':$('#name').val(),
'password':$('#password').val(),
'code':$('#code').val(),
'csrfmiddlewaretoken':'{{ csrf_token }}'},
success:function (args){
if(args.code===10000){
window.location.href =args.url //直接调整到url页面
}else{
//可以使用sweetalert插件美好展示
{#alert(args.msg) //展示错误信息#}
swal(args.msg,'error')
}
}
})
})
</script>
</body>
</html>
验证码函数
from PIL import Image, ImageFont, ImageDraw
from io import BytesIO, StringIO
def get_random():
return random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)
def get_code_func(request):
# 先产生一个图片对象
img_obj = Image.new('RGB', (350, 35), get_random())
# 将图片对象交给画笔对象
draw_obj = ImageDraw.Draw(img_obj)
font_obj = ImageFont.truetype('static/font/111.ttf', 35)
# 产生随机验证码
code = ''
for i in range(5):
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(0, 9))
temp_choice = random.choice([random_int, random_upper, random_lower])
# 将随机验证码写到图片
draw_obj.text((i * 60 + 45, 0), temp_choice, font=font_obj)
code += temp_choice
# 后端保存验证码,便于后续的比对
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
首页校验登录
简单代码操作
request.user.check_password
request.user.set_password
request.user.save
1.用户登录之后记录状态
# 用户存在就保存用户登录状态
auth.login(request, user_obj) # 执行之后就可以使用request.user获取登录用户对象
2.主页判断
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">注销登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'register_views' %}">注册</a></li>
<li><a href="{% url 'login_views' %}">登录</a></li>
{% endif %}
</ul>
修改密码
1.我们本次修改密码和退出登录,本次使用简易的模态框(动态实例,静态不行)来实现,因为用页面跳转很麻烦(涉及到子母页面的继承)
<button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myModal">
Launch demo modal
</button>
动态框是根据这个绑定,我们只需要把后面的绑定给修改密码的a标签就行
- 给提交按钮绑定事件,因为是home页面,我们需要开一个路由,修改密码的函数,需要登录之后才能修改,引入一个校验装饰器的模块
from django.contrib.auth.decorators import login_required
settings设置配置好
LOGIN_URL = '/login/'
- 给修改按钮绑定一个ajax点击事件
后端逻辑
# 登录之后才能修改,引入校验装饰器
@login_required
def set_pwd_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
old_pwd = request.POST.get('old_pwd')
new_pwd = request.POST.get('new_pwd')
confirm_pwd = request.POST.get('confirm_pwd')
# 先校验原密码是否正确
if request.user.check_password(old_pwd):
# 再校验两次密码是否一致,并且不能为空
if new_pwd == confirm_pwd and new_pwd:
request.user.set_password(new_pwd)
request.user.save()
back_dict['msg'] = '密码修改成功'
back_dict['url'] = '/login/'
else:
back_dict['code'] = 10001
back_dict['msg'] = '两次密码不一致'
else:
back_dict['code'] = 10002
back_dict['msg'] = '原密码错误'
return JsonResponse(back_dict)
点击事件
<script>
$('#setBtn').click(function (){
$.ajax({
url:'/set_pwd/',
type:'post',
data:{
'old_pwd':$('#old_pwd').val(),
'new_pwd':$('#new_pwd').val(),
'confirm_pwd':$('#confirm_pwd').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success:function (args){
if(args.code===10000){
window.location.href=args.url
}else {
$('#error').text(args.msg)
}
}
})
})
修改头像
1.首次确认用户登录之后,显示用户名和头像
<li style="width:60px;height: 60px ;border-radius:50% ; overflow:hidden;display: block" ><img src="/media/{{ request.user.avatar }}/" alt="" style="max-width: 100%" ></li>
2.修改用户头像
首先,确定使用一个模态框修改头像,照着样子修改(data-toggle="modal" data-target="#myModal")添加到修改头像的标签里。编写模态框的内容(当前登录对象、原原头像、新头像、提交按钮)
<!--模态框开始-->
<div class="modal fade" id="myModal" 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 text-center " id="myModalLabel" >修改头像</h4>
</div>
<!--form提交数据# 图片要用enctype-->
<div class="modal-body">
<form action="/set_avatar/" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control"> //获取当前用户名request.user.username
</div>
<div class="form-group">
<label for="">原头像</label>
<img src="media/{{ request.user.avatar }}/" alt="" width="100px"> //获取原头像我们有自己的路由
</div>
<div class="form-group">
<label for="myfile">新头像
<img src="/static/img.jpg" alt="" width="100px" id="myimg"> //先设一个默认头像
</label>
<input type="file" name="new_avatar" id="myfile" style="display: none"> //写在标签下面,点击图片直接获取文件 注意id值
</div>
<input type="submit" class="btn btn-warning btn-block"> //提交按钮
</form>
</div>
</div>
</div>
</div>
<!--模态框结束-->
其次,给获取头像的实时展示事件(js)
// 用户头像的实时展示
$('#myfile').change(function (){
//1.产生一个文件阅读器对象
let myFileReaderObj = new FileReader();
// 2.获取用户上传的头像文件
let fileObj = this.files[0];
//3.将文件对象交给阅读器对象获取
myFileReaderObj.readAsDataURL(fileObj)
myFileReaderObj.onload=function (){
// 4.修改img标签的src属性展示图片
$('#myimg').attr('src',myFileReaderObj.result)
}
})
最后给form的action="/set_avatar/"绑定具体的路由,编写函数,将form表单向后端提交数据编写逻辑
def set_avatar_func(request):
new_avator = request.FILES.get('new_avatar')
# models.UserInfo.objects.filter(pk=request.user.pk).update(avatar=new_avator) # update不会再自动添加avatar
user_obj = models.UserInfo.objects.filter(pk=request.user.pk).first()
user_obj.avatar=new_avator
user_obj.save() # 保存数据
return redirect('/backend/')
注销登录
1.键一个路由,编写后端代码
# 注销登录功能
path('logout/', views.logout),
后端逻辑
@login_required
def logout(request):
auth.logout(request)
return redirect('home_view')
搭建主页内容区
1.先给页面搭建基本的页面
2.数据录入我们使用的是django自带的admin后台管理,因为表数量及外键极多,使用create和pycharm提供是创建是创建不出来的。
admin后台管理表的录入
1.首先创建一个超级管理员
createsuperuser
admin
admin123
用管理员进去页面展示
表的录入
1.如果需要admin后台表的录入,首先要去admin.py先去注册模型类,访问admin路由使用管理员登录
from django.contrib import admin
from app01 import models
# Register your models here.
""" 只要注册了模型类,admin就会自动产生针对该注册表的增删改查至少四个功能"""
admin.site.register(models.UserInfo)
admin.site.register(models.Site)
admin.site.register(models.Article)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.Article2Tag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
2.修改admin后台管理的表名 ,去模型类里面添加下面的代码,后台的表名会自动修改。
# 修改admin后台管理给表名
class Meta:
verbose_name_plural='用户名'
表的绑定
1.先绑定结构最复杂的表>>>:文章表
2.为了显示站点清楚,在模型类里面添加双下str方法
def __str__(self):
return f'用户对象:{self.username}'
3.录入表的顺序
文章表里先去创建Site和Category表数据,先创建Site个人站点,再创建Category数据,创建文章分类的时候一定要注意绑定的个人站点
简易理解:文章>>>用户绑个人站点(用户表)>>标签>>>分清个人站点和个人文章
4.然后先去绑定用户的站点表,要不然登录之后不显示用户的站点表,用户表与站点表是一对一的关系
手机号会报错注意会报错,需要到models的手机字段修改blank=null,# blank参数用于控制admin后台管理 与数据库无关
phone = models.BigIntegerField(verbose_name='手机号', null=True ,blank=True) # # blank参数用于控制admin后台管理 与数据库无关
Kiki 212
rose 1 2
5.编写首页的内容区
'''文章过多的情况下应该考虑添加分页器'''
导入mypage分页器的代码
def home_func(request):
# 查询所有用户编写的文章
article_queryset = models.Article.objects.all()
"""文章过多的情况下应该考虑添加分页器 """
page_obj = mypage.Pagination(current_page=request.GET.get('page'),all_count=article_queryset.count())
page_queryset = article_queryset[page_obj.start:page_obj.end]
return render(request, 'homePage.html', locals())
首页展示
home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.js"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'bootstrap-3.4.1-dist/css/bootstrap.min.css' %}">
<script src="{% static 'bootstrap-3.4.1-dist/js/bootstrap.min.js' %}"></script>
</head>
<body>
<!--导航条开始-->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">BBS</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="搜索">
</div>
<button type="submit" class="btn btn-default">搜索</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#" data-toggle="modal" data-target="#myModal" >修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout/">注销登录</a></li>
</ul>
</li>
{% else %}
<li><a href="{% url 'register_views' %}">注册</a></li>
<li><a href="{% url 'login_views' %}">登录</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<!--导航条结束-->
<!--模态框开始-->
<!-- Button trigger modal -->
<!-- Modal -->
<div class="modal fade" id="myModal" 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">Modal title</h4>
</div>
{# form表单不好包,我们使用ajax提交数据#}
<div class="modal-body">
<div class="form-group">
<label for="">用户名</label>
<input type="text" value="{{ request.user.username }}" disabled class="form-control">
</div>
<div class="form-group">
<label for="">原密码</label>
<input type="text" id="old_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="text" id="new_pwd" class="form-control">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="text" id="confirm_pwd" class="form-control">
</div>
</div>
<div class="modal-footer">
<span id="error" style="color: red"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="setBtn">修改</button>
</div>
</div>
</div>
</div>
<!--模态框结束-->
<!--内容区域开始-->
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--面板-->
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一首</h3>
</div>
<div class="panel-body">
qq音乐
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一篇</h3>
</div>
<div class="panel-body">
博客园
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一写</h3>
</div>
<div class="panel-body">
pycharm编辑器
</div>
</div>
</div>
<div class="col-md-8">
{% for page_obj in page_queryset %}
<div class="media">
<h4 class="media-heading">{{ page_obj.title }}</h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ page_obj.site.userinfo.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body" style="padding: 10px">
{{ page_obj.desc }}
</div>
<div >
<span><a href="">{{ page_obj.site.userinfo.username }} </a></span>
<span >{{ page_obj.create_time|date:'Y-m-d H:i:s' }} </span>
<span class="glyphicon glyphicon-thumbs-up">{{ page_obj.up_num }} </span>
<span class="glyphicon glyphicon-thumbs-down">{{ page_obj.down_num }} </span>
<span class="glyphicon glyphicon-comment">{{ page_obj.comment_num }} </span>
</div>
</div>
<hr>
{% endfor %}
<div class="text-center">{{ page_obj.page_html|safe }}</div>
</div>
<div class="col-md-2">
<!--面板-->
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一首</h3>
</div>
<div class="panel-body">
qq音乐
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一篇</h3>
</div>
<div class="panel-body">
博客园
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">每日一写</h3>
</div>
<div class="panel-body">
pycharm编辑器
</div>
</div>
</div>
</div>
</div>
<!--内容区结束-->
<script>
$('#setBtn').click(function (){
$.ajax({
url:'/set_pwd/',
type:'post',
data:{
'old_pwd':$('#old_pwd').val(),
'new_pwd':$('#new_pwd').val(),
'confirm_pwd':$('#confirm_pwd').val(),
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success:function (args){
if(args.code===10000){
window.location.href=args.url
}else {
$('#error').text(args.msg)
}
}
})
})
</script>
</body>
</html>
首页展示图:
搭建个人站点
个人站点是在di和端口后面添加用户名,要想动态匹配获取用户名,用正则表达式或者或者转换器
路由转换器
#个人站点接口
path('<str:username>/',views.site_func)
视图层
def site_func(request, username):
# 查询个人站点是否存在
site_obj = models.Site.objects.filter(site_name=username).first()
if not site_obj:
return render(request, 'errorPage.html')
# 查询个人站点下所有的文章
article_queryset = models.Article.objects.filter(site=site_obj)
'''如果文章较多也应该添加分页器'''
# 查询个人站点下所有的分类名称以及每个分类下的文章数
category_queryset = models.Category.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num')
# 查询个人站点下所有的标签名称以及每个标签下的文章数
tag_queryset = models.Tag.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values('name',
'article_num')
# 年月分组并统计文章个数
from django.db.models.functions import TruncMonth
date_queryset = models.Article.objects.filter(site=site_obj).annotate(month=TruncMonth('create_time')).values(
'month').annotate(article_num=Count('pk')).values('month', 'article_num')
print(date_queryset)
return render(request, 'sitePage.html', locals())
模板层
图片防盗链:为了防爬虫,只有referer申请是自己网址的时候才允许使用图片errorPage.html
个人站点直接继承home页面,页面基本差不多,个人站点的标题不一样,要继承不一样的
{% block title %}
BBS
{% endblock %}
1.将下面的div移到右边class="pull-right"
2.根据个人用户名直接跳转到个人站点,到主页的下面的a标签添加路由/{{ page_obj.site.userinfo.username }}/
<span><a href="/{{ page_obj.site.userinfo.username }}/">{{ page_obj.site.userinfo.username }} </a></span>
3.设置个人的样式
侧边栏的设置
- 侧边栏展示
三道ORM查询题 :虚拟字段
时间展示格式
官网提供了针对日期字段的切割处理
id content create_time month
1 111 2020-11-11 2020-11
2 222 2020-11-12 2020-11
3 333 2020-11-13 2020-11
4 444 2020-11-14 2020-11
5 555 2020-11-15 2020-11
"""
django官网提供的一个orm语法
from django.db.models.functions import TruncMonth
-官方提供
from django.db.models.functions import TruncMonth
Sales.objects
.annotate(month=TruncMonth('timestamp')) # Truncate to month and add to select list
.values('month') # Group By month
.annotate(c=Count('id')) # Select the count of the grouping
.values('month', 'c') # (might be redundant, haven't tested) select month and count
时区问题报错,去设置里面更改
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
"""
侧边栏筛查功能
-
先研究博客园三种情况下的筛选特性
分类筛选路由特性: 站点名称/category/数据主键值 标签筛选路由特性: 站点名称/tag/数据主键值 日期筛选路由特性: 站点名称/archive/文章年月
-
研究路由开设接口
多个路由使用相同的视图函数,因为个人站点的文章和侧边栏筛选的文章互为父子集
# 侧边栏筛选接口 # path('<str:username>/category/<int:category_id>/', views.site_func), # path('<str:username>/tag/<int:tag_id>/', views.site_func), # path('<str:username>/archive/<str:yearAndmonth>/', views.site_func), # 上述三个路由可以合并成一个路由 re_path('^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<params>.*?)/', views.site_func) # condition是条件的意思
当多个路由同时使用一个视图函数时,涉及到函数里面传的参数不一致时,site_func(request,username)与site_func(request,username,category_id/tag_id/yearAndmonth)
把转换器转换使用正则表达式即可
简而言之,还是基于个人站点,所有多个路由使用相同的视图函数。通过视图函数接收的实参个数的不同而区分不同的业务逻辑。
3.视图函数
函数参数使用**kwargs接收,接下来判断的是我们拿到个人站点所有的文章,我们只需从站点文章下再筛选想要的文章,直接在文章对象下面做判断。对象通过正反向查询和神奇的双下划线
# 查询个人站点下所有的文章
article_queryset = models.Article.objects.filter(site=site_obj)
if kwargs:
condition = kwargs.get('condition')
params = kwargs.get('params')
if condition == 'category': # 文章分类下
article_queryset = article_queryset.filter(category_id=params)
print(article_queryset)
elif condition == 'tag': # 标签下的文章对象
article_queryset = article_queryset.filter(tags__pk=params)
else: # 年 月
year, month = params.split('-')
article_queryset.filter(create_time__year=year, create_time__month=month)
kiki第一篇文章的路由:Title
根据标签绑定相应的路由,我们可以跳转到相应文章的路由下
侧边栏制作inclusion_tag
1.侧边栏很多页面都需要使用,并且还需要传参才可以加载出来
2.直接制作成inclusion_tag制作
3.自定义制作固定步骤(模板层相关操作)
mytag.py
from django import template
from app01 import models
from django.db.models import Count
# 补充相应的模块
register = template.Library()
@register.inclusion_tag('leftmenu.html',name='mymenu')
def index(username):
site_obj = models.Site.objects.filter(site_name=username).first()
'''如果文章较多也应该添加分页器'''
# 查询个人站点下所有的分类名称以及每个分类下的文章数
category_queryset = models.Category.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values(
'name', 'article_num','pk')
# 查询个人站点下所有的标签名称以及每个标签下的文章数
tag_queryset = models.Tag.objects.filter(site=site_obj).annotate(article_num=Count('article__pk')).values('name', 'article_num','pk')
# 年月分组并统计文章个数
from django.db.models.functions import TruncMonth
date_queryset = models.Article.objects.filter(site=site_obj).annotate(month=TruncMonth('create_time')).values(
'month').annotate(article_num=Count('pk')).values('month', 'article_num')
return locals()
leftmenu.html
直接把固定区域的复制过来
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category_obj in category_queryset %}
<p><a href="/{{ site_obj.site_name }}/category/{{ category_obj.pk }}">{{ category_obj.name }}({{ category_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">标签分类</h3>
</div>
<div class="panel-body">
{% for tag_obj in tag_queryset %}
<p><a href="/{{ site_obj.site_name }}/tag/{{ tag_obj.pk }}">{{ tag_obj.name }}({{ tag_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">日期分类</h3>
</div>
<div class="panel-body">
{% for data_obj in date_queryset %}
<p><a href="/{{ site_obj.site_name }}/archive/{{ data_obj.month|date:'Y-m' }}"> {{ data_obj.month|date:'Y年m月' }}({{ data_obj.article_num }})</a></p>
{% endfor %}
</div>
</div>
文章详情页
文章详情页基础搭建
1.路由的设计
站点名称/article/数据主键值
2.后端逻辑
1.获取筛选某篇具体的文章对象
2.返回文章详情页,书写文章详情页,文章详情页跟个人站点只有内容区不一致,考虑用继承home.html
3.频繁的使用一个页面的区域和代码(频繁使用category_queryset\tag_queryset\date_queryset,装成inclusion_tag)>>>:考虑使用inclusion_tag
app01下面创建templatetags
templatetags下面创建一个mytag.py文件
4.编写好详情页后,把home和个人站点的标题都绑定上文章的url,点击标签直接跳转到相应的文章
<a href="/{{ article_obj.site.userinfo.username }}/article/{{ article_obj.pk }}">{{ article_obj.title }}</a>
3.文章内容copy
1.找到<div id=cnblogs_post_body></div>
2.点击copy>>copy outerHTML
3.需要转义|safe
点赞点踩样式和功能搭建
1.直接拷贝博客园样式 ,查看检查>>找到点赞点踩样式>>>copy(html还和css样式)
2.给点赞点踩绑定一个点击事件
let isUp = $(this).hasClass('diggit') //判断标签是否含有个class值 从而二选一区分赞和踩
alert(isUp)
当发送ajax时,考虑逻辑是否繁琐,逻辑比较的多的时候要开一个路由
3.针对路由匹配
此时要注意:因为上面路由是正则匹配,在含有动态匹配的路由很多时候,可能会出现顶替的情况,这个时候我们可以将简单的路由放前面 复杂的放后面 甚至修改匹配策略。
4.后端逻辑
"""
1.校验用户是否登录
2.校验当前文章是否时当前用户自己的
3.校验当前文章是否已经被当前用户点过
4.创建点赞点踩记录(完成数据库文章表中的优化字段,同步自动
"""
def up_or_down_func(request):
"""
1.校验用户是否登录
2.校验当前文章是否时当前用户自己的
3.校验当前文章是否已经被当前用户点过
4.创建点赞点踩记录(不要忘记文章表中的优化字段,同步自动
"""
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
# print(request.POST)
# 先判断用户是否登录
if request.user.is_authenticated:
article_pk = request.POST.get('article_pk')
is_up = request.POST.get('is_up') # true 是普通的字符串
article_obj = models.Article.objects.filter(pk=article_pk).first()
print(article_obj)
if not article_obj.site.userinfo == request.user: # 文章用户对象与当前用户对象比较
is_click = models.UpAndDown.objects.filter(user=request.user, article=article_obj)
if not is_click:
is_up = json.loads(is_up) # 自动转成python中的布尔值
if is_up:
models.Article.objects.filter(pk=article_pk).update(up_num=F('up_num') + 1)
back_dict['msg'] = '点赞成功'
else:
models.Article.objects.filter(pk=article_pk).update(down_num=F('down_num') + 1)
back_dict['msg'] = '点踩成功'
# 更新点赞点踩表三个键
models.UpAndDown.objects.create(user=request.user, is_up=is_up, article=article_obj)
else: # 如果自己已经点过了提示
back_dict['code'] = 10001
back_dict['msg'] = '您已经点过了'
else: # 当前用户登录在当前用户的文章里
back_dict['code'] = 10002
back_dict['msg'] = '自己不能给自己点的'
else:
back_dict['code'] = 10003
back_dict['msg'] = '请先<a href="/login/">登录</a>'
return JsonResponse(back_dict)
注意:前端发送过来的js类型的布尔值需要自己处理成python布尔值
is_up = json.loads(is_up)
# 自动转成python中的布尔值
5.前端页面优化
信息提示、数字动态变化(针对标签文本需要做类型转换,否则默认诗字符串拼接)
currentEle.children().first().text(Number(currentEle.children().first().text())+1)
// text拿到的字符串,要Number转
6.完整的点击事件
// 给点赞点踩绑定一个点击事件
$('.upordown').click(function (){
{#alert('你确定吗')#}
let currentEle = $(this);
let isUp = $(this).hasClass('diggit') //判断标签是否含有个class值 从而二选一区分赞和踩
{#alert(isUp)#}
// 发送AJAX请求
$.ajax({
url:'/up_or_down/', // 点赞点踩有一定的逻辑,单独开设接口处理
type:'post',
data:{
'csrfmiddlewaretoken':'{{ csrf_token }}',
'article_pk':'{{ article_obj.pk }}',
'is_up':isUp
},
success:function (args){
if(args.code===10000){
currentEle.children().first().text(Number(currentEle.children().first().text())+1)
// text拿到的字符串,要Number转
}
$('#d1').html(args.msg)
}
})
})
文章评论样式和功能
1.前端样式搭建 class="clearfix"
取消浮动,获取用户输入的textarea以及一个提交按钮、退出按钮
{% if request.user.is_authenticated %}
<div class="comment_area" >
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<textarea name="" id="comment" cols="30" rows="10" class="form-control"></textarea>
<button class="btn btn-primary" id="commentBtn">提交评论</button>
<span><a href="/login/">退出</a></span>
</div>
{% else %}
<p>
<a href="/register/">注册</a>
<a href="/login/">登录</a>
</p>
{% endif %}
2.评论逻辑>>>先考虑根评论 之后再考虑子评论
3.根评论
点击提交评论按钮 发送ajax请求 携带必要的参数(评论内容和文章主键)
//给提交评论的按钮绑定点击事件
$('#commentBtn').click(function (){
alert('你确定嘛')
$.ajax({
url: '/comment/',
type:'post',
data:{
'csrfmiddlewaretoken':'{{ csrf_token }}',
'article_pk':'{{ article_obj.pk }}',
'content':$('#comment').val(),
},
success:function (args){
}
})
})
因为评论的逻辑多还是开一个路由判断逻辑
#个人站点接口
path('<str:username>/',views.site_func),
后端的视图函数
后端直接获取数据并写入数据库
def comment_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
article_pk = request.POST.get('article_pk')
content = request.POST.get('content')
parent_id = request.POST.get('parent_id')
if parent_id:
name,content = content.split('\n')
print(name,content)
models.Article.objects.filter(pk=article_pk).update(comment_num=F('comment_num') + 1)
models.Comment.objects.create(user=request.user, article_id=article_pk, content=content, parent_id=parent_id)
back_dict['msg'] = '评论成功'
return JsonResponse(back_dict)
注意:要及时同步文章内容的评论数comment_num
4.渲染文章评论楼
1.# 获取当前文章所有的评论数据
comment_list = models.Comment.objects.filter(article=article_obj)
2.前端渲染页面
<div class="comment_list">
<ul class="list-group">
{% for comment_obj in comment_list %}
<li class="list-group-item">
<span><a href="#"> # {{ forloop.counter }}楼</a></span>
<span>{{ comment_obj.comment_time|date:'Y-m-d H:i' }}</span>
<span><a href="/{{ comment_obj.user.username }}/">{{ comment_obj.user.username }}</a></span>
<span>阅读数()</span>
<span>评论数({{ article_obj.comment_num }})</span>
<p class="pull-right">
<span><a href="">回复</a></span>
<span><a href="">删除</a></span>
</p>
<p>
{{ comment_obj.content }}
</p>
</li>
{% endfor %}
</ul>
注意:很多业务逻辑可能需要执行多条ORM语句,这个时候为了保证数据的完整性可以采用事务操作。
根评论完善
当我们提交评论时,页面会出现一个临时的评论样式,此时评论的主体还是根评论,页面刷新才会出现评论楼的样式
1.清空评论框里面的内容
$('#comment').val('');
2.动态创建标签并添加到评论楼中
// 获取用户评论的内容
let commentMsg = $('#comment').val();
let currentUserName = '{{ request.user.username }}';
let oldCommentMsg = commentMsg;
let tempComment = `
<li class="list-group-item">
<span class="glyphicon glyphicon-comment"><a href="/${currentUserName}/">${currentUserName}</a></span>
<p> ${oldCommentMsg}</p>
</li>`
3.定义一个全局变量
parentId = null; # 提前创建一个全局变量 用于存储评论主键值
4.查找ul标签然后添加上述的标签即可
$('.list-group').append(tempComment)
5.清空全局变量
parentId = null;
5.文章子评论业务逻辑
-
子评论特性
-
每一个根评论都应该有回复按钮,点击就可以填写子评论
添加一个回复和删除的按钮
<span><a href="#content" class="reply" username="{{ comment_obj.user.username }}" comment_id="{{ comment_obj.pk }}">回复</a></span>
-
点击回复按钮具体的动作:评论框自动添加@+评论的人名并换行,且聚焦
// 给回复按钮绑定点击事件 $('.reply').click(function (){ // 获取回复按钮所在的评论用户名 let targetUserName = $(this).attr('username'); //获取回复按钮所在的评论主键值,修改全局变量 $('#comment').val('@'+targetUserName + '\n').focus(); // focus()聚焦 })
-
如何区分不同的回复按钮所对应的用户名
利用标签自定义属性直接携带对应的评论用户名即可
username="{{ comment_obj.user.username }}"
-
-
子评论提交回复评论
-
提交根评论和子评论的是同一个按钮,两者的区别和联系是什么
a.根评论与子评论的区别:是否含有父评论的主键值
parent_id
(评论表)b.如何区分不同的回复按钮所对应的评论主键值:利用标签自定义属性直接携带对应的评论主键值
comment_id="{{ comment_obj.pk }}"
-
当两个点击事情同时用到一个变量parent_id,要把它设置为全局变量,方便后期使用
1.//提前创建一个全局变量,用户存储评论主键值 let parentId = null 完善ajax数据的给后端发送过去,后端更新处理到数据库 2. //获取回复按钮所在的评论主键值,修改全局变量 parentId = $(this).attr('comment_id');
-
页面不刷新,连续评论点击按钮发送子评论,评论的内容都是子评论的
原因是全局变量parent_id 没用情况导致的,所以我们每次提交评论都应该清除一下
5.清空全局变量 parentId = null;
-
针对子评论中的@用户名+换行 ,不是评论内容,是不应该记录到数据库的
在发送评论之前做一个筛选功能是否含有@和回复对象
1.前端剔除 // 如果发送的是子评论 那么需要处理掉前缀内容(前端可以做 后端也可以做) if(parentId){commentMsg = commentMsg.slice(commentMsg.indexOf('\n')+1) } 2.后端剔除 如果 parent_id if parent_id: name,content = content.split('\n') print(name,content)
-
-
子评论的渲染
1.判断是否是子评论,是就加上评论的对象用户名
评论内容加上@根评论用户名 {% if comment_obj.parent_id %} @{{ comment_obj.parent.user.username }} {% endif %}
针对评论的渲染也可以分页,也可以做根评论与子评论的集合操作(分类)
前端页面展示
文章详情、点赞点踩、文章 评论等功能
{% extends 'homePage.html' %}
{% block css %}
<link rel="stylesheet" href="media/css/{{ site_obj.site_theme }}/">
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
width: 46px;
height: 52px;
background: url('/static/img/upup.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.buryit {
float: right;
margin-left: 20px;
width: 46px;
height: 52px;
background: url('/static/img/downdown.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
}
</style>
{% endblock %}
{% block title %}
{{ site_obj.site_title }}
{% endblock %}
{% block content %}
<div class="col-md-2">
{% load mytag %}
{% mymenu username %}
</div>
<div class="col-md-10">
<h2 class="text-center">{{ article_obj.title }}</h2>
{{ article_obj.content|safe }}
{# 文章点赞点踩样式开始#}
<div class="clearfix">
<div id="div_digg">
<div class="diggit upordown">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit upordown">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<span style="color: red" id="d1"></span>
<div class="diggword" id="digg_tips">
</div>
</div>
</div>
{# 文章点赞点踩样式结束#}
{# 文章评论楼的渲染开始#}
<div class="comment_list">
<ul class="list-group">
{% for comment_obj in comment_list %}
<li class="list-group-item">
<span><a href="#">#{{ forloop.counter }}楼</a></span>
<span>{{ comment_obj.comment_time|date:'Y-m-d H:i' }}</span>
<span><a href="/{{ comment_obj.user.username }}/">{{ comment_obj.user.username }}</a></span>
<p class="pull-right"><a href="#">引用 </a></p>
<p class="pull-right"><a href="#" class="reply" username="{{ comment_obj.user.username }}" comment_id="{{ comment_obj.pk }}">回复 </a></p>
<p>
{% if comment_obj.parent_id %}
@{{ comment_obj.parent.user.username }}
{% endif %}
</p>
<p>
{{ comment_obj.content }}
</p>
</li>
{% endfor %}
</ul>
</div>
{# 文章评论楼的渲染结束#}
{# 文章评论样式开始#}
{% if request.user.is_authenticated %}
<div class="comment_area">
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<textarea name="" id="comment" cols="30" rows="10" class="form-control"></textarea>
<button class="btn btn-primary" id="commentBtn">提交评论</button>
</div>
{% else %}
<p>
<a href="/register/">注册</a>
<a href="/login/">登录</a>
</p>
{% endif %}
{# 文章评论样式结束#}
</div>
{% endblock %}
{% block js %}
<script>
// 给点赞点踩图标绑定点击事件
$('.upordown').click(function () {
let currentEle = $(this);
let isUp = $(this).hasClass('diggit') // 判断标签是否含有某个class值 从而二选一区分赞和踩
// 发送ajax请求
$.ajax({
url: '/up_or_down/', // 点赞点踩有一定的逻辑 单独开设接口处理
type: 'post',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'is_up': isUp,
},
success: function (args) {
if (args.code === 10000) {
currentEle.children().first().text(Number(currentEle.children().first().text()) + 1)
}
$('#d1').html(args.msg)
}
})
})
// 提前创建一个全局变量 用于存储评论主键值
let parentId = null;
// 给提交评论的按钮绑定点击事件
$('#commentBtn').click(function () {
// 获取用户评论的内容
let commentMsg = $('#comment').val();
let currentUserName = '{{ request.user.username }}';
let oldCommentMsg = commentMsg;
// 如果发送的是子评论 那么需要处理掉前缀内容(前端可以做 后端也可以做)
if(parentId){
commentMsg = commentMsg.slice(commentMsg.indexOf('\n') + 1)
}
$.ajax({
url: '/comment/',
type: 'post',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'article_pk': '{{ article_obj.pk }}',
'content': commentMsg,
'parent_id':parentId
},
success: function (args) {
if (args.code === 10000) {
// 清空评论框里面的内容
$('#comment').val('');
// 动态创建标签并添加到评论楼中
let tempComment = `
<li class="list-group-item">
<span class="glyphicon glyphicon-comment"><a href="/${currentUserName}/">${currentUserName}</a></span>
<p>
${oldCommentMsg}
</p>
</li>
`
// 查找ul标签然后添加上述的标签即可
$('.list-group').append(tempComment)
// 清空全局变量
parentId = null;
}
}
})
})
// 给回复按钮绑定点击事件
$('.reply').click(function () {
// 获取回复按钮所在的评论用户名
let targetUserName = $(this).attr('username');
// 获取回复按钮所在的评论主键值 修改全局变量
parentId = $(this).attr('comment_id');
$('#comment').val('@' + targetUserName + '\n').focus();
})
</script>
{% endblock %}
后台管理页面搭建
-
模仿博客园后台访问直接展示所有文章
1.首先新建路由
# 后台管理 path('backend/',views.backend_func),
2.后台函数
当我们业务有相同的功能,我们可以把将templates分类backend文件夹,里面就编写相应的html文件
def backend_func(request): return render(request,'backend/backend.html')
-
后台管理页面需要多次被当作模板页面使用,那么后台管理的页面不能继承其他的页面,否则无法被其他的子页面继承使用,需要单独开一个后台管理的模板页面
后台管理页面展示
添加文章页面搭建
基本功能搭建
路由>>>函数>>>返回页面
1.页面简易搭建
因为添加文章的页面和后台页面差不多,只有添加的内容不同,那就基于后台文章继承
2.文章内容区使用富文本编辑器
KindEditor 是一套开源的在线 HTML 编辑器,主要用于让用户在网站上获得所见即所得编辑效果,开发人员可以用 KindEditor 把传统的多行文本输入框 (textarea) 替换为可视化的富文本输入框。 KindEditor 使用 JavaScript 编写,可以无缝地与 Java、.NET、PHP、ASP 等程序集成,比较适合在 CMS、商城、论坛、博客、Wiki、电子邮件等互联网应用上使用。
a.官网下载:http://kindeditor.net/down.php
b. 如何使用富文本编辑器,点击文档查看或者网址:http://kindeditor.net/doc.php
c.下载编辑器并拷贝到static
目录下,照着上面的使用方法简介使用,类似于bootstrap的用法
使用富文本编辑后的页面
文章功能优化
1.编写后端逻辑
从文本的模型表中研究可知,文章简介desc可以开始路由写,也可以直接对文章进行截取,时间是自动创建的,三个优化字段可以为空,多对多关系外键要单独创建的(半自动要单独创建)
2 .添加文章需要注意的问题
- 要注意模型层设置的内容长度
出错的点desc=max_lenth=32,导致添加的过长报错
-
文章简介不应该有标签的存在
后端其实接收的是html页面是存在标签的,但是渲染到前端页面是需要去除标签的存在的,借助于bs4模块和插件lxml
soup = BeautifulSoup(content, 'lxml') desc=soup.text[0:50], #获取文本,然后切割150个字符
-
文章内容不允许编辑script脚本,在编辑页编辑script脚本也称为XSS攻击
涉及到html相关内容的处理,可以借助于爬虫相关模块bs4, 内部封装了正则 使用更方便
from bs4 import BeautifulSoup soup = BeautifulSoup(content, 'lxml') # 第二个参数是解析器,不同的解析器功能不同,最好使用lxml ,需要提前下载 tags = soup.find_all() # 拿所有的标签 for tag in tags: if tag.name == 'script': tag.decompose() # 删除script标签 content=str(soup), # 使用处理之后的内容(不包含script标签)
3.编辑器上传图片问题
因为编辑器是后端没有处理,正常上传图片会变成下载图片,我们需要去KindEditor官网查看文档是如何解决这类问题?
查看文档信息,锁定要查看的信息内容>>:查看上传文件和extraFileUploadParams(参数)的用法
1.首先在html的js部分导入静态文件
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
2.根据官方文档为边编辑器设置编辑内容的高宽以及滚动条的滑动、设置上传的路由以及参数
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#editor_id',{
width : '100%', //编辑器内容宽度
height: '600px', //编辑器内容高度
resizeType:1, //2或1或0,2时可以拖动改变宽度和高度,1时只能改变高度,0时不能拖动。
uploadJson : '/upload_img/',
extraFileUploadParams : {
csrfmiddlewaretoken:'{{ csrf_token }}',
}
});
});
</script>
3.根据上传的图片编写后端逻辑
def upload_img_func(request):
# 返回的数据格式是有要求的
"""
官方数据返回的格式
//成功时
{
"error" : 0,
"url" : "http://www.example.com/path/to/file.ext"
}
//失败时
{
"error" : 1,
"message" : "错误信息"
}
"""
back_dict = {
"error": 0,
}
print(request.FILES) # 通过打印可知结果是字典形式{“imgFile”,[] }
"""
<MultiValueDict: {'imgFile': [<InMemoryUploadedFile: src=http___i0.hdslb.com_bfs_archive_a950a0a98c107b172ac0ae55642998da3369cc62.jpg&refer=http___i0.hdslb.webp (image/webp)>]}>
"""
currentTime = datetime.datetime.today().strftime('%Y%m%d %H%M%S')
print(currentTime)
if request.method == 'POST':
# 获取编辑器上传的图片数据files
img_obj = request.FILES.get("imgFile")
# 拼接文章图片所存放的路径
img_dir_path = os.path.join(settings.BASE_DIR, 'media', 'article')
if not os.path.exists(img_dir_path):
os.mkdir(img_dir_path)
img_file_path = os.path.join(img_dir_path,
f'{request.user.username}${currentTime}${img_obj.name}') # 针对当前文件名,为了防止冲突可以添加唯一标识,比如:用户名+当前时间+文件名
with open(img_file_path, 'wb') as f:
for line in img_obj:
f.write(line)
back_dict[
'url'] = f'/media/article/{request.user.username}${currentTime}${img_obj.name}' # 不能返回后端绝对路径 而应该是暴露的接口路径
return JsonResponse(back_dict)
注意点:
- 如果不知道传到后端的数据形式,可以提前打印看看
- 获取图片格式是
request.FILES
- 拼接路径,路径一般存在暴露接口,为了让前端暴露给用户看
media
文件夹下 - 返回给前端的网址接口应该是项目暴露的接口路径
url
文章编辑功能
1.继承父页面backend.html,文章编辑页面跟添加文章的页面内容差不多,直接拷贝过来添加文章页面修改。
1.编辑页面内容框设置默认值
给前端页面需要的对象值
2.后端获取前端内容做逻辑判断
拿到前端文章内容、去除前端内容和标签使用模块bs4、添加到数据库
3.添加到数据库时注意多对多关系的创建
tag_obj_list=[]
for tag_id in tag_list:
article2tag_obj = models.Article2Tag(article=article_obj,tag_id=tag_id)
tag_obj_list.append(article2tag_obj)
models.Article2Tag.objects.bulk_create(tag_obj_list)
return redirect('/backend/')
4.多对多关系表要提前把之前的绑定关系清除了,再创建绑定关系,否则之前的记录还是数据库中
models.Article2Tag.objects.filter(article_id=article_pk).delete() # 先清除原来的绑定关系
文章删除功能
1.引入插件SweetArlet
直接在页面引入CND
<script src="https://unpkg.com/sweetalert/dist/sweetalert.min.js"></script>
2.给删除按钮绑定一个点击事件 ,还有自定义属性article_pk="{{ article_obj.pk }}"
<script>
$('.delBtn').click(function (){
let currentBtn = $(this);
swal({
title: "你确定要删除本篇文章吗?",
text: "删除之后不可恢复!",
icon: "warning",
buttons: true,
dangerMode: true,
})
.then((willDelete) => {
if (willDelete) {
//用户点击ok按钮,发送ajax请求
$.ajax({
url:'/delete_article/',
type:'post',
data: {'csrfmiddlewaretoken':'{{ csrf_token }}','article_pk':$(this).attr('article_pk')},
success:function (args){
if (args.code===10000){
swal("成功删除文章", //成功删除
{ icon: "success",});
//使用DOM操作,临时刷新页面
currentBtn.parent().parent().remove()
}else{
swal(args.msg)
}
}
})
} else {
swal("欢迎再来学习!"); // 取消删除
}
});
})
</script>
3.后端逻辑
def delete_article_func(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
print(request.POST)
article_pk = request.POST.get('article_pk')
# 一对多和一对一以及绑定了级联删除,只需要删除文章和多对多关系即可
models.Article.objects.filter(pk=article_pk).delete()
models.Article2Tag.objects.filter(article_id=article_pk).delete()
back_dict['code'] = '删除成功'
else:
back_dict['code'] = 10002
back_dict['msg'] = '取消删除'
return JsonResponse(back_dict)
文件分类与标签的创建
待补充
bbs总结
表设计:表名 外键字段
难点:前后端结合
1.注册功能
forms组件、自定义头像获取及校验
前端头像动态展示
2.登录功能
前端图片验证码
auth模块登录
3.首页搭建
auth模块编写修改密码、注销登录
admin后台管理(绑定数据千万不要出错)
media配置(自定义暴露文件资源)
4.个人站点
侧边栏数据展示(ORM查询题)
侧边栏筛选功能(路由设计 path() re_path() url())
侧边栏inclusion_tag制作
5.文章详情页
点赞点踩
1.拷贝html和css
2.绑定点击事件(根据标签的某个属性区分两种情况)
3.后端逻辑(是否登录、是否是原作者、是否点过、多个表同步更新)
4.前端动态展示
文章评论
1.先写根评论
2.再优化根评论(动态展示)
3.再写回复按钮功能
4.核心数据(评论主键值 评论用户名)
5.评论楼渲染(根评论 子评论)
6.后台管理
文章展示、添加文章(及格线)
作业
1.可以在登录之后有一个区域>>>>:展示个人站点
2.个人样式可以模仿头像的做法,配一个头像的路径avatar
3.个人站点名和个人用户名不一样的优化配置
4.绑定事务的三个操作(全局,装饰器、with上下管理)
5.给删除按钮添加一个路由
6.用户登录成功之后导航条制作用户头像
7.评论区子评论跟在根评论的下面
考试扩展的功能
搜索功能、分类标签、个人站点与用户的绑定、点赞点踩的取消
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库