【初心者向け】軽量! Flask と Docker コンテナ で 日記アプリを開発してWebアプリの基本を身につけよう!

記事の概要

この記事では、PythonのFlaskフレームワークを使って、Dockerコンテナで動作するWebアプリの開発環境の構築から開発、実行までの流れを紹介します。

この記事を参考に、Webアプリ開発のスタートの一助となれば幸いです。


今回の記事の対象者

この記事は次のような方に向けています:

  • Pythonで簡単にWebアプリを開発したい方
  • 基本機能を備えたWebアプリの実装イメージを得たい方
  • Dockerコンテナを使ってサーバー(VPSやクラウド環境)を環境を統一したい方

はじめに

普段は、JakartaEEやDjango使ってWebアプリやバックエンドのWebAPIの開発を行っていますが、もっと手軽なFlaskを使ってアプリが作ってみたくなりました。

Flaskは、WebアプリをつくるためのPythonのライブラリです。

Flaskはシンプルかつ軽量なフレームワークで、小規模から中規模のWebアプリケーションの開発に適しています。必要な機能をプラグイン形式で追加することができるため、プロジェクトの要件に応じたカスタマイズが容易です。ただし、Djangoと比較すると、標準で提供される機能が少ないため、手作業での設定や追加が多くなりがちです。

Flaskについての詳細は、公式サイトをご覧下さい。

本記事での主な学習ポイントを示します。

  • Flaskの基本的なルーティングとテンプレートレンダリングの理解
  • ユーザー認証とセッション管理の実装
  • データベースのCRUD操作とエントリー管理

目次


コンセプト

まずは、本記事で構築する開発環境やアプリのコンセプトを紹介していきます。

  • Flaskプロジェクト※: Flaskフレームワークを使ったディレクトリ構造やファイルの役割を理解し、効率的にプロジェクトを管理します。※アプリを開発するためのファイルやフォルダの集まり。
  • Webアプリ開発: Flaskを使ってWebアプリを作成し、Webブラウザからアプリを利用します。
  • ビルドと依存関係管理: Poetryを使ってプロジェクトのライブラリと依存関係を管理します。
  • コンテナ技術: DockerコンテナとDocker Composeを使って、アプリをコンテナ化し、開発および本番環境の差異を無くします。

開発するWebアプリの概要

  • デジタルジャーナルアプリの具体的な仕様を以下に示します。
  • このアプリは、ユーザーが日々の出来事や考えを記録し、必要に応じて他のユーザーと共有できるシンプルなオンライン日記システムです。
  • デジタルジャーナルアプリの機能概要は以下の通りです。以下に、login機能を含めたデジタルジャーナルアプリの機能一覧を表形式で示します。
機能概要URL
ホームページの表示/
新しい日記エントリーの作成/create
日記エントリーの一覧表示/entries
日記エントリーの詳細表示/entry/int:entry_id
日記エントリーの編集/edit/int:entry_id
日記エントリーの削除/delete/int:entry_id
ユーザーのログイン/login
ユーザーのログアウト/logout

ソフトウェア・ハードウェア

必要なツール、ライブラリ、端末は以下の通りです。

開発ツール

以下、開発ツールとその公式サイトの一覧です。今回利用する開発ツールが多いので、公式サイトを参照して事前に導入を実施しておくと後々作業が楽になります。

ツール名用途
PyCharm Community 2024.2開発全般
Poetry 1.8.3Pythonパッケージ管理
Docker Desktop 4.33Webアプリ開発

Docker Desktopのインストールは、以下の記事を参考にしてください。

ライブラリ

以下は、ニュースアグリゲーターAPIを構成する主要なソフトウェアとその解説です。導入は、記事本編の中で解説します。

ライブラリ説明
Python 3.12汎用プログラミングのための高水準プログラミング言語。
flask 3.0.3軽量なWSGIウェブアプリケーションフレームワーク。
flask-sqlalchemy 3.1.1FlaskにSQLAlchemy ORMのサポートを追加する拡張機能。
psycopg2-binary 2.9.9Python用のPostgreSQLデータベースアダプタ。(macOSでビルド出来ないため、バイナリ版を利用)
flask-migrate 4.0.7FlaskアプリケーションのためのSQLAlchemyデータベースのマイグレーションを管理する。
flask-login 0.6.3Flaskのユーザーセッションと認証を管理するための拡張機能。
python-dotenv 1.0.1.envファイルからキーと値のペアを読み込み、環境変数として設定する。

