fastAPI教程:数据库操作

news2024/12/28 17:48:22

FastAPI

六、数据库操作

FastAPI支持操作各种数据库,但本身并没有内置关于任何数据库相关的模块。因此我们可以根据需求使用任何数据库,包括关系型(SQL)数据库,例如:PostgreSQL、MySQL、SQLite、Oracle、Microsoft SQL Server,也包括非关系数据库(NoSQL),例如:Redis、MongoDB、Elasticsearch、向量数据库、图数据库等。

6.1 原生操作

6.1.1 pymysql

main.py,代码:

import uvicorn
from fastapi import FastAPI, status
from pydantic import BaseModel, Field
import pymysql

# 初始化数据库
conn = pymysql.connect(user="root", password="123", host="127.0.0.1", port=3306, database="school")
cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)

app = FastAPI()


def dict_to_sql(table, model):
    """
    把字典数据转化SQL添加语句
    :param table: 表名
    :param model: 数据模型
    :return:
    """
    data = dict(model)
    query = f"insert into {table} "
    # 组装字段列表
    fields = "`,`".join(data.keys())
    query += f"(`{fields}`) values "
    values = str(list(data.values()))
    query += f"({values[1:-1]})"
    return query


class StudentIn(BaseModel):
    name: str = Field(title="姓名")
    age: int = Field(title="年龄")
    class_id: str = Field(title="班级")
    sex: int = Field(title="性别")
    description: str = Field(title="个人介绍")


@app.post("/student", status_code=status.HTTP_201_CREATED)
async def index(student: StudentIn):
    """
    添加学生
    :param student:
    :return:
    """
    query = dict_to_sql("student", student)
    cursor.execute(query)
    conn.commit()
    return {
        "id": cursor.lastrowid,
        **dict(student)
    }

if __name__ == '__main__':
    uvicorn.run("main:app", reload=True)

上面的pymysql是同步操作数据库,所以实际上在开发中如果使用pymysql的话,没有进行异步,会导致两个问题:

  • 无法发挥FastAPI的高性能,也就是无法在高并发场景下发挥FastAPI的异步特性。
  • FastAPI的底层设计是基于异步的,所以pymysql在某些场景下会出现bug。

fastAPI提供给pymysql的同步转异步的方案:

pip install mysql-connector-python pymysql

连接数据库,db_conn.py,代码:

from fastapi import Depends
import mysql.connector


def get_db_connection():
    connection = mysql.connector.connect(
        host='localhost',
        port=3306,
        user="root",
        password="123456",
        database="example_db"
    )
    return connection

def get_db():
    connection = get_db_connection()
    db = connection.cursor()

    try:
        yield db
    finally:
        db.close()
        connection.close()

操作数据库,main.py,代码:

import uvicorn
from fastapi import FastAPI, Depends
import mysql.connector
from mysql.connector import cursor

app = FastAPI()

def get_db_connection():
    connection = mysql.connector.connect(
        host='localhost',
        port=3306,
        user="root",
        password="123",
        database="school"
    )
    return connection

def get_db():
    connection = get_db_connection()
    db = connection.cursor()

    try:
        yield db
    finally:
        db.close()
        connection.close()


@app.get("/users")
async def get_users(db: cursor.MySQLCursor = Depends(get_db)):
    query = "SELECT * FROM tb_student"
    db.execute(query)
    result = db.fetchall()
    if result:
        return {"users": result}
    else:
        return {"error": "User not found"}

if __name__ == '__main__':
    uvicorn.run("main:app", reload=True)

6.2 ORM

在大型的web开发中,我们肯定会用到数据库操作,那么FastAPI也支持数据库的开发,你可以用 PostgreSQL、MySQL、 SQLite Oracle 等。本文用MySQL为例。我们看下在fastapi是如何操作设计数据库的。

fastapi是一个很优秀的异步web框架,但是缺少一个合适的orm,fastAPI早期官方文档中数据操作代码里面的例子使用的是sqlalchemy,后面官方废弃了这块代码,选择让用户自己根据业务自主选择合适的ORM。

6.2.1 Tortoise

Tortoise ORM 是受 Django框架的ORM启发的易于使用的异步 ORM (对象关系映射器),基于asyncio模块开发的。

官方文档:https://tortoise.github.io/

在这里插入图片描述

依赖:

Tortoise ORM 必须运行CPython >= 3.8

