はじめに
本記事は次の連載の一部です。
No. 04-01ではFlaskを使って小説のデータベースを表示することを目標としています。
以下ディレクトリ構成です。
/work
├── flask_app
│ ├── models
│ │ └── novel_info.py
│ ├── static
│ ├── templates
│ ├── __init__.py
│ ├── config.py
│ ├── filters.py
│ └── views.py
├── instance (自動生成)
├── scraping_modules
├── init_db.py
└── secret_info.json
今回作成・編集するファイルは次のとおりです。
flask_app/models/novel_info.py
flask_app/__init__.py
init_db.py
flask_app/views.py
flask_app/templates/novel_list.html
flask_app/templates/add_novel.html
flask_app/templates/edit_novel.html
flask_app/filters.py
DBのデータモデルを作成する
まずは、DBに登録するデータのモデル(形式)を定義します。
flask_app/models/novel_info.py
を作成します。
No 03で抽出した小説データが入るようにデータを定義しておきます。
from flask_app import db
from datetime import datetime
tags = db.Table('tags',
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
db.Column('novel_id', db.String, db.ForeignKey('novels.id'))
)
class Tag(db.Model):
__tablename__ = "tag"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True)
class Novel(db.Model):
__tablename__ = "novels"
id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.String)
site_type = db.Column(db.String)
site_id = db.Column(db.String)
state = db.Column(db.String)
title = db.Column(db.String)
auther_name = db.Column(db.String)
auther_url = db.Column(db.String)
novel_update_date = db.Column(db.DateTime)
siori_ep = db.Column(db.Integer)
least_ep = db.Column(db.Integer)
summary = db.Column(db.String)
series = db.Column(db.String)
series_url = db.Column(db.String)
tags = db.relationship('Tag', secondary=tags)
genre = db.Column(db.String)
novel_init_date = db.Column(db.DateTime)
string_num = db.Column(db.Integer)
thoughts_num = db.Column(db.Integer)
review_num = db.Column(db.Integer)
bookmark_num = db.Column(db.Integer)
overall_eval_point = db.Column(db.Integer)
eval_point = db.Column(db.Integer)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
your_eval_point = db.Column(db.Integer)
your_thoughts = db.Column(db.String)
小説のタグは多対多のリレーションで定義されます。SQLAlchemyのドキュメントを参考にしました。
また、著者テーブルを作成するかを数日検討しましたが、ここでは作成せずに、著者情報は小説情報の一部として扱うことにしました。
なぜかというと、「著者名が同じでも著者が違う」、「著者名が異なっても著者URLが同じ」、「プラットフォーム事に著者ページがある」などの理由で、著者を検索して一意に決定することが困難だと判断したためです。
また、「同じ著者の作品でもプラットフォームが違うと、掲載されている話数が違う」ということもあるため、小説情報の一部として著者情報を持つことにしました。
novel_info.pyを作成したら、このモデルをDBに登録します。そのため__init__.pyを編集します。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object('flask_app.config')
db = SQLAlchemy(app)
from .models import novel_info
import flask_app.views
novel_info.pyをインポートすれば、定義した関数(モデル)が読み込まれて、DBで扱えるようになります。
DBの初期化
モデルを定義したらDBを初期化します。短いのでインタープリターで書いても良いのですが、アプリの作成過程で、試行錯誤するときに利用しやすいようにソースコードとして書いておきます。
init_db.pyを作成します。
from flask_app import app
from flask_app import db
with app.app_context():
db.create_all()
/workにて
python init_db.py
と実行すると、/work/instanceディレクトリが作成され、中にDBファイルが作成されます。
これでDBの初期化ができ、pythonからSQL命令が出せるようになりました。
DBのテーブル表示
DBのテーブルを表示するためにviews.pyを編集します。
/novel_listへアクセスしたときに呼び出される関数を編集します。
from flask import Flask, render_template, url_for, request, jsonify, redirect
from flask_app import app, db
from flask_app.models.novel_info import Novel, Tag
from datetime import datetime
# ... any code ...
@app.route('/novel_list')
def novel_list():
novels = Novel.query.all()
return render_template('novel_list.html', novels=novels)
# ... any code ...
上で定義したモデルのNovelをインポートし、それに対して.query.all()をすることで、Novelのテーブルが得られます。
得られたテーブルデータをnovel_list.htmlをレンダリングするときにテンプレート変数として渡します。
novel_list.htmlを編集して、DBのテーブルを表示させます。
{% extends 'base.html' %}
{% block main %}
<main class="main-content position-relative border-radius-lg ">
<nav class="navbar navbar-main navbar-expand-lg px-0 mx-4 shadow-none border-radius-xl " id="navbarBlur" data-scroll="false">
<div class="container-fluid py-1 px-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent mb-0 pb-0 pt-1 px-0 me-sm-6 me-5">
<li class="breadcrumb-item text-sm"><a class="opacity-5 text-white">Pages</a></li>
<li class="breadcrumb-item text-sm text-white active" aria-current="page">Novel List</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Novel List</h6>
</nav>
</div>
</nav>
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-body">
<div class="row">
<div class="col">
<a class="btn bg-gradient-dark mb-0">add Novel Info</a>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from narou</button>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from hameln</button>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from kakuyomu</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="card mb-4">
<div class="card-header pb-0">
<h6>Novel List</h6>
</div>
<div class="card-body px-0 pt-0 pb-2">
<div class="table-responsive p-0">
<table class="table align-items-center mb-0">
<thead>
<tr>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">title</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">auther</th>
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">state</th>
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">tags</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">edit</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">delete</th>
</tr>
</thead>
<tbody>
{% for novel in novels %}
<tr >
<td>
<div class="d-flex flex-column justify-content-between">
<h6 class="mb-0 text-sm">{{novel.title}}</h6>
<p class="text-xs text-secondary mb-0">{{novel.url}}</p>
</div>
</td>
<td class="align-middle text-center">
<div class="d-flex flex-column justify-content-center">
<h6 class="mb-0 text-sm">{{novel.auther_name}}</h6>
<p class="text-xs text-secondary mb-0">{{novel.auther_url}}</p>
</div>
</td>
<td class="align-middle text-center">
{% if novel.state == '連載中' %}
<span class="badge bg-gradient-primary">{{novel.state}}</span>
{% elif novel.state == '完結済' %}
<span class="badge bg-gradient-success">{{novel.state}}</span>
{% elif novel.state == '短編' %}
<span class="badge bg-gradient-info">{{novel.state}}</span>
{% elif novel.state == 'エタ' %}
<span class="badge bg-gradient-secondary">{{novel.state}}</span>
{% endif %}
</td>
<td class="align-middle text-center" style="word-wrap:break-word;">
{% for tag in novel.tags %}
<span class="badge badge-sm bg-gradient-dark">{{tag.name}}</span>
{% endfor %}
</td>
<td>
<a class="btn bg-gradient-primary mb-0">
<i class="bi bi-pencil-square"></i>
</a>
</td>
<td>
<form method="post">
<button class="btn bg-gradient-danger mb-0" type="submit", onclick="return confirm('本当に削除しますか?')">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
{% endblock %}
Novel Listのページへアクセスすると作成したUIが表示されます。