端末

以下、今回の環境を構築する対象の端末スペックです。

項目詳細
ハードウェアApple Silicon M3, RAM 24GB
OSmacOS Sonoma 14.5

本記事で紹介するソフトウェアおよびツールは、筆者の個人的な使用経験に基づくものであり、公式のサポート外の設定や使用方法を含む場合があります。利用に際しては、公式サイトの指示およびガイドラインを参照し、自己責任で行ってください。


プロジェクトの準備

プロジェクトディレクトリ構造

ディレクトリ構造(最終イメージ)を以下に示します。

digital-journal/
├── djapp/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── templates/
│   │   ├── layout.html
│   │   ├── index.html
│   │   ├── login.html
│   │   ├── create_entry.html
│   │   ├── list_entries.html
│   │   ├── view_entry.html
│   │   └── edit_entry.html
│   └── static/
│       └── styles.css
├── migrations/
├── .env
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml
├── poetry.lock
└── app.py

主な、ディレクトリの概要を以下に示します。

ディレクトリ名説明
djapp/アプリケーションのコア部分が入るフォルダです。プログラムのロジックやデータベースモデル、画面表示のテンプレートが含まれます。
templates/HTMLファイルが入るフォルダです。ユーザーが見る画面を定義します。
static/CSSファイルなどの静的ファイルが入るフォルダです。

Poetryのインストール

Pythonライブラリ管理ツールをインストールします。

Poetryがインストールされていない場合は、以下のコマンドでインストールします。

curl -sSL <https://install.python-poetry.org> | python3 -

Poertyのパスを環境変数に追加します。

export PATH="/Users/xxxxxxx/.local/bin:$PATH"

Docker Desktopのインストールは以下の記事を参考にしてください。


プロジェクトのセットアップ

新しいプロジェクトディレクトリ(news_aggregator)を作成します。このディレクトリにすべてのコードや設定ファイルが格納されます。

mkdir digital-journal
cd digital-journal

プロジェクトディレクトリ(news_aggregator)で以下のコマンドを実行してPoetryプロジェクトを初期化します。

poetry init --no-interaction

–no-interactionフラグを使うことで、対話的なセットアップをスキップできます。

初期化が終了すると、pyproject.tomlファイルが生成されます。

pyproject.toml

[tool.poetry]
name = "digital-journal"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
[build-system]
requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"

必要なライブラリのインストール

アプリ開発に必要なライブラリをPoetryを使って追加します。

今回は、FlaskやSQLAlchemyなどを使うので、以下のコマンドを実行します。

poetry add flask flask-sqlalchemy psycopg2-binary flask-migrate python-dotenv flask-login

ライブラリの追加が完了するとpoetry.lockファイルが生成されます。

pyproject.tomlファイルに依存関係が追加されます。

poetry.lockには、追加されたライブラリのバージョン管理情報が追加されます。

以下は、コマンドを実行した際に出力されるメッセージのサンプルです。指定したライブラリの依存関係を解決しつつ、各ライブラリの構成とバージョンが確定されているのが分かります。

(base) xxxxx@xxxxx digital-journal % poetry add flask flask-sqlalchemy psycopg2-binary flask-migrate python-dotenv flask-login
Creating virtualenv digital-journal-TrG25_U4-py3.12 in /Users/xxxxx/Library/Caches/pypoetry/virtualenvs
Using version ^3.0.3 for flask
Using version ^3.1.1 for flask-sqlalchemy
Using version ^2.9.9 for psycopg2-binary
Using version ^4.0.7 for flask-migrate
Using version ^1.0.1 for python-dotenv
Using version ^0.6.3 for flask-login

Updating dependencies
Resolving dependencies... (1.1s)

Package operations: 16 installs, 0 updates, 0 removals

  - Installing markupsafe (2.1.5)
  - Installing blinker (1.8.2)
  - Installing click (8.1.7)
  - Installing itsdangerous (2.2.0)
  - Installing jinja2 (3.1.4)
  - Installing typing-extensions (4.12.2)
  - Installing werkzeug (3.0.3)
  - Installing flask (3.0.3)
  - Installing mako (1.3.5)
  - Installing sqlalchemy (2.0.32)
  - Installing alembic (1.13.2)
  - Installing flask-sqlalchemy (3.1.1)
  - Installing flask-login (0.6.3)
  - Installing flask-migrate (4.0.7)
  - Installing psycopg2-binary (2.9.9)
  - Installing python-dotenv (1.0.1)

