Django基础篇:实战之BBS项目之注册登录
2022.5.26 BBS项目之注册登录
一、模型层
1、用户表继承AbstractUser
目的是为了让auth自动在数据库自动创建的user用户表加以拓展,符合用户注册的需求;
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
from django.contrib.auth.models import AbstractUser
class UserInfo(AbstractUser):
"""用户表"""
phone = models.BigIntegerField(verbose_name='手机号', null=True)
avatar = models.FileField(verbose_name='头像', upload_to='avatar/', default='avatar/default.jpg')
'''给该字段传文件对象 会保存到upload_to指定的位置 然后该字段存储该位置的路径'''
create_time = models.DateField(verbose_name='注册时间', auto_now_add=True)
blog = models.OneToOneField(to='Blog', null=True) # 外键设置null=True便于后期录入数据的方便
创建好user的拓展表后,需要在settings文件中注册:
复制代码
- 1
AUTH_USER_MODEL = 'app01.UserInfo'
总结:
(1)File字段:up_load='文件夹路径',字段获取的文件数据的保存位置;
(2)File字段:default='文件路径',设置文件字段的默认值,这里头像设置为默认头像;
(3)blog字段:null=True,外键字段设置便于后期录入数据
2、文章表字段优化
针对文章的评论数、点赞数、点踩数都可以跨表查询得到;
但是页面上有很多文章,如果每一篇都跨表查询,效率极低;
我们可以在文章表里面设计三个普通字段专门记录:
文章评论数:每次评论让该字段数值自增1;
文章点赞数:每次点赞让该字段数值自增1;
文章点踩数:每次点踩让该字段数值自增1;
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
class Article(models.Model):
"""文章表"""
title = models.CharField(verbose_name='文章标题', max_length=32)
desc = models.CharField(verbose_name='文章简介', max_length=255)
content = models.TextField(verbose_name='文章内容')
create_time = models.DateTimeField(verbose_name='创作时间', auto_now_add=True)
# 优化字段
up_num = models.IntegerField(verbose_name='点赞数', default=0)
down_num = models.IntegerField(verbose_name='点踩数', default=0)
comment_num = models.IntegerField(verbose_name='评论数', default=0)
# 外键字段
blog = models.ForeignKey(to='Blog', null=True)
category = models.ForeignKey(to='Category', null=True)
tags = models.ManyToManyField(to='Tag',
through='Article2Tag',
through_fields=('article', 'tag')
)
3、评论表设置自关联外键
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
class Comment(models.Model):
"""评论表"""
user = models.ForeignKey(to='UserInfo')
article = models.ForeignKey(to='Article')
content = models.CharField(verbose_name='评论内容', max_length=255)
comment_time = models.DateTimeField(verbose_name='评论时间', auto_now_add=True)
# 自关联
parent = models.ForeignKey(to='self', null=True) # 有些评论就是根评论
总结:
to='self':意思是设置当前表为关联的外键字段,因为该表中的每个评论可以作为根评论,也可以作为子评论
二、Forms组件校验
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
from django import forms
from app01 import models
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=8, label='用户名',
error_messages={
'min_length': '用户名最少三位',
'max_length': '用户名最多八位',
'required': '用户名不能为空'
},
widget=forms.widgets.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(...
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'})) # ...省略代码
confirm_password = forms.CharField(...
widget=forms.widgets.PasswordInput(attrs={'class': 'form-control'}))
email = forms.EmailField(...
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
三、视图层
1、注册
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
from django.shortcuts import render,HttpResponse
# Create your views here.
from app01 import myforms
from app01 import models
from django.http import JsonResponse
def register(request):
"""前后端使用ajax交互 一般采用字典(json格式数据)作为数据媒介"""
back_dic = {'code':10000,'msg':''} # code模拟响应状态码 msg模拟数据
form_obj = myforms.RegisterForm() # 获取写好的forms校验组件
if request.method == 'POST':
# 1.将request.POST里面的数据全部交给forms类校验
form_obj = myforms.RegisterForm(request.POST) # 多余的字段默认就不校验
# 2.判断是否符合条件
if form_obj.is_valid():
cleaned_data = form_obj.cleaned_data # username password confirm_password email
# 3.移除创建用户数据时没有用的键值对confirm_password
cleaned_data.pop('confirm_password') # username password email
# 4.获取用户头像
avatar_obj = request.FILES.get('avatar')
# 5.判断用户是否自定义了头像
if avatar_obj:
cleaned_data['avatar'] = avatar_obj # username password email avatar
# 6.创建用户对象
models.UserInfo.objects.create_user(**cleaned_data)
back_dic['msg'] = '用户注册成功'
back_dic['url'] = '/login/'
else:
back_dic['code'] = 10001
back_dic['msg'] = form_obj.errors
return JsonResponse(back_dic)
return render(request, 'register.html', locals())
注意:
(1)cleaned_data获取的数据,需要判断数据的多少,比如模型层并没有confirm_password字段,因此应该去除这一字段:
复制代码
- 1
cleaned_data.pop('confirm_password')
并且cleaned_data是一个容器类型的对象,可以使用**打散:
复制代码
- 1
models.UserInfo.objects.create_user(**cleaned_data)
(2)由于前端js需要判断后端获取数据需要判断是否成功登录并且返回错误信息,因此需要模拟响应状态码,登录失败返回错误信息:
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
back_dic = {'code':10000,'msg':''} # code模拟响应状态码 msg模拟数据 # 登录成功 back_dic['msg'] = '用户注册成功' back_dic['url'] = '/login/' # 登录失败 back_dic['code'] = 10001 # 更改状态码 back_dic['msg'] = form_obj.errors # 返回forms校验组件中写好的errors
(3)不管登录成功或者失败返回的back_dic都需要使用JsonResponse模块进行返回,因为前端需要获取数据:
复制代码
- 1
- 2
- 3
from django.http import JsonResponse ... return JsonResponse(back_dic)
2、登录
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
# 用户登录视图函数
from django.contrib import auth
def login(request):
back_dict = {'code': 10000, 'msg': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
code = request.POST.get('code')
# 1.先比对验证码是否正确 忽略大小写
if request.session.get('code').upper() == code.upper():
# 2.再比对用户名 密码是否正确
is_obj = auth.authenticate(request, username=username, password=password)
if is_obj:
# 3.保存用户登录状态
auth.login(request, is_obj)
back_dict['msg'] = f'{username}登录成功'
back_dict['url'] = '/home/' # 可以做成之前定向跳转的需求
else:
back_dict['code'] = 10001
back_dict['msg'] = '用户名或密码错误'
else:
back_dict['code'] = 10002
back_dict['msg'] = '验证码错误'
return JsonResponse(back_dict)
return render(request, 'login.html')
"""
图形化相关模块:pillow
pip3 install pillow
"""
from PIL import Image,ImageFont,ImageDraw
"""
Image 生成图片对象
ImageDraw 生成画笔对象 可以在图片上乱涂乱画
ImageFont 如果写文字 可以控制字体样式
"""
from io import BytesIO,StringIO
"""
BytesIO 内存中保存数据 并且取的时候返回bytes类型
StringIO 内存中保存数据 并且取的时候返回字符串类型
"""
# 专门随机生成三个数
import random
def get_random():
return random.randint(0,255),random.randint(0,255),random.randint(0,255)
# 生成图片验证码视图函数
def get_code(request):
img_obj = Image.new('RGB', (350, 35), get_random())
draw_obj = ImageDraw.Draw(img_obj)
font_obj = ImageFont.truetype('static/font/111.ttf', 30)
code = ''
for i in range(5):
random_upper = chr(random.randint(65, 90))
random_lower = chr(random.randint(97, 122))
random_int = str(random.randint(0, 9))
temp_choice = random.choice([random_upper, random_lower, random_int])
# 每选择出一个 就写上一个 这样还可以控制每个字的间距
draw_obj.text((i * 60 + 45, 0), temp_choice, font=font_obj)
code += temp_choice
# 将验证码保存一份 便于后续比对
request.session['code'] = code
io_obj = BytesIO()
img_obj.save(io_obj, 'png')
return HttpResponse(io_obj.getvalue())
总结:
(1)图形化相关模块:pillow
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
# 下载方式:cmd窗口 pip3 install pillow # 模块导入 from PIL import Image,ImageFont,ImageDraw Image 生成图片对象 ImageDraw 生成画笔对象 可以在图片上乱涂乱画 ImageFont 如果写文字 可以控制字体样式 eg1: img_obj = Image.new('RGB', (350, 35), get_random()) # RGB代表三基色,(350,35)代表长宽,get_random()为获取随机的三个数字的视图函数 img_obj.save(io_obj,'png') # 内存中保存图片 eg2: draw_obj = ImageDraw.Draw(img_obj) eg3: font_obj = ImageFont.truetype('/static/font/ccc.ttf',30)
(2)io模块
复制代码
- 1
- 2
- 3
- 4
- 5
# 模块导入 from io import BytesIO,StringIO BytesIO 内存中保存数据 并且取的时候返回bytes类型 StringIO 内存中保存数据 并且取的时候返回字符串类型
StringIO的使用
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
# 类似文件的缓冲区 from io import StringIO cache_file = StringIO() print(cache_file.write('hello world')) # 11 print(cache_file.seek(0)) # 0 print(cache_file.read()) # hello world print(cache_file.close()) # 释放缓冲区
- StringIO经常被用来作字符串的缓存,因为StringIO的一些接口和文件操作是一致的,也就是说同样的代码,可以同时当成文件操作或者StringIO操作;
- 要读取StringIO,可以用一个str初始化StringIO,然后像读文件一样读取;
- 当使用read()方法读取写入的内容时,则需要先用seek()方法让指针移动到最开始的位置,否则读取不到内容(写入后指针在最末尾);
- getvalue()方法:直接获得写入后的str;
- close()方法:在关闭文件的缓冲区之后就不能再进行读写操作了;
BytesIO的使用
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
# 类似文件的缓冲区 from io import BytesIO bytes_file = BytesIO() bytes_file.write(b'hello world') bytes_file.seek(0) print(bytes_file.read()) # b'hello world' bytes_file.close() bytes_file.getvalue() # 获取写入的二进制数据
StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO;
BytesIO实现了在内存中读写bytes,写入的不是str,而是经过UTF-8编码的bytes;
要读取BytesIO,可以用一个bytes初始化BytesIO,然后像读文件一样读取;
四、模板层
1、注册页面
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
<!-- register.html -->
<body>
<div class="container-fluid">
<div class="row">
<h2 class="text-center">用户注册</h2>
<div class="col-md-8 col-md-offset-2">
<form id="form">
{% csrf_token %}
<!--获取普通数据-->
{% for form in form_obj %}
<div class="form-group">
<label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id自动产生对应绑定的标签id值-->
{{ form }}
<span class="errors pull-right" style="color: red"></span>
</div>
{% endfor %}
<!--获取用户头像-->
<div class="form-group">
<p><label for="avatar">头像
<img src="/static/img/default.jpg" alt="" width="120" id="img">
</label></p>
<input type="file" name="avatar" id="avatar" style="display: none">
</div> <!--该div仅仅是增加了上下两个标签的距离 更好看一些-->
<input type="button" value='注册' class="btn btn-primary btn-block" id="subBtn">
</form> <!--仅仅用一个form标签 不使用它的提交功能-->
</div>
</div>
</div>
<script>
// 用户头像实时展示
$('#avatar').change(function () {
// 1.产生一个文件阅读器对象
let myFileReader = new FileReader();
// 2.获取用户上传的头像文件
let avatarObj = this.files[0];
// 3.将文件对象交给阅读器加载
myFileReader.readAsDataURL(avatarObj) // IO操作 需要消耗时间 但是是异步
// 4.修改img标签的src属性
// 等待文件阅读器对象加载完毕之后再修改src属性
myFileReader.onload = function(){
$('#img').attr('src',myFileReader.result)
}
})
// 绑定点击事件 提交数据
$('#subBtn').click(function () {
// ajax携带文件数据 需要利用内置对象
let myFormData = new FormData();
// 添加普通数据 利用form标签自带的序列化功能(一次性获取所有的普通键值对数据)
{#console.log($('#form').serializeArray())#}
$.each($('#form').serializeArray(),function (index,dictObj) {
myFormData.append(dictObj.name,dictObj.value)
})
// 添加文件数据
myFormData.append('avatar',$('#avatar')[0].files[0])
// 发送ajax请求
$.ajax({
url:'',
type:'post',
data:myFormData,
contentType:false,
processData:false,
success:function (args) {
if (args.code===10000){
// 注册成功跳转到登录页面
window.location.href = args.url
}else{
// 获取到了所有的字段错误提示 如何对应展示???
// 研究发现 渲染出来的标签id值都是 id_字段名 而后端返回的错误提示键是字段名 所以拼接即可
$.each(args.msg, function (i,j) {
let eleId = '#id_' + i;
$(eleId).next().text(j[0]).parent().addClass('has-error')
})
}
}
})
})
// 绑定聚焦事件 点击移除错误提示
$('input').focus(function () {
$(this).next().text('').parent().removeClass('has-error')
})
</script>
</body>
总结:
(1)循环展示forms表单
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
{% for form in form_obj %} <div class="form-group"> <label for="{{ form.auto_id }}">{{ form.label }}</label> <!--form.auto_id自动产生对应绑定的标签id值--> {{ form }} <span class="errors pull-right" style="color: red"></span> </div> {% endfor %}
(2)文件阅读器对象
为了让前端能够识别上传的文件,需要使用一个文件阅读器对象
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
$('#avatar').change(function () { // 1.产生一个文件阅读器对象 let myFileReader = new FileReader(); // 2.获取用户上传的头像文件 let avatarObj = this.files[0]; // 3.将文件对象交给阅读器加载 myFileReader.readAsDataURL(avatarObj) // IO操作 需要消耗时间 但是是异步 // 4.修改img标签的src属性 // 等待文件阅读器对象加载完毕之后再修改src属性 myFileReader.onload = function(){ $('#img').attr('src',myFileReader.result) } })
(3)FormData内置对象
ajax不能直接携带文件数据,需要使用内置对象FormData
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
let myFormData = new FormData(); // 添加普通数据 利用form标签自带的序列化功能(一次性获取所有的普通键值对数据) {#console.log($('#form').serializeArray())#} $.each($('#form').serializeArray(),function (index,dictObj) { myFormData.append(dictObj.name,dictObj.value) }) // 添加文件数据 myFormData.append('avatar',$('#avatar')[0].files[0]) $.ajax({ ... data:myFormData, ... })
(4)用户注册成功/失败后的异步回调函数
后端会返回一个back_dic字典,内部模拟的响应状态码、登录成功后的url、注册失败后的errors(forms校验后的数据),我们利用异步回调函数加以操作:
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
$.ajax({ ... success:function (args) { if (args.code===10000){ // 注册成功跳转到登录页面 window.location.href = args.url }else{ // 获取到了所有的字段错误提示 如何对应展示??? // 研究发现 渲染出来的标签id值都是 id_字段名 而后端返回的错误提示键是字段名 所以拼接即可 $.each(args.msg, function (i,j) { let eleId = '#id_' + i; $(eleId).next().text(j[0]).parent().addClass('has-error') }) } } })
(5)输入框错误提示开启和关闭
在异步回调函数获取到返回数据,如果注册失败,则通过字符串的拼接找到对应的标签,然后在更改对应标签的内容并加上属性has-error(输入框变红提示):
复制代码
- 1
- 2
# 异步回调函数内对应span标签更改内容,添加has-error属性 $(eleId).next().text(j[0]).parent().addClass('has-error')
给输入框绑定聚焦事件,点击移除错误提示:
复制代码
- 1
- 2
- 3
$('input').focus(function () { $(this).next().text('').parent().removeClass('has-error') })
2、登录页面
复制代码
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
<!-- login.html -->
<body>
<div class="container">
<div class="row">
<h2 class="text-center">用户登录</h2>
<div class="col-md-8 col-md-offset-2">
<form id="form">
{% csrf_token %}
<div class="form-group">
<p>用户名:
<input type="text" name="username" class="form-control">
</p>
</div>
<div class="form-group">
<p>密码:
<input type="password" name="password" class="form-control">
</p>
</div>
<div class="form-group">
<p>验证码: <span style="color: red" id="errors"></span>
<div class="row">
<div class="col-md-6 ">
<input type="text" name="code" class="form-control">
</div>
<div class="col-md-6 ">
<img src="/get_code/" alt="" width="350" height="35" id="img">
</div>
</div>
</p>
</div>
<input type="button" class="btn btn-success btn-block" id="subBtn" value="登录">
</form>
</div>
</div>
</div>
<script>
$('#img').click(function () {
// 1.获取img标签原来的src属性
let oldSrc = $(this).attr('src');
// 2.重新赋值即可 只要src跟原来的不一样 就会重新发请求
$(this).attr('src',oldSrc+'?')
})
$('#subBtn').click(function () {
let dataDict = {};
$.each($('#form').serializeArray(),function (index,dictObj) {
dataDict[dictObj.name] = dictObj.value
})
$.ajax({
url:'',
type:'post',
data:dataDict,
success:function (args) {
if(args.code === 10000){
window.location.href = args.url
}else{
// 针对错误的提示 可以直接找一个固定的位置展示
$('#errors').text(args.msg)
}
}
})
})
</script>
</body>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步