BBS项目:
创建数据库bbs
create database bbs
表分析
一共需要创建七张表
-用户表(基于auth模块的user表扩写)
-博客表(跟用户表一对一关系)
-分类表(和博客表一对多、和文章表一对多)
-标签表(和博客表一对多、和文章表多对多)
-点赞点踩表(和用户表一对多、和文章表一对多)
-评论表(和用户表一对多,和文章表一对多)
-文章表(和博客表一对多)
安装django 2.2.2版本
pip3 install django==2.2.2
使用pycharm创建django项目
配置setting.py
TEMPLATES = {
"DIRS":[os.path.joi(BASE_DIR, "templates")]
}
配置语言环境
LANGUAGE = 'zh-hans' # 语言汉化
TIME-ZONE = 'Asia/Shanghai' # 时区使用上海时区
USE_I18N = True
USE_L10N = True
USE_TZ = False
配置数据库
DATABASES = {
'default': {
'ENGINE':'django.db.backends.mysql',
'NAME': 'bbs',
'HOST': '127.0.0.1',
'PORT': 3306,
'USER': 'root',
'PASSWORD':'password'
}
}
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser): # 继承AbstractUser表 只用写auth表中没有的字段
phone = models.CharField(max_length=32, null=True, verbose_name='用户手机号')
# upload_to是文件保存在什么路径
icon = models.FileField(upload_to='icon/', default='icon/default.png', null=True, verbose_name='用户头像')
# 用户表和博客表一对一
blog = models.OneToOneField(to='Blog', on_delete=models.CASCADE, null=True)
class Blog(models.Model):
title = models.CharField(max_length=32, null=True, verbose_name='主标题')
site_title = models.CharField(max_length=32, null=True, verbose_name='副标题')
site_style = models.CharField(max_length=64, null=True, verbose_name='站点样式')
class Tag(models.Model):
name = models.CharField(max_length=32, verbose_name='标签名', null=True)
# 标签和博客是一对多 一个博客有多个标签
blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
class Classify(models.Model):
name = models.CharField(max_length=32, verbose_name='分类名')
# 分类和博客是一对多关系 一个博客有多个分类
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='文章内容')
create_time = models.DateTimeField(auto_now_add=True) # 第一次创建时自动添加时间
# 文章和分类表是一对多 一个分类有多篇文章
classify = models.ForeignKey(to='Classify', on_delete=models.CASCADE)
# 文章和标签是多对多关系 自动创建第三张表
tag = models.ManyToManyField(to='Tag')
# 文章和博客是一对多关系 一个博客对应多篇文章
blog = models.ForeignKey(to='Blog', on_delete=models.CASCADE)
class UpAndDown(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='点赞点踩时间')
# 和用户表是一对多关系 一个用户可以有多条点赞点踩记录
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
# 和文章也是一对多
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
# 1代表点赞 0代表点踩
is_up = models.BooleanField(verbose_name='是否点赞')
class Comment(models.Model):
content = models.CharField(max_length=64, verbose_name='评论内容')
create_time = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE, null=True)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE, null=True)
# 自关联字段 只能存已有评论的主键值
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
# 自关联的其他方式
# parent = models.ForeignKey(to='Comment', on_delete=models.CASCADE)
# parent = models.IntegerField(null=Ture)
终端执行数据库迁移命令
python38 manage.py makemigretions
python38 manage.py migrater
没有安装mysqlclient会报错
import pymysql
pymysql.install_as_MySQLdb()
pip3 instasll mysqlclient
知识点
on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。
models.CASCADE
删除关联数据,与之关联也删除
models.DO_NOTHING
删除关联数据,引发错误IntegrityError
models.PROTECT
删除关联数据,引发错误ProtectedError
models.SET_NULL
删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空)
models.SET_DEFAULT
删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值)
OneToOneField就是ForeignKey + unique
def __init__(self, to, on_delete, to_field=None, **kwargs):
kwargs['unique'] = True # 继承ForeignKey 自动加上unique=True
super().__init__(to, on_delete, to_field=to_field, **kwargs)
字段类的属性(字段类型)
max_length (最大长度)
null=True (可以为空)
default=‘’ (设置默认值)
unique=True (数据值必须唯一)
db_index=True (设置索引)
verbose_name=‘’ (注释)
db_constraint=False (数据约束 放在ForeignKey中,不建立外键关联 可以使用正反向查询 可能存在脏数据 可在代码层面进行限制)
ManyToManyField
OneToOneField,ForeignKey,ManyToManyField
在根目录下创建blog_forms.py文件
from django import forms
from django.forms import widgets
from blog.models import UserInfo
from django.core.exceptions import ValidationError # 合法性错误
class User(forms.Form):
# 用户名 密码 确认密码 邮箱
username = forms.CharField(max_length=8, min_length=3, label='用户名', required=True,
error_messages={'max_length': '用户名最多只能输入8位',
'min_length': '用户名最少输入3位',
'required': '用户名必须填'
},
# 添加bootstr样式
widget=widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
error_messages={
'max_length': '密码最长16位',
'min_length': '密码最短8位',
'required': '密码不能为空',
},
widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
re_password = forms.CharField(max_length=16, min_length=8, required=True, label='密码',
error_messages={
'max_length': '密码最长16位',
'min_length': '密码最短8位',
'required': '密码不能为空',
},
widget=widgets.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮箱地址', widget=widgets.EmailInput(attrs={'class': 'form-control'}))
# 局部钩子 校验用户名是否存在
def clean_username(self):
name = self.cleaned_data.get('username')
if UserInfo.objects.filter(username=name).first():
# 用户已存在
raise ValidationError('用户名已存在') # 校验错误抛出异常
else:
return name
# 局部钩子 校验用户名是否存在
# def clean_username(self):
# username = self.cleaned_data.get('username')
# try:
# UserInfo.objects.get(username=username)
# print(UserInfo.objects.get(username=username), type(UserInfo.objects.get(username=username)))
# raise ValidationError('用户名已存在')
# except Exception:
# return username
# 全局钩子 校验两次输入密码是否一致
def clean(self):
pwd = self.cleaned_data.get('password')
re_pwd = self.cleaned_data.get('re_password')
if pwd != re_pwd:
raise ValidationError('两次密码不一致') # 主动抛出合法性错误
else:
return self.cleaned_data
在项目同名文件夹下的urls.py中配置路由
from django.contrib import admin
from django.urls import path
from blog import views
urlpatterns = [
path('admin/', admin.site.urls),
path('register/', views.register),
]
views.py
from django.shortcuts import render
from blog.blog_forms import User
def register(request):
form_obj = User()
if request.method == 'GET': # 当请求为get时返回注册界面,并返回forms组件对象进行数据校验
return render(request, 'register.html', {'form_obj': form_obj})
register.html
需要先配置静态文件
-在setting.py中
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static')
]
-把bootstrap和jquery导入模板中
<body>
<div class="container-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center text-info">注册功能h1>
<form action="" id="id_form">
{% csrf_token %}
{% for foo in form_obj %}
<div class="form-group">
<label for="{{ foo.id_for_label }}">{{ foo.label }}label>
{{ foo }}
<span class="pull-right text-danger">span>
div>
{% endfor %}
<div class="form-group">
<label for="id_file">头像
<img src="/static/default.png" alt="" height="100px" width="100px" style="margin-right: 20px"
id="id_img">
<input type="file" id="id_file" accept="image/*" style="display: none">
label>
div>
<div class="form-group text-center">
<input type="button" value="注册" class="btn btn-success" id="id_submit">
<span class="text-danger error">span>
div>
form>
div>
div>
div>
body>
<script>
// 头像动态显示
$('#id_file').change(function () {
// 将上传的头像展示到img标签内 修改img标签内的src参数
// 读出图片文件 借助于文件阅读器
let reader = new FileReader()
// 拿到文件对象
let file = $('#id_file')[0].files[0]
// 将文件对象读到文件阅读器中
reader.readAsDataURL(file)
// 文件加载完后修改img标签的src参数
reader.onload = function () {
// $('#id_img')[0].src=reader.result
$('#id_img').attr('src', reader.result) # jquery对象方法
}
})
</script>
// 发送ajax请求
$('#id_submit').click(function () {
let data = new FormData // 可以传递文件数据
// 方式一:根据id获取标签数据添加至data中
// data.append('username', $('#id_username').val())
// data.append('password', $('#id_password').val())
// data.append('re_password', $('#id_re_password').val())
// data.append('email', $('#id_email').val())
// data.append('icon', $('#id_file')[0].files[0])
// data.append('csrfmiddlewaretoken', $("[name='csrfmiddlewaretoken']").val())
// ...发送ajax请求
// 方式二:利用form组件批量处理
let data_arr = $('#id_form').serializeArray() // 序列化数组
console.log(data_arr) // 是一个数组套对象 对象中k是name v是value 自动添加csrf
// 使用for循环把数据添加到data对象中
$.each(data_arr, function (i, v) {
console.log("index:",i)
console.log("value:", v)
console.log("-----------------------")
data.append(v.name, v.value)
})
// 文件需要单独放入
data.append('icon', $('#id_file')[0].files[0])
// 使用ajax发送请求
$.ajax({
url: '/register/',
type: 'post',
data: data,
processData: false,
contentType: false,
success: function (data) {
}
打印结果
现在后端可以收到数据 继续写后端
views.py
def register(request):
form_obj = User()
if request.method == 'GET':
return render(request, 'register.html', {'form_obj': form_obj})
else: # 当发送post请求
res = {'code': 100, 'msg': '注册成功'}
forms_obj = User(data=request.POST) # forms组件检验
if forms_obj.is_valid(): # 如果数据全部合法
register_data = forms_obj.cleaned_data # 拿出所有的合法数据
register_data.pop('re_password') # 弹出二次输入密码 因为用户表中不需要改字段
if request.FILES.get('icon'): # 判断是否上传了图片文件
register_data['icon'] = request.FILES.get('icon') # 上传了的话就添加进去
# 一定要用create_user 密码是密文 后面才可以使用auth模块的功能
UserInfo.objects.create_user(**register_data) # 将register_data打散保存至数据库
return JsonResponse(res) # 注册成功返回信息
else: # 弱国数据不是全部合法
res['code'] = 101
res['msg'] = '注册失败'
res['errors'] = forms_obj.errors # 返回错误信息
return JsonResponse(res)
前端ajax可以接受到后端返回的json字符串
$.ajax({
url: '/register/',
type: 'post',
data: data,
processData: false,
contentType: false,
success: function (data) {
console.log(data)
if (data.code === 100) {
// 注册成功跳转至登录界面
location.href = '/login/'
} else {
// 在前端渲染出错误信息
console.log(data)
$.each(data.errors, function (k, v) {// for循环错误字典
if (k === '__all__') {
// 全局钩子错误 两次密码不一致
$('.error').html(data.errors['__all__'][0])
} else {
// 其他错误找到相应的input框后的span标签渲染 父类标签加上has-error属性变红
$('#id_' + k).next().html(v[0]).parent().addClass('has-error')
}
})
}
}
})
打印结果
此时的错误提示信息不会消失 需要绑定一个定时任务
// 定时任务 渲染的错误信息三秒后清除
setTimeout(function () {
// 把所有的span标签的内容清除 父类中的属性has-error去除
$('.text-danger').html('').parent().removeClass('has-error')
}, 3000)
需求:当用户输入用户名后鼠标离开用户名框,校验用户名是否存在且不能刷新页面
前端
<script>
// 后端ajax校验用户名是否存在
// 前端使用get请求传入用户名
// 绑定一个失去焦点事件
$('#id_username').blur(function () {
$.ajax({
url: '/check_name/?name=' + $('#id_username').val(),
type: 'get',
success: function (data) {
if (data.code === 110) {// 当用户名存在 添加提示信息
$('#id_username').next().html(data.msg)
}else {// 当用户不存在时清除提示信息
$('#id_username').next().html('')
}
}
})
})
</script>
后端
urls.py
path('check_name/', views.check_name),
views.py
def check_name(request):
# print(request.GET)
res = {'msg': '用户已存在', 'code': 110}
name = request.GET.get('name')
obj = UserInfo.objects.filter(username=name).first()
if obj:
return JsonResponse(res)
else:
res['code'] = 100
res['msg'] = '用户不存在'
return JsonResponse(res)
注册成功跳转至/login/,创建login.html。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script src="/static/jQuery.js">script>
<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-fluid">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h1 class="text-center text-info">登录功能h1>
<form action="" id="id_form" method="post">
{% 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="row">
<div class="col-md-6 form-group">
<label for="id_code">验证码label>
<input type="text" id="id_code" class="form-control" name="code">
div>
<div class="col-md-6">
<img src="/get_code/" alt="" id="id_img" width="350px" height="50px">
div>
div>
<div class="form-group">
<input type="button" value="登录" class="btn btn-block btn-danger" id="id_submit">
<div class="text-center">
<span class="text-danger error">span>
div>
div>
form>
<script>
// 点击验证码图片刷新验证码
$('#id_img').click(function () {
let time = new Date().getTime()
console.log(time)
// 再次获取随机验证码图片
$('#id_img')[0].src = '/get_code/?t=' + time
})
// 提交ajax
$('#id_submit').click(function () {
// 将form表单的input标签数据序列化成数组套对象 name value
dataArray = $('#id_form').serializeArray()
$.ajax({
url: '/login/',
type: 'post',
data: dataArray,
success: function (data) {
console.log(data)
if(data.code===100){
location.href = '/'
}else {
$('.error').html(data.msg)
}
}
})
})
// 定时器任务 自动关闭错误提示信息
let test = function () {
$('.error').html('')
}
// 可重复关闭
timer = setInterval(test, 2000)
//60秒后关闭循环定时任务
setTimeout(function () {
clearTimeout(timer)
},60*1000)
script>
div>
div>
div>
body>
html>
验证码:字母数字共五位
views.py
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
import random
def get_code(request):
# 1 生成一张图片 pillow模块
img = Image.new('RGB', (350, 50), color=(255, 255, 255))
# 2 生成一个画图对象 将img传入
draw = ImageDraw.Draw(img)
# 3 生成字体对象
font = ImageFont.truetype(font='./static/font/1641263938811335.ttf', size=50)
# 4 生成随机字符串
ran_str = ''
for i in range(5):
ran_num = str(random.randint(0, 9))
ran_upper = chr(random.randint(65,90))
# 去除I和L
while (ran_upper == 'L' or ran_upper == 'I'):
ran_upper = chr(random.randint(65,90))
ran_lower = chr(random.randint(97, 122))
# 去除i和l
while (ran_lower == i or ran_lower == l):
ran_lower = chr(random.randint(97, 122))
res = random.choice([ran_num , ran_upper, ran_lower])
# 将生成的随机字符画到图片中
# fill=get_color 字体颜色也随机
draw.text(xy=(10 + i * 60, 0), text=res, font=font, fill=get_color())
# 5 画线
for i in range(10):
draw.line([(random.randint(0, 350), random.randint(0, 50)), (random.randint(0, 350), random.randint(0, 50))],
fill=get_color()) # 起点和终点
# 6 画点
for i in range(100):
draw.point((random.randint(0, 350), random.randint(0, 50)), fill=get_color())
# 7 将图片保存在内存中 BytesIo模块 并返回给前端
byte_io = BytesIo()
img.save(fp=byte_io, format='png')
# 怎样校验前端传过来的验证码?
# 可以存在session表中 前端访问返回给前端 前端再次访问携带session 后端取出data进行校验
request.session['code'] = res
return HttpResponse(byte_io.getvalue())
def get_color():
x, y = 0, 255
return (random.randint(x, y), random.randint(x, y), random.randint(x, y))
上面是自定义的图片验证码,也可以使用第三方模块,比如gvcode模块
from gvcode import VFCode
"""
使用方法:
vc = VFCode(
width=200, # 图片宽度
height=80, # 图片高度
fontsize=50, # 字体尺寸
font_color_values=[
'#ffffff',
'#000000',
'#3e3e3e',
'#ff1107',
'#1bff46',
'#ffbf13',
'#235aff'
], # 字体颜色值
font_background_value='#ffffff', # 背景颜色值
draw_dots=False, # 是否画干扰点
dots_width=1, # 干扰点宽度
draw_lines=True, # 是否画干扰线
lines_width=3, # 干扰线宽度
mask=False, # 是否使用磨砂效果
font='arial.ttf' # 字体 内置可选字体 arial.ttf calibri.ttf simsun.ttc
)
# 验证码类型
# 自定义验证码
# vc.generate('abcd')
# 数字验证码(默认5位)
# vc.generate_digit()
# vc.generate_digit(4)
# 字母验证码(默认5位)
# vc.generate_alpha()
# vc.generate_alpha(5)
# 数字字母混合验证码(默认5位)
# vc.generate_mix()
# vc.generate_mix(6)
# 数字加减验证码(默认加法)
vc.generate_op()
# 数字加减验证码(加法)
# vc.generate_op('+')
# 数字加减验证码(减法)
# vc.generate_op('-')
# 图片字节码
# print(vc.get_img_bytes())
# 图片base64编码
print(vc.get_img_base64())
# 保存图片
vc.save()
"""
def get_code(request):
vc = VFCode(width=350, height=50)
vc.generate_mix()
# vc.generate_op()
print(vc.get_img_base64()[0])
byte_io = BytesIO()
vc.save(byte_io, fm='png')
request.session['code'] = vc.get_img_base64()[0]
return HttpResponse(byte_io.getvalue())
login.html
<script>
// 提交ajax
$('#id_submit').click(function () {
let dataArray = $('#id_form').serializeArray()
console.log(dataArray)
$.ajax({
url: '/login/',
type: 'post',
data: dataArray,
success: function (data) {
console.log(data)
if(data.code===100){
// 登陆成功 去首页
location.href = '/'
}else {
// 登陆失败 显示错误信息
$('.error').html(data.msg)
}
}
})
})
// 计时器 关闭错误提示
let test = function () {
$('.error').html('')
}
// 循环执行
timer = setInterval(test, 2000)
//60秒后关闭循环定时任务
setTimeout(function () {
clearTimeout(timer)
},60*1000)
</script>
def login(request):
if request.method == 'GET':
return render(request, 'login.html')
res = {'code': 100, 'msg': '登陆成功'}
code = request.POST.get('code')
# 校验验证码
if request.session.get('code').lower() == code.lower():
username = request.POST.get('username')
password = request.POST.get('password')
# 如果认证成功(用户名和密码正确有效),便会返回一个 User 对象。
obj = authenticate(username=username, password=password)
if obj:
return JsonResponse(res)
res['code'] = 110
res['msg'] = '用户名或密码错误'
return JsonResponse(res)
res['code'] = '120'
res['msg'] = '验证码错误'
return JsonResponse(res)
创建index.html,添加路由,get请求时返回index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>indextitle>
<script src="/static/jQuery.js">script>
<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="my_nav">
<nav class="navbar 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 navigationspan>
<span class="icon-bar">span>
<span class="icon-bar">span>
<span class="icon-bar">span>
button>
<a class="navbar-brand" href="#">博客园a>
div>
<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>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">jaspera>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>
div>
nav>
div>
<div class="container-fluid">
<div class="row">
<div class="view_left">
<div class="col-md-2">
<div class="list-group">
<a href="#" class="list-group-item active">
头条
a>
<a href="#" class="list-group-item">286 亿元!败了的 Google 是否会为 Android 交最贵罚单?a>
<a href="#" class="list-group-item">苹果灵动岛华而不实?网友整活改进,竟可以“一键抢大米”a>
<a href="#" class="list-group-item">“AI 终有可能消灭人类!”a>
<a href="#" class="list-group-item">Python 3.14 将比 C++ 更快a>
div>
<div class="list-group">
<a href="#" class="list-group-item active">
热点
a>
<a href="#" class="list-group-item">《羊了个羊》否认抄袭;安卓反垄断案再次败诉,罚款金额下降至286亿元a>
<a href="#" class="list-group-item">聊聊Redis的数据热点问题a>
<a href="#" class="list-group-item">抖音开放平台,究竟开放了什么?a>
<a href="#" class="list-group-item">谷歌CEO皮查伊暗示要裁员;华为研发投入位居首位;Android 13首个安全更新|极客头条a>
div>
div>
div>
<div class="view_mid">
<div class="col-md-7">
<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>
<li data-target="#carousel-example-generic" data-slide-to="2">li>
ol>
<div class="carousel-inner" role="listbox">
<div class="item active">
<img src="../media/slideshow/1.png" alt="...">
<div class="carousel-caption">
...
div>
div>
<div class="item">
<img src="../media/slideshow/2.png" alt="...">
<div class="carousel-caption">
...
div>
div>
<div class="item">
<img src="../media/slideshow/3.png" alt="...">
<div class="carousel-caption">
...
div>
div>
div>
<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">Previousspan>
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">Nextspan>
a>
div>
div>
div>
<div class="view_right">
<div class="col-md-3">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">广告招租h3>
div>
<div class="panel-body">
vx:xxx
div>
div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">广告招租h3>
div>
<div class="panel-body">
vx:xxx
div>
div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">广告招租h3>
div>
<div class="panel-body">
vx:xxx
div>
div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">广告招租h3>
div>
<div class="panel-body">
vx:xxx
div>
div>
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">广告招租h3>
div>
<div class="panel-body">
vx:xxx
div>
div>
div>
div>
div>
div>
body>
html>
登陆超级管理员录入数据。
先在admin.py中将表注册后 可以看到表名
from django.contrib import admin
from .models import *
# Register your models here.
admin.site.register(UserInfo)
admin.site.register(Blog)
admin.site.register(Tag)
admin.site.register(Classify)
admin.site.register(Article)
admin.site.register(UpAndDown)
admin.site.register(Comment)
注意:
显示表名需在models.py中创建表时添加一个Meta类
class Meta:
verbose_name_plural = '博客表'
定义字段时添加属性verbose_name在添加数据时显示
在创建表时定义双下str方法可以自定义显示的创建对象名字
def __str__(self):
return self.title
Django中的media文件夹一般用来存放文件,图片等不重要的数据,想在前端通过路径访问media中的数据,是不可以的,需要开启media访问。
MEDIA_ROOT = os.path.join(BASEDIR, 'media')
from django.views.static import serve
from django.conf import settings
path('media/' , serve, {'document_root':settings.MEDIA_ROOT})
注意:static文件夹已经默认开启,可以从浏览器进行访问,所以static和media文件夹下不能放重要文件。
有的网站有上传图片功能,可以上传到该网站,然后再自己的网站使用,这样就不会消耗自己的带宽。
图片防盗链就是抑制这种行为,本质原理是:浏览器发送http请求,请求头中会携带referer参数,是一个url地址,表示上一次访问的地址,图片防盗链可以跟据这个地址判断是不是自己的网址发的请求,如果不是直接拒绝响应。
views.py
后端返回所有文章
def index(request):
article_query_set = Article.objects.all()
return render(request, 'index.html', context={'article_query_set': article_query_set})
index.html
<div class="article" style="margin-top: 20px">
{% for foo in article_query_set %}
<div style="margin-top: 20px">
<h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
<hr>
<div class="media">
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ foo.blog.userinfo.icon }}" alt="..."
width="60px" height="60px">
a>
div>
<div class="media-body">
<h4 class="media-heading">{{ foo.desc }}h4>
div>
div>
<div class="" style="margin-top: 20px">
<a href="{{ foo.blog.userinfo.username }}"><span
style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>a>
<span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
<span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
aria-hidden="true">i>{{ foo.up_num }}span>
<span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
aria-hidden="true">i>{{ foo.down_num }}span>
<span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
aria-hidden="true">i>{{ foo.comment_num }}span>
div>
div>
{% endfor %}
div>
当点击用户名 则跳转到用户对应的站点(如果存在)不存在就返回404界面
# 站点匹配 必须放最后
path('/' , views.site),
def site(request, name, **kwargs):
user = UserInfo.objects.filter(username=name).first()
if not user:
return render(request, 'error.html')
article_set = user.blog.article_set.all()
return render(request, 'site.html', locals())
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>404title>
head>
<body>
<script type="text/javascript"
src="//qzonestyle.gtimg.cn/qzone/hybrid/app/404/search_children.js"
charset="utf-8">
script>
body>
html>
使用模板的继承,变得只是中间文章的展示,分类标签随笔部分不变。
base.html
<head>
<meta charset="UTF-8">
<title>
{% block title %}
{% endblock %}
title>
<script src="/static/jQuery.js">script>
<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>
<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">
{% block handle %}
{% endblock %}
div>
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<div class="list-group">
<a href="#" class="list-group-item active">
我的标签
a>
{% for foo in tag_res %}
<div class="list-group">
<a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
class="list-group-item"><span>{{ foo.1 }}span>
<span>({{ foo.2 }})span>a>
{% endfor %}
div>
div>
<div class="list-group">
<a href="#" class="list-group-item active">
我的分类
a>
{% for foo in classify_res %}
<div class="list-group">
<a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
class="list-group-item"><span>{{ foo.1 }}span>
<span>({{ foo.2 }})span>a>
{% endfor %}
div>
div>
<div class="list-group">
<a href="#" class="list-group-item active">
随笔分类
a>
{% for foo in date_res %}
<div class="list-group">
<a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
<span>({{ foo.1 }})span>a>
{% endfor %}
div>
div>
div>
<div class="col-md-10">
{% block crticle %}
{% endblock %}
div>
div>
div>
div>
body>
site.html
{% extends 'base.html' %}
{% block title %}
{{ user.username }}
{% endblock %}
{% block link %}
{% endblock %}
{% block handle %}
<div class="my_nav">
<nav class="navbar 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 navigationspan>
<span class="icon-bar">span>
<span class="icon-bar">span>
<span class="icon-bar">span>
button>
<a class="navbar-brand" href="#">{{ user.username }}a>
div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
ul>
<ul class="nav navbar-nav navbar-right">
<li>
<button type="button" class="btn btn-danger navbar-btn">管理button>
li>
ul>
div>
div>
nav>
div>
{% endblock %}
{% block crticle %}
<div class="article">
{% for foo in article_set %}
<div style="margin-top: 20px">
<h4 class="media-heading"><a href="">{{ foo.title }}a>h4>
<hr>
<div class="media">
<div class="media-body">
<h4 class="media-heading">{{ foo.desc }}h4>
div>
div>
<div class="" style="margin-top: 20px">
<span
style="padding: 10px;font-size: 13px">{{ foo.blog.userinfo.username }}span>
<span style="padding: 5px;font-size: 13px">{{ foo.create_time|date:'Y-m-d H:s' }}span>
<span style="padding: 5px;font-size: 13px"><i class="fa fa-thumbs-o-up"
aria-hidden="true">i>{{ foo.up_num }}span>
<span style="padding: 10px;font-size: 13px"><i class="fa fa-thumbs-o-down"
aria-hidden="true">i>{{ foo.down_num }}span>
<span style="padding: 10px;font-size: 13px"><i class="fa fa-commenting"
aria-hidden="true">i>{{ foo.comment_num }}span>
div>
div>
{% endfor %}
div>
{% endblock %}
urls.py
# jasper/tag/4.html 标签匹配
path('/tag/.html' , views.site),
# jasper/classify/3.html
path('/classify/.html' , views.site),
# jasper/archive/202209.html
path('/archive/.html' , views.site),
views.py
def site(request, name, **kwargs):
# name是传过来的站点对应的用户名
user = UserInfo.objects.filter(username=name).first()
if not user:
# 博主不存在 返回错误界面
return render(request, 'error.html')
# 查询该博主的所有文章
article_set = user.blog.article_set.all()
# 取名字后的路由后缀
tag = kwargs.get('tag')
classify = kwargs.get('classify')
time = kwargs.get('time')
# 有tag后缀
if tag:
# 返回当前标签的所有文章
article_set = article_set.filter(tag__id=tag)
elif classify:
article_set = article_set.filter(classify__id=classify)
# 按时间分类
elif time:
year = str(time)[:4]
month = str(time)[4:]
article_set = article_set.filter(create_time__year=year, create_time__month=month)
# # 需要标签名和统计标签内文章数
classify_res = Classify.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')
return render(request, 'site.html', locals())
# 自定义标签
1. 在应用下创建templatetags包,必须是templatetags
2. 在templatetags中新建一个new_tag.py文件,py文件名随意。
from django import template
from blog.models import Classify, Tag, Article, UserInfo
from django.db.models import Count
from django.db.models.functions import TruncMonth
register = template.Library() # 生成一个Library对象 名字必须叫register
# 装饰函数
@register.inclusion_tag(filename='left.html', name='left') # 返回html片段,第一个参数是html文件
def left(name):
# user 当前根据用户名查到的用户,需要传入用户名,一定会有user
user = UserInfo.objects.filter(username=name).first()
# 需要标签名和统计标签内文章数
classify_res = Classify.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')
return {'classify_res': classify_res, 'tag_res': tag_res, 'date_res': date_res, 'user':user} # 字典中的数据可以在left中使用
left.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<div class="list-group">
<a href="#" class="list-group-item active">
我的标签
a>
{% for foo in tag_res %}
<div class="list-group">
<a href="/{{ user.username }}/tag/{{ foo.0 }}.html"
class="list-group-item"><span>{{ foo.1 }}span>
<span>({{ foo.2 }})span>a>
div>
{% endfor %}
div>
<div class="list-group">
<a href="#" class="list-group-item active">
我的分类
a>
{% for foo in classify_res %}
<div class="list-group">
<a href="/{{ user.username }}/classify/{{ foo.0 }}.html"
class="list-group-item"><span>{{ foo.1 }}span>
<span>({{ foo.2 }})span>a>
div>
{% endfor %}
div>
<div class="list-group">
<a href="#" class="list-group-item active">
随笔分类
a>
{% for foo in date_res %}
<div class="list-group">
<a href="/{{ user.username }}/archive/{{ foo.0|date:'Ym' }}.html"
class="list-group-item"><span>{{ foo.0|date:'Y年m月' }}span>
<span>({{ foo.1 }})span>a>
div>
{% endfor %}
div>
body>
html>
base.html
在base中的左侧栅格使用inclusion_tag
需要先将自定义标签load过来,在使用标签并传入参数。
<div class="col-md-2">
{% load new_tag %}
{% left name %}
div>
渲染site.html页面时,返回的locals(),所以可以用到site函数的所有变量,site函数的name是它的形参,是点击首页博主用户名跳转过来的,name参数就是用文章取到的博主用户名,所以base可以用到name属性。
将那么属性传到new_tag文件中的left函数中,执行该函数。进行标签等数据的过滤,然后返回参数供left.html文件使用,left.html文件渲染完后,贴在base.html的相应位置。views.py中的
注意:添加templatetags模块后 需要重启服务器 才可以使用标签
点击首页文章和个人站点中的文章跳转到文章详情页面去。
path('/articles/' , views.article_detail),
views.py
def article_detail(request, name, article_id):
# 文章博主
user = UserInfo.objects.filter(username=name).first()
# 文章
article = Article.objects.filter(id=article_id).first()
if user and article:
return render(request, 'article.html', context={'user': user, 'article': article, 'name': name})
return render(request, 'error.html')
article.html
{% extends 'base.html' %}
{% block title %}
{{ article.title }}
{% endblock %}
{% block link %}
<link rel="stylesheet" href="/static/css/up.css">
{% endblock %}
{% block handle %}
<div class="my_nav">
<nav class="navbar 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 navigationspan>
<span class="icon-bar">span>
<span class="icon-bar">span>
<span class="icon-bar">span>
button>
<a class="navbar-brand" href="#">{{ user.username }}a>
div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
ul>
<ul class="nav navbar-nav navbar-right">
<li>
<button type="button" class="btn btn-danger navbar-btn">管理button>
li>
ul>
div>
div>
nav>
div>
{% endblock %}
{% block crticle %}
<div>
<h3>{{ article.title }}h3>
div>
<div>
{{ article.content }}
div>
<div id="div_digg" class="pull-right">
<div class="diggit is_up">
<span class="diggnum" id="digg_count">{{ article.up_num }}span>
div>
<div class="buryit is_up">
<span class="burynum" id="bury_count">{{ article.down_num }}span>
div>
<div class="clear">div>
<div class="diggword" id="digg_tips">
div>
div>
{% endblock %}
/static/css/up.css
.diggit {
float: left;
width: 46px;
height: 52px;
background: url(/static/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/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;
}
<script>
// 将点赞点踩设置成一个点击事件
$('.is_up').click(function () {
let is_up = ($(this).hasClass('diggit'))//根据类属性来判断是点赞还是点踩
$.ajax({
url: '/is_up/', //处理点赞点踩的接口
type: 'post',
// 需要传谁给哪篇文章点赞还是点踩了 谁点赞可以不传 只要后端登陆了就可以查到
data: {
article_id:{{ article.id }},
is_up: is_up,
csrfmiddlewaretoken: '{{ csrf_token }}'
},
success: function (data) {
if (data.code == 100) {
// 如果成功,点赞数+1
$('#digg_count').html({{ article.up_num }} +1)
} else if (data.code == 103) {
// 如果失败,点踩数+1
$('#bury_count').html({{ article.down_num }} +1)
}
// 每次打印提示信息
$('.diggword').html(data.msg)
}
})
})
</script>
设置路由
# is_up 处理点赞相关路由
path('is_up/', views.is_up),
def is_up(request):
article_id = request.POST.get('article_id')
is_up = json.loads(request.POST.get('is_up')) # 直接取出来是字符串 需要转成bool值
res = {'code': 100, 'msg': '点赞成功了'}
# 1. 判断当前用户是否登录
if not request.user.is_authenticated: # 只要用户登录就是当前用户 没有登陆就是匿名用户
res['code'] = 101
res['msg'] = '没有登录点击跳转登录'
return JsonResponse(res)
# 2. 判断当前用户是否已经给这篇文章点过赞或踩了
if UpAndDown.objects.filter(user=request.user, article_id=article_id).first():
res['code'] = 102
res['msg'] = '已经点赞或点踩了'
return JsonResponse(res)
# 3. 用户是点赞还是点踩 存入点赞点踩表和文章表 并将点赞点踩数返回bbs
# 开启事务
with transaction.atomic():
UpAndDown.objects.create(user=request.user, article_id=article_id, is_up=is_up)
if is_up:
# 文章表点赞数加1
Article.objects.filter(id=article_id).update(up_num=F('up_num') + 1)
else:
Article.objects.filter(id=article_id).update(down_num=F('down_num') + 1)
res['code'] = 103
res['msg'] = '点踩成功了'
return JsonResponse(res)
<div class="comment-show">
<div style="margin-top: 60px">
<b>评论列表b>
div>
<ul class="list-group comment-ajax">
{% for foo in comment %}
<li class="list-group-item">
<div>
<span># {{ forloop.counter }} 楼span> <span
style="margin-left: 20px">{{ foo.create_time|date:'Y-m-d H:i' }}span>
<a href="/{{ foo.user.username }}/"><span
style="margin-left: 20px">{{ foo.user.username }}span>a>
<div class="fa-pull-right">
<a class="reply" parent_id="{{ foo.article.id }}" username="{{ foo.user.username }}">回复a>
div>
div>
{% if foo.parent_id %}
<p style="margin-top: 10px">@ {{ foo.parent.user.username }}p>
<p>{{ foo.content|safe }}p>
{% else %}
<p style="margin-top: 10px">{{ foo.content }}p>
{% endif %}
li>
{% endfor %}
ul>
div>
<div>
<a href="">刷新页面a>
div>
<div style="margin-top: 60px">
<i class="fa fa-commenting-o" aria-hidden="true">i>
<b>发表评论b>
div>
{% if request.user.is_authenticated %}
<div>
<label for="content">label>
<textarea name="" id="content" cols="170" rows="10">textarea>
div>
<div class="pull-right">
<button class="btn btn-info" id="comment" style="margin-bottom: 50px">提交评论button>
div>
{% else %}
<div>
<i class="fa fa-commenting-o" aria-hidden="true">i> <span style="margin-left: 10px">登录后才能发表评论,立即 <a
href="/login/">登录a> 或者 <a
href="/">逛逛a> 首页span>
div>
{% endif %}
js代码
</script>
// 评论按钮点击事件
var parent_id = ''
$('#comment').click(function () {
// 取出评价内容 包括子评论和跟评论
var content = $('#content').val()
// 判断 如果是子评论要删除 @ 名字 换行
if (parent_id) {
console.log(content)
var i = content.indexOf('\n')//取到换行的索引
content = content.slice(i)//从索引位置往后切
}
$.ajax({
url: '/comment/',
type: 'post',
data: {// 谁给哪篇文章评论了什么 父评论的id
parent_id: parent_id,
article_id: {{ article.id }},
content: content,
csrfmiddlewaretoken: '{{ csrf_token }}' // 坑!!! 一定要加引号
},
success: function (data) {
console.log(data)
if (data.code == 100) {
$('#content').val('') //评论成功将评论区文字清空
var cur_name = data.people // 当前评论人
var content = data.content // 评论内容
var s = '' // 将评论拼接到评论列表中
if (data.comment_name) {//如果是子评论
var comment_name = data.comment_name
s = `
-
${cur_name}:
@${comment_name}
${content}
`
} else {
s = `
-
${cur_name}:
${content}
`
}
}
$('.comment-ajax').append(s)//追加到评论组的最后边 ajax提交跟评论和子评论
}
})
})
// 回复事件
$('.reply').click(function () {
parent_id = $(this).attr('parent_id')
console.log(parent_id)
var name = $(this).attr('username')
// 将 @ 名字 换行 加到输入框中
$('#content').val(`@${name}\n`).focus()//光标聚焦
})
</script>
def comment(request):
res = {'code': 100, 'msg': '评论成功'}
if request.user.is_authenticated:
article_id = request.POST.get('article_id')
content = request.POST.get('content')
parent_id = request.POST.get('parent_id')
# 保存评论
# 开启事务
with transaction.atomic():
res_comment = Comment.objects.create(content=content, user=request.user, article_id=article_id,
parent_id=parent_id)
# 文章表中评论数加1
Article.objects.filter(id=article_id).update(comment_num=F('comment_num') + 1)
# 评论成功发送邮件
# 使用多线程
article_title = Article.objects.filter(pk=article_id).first().title
send = Article.objects.filter(pk=article_id).first().blog.userinfo.email
t = Thread(target=send_mail,
args=(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, [send]))
t.start()
# send_mail(f'[博客评论通知]Re:{article_title}', content, settings.EMAIL_HOST_USER, ['xuxiaoxu152@163.com']) # subject, message, from_email, recipient_list,
# 返回给前端当前评论人 和评论内容
res['people'] = request.user.username
res['content'] = content
if parent_id: # 如果这是一条子评论 将他评论的这条评论的博主名返回
res['comment_name'] = res_comment.parent.user.username
return JsonResponse(res)
res['code'] = 101
res['msg'] = '未登录 不能评论'
return JsonResponse(res)
base.html
<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="false" aria-controls="collapseOne" class="collapsed">
博客后台
a>
h4>
div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel"
aria-labelledby="headingOne" aria-expanded="false" style="height: 0px;">
<div class="panel-body">
<a href="/add/">新建随笔a>
div>
<div class="panel-body">
<a href="">草稿箱a>
div>
<div class="panel-body">
<a href="">回收站a>
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" aria-expanded="false" style="height: 0px;">
<div class="panel-body">
<a href="">新增分类a>
div>
<div class="panel-body">
<a href="">分类列表<a>
div>
div>
div>
div>
div>
<div class="col-md-10">
<div class="is-show">
<h4 class="is-show">文章展示h4>
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a href="#">文章a>li>
<li role="presentation"><a href="#">新闻a>li>
<li role="presentation"><a href="#">标签a>li>
ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane fade in active" id="home">
{% block crticle %}
{% endblock %}
div>
div>
div>
{% block add %}
{% endblock %}
div>
div>
div>
index.html
{% extends 'backend/base.html' %}
{% block title %}
后台管理
{% endblock %}
{% block crticle %}
<div class="bs-example" data-example-id="hoverable-table">
<table class="table table-hover">
<thead>
<tr>
<th>编号th>
<th>标题th>
<th>发布时间th>
<th>评论数th>
<th>操作th>
<th>操作th>
tr>
thead>
<tbody>
{% for article in article_list %}
<tr>
<td>{{ forloop.counter }}td>
<td><a href="/{{ article.blog.userinfo.username }}/articles/{{ article.id }}">{{ article.title }}/a>td>
<td>{{ article.create_time|date:'Y-m-d H:i' }}td>
<td>{{ article.comment_num }}td>
<td><a href="/delete/?pk={{ article.id }}">删除a>td>
<td><a href="/alter_article/?pk={{ article.id }}">修改a>td>
tr>
{% endfor %}
tbody>
table>
div>
{% endblock %}
前端模板
{% extends 'backend/base.html' %}
{% 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 title %}
添加文章
{% endblock %}
{% block add %}
<div class="text-center" style="background: #2aabd2">
<h3>添加随笔h3>
div>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="add-title">标题label>
<input type="text" id="add-title" name="title" class="form-control">
div>
<div class="form-group">
<label for="add-content">内容label>
<div>
<textarea name="content" id="editor_id" cols="300" rows="20">textarea>
div>
div>
<div class="form-group">
<label for="add-classify">分类label>
<select class="form-control" name="category" id="add-classify">
{% for classify in classify_list %}
<option value="{{ classify.id }}">{{ classify.name }}option>
{% endfor %}
select>
div>
<div class="form-group">
<label for="add-tag">标签label>
<select class="form-control" name="tag" id="add-tag" multiple>
{% for tag in tag_list %}
<option value="{{ tag.id }}">{{ tag.name }}option>
{% endfor %}
select>
div>
<button class="btn btn-success form-control">上传文章button>
form>
{% endblock %}
{% block js %}
// 使用富文本编辑器
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id', {
width: '100%',
height: '300px',
resizeType: '1',
// 上传图片相关
uploadJson: '/put_img/',
//filePostName: 'myfile', //默认imgFile
//extraFileUploadParams: {
// 'csrfmiddlewaretoken': '{{ csrf_token }}'
// } 后端没有取消校验 需要传csrf
});
});
script>
{% endblock %}
def add(request):
if request.method == 'GET':
tag_list = Tag.objects.filter(blog=request.user.blog)
classify_list = Classify.objects.filter(blog=request.user.blog)
return render(request, 'backend/add.html', context={'tag_list': tag_list, 'classify_list': classify_list})
title = request.POST.get('title')
content = request.POST.get('content')
# BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
bs = BeautifulSoup(content, features='html.parser')
# 截取html文本,将空格和换行替换成空,并截取70个字符
desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
# 剔除script标签
script_list = bs.findAll('script')
for i in script_list:
i.decompose() # 将每个script标签删除
classify = request.POST.get('category')
tag = request.POST.getlist('tag') # 这是多对多的
res = Article.objects.create(title=title, content=str(bs), desc=desc, classify_id=classify, blog=request.user.blog)
# 多对多添加外键关系
res.tag.add(*tag)
return redirect('/backend/')
富文本编辑器图片处理,查看官方文档。
# 文章图片处理
# 需要处理csrf 可已经用掉这个接口的csrf
@csrf_exempt # 免除校验
def put_img(request):
img = request.FILES.get('imgFile')
path = os.path.join(settings.MEDIA_ROOT, 'upload', img.name)
with open(path, 'wb') as f:
for i in img:
f.write(i)
return JsonResponse({
"error": 0,
"url": f"http://127.0.0.1:8000/media/upload/{img.name}"
})
xss跨站脚本,在内容中存script脚本,前端渲染时使用了safe,如果存在script脚本,就会执行。解决方案。富文本编辑器在输入代码块时会自动将尖括号转换成对应的字符,只需在后端将恶意的script清除即可。
需要使用beautifulsoup4模块。
-pip3 install beautifulsoup4
-删除script标签
soup = BeautifulSoup(content, 'html.parser')
script_list=soup.findAll('script') # 搜索到html中所有的script标签
for script in script_list:
script.decompose() # 把搜到的script标签一个个删除
用户登陆后展示用户名和和管理选项按钮
{% if request.user.is_authenticated %}
<li><a href="{{ request.user.username }}">{{ 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="/set_pwd/">修改密码a>li>
<li><a href="/backend/">后台管理a>li>
<li><a href="/alter_icon/">修改头像a>li>
<li role="separator" class="divider">li>
<li><a href="/login_out/">退出登录a>li>
ul>
li>
{% else %}
<a href="/login/">登录a>
<a href="/register/">注册a>
{% endif %}
# 退出登录
def login_out(request):
logout(request) # request.session.flush() 清除掉session和cookie
return redirect('/')
{% extends 'backend/base.html' %}
{% block title %}
修改头像
{% endblock %}
{% block add %}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div style="margin-top: 100px">
<h3 style="color: darkslateblue">修改头像h3>
<label for="icon">
<img src="/media/{{ icon }}" alt="" width="100px" height="100px" id="img">
label>
<input type="file" id="icon" style="display: none" name="icon">
<button class="btn btn-success">确认修改button>
div>
form>
{% endblock %}
{% block js %}
<script>
$('.is-show').toggle()
// 头像动态显示 给文件标签绑定一个变化事件
$('#icon').change(function () {
var reader = new FileReader()
// 获取文件内容
var file = $('#icon')[0].files[0]
reader.readAsDataURL(file)
reader.onload = (function () {
$('#img').attr('src', reader.result)
})
})
script>
{% endblock %}
# 修改头像
def alter_icon(request):
if request.method == "GET":
# 需要当前用户头像
icon = request.user.icon
return render(request, 'backend/alter_icon.html', context={'icon': icon})
icon = request.FILES.get('icon')
request.user.icon = icon
request.user.save()
return redirect('/')
{% extends 'backend/base.html' %}
{% block title %}
修改密码
{% endblock %}
{% block add %}
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="pwd1">原密码label>
<input type="password" id="pwd1" name="old_password" class="form-control">
div>
<div class="form-group">
<label for="pwd2">新密码label>
<input type="password" id="pwd2" name="new_password" class="form-control">
div>
<div class="form-group">
<label for="pwd3">确认密码label>
<input type="password" id="pwd3" name="re_password" class="form-control">
div>
<button class="form-control btn-success">提交button> <span style="color: red">{{ error }}span>
form>
{% endblock %}
def set_pwd(request):
if request.method == 'GET':
return render(request, 'backend/set_pwd.html')
old_password = request.POST.get('old_password')
new_password = request.POST.get('new_password')
re_password = request.POST.get('re_password')
if request.user.check_password(old_password):
if new_password == re_password:
request.user.set_password(new_password)
request.user.save()
# 退出当前登录 跳转至登录
login_out(request)
return redirect(to='/login/')
return render(request, 'backend/set_pwd.html', context={'error': '两次密码不一致'})
return render(request, 'backend/set_pwd.html', context={'error': '原密码不一致'})
{% extends 'backend/base.html' %}
{% 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 title %}
修改文章
{% endblock %}
{% block add %}
<div class="text-center" style="background: #2aabd2">
<h3>修改文章h3>
div>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="add-title">标题label>
<input type="text" id="add-title" name="title" class="form-control" value="{{ article.title }}">
div>
<div class="form-group">
<label for="add-content">内容label>
<div>
<textarea name="content" id="editor_id" cols="300" rows="20">{{ article.content }}textarea>
div>
div>
<div class="form-group">
<label for="add-classify">分类label>
<select class="form-control" name="category" id="add-classify">
{% for classify in classify_list %}
{% if classify == article.classify %}
<option value="{{ classify.id }}" selected>{{ classify.name }}option>
{% else %}
<option value="{{ classify.id }}">{{ classify.name }}option>
{% endif %}
{% endfor %}
select>
div>
<div class="form-group">
<label for="add-tag">标签label>
<select class="form-control" name="tag" id="add-tag" multiple>
{% for tag in tag_list %}
{% if tag in tag_list %}
<option value="{{ tag.id }}" selected>{{ tag.name }}option>
{% else %}
<option value="{{ tag.id }}">{{ tag.name }}option>
{% endif %}
{% endfor %}
select>
div>
<button class="btn btn-success form-control">上传文章button>
form>
{% endblock %}
{% block js %}
<script>
KindEditor.ready(function (K) {
window.editor = K.create('#editor_id', {
width: '100%',
height: '300px',
resizeType: '1',
// 上传图片相关
uploadJson: '/put_img/',
//filePostName: 'myfile', //默认imgFile
//extraFileUploadParams: {
// 'csrfmiddlewaretoken': '{{ csrf_token }}'
// } 后端没有取消校验 需要传csrf
});
});
script>
{% endblock %}
def alter_article(request):
pk = request.GET.get('pk')
# 需要当前文章 当前用户的分类和标签
if request.method == 'GET':
article = Article.objects.filter(pk=pk).first()
classify_list = Classify.objects.filter(blog=request.user.blog)
tag_list = Tag.objects.filter(blog=request.user.blog)
return render(request, 'backend/alter_article.html',
context={'article': article, 'classify_list': classify_list, 'tag_list': tag_list})
# post请求 修改文章
title = request.POST.get('title')
content = request.POST.get('content')
# BeautifulSoup第一个参数是html内容,第二个参数:使用的解析器
bs = BeautifulSoup(content, features='html.parser')
# 截取html文本,将空格和换行替换成空,并截取70个字符
desc = bs.text.replace(' ', '').replace('\n', '')[:70] + '...'
# 剔除script标签
script_list = bs.findAll('script')
for i in script_list:
i.decompose() # 将每个script标签删除
classify = request.POST.get('category')
tag = request.POST.getlist('tag') # 这是多对多的
article = Article.objects.filter(pk=request.GET.get('pk')) # 必须是一个queryset
# 还需要将该文章的评论点赞点踩一起更新
up_num = Article.objects.filter(pk=pk).first().up_num
down_num = Article.objects.filter(pk=pk).first().down_num
comment_num = Article.objects.filter(pk=pk).first().comment_num
with transaction.atomic():
article.update(title=title, desc=desc, classify_id=classify, content=str(bs), blog=request.user.blog,
up_num=up_num, down_num=down_num, comment_num=comment_num)
article.first().save()
# 多对多关系添加
article.first().tag.set(tag)
return redirect(f'/{request.user.username}/articles/{pk}')
from django.core.mail import send_mail
# (subject, message, from_email, recipient_list,)
res1 = send_mail('邮件标题', '邮件内容', settings.EMAIL_HOST_USER, ["@qq.com"])
settings.py配置
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.qq.com' # 如果是 163 改成 smtp.163.com
EMAIL_PORT = 465
EMAIL_HOST_USER = '@qq.com' # 帐号
EMAIL_HOST_PASSWORD = '***' # 密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
#这样收到的邮件,收件人处就会这样显示
#DEFAULT_FROM_EMAIL = ''
EMAIL_USE_SSL = True #使用ssl
#EMAIL_USE_TLS = False # 使用tls
#EMAIL_USE_SSL 和 EMAIL_USE_TLS 是互斥的,即只能有一个为 True