11-Flask

Flask(Part.21予定)| 【複数アプリケーションの統合(1)】

python| まとめ | 現役エンジニア&プログラミングスクール講師「python」のまとめページです。pythonに関して抑えておきたい知識や文法やにについて記事をまとめています。まとめページの下部には「おすすめの学習書籍」「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中...

目標

  • 複数のアプリケーションを統合できる。

Flaskでアプリケーションを統合する方法

テーブル管理アプリケーションの作成準備

ここからは、これまでの商品管理アプリケーションとは別に、テーブル管理アプリケーションや注文管理アプリケーションを作成します。まずは、テーブル管理アプリケーションに必要なディレクトリやファイルを作成し、商品管理アプリケーションとテーブル管理アプリケーションを統合してひとつのアプリケーションとして扱えるようにします。その後、テーブル管理アプリケーションのプログラミングを進めます。

注文管理アプリケーションはテーブル管理アプリケーションの完成後に行います。

feature-tablessappブランチの作成

Git Bushを立ち上げdevelopブランチから次のコマンドを入力してfeature-tablesappブランチを作成して、ブランチの切り替えを行います。

git checkout -b feature-tablesappコマンド

プロンプトに(feature-tablesapp)と表示されます。テーブル管理アプリケーションはこのブランチで作成していきます。

テーブル管理アプリケーションに必要なディレクトリとファイルの作成

Visual Studio Codeのエクスプローラーから appsディレクトリ に tablesappディレクトリを作成します。

tablesappディレクトリのような追加のアプリケーションを作成する場合、既にあるアプリケーションの名前を変更して作業を行うとリファクタリングがうまくいかず、ルーティングがうまくいかない場合があります。アプリケーションの名前を新しい名前にしたい場合は、名前変更でなく、新規作成を行うのが良いです。

apps/tablesappディレクトリに次のディレクトリとファイルを作成します。※作成するファイルについては、名前や中身について、今後変更することものもあります。

  • static/css/style.css
  • templates/table_base.html
  • tamplates/table_form.html
  • tamplates/table_list.html
  • app.py
  • forms.py
  • models.py

「productsapp」と「ordersapp」の統合作業

ここでは、これまで作成してきた「productsapp」と、新たに準備した「ordersapp」を統合します。そのために、appsディレクトリ直下にcommon/db.pyファイルやmain.pyを作成し、両アプリをひとつのアプリケーションとして動作させるプログラムを実装します。

commonはディレクトリです。

次のディレクトリやファイルをappsディレクトリ直下に作成します。

  • common/db.py
  • main.py
common/db.pyファイルとmain.pyファイルの作成
common/db.pyファイル

common/db.pyファイルに次のプログラムを記述します。アプリケーションの統合では、複数のアプリケーションが同じデータベースを利用することが一般的です。このため、SQLAlchemyで生成するdbインスタンスを、このようなcommon/db.pyファイルで作成して、各アプリケーションからインポートできるようにします。

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
main.pyファイル

この記事で作成するアプリケーションのように、各アプリケーションにapp.py(またはroute.py)を配置してルートを定義している場合、ルーティングの競合が発生し、正常に動作しません。これを避けるために、main.pyを作成し、ふたつのアプリケーションを統合します。なお、main.pyはappsディレクトリに配置します。

main.pyファイルに次のプログラムを記述します。main.pyでは、もともとproductsappディレクトリ内のapp.pyに記述されていたデータベース設定や画像アップロード処理の設定を記述します。そのため、productsappディレクトリにあるapp.pyファイル内で、不要な重複コードを削除し、必要なモジュールのインポートやルート設定を適切に調整する必要があります。

main.pyファイルで最も重要な個所は、各アプリケーションをBlueprintとして登録する部分です。この部分は各アプリケーションのapp.pyファイルで定義されたBlueprintの設定をインポートして行っています。

import os

from flask import Flask

from apps.common.db import db
from apps.productsapp.app import create_products_app
from apps.tablesapp.app import create_tables_app


def create_main_app():
    app = Flask(__name__)
    app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///main.db"
    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.secret_key = "your_secret_key"

    # 画像アップロードの設定
    UPLOAD_FOLDER = os.path.join(os.getcwd(), "apps", "productsapp", "static", "images")
    if not os.path.exists(UPLOAD_FOLDER):
        os.makedirs(UPLOAD_FOLDER)
    app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
    app.config["ALLOWED_EXTENSIONS"] = {"png", "jpg", "jpeg", "gif"}
    app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024  # 最大16MB

    # DB初期化
    db.init_app(app)

    # アプリコンテキスト内でテーブル作成
    with app.app_context():
        db.create_all()

        # 各アプリをBlueprintとして登録
    products_app = create_products_app(app)
    app.register_blueprint(products_app, url_prefix="/products")

    tables_app = create_tables_app(app)
    app.register_blueprint(tables_app, url_prefix="/tables")

    return app


