一、简介
使用Flask实现了一个简单的电影信息管理系统,用户可以通过网页界面进行电影信息的添加、编辑和删除操作。对应官网教程的第七章。
二、文件组成
在文章顶部下载相关资源
三、代码运行
先按照官网上的教程将环境配置好
在watchlist文件夹下运行Git Bash
输入 . env/Scripts/activate 进入虚拟环境
(注意在Git Bash中,Shift+Insert 是粘贴 Ctrl + C 是退出)
输入 flask run 启动程序
在浏览器中访问 localhost:5000 进入界面
测试一下加入新的数据
可以发现加入到了里面
四、代码解读
3.1 app.py 代码解读
首先引入一些相关的库
# -*- coding: utf-8 -*- 声明了该文件的编码方式为 UTF-8
import os # 提供了与操作系统进行交互的功能,如文件和目录操作
import sys # 提供了与 Python 解释器相关的功能,如访问命令行参数和解释器状态
import click # 一个用于创建命令行接口的 Python 包
from flask import Flask
from flask import render_template # 用于渲染 HTML 模板
from flask import request # 用于访问请求数据(如表单数据)
from flask import url_for # 用于生成 URL
from flask import redirect # 用于重定向用户到指定的 URL
from flask import flash # 用于在请求之间传递一次性消息
from flask_sqlalchemy import SQLAlchemy # 使得我们可以使用 Python 类来表示数据库表,并使用 SQLAlchemy 提供的高级 API 来操作数据库
SQLite 数据库 URI 的前缀在不同操作系统上有所不同,为了确保 SQLAlchemy 能够正确地连接到指定路径的 SQLite 数据库,需要进行设置
# # sys.platform 返回一个描述操作系统的字符串 例如 'win32' 或 'win64'
WIN = sys.platform.startswith('win')
if WIN:
prefix = 'sqlite:///' # 操作系统是 Windows,则 prefix 被设置为 'sqlite:///'
else:
prefix = 'sqlite:' # 操作系统不是 Windows,则 prefix 被设置为 'sqlite:'
创建并配置一个 Flask 应用,并初始化了SQLAlchemy,用于与 SQLite 数据库进行交互
app = Flask(__name__) # 创建一个 Flask 应用实例
app.config['SECRET_KEY'] = 'dev' # 设置应用的密钥,这里设置为 'dev',表示这是一个开发环境的密钥,实际生产环境中应该设置为一个更复杂的随机值
app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db') # 设置 SQLAlchemy 的数据库 URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁用 SQLAlchemy 的事件系统,该系统会跟踪对象的修改。默认情况下,这个功能是启用的,但是它会消耗额外的内存,不需要时可以禁用以提高性能
db = SQLAlchemy(app) # 初始化 SQLAlchemy
定义一个initdb
命令行命令用于初始化数据库,drop可以实现先删除再创建
仅创建数据库: flask initdb
删除现有表并重新创建: flask initdb --drop
@app.cli.command() # 是一个装饰器,用于将一个函数注册为 Flask 应用的命令行命令。在命令行中使用 flask initdb 命令可以调用这个函数
@click.option('--drop', is_flag=True, help='Create after drop.')
def initdb(drop): # drop 参数的值由 --drop 选项决定
if drop:
db.drop_all() # 如果 drop 为 True,则调用 db.drop_all() 删除所有数据库表
db.create_all() # 调用 db.create_all() 创建所有数据库表
click.echo('Initialized database.') # 表示数据库初始化完成
定义两个 SQLAlchemy 模型类 User
和 Movie
,分别对应数据库中的两个表。每个模型类都继承自 db.Model
,并定义了相应的字段(列)
# 下面的forge会用到
class User(db.Model):
id = db.Column(db.Integer, primary_key=True) # 整数类型,主键
name = db.Column(db.String(20)) # 字符串类型的字段,最大长度为20个字符
class Movie(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(60))
year = db.Column(db.String(4))
定义一个 forge
命令行命令,用于生成假数据并插入到数据库中
在命令行中运行该命令以生成假数据并插入数据库:flask forge
@app.cli.command()
def forge():
db.create_all() # 创建数据库,如果已存在则不会重新创建
name = 'Grey Li'
movies = [
{'title': 'My Neighbor Totoro', 'year': '1988'},
{'title': 'Dead Poets Society', 'year': '1989'},
{'title': 'A Perfect World', 'year': '1993'},
{'title': 'Leon', 'year': '1994'},
{'title': 'Mahjong', 'year': '1996'},
{'title': 'Swallowtail Butterfly', 'year': '1996'},
{'title': 'King of Comedy', 'year': '1999'},
{'title': 'Devils on the Doorstep', 'year': '1999'},
{'title': 'WALL-E', 'year': '2008'},
{'title': 'The Pork of Music', 'year': '2012'},
] # 包含多个字典的列表 movies,每个字典表示一部电影,包含 title 和 year 两个键
user = User(name=name) # 创建一个 User 实例 user,其 name 属性为 'Grey Li'
db.session.add(user) # 将该 user 实例添加到数据库会话中
for m in movies:
movie = Movie(title=m['title'], year=m['year']) # 遍历 movies 列表,为每部电影创建一个 Movie 实例
db.session.add(movie) # 将每个 Movie 实例添加到数据库会话中
db.session.commit() # 提交数据库会话,将所有添加的记录写入数据库
click.echo('Done.') # 使用 click.echo 在命令行中输出 'Done.',表示数据生成和插入完成
注册一个上下文处理器
@app.context_processor # 注册一个上下文处理器函数
# 在模板 index.html 中,可以直接使用 {{ user.name }} 来访问用户的名字
def inject_user():
user = User.query.first() # 查询 User 表中的第一条记录,通常用于获取数据库中的第一个用户
return dict(user=user) # 如果用户表中没有记录,则 user 变量将为 None
定义一个错误处理函数 page_not_found
,用于处理 HTTP 404 错误。
主要是为了美化404界面
@app.errorhandler(404) # 是 Flask 提供的装饰器,用于注册一个处理 HTTP 404 错误的函数。美化404界面
def page_not_found(e): # 定义了一个名为 page_not_found 的函数,用于处理 404 错误
return render_template('404.html'), 404 # 当用户访问的页面不存在时,会返回一个自定义的 404 页面
实现对根目录的 GET 和 POST 请求的处理。当用户访问网站首页时,会显示所有电影的列表;当用户提交电影信息时,会将新电影添加到数据库,并显示成功消息。
@app.route('/', methods=['GET', 'POST']) # Flask 提供的路由装饰器,用于将函数绑定到指定的 URL 路径上,指定了允许的 HTTP 请求方法,即 GET 和 POST
def index(): # 定义了一个名为 index 的函数,用于处理对根目录的请求
if request.method == 'POST': # 当请求方法为 POST 时,从请求的表单中获取电影的标题和年份
title = request.form['title']
year = request.form['year']
if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.') # 不合法输入,显示一个闪现消息,并重定向到首页
return redirect(url_for('index')) # 重定向到首页
movie = Movie(title=title, year=year)
db.session.add(movie)
db.session.commit()
flash('Item created.') # 输入合法,将电影对象添加到数据库后,显示一个成功的闪现消息
return redirect(url_for('index'))
movies = Movie.query.all() # 当请求方法为 GET 时,查询所有的电影记录,并将它们传递给模板
return render_template('index.html', movies=movies) # 渲染名为 index.html 的模板,并将查询到的电影列表传递给模板
实现对电影编辑页面的 GET 和 POST 请求的处理。当用户访问电影编辑页面时,会显示当前电影的信息;当用户提交编辑后的信息时,会更新数据库中对应的电影信息,并显示成功消息。
@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST']) # 指定了一个动态 URL 变量 movie_id,它是一个整数类型
def edit(movie_id):
movie = Movie.query.get_or_404(movie_id) # 如果找不到对应的电影对象,则会返回 404 错误页面
# 与上面的命令行命令类似
if request.method == 'POST':
title = request.form['title']
year = request.form['year']
if not title or not year or len(year) > 4 or len(title) > 60:
flash('Invalid input.')
return redirect(url_for('edit', movie_id=movie_id))
movie.title = title
movie.year = year
db.session.commit()
flash('Item updated.')
return redirect(url_for('index'))
return render_template('edit.html', movie=movie)
实现对电影删除页面的 POST 请求的处理。当用户在电影删除页面上确认删除时,会从数据库中删除对应的电影信息,并显示成功消息。
@app.route('/movie/delete/<int:movie_id>', methods=['POST'])
def delete(movie_id):
movie = Movie.query.get_or_404(movie_id)
db.session.delete(movie)
db.session.commit()
flash('Item deleted.')
return redirect(url_for('index'))
3.2 父模板 base.html 代码解读
<!DOCTYPE html> <!-- 声明文档类型为html5 -->
<html lang="en"> <!-- 定义了文档的语言为英语-->
<head>
<!-- 这是一个 Jinja2 块,用于定义模板中的头部内容。在子模板中,可以通过重写这个块来自定义头部内容 -->
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 假设在 Python 代码中已经定义了一个 user 对象,其具有一个名为 name 的属性,那么在模板中使用 {{ user.name }} 就会将用户的名字动态地显示在页面标题中 -->
<title>{{ user.name }}'s Watchlist</title>
<!-- 生成一个指向静态文件夹static中的 favicon.ico 文件的 URL 地址 -->
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" type="text/css">
{% endblock %}
</head>
<body>
<!-- Jinja2 模板中的循环结构,用于遍历闪现消息列表,并将每条消息以 <div class="alert"> 的形式显示在页面上 -->
{% for message in get_flashed_messages() %}
<div class="alert">{{ message }}</div>
{% endfor %}
<h2>
<img alt="Avatar" class="avatar" src="{{ url_for('static', filename='images/avatar.png') }}">
{{ user.name }}'s Watchlist
</h2>
<nav>
<ul>
<!-- 指向首页链接 -->
<li><a href="{{ url_for('index') }}">Home</a></li>
</ul>
</nav>
<!-- 允许在模板的子类中重写 content 块,以提供特定页面的自定义内容 -->
{% block content %}{% endblock %}
<!-- 页面的页脚部分,包含了版权信息和一个链接到 HelloFlask 网站的小标签 -->
<footer>
<small>© 2018 <a href="http://helloflask.com/tutorial">HelloFlask</a></small>
</footer>
</body>
</html>
3.3 子模板 index.html 代码解读
<!-- 指示模板继承自名为 base.html 的父模板 -->
{% extends 'base.html' %}
<!-- 块覆盖了父模板中同名的块(在父模板中是空白),因此在渲染页面时,将会显示子模板中定义的内容 -->
{% block content %}
<!-- 使用了模板语法,表示对电影列表 movies 应用了过滤器 length,以获取列表的长度 -->
<p>{{ movies|length }} Titles</p>
<!-- 定义了一个表单,使用 POST 方法提交数据 -->
<form method="post">
<!-- autocomplete 表示禁止浏览器自动填充 required 表示不能为空 -->
Name <input type="text" name="title" autocomplete="off" required>
Year <input type="text" name="year" autocomplete="off" required>
<input class="btn" type="submit" name="submit" value="Add">
</form>
<ul class="movie-list">
{% for movie in movies %}
<li>{{ movie.title }} - {{ movie.year }}
<span class="float-right">
<!-- 编辑按钮,链接到编辑页面。使用了 url_for 函数生成编辑页面的 URL 地址 -->
<a class="btn" href="{{ url_for('edit', movie_id=movie.id) }}">Edit</a>
<!-- 删除表单,用于提交删除电影的请求。使用了 url_for 函数生成删除电影的 URL 地址。该表单使用了 POST 方法提交数据 -->
<form class="inline-form" method="post" action="{{ url_for('.delete', movie_id=movie.id) }}">
<input class="btn" type="submit" name="delete" value="Delete" onclick="return confirm('Are you sure?')">
</form>
<!-- IMDb 链接,点击链接将在新标签页中打开 IMDb 并搜索该电影的标题 -->
<a class="imdb" href="https://www.imdb.com/find?q={{ movie.title }}" target="_blank" title="Find this movie on IMDb">IMDb</a>
</span>
</li>
{% endfor %}
</ul>
<!-- alt="Walking Totoro":指定了图片的替代文本,即在图片无法显示时显示的文本 -->
<img alt="Walking Totoro" class="totoro" src="{{ url_for('static', filename='images/totoro.gif') }}" title="to~to~ro~">
{% endblock %} <!-- 用于结束 content 块的定义 -->
3.4 edit.html 代码解读
与index差不多,很容易理解
{% extends 'base.html' %}
{% block content %}
<h3>Edit item</h3>
<form method="post">
Name <input type="text" name="title" autocomplete="off" required value="{{ movie.title }}">
Year <input type="text" name="year" autocomplete="off" required value="{{ movie.year }}">
<input class="btn" type="submit" name="submit" value="Update">
</form>
{% endblock %}
3.5 404.html 代码解读
{% extends 'base.html' %}
{% block content %}
<ul class="movie-list">
<li>
Page Not Found - 404
<span class="float-right">
<a href="{{ url_for('index') }}">Go Back</a>
</span>
</li>
</ul>
{% endblock %}