目前支持以下数据库:

  • PostgreSQL >= 9.4(使用asyncpg
  • SQLite(使用aiosqlite
  • MySQL/MariaDB(使用aiomysql或使用asyncmy)
  • Microsoft SQL Server/Oracle(使用asyncodbc)

在终端下使用以下命令进行安装:

pip install tortoise-orm[aiomysql]
快速使用

配置数据库信息,settings.py,代码:

""""项目配置"""


# tortoise-orm数据库配置
TORTOISE_ORM = {
    "connections": {
        "default": {
            'engine': 'tortoise.backends.mysql',  # MySQL or Mariadb
            'credentials': {  # 连接参数
                'host': '127.0.0.1',  # 数据库IP/域名地址
                'port': 3306,  # 端口
                'user': 'root',  # 连接账户
                'password': '123',  # 连接密码
                'database': 'school',  # 数据库
                'charset': 'utf8mb4',  # 编码
                'minsize': 1,  # 连接池中的最小连接数
                'maxsize': 5,  # 连接池中的最大连接数
                "echo": True  # 执行数据库操作时,是否打印SQL语句
            }
        }
    },
    'apps': {  # 默认所在的应用目录
        'models': {  # 数据模型的分组名
            'models': ['models'],  # 模型所在目录文件的导包路径[字符串格式]
            'default_connection': 'default',  # 上一行配置中的模型列表的默认连接配置
        }
    },
    # 当use_tz=False,当前tortoise-orm会默认使用当前程序所在操作系统的时区,
    'use_tz': True,
    # 当use_tz=True时,当前tortoise-orm会默认使用timezone配置项中的时区
    'timezone': 'Asia/Shanghai'  # 自定义时区,
}

完成上面的数据配置操作以后,我们需要在上面TORTOISE_ORM配置字典的apps.models的models中填写的路径里面补充上数据表模型,models.py,代码:

from tortoise.models import Model  # 类似SQLALchemy的BaseModel
from tortoise import fields        # 类似SQLAlchemy的mapped_column

"""
tortoise中所有的数据模型,必须直接或间接继承于tortoise.models.Model
"""

class Class(Model):
    """班级"""
    # tortoise ORM默认生成一个主键,默认叫pk,
    # 如果不想要pk作为默认的主键,可以自己重新声明一个新的字段,把这个字段设置为pk=True,则会自动覆盖原来的pk主键
    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=255, unique=True, description='班级名称')

    def __repr__(self):
        """类似SQLAlchemy的__repr__,打印模型方法,这里也必须返回字符串格式"""
        # tortoise中的self.pk用于代表pk=True的那个字段值
        return f"Class(id={self.pk}, name={self.name})"


class Teacher(Model):
    """老师"""
    id = fields.IntField(pk=True, description='主键')
    name = fields.CharField(max_length=255, description='姓名')
    tno = fields.IntField(unique=True, description='工号')
    password = fields.CharField(max_length=255, description='密码')

    def __repr__(self):
        return f"Teacher(id={self.id}, tno={self.tno}, name={self.name})"


class Student(Model):
    """学生"""
    id = fields.IntField(pk=True, description='主键')
    sno = fields.IntField(description='学号')
    name = fields.CharField(max_length=255, description='姓名')
    password = fields.CharField(max_length=255, description='密码')
    # 一对多
    class_ = fields.ForeignKeyField('models.Class', related_name='students')
    # 多对多
    courses = fields.ManyToManyField('models.Course', related_name='students', description='学生选课表')

    def __repr__(self):
        return f"Student(id={self.id}, sno={self.sno}, name={self.name})"


class Course(Model):
    """课程"""
    id = fields.IntField(pk=True, description='主键')
    name = fields.CharField(max_length=255, description='课程名')
    teacher = fields.ForeignKeyField('models.Teacher', related_name='courses', description='课程讲师')

    def __repr__(self):
        return f"Course(id={self.id}, name={self.name}, teacher={self.teacher.name})"

常用模型字段声明

模型字段python格式mysql格式描述
fields.BigIntField()intbig int长整型
fieds.IntField()intint整型
fieds.SmallIntField()intsmallint整型
fields.DecimalField(max_digits=整体长度, decimal_places=小数点后的小数位)decimal.Decimalnumeric浮点型的定点数
fields.FloatField()floatfloat浮点型
fields.BooleanField()boolbool/tiny布尔类型
fields.CharField()strvarchar()可变长度字符串
fields.TextField()strtext文本类型
fields.DateField()datetime.datedate日期格式,“%Y-%m-%d”
fields.TimeField()datetime.timetime时间格式,“%H:%M:%S”
fields.DatetimeField()datetime.datetimedatetime日期时间格式,“%Y-%m-%d %H:%M:%S”
fields.IntEnumField()enum.Enumenum单选类型,成员必须是整型
fields.CharEnumField()enum.Enumenum单选类型,成员必须是字符串
fields.UUIDField()uuid.uuid4()struuid格式的字符串
fields.OneToOneField(“主模型导包路径”, related_name=“反向字段”, on_delete=fields.CASCADE)1对1,外键字段
fields.ForeignKeyField(“主模型导包路径”, related_name=“反向字段”)1对多,外键字段
fields.ManyToManyField(“主模型导包路径”, related_name=“反向字段”)多对多,外键字段

注册Tortoise到项目中,main.py,代码:

import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from tortoise.contrib.fastapi import register_tortoise
import settings

app = FastAPI()
# 把Tortoise-orm注册到App应用对象中
register_tortoise(
    app,
    config=settings.TORTOISE_ORM,
    generate_schemas=True,
    add_exception_handlers=True,
)


if __name__ == '__main__':
    uvicorn.run('main:app', host='0.0.0.0', port=8000, reload=True)
数据迁移

如果希望让tortoiseORM也像SQLAlchemy那样具备数据迁移功能【根据python代码中的数据模型自动建表】,可以考虑使用aerich迁移工具来完成数据迁移。aerich是一种开源的数据库ORM迁移工具,需要结合tortoise异步orm框架使用。安装aerich命令如下:

pip install aerich
首次运行需要初始化数据迁移工具

要使用aerich,必须在tortoiseORM的连接配置信息中新增关于aerich的模型。settings.py,代码:

# TORTOISE ORM的数据库连接配置
TORTOISE_ORM = {
    ....
    'apps': {  
        'models': { 
            'models': ['models', "aerich.models"],  # "aerich.models" 就是新增内容
            'default_connection': 'default', 
        }
    },
    ...
}

如果是项目开发的首次进行数据库迁移,则需要先进行aerich插件的初始化配置(这个只需要操作一次),项目根目录下执行[main.py入口文件所在目录下]:

# 切换到main.py所在目录下
cd xxx
aerich init -t settings.TORTOISE_ORM
# settings.TORTOISE_ORM 表示tortoiseORM配置信息所在导包路径

在这里插入图片描述

说明:

初始化完会在当前目录生成一个文件:pyproject.toml和一个文件夹:migrations

  • pyproject.toml:保存配置文件路径,低版本可能是aerich.ini
  • migrations:存放数据库的迁移历史文件
初始化数据库(一般情况下只用一次)

在项目根目录下使用终端执行初始化数据库的命令,如下:

aerich init-db

执行这段命令,aerich会自动根据TORTOISE_ORM配置字典的apps.models的models中填写的路径搜索所有直接或间接继承了tortoise.models.Model模型基类的所有数据表模型,根据这些数据表模型自动创建数据表。执行效果:

在这里插入图片描述

数据迁移的用法
命令描述
aerich migrate [–name 本次操作的备注信息]根据当前模型的实际结构生成一个迁移文件记录,备注信息建议是英文的
aerich upgrade对数据迁移进行升级操作(0->1->2…->n),表示把迁移记录中的SQL语句执行并同步到数据库中
aerich downgrade对数据迁移进行降级操作(n->…->2->1->0),表示回滚操作
aerich history查看数据库中的迁移版本历史
常用操作
添加数据

模型类.create() 使用模型类方法添加一条数据

student = await Student.create(name="张无忌", password='123456', sno=2009, class__id=1)

模型对象.save() 使用模型对象方法添加一条数据

student = Student(name="张无忌", password='123456', sno=2009, class__id=1)
await student.save()

模型类.bulk_create 批量添加多条数据

student_list = [
    Student(name="张晓明", password='123456', sno=2007, class__id=1),
    Student(name="张晓亮", password='123456', sno=2010, class__id=1),
]
await Student.bulk_create(student_list)
查询数据

get 获取一条数据,获取不到数据则报错

from tortoise import exceptions 
try:
    student = await Student.get(id=1)
except exceptions.DoesNotExist as e:
    # 查不到数据
    print(e)

get_or_none 获取一条数据,获取不到则返回None,不会报错

student = await Student.get_or_none(id=1)

all 查询所有数据,返回所有数据集(QuerySet对象),如果不加任何查询过滤条件,则默认返回表中的所有记录。

students = await Student.all()
更新数据

更新一条数据

# 先查询数据
student = await Student.get_or_none(id=1)
# 如果有数据,则修改模型对象
if student:
    student.name = "张晓龙"
    student.sex = True
    await student.save() # 保存数据,达到修改目的

更新一条或多条数据,取决于过滤条件

await Student.filter(id=1).update(name='张大明')
删除数据

删除一条数据

student = await Student.get_or_none(id=1)
if student:
    await student.delete()

删除一条或多条数据,取决于过滤条件

await Student.filter(id=1).delete()
过滤查询

filter 根据条件查询数据,返回满足条件的数据集(QuerySet对象),后续可以使用 all() 方法获取所有的查询结果,或者使用 first() 方法获取第一个结果。

# 查询所有的男生
students = await Student.filter(sex=True).all()
for student in students:
    print(student.id, student.name)
    
# 获取名字为'xiaoming'的第一个数据
student = await Student.filter(name="xiaoming").first()
if student:
    print(student.id, student.name)
else:
    print("No Found")   

使用比较运算符进行条件过滤

# =  获取男生的数据
students = await Student.filter(sex=True).all()

# __not 获取学号(sno)不等于1的学生数据
students = await Student.filter(sno__not=1).all()

# __gt 获取年龄(age)大于16的数据
students = await Student.filter(age__gt=16).all()

# __gte 获取id大于等于1的数据
students = await Student.filter(id__gte=1).all()

# __lt 获取age小于16的数据
students = await Student.filter(age__lt=16).all()

# __lte 获取age小于等于18的数据
students = await Student.filter(age__lte=18).all()

使用成员运算符作为过滤条件

# __in  获取姓名 在 指定列表中的数据
names = ['小明', '小红']
students = await Student.filter(name__in=names).all()
for student in students:
    print(student.id, student.name)


# __not_in 获取姓名 不在 指定列表中的数据
names = ['小明', '小红']
students = await Student.filter(name__not_in=names).all()
for student in students:
    print(student.id, student.name)

使用模糊查询作为过滤条件,类似SQL语句中的Like关键字。

"""
Tortoise ORM 提供了icontains、istartswith、iendswith等操作符进行模糊查询。
"""

# 字段istartswith 以指定条件开头 
# 姓张的同学
student = await Student.filter(sno__istartswith='张')

# 名字里面包含'白'字的同学
student = await Student.filter(name__icontains='白')

# 手机尾号是5989的学生
student = await Student.filter(mobile__iendswith='5989')

__range查询数值大小在指定区间的数据

# 查询学号在2001-2024之间的
students = await Student.filter(sno__range=[2001, 2024]).all()
for student in students:
    print(student.id, student.name)

__isnull:是否为空(IS NULL)

# 查询学生姓名为空的数据
students = await Student.filter(name__isnull=True).all()

__regex:正则表达式匹配(REGEXP 或 LIKE,取决于数据库)

__iregex:不区分大小写的正则表达式匹配

# 查询名字匹配正则表达式的数据
pattern = r'^张.*'  # 以'张'开头的名字
students = await Student.filter(name__regex=pattern).all()

exclude() 方法用于排除满足条件的数据,返回不满足条件的数据集。与filter结果相反。

# 获取名字不是'赵小明'的所有数据
students = await Student.exclude(name='赵小明').all()
for student in students:
    print(student.id, student.name)
结果处理

count() 方法用于统计满足条件的数据数量。

# 统计女生的数量
count = await Student.filter(sex=False).count()
print(count)

order_by() 方法用于按照指定字段排序查询结果。

# 按id升序(从小到大)获取所有数据
students = await Student.all().order_by("id")
for student in students:
	print(student.id, student.name)

# 按id降序(从大到小,字段左边使用减号)获取所有数据
students = await Student.all().order_by("-id")
for student in students:
	print(student.id, student.name)

limitoffset 用于限制返回的结果数量和跳过指定数量的结果。

limit 对结果进行数量限制

offset 查询结果提取的开始下标

# limit 和 offset() 方法可以用于限制返回的结果数量和跳过指定数量的结果。

# 获取前5个用户
sutdents_0_5 = await Student.all().limit(5)
for student in sutdents_0_5:
	print(student.id, student.name)

# 跳过前5个用户,再获取5个用户
sutdents_6_10 = await Student.all().offset(5).limit(5)
for student in sutdents_6_10:
    print(student.id, student.name)

values() 指定列查询,表示只查询指定的单个或多个字段。

print(await students.courses.values("id","name"))
连表查询

连表查询指定外键所在模型的部分字段

students = await Student.all().values("name", "class___name", "courses__name")

连表查询指定外键所在模型的全部字段

students = await Student.all().prefetch_related("class_", "courses")
案例:学生选课系统

基于tortoise-orm实现学生管理,项目目录结构如下:

school/  # 项目根目录
├── static/  # 项目静态文件[css/js/images/视频/音频]文件存储目录
│     └── images/ # 图片存储目录
├── apps/  # 项目应用目录
│   ├── __init__.py
│   ├── news/  # 新闻相关的应用代码
│   │   └── models.py # 新闻相关模型文件保存目录
│   ├── students/ # 学生相关的应用代码
│   │   ├── templates/ # 学生相关模板文件保存目录
│   │   ├── models.py # 学生相关模型文件保存目录
│   │   ├── api.py      # 学生相关接口文件,前后端分离【目前不存要创建】
│   │   └── views.py  # 学生相关视图文件,前后端不分离
│   └── user/  # 用户相关的应用代码
│       └── models.py  # 用户相关模型的保存文件
├── main.py  # 项目程序入口,从这里启动项目
├── migrations/  # 数据迁移文件存储目录
│   └── models/  # 数据迁移的历史版本文件
└── settings.py   # 项目的配置文件

school/settings.py,代码:

""""项目配置"""


# tortoise-orm数据库配置
TORTOISE_ORM = {
    "connections": {
        "default": {
            'engine': 'tortoise.backends.mysql',  # MySQL or Mariadb
            'credentials': {  # 连接参数
                'host': '127.0.0.1',  # 数据库IP/域名地址
                'port': 3306,  # 端口
                'user': 'root',  # 连接账户
                'password': '123',  # 连接密码
                'database': 'school',  # 数据库
                'charset': 'utf8mb4',  # 编码
                'minsize': 1,  # 连接池中的最小连接数
                'maxsize': 5,  # 连接池中的最大连接数
                "echo": True  # 执行数据库操作时,是否打印SQL语句
            }
        }
    },
    'apps': {  # 默认所在的应用目录
        'models': {  # 数据模型的分组名
            'models': ['apps.news.models', 'apps.user.models', 'apps.students.models', 'aerich.models'],  # 模型所在目录文件的导包路径[字符串格式]
            'default_connection': 'default',  # 上一行配置中的模型列表的默认连接配置
        }
    },
    # 当use_tz=False,当前tortoise-orm会默认使用当前程序所在操作系统的时区,
    'use_tz': True,
    # 当use_tz=True时,当前tortoise-orm会默认使用timezone配置项中的时区
    'timezone': 'Asia/Shanghai'  # 自定义时区,
}

school/apps/students/models.py,代码:

"""模型文件 Model"""
from tortoise import models, fields


class Class(models.Model):
    # 字段列表
    id = fields.IntField(pk=True, description='主键')
    name = fields.CharField(max_length=50, unique=True, description='班级名称')

    # 元数据
    class Meta:
        table = "school_class"
        description="班级信息"

    def __repr__(self):
        return f"Class (id={self.pk}, name={self.name})"


class Student(models.Model):
    id = fields.IntField(pk=True, description='主键')
    sno = fields.IntField(description='学号')
    name = fields.CharField(max_length=50, unique=True, description='班级名称')
    password = fields.CharField(max_length=255, description='密码')
    class_ = fields.ForeignKeyField('models.Class', related_name='students', description='所属班级')
    courses = fields.ManyToManyField('models.Course', related_name='students', description='课表')
    sex = fields.BooleanField(default=True, null=True, description='性别')
    # auto_now_add=True 当新增数据时,默认使用当前时间对象作为默认值填写到当前字段
    created_time = fields.DatetimeField(auto_now_add=True, description='创建时间')
    # auto_now=True 当数据被修改时,默认使用当前时间对象作为默认值写入到当前字段
    updated_time = fields.DatetimeField(auto_now=True, description="更新时间")

    # 元数据
    class Meta:
        table = "school_student"
        description="学生信息"

    def __repr__(self):
        return f"Student(id={self.id}, sno={self.sno}, name={self.name})"


class Teacher(models.Model):
    id = fields.IntField(pk=True, description='主键')
    name = fields.CharField(max_length=255, description='姓名')
    sex = fields.BooleanField(default=True, description='性别')
    tno = fields.IntField(unique=True, description='工号')
    password = fields.CharField(max_length=255, description='密码')

    # 元数据
    class Meta:
        table = "school_teacher"
        description="老师信息"

    def __repr__(self):
        return f"Teacher(id={self.id}, tno={self.tno}, name={self.name})"


class Course(models.Model):

    id = fields.IntField(pk=True, description='主键')
    name = fields.CharField(max_length=255, description='课程名')
    teacher = fields.ForeignKeyField('models.Teacher', related_name='courses', description='课程讲师')

    # 元数据
    class Meta:
        table = "school_course"
        description="课程信息"

    def __repr__(self):
        return f"Teacher(id={self.id}, name={self.name}, teacher={this.teacher.name})"

school/main.py,代码:

"""程序入口,将来在这个文件中启动项目"""
import uvicorn
from fastapi import FastAPI
from apps.students.views import router as student_router
from fastapi.staticfiles import StaticFiles
from tortoise.contrib.fastapi import register_tortoise
import settings

app = FastAPI()

# 把tortoise-orm注册到app中
register_tortoise(
    app,
    config=settings.TORTOISE_ORM,
    generate_schemas=True,
    add_exception_handlers=True,
)

# 允许外界通过指定路径访问到服务端对应目录下的文件信息
app.mount("/static", StaticFiles(directory="static"))

app.include_router(student_router, prefix='/students')

if __name__ == '__main__':
    uvicorn.run("main:app", host='127.0.0.1', port=8000, reload=True)

接下来,就可以编写操作数据的视图代码了,school/students/views.py,代码:

"""视图文件 View"""
# 导入路由类
from fastapi import APIRouter
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="apps/students/templates")