if __name__ == "__main__":
    app = create_main_app()
    app.run(debug=True)
app.pyの編集(productsapp内)

app.pyファイルを次のように編集します。このファイルでは、main.py でアプリケーションを統合するために Blueprint を利用しています。

主な変更点1:url_for 関数でのエンドポイント指定
url_for で指定するエンドポイントは、Blueprint の名前を付加する必要があります。

例:url_for(“category_list”) → url_for(“products.category_list”)

主な変更点2:app.config の利用方法の変更
app.config を直接参照するのではなく、current_app.config を使用します。これは、Blueprint 内で設定を取得するために必要です。

例:app.config[“UPLOAD_FOLDER”] → current_app.config[“UPLOAD_FOLDER”]

主な変更点3:Blueprint オブジェクトに関連付けられたルートの定義

@products_bp.route は、Flask の デコレータ(関数装飾子)です。@ の後に続く products_bp.route は、products_bp という Blueprint オブジェクトに関連付けられたルートを定義します。この部分が重要で、Flask アプリケーション内で URL パスと関数を関連付けるために使用されます。

import os
from itertools import groupby

from flask import (
    Blueprint,
    current_app,
    flash,
    redirect,
    render_template,
    request,
    url_for,
)
from werkzeug.utils import secure_filename

from apps.common.db import db
from apps.productsapp.forms import AddProductForm, CategoryForm
from apps.productsapp.models import Category, Product  # models.pyからインポート

products_bp = Blueprint(
    "products",
    __name__,
    template_folder=os.path.join(os.getcwd(), "apps", "productsapp", "templates"),
    static_folder=os.path.join(os.getcwd(), "apps", "productsapp", "static"),
)


def create_products_app(app):
    """Blueprintを新規作成し、登録する関数"""
    return products_bp  # 返り値としてBlueprintを返す


# アップロードが許可されている拡張子かをチェック
def allowed_file(filename):
    return (
        "." in filename
        and filename.rsplit(".", 1).lower()
        in current_app.config["ALLOWED_EXTENSIONS"]
    )


# ルート: トップページ
@products_bp.route("/")
def index():
    return "Top page"


# カテゴリー一覧を表示
@products_bp.route("/categories")
def category_list():
    categories = Category.query.all()
    return render_template(
        "categories.html", categories=categories, title="カテゴリー一覧"
    )


# カテゴリーを追加
@products_bp.route("/category/add", methods=["GET", "POST"])
def add_category():
    form = CategoryForm()
    if form.validate_on_submit():
        new_category = Category(name=form.name.data)
        db.session.add(new_category)
        db.session.commit()
        flash("カテゴリーを追加しました", "success")
        return redirect(url_for("products.category_list"))
    return render_template(
        "category_form.html", form=form, title="カテゴリー追加フォーム"
    )


# カテゴリーを編集
@products_bp.route("/category/edit/<int:category_id>", methods=["GET", "POST"])
def edit_category(category_id):
    category = Category.query.get_or_404(category_id)
    form = CategoryForm(obj=category)

    if form.validate_on_submit():
        category.name = form.name.data
        db.session.commit()
        flash("カテゴリーを更新しました", "success")
        return redirect(url_for("products.category_list"))

    return render_template("category_form.html", form=form, title="カテゴリー編集")


# カテゴリーを削除
@products_bp.route("/category/delete/<int:category_id>", methods=["POST"])
def delete_category(category_id):
    category = Category.query.get_or_404(category_id)

    # もしカテゴリーに紐づいた商品があればエラーを出す
    if category.products:
        flash("このカテゴリーには商品が登録されているため削除できません", "danger")
        return redirect(url_for("products.category_list"))

    db.session.delete(category)
    db.session.commit()
    flash("カテゴリーを削除しました", "success")
    return redirect(url_for("products.category_list"))


# ルート: 商品一覧ページ
@products_bp.route("/products")
def product_list():
    products = Product.query.all()  # SQLiteから商品データを取得
    # 商品をカテゴリーごとにグループ化
    grouped_products = {}
    for key, group in groupby(
        sorted(products, key=lambda p: p.category.name if p.category else "なし"),
        key=lambda p: p.category.name if p.category else "なし",
    ):
        grouped_products[key] = list(group)

    return render_template(
        "products.html", grouped_products=grouped_products, title="商品一覧"
    )


