[Web小説を管理するアプリ] No.04-01 Flaskを使ったDBテーブルの表示・追加・更新

はじめに

本記事は次の連載の一部です。

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のドキュメントを参考にしました。

Basic Relationship Patterns — SQLAlchemy 2.0 Documentation

また、著者テーブルを作成するかを数日検討しましたが、ここでは作成せずに、著者情報は小説情報の一部として扱うことにしました。

なぜかというと、「著者名が同じでも著者が違う」、「著者名が異なっても著者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では、小説追加時の自動入力機能などを実装します。


タイトルとURLをコピーしました