Writing lock file
(base) siwamin@orcusmba digital-journal % 

pyproject.toml

[tool.poetry]
name = "digital-journal"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12" 
flask = "^3.0.3" 
flask-sqlalchemy = "^3.1.1" 
psycopg2-binary = "^2.9.9" 
flask-migrate = "^4.0.7"
python-dotenv = "^1.0.1" 
flask-login = "^0.6.3"
[build-system]
requires = ["poetry-core"] build-backend = "poetry.core.masonry.api"

poetry.lock

生成されたpoetry.lockファイルの一部を以下に抜粋します。かなり詳細な管理情報が記載されています。このファイルを用いることで、どの環境でも厳密に同じライブラリ構成で環境を再現することが出来ます。

# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.

[[package]]
name = "alembic"
version = "1.13.2"
description = "A database migration tool for SQLAlchemy."
optional = false
python-versions = ">=3.8"
files = [
    {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"},
    {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"},
]
[package.dependencies]
Mako = "*" SQLAlchemy = ">=1.3.0" typing-extensions = ">=4"
[package.extras]
tz = ["backports.zoneinfo"]

デジタルジャーナルアプリの開発

アプリの開発を順次行っていきます。開発は、設定ファイル、アプリの基礎となる機能、UI、デーブル定義、データ作成の順で進めていきます。ファイル毎に解説を記載していますので、中身を確認しつつ実装を勧めてください。

環境変数

.env

.envファイルを作成し、機密情報(データベース接続情報やシークレットキー)を外部化します。

FLASK_ENV=development
FLASK_SECRET_KEY=supersecretkey
FLASK_APP=app.py

POSTGRES_USER=my_postgres_user
POSTGRES_PASSWORD=my_postgres_password
POSTGRES_DB=journal_db
POSTGRES_HOST=db
POSTGRES_PORT=5432

.envファイルは環境ごとに異なる設定を保存するために使います。ここに保存された情報は、他のプログラムからも安全に利用できるようになります。


Flaskアプリケーションの初期設定

  • 手順: djapp/init.pyにFlaskアプリケーションの基本設定を行います。
  • 説明: create_app関数はFlaskアプリケーションのインスタンスを作成し、設定を行います。このファイルでは、データベースやログイン管理機能を初期化し、必要な設定を行います。
  • コード:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
import os
from dotenv import load_dotenv

# 環境変数の読み込み
load_dotenv()

# Flask-SQLAlchemy のインスタンスを作成
db = SQLAlchemy()

# Flask-Migrate のインスタンスを作成
migrate = Migrate()

# Flask-Login のインスタンスを作成
login_manager = LoginManager()

# ログインページのビュー名を設定
login_manager.login_view = 'login'

def create_app():
    # Flask アプリケーションのインスタンスを作成
    app = Flask(__name__)

    # アプリケーション設定
    app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY')

    # データベース接続情報
    postgres_user = os.getenv('POSTGRES_USER')
    postgres_password = os.getenv('POSTGRES_PASSWORD')
    postgres_db = os.getenv('POSTGRES_DB')
    postgres_host = os.getenv('POSTGRES_HOST')
    postgres_port = os.getenv('POSTGRES_PORT')
    app.config[
        'SQLALCHEMY_DATABASE_URI'] = f'postgresql://{postgres_user}:{postgres_password}@{postgres_host}:{postgres_port}/{postgres_db}'

    # SQLAlchemy の設定
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    # アプリケーション設定の反映
    app.app_context().push()

    # データベースとマイグレーションの初期化
    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)

    # ルートとモデルのインポート
    from djapp.models import User
    from djapp import routes

    # ユーザーロード関数を定義
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))

    return app

データベースモデルの作成

  • 手順: djapp/models.pyにデータベースのモデル(テーブルの構造)を定義します。
  • 説明: UserとJournalEntryという2つのデータベースモデルを定義します。Userはユーザー情報を、JournalEntryは日記エントリーの情報を格納します。
  • コード:
from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from djapp import db

