BBS项目练习一(模型表、注册、登录、站点管理)

BBS项目练习一

项目启动

BBS是电子报系统,我们将参考博客园界面来进行需求分析和项目搭建。

需求分析

表层功能需求(大致)

  • 博客园,有一个首页,首页用于展示文章,提供一些其他站内链接,可能还包括广告展示等。
  • 用户可以注册、登录、修改密码、注销登录、修改头像等
  • 用户可以申请个人站点,申请后可以添加,编辑,删除自己的文章
  • 用户可以查看文章列表,在首页展示全部文章列表,在个人站点显示站点的所有文章,文章可以被分类、标签、归档,用户可以根据这些筛选条件查看筛选的文章列表展示
  • 点击某篇文章的标题进入文章详情页,文章详情页应该拥有文章具体内容,个人站点相关链接,点赞点踩功能,评论功能等。

数据结构分析(大致)

根据上述的功能,我们可以大概的整理一下,一个bbs项目需要哪些数据,这些数据的关系是什么。

  1. 用户数据:每个用户拥有一些各自的权限,如开设自己的站点,对自己的站点进行管理。也有一些共有的权限,如查看站点内的文章。
  2. 个人站点数据:用户可以选择而开设和不开设站点,站点数据和用户是一对一的关系,两者深度绑定
  3. 文章数据:文章可以被某个个人站点发布和管理,可以被所有用户查看和点赞评论交互
  4. 文章分类数据:个人站点可以设置文章分类来方便的管理文章,文章分类属于站点,文章首先绑定站点,其次可以绑定文章分类
  5. 文章标签数据:与文章分类很类似,不同的是,一篇文章只能在一个分类中,但可以拥有多个标签,相当于文章与标签是多对多的关系
  6. 点赞点踩关系表:关联一个用户,关联一篇文章,记录点赞点踩的数据,表示出用户对文章的点赞点踩操作。
  7. 评论表:关联一个用户,关联一篇文章,记录评论的数据,表示出用户对文章的评论操作。其次可以增加一个自关联外键,让评论可以是其他评论的回复(子评论)

以上的数据就决定了七张信息表的基本数据结构。

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")),
]

七张信息表

  1. 用户表(与站点一对一)
    • 继承AbstractUser
    • 电话号码
    • 用户头像
    • 站点外键
  2. 站点表
    • 站点域名
    • 站点标题
    • 站点主题
  3. 文章表(与站点一对多)
    • 标题
    • 摘要
    • 内容
    • 发布时间
    • 点赞、点踩、评论数 三个字段(注意更新)
    • 站点外键
    • 分类外键(set_null,不随分类消失而消失)
    • 标签外键(多对多)
  4. 文章分类表
    • 分类名称
    • 站点外键
  5. 文章标签表
    • 标签名称
    • 标签外键
  6. 点赞点踩表
    • 用户外键
    • 文章外键
    • 点赞or点踩
  7. 评论表
    • 用户外键
    • 文章外键
    • 评论内容
    • 评论时间
    • 父评论自关联外键

一些注意:

  • 建立用户信息表:通过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模板层

主要实现了以下内容:

  • 渲染标签
  1. 提交表单的ajax请求,并处理后端返回过来的信息
    • 如果是注册成功的状态码,跳转到指定界面
    • 如果是其他状态码,说明错误,将错误信息渲染到界面上
  2. 当用户上传头像图片时,实时将其渲染到界面上(纯前端)
  3. 当用户聚焦到输入框时,将错误信息及样式移除(纯前端)
模板层
<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结果
    }
})

注册功能进阶

上述注册功能是对个人用户的注册,下面将一对一用户和站点两份数据一块注册,实现效果如下:

image

我们需要对功能的逻辑进行梳理:

  • 用户不注册站点时可以注册用户,只校验用户输入是否正确,正确则创建用户数据,注册成功跳转界面,输入有误显示错误信息。
  • 用户注册站点时会同时注册用户和站点,那么两者都需要进行校验,有一方错误都不创建用户或站点,两者同时校验成功才创建,并在前端跳转界面。

对以上两种方向的内容进行归纳:

  • 有错误信息,那就让返回的字典的code值不为10000,前端会打印错误信息
  • 无错误信息,那就对是否建立站点进行判断,如果不建立,则直接创建用户,如果建立还要创建站点。
  1. 对用户的输入进行判断,记录是否出错,并准备用于用户注册的信息或者返回的用户错误信息
  2. 对站点的输入数据进行判断,记录是否出错,并准备站点注册的信息或者返回的站点注册信息
  3. 依据两者的校验记录,两者输入信息都有效则执行注册,如果有一方错误则执行返回错误。
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">&times;</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/'来进入这个入口,提示需要登录:

image

后台管理的用户需要一个superuser,这里我已经提前用manage命令createsuperuser创建了一个admin的超级用户,点击登录后出现以下界面:

image

上面的界面几乎什么都没有,我们如果想要通过admin后台管理对模型表进行增删改查的话,则需要在模型表所在app的admin.py中对模型表进行注册:

from apps import models

admin.site.register(models.UserInfo)  # 注册用户信息表

在这个py文件中注册后,django就为我们自动搭建好了一个表的增删改查的交互界面,如下,也产生了对应的站点管理的多个选项。

image

这里的表的名称就是模型类的名字的小变形,是英文名,我们也可以通过一些代码将其更改为其他字符。

在模型类中添加:

class Meta:
    verbose_name_plural = '个人站点'

就可以让这里的站点管理显示的表名为注释的名字

image

ps:虽然这里对模型表进行了修改,但是与数据库无关,所以不用执行makemigrations的迁移命令

站点管理各界面展示

展示数据界面:

image

添加数据界面:

image

其中:对于外键字段的选项表单,有快捷添加按钮,它会弹窗出来添加该表数据的界面,并在添加完成后实时的显示到外键的选项中。

image

而外键的数据对象默认显示的是类似于site_object(1)的形式,不好辨认,我们可以在模型类中添加双下魔法方法__str__来改变对象展示的字符串。

# UserInfo模型表的类体中,数据对象以 超级用户:admin 或者 用户:leethon 的形式展示
    def __str__(self):
        if self.is_superuser:
            return f'超级用户:{self.username}'
        return f'用户:{self.username}'

更新界面与添加界面大同小异,删除数据的交互逻辑也比较简单,在此不再赘述。

posted @ 2023-01-02 21:24  leethon  阅读(65)  评论(0编辑  收藏  举报