BBS(Bulletin Board Service,公告牌服务)是Internet上的一种电于信息服务系统。它提供一块公共电子白板,每个用户都可以在上面书写,可发布信息或提出看法。
注:这里只对主要功能进行实现,其中还有很多小功能未实现,有小bug未调试。
前期准备
准备一个新的django项目,数据库配置为mysql的bbs库。
templates、static目录配置。
表设计
一个项目中,表设计环节是最重要的。
其中:分类表的设定是一个文章只属于一类。
建表
模型层 app01\models.py
点击查看代码
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.forms import BooleanField
# Create your models here.
# 用户表 新增4个字段
class UserInfo(AbstractUser):
phone = models.CharField(max_length=11,verbose_name='手机号')
avatar = models.FileField(upload_to="avatar/",default='vatar/default.png',verbose_name='头像')
'''
给vatar字段传文件对象,文件会自动存储到 avatar/ 下,然后vatar字段只保存文件路径 vatar/xxx.png
不传默认是vatar/default.png
'''
create_time = models.DateField(auto_now_add=True,verbose_name='注册时间')
blog = models.OneToOneField(to='Blog',null=True)
# 个人站点表
class Blog(models.Model):
site_name = models.CharField(max_length=32,verbose_name='站点名称')
site_title = models.CharField(max_length=32,verbose_name='站点标题')
site_theme = models.CharField(max_length=64,verbose_name='站点样式') #存css/js的文件路径
# 文章分类表
class Category(models.Model):
name = models.CharField(max_length=32,verbose_name='文章类名')
blog = models.ForeignKey(to='Blog',null=True)
# 文章标签表
class Tag(models.Model):
name = models.CharField(max_length=32,verbose_name='文章标签名')
blog = models.ForeignKey(to='Blog',null=True)
# 文章表
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.DateField(auto_now_add=True,verbose_name='创建时间')
#数据库字段优化设计 不直接统计对应点赞表中的赞数,而是每一次点赞 +1
up_num = models.BigIntegerField(verbose_name='点赞数',default=0)
dowm_num = models.BigIntegerField(verbose_name='点踩数',default=0)
comment_num= models.BigIntegerField(verbose_name='评论数',default=0)
blog = models.ForeignKey(to='Blog',null=True)
category = models.ForeignKey(to='Category',null=True)
# 半自动的方式创建多对多关系
tags = models.ManyToManyField(to='Tag',
through='ArticleToTag',
through_fields=('article','tag'),
)
class ArticleToTag(models.Model):
article = models.ForeignKey(to='Article')
tag = models.ForeignKey(to='Tag')
# 点踩点赞表
class UpAndDown(models.Model):
user = models.ForeignKey(to='UserInfo')
article = models.ForeignKey(to='Article')
is_up = BooleanField()
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo')
article = models.ForeignKey(to='Article')
content = models.CharField(max_length=255,verbose_name='评论内容')
comment_time = models.DateTimeField(auto_now_add=True,verbose_name='评论时间')
# 自关联 此字段用来记录子评论对于的父评论
parent = models.ForeignKey(to='self',null=True,verbose_name='父评论')
将默认的用户头像放在 用户表的vatar字段定义的位置 avatar\default.png
配置文件中加上 AUTH_USER_MODEL = 'app01.UserInfo',声明user表
执行两条数据库迁移命令,创建定义好的表。
用户注册
- forms组件
- 用户头像实时展示
- ajax 提交注册信息以及展示提示信息
1. 注册相关的forms组件
不同功能代码需要解耦合。
如果你的项目只用到一个forms组件,你可以直接新建一个py文件(eg:myforms.py)书写forms相关的自定义类;
如果你用到多个forms组件,你可以创建一个文件夹,再在文件夹内根据forms组件功能的不同创建不同的py文件。
2. 利用form组件渲染前端标签
1) 不用form表单提交,用ajax
2) 利用form标签获取到用户输入数据
$('#myform').serializeArray()
form标签的jquery对象.serializeArray() 可以拿到form表单中传输的普通键值对组成的列表(不包括文件)。
[{name: 'username', value: 'yxf'}, {name: 'password', value: 'xxx'}}, {…}, {…}]
3. 手动渲染用户头像标签
label标签中的所有内容(包括图片),都能绑定对应的input标签
<label for="id_avatar">
头像:
<img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;">
</label>
<input type="file" id="id_avatar" style="display:none">
4. 实时展示用户头像
1) .change() change事件某个标签值发送变化时触发,例如input标签上传文件时触发
2) 利用到了文件阅读器,需要注意myFileReaderObj.readAsDataURL(fileObj)是异步操作
3) .onload() onload事件会等待加载完毕才会触发
$('#id_avatar').change(function(){
// 文件阅读器对象
// 1. 生成一个文件阅读器
let myFileReaderObj = new FileReader();
// 2. 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3. 将文件对象交给阅读器处理
myFileReaderObj.readAsDataURL(fileObj) // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
// 4. 利用阅读器将文件展示到前端页面
myFileReaderObj.onload = function(){ // onload 等待myFileReaderObj对象的任务都执行完成才执行
$('#myimg').attr('src',myFileReaderObj.result)
}
})
5. 一旦用户信息不合法,如何精确地渲染提示
1) form组件渲染的input标签都有id值: id_字段名
.auto_id可以获取到对应的id值
<label for="{{ form.auto_id }}">{{form.label}}:</label>
2) 根据后端返回的字段及报错信息可以手动拼接对应的input标签的id,以定位到span提示信息的标签
3) 提示功能的完善:
jQuery链式操作 展示提示信息;添加 has-error类,使输入框变红
input 框获取焦点时 触发focus事件,去重对应的红色边框与提示信息
效果:
项目结构展示
注册代码:
路由层 BBS\urls.py
点击查看代码
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 用户注册
url(r'^register/',views.register,name='register'),
# 用户登录
url(r'^login/',views.login,name='login')
]
视图层
app01\myform.py
点击查看代码
from django import forms
from app01 import models
class Myform(forms.Form):
username = forms.CharField(label='用户名',max_length=8,min_length=3,
error_messages={
'required':'用户名不能为空',
'max_length':'用户名不能超过8位',
'min_length':'用户名不能少于3位',
},
widget = forms.widgets.TextInput(attrs={'class':'form-control'}),
)
password = forms.CharField(label='密码',max_length=8,min_length=3,
error_messages={
'required':'密码不能为空',
'max_length':'密码不能超过8位',
'min_length':'密码不能少于3位',
},
widget = forms.widgets.PasswordInput(attrs={'class':'form-control'}),
)
confirm_password = forms.CharField(label='确认密码',max_length=8,min_length=3,
error_messages={
'required':'确认密码不能为空',
'max_length':'确认密码不能超过8位',
'min_length':'确认密码不能少于3位',
},
widget = forms.widgets.PasswordInput(attrs={'class':'form-control'}),
)
email = forms.EmailField(label='邮箱',
error_messages={
'required':'邮箱不能为空',
'invalid':'邮箱格式不正确',
},
widget = forms.widgets.EmailInput(attrs={'class':'form-control'}),
)
# 局部钩子校验用户名是否已存在
def clean_username(self):
username = self.cleaned_data.get('username')
is_exist = models.UserInfo.objects.filter(username=username)
if is_exist:
self.add_error('username','用户名已存在')
return username
#全局钩子校验两次密码是否一致
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password','两次密码不一致')
return self.cleaned_data
app01\views.py
点击查看代码
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render,reverse
from app01.myform import Myform
from app01 import models
# Create your views here.
def register(request):
form_obj=Myform()
back_dic = {'code':1000}
if request.is_ajax():
form_obj=Myform(request.POST)
if not form_obj.is_valid():
back_dic['code'] = 2000
back_dic['msg'] = form_obj.errors
#form_obj.errors => {'password': ['密码不能为空'], 'confirm_password': ['确认密码不能为空'], ...}}
else:
clean_data = form_obj.cleaned_data
clean_data.pop('confirm_password')
avatar = request.FILES.get('avatar')
if avatar:
clean_data['avatar'] = avatar
#clean_data是字典形式, **clean_data能打散字典进行传参
models.UserInfo.objects.create_user(**clean_data)
back_dic['url'] = reverse('login')
return JsonResponse(back_dic)
return render(request,'register.html',locals())
def login(request):
return HttpResponse('login')
模板层 templates\register.html
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h2 class="text-center">注册页面</h2>
{% csrf_token %}
<form action="" id="myform">
{% for form in form_obj %}
<div class="form-group">
<!-- {{ form.auto_id }} 可以获取到input标签对应的id -->
<label for="{{ form.auto_id }}">{{form.label}}:</label>
{{form}}
<span style="color:red" class="pull-right"></span>
</div>
{% endfor %}
<br>
<label for="id_avatar">
头像:
<img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;">
</label>
<input type="file" id="id_avatar" style="display:none">
<br><br>
<input type="button" id="id_commit" class="btn form-control btn-primary" value="提交">
</form>
</div>
</div>
</div>
<script>
// 将前端页面的默认头像展示为用户上传的头像
// .change(function(){}) 当某个标签值发送变化时触发
$('#id_avatar').change(function(){
// 文件阅读器对象
// 1. 生成一个文件阅读器
let myFileReaderObj = new FileReader();
// 2. 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3. 将文件对象交给阅读器处理
myFileReaderObj.readAsDataURL(fileObj) // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
// 4. 利用阅读器将文件展示到前端页面
myFileReaderObj.onload = function(){ // onload 等待myFileReaderObj对象的任务都执行完成才执行
$('#myimg').attr('src',myFileReaderObj.result)
}
})
$('#id_commit').click(function(){
// console.log($('#myform').serializeArray())
// 打印数组包字典的形式 [{name: 'username', value: 'yxf'}, {name: 'password', value: 'xxx'}}, {…}, {…}]
formDataObj = new FormData();
$('#myform').serializeArray().forEach(function(i){formDataObj.append(i.name,i.value)})
formDataObj.append('avatar',$('#id_avatar')[0].files[0])
$.ajax({
url:'',
data: formDataObj,
method: 'post',
contentType: false,
processData: false,
success: function(args){
if (args.code==1000){window.location.href=args.url}
else {
console.log(args.code)
let msg=args.msg
for (var i in msg) {
// console.log(i,msg[i][0]) // username 用户名不能为空
let id = '#id_' + i
// 为span空标签添加提示语,再将提示框变为红色
$(id).next('span').text(msg[i][0]).parent().addClass('has-error')
}
}
}
})
$('input').focus(function(){
// 当提示框获取到焦点时,提示框去掉红色边框效果 以及不再展示提示语
$(this).next('span').text('').parent('div').removeClass('has-error')
})
})
</script>
</body>
</html>
将默认头像放到static\img\default.png供前端页面展示;
ajax使用以引入文件static\js\mysetup.js的方式使用csrf中间件。(ajax使用csrf)
bootstrap资源放到static\others\bootstrap-3.4.1,供前端页面调用
用户登录
实现两大功能:
/login 登录
/get_code 图片验证码
- auth模块 实现用户信息校验 与登录状态保存
- ajax 提交登录信息 与 展示提示信息
1. 图片验证码如何展示
注:
img标签的src属性可以写三种类型:
a) 图片url (完整的url https://xx.xx.com/xx 与当前网页的url /get_code/)
b)图片路径
c)图片二进制数据
1) 借助pillow模块 Image ImageDraw ImageFont
2) 借助内存管理器io模块 BytesIo 临时存储图片
3) 产生随机验证码
def random_str():
random_upper = chr(random.randint(65,90))
random_lower = chr(random.randint(97,122))
random_int = str(random.randint(0,9))
return random.choice([random_upper,random_lower,random_int])
在session中保存验证码用于后续校验
4) 产生随机颜色
def random_color():
return (random.randint(0,255),random.randint(0,255),random.randint(0,255))
5) 当验证码看不清时如何重载
img标签的src属性改变时,会触发图片的重载。
可以在点击看不清按钮时,触发更改img的src属性值,从而实现验证码图片的重载
6) 使用事务绑定 评论表新增数据 以及 文章表评论数+1
效果展示:
登录代码
路由层 BBS\urls.py
点击查看代码
# 用户登录
url(r'^login/',views.login,name='login'),
# 图片验证码
url(r'^get_code/',views.get_code,name='getCode'),
视图层 app01\views.py
点击查看代码
from django.contrib import auth
def login(request):
back_dic = {'code':1000}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 核对用户名密码是否正确
user_obj = auth.authenticate(request,username=username,password=password)
if user_obj:
if code.upper() == request.session.get('code').upper():
#保存该用户登录状态
auth.login(request,user_obj)
back_dic['url'] = reverse('home')
else:
back_dic['code'] = 2000
back_dic['msg'] = '验证码输入错误'
else:
back_dic['code'] = 3000
back_dic['msg'] = '用户名或密码输入错误'
return JsonResponse(back_dic)
return render(request,'login.html')
from PIL import Image,ImageDraw,ImageFont
from io import BytesIO
import random
def get_code(request):
'''生成验证码图片'''
def random_color():
'''生成随机颜色'''
return (random.randint(0,255),random.randint(0,255),random.randint(0,255))
def random_str():
'''大写字母/小写字母/数字 产生一个随机字符'''
random_upper = chr(random.randint(65,90))
random_lower = chr(random.randint(97,122))
random_int = str(random.randint(0,9))
return random.choice([random_upper,random_lower,random_int])
img_obj = Image.new('RGB',(300,35),random_color())
img_draw = ImageDraw.Draw(img_obj)
img_font = ImageFont.truetype('static/font/impact.ttf',28)
code = ''
for i in range(5):
randomStr = random_str()
img_draw.text((i*50+50,0),randomStr,random_color(),img_font)
code += randomStr
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj,'png')
return HttpResponse(io_obj.getvalue())
字体文件可直接拷贝 C:\Windows\Fonts下的文件
模板层 templates\login.html
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
</head>
<body>
<div class="container">
<div class="row">
<h2 class="text-center">登陆页面</h2>
<form action="">
<div class="form-group">
<label for="id_username">用户名:</label>
<input type="text" id="id_username" class="form-control">
</div>
<div class="form-group">
<label for="id_password">密码:</label>
<input type="password" id="id_password" class="form-control">
</div>
<div class="form-group">
<label for="">验证码:</label>
<div class="row">
<div class="col-md-3">
<input type="text" id="id_code" class="form-control">
</div>
<div class="col-md-3">
<img src="{% url 'getCode' %}" alt="" id="id_img">
</div>
<div class="col-md-3">
<input type="button" id="id_changeImg" class="btn btn-warning" value="看不清">
</div>
</div>
</div>
<div class="form-group">
<input type="button" id="id_commit" class="btn btn-primary" value="登录">
<span id="id_prompt" style="color: red;"></span>
</div>
</form>
</div>
</div>
<script>
// 点击看不清按钮刷新验证码图片 img标签的src改变时会重载图片
$('#id_changeImg').click(function(){
let src = $('#id_img').attr('src')+'?'
$('#id_img').attr('src',src)
})
$('#id_commit').click(function(){
$.ajax({
url: '',
method: 'post',
data: {'username':$('#id_username').val(),'password':$('#id_password').val(),'code':$('#id_code').val()},
success: function(args){
if(args.code==1000){
window.location.href = args.url
}
else{
$('#id_prompt').text(args.msg)
}
},
})
})
</script>
</body>
</html>
bbs首页搭建
首页url /home
顶部导航条
实现效果:
当用户未登录时展示 登录与注册
当用户登录时展示用户名与更多操作
代码:
路由层 BBS\urls.py
点击查看代码
# 首页
url(r'^home/',views.home,name='home'),
视图层 app01\views.py
点击查看代码
def home(request):
return render(request,'home.html',locals())
模板层 templates\home.html
拷贝bootstrap的导航栏代码,颜色改为反色导航条
点击查看home.html 导航条雏形代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
</head>
<body>
<!-- 导航条 -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">BBS</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
<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="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
<!-- 用if判断登录状态实现登录前后的不同内容展示 -->
{% if request.user.is_authenticated %}
<li><a href="">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">修改密码</a></li>
<li><a href="#">更换头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">退出登录</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
</body>
</html>
修改密码
1. 入口: 登陆后点击更多信息中的更改密码
2. 绑定更改密码标签与模态框,点击时弹出模态框可输入信息
3. 提交信息使用ajax
5. 后端对输入的内容进行比对,有误则展示到模态框,正确则刷新页面
效果:
代码:
使用login_required装饰器设置此路由为登陆状态可访问,此处全局配置,配置非登录状态访问跳转/login/
settings.py
LOGIN_URL = '/login/'
路由层 BBS\urls.py
点击查看代码
# 修改密码
url(r'^set_password/',views.set_password,name='setPassword'),
视图层 app01\views.py
点击查看代码
from django.contrib.auth.decorators import login_required
@login_required
def set_password(request):
back_dic = {'code':1000}
if request.method == 'POST':
old_password = request.POST.get('old_password')
new_password = request.POST.get('new_password')
confirm_password = request.POST.get('confirm_password')
if request.user.check_password(old_password):
if new_password == confirm_password:
request.user.set_password(new_password)
request.user.save()
back_dic['msg'] = '修改成功'
else:
back_dic['code'] = 1002
back_dic['msg'] = '两次密码不一致'
else:
back_dic['code'] = 1001
back_dic['msg'] = '原密码错误'
return JsonResponse(back_dic)
模板层 templates\home.html
拷贝bootstrap大模态框代码进行更改
点击查看修改密码代码
...
<!-- 添加 data-toggle="modal" data-target=".bs-example-modal-lg属性后点击此标签弹出模态框 -->
<li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
...
<!-- Large modal 大模态框 点击修改密码弹出 将data-toggle="modal" data-target=".bs-example-modal-lg"加到点击密码的a标签-->
<!-- <button type="button" class="btn btn-primary" data-toggle="modal" data-target=".bs-example-modal-lg" >Large modal</button> -->
<!-- 这里的class中还需要加上bs-example-modal-lg才能弹出 -->
<div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h2 class="text-center">修改密码</h2>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" value="{{ request.user.username }}" disabled>
</div>
<div class="form-group">
<label for="old_password">原密码</label>
<input type="password" class="form-control" id="old_password">
</div>
<div class="form-group">
<label for="new_password">新密码</label>
<input type="password" class="form-control" id="new_password">
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<input type="password" class="form-control" id="confirm_password">
</div>
<div class="modal-footer">
<span class="pull-left" id="prompt" style="color: red;"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="change_pwd_btn">确认</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修改密码使用ajax提交信息
$('#change_pwd_btn').click(function(){
$.ajax({
url: "{% url 'setPassword' %}",
method: 'post',
data: {'old_password':$('#old_password').val(),'new_password':$('#new_password').val(),'confirm_password':$('#confirm_password').val()},
success: function(args){
if (args.code == 1000){
$('#prompt').text(args.msg).attr('style','color: blue');
window.location.reload()
}
else {$('#prompt').text(args.msg)}
}
})
})
</script>
退出登录
1. 入口: 登陆后点击更多信息中的退出登录
2. auth.logout(request) 清除本地与浏览器登录状态的session
效果:
代码:
路由层 BBS\urls.py
点击查看代码
# 退出登录
url(r'^logout/',views.logout,name='logout'),
视图层 app01\views.py
点击查看代码
@login_required
def logout(request):
auth.logout(request)
return redirect(reverse('home'))
模板层 templates\home.html
点击查看退出登录代码
<!-- 添加退出登录的url -->
<li><a href="{% url 'logout' %}">退出登录</a></li>
**admin后台管理
django 提供了一个可视化界面 /admin 方便程序员对模型表进行数据的增删改查。
1. 创建超级用户
2. 模型表注册:
首先要在admin.py中注册你的模型表,告诉admin你需要操作哪些表,注册完才会在后台管理页面显示该表,且表名后会自动加‘s’
#admin.py
from django.contrib import admin
from app01 import models
# Register your models here.
admin.site.register(models.UserInfo)
admin.site.register(models.Blog)
admin.site.register(models.Article)
admin.site.register(models.Category)
admin.site.register(models.Tag)
admin.site.register(models.ArticleToTag)
admin.site.register(models.UpAndDown)
admin.site.register(models.Comment)
3. 后台管理页面展示中文表名
默认后台管理页面展示models中定义的英文表名,想要展示中文表名可在models.py中定义
class Category(models.Model):
name = models.CharField(max_length=32,verbose_name='文章类名')
blog = models.ForeignKey(to='Blog',null=True)
class Meta:
# verbose_name = '文章分类表' #修改admin后台表名展示为 文章分类表s
verbose_name_plural = '文章分类表' #修改admin后台表名展示为 文章分类表
class Tag(models.Model):
name = models.CharField(max_length=32,verbose_name='文章标签名')
blog = models.ForeignKey(to='Blog',null=True)
class Meta:
# verbose_name = '文章标签表' #修改admin后台表名展示为 文章标签表s
verbose_name_plural = '文章标签表' #修改admin后台表名展示为 文章标签表
4. 增删改查
django会给每一个注册了的模型表自动生成增删改查4条url
http://127.0.0.1:8000/admin/app01/userinfo(表名)/ # 查
http://127.0.0.1:8000/admin/app01/userinfo(表名)/add/ # 增
http://127.0.0.1:8000/admin/app01/userinfo(表名)/5(主键值)/change/ # 改
http://127.0.0.1:8000/admin/app01/userinfo(表名)/5(主键值)/delete/ # 删
录入数据
http://127.0.0.1:8000/admin
从文章表入手创建blog与category对象
创建文章标签表对象
去Article to tag表绑定文章与标签
去user表绑定与blog的一对一关系(phone未填写会报错,可填写或配置blank=True)
添加双下str方法后,展示为
admin后台管理页面录入数据时,有null=True的字段,这里不填还是会报错,需要加上blank=True
phone = models.CharField(max_length=11,verbose_name='手机号',null=True,blank=True)
null=True 表示数据库改字段可以为空
blank=True 表示admin后台管理录入数据该字段可以为空
**django暴露指定文件夹
需求:暴露用户头像资源
1. 将用户上传的文件放到指定文件夹 项目根目录/media/下
2. 开放media下的用户头像资源,使 127.0.0.1:8000/media/avatar/bbb.png 可以访问到项目根目录/media/avatar/bbb.png
配置:
1. BBS\settings.py添加配置
#配置用户上传的文件的存储位置(这里的目录名可自行定义)
'''
配置好后用户上传的文件默认放到此目录下,例如之前定义的
avatar = models.FileField(upload_to="avatar/",default='vatar/default.png',verbose_name='头像')
头像默认放到 根目录的avatar下,定义了此配置后,头像放到 根目录/media/下的avatar下
'''
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
2. BBS\urls.py 添加路由
from django.views.static import serve
from BBS import settings
# 暴露指定文件夹资源 media,固定写法,特别是(?P<path>.*)后面不能加/
url(r'^media/(?P<path>.*)',serve,{'document_root':settings.MEDIA_ROOT}),
3. 访问测试:
之前创建的项目根目录下的avatar目录直接移到media下,即头像的路径为 media\avatar\111.png,。
可通过此链接访问 http://127.0.0.1:8000/media/avatar/111.png
主页面展示
1. 主页面/home282布局,中间展示文章信息,两边展示广告;
2. 为中部文章添加分页器,每页展示5篇
3. 头像展示 已暴露头像资源,直接拼接路径即可
效果:
代码:
分页器代码 app01\utils\split_page.py
点击查看分页器代码
class Pagination(object):
def __init__(self, current_page, all_count, per_page_num=10, pager_count=11):
"""
封装分页相关数据
:param current_page: 当前页
:param all_count: 数据库中的数据总条数
:param per_page_num: 每页显示的数据条数
:param pager_count: 最多显示的页码个数
"""
try:
current_page = int(current_page)
except Exception as e:
current_page = 1
if current_page < 1:
current_page = 1
self.current_page = current_page
self.all_count = all_count
self.per_page_num = per_page_num
# 总页码
all_pager, tmp = divmod(all_count, per_page_num)
if tmp:
all_pager += 1
self.all_pager = all_pager
self.pager_count = pager_count
self.pager_count_half = int((pager_count - 1) / 2)
@property
def start(self):
return (self.current_page - 1) * self.per_page_num
@property
def end(self):
return self.current_page * self.per_page_num
def page_html(self):
# 如果总页码 < 11个:
if self.all_pager <= self.pager_count:
pager_start = 1
pager_end = self.all_pager + 1
# 总页码 > 11
else:
# 当前页如果<=页面上最多显示11/2个页码
if self.current_page <= self.pager_count_half:
pager_start = 1
pager_end = self.pager_count + 1
# 当前页大于5
else:
# 页码翻到最后
if (self.current_page + self.pager_count_half) > self.all_pager:
pager_end = self.all_pager + 1
pager_start = self.all_pager - self.pager_count + 1
else:
pager_start = self.current_page - self.pager_count_half
pager_end = self.current_page + self.pager_count_half + 1
page_html_list = []
# 添加前面的nav和ul标签
page_html_list.append('''
<nav aria-label='Page navigation>'
<ul class='pagination'>
''')
first_page = '<li><a href="?page=%s">首页</a></li>' % (1)
page_html_list.append(first_page)
if self.current_page <= 1:
prev_page = '<li class="disabled"><a href="#">上一页</a></li>'
else:
prev_page = '<li><a href="?page=%s">上一页</a></li>' % (self.current_page - 1,)
page_html_list.append(prev_page)
for i in range(pager_start, pager_end):
if i == self.current_page:
temp = '<li class="active"><a href="?page=%s">%s</a></li>' % (i, i,)
else:
temp = '<li><a href="?page=%s">%s</a></li>' % (i, i,)
page_html_list.append(temp)
if self.current_page >= self.all_pager:
next_page = '<li class="disabled"><a href="#">下一页</a></li>'
else:
next_page = '<li><a href="?page=%s">下一页</a></li>' % (self.current_page + 1,)
page_html_list.append(next_page)
last_page = '<li><a href="?page=%s">尾页</a></li>' % (self.all_pager,)
page_html_list.append(last_page)
# 尾部添加标签
page_html_list.append('''
</nav>
</ul>
''')
return ''.join(page_html_list)
点击查看代码
from app01.utils.split_page import Pagination
def home(request):
# 分页器
article_obj_list = models.Article.objects.all() #获取到所有文章对象用于首页展示
current_page = request.GET.get('page',1)
all_count = article_obj_list.count()
# 1. 传值生成对象
page_obj = Pagination(current_page=current_page,all_count=all_count,per_page_num=5)
# 2. 直接对数据集进行切片
page_queryset = article_obj_list[page_obj.start:page_obj.end]
return render(request,'home.html',locals())
模板层 templates\home.html
点击查看首页文章展示代码
<div class="row">
<!-- 左侧广告 -->
<div class="col-md-2">
<div class="jumbotron">
<h1>python零基础入学!</h1>
<p>人工智能&数据分析实战</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">了解更多</a></p>
</div>
<div class="panel panel-danger">
<div class="panel-heading">Fun! 知识开放开放麦</div>
<div class="panel-body">
快来加入我们吧~
</div>
</div>
<div class="panel panel-warning">
<div class="panel-heading">老百金</div>
<div class="panel-body">
今年过节不收礼
</div>
</div>
</div>
<!-- 中间文章展示 -->
<div class="col-md-8">
<br>
{% for article_obj in page_queryset %}
<h3 style="color: royalblue;font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
<!-- 媒体对象 头像左,内容右 -->
<div class="media">
<div class="media-left media-middle">
<a href="#">
<!-- 用户头像 -->
<img class="media-object" src="/media/{{ article_obj.blog.userinfo.avatar }}" alt="" style=" 60px;">
</a>
</div>
<div class="media-body">
<p>{{ article_obj.desc }}</p>
</div>
</div>
<br>
<!-- 用户名 文章创建事件 点赞图标 点赞数 评论图标 评论数 -->
<!-- 点击用户名跳转个人主页 -->
<a href="/{{ article_obj.blog.userinfo.username }}">{{ article_obj.blog.userinfo.username }}</a>
{{ article_obj.create_time|date:'Y-m-d' }}
<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }}
<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }}
<!-- 分页器代码 -->
<nav aria-label="Page navigation">
</nav>
<br><br><hr>
{% endfor %}
<!-- 分页器代码 -->
{{ page_obj.page_html|safe }}
</div>
<!-- 右侧广告 -->
<div class="col-md-2">
<div class="panel panel-primary">
<div class="panel-heading">ANTA</div>
<div class="panel-body">
安踏 永不止步
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">鸿星尔克 </div>
<div class="panel-body">
TO BE NUMBER 1
</div>
</div>
<div class="jumbotron">
<h1>2核4G 298 3年!</h1>
<p>广厦云服务</p>
<p><a class="btn btn-primary btn-lg" href="#" role="button">我感兴趣</a></p>
</div>
</div>
</div>
</div>
更换头像
1. 入口: 登陆后点击更多信息中的更换头像,弹出模态框
2. 点击原头像图像弹出文件选择窗口,选择好新图片后展示为新的头像
3. 点击确认数据库更新头像,前端重载页面
效果:
代码:
路由层 BBS\urls.py
点击查看代码
#更换头像
url(r'^change_avatar/',views.change_avatar,name='changeAvatar'),
视图层 app01\views.py
点击查看代码
@login_required
def change_avatar(request):
back_dic = {'code':1000}
if request.method == 'POST':
new_avatar = request.FILES.get('new_avatar')
request.user.avatar = new_avatar
request.user.save()
return JsonResponse(back_dic)
模板层 templates\home.html
点击查看代码
<!-- 入口: 添加 data-toggle="modal" data-target=".vatar-modal-lg" 点击弹出更换头像的模态框 -->
<li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
<!-- 修改头像模态框 -->
<div class="modal fade vatar-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h2 class="text-center">更换头像</h2>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<label for="id_avatar">
头像:
<img src="/media/{{ request.user.avatar }}" alt="" style=" 150px;" id="img_avatar">
<!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;"> -->
</label>
<input type="file" id="id_avatar" style="display:none">
<div class="modal-footer">
<span class="pull-left" id="prompt" style="color: red;"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="change_vatar_btn">确认</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修改头像处上传后更新展示
$('#id_avatar').change(function(){
// 文件阅读器对象
// 1. 生成一个文件阅读器
let myFileReaderObj = new FileReader();
// 2. 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3. 将文件对象交给阅读器处理
myFileReaderObj.readAsDataURL(fileObj) // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
// 4. 利用阅读器将文件展示到前端页面
myFileReaderObj.onload = function(){ // onload 等待myFileReaderObj对象的任务都执行完成才执行
$('#img_avatar').attr('src',myFileReaderObj.result)
}
})
// 修改头像提交
$('#change_vatar_btn').click(function(){
let formDataObj = new FormData();
formDataObj.append('new_avatar',$('#id_avatar')[0].files[0])
$.ajax({
url: "{% url 'changeAvatar' %}",
method: 'post',
data: formDataObj,
contentType: false,
processData: false,
success: function(args){
if (args.code==1000){window.location.reload()}
else{window.location.reload()}
},
})
})
</script>
个人站点展示
1. 个人站点: http://127.0.0.1/用户名/
2. 用户名不存在,返回404页面
直接拷贝的博客园404页面代码
图片不能展示 --> 图片防盗链
3. 用户存在:
顶部一个导航条
下面3-9布局,右侧默认展示该用户的所有文章
3. 左侧侧边栏展示文章分类、标签、创建时间 年-月,点击不同分组右侧展示对应的文章
http://127.0.0.1:8000/yxf/category/4(该分类的主键值) 分类链接
http://127.0.0.1:8000/yxf/tag/6(该标签的主键值) 标签链接
http://127.0.0.1:8000/yxf/archive/2022-05 create_time分组链接
4. 创建时间按 年-月 分组,并统计每组文章数
models.Article.objects.filter(blog=blog_obj) # 对象集
.annotate(month=TruncMonth('create_time')) # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
.values('month').annotate(c=Count('pk')) # 按month字段分组,并且每组根据主键计数
.values_list('month','c') # 返回month与c字段
返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
[(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
如果报错,可尝试修改配置文件中的时区:
# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Shanghai'
# USE_TZ = True
USE_TZ = False
5. 个人主页引入自定义样式
每个用户都可以有自己的站点样式
1)在site.html中加载自定义样式文件
<link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
2)创建样式文件 media\css\huandada.css 这里的文件名要与blog表的site_theme的文件名一致
效果:
个人主页:
文章分类展示:
文章标签展示:
日期分组展示:
自定义样式展示: a标签变绿
代码:
路由层 BBS\urls.py
点击查看代码
# 个人站点
url(r'^(?P<username>\w+)/$',views.site,name='site'),
# 个人站点侧边栏筛选功能 (文章category/tag/create_time分组展示)
url(r'^(?P<username>\w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/',views.site)
视图层 app01\views.py
点击查看代码
from django.db.models.functions import TruncMonth
from django.db.models import Count
# 个人站点
def site(request,username,**kwargs):
'''
:param kwargs: 如果此参数有值,就是个人主页侧边栏的分组展示,也就意味着 article_list 要做额外筛选
'''
user_obj = models.UserInfo.objects.filter(username=username)
# 如果该用户不存在返回404页面
if not user_obj:
return render(request,'404.html')
# 用户存在就展示相关的内容
else:
blog_obj = user_obj[0].blog
# 该用户的文章列表,kwargs没值,就展示所有文章
article_list = blog_obj.article_set.all()
# 如果kwargs有值,就是个人主页侧边栏category/tag/create_time分组展示
if kwargs:
# print(kwargs) # {'condition': 'category', 'param': '4'}
condition = kwargs.get('condition')
param = kwargs.get('param')
# 文章分类查询
if condition == 'category':
article_list = article_list.filter(category__pk=param)
# 文章标签查询
if condition == 'tag':
article_list = article_list.filter(tags__pk=param)
# 创建时间按 年-月 查询
if condition == 'archive':
year,month = param.split('-') #2022-3 => [2022,3]
article_list = article_list.filter(create_time__year=year,create_time__month=month)
# 查询该用户的文章分类及对应的每一类对于的文章数
category_list = models.Category.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
# <QuerySet [('猫粮', 2), ('猫咪生活', 2), ('喵品种', 3)]>
# 查询该用户的标签及每种标签对于的文章数
tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
# <QuerySet [('yxf标签一', 1), ('yxf标签二', 2), ('yxf标签三', 1)]>
# 将该用户文章创建日期按 年-月 分组并统计文章数
date_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
'''
# 日期字段 按 年-月 分组
models.Article.objects.filter(blog=blog_obj) # 对象集
.annotate(month=TruncMonth('create_time')) # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
.values('month').annotate(c=Count('pk')) # 按month字段分组,并且每组根据主键计数
.values_list('month','c') # 返回month与c字段
返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
[(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
'''
# 对时间对象进行反向排序
date_list = sorted(date_list,key=lambda i:i[0],reverse=True)
return render(request,'site.html',locals())
模板层
templates\404.html
点击查看代码
<!-- 直接cp的博客园404页面代码 -->
<html>
<head>
<meta charset='utf-8'>
<link rel="icon" href="//common.cnblogs.com/favicon.ico" type="image/x-icon" />
<title>404 页面不存在 - 博客园</title>
<style type='text/css'>
body {
margin: 8% auto 0;
max- 400px;
min-height: 200px;
padding: 10px;
font-family: 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;
font-size: 14px;
padding-right: 200px;
position: relative;
}
p { color: #555;margin: 15px 0px; }
img { border: 0px; }
.d { color: #404040; }
.robot img { max- 192px; }
.robot { position: absolute; top: 0; right: 0; }
</style>
</head>
<body>
<p style="margin-left: 5px;"><a href="https://www.cnblogs.com/"><img src="//common.cnblogs.com/logo.svg" style="height:45px" alt="cnblogs"></a></p>
<div style="margin-top:20px">
<p><b>404.</b> 抱歉,您访问的资源不存在。</p>
<p class="d">可能是网址有误,或者对应的内容被删除,或者处于私有状态。</p>
<p style="color:#777;">代码改变世界,联系邮箱 contact@cnblogs.com</p>
</div>
<div class="robot"><a href="//www.cnblogs.com/cmt/articles/13940458.html"><img src="//common.cnblogs.com/images/404-robot.png" alt="404 robot" /></a></div>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4CQQXWHK3C"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-4CQQXWHK3C');
</script>
</body>
</html>
templates\site.html
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load static %}
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
<!-- 引入自定义样式 -->
<link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
</head>
<body>
<!-- 导航条 直接cp的home.html的导航条,改了一个地方。可根据需求自行更改 -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<!-- 改了这里 -->
<a class="navbar-brand" href="/{{ username }}">{{ blog_obj.site_name }}</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>
<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="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
<li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'logout' %}">退出登录</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="row">
<!-- 左侧 侧边栏 展示 文章分类/标签/创建时间 的分组 及对应的文章数 -->
<div class="col-md-3">
<div class="panel panel-success">
<div class="panel-heading">文章分类</div>
<div class="panel-body">
{% for category in category_list %}
<p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }} ({{ category.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">文章标签</div>
<div class="panel-body">
{% for tag in tag_list %}
<p><a href="/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }} ({{ tag.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">日期归档</div>
<div class="panel-body">
{% for create_time in date_list %}
<p><a href="/{{ username }}/archive/{{ create_time.0|date:'Y-m' }}">{{ create_time.0|date:'Y-m' }} ({{ create_time.1 }})</a></p>
{% endfor %}
</div>
</div>
</div>
<!-- 右侧 展示 article_list文章中的文章对象 -->
<div class="col-md-9">
<br>
{% for article_obj in article_list %}
<h3 style="font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
<!-- 媒体对象 头像左,内容右 -->
<div class="media">
<div class="media-left media-middle">
<a href="#">
<img class="media-object" src="/media/{{ blog_obj.userinfo.avatar }}" alt="" style=" 60px;">
</a>
</div>
<div class="media-body">
<p>{{ article_obj.desc }}</p>
</div>
</div>
<!-- posted @ 文章创建时间 点赞图标 点赞数 评论图标 评论数 编辑-->
<div class="pull-right" style="margin-right: 30px">
<span>posted @ {{ article_obj.create_time|date:'Y-m-d' }} </span>
<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }}
<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }}
<span><a href="">编辑</a></span>
</div>
<br><br><hr>
{% endfor %}
</div>
</div>
</div>
</body>
</html>
自定义样式文件 media\css\huandada.css(可以没有)
点击查看代码
/* a标签颜色 */
a {
color: green;
}
图片防盗链
直接拷贝的博客园404页面代码,本项目使用,图片展示不全,因为博客园对该图片链接设置了图片防盗
博客园的404页面
拷贝404代码到我的项目后的页面
# 如何避免别的网站通过本网站的url访问本网站的资源 ?
# 简单防盗
当请求来的 时候,先看当前请求是从哪个网站来的,
如果是本网站来的,则正常访问;
是其他网站来的,则拒绝访问。
请求头中的Referer是专门记录请求来源于哪个网站的
Referer: http://127.0.0.1:8000/
referer: https://www.cnblogs.com/
# 如何绕过这种防盗措施
1. 修改请求头中的 Referer地址
2. 直接爬虫把对方的资源下载到我们本地
模板继承 与 inclusion_tag
侧边栏需要后端传输数据才能渲染,并且多个页面都有侧边栏的,那么:
- 在需要侧边栏的地方直接拷贝侧边栏的代码(不推荐)
- 将侧边栏制作成inclusion_tag,哪里需要就在哪里加载
侧边栏做成inclusion_tag形式:
定义inclusion_tag app01\templatetags\mytags.py
直接 剪切 site视图函数中侧边栏的代码
点击查看代码
from django import template
from app01 import models
from django.db.models.functions import TruncMonth
from django.db.models import Count
register=template.Library()
@register.inclusion_tag('left_menu.html')
def left_menu(username):
user_obj = models.UserInfo.objects.filter(username=username)[0]
blog_obj = user_obj.blog
# 查询该用户的文章分类及对应的每一类对于的文章数
category_list = models.Category.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
# <QuerySet [('猫粮', 2), ('猫咪生活', 2), ('喵品种', 3)]>
# 查询该用户的标签及每种标签对于的文章数
tag_list = models.Tag.objects.filter(blog=blog_obj).annotate(count_num=Count('article__pk')).values_list('name','count_num','pk')
# <QuerySet [('yxf标签一', 1), ('yxf标签二', 2), ('yxf标签三', 1)]>
# 将该用户文章创建日期按 年-月 分组并统计文章数
date_list = models.Article.objects.filter(blog=blog_obj).annotate(month=TruncMonth('create_time')).values('month').annotate(c=Count('pk')).values_list('month','c')
'''
# 日期字段 按 年-月 分组
models.Article.objects.filter(blog=blog_obj) # 对象集
.annotate(month=TruncMonth('create_time')) # 将create_time字段截取到月(2022-3),并将此字段month添加到查询列表中
.values('month').annotate(c=Count('pk')) # 按month字段分组,并且每组根据主键计数
.values_list('month','c') # 返回month与c字段
返回: 虽然month截取到月,但是这里返回还是有day,默认都为1号
[(datetime.date(2022, 5, 1), 1), (datetime.date(2022, 3, 1), 2),...]
'''
# 对时间对象进行反向排序
date_list = sorted(date_list,key=lambda i:i[0],reverse=True)
return locals()
个人站点以及后续的文章详情页页面布局一致,可以直接改为模板继承的模式
模板页面 base.html (直接拷贝site.html稍作修改)
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% load static %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
<!-- 引入自定义样式 -->
<link rel="stylesheet" href="/media/css/{{ blog_obj.site_theme }}">
{% block css_part %}
{% endblock css_part %}
</head>
<body>
<!-- 导航条 直接cp的home.html的导航条,改了一个地方。可根据需求自行更改 -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<!-- 改了这里 -->
<a class="navbar-brand" href="/{{ username }}">{{ blog_obj.site_name }}</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>
<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="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
<li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'logout' %}">退出登录</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="row">
<!-- 左侧 侧边栏 展示 文章分类/标签/创建时间 的分组 及对应的文章数 -->
<div class="col-md-3">
<!-- 引用inclusion_tag left_menu 侧边栏 -->
{% load mytags %}
{% left_menu username %}
</div>
<!-- 右侧 展示 article_list文章中的文章对象 -->
<div class="col-md-9">
{% block content %}
{% endblock content %}
</div>
</div>
</div>
{% block js_part %}
{% endblock js_part %}
</body>
</html>
个人站点的页面
点击查看代码
{% extends "base.html" %}
{% block content %}
<br>
{% for article_obj in article_list %}
<h3 style="font-weight:bold;"><a href="">{{ article_obj.title }}</a></h3>
<!-- 媒体对象 头像左,内容右 -->
<div class="media">
<div class="media-left media-middle">
<a href="#">
<img class="media-object" src="/media/{{ blog_obj.userinfo.avatar }}" alt="" style=" 60px;">
</a>
</div>
<div class="media-body">
<p>{{ article_obj.desc }}</p>
</div>
</div>
<!-- posted @ 文章创建时间 点赞图标 点赞数 评论图标 评论数 编辑-->
<div class="pull-right" style="margin-right: 30px">
<span>posted @ {{ article_obj.create_time|date:'Y-m-d' }} </span>
<span class="glyphicon glyphicon-thumbs-up"></span> {{ article_obj.up_num }}
<span class="glyphicon glyphicon-comment"></span> {{ article_obj.comment_num }}
<span><a href="">编辑</a></span>
</div>
<br><br><hr>
{% endfor %}
{% endblock content %}
文章详情页
1. 单篇文章url /用户名/article/文章id
2. 文章详情页继承模板页面
3. 文章内容填充,拷贝博客园 $('#cnblogs_post_body')
效果:
先在admin后台管理页面添加html格式的文章内容 (直接从博客园cp)
代码:
路由层 BBS\urls.py
点击查看代码
# 文章详情页
url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/$',views.article_detail),
视图层 app01\views.py
点击查看代码
def article_detail(request,username,article_id):
user_obj = models.UserInfo.objects.filter(username=username)
article_obj = models.Article.objects.filter(pk=article_id,blog__userinfo__username=username )
if not article_obj:
return render(request,'404.html')
article_obj = article_obj[0]
blog_obj = article_obj.blog
return render(request,'article_detail.html',locals())
模板层 templates\article_detail.html
点击查看代码
{% extends "base.html" %}
{% block content %}
<h1 style="font-weight: bold;">{{ article_obj.title }}</h1>
<br>
{{ article_obj.content|safe }}
{% endblock content %}
文章点赞点踩
1. 点赞点踩url up_or_down/
2. 直接cp博客园点赞点踩的代码以及对应的每一个标签的样式
3. cp 博客园点踩点赞的html代码与css样式
将图片下载到本地(图片防盗链)
4. 如何区分用户点赞还是点踩
1) 给点赞的标签绑定id="digg_count"
2) let is_up = $(this).hasClass('diggit');
返回true 或 false
5. 点赞点踩内部逻辑
1)登陆后才能点
2)不能给自己点
3)点过了赞/踩 就不能再点
4)点击之后,点赞/踩数展示+1
效果:
代码:
路由层 BBS\urls.py
点击查看代码
# 文章点赞点踩
url(r'^up_or_down/',views.up_or_down),
视图层 app01\views.py
点击查看代码
import json
from django.db.models import F
def up_or_down(request):
back_dic={'code':1000}
print('in updown')
if request.method == 'POST':
# 判断用户是否登录
if request.user.is_authenticated():
is_up = request.POST.get('is_up')
article_id = request.POST.get('article_id')
# 将is_up 从str转为bool
is_up = json.loads(is_up)
article_obj_query = models.Article.objects.filter(pk=article_id)
article_obj = article_obj_query.first()
# 判断文章是否存在
if article_obj:
article_user_obj = article_obj.blog.userinfo
# 如果点赞/踩 人就是文章作者自己
if not request.user == article_user_obj:
clicked = models.UpAndDown.objects.filter(article=article_obj,user=request.user).first()
# 如果该用户已经对该文章点过赞/踩了
if not clicked:
# 写入到点赞点踩表
models.UpAndDown.objects.create(article=article_obj,user=request.user,is_up=is_up)
if is_up:
back_dic['msg']='点赞成功'
article_obj_query.update(up_num=F('up_num')+1)
else:
back_dic['msg']='点踩成功'
article_obj_query.update(down_num=F('down_num')+1)
else:
if clicked.is_up:
back_dic['code']='1001'
back_dic['msg']='您已点过赞了'
else:
back_dic['code']='1001'
back_dic['msg']='您已点过踩了'
else:
if is_up:
back_dic['code']='1002'
back_dic['msg']='您不能给自己点赞哦'
else:
back_dic['code']='1002'
back_dic['msg']='您不能给自己点踩哦'
else:
return render(request,'404.html')
else:
back_dic['code']='1003'
back_dic['msg']='请先<a href="/login/" style="color:red">登录</a>'
return JsonResponse(back_dic)
模板层 templates\article_detail.html
点击查看代码
{% extends "base.html" %}
{% block css_part %}
<!-- 博客园cp的点赞点踩样式开始 -->
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
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;
46px;
height: 52px;
background: url(/static/img/downdown.gif) no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
}
</style>
<!-- 博客园cp的点赞点踩样式结束 -->
{% endblock css_part %}
{% block content %}
<!-- 文章展示 -->
<h1 id='title' style="font-weight: bold;" article_id="{{ article_obj.pk }}">{{ article_obj.title }}</h1>
<br>
{{ article_obj.content|safe }}
<!-- 点赞点踩代码开始 -->
<div id="div_digg">
<div class="diggit upAndDown">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit upAndDown">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<span style="color: red;" id="up_down_comment"></span>
</div>
<!-- 点赞点踩代码结束 -->
{% endblock content %}
{% block js_part %}
<script>
// 点赞点踩请求提交与返回结果渲染
$('.upAndDown').click(function(){
let is_up = $(this).hasClass('diggit');
let article_id = $('#title').attr('article_id');
let clicked = $(this)
$.ajax({
url: '/up_or_down/',
method: 'post',
data: {'is_up':is_up,'article_id':article_id,'csrfmiddlewaretoken':'{{ csrf_token }}'},
success: function(args){
if (args.code==1000){
$('#up_down_comment').text(args.msg);
let num = clicked.children().text()
clicked.children().text(Number(num)+1)
}
else{
// 这里用html不用text是因为msg中有个登录的a标签
$('#up_down_comment').html(args.msg)
}
}
})
})
</script>
{% endblock js_part %}
文章评论
1. 使用ajax提交评论内容
2. 分为两部分:
1) 评论框提交评论
2) 展示评论
- 已评论内容 展示,每篇文章展示其自己的评论
- 刚评论内容 临时渲染
3. 评论分为两种:
1) 根评论: 点击评论框直接评论
2) 子评论: 点击某一条评论的回复/引用,自动聚焦到评论框并@父评论用户名或信息
4. 限制:
1) 要先登录
2)评论内容不能为空
效果:
代码:
路由层 BBS\urls.py
点击查看代码
# 评论
url(r'^comment/',views.comment),
视图层 app01\views.py
点击查看代码
def article_detail(request,username,article_id):
user_obj = models.UserInfo.objects.filter(username=username)
article_obj = models.Article.objects.filter(pk=article_id,blog__userinfo__username=username).first()
if not article_obj:
return render(request,'404.html')
blog_obj = article_obj.blog
# 拿到该篇文章的评论数据
comment_list = models.Comment.objects.filter(article=article_obj).all()
return render(request,'article_detail.html',locals())
# 评论功能
from django.db import transaction
def comment(request):
back_dic={'code':1000}
if request.is_ajax() and request.method == 'POST':
comment = request.POST.get('comment')
article_id = request.POST.get('article_id')
parent_id = request.POST.get('parent')
# 判断用户是否登录
if request.user.is_authenticated():
# 判断评论是否为空
if comment:
article_obj=models.Article.objects.filter(pk=article_id).first()
with transaction.atomic():
models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num')+1)
models.Comment.objects.create(user=request.user,article=article_obj,content=comment,parent_id=parent_id)
else:
back_dic['code']=1001
back_dic['msg']='评论内容不能为空'
else:
back_dic['code']=1002
back_dic['msg']='请先<a href="/login/" style="color:red">登录</a> <a href="/register/" style="color:red">注册</a>'
return JsonResponse(back_dic)
模板层 templates\article_detail.html
点击查看代码
{% extends "base.html" %}
{% block css_part %}
<!-- 博客园cp的点赞点踩样式开始 -->
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
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;
46px;
height: 52px;
background: url(/static/img/downdown.gif) no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
.diggword {
margin-top: 5px;
margin-left: 0;
font-size: 12px;
color: #808080;
}
</style>
<!-- 博客园cp的点赞点踩样式结束 -->
{% endblock css_part %}
{% block content %}
<!-- 文章展示 -->
<h1 id='title' style="font-weight: bold;" article_id="{{ article_obj.pk }}">{{ article_obj.title }}</h1>
<br>
{{ article_obj.content|safe }}
<!-- 点赞点踩代码开始 -->
<div id="div_digg" class="clearfix">
<div class="diggit upAndDown">
<span class="diggnum" id="digg_count">{{ article_obj.up_num }}</span>
</div>
<div class="buryit upAndDown">
<span class="burynum" id="bury_count">{{ article_obj.down_num }}</span>
</div>
<div class="clear"></div>
<span style="color: red;" id="up_down_comment"></span>
</div>
<br><br><br>
<!-- 点赞点踩代码结束 -->
<!-- 评论功能开始 -->
<!-- 评论信息展示 -->
<div class="row">
<div class="col-md-8">
<div>
{% for comment_obj in comment_list %}
<hr>
<div>#{{ forloop.counter }}楼 {{ comment_obj.comment_time|date:'Y-m-d H:i:s'}} <a href="/{{ comment_obj.user.username }}">{{ comment_obj.user.username }}</a></div>
<br>
{% if comment_obj.parent %}
<div>@{{ comment_obj.parent.user.username }}</div>
{% endif %}
<div>{{ comment_obj.content }}</div>
<div class="pull-right" username="{{ comment_obj.user.username }}" comment_id="{{ comment_obj.pk }}">
<a class="comment_replay replay">回复</a>
<a class="comment_replay quote">引用</a>
</div>
{% endfor %}
<hr>
<!-- 临时渲染评论 -->
<div id="new_comment"></div>
</div>
</div>
</div>
<!-- 评论框 -->
<div>
<h2>发表评论</h2>
<div>
<textarea id="comment_box" cols="70" rows="10"></textarea>
</div>
<button type="button" id="commit_comment" class="btn btn-default"> 提交评论 </button>
<button type="button" id="cancel_comment" class="btn btn-default"> 取消 </button>
<span style="color: red;" id="comment_msg"></span>
</div>
<!-- 评论功能结束 -->
{% endblock content %}
{% block js_part %}
<script>
</script>
<script>
// 点赞点踩请求提交与返回结果渲染
$('.upAndDown').click(function(){
let is_up = $(this).hasClass('diggit');
let article_id = $('#title').attr('article_id');
let clicked = $(this)
$.ajax({
url: '/up_or_down/',
method: 'post',
data: {'is_up':is_up,'article_id':article_id,'csrfmiddlewaretoken':'{{ csrf_token }}'},
success: function(args){
if (args.code==1000){
$('#up_down_comment').text(args.msg);
let num = clicked.children().text()
clicked.children().text(Number(num)+1)
}
else{
// 这里用html不用text是因为msg中有个登录的a标签
$('#up_down_comment').html(args.msg)
}
}
})
})
// ajax提交评论
let parent_id = ''
let paren_username = ''
$('#commit_comment').click(function(){
let comment = $('#comment_box').val();
if (parent_id){
index_num = comment.indexOf('\n')
comment = comment.slice(index_num+1)
}
let article_id = $('#title').attr('article_id');
$.ajax({
url: '/comment/',
method: 'post',
data: {'comment':comment,'article_id':article_id,'parent':parent_id},
success: function(args){
// 1000为评论成功
if (args.code==1000){
// 临时渲染评论
// 子评论会多一个 @父评论用户名
if (parent_id){
new_comment = `<div><div><span class="glyphicon glyphicon-comment"></span> <span>{{ request.user.username }}</span></div><div>@${parent_username}</div><div>${comment}</div><hr></div>`
$('#new_comment').append(new_comment)
}
else{
new_comment = `<div><div><span class="glyphicon glyphicon-comment"></span> <span>{{ request.user.username }}</span><br><br></div><div>${comment}</div><hr></div>`
$('#new_comment').append(new_comment)
}
$('#comment_box').val('')
}
// 评论失败,展示提示信息
else{$('#comment_msg').html(args.msg)}
parent_id = ''
}
})
})
// 评论框获取到焦点,清空提示信息
$('#comment_box').focus(function(){
$('#comment_msg').text('')
})
// 评论框的取消按钮,点击后取消评论(主要用于取消子评论)
$('#cancel_comment').click(function(){
$('#comment_box').val('')
parent_id = ''
})
// 点击评论的 回复 自动获取评论框焦点,并自动输入了@父评论用户名\n
// 点击评论的 引用 自动获取评论框焦点,并自动输入了@父评论用户名\n父评论\n------\n
$('.comment_replay').click(function(){
parent_username = $(this).parent().attr('username')
parent_id = $(this).parent().attr('comment_id')
$('#comment_box').focus()
// 回复标签右replay类,所以点击回复is_replay为true,点击引用为false
is_replay = $(this).hasClass('replay')
if (is_replay){$('#comment_box').val(`@${parent_username}\n`)}
else {
let content = $(this).parent().prev().text()
$('#comment_box').val(`@${parent_username}\n${content}\n------\n`)
}
})
</script>
{% endblock js_part %}
**富文本编辑器
后续后台管理中会用到富文本编辑器,这里先介绍怎么在我们的项目中集成此编辑器
编辑器官网: http://kindeditor.net/demo.php
-
首先下载此编辑器,放到项目的静态资源下
-
html页面加载编辑器资源
<script charset="utf-8" src="{% static '/others/editor/kindeditor.js' %} "></script>
<textarea name="content" id="edit" cols="200" rows="10"></textarea>
<script>
KindEditor.ready(function(K) {
window.editor = K.create('#edit');
});
</script>
- 相关的常用配置
KindEditor.ready(function(K) {
window.editor = K.create('#edit',{
'100%', // 编辑框宽度设置
height: '600px', // 编辑框高度设置
resizeType: 1, // resizeType为1,表示也没事编辑框高度可调,宽度不可变
// 以下都是上传图片/文件相关配置
uploadJson : '/upload_img/', // 上传文件的url
extraFileUploadParams : {
'csrfmiddlewaretoken':'{{ csrf_token }}', // 额外携带的参数
}
}
)
});
后台管理
url:
/backend/ 后台管理页面
/upload_img/ 富文本编辑器上传图片
/change/article/14/ 修改某个文章
/add/article/ 添加新文章
/delete/article/6 删除某个文章
/backend/recycler/ 回收站
/edit/category/ 编辑分类
/add/category/ 新增分类
/delete/category/ 删除分类
/backend/category/2/ 展示某分类对应的文章
1. 文章增删改查;
文章的删除:
1)移动到回收站
2)完全删除
2. 前端编辑器-kindeditor富文本编辑器
1) 处理xss攻击
2)文章摘要获取text文本前150字符
3)编辑器上传图片
3. 文章分类的增删
4. 使用bs4模块实现:
1. 防止xss攻击
XSS攻击通常指的是通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。这些代码通常是js。所以需要用到bs4清除用户编辑内容中的js代码。
2. 获取文章的前150个text字符作为desc
因为文章提交时是html类型,包括标签,需要使用bs4获取到纯文本。
5. category与article表添加了delete的布尔值字段。
效果:
后台管理主页面
主页面
新增文章
编辑旧文章
回收站两个功能: 1. 移出回收站 2. 删除
编辑分类: 删除与新增两个功能
展示某分类对应的文章
代码:
路由层 BBS\urls.py
点击查看代码
# 后台管理
url(r'^backend/$',views.backend),
# 后台展示分类文章
url(r'^backend/(?P<condition>category)/(?P<param>\d+)/',views.backend),
# 后台回收站文章展示
url(r'^backend/recycler/',views.recycler),
# 添加文章
url(r'^add/article/',views.add_article),
# 添加文章
url(r'^change/article/(?P<param>\d+)/',views.add_article),
# 删除文章
url(r'^delete/article/(?P<param>\d+)/',views.delete_article),
# 编辑|新增|删除分类
url(r'^(?P<action>edit|add|delete)/category/',views.edit_category),
# 上传图片
url(r'^upload_img/',views.upload_img),
视图层 app01\views.py
点击查看代码
# 后台管理主页面
def backend(request,**kwargs):
category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
tag_list = models.Tag.objects.filter(blog=request.user.blog)
# 展示该用户全部文章
article_obj_list = models.Article.objects.filter(blog=request.user.blog,delete=0)
if kwargs:
param = kwargs['param']
# 展示某个分类的文章
article_obj_list = models.Article.objects.filter(blog=request.user.blog,category_id=param,delete=0)
return render(request,'backend/backend.html',locals())
from bs4 import BeautifulSoup
# 新增文章 或 编辑旧文章的编辑页面 与 数据提交
def add_article(request,**kwargs):
title = ''
content = ''
category_id = ''
tag_id_list = ''
category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
tag_list = models.Tag.objects.filter(blog=request.user.blog)
# 如果 kwargs有值就是编辑旧的文章,需要获取旧文章数据展示到编辑页面
if kwargs:
article_id = kwargs['param']
article_obj = models.Article.objects.filter(pk=article_id).first()
title = article_obj.title
content = article_obj.content
category_id = article_obj.category_id
tags_list = article_obj.tags.all()
tag_id_list = [i.pk for i in tags_list]
# 获取提交的文章数据保存到数据库
if request.method == 'POST':
title = request.POST.get('title')
content = request.POST.get('content')
category_id = request.POST.get('category_id')
tag_id_list = request.POST.getlist('tag_id')
article_id = request.POST.get('active')
# bs4的使用 'html.parser'为python自带的html解析器
soup = BeautifulSoup(content,'html.parser')
# 获取到所有标签
content_eles = soup.find_all()
# content_tag是一个一个的标签对象
for content_ele in content_eles:
# 将提交内容中的script标签删掉,防止xss攻击
if content_ele.name == 'script':
content_ele.decompose()
# 获取到内容中的纯文本,截取前150个字符为desc字段
# print(type(soup)) # <class 'bs4.BeautifulSoup'> 存储上要用str(soup)
desc = soup.text[0:150]
# 新增文章
if article_id == '0':
article_obj = models.Article.objects.create(
title=title,
desc=desc,
content=str(soup),
blog=request.user.blog,
category_id=category_id,
)
# 修改文章
else:
article_obj = models.Article.objects.filter(pk=article_id).first()
article_obj.title=title
article_obj.desc=desc
article_obj.content=str(soup)
article_obj.category_id=category_id
article_obj.save()
models.ArticleToTag.objects.filter(article_id=article_id).delete()
# 存储tag bulk_create批量创建tag数据
articleToTag_obj_list = []
for tag_id in tag_id_list:
articleToTag_obj = models.ArticleToTag(
article = article_obj,
tag_id = tag_id,
)
articleToTag_obj_list.append(articleToTag_obj)
models.ArticleToTag.objects.bulk_create(articleToTag_obj_list)
return redirect('/backend/')
# 展示编辑页面
return render(request,'backend/add_article.html',locals())
import time
# 文本编辑器上传图片保存
def upload_img(request):
# 富文本编辑器上传图片返回固定格式数据
back_dic={
"error" : 0,
"url" : ""
}
img = request.FILES.get('imgFile')
# 获取到文件的扩展名
file_type = img.name.split('.')[-1]
# 拼接不会重复的文件名
filename=str(time.time()).replace('.','_')+'.%s' %file_type
# 将文件保存到对应文件夹
with open('media/img/%s' %filename,'wb') as f:
for line in img:
f.write(line)
back_dic['url']='/media/img/%s' %filename
return JsonResponse(back_dic)
# 删除文章 包括完全删除与放到回收站两种
def delete_article(request,param):
back_dic = {'code':1000}
if request.method == 'POST':
# from_p区分是/backend/页面删除还是回收站删除
from_p = request.POST.get('from')
delete_id = request.POST.get('delete_id')
if from_p == "backend":
models.Article.objects.filter(pk=delete_id).update(delete='1')
elif from_p == "recycler":
models.Article.objects.filter(pk=delete_id).delete()
return JsonResponse(back_dic)
# 回收站
def recycler(request):
article_obj_list = models.Article.objects.filter(blog=request.user.blog,delete=1)
category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
#移出回收站
back_dic = {'code':1000}
if request.method == 'POST':
move_id = request.POST.get('move_id')
models.Article.objects.filter(pk=move_id).update(delete='0')
return JsonResponse(back_dic)
# 展示回收站文章
return render(request,'backend/recycler.html',locals())
# 编辑分类 包括 展示/删除/新增
def edit_category(request,action):
category_list = models.Category.objects.filter(blog=request.user.blog,delete=0).annotate(article_c=Count("article__pk")).values_list("pk","name","article_c")
if request.method == 'POST':
back_dic = {"code":1000,'url':'/edit/category/'}
# 删除
if action == 'delete':
delete_id = request.POST.get('delete_id')
models.Category.objects.filter(pk=delete_id).update(delete=1)
# 新增
elif action == 'add':
category_name = request.POST.get('category_name')
if category_name:
models.Category.objects.create(name=category_name,blog=request.user.blog)
else:
back_dic['code'] = 1001
back_dic['msg'] = "类名不能为空"
return JsonResponse(back_dic)
# 展示分类
return render(request,'backend/show_category.html',locals())
模板层 后台页面放到 templates/backend/下
模板页面 templates\backend\backend_base.html
点击查看代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"></script>
{% load static %}
<link rel="stylesheet" href="{% static 'others/bootstrap-3.4.1/css/bootstrap.css' %}">
<script src="{% static 'others/bootstrap-3.4.1/js/bootstrap.js' %}"></script>
<script src="{% static '/js/mysetup.js' %}"></script>
<title>BBS</title>
<script charset="utf-8" src="{% static 'others/kindeditor/kindeditor-all.js' %} "></script>
{% block css_part %}
{% endblock css_part %}
</head>
<body>
<!-- 导航条 -->
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/home/">BBS</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
<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="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="/backend/">后台管理</a></li>
<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">
<!-- 添加 data-toggle="modal" data-target=".bs-example-modal-lg属性后点击此标签弹出模态框 -->
<li><a href="" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
<!-- 添加 data-toggle="modal" data-target=".vatar-modal-lg" 点击弹出更换头像的模态框 -->
<li><a href="" data-toggle="modal" data-target=".vatar-modal-lg">更换头像</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'logout' %}">退出登录</a></li>
</ul>
</li>
{% else %}
<li><a href="/login/">登录</a></li>
<li><a href="/register/">注册</a></li>
{% endif %}
</ul>
</li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<!-- 修改密码模态框 将data-toggle="modal" data-target=".bs-example-modal-lg"加到点击密码的a标签-->
<!-- <button type="button" class="btn btn-primary" data-toggle="modal" data-target=".bs-example-modal-lg" >Large modal</button> -->
<!-- 这里的class中还需要加上bs-example-modal-lg才能弹出 -->
<div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h2 class="text-center">修改密码</h2>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="form-group">
<label for="username">用户名</label>
<input type="text" class="form-control" id="username" value="{{ request.user.username }}" disabled>
</div>
<div class="form-group">
<label for="old_password">原密码</label>
<input type="password" class="form-control" id="old_password">
</div>
<div class="form-group">
<label for="new_password">新密码</label>
<input type="password" class="form-control" id="new_password">
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<input type="password" class="form-control" id="confirm_password">
</div>
<div class="modal-footer">
<span class="pull-left" id="prompt" style="color: red;"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="change_pwd_btn">确认</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 修改头像模态框 -->
<div class="modal fade vatar-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h2 class="text-center">更换头像</h2>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<label for="id_avatar">
头像:
<img src="/media/{{ request.user.avatar }}" alt="" style=" 150px;" id="img_avatar">
<!-- <img src="{% static 'img/default.png' %}" alt="" id='myimg' style=" 100px;margin-left: 10px;"> -->
</label>
<input type="file" id="id_avatar" style="display:none">
<div class="modal-footer">
<span class="pull-left" id="prompt" style="color: red;"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="change_vatar_btn">确认</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 2-10布局 -->
<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-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">
<a href="/add/article/">添加文章</a>
</div>
<div class="panel-body">
<a href="">草稿箱</a>
</div>
<div class="panel-body">
<a href="/backend/recycler/">回收站</a>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingTwo">
<h4 class="panel-title">
<a class="" role="button" data-parent="#accordion" href="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo">
分类
</a>
</h4>
</div>
<div id="collapseTwo" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingTwo" aria-expanded="true">
<div class="panel-body">
<a href="/edit/category/">编辑分类</a>
</div>
{% for category in category_list %}
<div class="panel-body">
<a href="/backend/category/{{category.0}}">{{category.1}}({{category.2}})</a>
</div>
{% endfor %}
</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 article %}
</div>
<div role="tabpanel" class="tab-pane" id="profile">
{% block comment %}
{% endblock comment %}
</div>
<div role="tabpanel" class="tab-pane" id="messages">
{% block tag %}
{% endblock tag %}
</div>
<div role="tabpanel" class="tab-pane" id="settings">
{% block file %}
{% endblock file %}
</div>
</div>
</div>
</div>
</div>
<script>
// 修改密码使用ajax提交信息
$('#change_pwd_btn').click(function(){
$.ajax({
url: "{% url 'setPassword' %}",
method: 'post',
data: {'old_password':$('#old_password').val(),'new_password':$('#new_password').val(),'confirm_password':$('#confirm_password').val()},
success: function(args){
if (args.code == 1000){
$('#prompt').text(args.msg).attr('style','color: blue');
window.location.reload()
}
else {$('#prompt').text(args.msg)}
}
})
})
// 修改头像处上传后更新展示
$('#id_avatar').change(function(){
// 文件阅读器对象
// 1. 生成一个文件阅读器
let myFileReaderObj = new FileReader();
// 2. 获取用户上传的头像文件
let fileObj = $(this)[0].files[0];
// 3. 将文件对象交给阅读器处理
myFileReaderObj.readAsDataURL(fileObj) // 异步 IO操作 所以这里文件还没读完就会执行下一行代码
// 4. 利用阅读器将文件展示到前端页面
myFileReaderObj.onload = function(){ // onload 等待myFileReaderObj对象的任务都执行完成才执行
$('#img_avatar').attr('src',myFileReaderObj.result)
}
})
// 修改头像提交
$('#change_vatar_btn').click(function(){
let formDataObj = new FormData();
formDataObj.append('new_avatar',$('#id_avatar')[0].files[0])
$.ajax({
url: "{% url 'changeAvatar' %}",
method: 'post',
data: formDataObj,
contentType: false,
processData: false,
success: function(args){
if (args.code==1000){window.location.reload()}
else{window.location.reload()}
},
})
})
</script>
{% block js_part %}
{% endblock js_part %}
</body>
</html>
templates\backend\add_article.html
点击查看代码
{% extends "backend\backend_base.html" %}
{% block css_part %}
<style>
h4 {
font-weight: bold;
border-style: solid none dashed none;
border-thin;
border-color:rgb(150, 144, 144);
padding: 10px;
background-color:rgb(206, 202, 202)
}
</style>
{% endblock css_part %}
{% block article %}
<div style="margin-right: 30px;background-color: rgb(228, 228, 228);">
<h4 >添加文章</h4>
<form action="/add/article/" method="post">
{% csrf_token %}
<p style="margin-top: 20px;margin-left: 10px; font-weight: bold;font-size: large;">标题</p>
<!-- 判断是更改 还是 新增 -->
{% if title %}
<input type="text" name="active" value="{{article_obj.pk}}" style="display: none;">
{% else %}
<input type="text" name="active" value="0" style="display: none;">
{% endif %}
<input type="text" name="title" value="{{ title }}" class="form-control" style="font-weight: bold; border-style: solid ;border-medium;height: 40px;">
<!-- 编辑框 -->
<textarea id="edit" name="content" value="{{ content }}">
</textarea>
<h4 >文章分类</h4>
{% for category_obj in category_list %}
{% if category_obj.0 == category_id %}
<label><input type="radio" name="category_id" value="{{ category_obj.0 }}" checked>{{ category_obj.1 }}</label>
{% else %}
<label><input type="radio" name="category_id" value="{{ category_obj.0 }}" >{{ category_obj.1 }}</label>
{% endif %}
{% endfor %}
<h4>文章标签</h4>
{% for tag_obj in tag_list %}
{% if tag_obj.pk in tag_id_list %}
<label><input type="checkbox" name="tag_id" value="{{ tag_obj.pk }}" checked> {{ tag_obj.name }}</label>
{% else %}
<label><input type="checkbox" name="tag_id" value="{{ tag_obj.pk }}"> {{ tag_obj.name }}</label>
{% endif %}
{% endfor %}
<hr>
<div ><button type="submit" class="btn btn-primary pull-right"> 发布 </button></div>
</form>
</div>
{% endblock article %}
{% block js_part %}
<script>
// 加载富文本编辑器
KindEditor.ready(function(K) {
window.editor = K.create('#edit',{
'100%', // 编辑框宽度设置
height: '600px', // 编辑框高度设置
resizeType: 1, // resizeType为1,表示也没事编辑框高度可调,宽度不可变
// 以下都是上传图片/文件相关配置
uploadJson : '/upload_img/', // 上传文件的url
extraFileUploadParams : {
'csrfmiddlewaretoken':'{{ csrf_token }}', // 额外携带的参数
}
})
window.editor.html($('textarea[name=content]').attr('value'))
});
</script>
{% endblock js_part %}
templates\backend\backend.html
点击查看代码
{% extends "backend\backend_base.html" %}
{% block article %}
<br>
<table class="table table-striped">
<thead>
<tr>
<th>标题</th>
<th>发布时间</th>
<th>评论数</th>
<th>点赞数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article_obj in article_obj_list %}
<tr>
<td><a href="/{{article_obj.blog.userinfo}}/article/{{article_obj.pk}}">{{article_obj.title}}</a></td>
<td>{{article_obj.create_time|date:'Y-m-d'}}</td>
<td>{{article_obj.comment_num}}</td>
<td>{{article_obj.up_num}}</td>
<td><a href="/change/article/{{ article_obj.pk }}">编辑</a></td>
<td><a class="delete_article" article_id="{{ article_obj.pk }}">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock article %}
{% block js_part %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
$(".delete_article").click(function(){
// 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
let delete_id = $(this).attr('article_id')
url = '/delete/article/'+delete_id+'/'
from = 'backend'
swal({
title: "请再次确认?", // 弹出框标题
text: "删除后可以在回收站找回", // 弹出框正文
type: "warning", // 弹出框类型,图标显示不同,有四种:warning,error,success,info
showCancelButton: true, // 显示取消按钮(确认按钮默认显示)
cancelButtonClass: "btn-success", // 取消按钮为btn-success样式,蓝色
cancelButtonText: "取消", // 取消按钮显示内容
confirmButtonClass: "btn-danger", // 确认按钮样式,红色
confirmButtonText: "删除", // 确认按钮显示内容
closeOnConfirm: false, // false表示点击确认按钮不关闭弹框
},
function(){ // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
$.ajax({
url: url,
type: 'post',
data: {'delete_id':delete_id,'from':from},
success: function(args){
if (args.code === 1000){
// 弹窗显示已删除,点击确认后刷新页面
swal({title:"已删除!!!",},function(){window.location.reload()})
}
else {
swal("未知错误!!!")}
}
})
});
})
</script>
{% endblock js_part %}
templates\backend\recycler.html
点击查看代码
{% extends "backend\backend_base.html" %}
{% block article %}
<br>
<table class="table table-striped">
<thead>
<tr>
<th>标题</th>
<th>发布时间</th>
<th>评论数</th>
<th>点赞数</th>
<th>操作</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for article_obj in article_obj_list %}
<tr>
<td><a href="/{{article_obj.blog.userinfo}}/article/{{article_obj.pk}}">{{article_obj.title}}</a></td>
<td>{{article_obj.create_time|date:'Y-m-d'}}</td>
<td>{{article_obj.comment_num}}</td>
<td>{{article_obj.up_num}}</td>
<td><a class="moveout" article_id="{{ article_obj.pk }}">移出回收站</a></td>
<td><a class="delete_article" article_id="{{ article_obj.pk }}">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock article %}
{% block js_part %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
$(".delete_article").click(function(){
// 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
let delete_id = $(this).attr('article_id')
url = '/delete/article/'+delete_id+'/'
from = 'recycler'
swal({
title: "请再次确认?", // 弹出框标题
text: "删除后将无法找回", // 弹出框正文
type: "warning", // 弹出框类型,图标显示不同,有四种:warning,error,success,info
showCancelButton: true, // 显示取消按钮(确认按钮默认显示)
cancelButtonClass: "btn-success", // 取消按钮为btn-success样式,蓝色
cancelButtonText: "取消", // 取消按钮显示内容
confirmButtonClass: "btn-danger", // 确认按钮样式,红色
confirmButtonText: "删除", // 确认按钮显示内容
closeOnConfirm: false, // false表示点击确认按钮不关闭弹框
},
function(){ // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
$.ajax({
url: url,
type: 'post',
data: {'delete_id':delete_id,'from':from},
success: function(args){
if (args.code === 1000){
// 弹窗显示已删除,点击确认后刷新页面
swal({title:"已删除!!!",},function(){window.location.reload()})
}
else {
swal("未知错误!!!")}
}
})
});
})
$('.moveout').click(function(){
let move_id = $(this).attr('article_id')
$.ajax({
url: '/backend/recycler/',
type: 'post',
data: {'move_id':move_id},
success: function(args){
if (args.code === 1000){
window.location.href="/backend/"
}
else {
swal("未知错误!!!")
}
}
})
})
</script>
{% endblock js_part %}
templates\backend\show_category.html
点击查看代码
{% extends "backend\backend_base.html" %}
{% block article %}
<br>
<button class="btn-primary btn pull-right" style="margin-right: 50px;" data-toggle="modal" data-target=".category-modal-lg"> 新增 </button>
<br>
<table class="table table-striped">
<thead>
<tr>
<th>分类</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for category_obj in category_list %}
<tr>
<td>{{category_obj.1}}</td>
<td><a class="delete_category" category_id="{{ category_obj.0 }}">删除</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 新增分类模态框 -->
<div class="modal fade category-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h2 class="text-center">新增分类</h2>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="form-group">
<label for="new_category">新类名</label>
<input type="text" class="form-control" id="new_category">
</div>
<div class="modal-footer">
<span class="pull-left" id="prompt" style="color: red;"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="add_category">确认</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock article %}
{% block js_part %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.css" integrity="sha512-f8gN/IhfI+0E9Fc/LKtjVq4ywfhYAVeMGKsECzDUHcFJ5teVwvKTqizm+5a84FINhfrgdvjX8hEJbem2io1iTA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-sweetalert/1.0.1/sweetalert.js" integrity="sha512-XVz1P4Cymt04puwm5OITPm5gylyyj5vkahvf64T8xlt/ybeTpz4oHqJVIeDtDoF5kSrXMOUmdYewE4JS/4RWAA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
$(".delete_category").click(function(){
// 在按钮的属性中定义该条数据的主键,再拿到主键值,传递给后端
let delete_id = $(this).attr('category_id')
swal({
title: "请再次确认?", // 弹出框标题
text: "删除后将无法找回", // 弹出框正文
type: "warning", // 弹出框类型,图标显示不同,有四种:warning,error,success,info
showCancelButton: true, // 显示取消按钮(确认按钮默认显示)
cancelButtonClass: "btn-success", // 取消按钮为btn-success样式,蓝色
cancelButtonText: "取消", // 取消按钮显示内容
confirmButtonClass: "btn-danger", // 确认按钮样式,红色
confirmButtonText: "删除", // 确认按钮显示内容
closeOnConfirm: false, // false表示点击确认按钮不关闭弹框
},
function(){ // 点击确认后会触发这个function,点击取消后弹窗会关闭,不触发。
$.ajax({
url: '/delete/category/',
type: 'post',
data: {'delete_id':delete_id},
success: function(args){
if (args.code === 1000){
// 弹窗显示已删除,点击确认后刷新页面
swal({title:"已删除!!!",},function(){window.location.reload()})
}
else {
swal("未知错误!!!")}
}
})
});
})
// 新增分类
$('#add_category').click(function(){
$.ajax({
url: "/add/category/",
method: 'post',
data: {'category_name':$('#new_category').val()},
success: function(args){
if (args.code == 1000){
window.location.href=args.url
}
else {$('#prompt').text(args.msg)}
}
})
})
</script>
{% endblock js_part %}
后续bug调试
1. 用户注册时,要自动创建对应的blog
2. tag标签没有写添加与删除
3. 草稿箱功能以及后台管理的评论、标签、文件都没写
4. 导航条很多选项没有用到
...
完整代码(这个版本只有主要功能,且还有bug):
https://files.cnblogs.com/files/huandada/BBS.zip