# デフォルトのタイムスタンプを現在のUTC時間に設定
DEFAULT_TIMESTAMP = datetime.utcnow

# タイムスタンプカラムを作成する関数
def create_timestamp_column():
    return db.Column(db.DateTime, default=DEFAULT_TIMESTAMP)

# ユーザーモデルの定義
class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)  # 主キーとしてのIDカラム
    username = db.Column(db.String(150), nullable=False, unique=True)  # ユーザー名カラム
    email = db.Column(db.String(150), nullable=False, unique=True)  # メールアドレスカラム
    password_hash = db.Column(db.String(300), nullable=False)  # パスワードのハッシュを保存するカラム

    # パスワードを設定するメソッド
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    # パスワードを検証するメソッド
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

# ジャーナルエントリーモデルの定義
class JournalEntry(db.Model):
    id = db.Column(db.Integer, primary_key=True)  # 主キーとしてのIDカラム
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)  # 外部キーとしてのユーザーIDカラム
    title = db.Column(db.String(200), nullable=False)  # エントリータイトルカラム
    content = db.Column(db.Text, nullable=False)  # エントリー内容カラム
    created_at = create_timestamp_column()  # 作成日時カラム
    updated_at = create_timestamp_column()  # 更新日時カラム
    is_private = db.Column(db.Boolean, default=True)  # 非公開フラグカラム


ルーティングとビューの設定

  • 手順: djapp/routes.pyでルーティング(URLと機能の紐付け)を行います。
  • 説明: ここでは、アプリケーションの各ページ(ホーム、エントリー作成、エントリー一覧、エントリー表示、エントリー編集、エントリー削除)に対応するルートとその処理を定義しています。ユーザーのアクションに対してサーバー側で適切に処理が行われます。
  • コード:
from flask import current_app as app, render_template, redirect, url_for, request, flash
from djapp import db
from djapp.models import JournalEntry, User
from flask_login import login_user, logout_user, current_user, login_required

# ホームページのルート
@app.route('/')
def index():
    return render_template('index.html')  # index.html テンプレートをレンダリングして返す

# 新しい日記エントリーの作成
@app.route('/create', methods=['GET', 'POST'])
@login_required
def create_entry():
    if request.method == 'POST':
        title = request.form.get('title')  # フォームからタイトルを取得
        content = request.form.get('content')  # フォームから内容を取得
        is_private = 'is_private' in request.form  # フォームに非公開オプションが含まれているかを判断
        new_entry = JournalEntry(
            title=title,
            content=content,
            user_id=current_user.id,  # 現在のユーザーIDをセット
            is_private=is_private
        )
        db.session.add(new_entry)  # 新エントリーをセッションに追加
        db.session.commit()  # データベースにコミット
        flash('エントリーが正常に作成されました!', 'success')  # 成功メッセージを表示
        return redirect(url_for('list_entries'))  # エントリー一覧ページにリダイレクト
    return render_template('create_entry.html')  # create_entry.html テンプレートをレンダリングして返す

# エントリーの一覧表示
@app.route('/entries')
@login_required
def list_entries():
    entries = JournalEntry.query.filter_by(user_id=current_user.id).all()  # 現在のユーザーのエントリーを取得
    return render_template('list_entries.html', entries=entries)  # list_entries.html テンプレートをレンダリングして返す

# エントリーの詳細表示
@app.route('/entry/<int:entry_id>')
@login_required
def view_entry(entry_id):
    entry = JournalEntry.query.get_or_404(entry_id)  # エントリーをIDで取得、なければ404エラー
    if entry.user_id != current_user.id and entry.is_private:
        flash('このエントリーを表示する権限がありません。', 'error')  # 権限エラーメッセージを表示
        return redirect(url_for('list_entries'))  # エントリー一覧ページにリダイレクト
    return render_template('view_entry.html', entry=entry)  # view_entry.html テンプレートをレンダリングして返す

