11-Flask

Flask(Part.37)| 【商品管理アプリケーションの修正(2)】

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

目標

  • 商品編集時に以前の商品名や価格をテキストボックスへ表示させる
  • アプリケーション内の画像を選択できるようにする
  • 商品に「販売中」「完売」「在庫切れ」「販売中止」ステータスを作成して「完売」「在庫切れ」は選択不可とし、「販売中止」は表示しない

商品管理アプリケーションの修正

商品管理アプリケーション修正

ここからは、これまで、商品管理アプリケーションで不便と感じていた部分について修正を行います。具体的には次の通りです。

  1. 商品一覧の編集ボタンをクリックしたときに、表示される編集画面で、編集前の商品名、価格、カテゴリーを初期値でセットさせます。
    (Git では release-products-001 ブランチで修正)
  2. 商品編集で画像をアップロードする以外に、既にアップロード済みの画像からも画像を選択できるようにします。
    (Git では release-products-002 ブランチで修正)
  3. 商品に「販売中」「完売」「在庫切れ」「販売中止」ステータスを作成して「完売」「在庫切れ」は選択不可とし、「販売中止」は表示しないようにします。
    (Git では release-products-003 ブランチで修正)

release-productsブランチの作成

release-productsブランチの作成

商品管理アプリケーションは既に基本となる構成については作成を完了しています。このように、機能の追加漏れがあるような場合は、再度、featureブランチを作成するよりも、releaseブランチとしてブランチを作成して作業を行うのが一般的です。

Git Bushを立ち上げるとプロンプトの右側に(develop)と表示され、現在のブランチがdevelopブランチであることが分かります。(次の表示はVisual Studio CodeのGit Bushのプロンプトです。)

User@dellDevice MINGW64 ~/Desktop/FlaskProj (develop)

次のようにコマンドを入力していきます。次のように表示されるはずです。
$ git status

$ git checkout -b release-products-002

$ git status

商品管理アプリケーションの修正(release-products-002)

app.pyの編集(productsapp内)

app.pyファイルを次のように編集します。

import os
from itertools import groupby