まだ、DBのテーブルにデータを入れていないので何も表示されません。
テーブルへのデータ追加
データを追加するフォームを作成します。
追加フォームへの移動ボタン
まず、「add Novel Info」ボタンにリンクを設置します。
novel_hist.htmlの「add Novel Info」ボタンにhrefを追加します。flaskのテンプレートでは「url_for(‘関数名’)」とすれば、appに定義された関数のリンクへ変換されます。
<div class="col-12">
<div class="card mb-4">
<div class="card-body">
<div class="row">
<div class="col">
<a class="btn bg-gradient-dark mb-0" href="{{url_for('add_novel')}}">add Novel Info</a>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from narou</button>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from hameln</button>
</div>
<div class="col">
<button class="btn bg-gradient-dark mb-0">Import Novel Info from kakuyomu</button>
</div>
</div>
</div>
</div>
</div>
追加フォームのPOSTとGETの定義
add_novel関数が定義されていないので、views.pyへ追記します。リクエストがGETかPOSTかで挙動を変えます。
if文で分岐するのが嫌な人は、追加フォームを表示するリンクと、フォームからDBへ追加するリンクを別関数で定義してもOKです。
# ... any code ...
@app.route('/add_novel', methods=['GET', 'POST'])
def add_novel():
if request.method == 'GET':
return render_template('add_novel.html')
if request.method == 'POST':
novel = Novel(
url = request.form.get('url', default=''),
site_type = request.form.get('site_type'),
site_id = request.form.get('site_id'),
state = request.form.get('state'),
title = request.form.get('title'),
auther_name = request.form.get('auther_name'),
auther_url = request.form.get('auther_url', default=''),
#novel_update_date = request.form.get('novel_update_date', type=datetime, default=datetime.now()),
siori_ep = request.form.get('siori_ep', type=int, default=0),
least_ep = request.form.get('least_ep', type=int, default=0),
summary = request.form.get('summary'),
series = request.form.get('series'),
series_url = request.form.get('series_url', default=''),
genre = request.form.get('genre'),
#novel_init_date = request.form.get('novel_init_date', type=datetime, default=datetime.now()),
string_num = request.form.get('string_num', type=int, default=0),
thoughts_num = request.form.get('thoughts_num', type=int, default=0),
review_num = request.form.get('review_num', type=int, default=0),
bookmark_num = request.form.get('bookmark_num', type=int, default=0),
overall_eval_point = request.form.get('overall_eval_point', type=int, default=0),
eval_point = request.form.get('eval_point', type=int, default=0),
your_eval_point = request.form.get('your_eval_point', type=int, default=0),
your_thoughts = request.form.get('your_thoughts')
)
novel_update_date = request.form.get('novel_update_date')
if novel_update_date == '':
novel.novel_update_date = datetime.now()
else:
novel.novel_update_date = datetime.fromisoformat(novel_update_date)
novel_init_date = request.form.get('novel_init_date')
if novel_init_date == '':
novel.novel_init_date = datetime.now()
else:
novel.novel_init_date = datetime.fromisoformat(novel_init_date)
tags_str = request.form.get('tags')
tags = tags_str.split(',')
for tag in tags:
tag = tag.strip()
tag_item = Tag.query.filter_by(name=tag).first()
if tag_item == None:
tag_item = Tag(name=tag)
novel.tags.append(tag_item)
db.session.add(novel)
db.session.commit()
return redirect(url_for('novel_list'))
# ... any code ...
GETの場合は単純で、add_novel.htmlを返すだけです。
POSTの場合は、add_novel.htmlからPOSTされることを想定しています。フォームから要素を取り出して、DBモデルのNovelを新しく作成します。
また、タグは新規のタグだけを新しく作成し、すでにタグテーブルにある場合は、それを呼び出します。その後、タグオブジェクトをNovelのtagsへ追加します。
flaskのデータベースのdatetime型はpythonのdatetime型じゃないと怒られるので、datetime.fromisoformatで変換してから入れることに注意します。また、未入力の場合には現在時刻を入れるようにしておきます。
追加フォームのHTMLの作成
あとは、追加フォームをtemplatesへと新しく追加します。名前はadd_novel.htmlとしました。
{% extends 'base.html' %}
{% block main %}
<main class="main-content position-relative border-radius-lg ">
<nav class="navbar navbar-main navbar-expand-lg px-0 mx-4 shadow-none border-radius-xl " id="navbarBlur" data-scroll="false">
<div class="container-fluid py-1 px-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent mb-0 pb-0 pt-1 px-0 me-sm-6 me-5">
<li class="breadcrumb-item text-sm"><a class="opacity-5 text-white">Pages</a></li>
<li class="breadcrumb-item text-sm text-white active" aria-current="page">Novel List</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Add Novel Form</h6>
</nav>
</div>
</nav>
<div class="container-fluid py-4">
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<p class="mb-0">Auto collect from URL</p>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col">
<input class="form-control" type="url" placeholder="https://" id="autocollect_novel_url">
</div>
<div class="col">
<button type="button" class="btn btn-primary btn-sm ms-auto">auto collect</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container-fluid">
<form method="POST">
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<p class="mb-0">Infomation</p>
</div>
</div>
<div class="card-body">
<p class="text-sm">Main</p>
<div class="form-group">
<label for="example-text-input" class="form-control-label">タイトル</label>
<input id="title" name="title" class="form-control" type="text">
</div>
<div class="form-group">
<label for="example-text-input" class="form-control-label">URL</label>
<input id="url" name="url" class="form-control" type="url">
</div>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<label for="example-text-input" class="form-control-label">ジャンル</label>
<input id="genre" name="genre" class="form-control" type="text">
</div>
</div>
<div class="col-md-10">
<div class="form-group">
<label for="example-text-input" class="form-control-label">タグ</label>
<input id="tags" name="tags" class="form-control" type="text">
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">短編/連載中/完結済/エタ</label>
<select id="state" name="state" class="form-control">
<option value="短編">短編</option>
<option value="連載中">連載中</option>
<option value="完結済">完結済</option>
<option value="エタ">エタ</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">話数</label>
<input id="least_ep" name="least_ep" class="form-control" type="number">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">栞</label>
<input id="siori_ep" name="siori_ep" class="form-control" type="number">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">更新日</label>
<input id="novel_update_date" name="novel_update_date" class="form-control" type="datetime-local">
</div>
</div>
</div>
<p class="text-sm">Auther</p>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<label for="example-text-input" class="form-control-label">筆者</label>
<input id="auther_name" name="auther_name" class="form-control" type="text">
</div>
</div>
<div class="col-md-10">
<div class="form-group">
<label for="example-text-input" class="form-control-label">筆者URL</label>
<input id="auther_url" name="auther_url" class="form-control" type="url">
</div>
</div>
</div>
<p class="text-sm">Detail</p>
<div class="form-group">
<label for="example-text-input" class="form-control-label">あらすじ</label>
<textarea id="summary" name="summary" class="form-control" type="text", rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">シリーズ名</label>
<input id="series" name="series" class="form-control" type="text">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">シリーズURL</label>
<input id="series_url" name="'series_url" class="form-control" type="url">
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">公開日</label>
<input id="novel_init_date" name="novel_init_date" class="form-control" type="datetime-local">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">文字数</label>
<input id="string_num" name="string_num" class="form-control" type="number">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">感想数</label>
<input id="thoughts_num" name="thoughts_num" class="form-control" type="number">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">レビュー数</label>
<input id="review_num" name="review_num" class="form-control" type="number">
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">ブックマーク数</label>
<input id="bookmark_num" name="bookmark_num" class="form-control" type="datetime">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内総評価ポイント</label>
<input id="overall_eval_point" name="overall_eval_point" class="form-control" type="number">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内評価ポイント</label>
<input id="eval_point" name="eval_point" class="form-control" type="number">
</div>
</div>
</div>
<p class="text-sm">Other</p>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイトタイプ</label>
<input id="site_type" name="site_type" class="form-control" type="text">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内ID</label>
<input id="site_id" name="site_id" class="form-control" type="text">
</div>
</div>
</div>
<p class="text-sm">Your Rating</p>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">評価</label>
<input id="your_eval_point" name="your_eval_point" class="form-control" type="text">
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="example-text-input" class="form-control-label">感想</label>
<textarea id="your_thoughts" name="your_thoughts" class="form-control" type="text", rows="3"></textarea>
</div>
</div>
<div class="col">
<input type="submit" class="btn btn-primary btn-sm ms-auto" value="登録">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</main>
{% endblock %}
HTMLを追加して、「add Novel Info」ボタンを押すとページが移動します。

