はじめに
本記事は次の連載の一部です。
No. 04ではFlaskを使って小説のデータベース管理画面を作成することを目標としています。
以下ディレクトリ構成です。
.
├── .devcontainer
├── argon-dashboard-master
├── flask_app
│ ├── models
│ │ └── novel_info.py
│ ├── static
│ ├── templates
│ ├── __init__.py
│ ├── config.py
│ └── views.py
├── instance (自動生成)
├── scraping_modules
├── init_db.py
└── secret_info.json
flaskアプリのディレクトリ構成はいろいろな種類がありますが、今回は、モジュールをflask_appというディレクトリにまとめることにしました。
この場合は、一般的に使われるapp.pyやrun.pyの内容を__init__.pyに各記法です。
また、WebのUIはargon-dashboardから持ってきました。adminliteと迷いましたが、気分的にargonだったので、そうしました。これは好きなdashboardテンプレートを選んでください。
参考にするときに開くのが便利なので、argon-dashboard-masterをプロジェクトディレクトリに入れていますが、flaskのWeb画面として参照するhtmlは、staticやtemplatesにいれます。
データベースの初期化のためのスクリプトinit_db.pyを用意しました。これを実行すると、instanceディレクトリが生成され、データベースファイルが作られます。
Argon Dashboard 2
今回のWebのテンプレートはArgon Dashboard 2を選びました。これはMITライセンスで配布されている無料のテンプレートです。
![](https://s3.amazonaws.com/creativetim_bucket/products/96/original/argon-dashboard-2.jpg?1643114907)
なんとなく、丸いUIが良いかなと思ったので、これにしました。好きなBootstrapのテンプレートを選ぶと良いと思います。
ダウンロードしたテンプレートは解凍して、適当なディレクトリに移します。今回は、プロジェクトのディレクトリに移動させました。
中身のindex.htmlを開くと、デモ画面が表示されます。
![](https://emoclework.jp/wp-content/uploads/2024/07/image.png)
ログインとかの機能は実装しませんが、こんな雰囲気の小説管理Web UIが作れることを想像しておきます。
FlaskでのWeb UIの実装
Flaskはテンプレートと呼ばれる機能を使って、Flask側でhtmlファイルを生成し、アクセスしてきたURLに返す仕組みを採用しています。
ですから、そのままArgon dashboardをFlask内にコピーしても、Flask側ではindex.htmlを読み取ってはくれません。
まずは、flaskアプリを生成するプログラムを書きます。これは__init__.pyに書いておくことで、flask_appディレクトリをpythonで読み込むたびに、利用できるようになります。
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config.from_object('flask_app.config')
import flask_app.views
また、同ディレクトリにあるviews.pyを読み込むことで、URLによるリクエストに対して何を返すかを記述します。
一般的なFlaskのチュートリアルでは、appの生成とviews.pyでのリクエスト関数は同じファイルで記述されますが、別ファイルに分けることで、管理がしやすくなります。
from flask import Flask, render_template, url_for
from flask_app import app
@app.route('/')
def index():
return render_template('dashboard.html')
@app.route('/novel_list')
def novel_list():
return render_template('novel_list.html')
@app.route('/save')
def save():
return render_template('save.html')
@app.route('/analysis')
def analysis():
return render_template('analysis.html')
@app.route('/recommend')
def recommend():
return render_template('recommend.html')
@app.route('/settings_general')
def settings_general():
return render_template('settings_general.html')
url_forを利用しないと、Web UI側でサイドバーの画面遷移ができないので、インポートしておきます。
views.pyでは、「ルート、小説リスト、保存、分析、推薦、設定画面」の6つのURLに対して、返答するhtmlをここで決めておきます。
また、flaskの設定をconfig.pyに追記できます。
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///novel_manager.db'
SQLALCHEMY_TRACK_MODIFICATIONS = True
ここでは小説のデータベースのファイルを設定していますが、この記事ではまだ、データベースを利用しません。
views.pyを書き終わったら、それぞれの関数が呼び出すhtmlを作成していきます。Flaskではデフォルトでtemplatesディレクトリにあるhtmlを読み込みますので、templatesディレクトリを作り、そこに各種のhtmlを配置します。
さらに各htmlが読み込むassets(jsやcssなど)はstaticに配置します。
配置予定は以下のとおりです。
├── flask_app
...
│ ├── static
│ │ ├── css
│ │ ├── fonts
│ │ ├── img
│ │ ├── js
│ │ └── scss
│ ├── templates
│ │ ├── analysis.html
│ │ ├── base.html
│ │ ├── dashboard.html
│ │ ├── novel_list.html
│ │ ├── recommend.html
│ │ ├── save.html
│ │ └── settings_general.html
...
staticへのファイル配置
staticにはcssやjavascriptなどの固定要素を配置します。
これは、自分が用意したテンプレートが読み込むファイルを配置すればよいです。
今回は、Argon Dashboard 2の中にあった、assets以下のディレクトリをそのままコピーしました。
base.htmlの実装
共通するhtmlを記述するbase.htmlを作成します。以下に今回作成したbase.htmlを記述しておきます。
<!--
=========================================================
* Argon Dashboard 2 - v2.0.4
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard
* Copyright 2022 Creative Tim (https://www.creative-tim.com)
* Licensed under MIT (https://www.creative-tim.com/license)
* Coded by Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="apple-touch-icon" sizes="76x76" href="/static/img/apple-icon.png">
<link rel="icon" type="image/png" href="/static/img/favicon.png">
<title>
Novel Manager
</title>
<!-- Fonts and icons -->
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet" />
<!-- Nucleo Icons -->
<link href="/static/css/nucleo-icons.css" rel="stylesheet" />
<link href="/static/css/nucleo-svg.css" rel="stylesheet" />
<!-- Font Awesome Icons -->
<script src="https://kit.fontawesome.com/42d5adcbca.js" crossorigin="anonymous"></script>
<link href="/static/css/nucleo-svg.css" rel="stylesheet" />
<!-- CSS Files -->
<link id="pagestyle" href="/static/css/argon-dashboard.css?v=2.0.4" rel="stylesheet" />
<!-- Bootstrap icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
</head>
<body class="g-sidenav-show bg-gray-100">
<div class="min-height-300 bg-primary position-absolute w-100"></div>
<aside class="sidenav bg-white navbar navbar-vertical navbar-expand-xs border-0 border-radius-xl my-3 fixed-start ms-4 " id="sidenav-main">
<div class="sidenav-header">
<i class="fas fa-times p-3 cursor-pointer text-secondary opacity-5 position-absolute end-0 top-0 d-none d-xl-none" aria-hidden="true" id="iconSidenav"></i>
<a class="navbar-brand m-0" target="_blank">
<span class="ms-1 font-weight-bold">Novel Manager</span>
</a>
</div>
<hr class="horizontal dark mt-0">
<div class="collapse navbar-collapse w-auto " id="sidenav-collapse-main">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link " href="{{url_for('index')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-app text-primary text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">Dashboard</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="{{url_for('novel_list')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-bullet-list-67 text-warning text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">Novel List</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="{{url_for('save')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-folder-17 text-success text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">Save</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="{{url_for('analysis')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-chart-bar-32 text-info text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">Analysis</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link " href="{{url_for('recommend')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-like-2 text-danger text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">Recommend</span>
</a>
</li>
<li class="nav-item mt-3">
<h6 class="ps-4 ms-2 text-uppercase text-xs font-weight-bolder opacity-6">Settings</h6>
</li>
<li class="nav-item">
<a class="nav-link " href="{{url_for('settings_general')}}">
<div class="icon icon-shape icon-sm border-radius-md text-center me-2 d-flex align-items-center justify-content-center">
<i class="ni ni-settings-gear-65 text-dark text-sm opacity-10"></i>
</div>
<span class="nav-link-text ms-1">General</span>
</a>
</li>
</ul>
</div>
</aside>
{% block main %}
{% endblock %}
<div class="fixed-plugin">
<a class="fixed-plugin-button text-dark position-fixed px-3 py-2">
<i class="fa fa-cog py-2"> </i>
</a>
<div class="card shadow-lg">
<div class="card-header pb-0 pt-3 ">
<div class="float-start">
<h5 class="mt-3 mb-0">Argon Configurator</h5>
<p>See our dashboard options.</p>
</div>
<div class="float-end mt-4">
<button class="btn btn-link text-dark p-0 fixed-plugin-close-button">
<i class="fa fa-close"></i>
</button>
</div>
<!-- End Toggle Button -->
</div>
<hr class="horizontal dark my-1">
<div class="card-body pt-sm-3 pt-0 overflow-auto">
<!-- Sidebar Backgrounds -->
<div>
<h6 class="mb-0">Sidebar Colors</h6>
</div>
<a href="javascript:void(0)" class="switch-trigger background-color">
<div class="badge-colors my-2 text-start">
<span class="badge filter bg-gradient-primary active" data-color="primary" onclick="sidebarColor(this)"></span>
<span class="badge filter bg-gradient-dark" data-color="dark" onclick="sidebarColor(this)"></span>
<span class="badge filter bg-gradient-info" data-color="info" onclick="sidebarColor(this)"></span>
<span class="badge filter bg-gradient-success" data-color="success" onclick="sidebarColor(this)"></span>
<span class="badge filter bg-gradient-warning" data-color="warning" onclick="sidebarColor(this)"></span>
<span class="badge filter bg-gradient-danger" data-color="danger" onclick="sidebarColor(this)"></span>
</div>
</a>
<!-- Sidenav Type -->
<div class="mt-3">
<h6 class="mb-0">Sidenav Type</h6>
<p class="text-sm">Choose between 2 different sidenav types.</p>
</div>
<div class="d-flex">
<button class="btn bg-gradient-primary w-100 px-3 mb-2 active me-2" data-class="bg-white" onclick="sidebarType(this)">White</button>
<button class="btn bg-gradient-primary w-100 px-3 mb-2" data-class="bg-default" onclick="sidebarType(this)">Dark</button>
</div>
<p class="text-sm d-xl-none d-block mt-2">You can change the sidenav type just on desktop view.</p>
<!-- Navbar Fixed -->
<div class="d-flex my-3">
<h6 class="mb-0">Navbar Fixed</h6>
<div class="form-check form-switch ps-0 ms-auto my-auto">
<input class="form-check-input mt-1 ms-auto" type="checkbox" id="navbarFixed" onclick="navbarFixed(this)">
</div>
</div>
<hr class="horizontal dark my-sm-4">
<div class="mt-2 mb-5 d-flex">
<h6 class="mb-0">Light / Dark</h6>
<div class="form-check form-switch ps-0 ms-auto my-auto">
<input class="form-check-input mt-1 ms-auto" type="checkbox" id="dark-version" onclick="darkMode(this)">
</div>
</div>
</div>
</div>
</div>
<!-- Core JS Files -->
<script src="/static/js/core/popper.min.js"></script>
<script src="/static/js/core/bootstrap.min.js"></script>
<script src="/static/js/plugins/perfect-scrollbar.min.js"></script>
<script src="/static/js/plugins/smooth-scrollbar.min.js"></script>
<script src="/static/js/plugins/chartjs.min.js"></script>
<script>
var win = navigator.platform.indexOf('Win') > -1;
if (win && document.querySelector('#sidenav-scrollbar')) {
var options = {
damping: '0.5'
}
Scrollbar.init(document.querySelector('#sidenav-scrollbar'), options);
}
</script>
<!-- Github buttons -->
<script async defer src="https://buttons.github.io/buttons.js"></script>
<!-- Control Center for Soft Dashboard: parallax effects, scripts for the example pages etc -->
<script src="/static/js/argon-dashboard.min.js?v=2.0.4"></script>
</body>
</html>
これは、Argon dashboardのindex.htmlから次の修正を加えたものです。
基本的にこれをベースにして、htmlを生成しURLに対して返答します。ですので、全ページに共通するサイドバーの要素をここで実装しました。
各種htmlファイルの実装
dashboard.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">Dashboard</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Dashboard</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
ちなみに、以下、novel_list.html、save.html、analysis.html、recommend.html、settings_general.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">Novel List</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
{% 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">Save</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Save</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
{% 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">Analysis</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Analysis</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
{% 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">Recommend</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Recommend</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
{% 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">Setting General</li>
</ol>
<h6 class="font-weight-bolder text-white mb-0">Setting General</h6>
</nav>
</div>
</nav>
</main>
{% endblock %}
実行
各プログラムを配置したら、flaskアプリを起動させます。
デフォルトではFLASK_APP環境変数を参照して、実行されます。今回は、DockerfileにENV FLASK_APP=flask_app
と書いておいたので、flask_appディレクトリ(__init__.py)をデフォルトで参照します。
プロジェクトディレクトリで以下のコマンドを実行すると、flaskアプリが起動します。
vscode@novel_management_service-container:/work$ flask run
正常に起動すれば、アクセスするURLが書かれたログが流れます。
* Serving Flask app 'flask_app'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 630-684-963
開発サーバーだから注意しろみたいな警告が出ていますが、個人で使う分には問題ないので無視します。
ブラウザから「http://127.0.0.1:5000」へアクセスして、Web UIを確かめます。
ちゃんとWebのダッシュボードが表示されました。
サイドバーの要素をクリックして、ページが変わることを確認しておきます。
![](https://emoclework.jp/wp-content/uploads/2024/07/image-1.png)
ページの読み込みエラーなどが無いことを確認したら、これでFlaskでの基本的なWeb UIの実装は終了です。あとは、各ページとデータベース、およびそれに伴う処理を追加していけば、Webアプリが作成できます。
Section 04-01では、データベースの作成と小説リストのページを作っていきます。