# 创建一个分组路由对象
router = APIRouter()

"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
    """添加班级[表单功能]"""
    return templates.TemplateResponse("add_class.html", locals())
班级管理-添加班级功能

展示添加班级的表单数据,school/apps/students/templates/add_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>添加班级信息</h1>
      <form action="/students/class/add" method="post">
        <div class="item">
          <label>班级名称:</label>
          <input type="text" name="name">
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

``school/students/views.py`,代码:

"""视图文件 View"""
# 导入路由类
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="apps/students/templates")

# 创建一个分组路由对象
router = APIRouter()

"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
    """添加班级[表单展示]"""
    return templates.TemplateResponse("add_class.html", locals())


@router.post('/class/add')
async def add_class(name: str = Form()):
    """添加班级[数据处理]"""
    print(name)
    return {"提交成功!"}

运行项目之前,先保证school数据库中的数据表没有与项目同名的(当然,也可以清空school数据库中所有的数据表):

cd xxx/school
aerich init -t settings.TORTOISE_ORM
aerich init-db

访问路径:http://127.0.0.1:8000/students/class/add,效果如下:

在这里插入图片描述

有了基本效果以后,接下来,我们通过学习Tortoise-orm的数据库操作来完成业务功能。

基于添加操作完成班级数据的添加

school/apps/students/views.py,代码:

"""视图文件 View"""
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
from . import models
from tortoise.exceptions import IntegrityError

templates = Jinja2Templates(directory="apps/students/templates")

# 创建一个分组路由对象
router = APIRouter()

"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
    """添加班级[表单展示]"""
    return templates.TemplateResponse("add_class.html", locals())


@router.post('/class/add')
async def add_class(request: Request, name: str = Form()):
    """添加班级[数据处理]"""
    try:
        # 添加数据,方式1,通过模型类提供的create来添加数据
        # class_ = await models.Class.create(name=name)
        # print(class_)

        # 添加数据,方式2,通过模型类实例对象的save方法来添加数据
        class_ = models.Class(name=name)
        await class_.save()
        errors = "创建成功!"
        url = "/students/class"
        return templates.TemplateResponse("success.html",locals())
    except IntegrityError as e:
        errors = "当前班级已存在,不能重复添加!"
        url = "/students/class/add"
        return templates.TemplateResponse("errors.html", locals())
    except Exception as e:
        errors = "位置的错误,导致添加数据失败!"
        url = "/students/class/add"
        return templates.TemplateResponse("errors.html", locals())

新增错误过渡页面和成功过渡页面

school/apps/students/templates/errors.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
      body{
          margin: 0;
          padding: 0;
      }
      .wrapper{
          background: url("/static/images/errors.png") no-repeat;
          background-position: center 20px;
          background-size: 20%;
          position: relative;
          padding: 300px;
      }
      .title{
          font-size: 36px;
      }
      p{
          text-align: center;
      }
  </style>
</head>
<body>
  <div class="wrapper">
      <p class="title">{{ errors }}</p>
      <p>页面跳转中,<span class="has_time">5</span>秒后跳转</p>
  </div>
  <script>
    var time = 3;
    var has_time = document.querySelector('.has_time')
    setInterval(()=>{
      time-=1;
      if(time<1){
        location.href="{{ url }}";
      }else{
        has_time.innerHTML = time;
      }
    }, 1000)
  </script>
</body>
</html>

school/apps/students/templates/success.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
      body{
          margin: 0;
          padding: 0;
      }
      .wrapper{
          background: url("/static/images/success.jpg") no-repeat;
          background-position: center 20px;
          background-size: 40%;
          position: relative;
          padding: 300px;
      }
      .title{
          font-size: 36px;
      }
      p{
          text-align: center;
      }
  </style>
</head>
<body>
  <div class="wrapper">
      <p class="title">{{ errors }}</p>
      <p>页面跳转中,<span class="has_time">5</span>秒后跳转</p>
  </div>
  <script>
    var time = 3;
    var has_time = document.querySelector('.has_time')
    setInterval(()=>{
      time-=1;
      if(time<1){
        location.href="{{ url }}";
      }else{
        has_time.innerHTML = time;
      }
    }, 1000)
  </script>
</body>
</html>

提示图标如下,保存即可。

在这里插入图片描述

在这里插入图片描述

班级管理-班级列表功能

班级列表页的默认模板效果,school/apps/students/templates/list_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">班级</div>
        <div class="fields">操作</div>
      </div>
      <div class="list-item">
        <div class="keys">1</div>
        <div class="keys">人工2023</div>
        <div class="keys">
          <span class="btn">编辑</span>
          <span class="btn">删除</span>
        </div>
      </div>
      <div class="list-item">
        <div class="keys">2</div>
        <div class="keys">人工2024</div>
        <div class="keys">
          <span class="btn">编辑</span>
          <span class="btn">删除</span>
        </div>
      </div>
      <div class="pagination">
        <a href="?pn=1">1</a>
        <a href="?pn=2">2</a>
        <a href="?pn=3">3</a>
      </div>
    </div>
  </div>
</body>
</html>

school/apps/students/views.py,代码:

"""视图文件 View"""
from fastapi import APIRouter, Form
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
from . import models
from tortoise.exceptions import IntegrityError
from tortoise.functions import Count
import math
templates = Jinja2Templates(directory="apps/students/templates")

# 创建一个分组路由对象
router = APIRouter()

"""班级管理"""
@router.get("/class/add")
async def add_class(request: Request):
    """添加班级[表单展示]"""
    return templates.TemplateResponse("add_class.html", locals())


@router.post('/class/add')
async def add_class(request: Request, name: str = Form()):
    """添加班级[数据处理]"""
    try:
        # 添加数据,方式1,通过模型类提供的create来添加数据
        # class_ = await models.Class.create(name=name)
        # print(class_)

        # 添加数据,方式2,通过模型类实例对象的save方法来添加数据
        class_ = models.Class(name=name)
        await class_.save()
        errors = "创建成功!"
        url = "/students/class" # 班级列表的url地址
        return templates.TemplateResponse("success.html",locals())
    except IntegrityError as e:
        errors = "当前班级已存在,不能重复添加!"
        url = "/students/class/add" # 返回添加页面的表单url地址
        return templates.TemplateResponse("errors.html", locals())
    except Exception as e:
        errors = "未知的错误,导致添加数据失败!"
        url = "/students/class/add" # 返回添加页面的表单url地址
        return templates.TemplateResponse("errors.html", locals())


@router.get("/class")
async def list_class(request: Request, pn:int = 1):
    """
    班级列表
    # 数据分页,依靠SQL语句来完成的
    # 第1页   offset 0, limit 6   下标:0,1,2,3,4,5
    # 第2页   offset 6, limit 6   下标: 6,7,8,9,10,11
    # 第3页   offset 12, limit 6  下标: 12,...
    """
    # 每一页展示的数据量
    limit = 6
    # 获取总数据量
    class_list = await models.Class.all()
    total = len(class_list)
    # 向上取整获取总页数
    max_pn = math.ceil(total/limit)
    # 判断当前页码不能超出总页码的范围
    if pn > max_pn:
        pn = max_pn

    if pn < 1:
        pn = 1

    start = (pn-1) * limit # 每一页数据的查询开始位置
    class_list = await models.Class.all().limit(limit).offset(start)
    return templates.TemplateResponse('list_class.html', locals())

school/apps/students/templates/list_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">班级</div>
        <div class="fields">操作</div>
      </div>
      {% for class in class_list %}
      <div class="list-item">
        <div class="keys">{{ class.id }}</div>
        <div class="keys">{{ class.name }}</div>
        <div class="keys">
          <span class="btn">编辑</span>
          <span class="btn">删除</span>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>

访问http://127.0.0.1:8000/students/class,效果如下:

在这里插入图片描述

班级管理-删除班级功能

school/apps/students/views.py,代码:

from tortoise.exceptions import IntegrityError, DoesNotExist
.....
前面代码省略
.....

@router.get('/class/del/{id}')
async def del_class(request: Request, id: int):
    """删除班级"""
    # # 删除方式1:
    # try:
    #     # 使用get方法获取一个数据,查不到或者查询到的数据有多条都会报错
    #     class_ = await models.Class.get(id=id)
    #     # 使用first方法获取一个数据,查不到数据会返回None,查询到多条结果,则返回下标为0的数据作为结果
    # except DoesNotExist:
    #     errors = "不存在的班级,无法删除!"
    #     url = "/students/class"  # 返回班级列表页面的url地址
    #     return templates.TemplateResponse("errors.html", locals())

    # 删除方式2:
    class_ = await models.Class.filter(id=id).first()
    if class_:
        await class_.delete()
    errors = "删除成功!"
    url = "/students/class"  # 班级列表的url地址
    return templates.TemplateResponse("success.html", locals())

school/apps/students/templates/list_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">班级</div>
        <div class="fields">操作</div>
      </div>
      {% for class in class_list %}
      <div class="list-item">
        <div class="keys">{{ class.id }}</div>
        <div class="keys">{{ class.name }}</div>
        <div class="keys">
          <span class="btn">编辑</span>
          <a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>
班级管理-更新班级功能

服务端提供显示更新数据表单视图函数,school/apps/students/views.py,代码:

@router.get("/class/edit/{id}")
async def edit_class(request: Request, id: int):
    """显示编辑班级数据的表单页面"""
    class_ = await models.Class.filter(id=id).first()
    """避免写面条式代码不利于维护,不利于后续人员的查看"""
    # if class_:
    #     return templates.TemplateResponse("edit_class.html", locals())
    # else:
    #     errors = "当前班级不存在,请重新确认!"
    #     url = "/students/class"  # 班级列表的url地址
    #     return templates.TemplateResponse("errors.html", locals())

    if not class_:
        errors = "当前班级不存在,请重新确认!"
        url = "/students/class"  # 班级列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    return templates.TemplateResponse("edit_class.html", locals())

在列表页中,提供点击跳转到修改指定班级的表单页面的a标签,school/apps/students/templates/list_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>班级信息列表 <a class="btn add-btn" href="/students/class/add">新建班级</a></h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">班级</div>
        <div class="fields">操作</div>
      </div>
      {% for class in class_list %}
      <div class="list-item">
        <div class="keys">{{ class.id }}</div>
        <div class="keys">{{ class.name }}</div>
        <div class="keys">
          <a href="/students/class/edit/{{ class.id }}"><span class="btn">编辑</span></a>
          <a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>

编辑班级数据的表单页面,school/apps/students/templates/edit_class.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>编辑班级信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>编辑班级信息 <a class="btn add-btn" href="/students/class">班级列表</a></h1>
      <form action="/students/class/edit/{{ class_.id }}" method="post">
        <div class="item">
          <label>班级名称:</label>
          <input type="text" name="name" value="{{ class_.name }}">
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

在表单提交数据的时候,服务端提供一个post的接口接受本次编辑的数据,school/apps/students/views.py,代码:

@router.post("/class/edit/{id}")
async def edit_class(request: Request, id: int, name: str=Form()):
    """编辑班级数据的表单处理程序"""
    try:
        # # 更新数据,方式1:
        # class_ = await models.Class.filter(id=id).first()
        # if not class_:
        #     errors = "当前班级不存在,请重新确认!"
        #     url = "/students/class"  # 班级列表的url地址
        #     return templates.TemplateResponse("errors.html", locals())
        #
        # # 把查询出来的模型对象的属性进行修改
        # class_.name = name
        # await class_.save()

        # 更新数据,方式2:
        await models.Class.filter(id=id).update(**{"name": name})
    except IntegrityError as e:
        errors = "已存在同名班级,无法修改!"
        url = "/students/class"  # 班级列表的url地址
        return templates.TemplateResponse("errors.html", locals())
    except Exception as e:
        errors = "未知的错误导致修改失败!"
        url = "/students/class"  # 班级列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    errors = "编辑成功!"
    url = "/students/class"  # 班级列表的url地址
    return templates.TemplateResponse("success.html", locals())
学生管理-添加学生功能

school/apps/students/templates/add_student.html,填写学生数据的表单模板,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加学生信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>添加学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
      <form action="/students/stu/add" method="post">
        <div class="item">
          <label>学号:</label><input type="text" name="sno">
        </div>
        <div class="item">
          <label>班级:</label>
          <select name="class_id">
            <option value="52">中文11班</option>
            <option value="51">商务英语10班</option>
          </select>
        </div>
        <div class="item">
          <label>账号:</label><input type="text" name="name">
        </div>
        <div class="item">
          <label>密码:</label><input type="password" name="password">
        </div>
        <div class="item">
          <label>性别:</label>
          <label><input type="radio" name="sex" checked value="1"></label>
          <label><input type="radio" name="sex" value="0"></label>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

效果:

在这里插入图片描述

在视图中加载当前表单,显示到浏览器,school/apps/students/views.py,视图,代码:

"""学生管理"""
@router.get("/stu/add")
async def add_student(request: Request):
    """添加学生[表单显示]"""
    class_list = await models.Class.all()
    return templates.TemplateResponse("add_student.html", locals())

模板中显示班级列表,让用户在浏览器可以选择对应的班级,school/apps/students/templates/add_student.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加学生信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>添加学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
      <form action="/students/stu/add" method="post">
        <div class="item">
          <label>学号:</label><input type="text" name="sno">
        </div>
        <div class="item">
          <label>班级:</label>
          <select name="class_id">
            {% for class_ in class_list %}
            <option value="{{ class_.id }}">{{ class_.name }}</option>
            {% endfor %}

          </select>
        </div>
        <div class="item">
          <label>账号:</label><input type="text" name="name">
        </div>
        <div class="item">
          <label>密码:</label><input type="password" name="password">
        </div>
        <div class="item">
          <label>性别:</label>
          <label><input type="radio" name="sex" checked value="1"></label>
          <label><input type="radio" name="sex" value="0"></label>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

效果:

在这里插入图片描述

在视图中,接受并验证客户端提交过来的表单数据,并保存数据库中,school/apps/students/views.py,视图,代码:

@router.post("/stu/add")
async def add_student(request: Request, sno: int = Form(), name: str=Form(), password: str = Form(), class_id: int=Form(), sex: bool=Form()):
    """添加学生[表单处理]"""
    """接收数据,验证数据"""
    # 学号不能重复
    student = await models.Student.filter(sno=sno).first()
    if student:
        errors = "当前学号已经重复,请重新填写!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())
    # 班级必须存在
    class_ = await models.Class.filter(id=class_id).first()
    if not class_:
        errors = "当前班级不存在,请重新选择!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 用户名不能重复,必须唯一
    student = await models.Student.filter(name=name).first()
    if student:
        errors = "当前账号已经重复,请重新填写!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # todo 密码需要加密,后面加上去
    password =
    """添加数据"""
    try:
        student = await models.Student.create(sno=sno, name=name, password=password, sex=sex, class__id=class_id)
        errors = "添加成功!"
        url = "/students/stu"  # 学生列表的url地址
        return templates.TemplateResponse("success.html", locals())
    except Exception as e:
        errors = "未知的错误导致添加失败!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

上面的代码中,虽然已经实现了学生的数据添加功能,但是密码直接明文保存到数据库中,并不安全。所以我们需要对密码进行加盐加密,school/apps/students/views.py,视图,代码:

import hmac, secrets, string
import settings

... 中间代码省略...
... 中间代码省略...
... 中间代码省略...

@router.post("/stu/add")
async def add_student(request: Request, sno: int = Form(), name: str=Form(), password: str = Form(), class_id: int=Form(), sex: bool=Form()):
    """添加学生[表单处理]"""
    """接收数据,验证数据"""
    # 学号不能重复
    student = await models.Student.filter(sno=sno).first()
    if student:
        errors = "当前学号已经重复,请重新填写!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())
    # 班级必须存在
    class_ = await models.Class.filter(id=class_id).first()
    if not class_:
        errors = "当前班级不存在,请重新选择!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 用户名不能重复,必须唯一
    student = await models.Student.filter(name=name).first()
    if student:
        errors = "当前账号已经重复,请重新填写!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 密码加盐加密
    salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6))
    password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password+salt).encode(encoding="utf-8"), digestmod='SHA256').hexdigest()
    password = f"{password}|{salt}"
    """添加数据"""
    try:
        student = await models.Student.create(sno=sno, name=name, password=password, sex=sex, class__id=class_id)
        errors = "添加成功!"
        url = "/students/stu"  # 学生列表的url地址
        return templates.TemplateResponse("success.html", locals())
    except Exception as e:
        errors = "未知的错误导致添加失败!"
        url = "/students/stu/add"  # 添加学生[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

school/settings.py,代码:

# 用于加密的秘钥[整个项目的统一盐值,随机生成或者自己乱写就可以了],数据库中有数据以后,就不能随便更改了!
SECRET_KEY = "a@#@)%sdkgml12,^(@M,.d,acxm"
学生管理-学生列表功能

school/apps/students/views.py,视图,代码:

@router.get('/stu')
async def list_student(request: Request, pn: int=1):
    """学生列表"""
    import math
    # 每一页展示的数据量
    limit = 6
    # 获取总数据量
    student_list = await models.Student.all()
    total = len(student_list)
    # 向上取整获取总页数
    max_pn = math.ceil(total/limit)
    # 判断当前页码不能超出总页码的范围
    if pn > max_pn:
        pn = max_pn

    if pn < 1:
        pn = 1

    start = (pn-1) * limit # 每一页数据的查询开始位置
    student_list = await models.Student.all().limit(limit).offset(start)
    for student in student_list:
        student.my_class = await student.class_
    return templates.TemplateResponse('list_student.html', locals())

school/apps/students/templates/add_student.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>学生信息列表</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>学生信息列表 <a class="btn add-btn" href="/students/stu/add">新建学生</a></h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">账号</div>
        <div class="fields">性别</div>
        <div class="fields">班级</div>
        <div class="fields">操作</div>
      </div>
      {% for student in student_list %}
      <div class="list-item">
        <div class="keys">{{ student.id }}</div>
        <div class="keys">{{ student.name }}</div>
        <div class="keys">{% if student.sex %}男{% else %}女{% endif %}</div>
        <div class="keys">{{ student.my_class.name }}</div>
        <div class="keys">
          <a href="/students/stu/edit/{{ student.id }}"><span class="btn">编辑</span></a>
          <a href="/students/stu/del/{{ student.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>

访问学生列表http://127.0.0.1:8000/students/stu,浏览器显示效果,如下:

在这里插入图片描述

学生管理-删除学生功能

school/apps/students/views.py,代码:

@router.get("/stu/del/{id}")
async def del_student(request: Request, id: int):
    """删除学生数据"""
    await models.Student.filter(id=id).delete()
    errors = "删除成功!"
    url = "/students/stu"  # 学生列表的url地址
    return templates.TemplateResponse("success.html", locals())
学生管理-更新学生数据

显示更新学生数据的表单页面,schooh/apps/students/views.py,代码:

@router.get("/stu/edit/{id}")
async def edit_student(request: Request, id: int):
    """更新学生数据[显示表单]"""
    student = await models.Student.filter(id=id).first()
    if not student:
        errors = "当前学生不存在!请重新确认!"
        url = "/students/stu"  # 学生列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 和添加一样,需要查询班级列表,让学生可以修改班级ID
    class_list = await models.Class.all()

    return templates.TemplateResponse("edit_student.html", locals())

表单页面模板是直接复制add_student.html的模板,因为这两个页面的表单数据一样,school/apps/students/templates/edit_student.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>更新学生信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>更新学生信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
      <form action="/students/stu/edit/{{ student.id }}" method="post">
        <div class="item">
          <label>学号:</label><input type="text" name="sno" value="{{ student.sno }}">
        </div>
        <div class="item">
          <label>班级:</label>
          <select name="class_id">
            {% for class_ in class_list %}
            <option value="{{ class_.id }}" {% if student.class__id == class_.id %}selected{% endif %}>{{ class_.name }}</option>
            {% endfor %}
          </select>
        </div>
        <div class="item">
          <label>账号:</label><input type="text" name="name" value="{{ student.name }}">
        </div>
        <div class="item">
          <label>密码:</label><input type="password" name="password" placeholder="如需重设密码,请填写新密码。" title="如果需要重新设置密码,请填写新密码。">
        </div>
        <div class="item">
          <label>性别:</label>
          <label><input type="radio" name="sex" {% if student.sex %}checked{% endif %} value="1"></label>
          <label><input type="radio" name="sex" {% if not student.sex %}checked{% endif %} value="0"></label>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

在服务端提供表单处理的接口,schooh/apps/students/views.py,代码:

@router.post("/stu/edit/{id}")
async def edit_student(request: Request, id: int, sno: int = Form(), name: str=Form(), password: str = Form(""), class_id: int=Form(), sex: bool=Form()):
    """更新学生数据[表单处理]"""
    student = await models.Student.filter(id=id).first()
    if not student:
        errors = "当前学生不存在!请重新确认!"
        url = "/students/stu"  # 学生列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    try:
        student.sno = sno
        student.name = name
        student.class__id = class_id
        student.sex = sex
        if len(password) > 1:
            # 密码加盐加密
            salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6))  # 随机盐值
            password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password + salt).encode(encoding="utf-8"),
                                digestmod='SHA256').hexdigest()
            password = f"{password}|{salt}"

            student.password = password

        await student.save()

        errors = "更新成功!"
        url = "/students/stu"  # 学生列表的url地址
        return templates.TemplateResponse("success.html", locals())

    except Exception as e:
        errors = f"更新出错!错误提示:{e}"
        url = f"/students/stu/edit/{id}"  # 对应ID的学生的编辑表单的url地址
        return templates.TemplateResponse("errors.html", locals())
班级管理-查询当前班级学生

主要是在原有所有学生列表的基础上新增一个Query查询参数,表示班级ID。

在班级列表页模板中,school/apps/students/templates/list_class.html,代码:

      {% for class in class_list %}
      <div class="list-item">
        <div class="keys">{{ class.id }}</div>
        <div class="keys">{{ class.name }}</div>
        <div class="keys">
          <a href="/students/stu?class_id={{ class.id }}"><span class="btn">学生</span></a>
          <a href="/students/class/edit/{{ class.id }}"><span class="btn">编辑</span></a>
          <a href="/students/class/del/{{ class.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}

在服务端接口代码中原来的学生列表方法中新增接收查询参数class_id,并在代码中判断是否有该参数传递过来,如果有则按班级查询学生信息,school/apps/students/views.py,代码:

@router.get('/stu')
async def list_student(request: Request, pn: int=1, class_id: int = None):
    """学生列表"""
    import math
    # 每一页展示的数据量
    limit = 6
    query = models.Student.all()
    if class_id:
        query = models.Student.filter(class__id=class_id).all()

    # 获取总数据量
    total = await query.count()

    # 向上取整获取总页数
    max_pn = math.ceil(total/limit)
    # 判断当前页码不能超出总页码的范围
    if pn > max_pn:
        pn = max_pn

    if pn < 1:
        pn = 1

    start = (pn-1) * limit # 每一页数据的查询开始位置
    student_list = await query.limit(limit).offset(start)
    for student in student_list:
        student.my_class = await student.class_
    return templates.TemplateResponse('list_student.html', locals())

老师管理-老师列表

school/apps/students/views.py,视图代码:

@router.get('/teacher')
async def list_teacher(request: Request, pn: int=1):
    """老师列表"""
    import math
    # 每一页展示的数据量
    limit = 6
    query = models.Teacher.all()

    # 获取总数据量
    total = await query.count()

    # 向上取整获取总页数
    max_pn = math.ceil(total/limit)
    # 判断当前页码不能超出总页码的范围
    if pn > max_pn:
        pn = max_pn

    if pn < 1:
        pn = 1

    start = (pn-1) * limit # 每一页数据的查询开始位置
    teacher_list = await query.limit(limit).offset(start)
    return templates.TemplateResponse('list_teacher.html', locals())

复制原来学生的列表页模板,改名为:list_teacher.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>老师信息列表</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>老师信息列表
        <a class="btn add-btn" href="/students/teacher/add">新建老师</a>
      </h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">工号</div>
        <div class="fields">账号</div>
        <div class="fields">操作</div>
      </div>
      {% for teacher in teacher_list %}
      <div class="list-item">
        <div class="keys">{{ teacher.id }}</div>
        <div class="keys">{{ teacher.tno }}</div>
        <div class="keys">{{ teacher.name }}</div>
        <div class="keys">
          <a href="/students/teacher/edit/{{ teacher.id }}"><span class="btn">编辑</span></a>
          <a href="/students/teacher/del/{{ teacher.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>
老师管理-添加老师功能

视图代码:

@router.get("/teacher/add")
async def add_teacher(request: Request):
    """添加老师[表单显示]"""
    return templates.TemplateResponse("add_teacher.html", locals())


@router.post("/teacher/add")
async def add_teacher(request: Request, tno: int = Form(), name: str=Form(), password: str = Form()):
    """添加老师[表单处理]"""
    """接收数据,验证数据"""
    # 学号不能重复
    teacher = await models.Teacher.filter(tno=tno).first()
    if teacher:
        errors = "当前工号已经重复,请重新填写!"
        url = "/students/teacher/add"  # 添加老师[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 用户名不能重复,必须唯一
    teacher = await models.Teacher.filter(name=name).first()
    if teacher:
        errors = "当前账号已经重复,请重新填写!"
        url = "/students/teacher/add"  # 添加老师[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    # 密码加盐加密
    salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6)) # 随机盐值
    password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password+salt).encode(encoding="utf-8"), digestmod='SHA256').hexdigest()
    password = f"{password}|{salt}"
    """添加数据"""
    try:
        student = await models.Teacher.create(tno=tno, name=name, password=password)
        errors = "添加成功!"
        url = "/students/teacher"  # 老师列表的url地址
        return templates.TemplateResponse("success.html", locals())
    except Exception as e:
        errors = "未知的错误导致添加失败!"
        url = "/students/teacher/add"  # 添加老师[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

模板从add_student.html中复制改名为add_teacher.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加老师信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>添加老师信息 <a class="btn add-btn" href="/students/teacher">老师列表</a></h1>
      <form action="/students/teacher/add" method="post">
        <div class="item">
          <label>工号:</label><input type="text" name="tno">
        </div>
        <div class="item">
          <label>账号:</label><input type="text" name="name">
        </div>
        <div class="item">
          <label>密码:</label><input type="password" name="password">
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>
老师管理-删除老师功能

视图代码:

@router.get("/teacher/del/{id}")
async def del_teacher(request: Request, id: int):
    """删除老师数据"""
    await models.Teacher.filter(id=id).delete()
    errors = "删除成功!"
    url = "/students/teacher"  # 老师列表的url地址
    return templates.TemplateResponse("success.html", locals())

老师管理-编辑老师功能

视图代码:

@router.get("/teacher/edit/{id}")
async def edit_teacher(request: Request, id: int):
    """更新老师数据[显示表单]"""
    teacher = await models.Teacher.filter(id=id).first()
    if not teacher:
        errors = "当前老师不存在!请重新确认!"
        url = "/students/teacher"  # 老师列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    return templates.TemplateResponse("edit_teacher.html", locals())


@router.post("/teacher/edit/{id}")
async def edit_teacher(request: Request, id: int, tno: int = Form(), name: str=Form(), password: str = Form("")):
    """更新老师数据[表单处理]"""
    teacher = await models.Teacher.filter(id=id).first()
    if not teacher:
        errors = "当前老师不存在!请重新确认!"
        url = "/students/teacher"  # 老师列表的url地址
        return templates.TemplateResponse("errors.html", locals())

    try:
        teacher.tno = tno
        teacher.name = name
        if len(password) > 1:
            # 密码加盐加密
            salt = "".join(secrets.choice(string.ascii_letters + string.digits) for i in range(6))  # 随机盐值
            password = hmac.new(settings.SECRET_KEY.encode(encoding="utf-8"), (password + salt).encode(encoding="utf-8"),
                                digestmod='SHA256').hexdigest()
            password = f"{password}|{salt}"

            teacher.password = password

        await teacher.save()

        errors = "更新成功!"
        url = "/students/teacher"  # 老师列表的url地址
        return templates.TemplateResponse("success.html", locals())

    except Exception as e:
        errors = f"更新出错!错误提示:{e}"
        url = f"/students/teacher/edit/{id}"  # 对应ID的老师的编辑表单的url地址
        return templates.TemplateResponse("errors.html", locals())

模板复制edit_student.html,改名为edit_teacher.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>更新老师信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>更新老师信息 <a class="btn add-btn" href="/students/teacher">老师列表</a></h1>
      <form action="/students/teacher/edit/{{ teacher.id }}" method="post">
        <div class="item">
          <label>工号:</label><input type="text" name="tno" value="{{ teacher.tno }}">
        </div>
        <div class="item">
          <label>账号:</label><input type="text" name="name" value="{{ teacher.name }}">
        </div>
        <div class="item">
          <label>密码:</label><input type="password" name="password" placeholder="如需重设密码,请填写新密码。" title="如果需要重新设置密码,请填写新密码。">
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>
课程管理-添加课程功能

视图代码:

"""课程管理"""
@router.get("/course/add")
async def add_course(request: Request):
    """添加课程[表单显示]"""
    teacher_list = await models.Teacher.all()
    return templates.TemplateResponse("add_course.html", locals())


@router.post("/course/add")
async def add_course(request: Request, name: str=Form(), teacher_id: int=Form()):
    """添加课程[表单处理]"""
    """接收数据,验证数据"""
    # 课程名称不能重复
    course = await models.Course.filter(name=name).first()
    if course:
        errors = "当前课程已经重复,请重新填写!"
        url = "/students/course/add"  # 添加课程[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

    try:
        course = await models.Course.create(name=name, teacher_id=teacher_id)
        errors = "添加成功!"
        url = "/students/course"  # 课程列表的url地址
        return templates.TemplateResponse("success.html", locals())
    except Exception as e:
        errors = "未知的错误导致添加失败!"
        url = "/students/course/add"  # 添加课程[表单显示]的url地址
        return templates.TemplateResponse("errors.html", locals())

因为添加学生需要查询班级,同理添加课程需要查询老师,所以从模板add_student.html复制并改名为add_course.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>添加课程信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input, select{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>添加课程信息 <a class="btn add-btn" href="/students/course">课程列表</a></h1>
      <form action="/students/course/add" method="post">
        <div class="item">
          <label>课程名称:</label><input type="text" name="name">
        </div>
        <div class="item">
          <label>授课老师:</label>
          <select name="teacher_id">
            {% for teacher in teacher_list %}
            <option value="{{ teacher.id }}">{{ teacher.name }}</option>
            {% endfor %}
          </select>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>
课程管理-课程列表功能

视图代码:

@router.get('/course')
async def list_course(request: Request, pn: int=1, teacher_id: int = None):
    """课程列表"""
    import math
    # 每一页展示的数据量
    limit = 6
    query = models.Course.all()
    if teacher_id:
        query = models.Course.filter(teacher_id=teacher_id).all()

    # 获取总数据量
    total = await query.count()

    # 向上取整获取总页数
    max_pn = math.ceil(total/limit)
    # 判断当前页码不能超出总页码的范围
    if pn > max_pn:
        pn = max_pn

    if pn < 1:
        pn = 1

    start = (pn-1) * limit # 每一页数据的查询开始位置
    course_list = await query.limit(limit).offset(start)
    for course in course_list:
        course.teacher = await course.teacher
    return templates.TemplateResponse('list_course.html', locals())

从学生列表页模板list_student.html复制改名为list_course.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>课程信息列表</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 1200px;
      height: 640px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  .header-item, .list-item{
      height: 48px;
      line-height: 48px;
      cursor: pointer;
      display: flex;
      margin: 15px;
  }
  .list-item .keys, .pk, .fields{
      flex: 2;
      border-bottom: 1px solid rgba(0,0,0,.3);
      text-align: center;
  }
  .pk, .list-item .keys:first-child{
      flex: 1;
  }
  .btn{
      border: 1px solid rgba(0,0,0,.3);
      padding: 8px 16px;
  }
  .btn:hover{
      border-radius: 6px;
      background: rgba(0,0,0,0.6);
      color: #fff;
  }
  .pagination{
      text-align: center;
  }
  .pagination span,.pagination  a{
      text-decoration: none;
      font-size: 16px;
      color: #666;
      border: 1px solid rgba(0,0,0,0.3);
      padding: 5px 12px;
  }
  .pagination span:hover,.pagination  a:hover{
      border: 1px solid rgba(0,0,0,0.8);
      color: #333;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>课程信息列表
        <a class="btn add-btn" href="/students/course/add">新建课程</a>
      </h1>
      <!-- 表头 -->
      <div class="header-item">
        <div class="pk">ID编号</div>
        <div class="fields">课程名称</div>
        <div class="fields">授课老师</div>
        <div class="fields">操作</div>
      </div>
      {% for course in course_list %}
      <div class="list-item">
        <div class="keys">{{ course.id }}</div>
        <div class="keys">{{ course.name }}</div>
        <div class="keys">{{ course.teacher.name }}</div>
        <div class="keys">
          <a href="/students/stu/edit/{{ course.id }}"><span class="btn">编辑</span></a>
          <a href="/students/stu/del/{{ course.id }}"><span class="btn">删除</span></a>
        </div>
      </div>
      {% endfor %}
      <div class="pagination">
        <a href="?pn=1">首页</a>
        {% if pn > 1 %}
        <a href="?pn={{ pn - 1 }}">{{ pn - 1 }}</a>
        {% endif %}
        <span href="?pn={{ pn }}">{{ pn }}</span>
        {% if pn < max_pn %}
        <a href="?pn={{ pn + 1 }}">{{ pn + 1}}</a>
        {% endif %}
        <a href="?pn={{ max_pn }}">尾页</a>
      </div>
    </div>
  </div>
</body>
</html>
学生管理-我的课程功能

1、在当前学生列表中新增一个选课按钮,list_student.html,代码:

        <div class="keys">
          <a href="/students/stu/course/{{ student.id }}"><span class="btn">选课</span></a>
          <a href="/students/stu/edit/{{ student.id }}"><span class="btn">编辑</span></a>
          <a href="/students/stu/del/{{ student.id }}"><span class="btn">删除</span></a>
        </div>

新建模板文件,school/apps/students/templates/my_course.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>xiaobai的课程信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  .transfer{
      width: 100%;
      border: 1px solid rgba(0,0,0,.3);
      min-height: 240px;
      text-align: center;
      position: relative;
  }
  .transfer .all-box{
      float: left;
      width: 45%;
      min-height: 240px;
      border: 1px solid rgba(0,0,0,.3);
  }
  .transfer .selected-box{
      float: right;
      width: 45%;
      min-height: 240px;
      border: 1px solid rgba(0,0,0,.3);
  }
  .transfer .all-box ul,
  .transfer .selected-box ul{
      overflow: scroll;
      height: 180px;
  }
  .transfer ul{
      list-style: none;
      padding: 0;
      margin: 0;
      font-size: 14px;
      color: #555;
  }
  .transfer ul li{
      height: 24px;
      line-height: 28px;
      text-align: left;
      text-indent: 100px;
  }
  .transfer .btn-list{
      position: absolute;
      margin: auto;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      height: 84px;
      width: 60px;
  }
  .transfer .btn-list div{
      background: cornflowerblue;
      height: 40px;
      color: #ffffff;
      line-height: 40px;
      font-size: 24px;
      cursor: pointer;
      width: 60px;
      border-radius: 8px;
      border-bottom: 4px solid #ffffff;
      border-top: 4px solid #ffffff;

  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>xiaobai的课程信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
      <form action="/students/stu/course" method="post">
        <div class="transfer">
          <div class="all-box">
            <p>未选课程</p>
            <ul>
              <li><label><input type="checkbox" name="" id=""> Python基础</label></li>
              <li><label><input type="checkbox" name="" id=""> Python入门</label></li>
              <li><label><input type="checkbox" name="" id=""> Python进阶</label></li>
              <li><label><input type="checkbox" name="" id=""> Python深入</label></li>
              <li><label><input type="checkbox" name="" id=""> Python实战</label></li>
              <li><label><input type="checkbox" name="" id=""> Python实战</label></li>
              <li><label><input type="checkbox" name="" id=""> Python实战</label></li>
              <li><label><input type="checkbox" name="" id=""> Python实战</label></li>
            </ul>
          </div>
          <div class="btn-list">
            <div>&gt;</div>
            <div>&lt;</div>
          </div>
          <div class="selected-box">
            <p>已选课程</p>
            <ul>
              <li><label><input type="checkbox" name="" id=""> fastAPI基础</label></li>
              <li><label><input type="checkbox" name="" id=""> fastAPI入门</label></li>
            </ul>
          </div>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
</body>
</html>

FastAPI服务端提供视图接口,加载上面的模板文件,展示我的课程界面,students/views.py,代码:

@router.get('/stu/course/{id}')
async def my_course(request: Request, id: int):
    # 只查询当前模型的数据,并不会帮我们把外键数据查询出来
    # student = await models.Student.filter(id=id).first()
    # 查询当前模型数据的同时,顺便把外键字段courses的数据也查询出来
    student = await models.Student.filter(id=id).first().prefetch_related('class_', 'courses')
    if not student:
        errors = "当前学生不存在,请重新确认!"
        url = "/students/stu"  # 课程列表的url地址
        return templates.TemplateResponse("errors.html", locals())
    # 当前学生已选课程
    my_coruses_id = [course.id for course in student.courses]
    # 查询所有未选课程
    course_list = await models.Course.filter(id__not_in=my_coruses_id).all()
    return templates.TemplateResponse('my_course.html', locals())

展示课程数据到模板中,my_course.html,代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ student.name }}的课程信息</title>
  <style>
  body{
      margin: 0;
      padding: 0;
  }
  .bg{
      width: 100vw;
      height: 100vh;
      background: url('/static/images/bg.jpg') no-repeat;
      background-size: 100%;
      position: relative;
  }
  .form-box{
      background: rgba(255,255,255,0.8);
      width: 800px;
      height: 440px;
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      margin: auto;
      border-radius: 10px;
      box-shadow: 6px 6px 6px rgba(0,0,0,.1);
  }
  h1{
      font-size: 32px;
      font-family: "Monotype Corsiva", "Apple Chancery", "URW Chancery L", cursive;
      font-weight: normal;
      text-align: center;
      line-height: 32px;
      height: 54px;
      border-bottom: 1px solid rgba(0,0,0,.7);
      margin: 15px 15px 0 ;
      color: #666;
  }
  form{
      width: 700px;
      margin: 20px auto 20px;
  }
  form .item{
      height: 50px;
      line-height: 50px;
      border-bottom: 1px solid rgba(0,0,0,.1);
      margin-bottom: 10px;
  }
  form .item input{
      outline: none;
      height: 24px;
      line-height: 24px;
      font-size: 16px;
      text-indent: 4px;
      background: transparent;
      border: 1px solid rgba(0,0,0,.3);
  }
  .submit{
      position: absolute;
      bottom: 20px;
      left: 0;
      right: 0;
      margin: auto;
      width: 120px;
      height: 46px;
      border-radius: 8px;
      border: 1px solid rgba(0,0,0,.3);
      cursor: pointer;
      outline: none;
  }
  .add-btn{
      position: absolute;
      right: 15px;
      text-decoration: none;
      color: #333333;
      font-size: 16px;
  }
  .transfer{
      width: 100%;
      border: 1px solid rgba(0,0,0,.3);
      min-height: 240px;
      text-align: center;
      position: relative;
  }
  .transfer .all-box{
      float: left;
      width: 45%;
      min-height: 240px;
      border: 1px solid rgba(0,0,0,.3);
  }
  .transfer .selected-box{
      float: right;
      width: 45%;
      min-height: 240px;
      border: 1px solid rgba(0,0,0,.3);
  }
  .transfer .all-box ul,
  .transfer .selected-box ul{
      overflow: scroll;
      height: 180px;
  }
  .transfer ul{
      list-style: none;
      padding: 0;
      margin: 0;
      font-size: 14px;
      color: #555;
  }
  .transfer ul li{
      height: 24px;
      line-height: 28px;
      text-align: left;
      text-indent: 100px;
  }
  .transfer .btn-list{
      position: absolute;
      margin: auto;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      height: 84px;
      width: 60px;
  }
  .transfer .btn-list div{
      background: cornflowerblue;
      height: 40px;
      color: #ffffff;
      line-height: 40px;
      font-size: 24px;
      cursor: pointer;
      width: 60px;
      border-radius: 8px;
      border-bottom: 4px solid #ffffff;
      border-top: 4px solid #ffffff;

  }
  </style>
</head>
<body>
  <div class="bg">
    <div class="form-box">
      <h1>{{ student.name }}的课程信息 <a class="btn add-btn" href="/students/stu">学生列表</a></h1>
      <form action="/students/stu/course/{{ student.id }}" method="post">
        {# 设置一个用户看不见的隐藏框,里面记录所有已选课程  #}
        <input type="hidden" name="courses" value="{{ my_coruses_id }}">
        <div class="transfer">
          <div class="all-box">
            <p>未选课程</p>
            <ul>
              {% for course in course_list %}
              <li><label><input type="checkbox" name="" value="{{ course.id }}" > {{ course.name }}</label></li>
              {% endfor %}
            </ul>
          </div>
          <div class="btn-list">
            <div class="right">&gt;</div>
            <div class="left">&lt;</div>
          </div>
          <div class="selected-box">
            <p>已选课程</p>
            <ul>
              {% for course in student.courses %}
              <li><label><input type="checkbox" name="" value="{{ course.id }}" > {{ course.name }}</label></li>
              {% endfor %}
            </ul>
          </div>
        </div>
        <button class="submit">提交</button>
      </form>
    </div>
  </div>
  <script>
    var all_box = document.querySelector('.all-box ul');
    var selected_box = document.querySelector('.selected-box ul');
    let right = document.querySelector('.btn-list .right');
    let left = document.querySelector('.btn-list .left');
    let course_input = document.querySelector('input[name=courses]');
    let submit = document.querySelector('.submit');

    // 选课
    right.onclick = ()=>{
      let selected_input = all_box.querySelectorAll('li input:checked');
      for(let item of selected_input){
        // 循环每一个input,获取该input框的父级元素label的父级元素li,并把li追加到已选课程的ul下面作为子元素
        selected_box.append(item.parentElement.parentElement);
        // 取消勾选
        item.checked =false;
      }
    }
    // 消课
    left.onclick = ()=>{
      let selected_input = selected_box.querySelectorAll('li input:checked');
      for(let item of selected_input){
        // 循环每一个input,获取该input框的父级元素label的父级元素li,并把li追加到未选课程的ul下面作为子元素
        all_box.append(item.parentElement.parentElement);
        // 取消勾选
        item.checked = false;
      }
    }

    // 点击提交表单
    submit.onclick = ()=>{
      let all_input = selected_box.querySelectorAll('li input');
      let course_id_list = []
      for(let item of all_input){
        course_id_list.push(parseInt(item.value));
      }
      console.log(course_id_list);
      course_input.value = `[${course_id_list}]`;
      // 在提交表单事件中,可以使用return false阻止提交按钮触发表单提交事件,
      // return false;
    }
  </script>
</body>
</html>

FastAPI服务端接受表单隐藏数据,把学生选的课程添加到数据库中。

@router.post("/stu/course/{id}")
async def my_course(request: Request, id: int, courses: str = Form()):
    """我的课程[数据处理]"""
    student = await models.Student.filter(id=id).first().prefetch_related('class_', 'courses')
    if not student:
        errors = "当前学生不存在,请重新确认!"
        url = "/students/stu"  # 课程列表的url地址
        return templates.TemplateResponse("errors.html", locals())
    # 分割表单提交上来的已选课程的ID列表
    # course_id_list = eval(courses)
    course_id_list = []
    if courses[1:-1]:
        course_id_list = [int(id) for id in courses[1:-1].split(",")]

    try:
        # 把表单提交的已选课程的ID保存到课程与学生的关系表中
        await student.courses.clear() # 清空当前学生的所有选课
        # 把本次学生的选课全部查询出来,并与当前学生进行关系绑定
        course_list = await models.Course.filter(id__in=course_id_list).all()
        for course in course_list:
            await student.courses.add(course)
        errors = "选课成功!"
        url = f"/students/stu/course/{student.id}"  # 我的课程的url地址
        return templates.TemplateResponse("success.html", locals())
    except Exception as e:
        errors = f"未知的错误导致选课失败!{e}"
        url = f"/students/course/{student.id}"  # 我的课程的url地址
        return templates.TemplateResponse("errors.html", locals())

6.2.2 SQLAlchemy

async_sqlalchemy.py,使用ORM连接mysql,并初始化ORM,代码:

from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Integer
# 创建异步引擎
database_url = "mysql+aiomysql://root:123@127.0.0.1:3306/school?charset=utf8mb4"
engine = create_async_engine(database_url, future=True, echo=True)

# 创建异步 SessionLocal
async_session = async_sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False, autocommit=False
)

class BaseModel(AsyncAttrs, DeclarativeBase):
    """异步模型基类"""
    pass

main.py,代码:

import uvicorn
from fastapi import FastAPI, Body, status
from async_sqlalchemy import async_engine, BaseModel,  mapped_column, Mapped, async_session, String, select, update, delete, insert
from pydantic import BaseModel as PyModel


class Class(BaseModel):
    """班级信息"""
    __tablename__ = "my_class"
    __table_args__ = {
        "extend_existing": True,
    }
    id: Mapped[int] = mapped_column(primary_key=True, comment='主键')
    name: Mapped[str] = mapped_column(String(50), unique=True, comment='分类名称')

    def __repr__(self) -> str:
        """打印模型对象时输入文本提示"""
        return f"Class(id={self.id}, name={self.name})"


async def init_db():
    """项目启动时提前建表"""
    async with async_engine.begin() as conn:
        # 删除当前程序中所有的模型对应的数据表
        # await conn.run_sync(BaseModel.metadata.drop_all)

        # 创建当前程序中所有的模型的数据表,如果数据表不存在的情况下
        await conn.run_sync(BaseModel.metadata.create_all)

app = FastAPI(
    title="学生选课系统",
    summary="学生选课系统API文档",
    description="学生选课系统API文档的详细说明内容",
    version="v1.0.0",
    on_startup=[init_db], # 全局事件,当项目启动时,会自动执行的函数列表
    on_shutdown=[], # 全局事件,当项目停止时,会自动执行的函数列表
)


@app.get("/")
async def main():
    return {}



class ClassIn(PyModel):
    name: str



@app.post("/class")
async def add_class(class_in: ClassIn):
    """添加班级"""
    name = class_in.name
    async with async_session() as session:
        # # 方式1:使用add添加数据
        # class_object = Class(name=name)
        # session.add(class_object)
        # await session.commit()
        # return class_object

        # 方式2:使用insert添加数据
        query = insert(Class).values(name=name)
        await session.execute(query)
        await session.commit()
        return {}


@app.get("/class/{id}")
async def get_class(id: int):
    """获取一条数据"""
    async with async_session() as session:
        # # 获取数据
        # query = select(Class).filter(Class.id == id)
        # queryset = await session.scalars(query)
        # return queryset.first()

        # 获取数据
        class_obj = await session.get(Class, id)
        return class_obj

@app.get("/class")
async def list_class():
    """获取多条数据"""
    async with async_session() as session:
        query = select(Class)
        queryset = await session.scalars(query)
        return queryset.all()


@app.put("/class/{id}")
async def edit_class(id: int, class_in: ClassIn):
    """编辑班级"""
    name = class_in.name
    async with async_session() as session:
        # # 方式1:先查询当前要编辑的模型,修改模型的字段值,提交事务
        # query = select(Class).where(Class.id == id)
        # queryset = await session.scalars(query)
        # class_object = queryset.first()
        # # 进行字段值替换
        # class_object.name = name
        # # 提交事务
        # await session.commit()
        # # 把修改后的模型返回给客户端
        # return class_object

        # 方式2:使用update方法来实现
        data = {
            "name": name
        }
        query = update(Class).where(Class.id == id).values(**data)
        await session.execute(query)
        await session.commit()
        return {
            'id': id,
            'name': name,
        }


@app.delete("/class/{id}", status_code=status.HTTP_204_NO_CONTENT)
async def del_class(id: int):
    """删除班级"""
    async with async_session() as session:
        # # 方式1:线查询模型,使用模型来删除
        # class_object = await session.get(Class, id)
        # if class_object:
        #     await session.delete(class_object)
        #     await session.commit()

        # 方式2: 直接使用delete对模型按条件删除
        query = delete(Class).where(Class.id == id)
        await session.execute(query)
        await session.commit()

if __name__ == '__main__':
    uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2183103.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【AGC005D】~K Perm Counting(计数抽象成图)

容斥原理。 求出f(m) &#xff0c;f(m)指代至少有m个位置不合法的方案数。 怎么求&#xff1f; 注意到位置为id&#xff0c;权值为v ,不合法的情况&#xff0c;当且仅当 v idk或 v id-k 因此&#xff0c;我们把每一个位置和权值抽象成点 &#xff0c;不合法的情况之间连一…

【JVM】基础篇

1 初识JVM 1.1 什么是JVM JVM 全称是 Java Virtual Machine&#xff0c;中文译名 Java虚拟机。JVM 本质上是一个运行在计算机上的程序&#xff0c;他的职责是运行Java字节码文件。 Java源代码执行流程如下&#xff1a; 分为三个步骤&#xff1a; 1、编写Java源代码文件。 …

自动驾驶系列—深度剖析自动驾驶芯片SoC架构:选型指南与应用实战

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

认知杂谈74《远离渣女陷阱,拥抱健康情感》

内容摘要&#xff1a; 渣女在感情中使用甜言蜜语陷阱&#xff0c;利用男性渴望理解和关爱的心理&#xff0c;通过虚假承诺和情感操控来获得利益。 男性易陷入这种陷阱&#xff0c;因为他们可能因压力大、感性而易受感动。为了避免这种情况&#xff0c;男性需要辨别言行一致性&a…

【含文档】基于Springboot+Vue的国风彩妆网站(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

软件设计之SSM(4)

软件设计之SSM(4) 路线图推荐&#xff1a; 【Java学习路线-极速版】【Java架构师技术图谱】 尚硅谷新版SSM框架全套视频教程&#xff0c;Spring6SpringBoot3最新SSM企业级开发 资料可以去尚硅谷官网免费领取 学习内容&#xff1a; 基于配置类方式管理Bean 完全注解开发第三…

共模电感工作原理:【图文讲解】

共模电感&#xff0c;相信做电源较多的朋友用的比较多&#xff0c;而做消费级产品的朋友或许用的不是那么的多。但是还是有必要了解了解。 先上图&#xff0c;看看它长什么样子&#xff1a; &#xff08;实物图&#xff09; &#xff08;结构图&#xff09; 很显然&#xff0…

【Ubuntu】安装常用软件包-mysql

我的几个服务是部署在docker的同一个网络里&#xff0c;这样相互访问就可以通过docker容器的名字访问&#xff0c;比如容器A访问容器B&#xff0c;就可以http://B:8080/xxx 这样访问&#xff0c;不用关心ip是多少。 所以mysql前面文章给安装到主机里&#xff0c;感觉有点坑自己…

02.usePrevious

在 React 开发中&#xff0c;有时我们需要访问组件的前一个状态或属性。这在进行比较、动画或其他需要历史数据的操作时特别有用。usePrevious 钩子提供了一种简单而有效的方式来存储和访问前一个值。以下是如何实现和使用这个自定义钩子&#xff1a; const usePrevious valu…

【数据类型】C和C++的区别

文章目录 一、字符串二、布尔类型 bool三、数据的输入和输出 C和C在数据类型上打区别不大&#xff0c;下面就二者在这方面的部分区别做比较。 一、字符串 C语言和C在字符串的定义和书写风格上略有差异。 C风格字符串&#xff1a; char str[]"hello";C风格字符串 st…

社交内容电商中的新机遇:2+1链动模式AI智能名片商城小程序

在当今的电商世界里&#xff0c;社交内容电商正蓬勃发展。这种模式基于高质量内容&#xff0c;将有着共同兴趣爱好的用户聚集起来形成社群&#xff0c;随后引导用户进行裂变式的传播与交易。无论是像微信、微博、快手、抖音、今日头条这样的平台形式&#xff0c;还是网红、“大…

算法笔记(四)——模拟

文章目录 替换所有的问号提莫攻击Z字形变换外观数列数青蛙 模拟算法就是根据题目的要求&#xff0c;题目让干神马就做神马&#xff0c;一步一步来 替换所有的问号 题目&#xff1a;替换所有的问号 思路 从左到右遍历整个字符串&#xff0c;找到问号之后&#xff0c;就⽤ a ~ z…

QT系统学习篇(2)- Qt跨平台GUI原理机制

一、Qt工程管理 新建项目&#xff1a; 我们程序员新建项目对话框所有5类项目模板 Application: Qt的应用程序&#xff0c;包含Qt Quick和普通窗口程序。 Library: 它可以创建动态库、静态库、Qt Creator自身插件、Qt Quick扩展插件。 其他项目: 创建单元测试项目、子目录项目…

自动驾驶系列—自动驾驶MCU架构全方位解析:从单核到多核的选型指南与应用实例

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

五子棋双人对战项目(3)——匹配模块

一、分析需求 二、约定前后端接口 三、实现游戏大厅页面&#xff08;前端代码&#xff09; 四、实现后端代码 五、线程安全问题 六、忙等问题 一、分析需求 需求&#xff1a;多个玩家&#xff0c;在游戏大厅进行匹配&#xff0c;系统会把实力相近的玩家匹配到一起。 要想实…

使用cmake配置pcl环境

项目文件在https://pan.quark.cn/s/d347f72c7432 文件中包含CMakeLists.txt&#xff0c;一个pcd文件&#xff0c;一个cpp源文件。 这里的话&#xff0c;首先你需要下载好cmake软件&#xff0c;并将其添加到环境变量。 CMakeLists.txt文件内容如下 cmake_minimum_required(VER…

「漏洞复现」EDU 某智慧平台 PersonalDayInOutSchoolData SQL注入漏洞

0x01 免责声明 请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;作者不为此承担任何责任。工具来自网络&#xff0c;安全性自测&#xff0c;如有侵权请联系删…

鸿蒙媒体开发系列16——图像变换与位图操作

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、概述 图片处理指对PixelMap进行相关的操作&#xff0c;如获取图片信息、裁剪、缩…

鸿蒙媒体开发系列17——图片编码与EXIF处理

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 1、图片编码 图片编码指将PixelMap编码成不同格式的存档图片&#xff08;当前仅支持…

完整网络模型训练(一)

文章目录 一、网络模型的搭建二、网络模型正确性检验三、创建网络函数 一、网络模型的搭建 以CIFAR10数据集作为训练例子 准备数据集&#xff1a; #因为CIFAR10是属于PRL的数据集&#xff0c;所以需要转化成tensor数据集 train_data torchvision.datasets.CIFAR10(root&quo…