# ルート: 商品追加ページ
@products_bp.route("/product/add", methods=["GET", "POST"])
def add_product():
    form = AddProductForm()
    form.set_category_choices()

    if form.validate_on_submit():
        # フォームのデータを取得
        name = form.product_name.data
        price = form.product_price.data
        category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files.get("product_image")
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)  # ファイル名を安全に処理
                image_path = os.path.join(current_app.config["UPLOAD_FOLDER"], filename)
                image.save(image_path)  # 画像を保存

                # 画像のパスをデータベースに保存
                image_url = f"images/{filename}"  # アップロードされた画像のURL
            else:
                image_url = "images/noimage.png"
        else:
            image_url = "images/noimage.png"

        # 商品の追加
        new_product = Product(
            name=name, price=price, category_id=category_id, image_url=image_url
        )
        db.session.add(new_product)
        db.session.commit()

        return redirect(url_for("products.product_list"))

    return render_template("product_form.html", form=form, title="商品追加フォーム")


# ルート: 商品を編集
@products_bp.route("/product/edit/<int:product_id>", methods=["GET", "POST"])
def edit_product(product_id):
    product = Product.query.get_or_404(product_id)
    form = AddProductForm(obj=product)
    form.set_category_choices()

    if form.validate_on_submit():
        product.name = form.product_name.data
        product.price = form.product_price.data
        product.category_id = form.category.data

        # 画像のアップロード処理
        if "product_image" in request.files:
            image = request.files["product_image"]
            if image and allowed_file(image.filename):
                filename = secure_filename(image.filename)
                image_path = os.path.join(current_app.config["UPLOAD_FOLDER"], filename)

                # もしすでに画像があれば削除
                if product.image_url and product.image_url != "images/noimage.png":
                    old_image_path = os.path.join(
                        current_app.config["UPLOAD_FOLDER"],
                        os.path.basename(product.image_url),
                    )
                    if os.path.exists(old_image_path):
                        os.remove(old_image_path)

                # 新しい画像を保存
                image.save(image_path)
                product.image_url = f"images/{filename}"  # 新しい画像URLを保存

        db.session.commit()
        flash("商品情報を更新しました", "success")
        return redirect(url_for("products.product_list"))

    return render_template(
        "product_form.html", form=form, title="商品編集", product=product
    )


# ルート: 商品を削除
@products_bp.route("/product/delete/<int:product_id>", methods=["POST"])
def delete_product(product_id):
    product = Product.query.get_or_404(product_id)
    db.session.delete(product)
    db.session.commit()
    flash("商品を削除しました", "success")
    return redirect(url_for("products.product_list"))


# ルート: 商品管理ページ
@products_bp.route("/manage_products")
def manage_products():
    return render_template("manage_products.html", title="商品管理MENU")


if __name__ == "__main__":
    from flask import Flask

    app = Flask(__name__)
    app.register_blueprint(products_bp, url_prefix="/products")

    app.run(debug=True)
models.pyの編集(productsapp内)

models.py を次のように編集します。このファイルでは、common ディレクトリ内の db.py から db インスタンスをインポートして使用します。これにより、アプリケーション全体で統一されたデータベース管理が可能になります。

from apps.common.db import db


# カテゴリーモデル
class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False, unique=True)
    products = db.relationship(
        "Product", backref="category", lazy=True
    )  # 商品とのリレーション


# 商品モデル(テーブル)の定義
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    price = db.Column(db.Integer, nullable=False)
    category_id = db.Column(
        db.Integer, db.ForeignKey("category.id"), nullable=True
    )  # カテゴリID
    image_url = db.Column(db.String(255), nullable=True)  # 画像のURLを保存するカラム

    def __repr__(self):
        return f"<Product {self.id} - {self.name} - {self.price}>"

forms.pyに変更箇所はありません。

base.htmlの編集(productsapp/templates内)

base.html を次のように編集します。テンプレートの主な変更点は、url_for 関数内のエンドポイントの記述方法です。Blueprint を導入したため、各エンドポイントの前に Blueprint の名前を追加する必要があります。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>
        {% block title %}
        <!-- 個別ページのタイトルがここに入る -->
        {% endblock %}
    </title>
    <link rel="stylesheet" href="{{ url_for('products.static', filename='css/style.css') }}">
</head>

<body>

    <header>
        {% block header %}
        <!-- 個別ページのヘッダーがここに入る -->
        {% endblock %}
    </header>

    <main>
        {% block content %}
        <!-- 個別ページのコンテンツがここに入る -->
        {% endblock %}
    </main>

    <footer>
        <p>© 2025 howahowa store</p>
    </footer>