今回は2つのUIブロックを導入しました。1つ目は「Auto collect from URL」というUIです。これは、次回実装しますが、小説のURLを入れると、以下のフォーム入力欄を自動で入れてくれる機能です。
2つ目が今回実装した、DBへの追加フォームです。試しにタイトルに「TEST」入れて、一番下の登録ボタンを押します。
エラーなく追加できれば、Novel Listページへと移動し、データが追加されたDBテーブルが表示されます。

テーブルのデータ編集
次に、追加したデータの編集ができるようにします。
編集フォームへの移動ボタン
編集ボタンはテーブルの要素に入れておいたので、そのアイコンをクリックすると、編集ページへ移動できるようにします。
<td>
<a class="btn bg-gradient-primary mb-0" href="{{ url_for('edit_novel', novel_id=novel.id)}}">
<i class="bi bi-pencil-square"></i>
</a>
</td>
編集フォームのPOSTとGETの定義
追加ページと同じように、views.pyにリンクが呼ばれたときの関数を追記します。add_novelとの違いは、novel_idを引数に取ることです。これによって編集するDBテーブルの要素を特定しています。
# ... any code ...
@app.route('/edit_novel/<int:novel_id>', methods=['GET', 'POST'])
def edit_novel(novel_id):
novel = Novel.query.get_or_404(novel_id)
if request.method == 'GET':
return render_template('edit_novel.html', novel=novel)
if request.method == 'POST':
novel.url = request.form.get('url', default='')
novel.site_type = request.form.get('site_type')
novel.site_id = request.form.get('site_id')
novel.state = request.form.get('state')
novel.title = request.form.get('title')
novel.auther_name = request.form.get('auther_name')
novel.auther_url = request.form.get('auther_url', default='')
novel_update_date = datetime.fromisoformat(request.form.get('novel_update_date'))
novel.novel_update_date = novel_update_date
novel.siori_ep = request.form.get('siori_ep', type=int, default=0)
novel.least_ep = request.form.get('least_ep', type=int, default=0)
novel.summary = request.form.get('summary')
novel.series = request.form.get('series')
novel.series_url = request.form.get('series_url', default='')
novel.genre = request.form.get('genre')
novel_init_date = datetime.fromisoformat(request.form.get('novel_init_date'))
novel.novel_init_date = novel_init_date
novel.string_num = request.form.get('string_num', type=int, default=0)
novel.thoughts_num = request.form.get('thoughts_num', type=int, default=0)
novel.review_num = request.form.get('review_num', type=int, default=0)
novel.bookmark_num = request.form.get('bookmark_num', type=int, default=0)
novel.overall_eval_point = request.form.get('overall_eval_point', type=int, default=0)
novel.eval_point = request.form.get('eval_point', type=int, default=0)
novel.your_eval_point = request.form.get('your_eval_point', type=int, default=0)
novel.your_thoughts = request.form.get('your_thoughts')
novel.updated_at = datetime.now()
db.session.merge(novel)
db.session.commit()
return redirect(url_for('novel_list'))
# ... any code ...
編集フォームのHTMLの作成
GETが来たときに返すedit_novel.htmlをtemplatesに作成します。内容はほとんどadd_novel.htmlと同じですが、テーブルの要素を自動入力するようにしておきます。
{% extends 'base.html' %}
{% block main %}
<main class="main-content position-relative border-radius-lg ">
<nav class="navbar navbar-main navbar-expand-lg px-0 mx-4 shadow-none border-radius-xl " id="navbarBlur" data-scroll="false">
<div class="container-fluid py-1 px-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb bg-transparent mb-0 pb-0 pt-1 px-0 me-sm-6 me-5">
<li class="breadcrumb-item text-sm"><a class="opacity-5 text-white">Pages</a></li>
<li class="breadcrumb-item text-sm text-white active" aria-current="page">Novel List</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Edit Novel Form</h6>
</nav>
</div>
</nav>
<div class="container-fluid">
<form method="POST">
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<p class="mb-0">Infomation</p>
</div>
</div>
<div class="card-body">
<p class="text-sm">Main</p>
<div class="form-group">
<label for="example-text-input" class="form-control-label">タイトル</label>
<input id="title" name="title" class="form-control" type="text" value={{novel.title}}>
</div>
<div class="form-group">
<label for="example-text-input" class="form-control-label">URL</label>
<input id="url" name="url" class="form-control" type="url" value={{novel.url}}>
</div>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<label for="example-text-input" class="form-control-label">ジャンル</label>
<input id="genre" name="genre" class="form-control" type="text" value={{novel.genre}}>
</div>
</div>
<div class="col-md-10">
<div class="form-group">
<label for="example-text-input" class="form-control-label">タグ</label>
<input id="tags" name="tags" class="form-control" type="text" value={{novel.tags | tags2str}}>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">短編/連載/完結/エタ</label>
<select id="state" name="state" class="form-control">
<option value="短編" {% if "短編" in novel.state %}selected{% endif %}>短編</option>
<option value="連載中" {% if "連載中" in novel.state %}selected{% endif %}>連載中</option>
<option value="完結済" {% if "完結済" in novel.state %}selected{% endif %}>完結済</option>
<option value="エタ" {% if "エタ" in novel.state %}selected{% endif %}>エタ</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">話数</label>
<input id="least_ep" name="least_ep" class="form-control" type="number" value={{novel.least_ep}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">栞</label>
<input id="siori_ep" name="siori_ep" class="form-control" type="number" value={{novel.siori_ep}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">更新日</label>
<input id="novel_update_date" name="novel_update_date" class="form-control" type="datetime-local" value={{novel.novel_update_date | time2str }}>
</div>
</div>
</div>
<p class="text-sm">Auther</p>
<div class="row">
<div class="col-md-2">
<div class="form-group">
<label for="example-text-input" class="form-control-label">筆者</label>
<input id="auther_name" name="auther_name" class="form-control" type="text" value={{novel.auther_name}}>
</div>
</div>
<div class="col-md-10">
<div class="form-group">
<label for="example-text-input" class="form-control-label">筆者URL</label>
<input id="auther_url" name="auther_url" class="form-control" type="url" value={{novel.auther_url}}>
</div>
</div>
</div>
<p class="text-sm">Detail</p>
<div class="form-group">
<label for="example-text-input" class="form-control-label">あらすじ</label>
<textarea id="summary" name="summary" class="form-control" type="text", rows="3">{{novel.summary}}</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">シリーズ名</label>
<input id="series" name="series" class="form-control" type="text" value={{novel.series}}>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">シリーズURL</label>
<input id="series_url" name="'series_url" class="form-control" type="url" value={{novel.series_url}}>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">公開日</label>
<input id="novel_init_date" name="novel_init_date" class="form-control" type="datetime-local" value={{novel.novel_init_date | time2str}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">文字数</label>
<input id="string_num" name="string_num" class="form-control" type="number" alue={{novel.string_num}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">感想数</label>
<input id="thoughts_num" name="thoughts_num" class="form-control" type="number" value={{novel.thoughts_num}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">レビュー数</label>
<input id="review_num" name="review_num" class="form-control" type="number" value={{novel.review_num}}>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">ブックマーク数</label>
<input id="bookmark_num" name="bookmark_num" class="form-control" type="datetime" value={{novel.bookmark_num}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内総評価ポイント</label>
<input id="overall_eval_point" name="overall_eval_point" class="form-control" type="number" value={{novel.overall_eval_point}}>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内評価ポイント</label>
<input id="eval_point" name="eval_point" class="form-control" type="number" value={{novel.eval_point}}>
</div>
</div>
</div>
<p class="text-sm">Other</p>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイトタイプ</label>
<input id="site_type" name="site_type" class="form-control" type="text" value={{novel.site_type}}>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="example-text-input" class="form-control-label">サイト内ID</label>
<input id="site_id" name="site_id" class="form-control" type="text" value={{novel.site_id}}>
</div>
</div>
</div>
<p class="text-sm">Your Rating</p>
<div class="col-md-3">
<div class="form-group">
<label for="example-text-input" class="form-control-label">評価</label>
<input id="your_eval_point" name="your_eval_point" class="form-control" type="text" value={{novel.your_eval_point}}>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="example-text-input" class="form-control-label">感想</label>
<textarea id="your_thoughts" name="your_thoughts" class="form-control" type="text", rows="3">{{novel.your_thoughts}}</textarea>
</div>
</div>
<p class="text-sm">DB Infomation</p>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label for="example-text-input" class="form-control-label">Item ID</label>
<input id="db_id" name="db_id" class="form-control" type="number" value={{novel.id}} disabled>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="example-text-input" class="form-control-label">Created at</label>
<input id="created_at" name="created_at" class="form-control" type="text" value={{novel.created_at | time2str}} disabled>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="example-text-input" class="form-control-label">Updated at</label>
<input id="updated_at" name="updated_at" class="form-control" type="text" value={{novel.updated_at | time2str}} disabled>
</div>
</div>
</div>
<div class="col">
<input type="submit" class="btn btn-primary btn-sm ms-auto" value="更新">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</main>
{% endblock %}
ここで、DBの要素であるNovelをhtmlのレンダリングに渡して、既知の小説の情報を自動入力させます。しかし、Novel.novel_update_dateなどの日付の要素は文字列ではないため、htmlへ出力するとバグります。
そこで、日付の要素はfilterという機能を使って別でレンダリングすることにしました。
filterを使うために、filters.pyを定義します。
from flask import Flask
from flask_app import app
@app.template_filter('time2str')
def time2str(time):
return time.isoformat(timespec='seconds')
@app.template_filter('tags2str')
def tags2str(tags):
return ','.join([tag.name for tag in tags])
ここでは2つの関数を用意しました。
- time2str : datetimeオブジェクトをisoフォーマット文字列に変換する
- tags2str : tagsオブジェクト(リスト)をカンマで並べた文字列に変換する
この2つの上のHTMLのテンプレート変数中で使うことで、変数から文字列の変換ができます。
filters.pyは__init__.py上で一度読み込む必要があります。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object('flask_app.config')
db = SQLAlchemy(app)
from .models import novel_info
import flask_app.views
import flask_app.filters
Novel Listページのテーブルの編集ボタンを押すと、編集ページが表示されます。

試しに、筆者の名前をTesterとして追記して、一番下の更新ボタンを押します。

無事、筆者の名前が追加された要素がNovel Listページに表示されました。
テーブルのデータ削除
最後に削除簿機能を実装します。
削除リクエストのボタン
削除ボタンもテーブルの要素に入れておいたので、そのアイコンをクリックすると、削除リクエストが飛ぶようにしておきます。
<td>
<form method="post" action="{{ url_for('delete_novel', novel_id=novel.id)}}">
<button class="btn bg-gradient-danger mb-0" type="submit", onclick="return confirm('本当に削除しますか?')">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
onclick="return confirm('本当に削除しますか?')"
という要素を入れると、確認ポップアップが表示されます。
削除リクエストのPOSTの定義
編集ページと同じように、views.pyにリンクが呼ばれたときの関数を追記します。削除リクエストには、移動ページは無いので、POSTだけ定義します。
# ... any code ...
@app.route('/delete_novel/<int:novel_id>', methods=['POST'])
def delete_novel(novel_id):
novel = Novel.query.get_or_404(novel_id)
db.session.delete(novel)
db.session.commit()
return redirect(url_for('novel_list'))
# ... any code ...
リクエストが来たら、指定された要素のIDをDBで検索し、それを削除します。
これだけで、削除機能は実装できました。試しに、Novel Listページの削除ボタンを押すと、テーブルの要素が消えます。
無事、削除できたら今回の内容は終了です。
Section 04-02では、小説追加時の自動入力機能などを実装します。