# エントリーの編集
@app.route('/edit/<int:entry_id>', methods=['GET', 'POST'])
@login_required
def edit_entry(entry_id):
    entry = JournalEntry.query.get_or_404(entry_id)  # エントリーをIDで取得、なければ404エラー
    if entry.user_id != current_user.id:
        flash('このエントリーを編集する権限がありません。', 'error')  # 権限エラーメッセージを表示
        return redirect(url_for('list_entries'))  # エントリー一覧ページにリダイレクト
    if request.method == 'POST':
        entry.title = request.form.get('title')  # フォームから新しいタイトルを取得
        entry.content = request.form.get('content')  # フォームから新しい内容を取得
        entry.is_private = 'is_private' in request.form  # フォームに非公開オプションが含まれているかを判断
        db.session.commit()  # データベースにコミット
        flash('エントリーが正常に更新されました!', 'success')  # 成功メッセージを表示
        return redirect(url_for('view_entry', entry_id=entry.id))  # 更新されたエントリーの詳細ページにリダイレクト
    return render_template('edit_entry.html', entry=entry)  # edit_entry.html テンプレートをレンダリングして返す

# エントリーの削除
@app.route('/delete/<int:entry_id>', methods=['POST'])
@login_required
def delete_entry(entry_id):
    entry = JournalEntry.query.get_or_404(entry_id)  # エントリーをIDで取得、なければ404エラー
    if entry.user_id != current_user.id:
        flash('このエントリーを削除する権限がありません。', 'error')  # 権限エラーメッセージを表示
        return redirect(url_for('list_entries'))  # エントリー一覧ページにリダイレクト
    db.session.delete(entry)  # エントリーをセッションから削除
    db.session.commit()  # データベースにコミット
    flash('エントリーが削除されました。', 'success')  # 成功メッセージを表示
    return redirect(url_for('list_entries'))  # エントリー一覧ページにリダイレクト

# ログイン機能
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        email = request.form.get('email')  # フォームからメールを取得
        password = request.form.get('password')  # フォームからパスワードを取得
        user = User.query.filter_by(email=email).first()  # メールアドレスでユーザーを検索
        if user and user.check_password(password):
            login_user(user)  # ユーザーをログイン
            flash('Logged in successfully.', 'success')  # 成功メッセージを表示
            return redirect(url_for('index'))  # ホームページにリダイレクト
        else:
            flash('Invalid email or password.', 'danger')  # エラーメッセージを表示
    return render_template('login.html')  # login.html テンプレートをレンダリングして返す

# ログアウト機能
@app.route('/logout')
@login_required
def logout():
    logout_user()  # ユーザーをログアウト
    flash('You have been logged out.', 'success')  # 成功メッセージを表示
    return redirect(url_for('index'))  # ホームページにリダイレクト


UIの構築

HTMLテンプレートの作成

  • 手順: djapp/templates/にHTMLファイルを作成し、各ページのUIを設定します。

