BBS项目练习一(模型表、注册、登录、站点管理)
BBS项目练习一
项目启动
BBS是电子报系统,我们将参考博客园界面来进行需求分析和项目搭建。
需求分析
表层功能需求(大致)
- 博客园,有一个首页,首页用于展示文章,提供一些其他站内链接,可能还包括广告展示等。
- 用户可以注册、登录、修改密码、注销登录、修改头像等
- 用户可以申请个人站点,申请后可以添加,编辑,删除自己的文章
- 用户可以查看文章列表,在首页展示全部文章列表,在个人站点显示站点的所有文章,文章可以被分类、标签、归档,用户可以根据这些筛选条件查看筛选的文章列表展示
- 点击某篇文章的标题进入文章详情页,文章详情页应该拥有文章具体内容,个人站点相关链接,点赞点踩功能,评论功能等。
数据结构分析(大致)
根据上述的功能,我们可以大概的整理一下,一个bbs项目需要哪些数据,这些数据的关系是什么。
- 用户数据:每个用户拥有一些各自的权限,如开设自己的站点,对自己的站点进行管理。也有一些共有的权限,如查看站点内的文章。
- 个人站点数据:用户可以选择而开设和不开设站点,站点数据和用户是一对一的关系,两者深度绑定
- 文章数据:文章可以被某个个人站点发布和管理,可以被所有用户查看和点赞评论交互
- 文章分类数据:个人站点可以设置文章分类来方便的管理文章,文章分类属于站点,文章首先绑定站点,其次可以绑定文章分类
- 文章标签数据:与文章分类很类似,不同的是,一篇文章只能在一个分类中,但可以拥有多个标签,相当于文章与标签是多对多的关系
- 点赞点踩关系表:关联一个用户,关联一篇文章,记录点赞点踩的数据,表示出用户对文章的点赞点踩操作。
- 评论表:关联一个用户,关联一篇文章,记录评论的数据,表示出用户对文章的评论操作。其次可以增加一个自关联外键,让评论可以是其他评论的回复(子评论)
以上的数据就决定了七张信息表的基本数据结构。
django项目启动
启动一个django项目,配置数据库路径,模板层路径,项目语言环境和时区等
项目语言环境和时区
# settings.py
LANGUAGE_CODE = 'zh-hans' # 中文语言环境
TIME_ZONE = 'Asia/Shanghai' # 时区选择东八区
USE_TZ = True
# 会将存入数据库的时区时间(如这里设置的东八区)转存为0时区时间,拿出来时再转换为时区时间
功能划分和路由分发
bbs项目本身的功能可以划分为好几个app分别完成,我们根据功能可以设置以下几个app:
-
数据处理和后台管理:datas_administer
这里主要存放模型表,附带超级用户管理数据的注册,相当于将数据表集中到一个app,其他的app就不必再写模型表了。
-
用户数据相关:user_app
用户注册、登录、修改密码、注销登录、修改头像等功能
-
公共区域功能:public_app
首页展示、个人站点展示、个人站点按分类|标签|归档展示、文章详情页、点赞点踩、评论功能
-
个人站点管理:backend_app
个人站点后台管理页面、添加文章数据、删除文章、编辑文章、分类标签增删改
路由分发:
"""
我们需要每个app中设置urls.py文件,分别编写各app的路由
项目总文件夹的路由文件,只需要按照以下统合即可
"""
# urls.py
from django.contrib import admin
from django.urls import path
from django.urls import include
urlpatterns = [
path('admin/', admin.site.urls),
path('user/', include("user_app.urls")), # include中传入其他路由文件的路径即可
path('public/', include("public_app.urls")),
path('backend/', include("backend_app.urls")),
]
七张信息表
- 用户表(与站点一对一)
- 继承AbstractUser
- 电话号码
- 用户头像
- 站点外键
- 站点表
- 站点域名
- 站点标题
- 站点主题
- 文章表(与站点一对多)
- 标题
- 摘要
- 内容
- 发布时间
- 点赞、点踩、评论数 三个字段(注意更新)
- 站点外键
- 分类外键(set_null,不随分类消失而消失)
- 标签外键(多对多)
- 文章分类表
- 分类名称
- 站点外键
- 文章标签表
- 标签名称
- 标签外键
- 点赞点踩表
- 用户外键
- 文章外键
- 点赞or点踩
- 评论表
- 用户外键
- 文章外键
- 评论内容
- 评论时间
- 父评论自关联外键
一些注意:
- 建立用户信息表:通过django提供的auth模块,快速的拓展建立用户表
- 建立一些表时需要时间字段:用户表(用户类自带),评论表,文章表,来支持时间展示的业务
- 点赞点踩写评论要修改所有相关的表和字段,字段即文章表中的点赞点踩评论数
建立以上七张表的模型表:
七张模型表
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class UserInfo(AbstractUser): # 用户表继承auth模块中的用户类
tel = models.CharField(verbose_name='电话号码', max_length=32)
avatar = models.FileField(verbose_name='个人头像', upload_to='avatar/')
# register_time = models.DateTimeField(verbose_name='注册时间', auto_now_add=True)
# 其实本身就有有data_joined字段,这里可以不用写,或者同名覆盖掉
site = models.ForeignKey(to='Site', on_delete=models.SET_NULL, null=True, blank=True)
class Site(models.Model):
name = models.CharField(verbose_name='站点域名', max_length=32)
title = models.CharField(verbose_name='站点标题', max_length=32)
theme = models.FileField(verbose_name='站点样式', upload_to='css/')
class Article(models.Model):
title = models.CharField(verbose_name='文章标题', max_length=32)
brief = models.CharField(verbose_name='文章简介', max_length=100)
content = models.TextField(verbose_name='文章内容')
publish_time = models.DateField(verbose_name='发布日期', auto_now_add=True)
# 当执行点赞点踩评论表的修改时,实时更新下列数据
like_num = models.IntegerField(verbose_name='点赞数目')
unlike_num = models.IntegerField(verbose_name='点踩数目')
comment_num = models.IntegerField(verbose_name='评论数目')
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
category = models.ForeignKey(to='Category', on_delete=models.SET_NULL, null=True)
tags = models.ManyToManyField(to='Tag')
class Category(models.Model):
name = models.CharField(verbose_name='分类名称', max_length=32)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
class Tag(models.Model):
name = models.CharField(verbose_name='标签名称', max_length=32)
site = models.ForeignKey(to='Site', on_delete=models.CASCADE)
class LikeOrNot(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
is_like = models.BooleanField(verbose_name='点赞或点踩') # 存1点赞,存0点踩
class Comment(models.Model):
user = models.ForeignKey(to='UserInfo', on_delete=models.CASCADE)
article = models.ForeignKey(to='Article', on_delete=models.CASCADE)
content = models.TextField(verbose_name='评论内容', max_length=100)
comment_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
parent = models.ForeignKey(to='self', on_delete=models.CASCADE)
注册功能
此次尝试通过ajax请求来完成提交表单,这种方式提交表单在代码层面会有一些复杂度,但也能够满足更多的业务逻辑。
注册代码总览
register视图层
视图层在get请求过来时会返回一个纯净的表单界面,而在post请求过来时会进行一系列的校验,并返回一个可能有含错误信息的表单界面。
视图层register代码
def register(request):
register_obj = myforms.RegisterForm() # 将表单类直接单独存放到另外的py文件
if request.method == 'POST':
# 用信息的表单类
register_obj = myforms.RegisterForm(request.POST)
# ajax请求通常以json格式的自定义对象(字典)通信
back_dict = {}
if register_obj.is_valid():
# 如果表单数据符合要求,处理数据,并完成注册
back_dict['code'] = 10000 # 自定义状态码
cleaned_data = register_obj.cleaned_data
cleaned_data.pop('confirm_password')
# 如果没有那就不给这个生成这个键,创建数据时按默认头像。
if request.FILES.get('avatar'):
cleaned_data['avatar'] = request.FILES.get('avatar')
models.UserInfo.objects.create_user(**cleaned_data)
back_dict['url'] = reverse('login')
back_dict['msg'] = '注册成功'
else:
# 否则返回错误信息
back_dict['code'] = 10001
back_dict['msg'] = register_obj.errors
return JsonResponse(back_dict)
return render(request, 'registerPage.html', locals())
registerPage模板层
主要实现了以下内容:
- 渲染标签
- 提交表单的ajax请求,并处理后端返回过来的信息
- 如果是注册成功的状态码,跳转到指定界面
- 如果是其他状态码,说明错误,将错误信息渲染到界面上
- 当用户上传头像图片时,实时将其渲染到界面上(纯前端)
- 当用户聚焦到输入框时,将错误信息及样式移除(纯前端)
模板层
<div class="container">
<div class="col-md-6 col-md-offset-3">
<!--表单标签-->
<form id="regi_form" action="" method="post">
<!--csrf校验-->
{% csrf_token %}
<!--循环生成form组件的input标签-->
{% for regi in register_obj %}
<div class="form-group">
<label for="{{ regi.auto_id }}">{{ regi.label }}</label>
{{ regi }}
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<!--头像提交是单独的-->
<div class="form-group">
<label for="avatar">头像
<img id="myimg" src="{% static '默认头像.png' %}"
style="width:120px; box-shadow: 5px 5px 5px gray; border-radius: 50%"
alt="">
</label>
<input type="file" id="avatar" name="avatar" value="{% static '默认头像.png' %}" style="display: none">
<!--提交按钮-->
</div>
<div class="form-group">
<input type="button" id="regi_submit" value="提交" class="btn btn-success form-control">
</div>
</form>
</div>
</div>
<!--js代码-->
<script>
// 注册按钮发送ajax
$('#regi_submit').click(function () {
let newFormObj = new FormData()
let regiObj = $('#regi_form').serializeArray()
$.each(regiObj, function (index, regiObj) {
newFormObj.append(regiObj.name, regiObj.value)
})
newFormObj.append('avatar', $('#avatar')[0].files[0])
$.ajax({
url: '',
type: 'post',
data: newFormObj,
contentType: false,
processData: false,
success: function (args) {
if (args.code === 10000){
window.location.href = args.url
}else{
let eleErrors = args.msg
$.each(eleErrors,function (key, errorsList) {
$('#id_' + key).next().text(errorsList[0]).parent().addClass('has-error')
}) // each
} //else
} // success
}) // ajax
}) // click
// 上传图片文件时实时展示到页面上
$('#avatar').change(function () {
let fileReaderObj = new FileReader()
fileReaderObj.readAsDataURL(this.files[0])
fileReaderObj.onload = function () {
$('#myimg').attr('src', fileReaderObj.result)
}
})
// 聚焦时,移除错误信息
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error')
})
</script>
RegisterForm表单类
表单类中除了帮助我们快速生成标签,也可以单独的添加widget参数来修改属性,包括class属性,所以可以利用这一点修改样式。
注册form组件
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8, label='用户名',
widget=forms.widgets.TextInput(
attrs={
'class': 'form-control'
}
)
)
password = forms.CharField(min_length=3, max_length=8, label='密码',
widget=forms.widgets.PasswordInput(
attrs={
'class': 'form-control'
}
)
)
confirm_password = forms.CharField(min_length=3, max_length=8, label='确认密码',
widget=forms.widgets.PasswordInput(
attrs={
'class': 'form-control'
}
)
)
email = forms.EmailField(label='邮箱', widget=forms.widgets.EmailInput(
attrs={
'class': 'form-control'
}
))
tel = forms.CharField(label='电话', required=False,
validators=[
RegexValidator('^1[0-9]{4}$', '号码必须是1开头的5位数字'),
],
widget=forms.widgets.TextInput(
attrs={
'class': 'form-control'
}
)
)
# 钩子函数
def clean_username(self):
username = self.cleaned_data.get('username')
user_obj = models.UserInfo.objects.filter(username=username)
if user_obj:
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
注册的业务逻辑总结
form表单序列化
let newFormObj = new FormData()
let regiObj = $('#regi_form').serializeArray() // 表单标签序列化,将表单中的value数据序列化成数组[{},{},{}]
$.each(regiObj, function (index, regi) {
newFormObj.append(regi.name, regi.value)
}) // 这里循环添加数据
newFormObj.append('avatar', $('#avatar')[0].files[0]) // 文件数据单独处理
利用表单序列化,将数据变成可迭代的数组,以便通过循环拿到表单的数据,也可以将其添加到提交的数据中。我们是通过js的内置对象FormData产生的对象来接收数据并将其作为ajax的data数据提交。
// 循环也可以这么写
for (let index in regiObj){
let regi = regiObj[index]
newFormObj.append(regi.name, regi.value)
}
// 两种循环都是前端的语法,第一种类似于对有序列表进行枚举,每次拿到索引和item,第二种则每次只有索引
ajax请求逻辑
我们通过ajax请求发送表单数据,相较于form本身的input标签提交,有哪些不同呢:
ajax请求特点
-
ajax有回调函数,可以选择向用户发送信息,也可以选择跳转界面,这些逻辑都可以在success的回调函数中进行编写。
-
有时,一些业务逻辑会提示用户一些信息,但是提示完后又会跳转界面,这无法通过表单标签的post请求实现。
success: function(args){ // 根据args返回的字典信息(接口信息),进行判断和逻辑编写 if (args.code === 10000){ alert(args.msg) // 跳转前先向用户提示一些信息 window.location.href = args.url // 10000状态码则跳转网页 }else{ $('span').text(args.msg) // 如果是其他状态码,则渲染一些信息到页面上 } }
form表单提交post请求特点
- form表单提交后,会立即刷新界面,页面上的元素都是重新渲染的结果,也就意味着如果想保存用户输入的数据需要发送到后端再传回前端(这个过程可以通过form组件很好的完成)
- form表单有固定的提交方式(点击提交标签),ajax则是通过绑定事件函数提交的,提交更灵活。
ajax回调渲染错误提示
由于后端采取form组件,自动渲染了input标签,所以我们并不清楚标签的id值,就没办法将json数据中的error信息精确的投放到对应的input标签附近。
- 我们可以去网页上查看,寻找出input标签id规律为
id_组件的name
- 也可以通过表单字段的widget自己添加一些属性方便自己定位
这里采取第一种,由于得知了input标签id值的规律,所以我们可以将返回的json数据里的错误信息组织成组件name:错误信息
的键值对形式,for循环将信息根据id值渲染到对应的标签处。
success: function(args){
// 错误信息展示部分
for (let i in args.error_list){
error = error_list[i]
let inputEleId = '#id_' + error.name
$(inputEleId).next().text(error.value).parent().addClass('has-error')
// 先将隔壁的span提示标签的文本显示出来
// 再将父标签的样式设置成错误状(form-group泛红)
}
}
上传图片文件时实时显示到界面上
在编写上传头像功能时,我们希望用户能够看到自己上传的图片文件做预览,那么上传后自动处理到页面就是整体的业务逻辑,进行拆分可以分为以下几步:
- 上传图片时即是提交图片的标签的value值发生了变化
- 将变化事件绑定一个函数,函数用于将用户上传的图片读取出来并显示到页面上
- 读取图片需要用到内置对象FileReader,它内置有一个方法可以将图片按url方式读取
- 将上述方法得到的url赋值给图片标签的src属性
$('#img_file').change(function(){
let imgReaderObj = new FileReader() // 产生一个文件读取的内置对象
imgReaderObj.readAsDataURL(this.files[0]) // 内置对象将标签中上传的文件处理成url
imgReaderObj.onload = function(){ // 等待读取完成后(防止未加载好url就进行以下步骤)
$('img#img_show').attr('src', imgReaderObj.result) // 将img标签的src换为url结果
}
})
注册功能进阶
上述注册功能是对个人用户的注册,下面将一对一用户和站点两份数据一块注册,实现效果如下:
我们需要对功能的逻辑进行梳理:
- 用户不注册站点时可以注册用户,只校验用户输入是否正确,正确则创建用户数据,注册成功跳转界面,输入有误显示错误信息。
- 用户注册站点时会同时注册用户和站点,那么两者都需要进行校验,有一方错误都不创建用户或站点,两者同时校验成功才创建,并在前端跳转界面。
对以上两种方向的内容进行归纳:
- 有错误信息,那就让返回的字典的code值不为10000,前端会打印错误信息
- 无错误信息,那就对是否建立站点进行判断,如果不建立,则直接创建用户,如果建立还要创建站点。
- 对用户的输入进行判断,记录是否出错,并准备用于用户注册的信息或者返回的用户错误信息
- 对站点的输入数据进行判断,记录是否出错,并准备站点注册的信息或者返回的站点注册信息
- 依据两者的校验记录,两者输入信息都有效则执行注册,如果有一方错误则执行返回错误。
def register(request):
if request.method == 'POST':
back_dict = {'code': 10000, 'msg': '', 'errors': {}}
# 1.先检查用户是否输入正确
register_form_obj = myforms.RegisterForm(request.POST)
is_regi_valid = register_form_obj.is_valid()
new_user_data = {}
if is_regi_valid:
# 用户信息输入正确则为创建用户数据做准备处理
new_user_data = register_form_obj.cleaned_data
new_user_data.pop('confirm_password')
# 如果avatar有值则赋值,没有值则不添加,不能为null,会覆盖默认值
user_avatar = request.FILES.get('avatar')
if user_avatar:
new_user_data['avatar'] = user_avatar
else:
# 输入错误则添加报错信息
back_dict['code'] = 10001
back_dict['errors'].update(register_form_obj.errors)
# 2.再检测是否创建站点
is_create_site = request.POST.get('is_create_site')
is_site_valid = False # 提前预设变量以便后续使用
if is_create_site:
# 如果创建站点,则校验站点信息输入
site_form_obj = myforms.SiteForm(request.POST)
is_site_valid = site_form_obj.is_valid()
if is_site_valid:
# 如果输入正确,则为创建站点数据做准备处理
new_site_data = site_form_obj.cleaned_data
else:
# 如果输入错误,则添加报错信息
back_dict['code'] = 10001
back_dict['errors'].update(site_form_obj.errors)
if is_regi_valid:
# 3.所有信息都校验成功,后端统一再创建数据,前端给个跳转网址
if not is_create_site:
models.User.objects.create_user(**new_user_data)
elif is_create_site and is_site_valid:
new_site_obj = models.Site.objects.create(**new_site_data)
models.User.objects.create_user(site=new_site_obj, **new_user_data)
back_dict['url'] = '/login/'
back_dict['msg'] = '注册成功!'
return JsonResponse(back_dict)
register_form_obj = myforms.RegisterForm() # 让模板语法渲染用户注册的标签
return render(request, 'registerPage.html', locals())
前端也加了一个小效果,就是站点注册的交互框默认隐藏,在点击要注册时显示,再次点击消失,用到了toggleClass的jQuery方法。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>注册界面</title>
{% load static %}
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.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>
<style>
.img-parent-div {
border-radius: 20%;
overflow: hidden;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<h2 class="text-center">注册界面</h2>
<form action="" id="regi-form">
{% csrf_token %}
{% for regi in register_form_obj %}
<label for="">{{ regi.label }}</label><span class="pull-right" style="color: darkred"> </span>
{{ regi }}
{% endfor %}
<div class="form-group">
<label for="id_avatar">选择头像
<div class="img-parent-div" style="width: 100px">
<img id="avatar_img" src="/media/avatar/默认头像.png" alt="" style="width: 100%">
</div>
</label>
<input type="file" id="id_avatar" style="display: none">
</div>
<div class="checkbox">
<label for="id_site" style="font-weight: bolder">
<input type="checkbox" id="id_site" name="is_create_site">
是否建立个人站点(个人站点可以发布、编辑文章)
</label>
</div>
<div id="site_form" class="hidden">
<div class="form-group">
<label for="id_site_name">站点域名(数字、字母、下划线组合)</label> <span class="text-danger pull-right"></span>
<input type="text" id="id_site_name" name="site_name" class="form-control">
</div>
<div class="form-group">
<label for="id_site_title">站点标题</label> <span class="text-danger pull-right"></span>
<input type="text" id="id_site_title" name="site_title" class="form-control">
</div>
</div>
<div class="form-group">
<input type="button" id="registerBtn" class="form-control btn btn-primary" value="注册">
</div>
</form>
</div>
</div>
</div>
<script>
// 站点输入标签切换
$('#id_site').change(function () {
$('#site_form').toggleClass('hidden')
})
// 头像实时显示
$('#id_avatar').change(function () {
let fileReaderObj = new FileReader()
fileReaderObj.readAsDataURL(this.files[0])
fileReaderObj.onload = function () {
$('#avatar_img').attr('src', fileReaderObj.result)
}
})
// 发送表单到后端处理
$('#registerBtn').click(function () {
let formDataObj = new FormData()
$.each($('#regi-form').serializeArray(), function (index, regiForm) {
formDataObj.append(regiForm.name, regiForm.value)
})
formDataObj.append('avatar', $('#id_avatar')[0].files[0])
$.ajax({
url: '',
type: 'post',
data: formDataObj,
contentType: false,
processData: false,
success: function (args) {
if (args.code === 10000) {
alert(args.msg)
window.location.href = args.url
} else {
let errorList = args.errors
$.each(errorList, function (key, error) {
$('#id_' + key).prev().text(error[0]).parent().addClass('has-error')
}
)
}
}
})
})
// 聚焦后清除错误提示
$('input').focus(function () {
$(this).prev().text('').parent().removeClass('has-error')
})
</script>
</body>
</html>
``
## 登录认证相关功能
这里的登录就用表单标签简单的提交了,也没有使用form组件,写的比较简陋。
### 登录认证代码总览
#### login视图层
<details>
<summary>用户登录/修改密码/注销 视图层代码</summary>
```py
def login(request):
error_msg = ''
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user_obj = auth.authenticate(username=username, password=password)
# 如果验证码正确
if request.POST.get('code').upper() == request.session.get('code').upper():
# 如果用户密码校验成功
if user_obj:
# 保存登录状态
auth.login(request, user_obj)
# 跳转至首页
return redirect('home')
error_msg = '用户名或密码错误'
else:
error_msg = '验证码输入错误'
return render(request, 'loginPage.html', locals())
# 修改密码
def set_pwd(request):
if request.method == 'POST':
# print(request.POST)
back_dict = {'code': 10000, 'msg': ''}
old_password = request.POST.get('old_password')
new_password = request.POST.get('new_password')
confirm_password = request.POST.get('confirm_password')
if not request.user.check_password(old_password):
back_dict['code'] = 10001
back_dict['msg'] = '原密码输入错误'
elif not new_password == confirm_password:
back_dict['code'] = 10002
back_dict['msg'] = '两次新密码输入不一致'
else:
request.user.set_password(new_password)
request.user.save()
back_dict['msg'] = '修改密码成功'
back_dict['url'] = '/login/'
# print(back_dict)
return JsonResponse(back_dict)
return HttpResponse('404 not found')
# 注销功能
def logout(request):
auth.logout(request)
return redirect('home')
loginPage模板层
用户登录视图层代码
<div class="container">
<div class="col-md-6 col-md-offset-3">
<h2 class="text-center">登录界面</h2>
<!--登录表单开始-->
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<label for="username">用户名</label>
<input type="text" id="username" name="username" class="form-control">
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" id="password" name="password" class="form-control">
</div>
<div class="form-group">
<label for="code">验证码</label>
<div class="row">
{# 验证码输入框 #}
<div class="col-md-6">
<input type="text" id="code" name="code" class="form-control">
</div>
{# 验证码图片展示 #}
<div class="col-md-6">
{# img的网址对应一个本站点的路由,为获取随机验证码的路由 #}
<img id="code_img" src="{% url 'get_code' %}" alt="" width="250px" height="30px">
</div>
</div>
</div>
<p style="color: red">{{ error_msg }}</p>
<div class="form-group">
<input type="submit" class="form-control btn-primary btn">
</div>
</form>
<!--登录表单结束-->
</div>
</div>
<script>
$('#code_img').click(function () {
this.src += '?'
})
</script>
设置密码视图层
<a href="#" data-toggle="modal" data-target="#myModal">修改密码</a>
<!--修改密码前端界面设计为模态框弹出--->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">修改密码</h4>
</div>
<div class="modal-body">
<div class="form-group">用户名
<input type="text" value="{{ request.user.username }}" disabled
class="form-control">
</div>
<div class="form-group">原密码
<input type="password" name="old_password" class="form-control">
</div>
<div class="form-group">新密码
<input type="password" name="new_password" class="form-control">
</div>
<div class="form-group">确认密码
<input type="password" name="confirm_password" class="form-control">
</div>
</div>
<div class="modal-footer">
<span class="pull-left text-danger" id="setError"></span>
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-warning" id="setPwd">修改密码</button>
</div>
</div>
</div>
</div>
登录业务逻辑总结
get_code随机验证码
在页面上增加了一个验证码校验的功能,验证码校验的基本逻辑是:
- 表层逻辑:页面上显示验证码的图片,用户根据图片输入验证码,比对成功即验证码正确
- 底层逻辑:随机产生验证码,本地session存储验证码,前端页面渲染验证码图片,获取用户输入,将用户请求中的验证码信息与本地session存储的验证码比对,比对时注意取消大小写。
- 底层所需模块:随机模块random|session表|图片绘制第三方模块pillow|BytesIO内存存储
产生随机验证码视图函数
from PIL import Image, ImageFont, ImageDraw # 导入pillow模块的图片、字体、画笔工具类
from io import BytesIO, StringIO # 导入IO模块
"""pillow模块
Image 图片
ImageFont 图片字体
ImageDraw 图片画笔
"""
"""io模块 与硬盘文件的open、write不同,将文件暂存在内存中,读取更快,也不会浪费内存空间
BytesIO 二进制形式读写文件
StringIO 字符串形式读写文件
"""
def get_code(request):
# 产生图片对象 # random_rgb_color()是一个随机产生三个0-255数字组成的元组的函数
img_obj = Image.new('RGB', (250, 30), common.random_rgb_color())
# 生成字体对象
font_obj = ImageFont.truetype('static/fonts/CHILLER.TTF', 25)
# 将图片对象传给画笔对象作画
draw_obj = ImageDraw.Draw(img_obj)
code = ''
for i in range(5):
random_num = str(random.randint(0, 9))
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_str = random.choice([random_upper, random_lower, random_num])
# 传入字体和文字内容,在指定位置写出文字
draw_obj.text((i * 45 + 40, random.randint(-2, 2)), random_str, font=font_obj)
code += random_str
# 保存验证码到后台,方便后续校验
request.session['code'] = code
# 保存图片到内存
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
# 读取图片并返回给前端
return HttpResponse(io_obj.getvalue())
前端页面刷新随机验证码
html中img标签的目标网址src发生变化时,会重新向网站发送请求,我们可以为验证码绑定一个点击事件,让其src属性加上一个?,那么就是原路由后面跟了一个?,不会影响请求的路由主体,img最终还会按照get_code视图函数拿到一个随机验证码图片。
// 点击事件使src的内容后面加一个?,src的内容变化就会自动请求一次资源
$('#code_img').click(function{
this.src += '?'
})
admin后台管理简述
站点管理界面登录
为了快速的填充网站的数据,我们前期可以使用django提供给我们的admin后台管理来对模型类产生的表进行手动的增删改查,我们通过路由'/admin/'来进入这个入口,提示需要登录:
后台管理的用户需要一个superuser,这里我已经提前用manage命令createsuperuser
创建了一个admin的超级用户,点击登录后出现以下界面:
上面的界面几乎什么都没有,我们如果想要通过admin后台管理对模型表进行增删改查的话,则需要在模型表所在app的admin.py中对模型表进行注册:
from apps import models
admin.site.register(models.UserInfo) # 注册用户信息表
在这个py文件中注册后,django就为我们自动搭建好了一个表的增删改查的交互界面,如下,也产生了对应的站点管理的多个选项。
这里的表的名称就是模型类的名字的小变形,是英文名,我们也可以通过一些代码将其更改为其他字符。
在模型类中添加:
class Meta:
verbose_name_plural = '个人站点'
就可以让这里的站点管理显示的表名为注释的名字
ps:虽然这里对模型表进行了修改,但是与数据库无关,所以不用执行makemigrations的迁移命令
站点管理各界面展示
展示数据界面:
添加数据界面:
其中:对于外键字段的选项表单,有快捷添加按钮,它会弹窗出来添加该表数据的界面,并在添加完成后实时的显示到外键的选项中。
而外键的数据对象默认显示的是类似于site_object(1)
的形式,不好辨认,我们可以在模型类中添加双下魔法方法__str__
来改变对象展示的字符串。
# UserInfo模型表的类体中,数据对象以 超级用户:admin 或者 用户:leethon 的形式展示
def __str__(self):
if self.is_superuser:
return f'超级用户:{self.username}'
return f'用户:{self.username}'
更新界面与添加界面大同小异,删除数据的交互逻辑也比较简单,在此不再赘述。