BBS多人博客系统
今日内容
项目开发流程
项目名字:
BBS:多人博客系统
传统软件行业:给第三方做解决方案
互联网行业:饿了么,叮咚买菜...
软件开发流程:
-项目立项(高层,市场人员:市场调研,跟客户对接)
-项目设计(产品经理:设计软件功能,设计原型图)
-项目的具体设计(UI:切图)
-分任务开发
-前端:pc,小程序,移动端
-分任务开发
-后端:
-架构,数据库设计---(设计表,表关联)
-多人系统开发
-联调
-测试:功能测试,自动化测试,接口测试、、、
-上线运行:运维
-出现bug,项目维护阶段
-版本迭代
bbs项目表设计及关联
# 要开发的功能
-注册功能
-登录功能
-首页:文章展示,导航栏,用户中心,广告位
-个人主页:文章展示,侧边栏过滤(分类,标签,时间)
-文章详情:点赞点踩,评论(父评论,子评论)
-后台管理:这个人文章展示(增加,删除,修改文章)
-发布文章:富文本编辑器,xss攻击处理
# 技术选型:python3.8 django2.2.2 mysql:5.7 jquery2.x bootstrap@3
# 设计数据库---》数据库名字 bbs
-用户表(基于auth的user表扩写,扩写字段)
-博客表(跟用户表一对一)
-标签表
-分类表
-文章表
-点赞点踩表
-评论表
# 表的关联关系
-用户表(基于auth的user表扩写,扩写字段)
-博客表(跟用户表一对一)
-标签表:跟博客表一对多,跟文章是多对多
-分类表:跟博客表一对多,跟文章是一对多
-文章表:跟博客表是一对多
-点赞点踩表:跟用户一对多,跟文章表一对多
-评论表:跟用户一对多,跟文章表一对多
补充:
OneToOneField,ForeignKey,ManyToManyField
-related_name:反向操作时,使用的字段名,用于代替原反向查询时的’表名_set’。
-related_query_name:反向查询操作时,使用的连接前缀,用于替换表名
项目表字段编写和表迁移
创建项目
# 第一步:安装djagno 2.2.2
pip3 install django==2.2.2
# 第二步:使用pycharm创建项目
# 第三步:配置文件
-58行:
'DIRS': [os.path.join(BASE_DIR , 'templates')]
-国际化
LANGUAGE_CODE = 'zh-hans' # 语言中文
TIME_ZONE = 'Asia/Shanghai' # 时区使用上海时区
USE_I18N = True
USE_L10N = True
USE_TZ = False
-使用mysql
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'HOST': '127.0.0.1',
'PORT': 3306,
'NAME': 'bbs', # 去创建数据库---》navicat创建
'USER': 'root',
'PASSWORD': '123',
}
}
-创建bbs数据库
navicat创建
# 扩写auth的user表
AUTH_USER_MODEL = 'blog.UserInfo'
在models中写表模型
from django.db import models
from django.contrib.auth.models import AbstractUser
# 7 张表
# 继承AbstractUser,扩写字段:头像字段,手机号字段, 一对一的博客字段
class UserInfo(AbstractUser):
# username,password,email。。。。
phone = models.CharField(max_length=32, null=True)
# 存文件的字段---》本质还是varchar,可以把文件自动保存(avatar文件夹下),存文件地址
avatar = models.FileField(upload_to='avatar', default='avatar/default.png')
blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True) # 一对一关联了blog表
class Blog(models.Model):
title = models.CharField(max_length=32, null=True, verbose_name='主标题')
site_name = models.CharField(max_length=32, null=True, verbose_name='副标题')
site_style = models.CharField(max_length=32, null=True, verbose_name='个人站点样式')
class Tag(models.Model):
name = models.CharField(max_length=32, verbose_name='标签名字')
# on_delete可以有很多选项,目前先用级联删除(很危险)
blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
class Category(models.Model):
name = models.CharField(max_length=32, verbose_name='分类名字')
# on_delete可以有很多选项,目前先用级联删除(很危险)
blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
class Article(models.Model):
title = models.CharField(max_length=32, verbose_name='文章名字')
desc = models.CharField(max_length=255, verbose_name='文章摘要')
content = models.TextField(verbose_name='文章内容') # 大文本
# auto_now_add auto_now
create_time = models.DateTimeField(auto_now_add=True,verbose_name='文章创建时间') #auto_now_add=True 新增文章这个字段可以不传,自动把当前时间加上
blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
category = models.ForeignKey(to='Category', on_delete=models.CASCADE)
# 多对多关系,需要创建第三张表,
# django的orm的ManyToManyField字段可以自动创建第三张表(ArticleToTag)
# 手动创建第三张表:ManyToManyField一定要加两个参数,through=中间表 , through_fields通过哪两个字段关联
tag = models.ManyToManyField(to='Tag')
class UpAndDown(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
is_up = models.BooleanField(verbose_name="点赞或点踩")
create_time = models.DateTimeField(auto_now_add=True)
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
content = models.CharField(max_length=64, verbose_name="评论的内容")
create_time = models.DateTimeField(auto_now_add=True)
# parent=models.ForeignKey(to='Comment', on_delete=models.CASCADE)
# parent=models.IntegerField(null=True)
parent = models.ForeignKey(to='self', on_delete=models.CASCADE, null=True)
# 自关联字段---》父评论--》子评论
'''
id user article content parent
1 2 1 写的真好 空
2 2 2 写的一般 空
3 3 2 人家明明写的很好 2
'''
# 安装pymysql
pip3 install pymysql
# 在__ini__.py中加入:djagno默认操作mysql数据库使用的是 mysqlDB模块,在python2中没问题,但是在python3中已经不维护了,不支持,python3中操作mysql咱么用pymysql比较多,但是需要加下面两句话才能正常使用-----》猴子补丁(动态替换--把源码使用mysqlDB替换成pymysql的东西)
# 但是在django2.0.7及以后版本,需要改源码才能使用:operations.py---》146行,改成query = query.encode(errors='replace')
import pymysql
pymysql.install_as_MySQLdb()
# 以后直接使用 mysqlclient:有可能装不上---》看人品
pip3 install mysqlclient
注册forms编写
from django import forms
from django.forms import widgets
from django.core.exceptions import ValidationError
from models import UserInfo # 模块导入:相对导入和绝对导入
class RegisterForm(forms.Form):
# 用户名,密码,确认密码,邮箱
username = forms.CharField(max_length=8, min_length=3, required=True, error_messages={
'max_length': '太长了',
'min_length': '太短了',
'required': '这个必填'
}, widget=widgets.TextInput(attrs={'class': 'form-control'}))
password = forms.CharField(max_length=8, min_length=3, required=True, error_messages={
'max_length': '太长了',
'min_length': '太短了',
'required': '这个必填'
}, widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
re_password = forms.CharField(max_length=8, min_length=3, required=True, error_messages={
'max_length': '太长了',
'min_length': '太短了',
'required': '这个必填'
}, widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
email = forms.EmailField(widget=widgets.EmailInput(attrs={'class': 'form-control'}))
# 局部钩子校验
# 方案一:
def clean_username(self):
username=self.cleaned_data.get('username')
user=UserInfo.objects.filter(username=username).first()
if user:
# 已经存在,不合理
raise ValidationError('该用户名已经存在')
else:
return username
#方案二:
# def clean_username(self):
# username=self.cleaned_data.get('username')
# try:
# UserInfo.objects.get(username=username) #有且只有一条才ok,否则就抛异常
# raise ValidationError('该用户名已经存在')
# except Exception:
# return username
# 全局钩子校验
def clean(self):
# 比较两个密码是否一致 :cleaned_data存的是校验过后的数据,是个字典
password = self.cleaned_data.get('password')
re_password = self.cleaned_data.get('re_password')
if password == re_password:
# 合理,返回校验过后的数据
return self.cleaned_data
else:
# 不合理,抛出校验失败的异常
raise ValidationError('两次密码不一致!!')
注册页面搭建
# bootstrap 搭建
-settings.py 中 加入static
STATIC_URL = '/static/'
STATICFILES_DIRS=[
os.path.join(BASE_DIR,'static')
]
-把bootstrap的静态资源copy到static文件夹下
# 新建模板文件 register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center">注册功能</h1>
<form id="id_form">
{% for item in form %}
<div class="form-group">
<label for="{{ item.id_for_label }}">{{ item.label }}</label>
{{ item }}
<span class="pull-right text-danger"></span>
</div>
{% endfor %}
<div class="form-group">
<label for="id_file">头像
<img src="/static/img/default.png" alt="" height="80px" width="80px" style="margin-left: 10px">
</label>
<input type="file" id="id_file" accept="image/*" style="display: none">
</div>
<div class="form-group text-center">
{# 如果input类型是submit或者button标签,放在form表单中,如果点提交,触发form的提交,如果我们写了ajax提交,会触发两次提交#}
<input type="button" value="注册" class="btn btn-danger" id="id_submit">
<span class="text-danger"></span>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
头像动态显示
# js 控制
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center">注册功能</h1>
<form id="id_form">
{% for item in form %}
<div class="form-group">
<label for="{{ item.id_for_label }}">{{ item.label }}</label>
{{ item }}
<span class="pull-right text-danger"></span>
</div>
{% endfor %}
<div class="form-group">
<label for="id_file">头像
<img src="/static/img/default.png" alt="" height="80px" width="80px" style="margin-left: 10px" id="id_img">
</label>
<input type="file" id="id_file" accept="image/*" style="display: none">
</div>
<div class="form-group text-center">
{# 如果input类型是submit或者button标签,放在form表单中,如果点提交,触发form的提交,如果我们写了ajax提交,会触发两次提交#}
<input type="button" value="注册" class="btn btn-danger" id="id_submit">
<span class="text-danger"></span>
</div>
</form>
</div
</div>
</div>
</body>
<script>
$("#id_file").change(function () {
// 把当前图片,放到img标签中
// 把图片地址放到img标签上,标签就显示了图片
//$("#id_img")[0].src='https://tva1.sinaimg.cn/large/00831rSTly1gd1u0jw182j30u00u043b.jpg'
// 1 id_file这个标签的图片读出来,借助于文件阅读器,js提供的一个类
var reader=new FileReader()
// 2 拿到文件对象,赋值给一个变量
var file =$("#id_file")[0].files[0]
// 3 把文件读到文件阅读器中
reader.readAsDataURL(file)
// 4 等读完后,把文件阅读器的内容写到img标签上
//$("#id_img")[0].src=reader.result
reader.onload=function (){
//$("#id_img")[0].src=reader.result
$('#id_img').attr('src', reader.result)
}
})
</script>
</html>
注册功能后端
def register(request):
if request.method == 'GET':
register_form = RegisterForm()
# context: 上下文
return render(request, 'register.html', context={'form': register_form})
else: # post请求的时候
res = {'code': 100, 'msg': '注册成功'}
# 取出用户名密码,使用form校验数据,如果校验通过,存到数据库中,如果校验不通过,返回错误信息
register_form = RegisterForm(data=request.POST)
if register_form.is_valid():
# 数据字段自己的规则,局部钩子,全局钩子都校验过后,通过了
# 1 re_password字段,不存到数据库的,剔除
register_data = register_form.cleaned_data
register_data.pop('re_password')
# 2 头像:如果携带了要存,头像是文件,在request.FILES中
my_file = request.FILES.get('my_file')
if my_file:
register_data['avatar'] = my_file
# 2 存到数据库
UserInfo.objects.create_user(**register_data)
# UserInfo.objects.create_user(username=register_data.get('username'),) 等同于上面,但是麻烦
return JsonResponse(res)
else:
res['code'] = 101
res['msg'] = '注册失败'
res['errors'] = register_form.errors
return JsonResponse(res)
注册功能前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center">注册功能</h1>
<form id="id_form" >
{% csrf_token %}
{% for item in form %}
<div class="form-group">
<label for="{{ item.id_for_label }}">{{ item.label }}</label>
{{ item }}
<span class="pull-right text-danger"></span>
</div>
{% endfor %}
<div class="form-group">
<label for="id_file">头像
<img src="/static/img/default.png" alt="" height="80px" width="80px" style="margin-left: 10px"
id="id_img">
</label>
<input type="file" id="id_file" accept="image/*" style="display: none">
</div>
<div class="form-group text-center">
{# 如果input类型是submit或者button标签,放在form表单中,如果点提交,触发form的提交,如果我们写了ajax提交,会触发两次提交#}
<input type="button" value="注册" class="btn btn-danger" id="id_submit">
<span class="text-danger"></span>
</div>
</form>
</div>
</div>
</div>
</body>
<script>
$("#id_file").change(function () {
// 把当前图片,放到img标签中
// 把图片地址放到img标签上,标签就显示了图片
//$("#id_img")[0].src='https://tva1.sinaimg.cn/large/00831rSTly1gd1u0jw182j30u00u043b.jpg'
// 1 id_file这个标签的图片读出来,借助于文件阅读器,js提供的一个类
var reader = new FileReader()
// 2 拿到文件对象,赋值给一个变量
var file = $("#id_file")[0].files[0]
// 3 把文件读到文件阅读器中
reader.readAsDataURL(file)
// 4 等读完后,把文件阅读器的内容写到img标签上
//$("#id_img")[0].src=reader.result
reader.onload = function () {
//$("#id_img")[0].src=reader.result
$('#id_img').attr('src', reader.result)
}
})
// 当点击注册按钮,发送ajax请求到后端的注册功能
$("#id_submit").click(function () {
// 上传文件,借助于formdata对象
var formdata = new FormData()
// 方式一
/*formdata.append('username',$('#id_username').val())
formdata.append('password',$('#id_username').val())
formdata.append('re_password',$('#id_username').val())
formdata.append('my_file',$('#id_file')[0].files[0])
// csrftoken也要加上*/
// 方式二:借助于form表单批量处理
var data = $("#id_form").serializeArray()
console.log(data)
// 使用for循环,把data中得数据,转存到formdata中 jquery的each循环
$.each(data, function (i, v) {
//console.log("索引是:",i)
//console.log("值是:",v)
//console.log("------")
formdata.append(v.name, v.value)
})
// 文件单独再放进去
formdata.append('my_file', $('#id_file')[0].files[0])
// 使用ajax向后端发送请求
// 1 三种编码格式:urlencoded,form-data,json
// {name:lqz,age:19}--->name=lqz&age=19
$.ajax({
url: '/register/',
type: 'post',
processData: false,
contentType: false,
data: formdata,
success: function (data) {
console.log(data)
if(data.code==100){
// 表示注册成功,跳转到登录页面
location.href='/login/'
}else {
// 在前端显示错误信息
}
}
})
})
</script>
</html>
头像上传路径问题
# 咱们写项目后台静态资源中得图片,一般放在static文件夹下
# 用户上传的文件,图片等,一般放在media文件夹下
-我们想做的是,avatar文件夹要在media文件夹下
# django中只需要在配置文件中加一句话
# 以后再上传的文件路径是从media文件夹下开始
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
注册错误信息渲染
$("#id_submit").click(function () {
// 上传文件,借助于formdata对象
var formdata = new FormData()
// 方式一
/*
formdata.append('username',$('#id_username').val())
formdata.append('password',$('#id_username').val())
formdata.append('re_password',$('#id_username').val())
formdata.append('my_file',$('#id_file')[0].files[0])
// csrftoken也要加上
*/
// 方式二:借助于form表单批量处理
var data = $("#id_form").serializeArray()
console.log(data)
// 使用for循环,把data中得数据,转存到formdata中 jquery的each循环
$.each(data, function (i, v) {
//console.log("索引是:",i)
//console.log("值是:",v)
//console.log("------")
formdata.append(v.name, v.value)
})
// 文件单独再放进去
formdata.append('my_file', $('#id_file')[0].files[0])
// 使用ajax向后端发送请求
// 1 三种编码格式:urlencoded,form-data,json
// {name:lqz,age:19}--->name=lqz&age=19
$.ajax({
url: '/register/',
type: 'post',
processData: false,
contentType: false,
data: formdata,
success: function (data) {
console.log(data)
if(data.code==100){
// 表示注册成功,跳转到登录页面
location.href='/login/'
}else {
// 在前端显示错误信息
console.log(data)
// 两次密码不一致的错误渲染
/*
if(data.errors['__all__']){
$(".error").html(data.errors['__all__'][0])
}*/
// 其他标签的错误渲染
$.each(data.errors,function (k,v){
if (k=='__all__'){ //两次密码不一致的错误渲染
$(".error").html(v[0])
}else {
// 链式调用,在对应的input后的span中插入错误文字,把父div加入has-error类,整个框变红
$("#id_"+k).next().html(v[0]).parent().addClass('has-error')
}
})
// 起一个定时任务
setTimeout(function (){
// 把所有span的文字去掉,把父div中得has-error类去掉
$('.text-danger').html("").parent().removeClass('has-error')
},3000)
}
}
})
})
用户存在校验功能
后端校验接口
# 前端通过get把用户名传入,我们根据用户名查询数据库,如果用户名存在,返回存在,如果不存在,返回不存在
/check_username/?username=lqz
def check_username(request):
# /check_username/?username=lqz
res = {'code': 100, 'msg': '用户存在'}
username = request.GET.get('username')
user = UserInfo.objects.filter(username=username).first()
if user:
# 用户存在
return JsonResponse(res)
else:
# 用户不存在
res['code'] = 101
res['msg'] = '用户不存在'
return JsonResponse(res)
前端
// username输入框,失去焦点,触发ajax执行
$('#id_username').blur(function () {
$.ajax({
url: '/check_username/?username=' + $(this).val(),
type: 'get',
success: function (data) {
if (data.code == 100) {
// 在span中插入错误信息
{#alert(data.msg)#}
$('#id_username').next().html(data.msg)
}
}
})
})
登录页面搭建
1 创建login.html
2 登录后台,get返回该页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center">登录功能</h1>
<form id="id_form">
{% csrf_token %}
<div class="form-group">
<label for="id_username">用户名</label>
<input type="text" id="id_username" name="username" class="form-control">
</div>
<div class="form-group">
<label for="id_password">密码</label>
<input type="password" id="id_password" name="password" class="form-control">
</div>
<div class="form-group">
<label for="id_code">验证码</label>
<div class="row">
<div class="col-md-6">
<input type="text" id="id_code" name="code" class="form-control">
</div>
<div class="col-md-6">
<img src="/static/img/default.png" alt="" width="500px" height="40px">
</div>
</div>
</div>
<div class="form-group text-center">
<input type="button" value="登录" class="btn btn-danger" id="id_submit">
<span class="text-danger error"></span>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
自定义图片验证码
# 验证码:数字,大写字母,小写字母的组合 5位
# 第三方的模块,可以直接生成
# 自己写
# 方式一,直接返回一张图片
# 方式二:自己生成一张图片,返回
# 方式三:把生成的图片写到内存中
# 方法四:在图片上写文字
# 方式五:图片上写文字,字体是指定的字体,字的颜色随机
# 方式6 最终方案
def get_rgb():
return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
def get_code(request):
# 方式一,直接返回一张图片
# with open('./media/avatar/2.jpeg', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 方式二:自己生成一张图片,返回 --->借助模块,pillow
# # rgb:三原色 red green blue
# # 宽度和高度
# # 三原色的色值 0-255
# #1 创建图片
# img=Image.new('RGB',(500,40),(0,255,255))
# #2 保存到本地
# with open('code.png','wb') as f:
# img.save(f)
# # 3 打开图片,返回给前端
# with open('./code.png', 'rb') as f:
# data = f.read()
# return HttpResponse(data)
# 方式三:把生成的图片写到内存中
# # 1 创建图片
# img = Image.new('RGB', (500, 40), (0, 0, 0))
# # 2 保存到内存
# byte_io = BytesIO()
# img.save(byte_io,'png') # 指定图片格式
# # 3 从BytesIO取出二进制,返回给前端
# return HttpResponse(byte_io.getvalue())
# 方法四:在图片上写文字
# # 1 创建图片
# img = Image.new('RGB', (500, 40), (0, 255, 0))
# # 2 在图片上写文字 ,相当于画板
# img_draw=ImageDraw.Draw(img)
# img_draw.text((0,0),"lqz")
# # 2 保存到内存
# byte_io = BytesIO()
# img.save(byte_io,'png') # 指定图片格式
# # 3 从BytesIO取出二进制,返回给前端
# return HttpResponse(byte_io.getvalue())
# 方式五:图片上写文字,字体是指定的字体,字的颜色随机
# img = Image.new('RGB', (500, 40), get_rgb())
#
# # 2 创建一个字体对象
# font=ImageFont.truetype('./static/font/xgdl.ttf',30)
# # 2 在图片上写文字 ,相当于画板
# img_draw = ImageDraw.Draw(img)
# img_draw.text((0, 0), "lqz",font=font)
# # 2 保存到内存
# byte_io = BytesIO()
# img.save(byte_io, 'png') # 指定图片格式
# # 3 从BytesIO取出二进制,返回给前端
# return HttpResponse(byte_io.getvalue())
# 第六:终极方案自己造 验证码
# img = Image.new('RGB', (500, 40), get_rgb())
img = Image.new('RGB', (500, 40), (255,255,255))
# 2 创建一个字体对象
font = ImageFont.truetype('./static/font/ss.TTF', 30)
# 3 在图片上写文字 ,相当于画板
img_draw = ImageDraw.Draw(img)
# 4 在图片上写文字(数字,大写字母,小写字母组合 5个)
code_str = ''
for i in range(5):
num = random.randint(0, 9)
upper = chr(random.randint(65, 90))
low = chr(random.randint(97, 122))
ran = str(random.choice([num, upper, low]))
code_str += ran # python 是强类型语言,不同类型直接不能直接相加,需要类型转换
img_draw.text((60+i*60, 0), ran, fill=get_rgb(),font=font)
request.session['code'] = code_str
#5 在图片上画点画线
width = 450
height = 30
for i in range(10):
x1 = random.randint(0, width)
x2 = random.randint(0, width)
y1 = random.randint(0, height)
y2 = random.randint(0, height)
# 在图片上画线
img_draw.line((x1, y1, x2, y2), fill=get_rgb())
for i in range(50):
# 画点
img_draw.point([random.randint(0, width), random.randint(0, height)], fill=get_rgb())
x = random.randint(0, width)
y = random.randint(0, height)
# 画弧形
# img_draw.arc((x, y, x + 4, y + 4), 0, 90, fill=get_rgb())
# 2 保存到内存
byte_io = BytesIO()
img.save(byte_io, 'png') # 指定图片格式
# 3 从BytesIO取出二进制,返回给前端
return HttpResponse(byte_io.getvalue())
点击图片验证码更新
$('#id_img').click(function (){
// 把图片的src地址变成原来地址?
//alert($(this)[0].src)
let timestamp=new Date().getTime()
$(this)[0].src='/get_code/?t='+timestamp
})
登录功能后端
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
else:
# 前端传入的用户名,密码,验证码, 校验验证码 去数据库比较用户名和密码
res = {'code': 100, 'msg': '成功'}
# 1 拿出用户名,密码,验证码,校验验证码
print(request.POST)
code = request.POST.get('code')
username = request.POST.get('username')
password = request.POST.get('password')
old_code = request.session.get('code')
if code.lower() == old_code.lower(): # 不区分大小写
# 2 校验用户名或密码
# user=UserInfo.objects.filter(username=username,password=password) # 这个不行
user = authenticate(username=username, password=password)
if user:
# 3 登录成功,返回json格式字符串
return JsonResponse(res)
else:
res['code'] = 101
res['msg'] = '用户名或密码错误'
return JsonResponse(res)
else:
res['code'] = 102
res['msg'] = '验证码错误'
return JsonResponse(res)
登录功能前端
$("#id_submit").click(function () {
// 取出填入的数据,发送ajax的post请求
//$('#id_form').serializeArray()
$.ajax({
url:'/login/',
type:'post',
data:{
'username':$('#id_username').val(),
'password':$('#id_password').val(),
'code':$('#id_code').val(),
'csrfmiddlewaretoken':$('[name="csrfmiddlewaretoken"]').val(), //根据属性取标签
},
success:function (data){
console.log(data)
if(data.code==100){
location.href='/'
}else {
$('.error').html(data.msg)
}
}
})
})
首页导航和首页轮播图
新建index.html---》写一个路由---》get请求返回index.html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<title>首页</title>
</head>
<body>
<div class="mynavbar">
<nav class="navbar navbar-default">
<div class="container-fluid">
<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="#">博客园</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">
<li><a href="#">用户名</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>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
</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">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">广告招商</h3>
</div>
<div class="panel-body">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
</div>
<div class="col-md-7">
<div class="top">
<div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
<ol class="carousel-indicators">
<li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
<li data-target="#carousel-example-generic" data-slide-to="1"></li>
</ol>
<div class="carousel-inner" role="listbox">
<div class="item active">
<img src="/static/img/banner1.jpg" alt="...">
<div class="carousel-caption">
博客开博了
</div>
</div>
<div class="item">
<img src="/static/img/banner2.jpg" alt="...">
<div class="carousel-caption">
欢迎大家投稿
</div>
</div>
</div>
<!-- Controls -->
<a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
<span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
<span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
</div>
<div class="article">
文章详情
</div>
</div>
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">广告招商</h3>
</div>
<div class="panel-body">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">广告招商</h3>
</div>
<div class="panel-body">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">广告招商</h3>
</div>
<div class="panel-body">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
首页文章列表页面
# admin 后台录入数据 文章表录入数据
# admin 是djagno的一个app,它是一个后台管理,内置的,可以方便我们做二次开发和录入数据
-在app的admin.py 把表注册一下 ,登录到后台就可以看到了
-直接录入数据,跟在数据表中录入数据本质一样的
开启media访问
# 上传的图片,目前访问不到,要开启media的访问,才能看到图片
# 开启media访问,本质就是把项目中某个目录可以让客户端(浏览器)直接访问,很危险,咱们只能开启media,以后meida下不能放重要的东西
# 本身static 文件夹从浏览器可以访问,默认这个目录已经开放了
# 如何开启
-1 在配置文件中
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-2 在路由中
path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
图片防盗链
# 有的网站有上传图片功能,我们可以把图片传上去,然后在自己的网站中使用,这样不会耗费我们自己网站的带宽,比如cnblogs,cnblogs就屏蔽了这种行为,这就是图片防盗链
# 本质原理是:
因为发送http请求,请求头中有个参数叫referer,是一个url地址,它表示上一次访问的地址
图片防盗链可以通过这个来控制,判断一下referer是不是我们自己的地址,如果不是就不返回
通过请求头中得referer来控制不被非自己的地址请求
个人站点页面搭建
路由
# 一定要放在最下边
path('<str:name>/', views.site )
视图函数
def site(request, name):
# 根据人名查到数据库中,才返回个人站点,如果没有,返回404页面
user = UserInfo.objects.filter(username=name).first()
if user:
# 返回当前这个人所有文章
article_list = Article.objects.all().filter(blog=user.blog)
# article_list = user.blog.article_set.all()
return render(request, 'site.html', locals())
else:
return render(request, '404.html')
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
{% block title %}
{% endblock %}
</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="/static/css/common.css">
{% block link %}
{% endblock %}
</head>
<body>
<div class="main">
<div class="header">
{% block header %}
{% endblock %}
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">我的标签</h3>
</div>
<div class="panel-body">
<div>
{% for tag in tag_res %}
<hr>
<a href="">
<span>{{ tag.1 }}</span>
<span>({{ tag.2}})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">随笔分类</h3>
</div>
<div class="panel-body">
<div>
{% for category in category_res %}
<hr>
<a href="">
<span>{{ category.1 }}</span>
<span>({{ category.2}})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">随笔档案</h3>
</div>
<div class="panel-body">
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
<hr>
<div>
重金求子 <a href="">点我</a>
</div>
</div>
</div>
</div>
<div class="col-md-10">
{% block article %}
{% endblock %}
</div>
</div>
</div>
</div>
</body>
{% block js %}
{% endblock %}
</html>
site.html
{% extends 'base.html' %}
{% block title %}
{{ user.blog.title }}
{% endblock %}
{% block link %}
<link rel="stylesheet" href="/static/css/{{ user.blog.site_style }}">
{% endblock %}
{% block header %}
<nav class="navbar navbar-default common">
<div class="container-fluid">
<div class="pull-right">
<button type="button" class="btn btn-default navbar-btn">管理</button>
</div>
<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="#">{{ user.blog.title }}</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><a href="/">首页 <span class="sr-only">(current)</span></a></li>
<li><a href="/">新闻 <span class="sr-only">(current)</span></a></li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{% endblock %}
{% block article %}
<div class="article">
{% for article in article_list %}
<div class="media">
<h4 class="media-heading"><a href="">{{ article.title }}</a></h4>
<hr>
<div class="media-body">
{{ article.desc }}
</div>
<div class="article-bottom pull-right" style="margin-top: 20px">
<span>posted @</span>
<span style="padding: 5px;font-size: 15px">{{ article.blog.userinfo.username }}</span>
<span style="padding: 5px;font-size: 15px">{{ article.create_time|date:"Y-m-d H:s" }}</span>
<span style="padding: 5px;font-size: 15px"><i class="fa fa-lock" aria-hidden="true"
style="margin-right: 5px"></i>{{ article.up_num }}</span>
<span style="padding: 5px;font-size: 15px"><i class="fa fa-hand-scissors-o" aria-hidden="true"
style="margin-right: 5px"></i>{{ article.down_num }}</span>
<span style="padding: 5px;font-size: 15px"><i class="fa fa-hand-rock-o" aria-hidden="true"
style="margin-right: 5px"></i>{{ article.comment_num }}</span>
<span><a href="">编辑</a></span>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
{% block js %}
{% endblock %}
左侧标签
# 分组查询---》mysql 的查询
-分类的id和名字
select id,name from category;
-查询当前博客下所有的分类和名字
select id,name from category where blog=user.blog
-查询当前博客下所有的分类和名字和分类下的文章数 ----》分组
-一个分类是一组,所有一分类id作为 group by category.id
select id,name,count(article.id),article.title from category,article where category.id=article.category and article.blog=user.blog group by category.id
# 分类表
id name
1 lqz分类1
2 lqz分类2
3 lqz分类3
4 张三分类1
# 文章表
id title category blog
1 go 1 1
2 python 1 1
3 java 2 1
4 js 4 2
# 连起来
id name id title category
1 lqz分类1 1 go 1
1 lqz分类1 2 python 1
2 lqz分类2 3 java 2
# 1 lqz分类1 2
# 2 lqz分类2 1
# 3 lqz分类3 0
# 原生sql转成orm
filter 在annotate前,表示where 条件
valeus 在annotate前,表示分组依据(group by 谁里面就写谁)
filter 在annotate后,表示having 条件
valeus 在annotate后,表示取字段 (id ,name ,c) Category.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list('id', 'name', 'c')
# 随笔档案的查询数据结构
-分组,单表 文章表---发布时间(年月日十分秒)
-文章表的数据,在单表中增加一个year_month字段,以它作为分组依据
id title create_time year_month
1 go 2022年9月18日 10时23分14秒 2022年9月
3 java 2022年9月14日 10时23分14秒 2022年9月
2 python 2022年7月10日 10时23分14秒 2022年7月
4 js 2022年8月12日 10时23分14秒 2022年8月
'''
Sales.objects.all().
.annotate(year_month=TruncMonth('create_time')) # Truncate to month and add to select list
.values('year_month') # Group By year_month
.annotate(c=Count('id')) # Select the count of the grouping
.values('year_month', 'c') # (might be redundant, haven't tested) select month and count
'''
视图函数
def site(request, name):
# 根据人名查到数据库中,才返回个人站点,如果没有,返回404页面
user = UserInfo.objects.filter(username=name).first()
if user:
# 返回当前这个人所有文章
article_list = Article.objects.all().filter(blog=user.blog)
# article_list = user.blog.article_set.all()
category_res = Category.objects.all().filter(blog=user.blog).values('id').annotate(
c=Count('article__id')).values_list('id', 'name', 'c')
tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
'id', 'name', 'c')
date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
print(tag_res)
print(category_res)
print(date_res)
return render(request, 'site.html', locals())
else:
return render(request, '404.html')
前端页面
base.html,代码28行开始
<div class="col-md-2">
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">我的标签</h3>
</div>
<div class="panel-body">
<div>
{% for tag in tag_res %}
<hr>
<a href="/{{ user.username }}/tag/{{ tag.0 }}.html">
<span>{{ tag.1 }}</span>
<span>({{ tag.2 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">随笔分类</h3>
</div>
<div class="panel-body">
<div>
{% for category in category_res %}
<hr>
<a href="/{{ user.username }}/category/{{ category.0 }}.html">
<span>{{ category.1 }}</span>
<span>({{ category.2 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">随笔档案</h3>
</div>
<div class="panel-body">
<div>
{% for data_itme in date_res %}
<hr>
<a href="/{{ user.username }}/archive/{{ data_itme.0|date:'Ym' }}.html">
<span>{{ data_itme.0|date:'Y年m月' }}</span>
<span>({{ data_itme.1 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
随笔档案,标签,分类点击过滤
路由
# 三个路由,标签的过滤,分类的过滤,随笔档案过滤
# lqz/tag/1.html
path('<str:name>/tag/<int:tag>.html', views.site),
#lqz/category/1.html
path('<str:name>/category/<int:category>.html', views.site),
# lqz/archive/2022/09.html
path('<str:name>/archive/<int:year>/<int:month>.html', views.site),
视图函数
# 带过滤的中级版本
def site(request, name, **kwargs):
# 根据人名查到数据库中,才返回个人站点,如果没有,返回404页面
user = UserInfo.objects.filter(username=name).first()
if user:
# 返回当前这个人所有文章
article_list = Article.objects.all().filter(blog=user.blog)
# 要么根据tag,category或时间过滤
tag = kwargs.get('tag')
category = kwargs.get('category')
year = kwargs.get('year')
month = kwargs.get('month')
if tag:
article_list = article_list.filter(tag__id=tag) # 跨表到tag表,过滤tag的id为传入的id号
elif category:
article_list = article_list.filter(category_id=category) # 跨表到tag表,过滤tag的id为传入的id号
elif year and month:
article_list = article_list.filter(create_time__year=year, create_time__month=month)
category_res = Category.objects.all().filter(blog=user.blog).values('id').annotate(
c=Count('article__id')).values_list('id', 'name', 'c')
tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
'id', 'name', 'c')
date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
print(tag_res)
print(category_res)
print(date_res)
return render(request, 'site.html', locals())
else:
return render(request, '404.html')
左侧标签,分类,归档终极方案(3条路由合一)
路由
# 上面的三个路由改成一个路由
re_path('^(?P<name>\w+)/(?P<type_name>tag|category|archive)/(?P<condition>\d+).html', views.site),
视图函数
# 三条路由合并成一条的版本
def site(request, name, **kwargs):
# 根据人名查到数据库中,才返回个人站点,如果没有,返回404页面
user = UserInfo.objects.filter(username=name).first()
if user:
# 返回当前这个人所有文章
article_list = Article.objects.all().filter(blog=user.blog)
# 要么根据tag,category或时间过滤
type_name = kwargs.get('type_name')
condition = kwargs.get('condition')
if type_name == 'tag':
article_list = article_list.filter(tag__id=condition) # 跨表到tag表,过滤tag的id为传入的id号
elif type_name == 'category':
article_list = article_list.filter(category_id=condition) # 跨表到tag表,过滤tag的id为传入的id号
elif type_name == 'archive':
year = str(condition)[:4]
month = str(condition)[4:]
article_list = article_list.filter(create_time__year=year, create_time__month=month)
category_res = Category.objects.all().filter(blog=user.blog).values('id').annotate(
c=Count('article__id')).values_list('id', 'name', 'c')
tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
'id', 'name', 'c')
date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
print(tag_res)
print(category_res)
print(date_res)
return render(request, 'site.html', locals())
else:
return render(request, '404.html')
模板语言和js语法比较
# 模板语言---》模板---》不是前端页面---》区分开
-django中:模板语法,dtl:django template language(django自己设计,写的)
-flask中:模板语法:jinja2,第三方
-java: jsp 理解为:java web 的模板语言
-php: php <? php语言 >
-xx.html 中写了python代码,这个不是前端页面,叫模板
# js语法
<script>
// 可以在js代码用模板渲染,但是只能用数字,字符串(加引号),列表 不能用元组,字典,对象 test
// 后端模板中得name给js的name
var name ='{{name}}'
// 后端模板中得age给js的age
var age = {{ age }}
var l= {{ test }}
//var d={'name': 'lyf', 'age': 40}
console.log(age)
console.log(name)
console.log(l)
//console.log(d)
$('h2').html(age)
// 把js的变量l,给python的视图函数中得l
</script>
左侧使用inclusion_tag方案实现
# inclusion_tag 是tag的一种
{{ }}
{% %}
# 自定义标签
-第一步:在app中创建包:templatetags
-第二步:在包下新建py文件:如:my_tags.py
-第三步:在py文件中导入
from django import template
-第四步:实例化得到对象
register = template.Library()
-第五步:使用register装饰函数
@register.inclusion_tag('left.html', name='left')
def left(name):
return {} # 字典的数据可以在left.html中使用
-第六步:在想引入inclusion_tag使用---》base.html 的栅格左侧占2个栅格的位置
{% load my_tags %}
{% left name%}
my_tag.py
from django import template
from blog.models import Category, Tag, Article, UserInfo
from django.db.models import Count
from django.db.models.functions import TruncMonth
register = template.Library()
@register.inclusion_tag('left.html', name='left') # 返回html片段,第一个参数是html文件
def left(name):
# user 当前根据用户名查到的用户,需要传入用户名,一定会有user
user = UserInfo.objects.filter(username=name).first()
# 标签id,名字和标签下的文章数
category_res = Category.objects.all().filter(blog=user.blog).values('id').annotate(
c=Count('article__id')).values_list('id', 'name', 'c')
tag_res = Tag.objects.all().filter(blog=user.blog).values('id').annotate(c=Count('article__id')).values_list(
'id', 'name', 'c')
date_res = Article.objects.all().filter(blog=user.blog).annotate(year_month=TruncMonth('create_time')).values(
'year_month').annotate(c=Count('id')).values_list('year_month', 'c')
print(tag_res)
print(category_res)
print(date_res)
return {'tag_res': tag_res, 'category_res': category_res, 'date_res': date_res} # 返回的字典,可以在left.html中使用
left.html
<div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">我的标签</h3>
</div>
<div class="panel-body">
<div>
{% for tag in tag_res %}
<hr>
<a href="/{{ user.username }}/tag/{{ tag.0 }}.html">
<span>{{ tag.1 }}</span>
<span>({{ tag.2 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">随笔分类</h3>
</div>
<div class="panel-body">
<div>
{% for category in category_res %}
<hr>
<a href="/{{ user.username }}/category/{{ category.0 }}.html">
<span>{{ category.1 }}</span>
<span>({{ category.2 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">随笔档案</h3>
</div>
<div class="panel-body">
<div>
{% for data_itme in date_res %}
<hr>
<a href="/{{ user.username }}/archive/{{ data_itme.0|date:'Ym' }}.html">
<span>{{ data_itme.0|date:'Y年m月' }}</span>
<span>({{ data_itme.1 }})</span>
</a>
{% endfor %}
</div>
</div>
</div>
</div>
点赞点踩样式和前后端
# 点赞或点踩流程
-1 用户点击赞标签
-2 发送ajax请求到后端
-3 后端判断一下是是否点过,如果点过了,就返回点过了
-4 如果没有点过,点赞表插入一条记录
-5 在文章表的点赞数+1
-6 如果没有登录,返回让它登录
点赞点踩前端样式
<div id="div_digg">
<div class="diggit up">
<span class="diggnum" id="digg_count">{{ article_detail.up_num }}</span>
</div>
<div class="buryit up">
<span class="burynum" id="bury_count">{{ article_detail.down_num }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips"></div>
</div>
article.css
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 128px;
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: red;
}
点赞点踩前端js代码
// 点赞点踩写成1个事件
$(".up").click(function () {
var up = $(this).hasClass('diggit') // 判断点击的这个标签有没有diggit类,如果有就是true,up是true表示点赞,否则表示点踩
// 先取出点击的span
var span = $(this).children('span')
$.ajax({
url: '/up_and_down/',
type: 'post',
data: {
// 谁给那篇文章点赞或点踩---》谁可以不传,当前登录用户就是点赞人
//article_id:$('h2').attr('name') // 方式1:把文章id,放在某个标签中,使用jq取到id
article_id: '{{ article_detail.id }}',
up_or_down: up,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (data) {
$('#digg_tips').html(data.msg)
if (data.code == 100) {
// 在点击的div内的span的数字要+1
span.html(Number(span.html()) + 1)
}
}
})
})
点赞后端
import json
from django.db.models import F
from django.db import transaction
def up_and_down(request):
res = {'code': 100, 'msg': '点赞成功'}
# 1 判断用户是否登录
if request.user.is_authenticated: # 只要登录了就是当前登录用户,如果没登录就是匿名用户
# 2 取出文件id,用户id,点赞或点踩
article_id = request.POST.get('article_id')
up = json.loads(request.POST.get('up_or_down')) # 坑:up是什么类型?<class 'str'>
# 把str类型的up:'true'--->转成---》python中布尔类型:up:True
# 3 根据是点赞还是点踩向点赞点踩表中存数据
count = UpAndDown.objects.filter(user=request.user, article_id=article_id).count()
if count:
res['code'] = 101
res['msg'] = '您已经点过了'
return JsonResponse(res)
else:
with transaction.atomic(): #开启事务,保证,插入点赞点踩表和文章表增加1 ,要么都成功,要么都失败
UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=up)
# 4 在aritlcle表中点赞或点踩数+1
if up:
Article.objects.filter(pk=article_id).update(up_num=F('up_num')+1)
res['msg'] = '点赞成功'
else:
Article.objects.filter(pk=article_id).update(down_num=F('down_num') + 1)
res['msg'] = '点踩成功'
return JsonResponse(res)
else:
res['code'] = 102
res['msg'] = '您没有登录,请先 <a href="/login/">登录</a>'
return JsonResponse(res)
根评论子评论后端
def article_detail(request, name, pk):
user = UserInfo.objects.filter(username=name).first()
article = Article.objects.filter(pk=pk).first()
# 该文章所有的评论--->就加了这一样
comment_list=Comment.objects.filter(article=article)
if user and article:
return render(request, 'article.html', context={'name': name, 'article_detail': article, 'user': user,'comment_list':comment_list})
else:
return render(request, '404.html')
根评论子评论样式前端
<div>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item">
<p>
<span># {{ forloop.counter }} 楼</span>
<span>{{ comment.create_time|date:'Y-m-d H:i' }}</span>
<span><a href="/{{ comment.user.username }}/">{{ comment.user.username }}</a></span>
</p>
{% if comment.parent_id %}
<p>
@{{ comment.parent.user.username }}
<p>{{ comment.content }}</p>
</p>
{% else %}
<p>
{{ comment.content }}
</p>
{% endif %}
</li>
{% endfor %}
</ul>
</div>
评论功能后端
根评论后端
def comment(request):
res = {'code': 100, 'msg': '评论成功'}
# 1 判断用户是否登录
if request.user.is_authenticated:
# 2 登录了后,取出前端传入的文章id,评论内容,取出当前登录用户
article_id = request.POST.get('article_id')
content = request.POST.get('content')
# 3 存到评论表中
Comment.objects.create(user=request.user, article_id=article_id, content=content)
# 4 在文章表的评论数+1
Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
# 还要返回一些东西:评论内容,评论人名
res['content'] = content
res['username'] = request.user.username
return JsonResponse(res)
else:
res['code'] = 102
res['msg'] = '您没有登录'
return JsonResponse(res)
子评论和根评论后端
def comment(request):
res = {'code': 100, 'msg': '评论成功'}
# 1 判断用户是否登录
if request.user.is_authenticated:
# 2 登录了后,取出前端传入的文章id,评论内容,取出当前登录用户
article_id = request.POST.get('article_id')
content = request.POST.get('content')
# 取出父评论id号
parent_id=request.POST.get('parent_id')
# 3 存到评论表中,子评论和根评论一样的
res_comment=Comment.objects.create(user=request.user, article_id=article_id, content=content,parent_id=parent_id)
# 4 在文章表的评论数+1
Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
# 还要返回一些东西:评论内容,评论人名
res['content'] = content
res['username'] = request.user.username
if parent_id: # 子评论,返回评论的评论人的名字
res['parent_name']=res_comment.parent.user.username
return JsonResponse(res)
else:
res['code'] = 102
res['msg'] = '您没有登录'
return JsonResponse(res)
评论功能前端,ajax显示
根评论前端,ajax显示
# javascript:ECMAScript 13+bom+dom
# ECMAScript 最新是13
# es6的语法 字符串的 `` 用来做字符串格式化
'我的名字:'+name+'我的年龄:'+age
`我的名字:${name}我的年龄:${age}`
// 评论事件
$("#btn_submit").click(function () {
$.ajax({
url: '/comment/',
type: 'post',
data: {
article_id: '{{ article_detail.id }}',
content: $("#id_content").val(),
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (data) {
console.log(data)
$("#id_content").val('')
// 把评论内容,拼接到前端 jq 的dom操作
var username=data.username
var res_content=data.content
// 字符串 '' " " ``:es6的语法
var s=`
<li class="list-group-item">
<p>
<span class='fa fa-address-card-o'></span>
<span>${username}:</span>
</p>
<p>${res_content}</p>
</li>
`
// 把格式化完成的字符串拼接到评论后面
$('.list-group').append(s)
}
})
})
子评论前端ajax显示
$("#btn_submit").click(function () {
var content = $("#id_content").val()
if (parent_id) {// 如果是子评论,把输入框中得 @某人截断不要了
var i = content.indexOf('\n') // 返回括号内字符的位置
content = content.slice(i + 1) //把字符串从回车换行后截取,只要后面的
}
$.ajax({
url: '/comment/',
type: 'post',
data: {
article_id: '{{ article_detail.id }}',
content: content,
parent_id: parent_id,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (data) {
console.log(data)
// 把输入框内容清空
$("#id_content").val('')
if (data.code == 100) {
// 把评论内容,拼接到前端 jq 的dom操作
// 取出后端返回的用户名,评论内容
var username = data.username
var res_content = data.content
// 字符串 '' " " ``:es6的语法,字符串格式化后,拼接到评论列表后端
var s = ''
if (parent_id) {
var parent_name = data.parent_name
s = `
<li class="list-group-item">
<p>
<span class='fa fa-address-card-o'></span>
<span>${username}:</span>
</p>
<p>@${parent_name}</p>
<p>${res_content}</p>
</li>
`
} else {
s = `
<li class="list-group-item">
<p>
<span class='fa fa-address-card-o'></span>
<span>${username}:</span>
</p>
<p>${res_content}</p>
</li>
`
}
// 把格式化完成的字符串拼接到评论后面
$('.list-group').append(s)
}else{
$('.error').html(data.msg)
}
},
error:function (data){ // 当次请求失败了
$('.error').html("小伙子,出错了")
}
})
})
$('.replay').click(function () {
var name = $(this).attr('username')
//alert(name)
// 把用户名写在输入框中
$('#id_content').val('@' + name + '\n').focus()
// 父评论id号,全局变量
parent_id = $(this).attr('comment')
})
后台管理页面
base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
{% block title %}
{% endblock %}
</title>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<script src="/static/jquery-3.3.1/jquery-3.3.1.min.js"></script>
<script src="/static/bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/static/font-awesome-4.7.0/css/font-awesome.min.css">
{% block link %}
{% endblock %}
</head>
<body>
<div class="main">
<div class="header">
<nav class="navbar navbar-default common navbar-inverse">
<div class="container-fluid">
<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="#">博客园-后台管理</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">
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne"
aria-expanded="true" aria-controls="collapseOne">
文章操作
</a>
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse in" role="tabpanel"
aria-labelledby="headingOne">
<div class="panel-body">
<ul class="nav">
<li><a href="">新增文章</a></li>
<li><a href="">修改文章</a></li>
<li><a href="">修改头像</a></li>
</ul>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingTwo">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion"
href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
其他操作
</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="headingTwo">
<div class="panel-body">
<ul class="nav">
<li><a href="">修改头像</a></li>
<li><a href="">修改密码</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-10">
<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">文章</a></li>
<li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">随笔</a>
</li>
<li role="presentation"><a href="#messages" aria-controls="messages" role="tab"
data-toggle="tab">评论</a></li>
<li role="presentation"><a href="#settings" aria-controls="settings" role="tab"
data-toggle="tab">相册</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="home">
{% block article %}
{% endblock %}
</div>
<div role="tabpanel" class="tab-pane" id="profile">随笔内容</div>
<div role="tabpanel" class="tab-pane" id="messages">评论</div>
<div role="tabpanel" class="tab-pane" id="settings">相册</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
{% block js %}
{% endblock %}
</html>
index.html
{% extends 'backend/base.html' %}
{% block title %}
后台管理
{% endblock %}
{% block article %}
<table class="table table-hover table-striped">
<thead>
<tr>
<th>文章id</th>
<th>文章名</th>
<th>发布时间</th>
<th>评论数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article in article_list %}
<tr>
<th scope="row">{{ article.id }}</th>
<td>{{ article.title }}</td>
<td>{{ article.create_time|date:'Y-m-d H-i' }}</td>
<td>{{ article.comment_num }}</td>
<td><a href="">删除</a></td>
<td><a href="">修改</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
backend视图函数
from django.contrib.auth.decorators import login_required
@login_required(login_url='/login/')
def backend(request):
article_list=Article.objects.filter(blog=request.user.blog)
return render(request,'backend/index.html',context={'article_list':article_list})
删除文章
后端
@login_required(login_url='/login/')
def delete(request):
pk = request.GET.get('id')
Article.objects.filter(pk=pk).delete()
return redirect('/backend/')
前端
<td><a href="/delete/?id={{ article.id }}">删除</a></td>
新增文章功能
前端
{% extends 'backend/base.html' %}
{% block title %}
新增文章
{% endblock %}
{% block link %}
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
{% endblock %}
{% block article %}
<div>
<h2>新增文章</h2>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="">标题</label>
<input type="text" name="title" class="form-control">
</div>
<div class="form-group">
<label for="">内容</label>
<textarea name="content" id="editor_id" cols="300" rows="20"></textarea>
</div>
<div class="form-group">
<label for="">分类</label>
<select class="form-control" name="category">
{% for category in category_list %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="">标签</label>
<select multiple class="form-control" name="tag">
{% for tag in tag_list %}
<option value="{{ tag.id }}">{{ tag.name }}</option>
{% endfor %}
</select>
</div>
<div class="text-center">
<input type="submit" value="提交" class="btn btn-success">
</div>
</form>
</div>
{% endblock %}
{% block js %}
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id',
{
width: '100%',
height: '500px',
resizeType: 0,
uploadJson: '/put_img/',
filePostName: 'myfile',
//额外带的参数
extraFileUploadParams: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}
});
});
</script>
{% endblock %}
后端
@login_required(login_url='/login/')
def add_article(request):
if request.method == 'GET':
# 当前博客下所有的分类
category_list = Category.objects.filter(blog=request.user.blog)
# 当前博客下所有的标签
tag_list = Tag.objects.filter(blog=request.user.blog)
return render(request, 'backend/add_article.html', locals())
else:
# 新增文章
# 1 文章标题,文章摘要内容中截取出摘要,文章内容,blog:当前登录用户,category,tag存中间表
title = request.POST.get('title')
content = request.POST.get('content')
# BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
soup = BeautifulSoup(content, 'html.parser')
desc = soup.text.replace('/n', '').replace('/r', '')[:70] # 把html的内容也截取出来,只截取文字
# 剔除script标签
script_list = soup.findAll('script') # 搜索到html中所有的script标签
for script in script_list:
script.decompose() # 把搜到的script标签一个个删除
category = request.POST.get('category')
tags = request.POST.getlist('tag')
article = Article.objects.create(title=title, desc=desc, content=str(soup), blog=request.user.blog,
category_id=category)
# 存标签,在第三张表中
article.tag.add(*tags)
return redirect('/backend/')
处理xss攻击
# xss攻击:跨站脚本攻击
-在内容中存储了script的脚本,前端渲染的时候,使用了safe,渲染html内容,导致如果存在script脚本,脚本就会执行---》解决方案,在存储的时候,就要把恶意脚本删除
# 后台文章内容存储的时候,要删除script标签
-我们文章中代码里插入的script标签不要删除----》富文本编辑器已经处理了这种标签了
-怕作者直接在html中加入script,这种是危险的
# beautifulsoup4 爬虫会用,解析,修改html文档
-pip3 install beautifulsoup4
-删除script标签
soup = BeautifulSoup(content, 'html.parser')
script_list=soup.findAll('script') # 搜索到html中所有的script标签
for script in script_list:
script.decompose() # 把搜到的script标签一个个删除
def add_article(request):
if request.method == 'GET':
# 当前博客下所有的分类
category_list = Category.objects.filter(blog=request.user.blog)
# 当前博客下所有的标签
tag_list = Tag.objects.filter(blog=request.user.blog)
return render(request, 'backend/add_article.html', locals())
else:
# 新增文章
# 1 文章标题,文章摘要内容中截取出摘要,文章内容,blog:当前登录用户,category,tag存中间表
title = request.POST.get('title')
content = request.POST.get('content')
# BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
soup = BeautifulSoup(content, 'html.parser')
desc = soup.text.replace('/n', '').replace('/r', '')[:70] # 把html的内容也截取出来,只截取文字
# 剔除script标签
script_list = soup.findAll('script') # 搜索到html中所有的script标签
for script in script_list:
script.decompose() # 把搜到的script标签一个个删除
category = request.POST.get('category')
tags = request.POST.getlist('tag')
article = Article.objects.create(title=title, desc=desc, content=str(soup), blog=request.user.blog,
category_id=category)
# 存标签,在第三张表中
article.tag.add(*tags)
return redirect('/backend/')
富文本编辑器上传图片
前端
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id',
{
width: '100%',
height: '500px',
resizeType: 0,
uploadJson: '/put_img/',
filePostName: 'myfile',
//额外带的参数
extraFileUploadParams: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}
});
});
</script>
后端
def put_img(request):
# img=request.FILES.get('imgFile')
img = request.FILES.get('myfile')
img_name = os.path.join(settings.MEDIA_ROOT, 'upload', img.name)
with open(img_name, 'wb') as f:
for line in img:
f.write(line)
return JsonResponse(
{
"error": 0,
"url": "http://127.0.0.1:8000/media/upload/"+img.name
})
首页用户信息显示,退出登录
前端 index.html
{% if request.user.is_authenticated %}
<ul class="nav navbar-nav navbar-right">
<li><a href="#">{{ request.user.username }}</a></li>
<li><a href="#">头像</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="/backend/">后台管理</a></li>
<li><a href="#">修改头像</a></li>
<li role="separator" class="divider"></li>
<li><a href="/logout/">退出</a></li>
</ul>
</li>
</ul>
{% else %}
<ul class="nav navbar-nav navbar-right">
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
</ul>
{% endif %}
退出后台
def logout(request):
# 如何就算退出了? 删除session,和cookie
out(request)
return redirect('/')
修改头像
前端update_head.html
{% extends 'backend/base.html' %}
{% block title %}
修改头像
{% endblock %}
{% block article %}
<div style="padding: 20px">
{# 一定要注意,如果是form表单提交文件指定编码格式#}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label for="">原头像</label>
<img src="/media/{{ request.user.avatar }}" alt="" width="80px" height="80px">
</div>
<hr>
<div class="form-group">
<label for="id_file">新头像
<img src="/static/img/default.png" alt="" height="80px" width="80px" style="margin-left: 10px"
id="id_img">
</label>
<input type="file" name='head_file' id="id_file" accept="image/*" style="display: none">
</div>
<input type="submit" value="提交" class="btn btn-default">
</form>
</div>
{% endblock %}
{% block js %}
<script>
$("#id_file").change(function () {
// 把当前图片,放到img标签中
// 把图片地址放到img标签上,标签就显示了图片
//$("#id_img")[0].src='https://tva1.sinaimg.cn/large/00831rSTly1gd1u0jw182j30u00u043b.jpg'
// 1 id_file这个标签的图片读出来,借助于文件阅读器,js提供的一个类
var reader = new FileReader()
// 2 拿到文件对象,赋值给一个变量
var file = $("#id_file")[0].files[0]
// 3 把文件读到文件阅读器中
reader.readAsDataURL(file)
// 4 等读完后,把文件阅读器的内容写到img标签上
//$("#id_img")[0].src=reader.result
reader.onload = function () {
//$("#id_img")[0].src=reader.result
$('#id_img').attr('src', reader.result)
}
})
</script>
{% endblock %}
后端
def update_head(request):
if request.method == 'GET':
return render(request, 'backend/update_head.html')
else:
head_file = request.FILES.get('head_file')
# 方式一:常用做饭,修改文件
request.user.avatar = head_file
request.user.save()
# 方式二:有问题,不会存前面avatar的路径
# UserInfo.objects.filter(pk=request.user.id).update(avatar=head_file)
return redirect('/backend/')
修改密码
前端 update_pwd.html
{% extends 'backend/base.html' %}
{% block title %}
修改密码
{% endblock %}
{% block article %}
<div style="padding: 20px">
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="">原密码</label>
<input type="text" class="form-control" name="old_pwd">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="text" class="form-control" name="new_pwd">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="text" class="form-control" name="re_new_pwd">
</div>
<input type="submit" value="提交" class="btn btn-default"><span style="color: red">{{ error }}</span>
</form>
</div>
{% endblock %}
后端
def update_pwd(request):
if request.method == 'GET':
return render(request, 'backend/update_pwd.html')
else:
old_pwd = request.POST.get('old_pwd')
new_pwd = request.POST.get('new_pwd')
re_new_pwd = request.POST.get('re_new_pwd')
# 校验原密码是否正确
if request.user.check_password(old_pwd):
if new_pwd == re_new_pwd:
request.user.set_password(new_pwd)
request.user.save() # 不要忘了保存
# 密码修改成功后推出
logout(request)
return redirect('/')
else:
return render(request, 'backend/update_pwd.html', context={'error': '两次密码不一致'})
else:
return render(request, 'backend/update_pwd.html', context={'error': '原密码输入错误'})
修改文章
前端update_article.html
{% extends 'backend/base.html' %}
{% block title %}
修改文章
{% endblock %}
{% block link %}
<script charset="utf-8" src="/static/kindeditor/kindeditor-all-min.js"></script>
<script charset="utf-8" src="/static/kindeditor/lang/zh-CN.js"></script>
{% endblock %}
{% block article %}
<div>
<h2>修改文章</h2>
<form action="" method="post">
{% csrf_token %}
<input type="hidden" name="id" value="{{ article.id }}">
<div class="form-group">
<label for="">标题</label>
<input type="text" name="title" class="form-control" value="{{ article.title }}">
</div>
<div class="form-group">
<label for="">内容</label>
<textarea name="content" id="editor_id" cols="300" rows="20">
{{ article.content }}
</textarea>
</div>
<div class="form-group">
<label for="">分类</label>
<select class="form-control" name="category">
{% for category in category_list %}
{% if article.category_id == category.id %}
<option value="{{ category.id }}" selected>{{ category.name }}</option>
{% else %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="">标签</label>
<select multiple class="form-control" name="tag">
{% for tag in tag_list %}
{% if tag in article.tag.all %}
<option value="{{ tag.id }}" selected>{{ tag.name }}</option>
{% else %}
<option value="{{ tag.id }}">{{ tag.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="text-center">
<input type="submit" value="提交" class="btn btn-success">
</div>
</form>
</div>
{% endblock %}
{% block js %}
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id',
{
width: '100%',
height: '500px',
resizeType: 0,
// 上传图片相关
uploadJson: '/put_img/',
filePostName: 'myfile',
//额外带的参数
extraFileUploadParams: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
}
});
});
</script>
{% endblock %}
后端
@login_required(login_url='/login/')
def update_article(request):
if request.method == 'GET':
article_id = request.GET.get('id')
article = Article.objects.filter(pk=article_id).first()
# 当前博客下所有的分类
category_list = Category.objects.filter(blog=request.user.blog)
# 当前博客下所有的标签
tag_list = Tag.objects.filter(blog=request.user.blog)
return render(request, 'backend/update_article.html',
context={'article': article, 'category_list': category_list, 'tag_list': tag_list})
else:
pk = request.POST.get('id')
# 1 文章标题,文章摘要内容中截取出摘要,文章内容,blog:当前登录用户,category,tag存中间表
title = request.POST.get('title')
content = request.POST.get('content')
# BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
soup = BeautifulSoup(content, 'html.parser')
desc = soup.text.replace('/n', '').replace('/r', '')[:70] + '...' # 把html的内容也截取出来,只截取文字
# 剔除script标签
script_list = soup.find_all('script') # 搜索到html中所有的script标签
for script in script_list:
script.decompose() # 把搜到的script标签一个个删除
category = request.POST.get('category')
tags = request.POST.getlist('tag')
article = Article.objects.filter(pk=pk) # 返回的是queryset对象
article.update(title=title, desc=desc, content=str(soup), blog=request.user.blog,
category_id=category) # queryset对象继续调用update
# 存标签,在第三张表中,自动创建的 手动创建的add就用不了了
article.first().tag.set(tags) # [1,3,4] 放对象列表和放id列表都可以
# article.first().tag.clear()
# article.first().tag.add(*tags)
return redirect('/backend/')
django发送邮件
# python 代码就可以发送邮件 smtplib 内置模块,可以发送邮件
# 你要发邮件要有个发件箱:就是你的邮箱(qq邮箱,163邮箱。。。)
-配置我们的邮箱,让它可以用python代码发送邮件
-以qq邮箱为例:开启smtp服务:IMAP/SMTP服务
fjkmdxelrgfzbgae
# django发送右键---》本质是封装了 smtplib 内置模块
-在django中
from django.core.mail import send_mail
res1 = send_mail('邮件标题', '邮件内容', settings.EMAIL_HOST_USER, ["2247675450@qq.com"])
BBS项目大总结
# 写了什么
-注册功能
-1 forms表单的使用:字段类,字段属性
- 作用:数据校验,渲染前端页面,渲染前端页面错误信息
-2 for循环form对象渲染页面
-3 头像上传:头像动态显示
-4 ajax提交注册信息
-ajax上传文件:FormData form表单上传文件:指定编码
-form表单中:var data = $("#id_form").serializeArray()
-jq的循环$.each(data,function(i,v){})
-登录成功: location.href = '/login/'
-错误:渲染再页面上
-定时任务:3s后清空错误信息
-5 后端
-取出前端传进的数据,校验通过后:剔除re_pwd,把头像给avatar
-create_user创建用户
-登录
-布局使用bootstrap实现的
-图片验证码:自己写的
-保存在session中
-前端图片验证码的刷新
-只要src对应的路径发生变化,就会重新加载
-ajax提交登录信息
-用户名,密码和验证码到后端
-登录接口后端
-authenticate 验证用户名密码是否正确(密码是加密的)
-登录成功,调用auth_login(request, user)---》再session中写入数据,cookie中写入
-首页
-布局
-bootstrap的导航条
-左中右的栅格布局
-中间位置上下:上面是轮播图,下面是文章列表
-文章列表:模板语法的for循环 ,media媒体组
-优化字段:点赞,点踩,评论数量
-后端接口
-查询所有文章(缺分页)
-轮播图内容:模板语法写的====》使用ajax加载轮播图
-登录成功显示用户名字,退出
-退出接口
-没有登录显示登录注册
-个人站点
-路由设计:一定要放在最下面
-布局
-base.html:用来做继承
-不同用户顶部颜色不一样,不同人博客样式不一样
-右侧文章:把首页文章复制过来的,最后一行布局推到右侧去了
-左侧:
-标签,分类,随笔档案的渲染---》后端接口查询出
-后端:3个分组查询,TruncMonth后再分组
-前端:通过后端查出来的渲染
-使用inclusion_tag重写了左侧栏的渲染
-左侧的过滤
-3条路由
-汇总成1条路由
-文章详情
-布局:base.html
-文章真正的渲染 | safe 文章内容是html
-点赞点踩的布局:复制粘贴过来的
-点赞点踩的后端
-如果点过了就不能点了
-没登录不能点
-事务
-评论
-render显示根和子评论
-评论ajax提交根评论
-评论的ajax提交子评论
-根评论和子评论的ajax显示
-后台管理
-布局
-文章列表的显示:表格
-删除文章
-新增文章:富文本编辑器,标签,分类的渲染
-处理xss攻击
-修改文章
-修改头像
-修改密码
-django发送邮件
# 可以扩展什么
-首页轮播图ajax加载
-标签列表,删除,修改,新增
-新增标签:Tag.objects.create(name=前端传进来的,blog=request.user.blog)
-输入密码错误3次,锁定账号,解锁账号
-修改密码:之前用过的密码不能用了
-异地登录,发邮件提醒
-ip地址根之前不同
-文章存到草稿
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库