layout.html: すべてのページで共通のレイアウトを提供します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <header>
        <nav>
            <a href="{{ url_for('login') }}">ログイン</a>
            <a href="{{ url_for('index') }}">ホーム</a>
            <a href="{{ url_for('create_entry') }}">新しいエントリー</a>
            <a href="{{ url_for('logout') }}">ログアウト</a>
        </nav>
    </header>
    <main>
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <ul class="flashes">
                    {% for category, message in messages %}
                        <li class="{{ category }}">{{ message }}</li>
                    {% endfor %}
                </ul>
            {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </main>
    <footer>
        <p>&copy; 2024 デジタルジャーナル</p>
    </footer>
</body>
</html>

index.html: ホームページのコンテンツを表示します。

{% extends "layout.html" %}

{% block content %}
<h1>デジタルジャーナルへようこそ</h1>
<p>新しい日記エントリーを作成するか、既存のエントリーを表示してください。</p>
<a href="{{ url_for('create_entry') }}" class="button">新しいエントリーを作成</a>
{% endblock %}

login.html: ログイン画面を表示します。

<form method="POST" action="{{ url_for('login') }}">
    <div>
        <label for="email">Email</label>
        <input type="email" name="email" id="email" required>
    </div>
    <div>
        <label for="password">Password</label>
        <input type="password" name="password" id="password" required>
    </div>
    <button type="submit">ログイン</button>
</form>

create_entry.html: 新しい日記エントリーを作成するためのフォームを表示します。

{% extends "layout.html" %}

{% block content %}
<h1>新しいエントリーを作成</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" id="title" name="title" required>
    
    <label for="content">本文</label>
    <textarea id="content" name="content" required></textarea>

    <label for="is_private">プライベート</label>
    <input type="checkbox" id="is_private" name="is_private" checked>
    
    <button type="submit">保存</button>
</form>
{% endblock %}

list_entries.html: ユーザーのエントリー一覧を表示します。

{% extends "layout.html" %}

{% block content %}
<h1>マイエントリー</h1>
<ul>
    {% for entry in entries %}
        <li>
            <h2>{{ entry.title }}</h2>
            <p>{{ entry.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
            <a href="{{ url_for('view_entry', entry_id=entry.id) }}">表示</a>
            <a href="{{ url_for('edit_entry', entry_id=entry.id) }}">編集</a>
            <form method="POST" action="{{ url_for('delete_entry', entry_id=entry.id) }}">
                <button type="submit">削除</button>
            </form>
        </li>
    {% endfor %}
</ul>
{% endblock %}

view_entry.html: 特定のエントリーを表示します。

{% extends "layout.html" %}

{% block content %}
<h1>{{ entry.title }}</h1>
<p>{{ entry.created_at.strftime('%Y-%m-%d %H:%M') }}</p>
<div>{{ entry.content }}</div>
<a href="{{ url_for('edit_entry', entry_id=entry.id) }}">編集</a>
{% endblock %}

edit_entry.html: 既存のエントリーを編集するためのフォームを表示します。

{% extends "layout.html" %}

{% block content %}
<h1>エントリーを編集</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" id="title" name="title" value="{{ entry.title }}" required>

    <label for="content">本文</label>
    <textarea id="content" name="content" required>{{ entry.content }}</textarea>

    <label for="is_private">プライベート</label>
    <input type="checkbox" id="is_private" name="is_private"{% if entry.is_private %}checked{% endif %}>

    <button type="submit">保存</button>
</form>
{% endblock %}

CSSファイルの作成

  • 手順: djapp/static/にCSSファイルを作成し、アプリケーションのスタイルを設定します。
  • 説明: このファイルでは、アプリ全体のスタイルを定義しています。背景色、ナビゲーションメニュー、フォーム、ボタン、フラッシュメッセージのデザインが含まれています。

styles.css:

body {
  font-family: Arial, sans-serif; /* フォントはArialかサンセリフ体 */
  margin: 0; /* 余白をゼロに設定 */
  padding: 0; /* パディングをゼロに設定 */
  background-color: #f4f4f4; /* 背景色を薄いグレーに設定 */
}
header {
  background-color: #333; /* ヘッダーの背景色をダークグレーに設定 */
  color: #fff; /* ヘッダーの文字色を白に設定 */
  padding: 10px 0; /* 上下のパディングを10pxに設定 */
  text-align: center; /* テキストを中央揃えに設定 */
}
nav a {
  color: #fff; /* ナビゲーションリンクの文字色を白に設定 */
  margin: 0 15px; /* 左右の余白を15pxに設定 */
  text-decoration: none; /* テキストの下線を無しに設定 */
}
nav a:hover {
  text-decoration: underline; /* ホバー時に下線を表示 */
}
main {
  padding: 20px; /* 全体のパディングを20pxに設定 */
  max-width: 800px; /* コンテンツの最大幅を800pxに設定 */
  margin: auto; /* コンテンツを中央に配置 */
  background-color: #fff; /* 背景色を白に設定 */
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); /* 薄いシャドウを追加 */
}
h1, h2 {
  color: #333; /* 見出しの文字色をダークグレーに設定 */
}
form {
  display: flex; /* フォームをフレックスコンテナに設定 */
  flex-direction: column; /* 垂直方向に配置 */
}
label {
  margin: 10px 0 5px; /* 上に10px、下に5pxの余白を設定 */
}
input[type="text"], textarea {
  padding: 10px; /* 全体のパディングを10pxに設定 */
  border: 1px solid #ccc; /* 薄いグレーのボーダーを設定 */
  border-radius: 5px; /* 角を丸く設定 */
  margin-bottom: 10px; /* 下に10pxの余白を設定 */
}
button {
  padding: 10px; /* 全体のパディングを10pxに設定 */
  background-color: #333; /* ボタンの背景色をダークグレーに設定 */
  color: #fff; /* ボタンの文字色を白に設定 */
  border: none; /* ボーダーを無しに設定 */
  border-radius: 5px; /* 角を丸く設定 */
  cursor: pointer; /* カーソルをポインタに設定 */
}
button:hover {
  background-color: #555; /* ホバー時に背景色を少し明るく設定 */
}
.flashes {
  list-style-type: none; /* リストのスタイルを無しに設定 */
  padding: 0; /* パディングをゼロに設定 */
}
.flashes li {
  padding: 10px; /* 全体のパディングを10pxに設定 */
  margin-bottom: 10px; /* 下に10pxの余白を設定 */
  border-radius: 5px; /* 角を丸く設定 */
}
.flashes .success {
  background-color: #4caf50; /* 成功メッセージの背景色を緑に設定 */
  color: #fff; /* 成功メッセージの文字色を白に設定 */
}
.flashes .error {
  background-color: #f44336; /* エラーメッセージの背景色を赤に設定 */
  color: #fff; /* エラーメッセージの文字色を白に設定 */
}

Docker環境の構築

Dockerfileの作成

  • 手順: Dockerfileを作成して、アプリケーションをDockerコンテナで実行する設定を行います。
  • 説明: これは、Dockerイメージを作成するためのファイルです。Python環境を設定し、アプリケーションを実行する準備を行います。
  • コード:
FROM python:3.12-slim

# 作業ディレクトリの設定
WORKDIR /app

# プロジェクトファイルのコピー
COPY pyproject.toml poetry.lock /app/

# Poetryのインストール
RUN pip install poetry

# 依存関係のインストール
RUN poetry install --no-dev

# アプリケーションのソースコードをコピー
COPY . /app

# Flaskアプリケーションを起動
CMD ["poetry", "run", "flask", "run", "--host=0.0.0.0"]

docker-compose.ymlの作成

  • 手順: docker-compose.ymlを作成して、アプリケーションとデータベースの連携を管理します。
  • 説明: このファイルで、アプリケーション用のウェブサーバとデータベースをDockerコンテナとして設定し、両者が連携できるようにします。
  • コード:
services:
  web:
    build: .
    ports:
      - "5001:5000"
    volumes:
      - .:/djapp
    env_file:
      - .env
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: journal_db
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

データベースのマイグレーション

マイグレーションは、データベーススキーマの変更(テーブルの作成、変更、削除など)を管理するためのプロセスです。

データベースのテーブルを作成するためには、Flask-Migrateを使用してデータベースのマイグレーションを行います。

  1. マイグレーションの初期化:
    • flask db initでプロジェクトにマイグレーションの設定を追加します。
  2. マイグレーションスクリプトの作成:
    • flask db migrateでデータベースモデルの変更を反映するスクリプトを作成します。
  3. データベースへの適用:
    • flask db upgradeでマイグレーションスクリプトをデータベースに適用し、テーブルを作成します。

事前に、Dockerコンテナ上のPostgreSQLにアクセス出来るように、macOS上でホスト名「db」を/etc/hostsファイルに登録しておいてください。

127.0.0.1       db

以下に、データベースのテーブルを作成するための手順を説明します。


マイグレーションの初期化

まず、プロジェクトでマイグレーションの初期化を行います。このステップは一度だけ実行する必要があります。

  • 説明: このコマンドにより、migrations/ディレクトリが作成され、マイグレーションの管理に必要なファイルが生成されます。
  • コマンド:
flask db init

実行結果のサンプルを以下に示します。

(digital-journal-py3.12) (base) xxxxx@xxxxx digital-journal % rm -fr migrations
(digital-journal-py3.12) (base) xxxxx@xxxxx digital-journal % flask db init           
  Creating directory '/Users/xxxxx/test-pj/digital-journal/migrations' ...  done
  Creating directory '/Users/xxxxx/test-pj/digital-journal/migrations/versions' ...  done
  Generating /Users/xxxxx/test-pj/digital-journal/migrations/script.py.mako ...  done
  Generating /Users/xxxxx/test-pj/digital-journal/migrations/env.py ...  done
  Generating /Users/xxxxx/test-pj/digital-journal/migrations/README ...  done
  Generating /Users/xxxxx/test-pj/digital-journal/migrations/alembic.ini ...  done
  Please edit configuration/connection/logging settings in '/Users/xxxxx/test-pj/digital-journal/migrations/alembic.ini' before proceeding.


マイグレーションの作成

次に、データベースモデルに基づいてマイグレーションスクリプトを作成します。

  • 説明: このコマンドは、現在のデータベースの状態とモデルの状態を比較し、変更を反映するマイグレーションスクリプトを生成します。-mオプションを使用して、マイグレーションに関するメッセージを付け加えることができます(例: “Initial migration.”)。
  • コマンド:
flask db migrate -m "Initial migration."

実行結果のサンプルを以下に示します。

(digital-journal-py3.12) (base) xxxxx@xxxxx digital-journal % poetry run flask db migrate -m "Initial migration."
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added table 'journal_entry'
  Generating /Users/xxxxx/test-pj/digital-journal/migrations/versions/a3e82a932b82_initial_migration.py ...  done

データベースへの反映(アップグレード)

作成したマイグレーションスクリプトを使用して、実際にデータベースにテーブルを作成します。

  • 説明: このコマンドを実行すると、マイグレーションスクリプトがデータベースに適用され、テーブルが作成されます。
  • コマンド:
flask db upgrade

実行結果のサンプルを以下に示します。

(digital-journal-py3.12) (base) xxxxx@xxxxx digital-journal % flask db upgrade                                   
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> a3e82a932b82, Initial migration.

データベースの確認

データベースにテーブルが正しく作成されたことを確認するには、PostgreSQLデータベースに接続してテーブルが存在するか確認します。

データベースへの接続と確認

  • 説明: 上記のコマンドで、PostgreSQLのコンテナに接続し、データベースを開きます。<POSTGRES_USER>は、あなたが設定したPostgreSQLのユーザー名に置き換えてください。
  • コマンド:
docker compose exec db psql -U <POSTGRES_USER> -d journal_db

テーブル一覧を確認

  • 説明: \dtコマンドを使うと、現在のデータベースに存在するテーブルの一覧が表示されます。
  • コマンド:
\\dt

実行結果のサンプルを以下に示します。

                  List of relations
 Schema |      Name       | Type  |      Owner       
--------+-----------------+-------+------------------
 public | alembic_version | table | my_postgres_user
 public | journal_entry   | table | my_postgres_user
 public | user            | table | my_postgres_user

pgAdmin4でも確認してみます。

pgAdmin4でマイグレーションされたテーブルが存在するか確認

新しいユーザーの作成:

デジタルジャーナルアプリの利用を始める前に、ユーザーの登録を以下のPythonコードを使って行います。

まずは、Flaskシェルを起動します。

flask shell

以下の、Pythonコードを実行して新しいユーザーを作成してください。username, email, passwordは任意の値を設定してください。

from app import db
from app.models import User

user = User(username="testuser", email="a@a")
user.set_password("test")
db.session.add(user)
db.session.commit()

Docker Composeの実行

これで、デジタルジャーナルアプリを起動する準備が終わりました。

以下のコマンドでアプリケーションとデータベースをDockerコンテナで起動します。

  • 手順: Docker Composeを使ってアプリケーションを起動します。
  • コマンド:
docker-compose up --build

動作イメージ

ブラウザでhttp://localhost:5000にアクセスすると、アプリケーションが動作していることを確認できます。

トップ画面

デジタルジャーナルアプリのトップ画面

ログイン

デジタルジャーナルアプリのログイン画面
デジタルジャーナルアプリのログイン成功画面

新しい日記のエントリー(入力)

デジタルジャーナルアプリのエントリー作成画面

エントリー後(確認)

デジタルジャーナルアプリのエントリー成功後の画面

日記の参照

デジタルジャーナルアプリのエントリー詳細画面

ログアウト


まとめ

今回は、Flaskを使って、日々の出来事や考えを記録できるデジタルジャーナル(オンライン日記)の開発を行いました。

初心者にとっても十分チャレンジしがいのあるプロジェクトだったかと思います。

このアプリケーションを作成することで、Flaskの基本を幅広く学習できたと思います。主な学習ポイントを示します。

  • Flaskの基本的なルーティングとテンプレートレンダリングの理解
  • ユーザー認証とセッション管理の実装
  • データベースのCRUD操作とエントリー管理

最後に、この記事がPythonの学習やFlaskを使ったWebアプリ開発に役立つことを願っています。

これからもFlaskとPyCharm, Dockerコンテナを活用して、より高度なアプリケーション開発に挑戦してみてください!

SNSでもご購読できます。

コメントを残す

*


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。