from flask import (
    Blueprint,
    current_app,
    flash,
    jsonify,
    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)[ 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()
    # 編集の場合、該当するカテゴリーをデフォルトで選択
    form.category.data = product.category_id

    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を保存

            # 既存画像の選択
            elif request.form.get("selected_image"):
                product.image_url = request.form["selected_image"]

        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")


@products_bp.route("/product/images")
def list_product_images():
    """保存されている商品画像の一覧を取得"""
    image_folder = current_app.config["UPLOAD_FOLDER"]
    images = [
        filename for filename in os.listdir(image_folder) if allowed_file(filename)
    ]
    return jsonify({"images": images})


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

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

    app.run(debug=True)
@products_bp.route("/product/images")
def list_product_images():
    """保存されている商品画像の一覧を取得"""
    image_folder = current_app.config["UPLOAD_FOLDER"]
    images = [
        filename for filename in os.listdir(image_folder) if allowed_file(filename)
    ]
    return jsonify({"images": images})

このプログラムは、Flaskで定義されたエンドポイント/product/imagesにアクセスしたときに、保存されている商品画像の一覧を取得してJSON形式で返す処理を行っています。

current_app.config[“UPLOAD_FOLDER”] はFlaskアプリケーションの設定からUPLOAD_FOLDERというキーの設定値を取得しています。ここで指定されているのは、商品画像が保存されているディレクトリのパスです。(main.pyに記述)

current_appは現在のFlaskアプリケーションインスタンスを指し、configはアプリケーション設定を格納するオブジェクトです。

os.listdir(image_folder)は、指定されたディレクトリ(image_folder)内のファイル名の一覧を取得します。このメソッドは、ディレクトリ内のすべてのファイルとフォルダをリストとして返します。

allowed_file(filename) は、ファイル名が指定された許可された拡張子(例えば .jpg, .png など)であるかを確認するための関数です。allowed_file関数がTrueを返す場合、そのファイルは許可された画像ファイルであると見なされ、imagesリストに追加されます。

このリスト内包表記により、許可された画像ファイルのみがimagesリストに格納されます。

jsonify({“images”: images})は、PythonのリストimagesをJSON形式に変換してHTTPレスポンスとして返します。jsonifyはFlaskでJSONレスポンスを返すために使う便利な関数です。

このコードは、{“images”: [“image1.jpg”, “image2.png”, …]}という形式で、クライアントに商品画像の一覧をJSONで返します。

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(value=product.name if product else '') }} <!-- 商品名入力フィールド -->
    {% 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(value=product.price if product else '') }} <!-- 価格入力フィールド -->
    {% 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 id="preview-image" src="{{ url_for('products.static', filename=product.image_url) }}" alt="現在の画像"
            style="width: 100px; height: 100px;">
        <p>※新しい画像を選択すると上書きされます</p>
        <p id="upload-file-name"></p>
    </div>
    {% else %}
    <img id="preview-image" src="" alt="プレビュー" style="display: none; width: 100px; height: 100px;">
    {% endif %}
    <label for="product_image" class="file-upload-label">画像のアップロード</label>
    <input type="file" id="product_image" name="product_image" accept="image/*">

    <!-- 画像選択ボタン -->
    <label for="selected-image" id="select-image-btn">アプリ内画像から選択</label>
    <input type="hidden" id="selected-image" name="selected_image" value="{{ product.image_url if product else '' }}">

    <!-- 画像選択モーダル(初期状態は非表示) -->
    <!-- 画像選択モーダル -->
    <div id="image-modal" class="modal" style="display: none;">
        <div class="modal-content">
            <span id="close-modal" class="close">×</span>
            <h3>画像を選択</h3>
            <div id="image-list"></div>
        </div>
    </div>

    <input type="submit" value="商品を登録する">
</form>
<script>
    document.getElementById("product_image").addEventListener("change", function () {
        const file = this.files[0];
        const fileNameDisplay = document.getElementById("upload-file-name");

        if (file) {
            fileNameDisplay.textContent = file.name;

            // 画像プレビューを表示
            const reader = new FileReader();
            reader.onload = function (e) {
                const preview = document.getElementById("preview-image");
                preview.src = e.target.result;
                preview.style.display = "block";
            };
            reader.readAsDataURL(file);
        }
    });

    document.addEventListener("DOMContentLoaded", function () {
        fetch("{{ url_for('products.list_product_images') }}")
            .then(response => response.json())
            .then(data => {
                const imageList = document.getElementById("image-list");
                imageList.innerHTML = "";
                data.images.forEach(image => {
                    const imgElement = document.createElement("img");
                    imgElement.src = "{{ url_for('products.static', filename='images/') }}" + image;
                    imgElement.classList.add("selectable-image");
                    imgElement.style.width = "100px";
                    imgElement.style.height = "100px";
                    imgElement.style.cursor = "pointer";
                    imgElement.dataset.image = "images/" + image;

                    imgElement.addEventListener("click", function () {
                        document.getElementById("selected-image").value = this.dataset.image;
                        document.getElementById("preview-image").src = this.src;
                        document.getElementById("preview-image").style.display = "block";
                        document.getElementById("image-modal").style.display = "none";
                    });

                    imageList.appendChild(imgElement);
                });
            })
            .catch(error => console.error("画像取得エラー:", error)); // エラーハンドリング
    });


    document.getElementById("select-image-btn").addEventListener("click", function () {
        document.getElementById("image-modal").style.display = "block";
    });

    document.getElementById("close-modal").addEventListener("click", function () {
        document.getElementById("image-modal").style.display = "none";
    });
</script>

{% endblock %}

ここで追加したプログラムは、商品画像を選択するためのインタラクティブなユーザーインターフェースを提供するものです。ユーザーが「アプリ内画像から選択」ボタンをクリックすると、モーダルウィンドウが表示され、商品画像を選んで登録できるようになります。

<label for="selected-image" id="select-image-btn">アプリ内画像から選択</label>
<input type="hidden" id="selected-image" name="selected_image" value="{{ product.image_url if product else '' }}">

<label>タグは「アプリ内画像から選択」ボタンを表示します。このラベルがクリックされると、id=”select-image-btn”がトリガーとなり、画像選択モーダルが表示されます。

<input type=”hidden”>は隠しフィールドです。選択された画像のパスがselected_imageという名前でフォームに送信されるように設定されています。

<div id="image-modal" class="modal" style="display: none;">
    <div class="modal-content">
        <span id="close-modal" class="close">×</span>
        <h3>画像を選択</h3>
        <div id="image-list"></div>
    </div>
</div>

これは画像選択用のモーダル(ポップアップウィンドウ)です。style=”display: none;”によって初期状態では非表示です。id=”image-modal”でモーダル全体を、id=”image-list”で選択可能な画像リストを表示するための場所を指定しています。

spanタグにあるid=”close-modal”は、モーダルを閉じるための「×」ボタンです。

document.getElementById("product_image").addEventListener("change", function () {
    const file = this.files[0];
    const fileNameDisplay = document.getElementById("upload-file-name");

    if (file) {
        fileNameDisplay.textContent = file.name;

        // 画像プレビューを表示
        const reader = new FileReader();
        reader.onload = function (e) {
            const preview = document.getElementById("preview-image");
            preview.src = e.target.result;
            preview.style.display = "block";
        };
        reader.readAsDataURL(file);
    }
});

この部分は、商品画像が選択されたときに、プレビューを表示する処理です。#product_image(ファイル選択フィールド)に画像が選択されると、FileReaderを使ってその画像をプレビュー表示します。

画像が選択されると、その画像のファイル名が表示され、画像のプレビューが画面に表示されます。

document.addEventListener("DOMContentLoaded", function () {
    fetch("{{ url_for('products.list_product_images') }}")
        .then(response => response.json())
        .then(data => {
            const imageList = document.getElementById("image-list");
            imageList.innerHTML = "";
            data.images.forEach(image => {
                const imgElement = document.createElement("img");
                imgElement.src = "{{ url_for('products.static', filename='images/') }}" + image;
                imgElement.classList.add("selectable-image");
                imgElement.style.width = "100px";
                imgElement.style.height = "100px";
                imgElement.style.cursor = "pointer";
                imgElement.dataset.image = "images/" + image;

                imgElement.addEventListener("click", function () {
                    document.getElementById("selected-image").value = this.dataset.image;
                    document.getElementById("preview-image").src = this.src;
                    document.getElementById("preview-image").style.display = "block";
                    document.getElementById("image-modal").style.display = "none";
                });

                imageList.appendChild(imgElement);
            });
        })
        .catch(error => console.error("画像取得エラー:", error));
});

ページがロードされると、fetchを使ってサーバーから画像の一覧を取得します。URL {{ url_for(‘products.list_product_images’) }} は、サーバー側で設定された画像一覧を返すエンドポイントです。

サーバーから返された画像リスト(data.images)を基に、<img>タグを動的に作成し、#image-list内に表示します。

各画像にはクリックイベントが追加されており、クリックするとその画像が選択され、隠し入力フィールド(#selected-image)に画像のパスが設定され、プレビューが更新されます。

モーダルは画像が選択された時に非表示になります。

document.getElementById("select-image-btn").addEventListener("click", function () {
    document.getElementById("image-modal").style.display = "block";
});

document.getElementById("close-modal").addEventListener("click", function () {
    document.getElementById("image-modal").style.display = "none";
});

#select-image-btnがクリックされると、画像選択モーダル(#image-modal)が表示されます。

モーダル内の#close-modal(×ボタン)がクリックされると、モーダルが非表示になります。

style.cssの編集(productsapp/static/css内)

style.cssを次のように編集します。

body {
    font-family: Arial, sans-serif;
    margin: 20px;
    padding: 20px;
    background-color: #f8f8f8;
}

header {
    margin: 0;
    padding: 10px 0;
}

main {
    margin: 0;
    padding: 20px;
}

footer {
    background-color: #aaa;
    color: white;
    text-align: center;
    padding: 10px 0;
    margin-top: 20px;
}

h1 {
    color: #333;
}

/* ボタン部分のリストを横並びにする */
header ul,
main ul {
    padding: 0;
}

header ul:first-of-type {
    display: flex;
    /* 横並び */
    justify-content: flex-start;
    /* 左寄せ */
}

header ul:first-of-type li {
    margin-right: 10px;
    /* ボタン間に余白 */
}

/* リストアイテムの基本スタイル */
header li {
    list-style-type: none;
    /* デフォルトのリストスタイルを削除 */
}

/* カテゴリーごとの商品リストのスタイル */
ul.product-list {
    list-style-type: none;
    padding: 0;
}

ul.product-list li {
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* ボタンリンクのスタイル */
.button-link {
    display: block;
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    text-decoration: none;
    border-radius: 5px;
    font-size: 16px;
    transition: background-color 0.3s ease;
}

/* ボタンにホバーしたときのスタイル */
.button-link:hover {
    background-color: #45a049;
}

/* 商品リストのスタイル */
h2 {
    margin-top: 30px;
    /* 商品リストの見出しに余白 */
}

ul:nth-of-type(2) {
    list-style-type: none;
    /* 商品リストのマーカーを削除 */
    padding: 0;
}

ul:nth-of-type(2) li {
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* フォーム全体のスタイル */
form {
    background-color: #fff;
    padding: 20px;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    margin-top: 30px;
}

/* フォーム内のラベルのスタイル */
form label {
    font-size: 16px;
    color: #333;
    display: block;
    margin-bottom: 8px;
}

/* フォーム内の入力フィールドのスタイル */
form input[type="text"],
form input[type="number"],
form input[type="submit"] {
    width: 100%;
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 16px;
    box-sizing: border-box;
    /* パディングを含めてサイズ調整 */
}

/* フォーム内のボタン(送信ボタン) */
form input[type="submit"] {
    background-color: #4CAF50;
    color: white;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* 送信ボタンにホバーしたときのスタイル */
form input[type="submit"]:hover,
form button:hover {
    background-color: #45a049;
}

/* ファイル選択ボタンを隠す */
input[type="file"] {
    display: none;
}

/* カスタムアップロードボタンのスタイル */
.file-upload-label {
    display: inline-block;
    padding: 10px 20px;
    background-color: #f2491f;
    color: white;
    text-align: center;
    border-radius: 5px;
    font-size: 16px;
    cursor: pointer;
    transition: background-color 0.3s ease;
}

/* ホバー時のスタイル */
.file-upload-label:hover {
    background-color: #45a049;
}

/* カテゴリーの選択ボックス(select) */
form select {
    width: 100%;
    padding: 10px;
    margin-bottom: 15px;
    border: 1px solid #ddd;
    border-radius: 5px;
    font-size: 16px;
    box-sizing: border-box;
    transition: border-color 0.3s ease;
}

/* フォーカス時のスタイル */
form select:focus {
    border-color: #4CAF50;
    outline: none;
    box-shadow: 0 0 5px rgba(76, 175, 80, 0.5);
}


/* リストアイテムを横並びにする */
.category-item,
.product-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #fff;
    padding: 10px;
    margin: 5px 0;
    border-radius: 5px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* カテゴリー名、商品名のスタイル */
.category-name,
.product-name {
    font-size: 16px;
    flex-grow: 1;
    /* 左側に余白を作り、ボタンを右寄せ */
}

/* ボタンを右側に配置 */
.button-group {
    display: flex;
    gap: 10px;
    /* ボタン間のスペース */
}

/* ボタンを囲む form のスタイルをリセット */
.button-group form {
    display: inline-block;
    margin: 0;
    padding: 0;
}

/* 編集・削除ボタンの共通スタイル */
.button-group button {
    display: inline-block;
    padding: 8px 16px;
    font-size: 14px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    transition: background-color 0.3s ease;
    color: white;
    width: 80px;
    /* 幅を統一 */
    text-align: center;
}

/* 編集ボタンのスタイル */
.button-group .edit-button {
    background-color: #2196F3;
    /* ブルー */
}

.button-group .edit-button:hover {
    background-color: #1976D2;
    /* ホバー時の色 */
}

/* 削除ボタンのスタイル */
.button-group .delete-button {
    background-color: #F44336;
    /* レッド */
}

.button-group .delete-button:hover {
    background-color: #D32F2F;
    /* ホバー時の色 */
}

/* 画像の間に隙間を追加 */
#image-list img {
    margin: 10px;
    /* 画像同士の間隔を10pxに設定 */
}

/* 画像選択ボタンのスタイル */
#select-image-btn {
    display: inline-block;
    padding: 10px 20px;
    background-color: #4CAF50;
    color: white;
    text-decoration: none;
    border-radius: 5px;
    font-size: 16px;
    transition: background-color 0.3s ease;
    outline: none;
}

/* ホバー時のスタイル */
button:focus,
#select-image-btn:hover {
    background-color: #45a049;
    outline: none;
}

スタイルシートについては解説を割愛します。

http://127.0.0.1:5000/products/manage_productsにアクセスし、商品一覧をクリックして次のページを表示します。

りんごジュースの「編集」をクリックします。

「アプリない画像から選択」をクリックします。これまでアップロードされた画像が表示されます。

右の画像をクリックします。選択画像が更新されます。

「商品を登録する」をクリックします。リダイレクトされたページで、りんごジュースの画像が変更されたのが確認できます。

再度、編集ボタンをクリックして、画像を以前のものに戻します。

「商品を登録する」をクリックします。

コミット作業とrelease-productsブランチの削除

作業が完了したので「release-products-002」ブランチを「develop」ブランチにマジして、「release-products-002」ブランチは削除していきます。

git statusコマンドでファイルの状態を確認します。

git add .コマンドを入力します。

再度、git statusコマンドを入力します。

git commitコマンドを入力します。

Fix:商品管理アプリケーションの修正-002(20250324)

今回は設計書などはないため、設計書番号などの提示はなし。商品編集で画像をアップロードする以外に、既にアップロード済みの画像からも画像を選択できるように修正。

上書き保存をして「COMMIT_EDITMSG」ファイルを閉じるとコミットが完了します。

vim や nano で開いている場合は操作が異なります。vi や vim では escキーを押し、コマンドモードに切り替えて :wq を入力し Enterキー を押下します。nanoでは「Ctrl + O」で上書き保存されます。
(※上キャプチャはvimエディタ)

developブランチへのマージ

コミットしたrelease-products-002ブランチをdevelopブランチへマージします。

git checkout developコマンドで、ブランチをdevelopブランチへ切り替えます。

git merge release-products-002コマンドで release-products-002ブランチの内容をdevelopブランチへマージします。

release-products-002ブランチの削除

developブランチへのマージが完了したら、release-products-002ブランチを削除します。

git branch -d release-products-002コマンドでrelease-products-002ブランチを削除します。

今回は以上になります。

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

ブックマークのすすめ

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

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

Flask(Part.11)| 【ふたつのテーブルの利用(1)実装と実行】

2025年3月4日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.38)| 【商品管理アプリケーションの修正(3)】

2025年3月31日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.33)| 【注文管理アプリケーションのプログラミング(7)】

2025年3月26日
プログラミング学習 おすすめ書籍情報発信 パソコン初心者 エンジニア希望者 新人エンジニア 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.22予定)| 【複数アプリケーションの統合(2)】

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