</body>

</html>
products.htmlの編集(productsapp/templates内)

products.htmlを次のように編集します。

{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('products.manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('products.add_product') }}" class="button-link">商品を追加する</a></li>
</ul>
{% endblock %}

{% block content %}
<h1>{{ title }}</h1>
{% for category, products in grouped_products.items() %}
<h2>{{ category }}</h2>
<ul class="product-list">
    {% for product in products %}
    <li class="product-item">
        <!-- 商品画像の表示 -->
        {% if product.image_url %}
        <img src="{{ url_for('products.static', filename=product.image_url) }}" alt="{{ product.name }}"
            style="width: 50px; height: 50px; margin-right: 10px; border-right: 2px;">
        {% endif %}

        <span class="product-name">{{ product.name }} : ¥{{ "{:,}".format(product.price) }}</span>

        <div class="button-group">
            <form action="{{ url_for('products.edit_product', product_id=product.id) }}" method="GET">
                <button type="submit" class="edit-button">編集</button>
            </form>
            <form action="{{ url_for('products.delete_product', product_id=product.id) }}" method="POST">
                <button type="submit" class="delete-button" onclick="return confirm('本当に削除しますか?')">削除</button>
            </form>
        </div>
    </li>
    {% endfor %}
</ul>
{% endfor %}
{% endblock %}
product_form.htmlの編集(productsapp/templates内)

product_form.htmlを次のように編集します。

{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('products.manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('products.product_list') }}" class="button-link">商品一覧に戻る</a></li>
</ul>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>

<form method="POST"
    action="{{ url_for('products.edit_product', product_id=product.id) if product else url_for('products.add_product') }}"
    enctype="multipart/form-data">

    {{ form.hidden_tag() }} <!-- CSRFトークンを含めるため -->

    <label for="product_name">商品名</label>
    {{ form.product_name() }} <!-- 商品名入力フィールド -->
    {% if form.product_name.errors %}
    <ul>
        {% for error in form.product_name.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="product_price">価格</label>
    {{ form.product_price() }} <!-- 価格入力フィールド -->
    {% if form.product_price.errors %}
    <ul>
        {% for error in form.product_price.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="category">カテゴリー</label>
    {{ form.category() }}
    {% if form.category.errors %}
    <ul>
        {% for error in form.category.errors %}
        <li>{{ error }}</li>
        {% endfor %}
    </ul>
    {% endif %}

    <label for="product_image">商品画像</label>
    <!-- 既存の画像がある場合は表示 -->
    {% if product and product.image_url %}
    <div>
        <img src="{{ url_for('products.static', filename=product.image_url) }}" alt="現在の画像"
            style="width: 100px; height: 100px;">
        <p>※新しい画像を選択すると上書きされます</p>
    </div>
    {% endif %}
    <label for="product_image" class="file-upload-label">画像を選択</label>
    <input type="file" id="product_image" name="product_image" accept="image/*">

    <input type="submit" value="商品を登録する">
</form>
<script>
    document.getElementById("product_image").addEventListener("change", function () {
        const fileName = this.files.length > 0 ? this.files[0].name : "画像を選択";
        document.querySelector(".file-upload-label").textContent = fileName;
    });
</script>

{% endblock %}
manage_products.htmlの編集(productsapp/templates内)

manage_products.htmlを次のように編集します。

{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<h1>{{ title }}</h1>
<ul>
    <li><a href="{{ url_for('products.product_list') }}" class="button-link">商品一覧</a></li>
    <li><a href="{{ url_for('products.add_product') }}" class="button-link">商品追加</a></li>
    <li><a href="{{ url_for('products.category_list') }}" class="button-link">カテゴリ一覧</a></li>
    <li><a href="{{ url_for('products.add_category') }}" class="button-link">カテゴリ追加</a></li>
</ul>
{% endblock %}
categories.htmlの編集(productsapp/templates内)

categories.htmlを次のように編集します。

{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('products.manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('products.add_category') }}" class="button-link">カテゴリーを追加</a></li>
</ul>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<ul>
    {% for category in categories %}
    <li class="category-item">
        <span class="category-name">{{ category.name }}</span>
        <div class="button-group">
            <form action="{{ url_for('products.edit_category', category_id=category.id) }}" method="GET">
                <button type="submit" class="edit-button">編集</button>
            </form>
            <form action="{{ url_for('products.delete_category', category_id=category.id) }}" method="POST">
                <button type="submit" class="delete-button"
                    onclick="return confirm('本当に削除しますか?※既に商品が存在する場合は削除できません。')">削除</button>
            </form>
        </div>
    </li>
    {% endfor %}
</ul>
{% endblock %}
category_form.htmlの編集(productsapp/templates内)

category_form.htmlを次のように編集します。

{% extends "base.html" %}

{% block title %}{{ title }}{% endblock %}

{% block header %}
<ul>
    <li><a href="{{ url_for('products.manage_products') }}" class="button-link">商品管理ページ</a></li>
    <li><a href="{{ url_for('products.category_list') }}" class="button-link">カテゴリー一覧ページ</a></li>
</ul>
{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<form method="POST">
    {{ form.hidden_tag() }}
    <label for="name">カテゴリー名</label>
    {{ form.name() }}
    {% for error in form.name.errors %}
    <p style="color: red;">{{ error }}</p>
    {% endfor %}
    {{ form.submit(class="button-link") }}
</form>
{% endblock %}
app.pyの編集(teblesapp内

tablesapp内のapp.pyファイルに次の内容を追記します。

すでにテーブル管理アプリケーション用のファイル等を準備しているので、アプリケーションの稼働にはteblesappアプリケーションで次のapp.pyが必須となります。

import os

from flask import Blueprint, flash, redirect, render_template, url_for

from apps.common.db import db
from apps.tablesapp.forms import TableForm
from apps.tablesapp.models import Table

tables_bp = Blueprint(
    "tables",
    __name__,
    template_folder=os.path.join(os.getcwd(), "apps", "tablesapp", "templates"),
    static_folder=os.path.join(os.getcwd(), "apps", "tablesapp", "static"),
)


def create_tables_app(app):
    """Blueprintを新規作成し、登録する関数"""
    return tables_bp  # 返り値としてBlueprintを返す


if __name__ == "__main__":
    from flask import Flask

    app = Flask(__name__)
    app.register_blueprint(tables_bp, url_prefix="/tables")

    app.run(debug=True)
.envの編集(apps内

appsディレクトリ内の.envファイルを次のように編集します。

FLASK_APP=apps.main:create_main_app
FLASK_ENV=development
FLASK_RUN_HOST=0.0.0.0
FLASK_RUN_PORT=5000

編集が完了したら、古いデータベースを削除して、仮想環境をactivateして、flask runコマンドででサーバーを起動します。起動をするとinstanceディレクトリにmain.dbが作成され、http://127.0.0.1:5000/products/manage_productsにアクセスすると商品管理アプリケーションの稼働が確認できます。

アプリケーションを統合するとアクセスするときのURLが変わります。商品管理アプリケーションにアクセスするときはBlueprintで決めた「products」がアドレスやポート番号の後に必要です。

次のように、適当に商品を追加しておきます。

今回は以上になります。

「python」おすすめ書籍 ベスト3 | 現役エンジニア&プログラミングスクール講師「python」の学習でお勧めしたい書籍をご紹介しています。お勧めする理由としては、考え方、イメージなどを適切に捉えていること、「生のpython」に焦点をあてて解説をしている書籍であることなどが理由です。勿論、この他にも良い書籍はありますが、特に質の高かったものを選んで記事にしています。ページの下部には「おすすめのITスクール情報」「おすすめ求人サイト」について情報を掲載中。...

ブックマークのすすめ

「ほわほわぶろぐ」を常に検索するのが面倒だという方はブックマークをお勧めします。ブックマークの設定は別記事にて掲載しています。

「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】「お気に入り」の登録・削除方法【Google Chrome / Microsoft Edge】について解説している記事です。削除方法も掲載しています。...
【パソコン選び】失敗しないための重要ポイント | 現役エンジニア&プログラミングスクール講師【パソコン選び】失敗しないための重要ポイントについての記事です。パソコンのタイプと購入時に検討すべき点・家電量販店で見かけるCPUの見方・購入者が必要とするメモリ容量・HDDとSSDについて・ディスプレイの種類・バッテリーの持ち時間や保証・Officeソフト・ウィルス対策ソフトについて書いています。...
RELATED POST
11-Flask

Flask(Part.20)| 【商品管理アプリケーションのコミット】

2025年3月13日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site
11-Flask

Flask(Part.12)| 【ふたつのテーブルの利用(2)ロジック部分の解説】

2025年3月5日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア IT業界への就職・転職希望者 サポートサイト Programming learning Recommended schools Recommended books Information dissemination Computer beginners Prospective engineers New engineers Prospective job seekers in the IT industry Support site