From 000d01aa1855b796374e6040540866f817f5e65e Mon Sep 17 00:00:00 2001 From: Grey Li Date: Tue, 25 Jun 2024 11:04:04 +0800 Subject: [PATCH] Add models for chapter 5 --- examples/README.md | 17 +- examples/{form => ch1}/.flaskenv | 0 examples/{hello => ch1}/app.py | 0 examples/{hello => ch2}/.flaskenv | 0 examples/{http => ch2}/app.py | 0 examples/{http => ch3}/.flaskenv | 0 examples/{template => ch3}/app.py | 0 examples/{template => ch3}/static/avatar.png | Bin .../static/css/bootstrap.min.css | 0 examples/{form => ch3}/static/favicon.ico | Bin .../static/js/bootstrap.bundle.min.js | 0 examples/{template => ch3}/static/styles.css | 0 .../{template => ch3}/templates/base.html | 0 .../templates/errors/404.html | 0 .../templates/errors/500.html | 0 .../{template => ch3}/templates/index.html | 0 .../{template => ch3}/templates/macros.html | 0 .../templates/watchlist.html | 0 .../templates/watchlist_with_static.html | 0 examples/{template => ch4}/.flaskenv | 0 examples/{form => ch4}/app.py | 0 examples/{form => ch4}/forms.py | 0 .../static/css/bootstrap.min.css | 0 examples/{template => ch4}/static/favicon.ico | Bin .../static/js/bootstrap.bundle.min.js | 0 examples/{form => ch4}/static/style.css | 0 examples/{form => ch4}/templates/base.html | 0 examples/{form => ch4}/templates/basic.html | 0 .../{form => ch4}/templates/bootstrap.html | 0 .../templates/custom_validator.html | 0 examples/{form => ch4}/templates/index.html | 0 examples/{form => ch4}/templates/macros.html | 0 .../{form => ch4}/templates/pure_html.html | 0 examples/ch5/.flaskenv | 1 + examples/ch5/app.py | 251 ++++++++++++++++++ 35 files changed, 265 insertions(+), 4 deletions(-) rename examples/{form => ch1}/.flaskenv (100%) rename examples/{hello => ch1}/app.py (100%) rename examples/{hello => ch2}/.flaskenv (100%) rename examples/{http => ch2}/app.py (100%) rename examples/{http => ch3}/.flaskenv (100%) rename examples/{template => ch3}/app.py (100%) rename examples/{template => ch3}/static/avatar.png (100%) rename examples/{form => ch3}/static/css/bootstrap.min.css (100%) rename examples/{form => ch3}/static/favicon.ico (100%) rename examples/{form => ch3}/static/js/bootstrap.bundle.min.js (100%) rename examples/{template => ch3}/static/styles.css (100%) rename examples/{template => ch3}/templates/base.html (100%) rename examples/{template => ch3}/templates/errors/404.html (100%) rename examples/{template => ch3}/templates/errors/500.html (100%) rename examples/{template => ch3}/templates/index.html (100%) rename examples/{template => ch3}/templates/macros.html (100%) rename examples/{template => ch3}/templates/watchlist.html (100%) rename examples/{template => ch3}/templates/watchlist_with_static.html (100%) rename examples/{template => ch4}/.flaskenv (100%) rename examples/{form => ch4}/app.py (100%) rename examples/{form => ch4}/forms.py (100%) rename examples/{template => ch4}/static/css/bootstrap.min.css (100%) rename examples/{template => ch4}/static/favicon.ico (100%) rename examples/{template => ch4}/static/js/bootstrap.bundle.min.js (100%) rename examples/{form => ch4}/static/style.css (100%) rename examples/{form => ch4}/templates/base.html (100%) rename examples/{form => ch4}/templates/basic.html (100%) rename examples/{form => ch4}/templates/bootstrap.html (100%) rename examples/{form => ch4}/templates/custom_validator.html (100%) rename examples/{form => ch4}/templates/index.html (100%) rename examples/{form => ch4}/templates/macros.html (100%) rename examples/{form => ch4}/templates/pure_html.html (100%) create mode 100644 examples/ch5/.flaskenv create mode 100644 examples/ch5/app.py diff --git a/examples/README.md b/examples/README.md index 485bcb82..c67abf53 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,10 +2,10 @@ 在运行示例程序之前,确保你已经把仓库克隆到本地,并完成创建虚拟环境,安装依赖等操作,详见[获取示例程序](https://docs.helloflask.com/installation)。 -每一章的示例程序放在 examples 目录下不同的子文件内,以第 1 章示例程序为例,你需要把工作目录切换到 examples/c1 目录内,然后执行 `flask run` 启动程序: +每一章的示例程序放在 examples 目录下不同的子文件内,以第 1 章示例程序为例,你需要把工作目录切换到 examples/ch1 目录内,然后执行 `flask run` 启动程序: ``` -cd examples/c1 +cd examples/ch1 flask run ``` @@ -20,8 +20,17 @@ pdm run flask run 通过切换到不同的示例程序目录来运行不同章节的示例程序。比如,下面的命令将会运行第 4 章的示例程序: ``` -cd examples/c4 +cd examples/ch4 flask run # 或 pdm run flask run ``` -*在书中相应位置会包含运行实例程序的提示。* +所有示例程序和章节对应关系如下: + +- 第 1 章:`ch1` +- 第 2 章:`ch2` +- 第 3 章:`ch3` +- 第 4 章:`ch4` +- 第 5 章:`ch5`、`notebook` +- 第 7 章:`longtalk`、`album` + +*在书中相应位置会包含运行示例程序的提示。* diff --git a/examples/form/.flaskenv b/examples/ch1/.flaskenv similarity index 100% rename from examples/form/.flaskenv rename to examples/ch1/.flaskenv diff --git a/examples/hello/app.py b/examples/ch1/app.py similarity index 100% rename from examples/hello/app.py rename to examples/ch1/app.py diff --git a/examples/hello/.flaskenv b/examples/ch2/.flaskenv similarity index 100% rename from examples/hello/.flaskenv rename to examples/ch2/.flaskenv diff --git a/examples/http/app.py b/examples/ch2/app.py similarity index 100% rename from examples/http/app.py rename to examples/ch2/app.py diff --git a/examples/http/.flaskenv b/examples/ch3/.flaskenv similarity index 100% rename from examples/http/.flaskenv rename to examples/ch3/.flaskenv diff --git a/examples/template/app.py b/examples/ch3/app.py similarity index 100% rename from examples/template/app.py rename to examples/ch3/app.py diff --git a/examples/template/static/avatar.png b/examples/ch3/static/avatar.png similarity index 100% rename from examples/template/static/avatar.png rename to examples/ch3/static/avatar.png diff --git a/examples/form/static/css/bootstrap.min.css b/examples/ch3/static/css/bootstrap.min.css similarity index 100% rename from examples/form/static/css/bootstrap.min.css rename to examples/ch3/static/css/bootstrap.min.css diff --git a/examples/form/static/favicon.ico b/examples/ch3/static/favicon.ico similarity index 100% rename from examples/form/static/favicon.ico rename to examples/ch3/static/favicon.ico diff --git a/examples/form/static/js/bootstrap.bundle.min.js b/examples/ch3/static/js/bootstrap.bundle.min.js similarity index 100% rename from examples/form/static/js/bootstrap.bundle.min.js rename to examples/ch3/static/js/bootstrap.bundle.min.js diff --git a/examples/template/static/styles.css b/examples/ch3/static/styles.css similarity index 100% rename from examples/template/static/styles.css rename to examples/ch3/static/styles.css diff --git a/examples/template/templates/base.html b/examples/ch3/templates/base.html similarity index 100% rename from examples/template/templates/base.html rename to examples/ch3/templates/base.html diff --git a/examples/template/templates/errors/404.html b/examples/ch3/templates/errors/404.html similarity index 100% rename from examples/template/templates/errors/404.html rename to examples/ch3/templates/errors/404.html diff --git a/examples/template/templates/errors/500.html b/examples/ch3/templates/errors/500.html similarity index 100% rename from examples/template/templates/errors/500.html rename to examples/ch3/templates/errors/500.html diff --git a/examples/template/templates/index.html b/examples/ch3/templates/index.html similarity index 100% rename from examples/template/templates/index.html rename to examples/ch3/templates/index.html diff --git a/examples/template/templates/macros.html b/examples/ch3/templates/macros.html similarity index 100% rename from examples/template/templates/macros.html rename to examples/ch3/templates/macros.html diff --git a/examples/template/templates/watchlist.html b/examples/ch3/templates/watchlist.html similarity index 100% rename from examples/template/templates/watchlist.html rename to examples/ch3/templates/watchlist.html diff --git a/examples/template/templates/watchlist_with_static.html b/examples/ch3/templates/watchlist_with_static.html similarity index 100% rename from examples/template/templates/watchlist_with_static.html rename to examples/ch3/templates/watchlist_with_static.html diff --git a/examples/template/.flaskenv b/examples/ch4/.flaskenv similarity index 100% rename from examples/template/.flaskenv rename to examples/ch4/.flaskenv diff --git a/examples/form/app.py b/examples/ch4/app.py similarity index 100% rename from examples/form/app.py rename to examples/ch4/app.py diff --git a/examples/form/forms.py b/examples/ch4/forms.py similarity index 100% rename from examples/form/forms.py rename to examples/ch4/forms.py diff --git a/examples/template/static/css/bootstrap.min.css b/examples/ch4/static/css/bootstrap.min.css similarity index 100% rename from examples/template/static/css/bootstrap.min.css rename to examples/ch4/static/css/bootstrap.min.css diff --git a/examples/template/static/favicon.ico b/examples/ch4/static/favicon.ico similarity index 100% rename from examples/template/static/favicon.ico rename to examples/ch4/static/favicon.ico diff --git a/examples/template/static/js/bootstrap.bundle.min.js b/examples/ch4/static/js/bootstrap.bundle.min.js similarity index 100% rename from examples/template/static/js/bootstrap.bundle.min.js rename to examples/ch4/static/js/bootstrap.bundle.min.js diff --git a/examples/form/static/style.css b/examples/ch4/static/style.css similarity index 100% rename from examples/form/static/style.css rename to examples/ch4/static/style.css diff --git a/examples/form/templates/base.html b/examples/ch4/templates/base.html similarity index 100% rename from examples/form/templates/base.html rename to examples/ch4/templates/base.html diff --git a/examples/form/templates/basic.html b/examples/ch4/templates/basic.html similarity index 100% rename from examples/form/templates/basic.html rename to examples/ch4/templates/basic.html diff --git a/examples/form/templates/bootstrap.html b/examples/ch4/templates/bootstrap.html similarity index 100% rename from examples/form/templates/bootstrap.html rename to examples/ch4/templates/bootstrap.html diff --git a/examples/form/templates/custom_validator.html b/examples/ch4/templates/custom_validator.html similarity index 100% rename from examples/form/templates/custom_validator.html rename to examples/ch4/templates/custom_validator.html diff --git a/examples/form/templates/index.html b/examples/ch4/templates/index.html similarity index 100% rename from examples/form/templates/index.html rename to examples/ch4/templates/index.html diff --git a/examples/form/templates/macros.html b/examples/ch4/templates/macros.html similarity index 100% rename from examples/form/templates/macros.html rename to examples/ch4/templates/macros.html diff --git a/examples/form/templates/pure_html.html b/examples/ch4/templates/pure_html.html similarity index 100% rename from examples/form/templates/pure_html.html rename to examples/ch4/templates/pure_html.html diff --git a/examples/ch5/.flaskenv b/examples/ch5/.flaskenv new file mode 100644 index 00000000..068d27fd --- /dev/null +++ b/examples/ch5/.flaskenv @@ -0,0 +1 @@ +FLASK_DEBUG=1 diff --git a/examples/ch5/app.py b/examples/ch5/app.py new file mode 100644 index 00000000..9e51eb39 --- /dev/null +++ b/examples/ch5/app.py @@ -0,0 +1,251 @@ +import os +import sys +from datetime import datetime +from pathlib import Path +from typing import Optional + +import click +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import String, Text, MetaData, ForeignKey, Column, event +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + +SQLITE_PREFIX = 'sqlite:///' if sys.platform.startswith('win') else 'sqlite:////' +SQLITE_PATH = Path(__file__).resolve().parent / 'data.db' + + +class Base(DeclarativeBase): + metadata = MetaData(naming_convention={ + 'ix': 'ix_%(column_0_label)s', + 'uq': 'uq_%(table_name)s_%(column_0_name)s', + 'ck': 'ck_%(table_name)s_%(constraint_name)s', + 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', + 'pk': 'pk_%(table_name)s' + }) + + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'secret string') +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', SQLITE_PREFIX + str(SQLITE_PATH)) +db = SQLAlchemy(app, model_class=Base) + + +# models +class Note(db.Model): + __tablename__ = 'note' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(50)) + body: Mapped[str] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(default=datetime.now) + updated_at: Mapped[Optional[datetime]] = mapped_column(onupdate=datetime.now) + + def __repr__(self): + return f'' + + +# one to many / many to one +class Author(db.Model): + __tablename__ = 'author' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(20)) + phone: Mapped[Optional[str]] = mapped_column(String(11)) + + articles: Mapped[list['Article']] = relationship(back_populates='author') + + def __repr__(self): + return f'' + + +class Article(db.Model): + __tablename__ = 'article' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(50)) + body: Mapped[str] = mapped_column(Text) + author_id: Mapped[int] = mapped_column(ForeignKey('author.id')) + + author: Mapped['Author'] = relationship(back_populates='articles') + + def __repr__(self): + return f'
' + + +# one to one +class Country(db.Model): + __tablename__ = 'country' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(30), unique=True) + + capital: Mapped['Capital'] = relationship(back_populates='country') + + def __repr__(self): + return f'' + + +class Capital(db.Model): + __tablename__ = 'capital' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(30), unique=True) + country_id: Mapped[int] = mapped_column(ForeignKey('country.id')) + + country: Mapped['Country'] = relationship(back_populates='capital') + + def __repr__(self): + return f'' + + +# many to many +student_teacher = db.Table( + 'student_teacher', + Column('student_id', ForeignKey('student.id'), primary_key=True), + Column('teacher_id', ForeignKey('teacher.id'), primary_key=True) +) + + +class Student(db.Model): + __tablename__ = 'student' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(70), unique=True) + grade: Mapped[str] = mapped_column(String(20)) + + teachers: Mapped[list['Teacher']] = relationship( + secondary=student_teacher, + back_populates='students' + ) + + def __repr__(self): + return f'' + + +class Teacher(db.Model): + __tablename__ = 'teacher' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(70), unique=True) + office: Mapped[str] = mapped_column(String(20)) + + students: Mapped[list['Student']] = relationship( + secondary=student_teacher, + back_populates='teachers' + ) + + def __repr__(self): + return f'' + + +# many to many with extra data +class Collection(db.Model): + __tablename__ = 'collection' + + user_id: Mapped[int] = mapped_column(ForeignKey('user.id'), primary_key=True) + photo_id: Mapped[int] = mapped_column(ForeignKey('photo.id'), primary_key=True) + created_at: Mapped[datetime] = mapped_column(default=datetime.now) + note: Mapped[Optional[str]] = mapped_column(Text) + + user: Mapped['User'] = relationship(back_populates='collections', lazy='joined') + photo: Mapped['Photo'] = relationship(back_populates='collections', lazy='joined') + + def __repr__(self): + return f'' + + +class User(db.Model): + __tablename__ = 'user' + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(50), unique=True) + + collections: Mapped[list['Collection']] = relationship( + back_populates='user', + cascade='all, delete-orphan' + ) + + def __repr__(self): + return f'' + + +class Photo(db.Model): + __tablename__ = 'photo' + + id: Mapped[int] = mapped_column(primary_key=True) + filename: Mapped[str] = mapped_column(String(255), unique=True) + + collections: Mapped[list['Collection']] = relationship( + back_populates='photo', + cascade='all, delete-orphan' + ) + + def __repr__(self): + return f'' + + +# cascade and event listener +class Post(db.Model): + __tablename__ = 'post' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[Optional[str]] = mapped_column(String(50)) + body: Mapped[Optional[str]] = mapped_column(Text) + edited_count: Mapped[int] = mapped_column(default=0) + + comments: Mapped[list['Comment']] = relationship( + back_populates='post', + cascade='all, delete-orphan' + ) + + +class Comment(db.Model): + __tablename__ = 'comment' + + id: Mapped[int] = mapped_column(primary_key=True) + body: Mapped[Optional[str]] = mapped_column(Text) + post_id: Mapped[int] = mapped_column(ForeignKey('post.id')) + + post: Mapped['Post'] = relationship(back_populates='comments') + + +@event.listens_for(Post.body, 'set') +def increment_edited_count(target, value, oldvalue, initiator): + if target.edited_count is not None: + target.edited_count += 1 + + +# shell context +@app.shell_context_processor +def make_shell_context(): + return dict( + db=db, + Note=Note, + Author=Author, + Article=Article, + Country=Country, + Capital=Capital, + Student=Student, + Teacher=Teacher, + User=User, + Photo=Photo, + Collection=Collection, + Post=Post, + Comment=Comment + ) + + +# commands +@app.cli.command('init') +@click.option('--drop-table', is_flag=True, help='Re-create the tables.') +def init_command(drop_table): + """Initialize the application.""" + if drop_table: + click.confirm( + 'This operation will delete the tables, do you want to continue?', + abort=True + ) + db.drop_all() + click.echo('Dropped tables.') + db.create_all() + click.echo('Initialized.')