Flaskでのフォーム処理の基礎
2025-12-26はじめに
Webアプリケーションにおいて、フォームはユーザーからの入力を受け付けるための重要な要素です。Flaskでは、シンプルかつ効果的な方法でフォーム処理を実装できます。この記事では、Flaskを使ったフォーム処理の基本から実践的なテクニックまで、初心者の方にもわかりやすく解説します。実際のコード例を交えながら、フォームの作成方法、データの送信と受け取り、そして基本的なバリデーションについて詳しく学んでいきましょう。これらの知識を身につけることで、対話型のWebアプリケーションを開発するための強固な基盤が築けます。
基本的なフォーム作成
Flaskでフォームを作成するには、まずHTMLフォームの基本構造を理解する必要があります。フォームはユーザーがデータを入力し、サーバーに送信するためのインターフェースを提供します。
最もシンプルなフォームから始めてみましょう。Flaskアプリケーションのテンプレートファイルにフォームを記述します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基本フォーム</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">お問い合わせフォーム</h1>
<form method="POST" action="/submit">
<div class="mb-3">
<label for="name" class="form-label">お名前</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="message" class="form-label">メッセージ</label>
<textarea class="form-control" id="message" name="message" rows="4" required></textarea>
</div>
<div class="mb-3">
<label for="category" class="form-label">お問い合わせカテゴリ</label>
<select class="form-select" id="category" name="category">
<option value="general">一般質問</option>
<option value="technical">技術的な質問</option>
<option value="support">サポート</option>
<option value="other">その他</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="newsletter" name="newsletter" value="yes">
<label class="form-check-label" for="newsletter">
ニュースレターを購読する
</label>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">送信する</button>
</form>
</div>
</div>
</div>
</body>
</html>
このフォームには、テキスト入力、メール入力、テキストエリア、セレクトボックス、チェックボックスなど、よく使用されるフォーム要素が含まれています。各入力フィールドにはname属性が設定されており、この値を使ってサーバー側でデータを識別します。
次に、このフォームを表示するためのFlaskルートを作成します。
from flask import Flask, render_template, request, redirect, url_for, flash
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # フラッシュメッセージ用の秘密鍵
@app.route('/')
def index():
return render_template('form.html')
if __name__ == '__main__':
app.run(debug=True)
フォームのmethod属性は「POST」に設定されています。これは、フォームデータがHTTP POSTリクエストとして送信されることを意味します。GETメソッドを使用することもできますが、データがURLに表示されるため、パスワードなどの機密情報には適していません。
アクセシビリティを考慮したフォーム作成も重要です。各入力フィールドには適切なlabel要素を関連付け、required属性を使用して必須項目を明示します。これにより、ユーザーはどの項目が必須かわかりやすくなり、スクリーンリーダーを使用するユーザーにも親切な設計となります。
データの送信と受け取り
フォームから送信されたデータをFlaskで処理する方法について詳しく見ていきましょう。フォームデータの受け取りには、Flaskのrequestオブジェクトを使用します。
まず、フォーム送信を処理するルートを追加します。
from flask import Flask, render_template, request, redirect, url_for, flash
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def index():
return render_template('form.html')
@app.route('/submit', methods=['POST'])
def submit():
# フォームデータの取得
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
category = request.form.get('category')
newsletter = request.form.get('newsletter')
# 取得したデータの表示(実際のアプリケーションではデータベースに保存など)
print(f"名前: {name}")
print(f"メール: {email}")
print(f"メッセージ: {message}")
print(f"カテゴリ: {category}")
print(f"ニュースレター: {'購読希望' if newsletter else '未購読'}")
# フラッシュメッセージでユーザーにフィードバック
flash('お問い合わせありがとうございます。正常に送信されました。', 'success')
# 別のページにリダイレクト
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)
request.formオブジェクトは、POSTリクエストで送信されたフォームデータを含む辞書のようなオブジェクトです。getメソッドを使用して個々のフィールドの値を取得できます。この方法では、該当するキーが存在しない場合でもエラーが発生せず、代わりにNoneが返されます。
フラッシュメッセージを表示するために、ベーステンプレートを更新しましょう。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基本フォーム</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<!-- フラッシュメッセージの表示 -->
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">お問い合わせフォーム</h1>
<form method="POST" action="/submit">
<!-- フォーム内容は前回と同じ -->
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
GETメソッドとPOSTメソッドの違いを理解することも重要です。同じURLでフォームの表示と送信処理の両方を扱う場合は、1つのルートで両方のメソッドを処理できます。
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
# フォーム送信の処理
name = request.form.get('name')
email = request.form.get('email')
message = request.form.get('message')
# 処理ロジック(データベース保存、メール送信など)
print(f"受信したデータ: {name}, {email}, {message}")
flash('メッセージが送信されました', 'success')
return redirect(url_for('contact'))
# GETリクエストの場合、フォームを表示
return render_template('contact.html')
ファイルのアップロードを処理する方法も理解しておきましょう。ファイルアップロードに対応したフォームを作成するには、enctype属性を「multipart/form-data」に設定する必要があります。
<form method="POST" action="/upload" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">ファイルを選択</label>
<input type="file" class="form-control" id="file" name="file">
</div>
<button type="submit" class="btn btn-primary">アップロード</button>
</form>
ファイルアップロードを処理するFlaskルートは以下のようになります。アップロードされたファイルを保存するディレクトリとして uploads を設定し、受け付ける拡張子の種類を指定しています。
import os
from werkzeug.utils import secure_filename
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['ALLOWED_EXTENSIONS'] = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
flash('ファイルが選択されていません', 'error')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('ファイルが選択されていません', 'error')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
flash('ファイルが正常にアップロードされました', 'success')
return redirect(url_for('index'))
flash('許可されていないファイル形式です', 'error')
return redirect(request.url)
allowed_file 関数は、ファイル名に拡張子が含まれているかどうかを確認し、その拡張子が許可された一覧に含まれている場合に True を返す役割を持ちます。/upload という URL に POST メソッドでアクセスがあった際には、アップロードされたファイルがリクエストに含まれているかどうかを確認し、ファイル名が空でないこともチェックしてます。
許可されている形式のファイルであれば、secure_filename を使って安全なファイル名に変換し、アップロード用ディレクトリに保存します。ファイルが正常に保存された場合には成功メッセージを通知してトップページへリダイレクトし、問題がある場合にはエラーメッセージとともに同じページへ戻す仕様になっています。
バリデーション初歩
フォームバリデーションは、ユーザーが入力したデータが正しい形式であることを確認する重要なプロセスです。適切なバリデーションを実装することで、不正なデータによるエラーを防ぎ、ユーザー体験を向上させられます。
まず、基本的なサーバーサイドバリデーションから実装してみましょう。
@app.route('/submit', methods=['POST'])
def submit():
# フォームデータの取得
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip()
message = request.form.get('message', '').strip()
# バリデーションエラーを格納するリスト
errors = []
# 名前のバリデーション
if not name:
errors.append('名前を入力してください')
elif len(name) < 2:
errors.append('名前は2文字以上で入力してください')
elif len(name) > 50:
errors.append('名前は50文字以内で入力してください')
# メールアドレスのバリデーション
if not email:
errors.append('メールアドレスを入力してください')
elif '@' not in email or '.' not in email:
errors.append('有効なメールアドレスを入力してください')
# メッセージのバリデーション
if not message:
errors.append('メッセージを入力してください')
elif len(message) < 10:
errors.append('メッセージは10文字以上で入力してください')
elif len(message) > 1000:
errors.append('メッセージは1000文字以内で入力してください')
# バリデーションエラーがある場合
if errors:
for error in errors:
flash(error, 'error')
return redirect(url_for('index'))
# バリデーション成功時の処理
flash('お問い合わせありがとうございます。正常に送信されました。', 'success')
# 実際のアプリケーションではここでデータベースに保存などの処理
print(f"検証済みデータ - 名前: {name}, メール: {email}, メッセージ: {message}")
return redirect(url_for('index'))
クライアントサイドでのバリデーションも重要です。HTML5の組み込みバリデーション機能を使用すると、ユーザーがフォームを送信する前に入力エラーを検出できます。
<form method="POST" action="/submit" novalidate>
<div class="mb-3">
<label for="name" class="form-label">お名前</label>
<input type="text" class="form-control" id="name" name="name"
minlength="2" maxlength="50" required>
<div class="invalid-feedback">
名前は2文字以上50文字以内で入力してください
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback">
有効なメールアドレスを入力してください
</div>
</div>
<div class="mb-3">
<label for="message" class="form-label">メッセージ</label>
<textarea class="form-control" id="message" name="message"
minlength="10" maxlength="1000" required></textarea>
<div class="invalid-feedback">
メッセージは10文字以上1000文字以内で入力してください
</div>
</div>
<button type="submit" class="btn btn-primary w-100">送信する</button>
</form>
<script>
// クライアントサイドバリデーションの強化
(function() {
'use strict';
const forms = document.querySelectorAll('form');
Array.from(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
})();
</script>
より高度なバリデーションが必要な場合は、WTFormsなどのライブラリを使用することも検討できます。WTFormsは、Flaskと統合しやすい強力なフォームバリデーションライブラリです。
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField, BooleanField
from wtforms.validators import DataRequired, Email, Length, Optional
class ContactForm(FlaskForm):
name = StringField('お名前', validators=[
DataRequired(message='名前は必須です'),
Length(min=2, max=50, message='名前は2文字以上50文字以内で入力してください')
])
email = StringField('メールアドレス', validators=[
DataRequired(message='メールアドレスは必須です'),
Email(message='有効なメールアドレスを入力してください')
])
message = TextAreaField('メッセージ', validators=[
DataRequired(message='メッセージは必須です'),
Length(min=10, max=1000, message='メッセージは10文字以上1000文字以内で入力してください')
])
category = SelectField('カテゴリ', choices=[
('general', '一般質問'),
('technical', '技術的な質問'),
('support', 'サポート'),
('other', 'その他')
], validators=[DataRequired()])
newsletter = BooleanField('ニュースレターを購読する', validators=[Optional()])
WTFormsを使用したルートの実装例です。
@app.route('/wtf-contact', methods=['GET', 'POST'])
def wtf_contact():
form = ContactForm()
if form.validate_on_submit():
# フォームデータの処理
name = form.name.data
email = form.email.data
message = form.message.data
category = form.category.data
newsletter = form.newsletter.data
# 処理ロジック
print(f"WTFormsで処理されたデータ: {name}, {email}, {message}")
flash('お問い合わせありがとうございます。正常に送信されました。', 'success')
return redirect(url_for('wtf_contact'))
return render_template('wtf_contact.html', form=form)
対応するテンプレートファイルでは、WTFormsのフィールドをレンダリングします。
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% if form.email.errors %}
<div class="invalid-feedback d-block">
{% for error in form.email.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
<!-- 他のフィールドも同様に -->
<button type="submit" class="btn btn-primary w-100">送信する</button>
</form>
バリデーションを設計する際のベストプラクティスとして、ユーザーに明確なエラーメッセージを提供すること、必須項目と任意項目を明確に区別すること、複数階層のバリデーションを実装することなどが挙げられます。また、常にサーバーサイドバリデーションを実施し、クライアントサイドバリデーションはユーザー体験向上のための補助として扱うことが重要です。
まとめ
Flaskでのフォーム処理の基礎について、基本的なフォーム作成からデータの送信と受け取り、そしてバリデーションの初歩までを詳しく解説してきました。フォームはWebアプリケーションとユーザーとの重要な接点であり、適切に設計・実装することで、使いやすく安全なアプリケーションを構築できます。
フォーム作成では、適切なHTML構造とアクセシビリティへの配慮が重要です。データの送信と受け取りでは、requestオブジェクトを活用した効率的な処理方法と、ユーザーへのフィードバックの提供方法を学びました。バリデーションでは、基本的なサーバーサイドバリデーションからWTFormsを使用したより構造化されたアプローチまで、さまざまな手法を紹介しました。
これらの技術を組み合わせることで、ユーザーからの入力を効果的に処理し、エラーの少ない堅牢なWebアプリケーションを開発できるようになります。実際のプロジェクトでは、ここで学んだ基礎を土台にして、より複雑なフォーム処理や高度なバリデーションルールに挑戦してみてください。Flaskの柔軟性を活かせば、あらゆる種類のフォーム要件に対応できるでしょう。
演習問題
各問題の解答例を作成する際は、以下の点に注意してください。
- 初学者向けの詳細な説明を付ける
- 完全な動作コードを提示する
- エラーハンドリングを適切に実装する
- ベストプラクティスに沿ったコードを書く
- セキュリティ考慮事項を説明する
- 実際の使用例を想定した実用的なコードにする
初級問題(3問)
初級1: 基本的なフォーム作成
問題
名前(name)とメールアドレス(email)を入力する簡単なコンタクトフォームを作成してください。Flaskアプリケーションで以下の要件を満たすコードを書いてください。
- ルート
/でフォームを表示する - フォームはPOSTメソッドで
/submitに送信する - 送信後は「ありがとうございます」というメッセージを表示する
解答例の要件
- 基本的なHTMLフォーム構造
- Flaskルートの設定
- データ受け取りの実装
初級2: フォームデータの取得
問題
以下のフォームフィールドを持つユーザー登録フォームを作成してください。
- ユーザー名(username)
- パスワード(password)
- 年齢(age)
- 性別(gender:ラジオボタン)
フォーム送信後、受け取ったデータをコンソールに表示するFlaskアプリケーションを作成してください。
解答例の要件
- 複数種類のフォーム要素の実装
- request.formからのデータ取得
- データの型変換(年齢は数値に)
初級3: 基本的なバリデーション
問題
名前(name)とメールアドレス(email)のフォームに以下のバリデーションを追加してください。
- 名前は必須入力、2文字以上
- メールアドレスは必須入力、@を含む
- バリデーションエラー時はエラーメッセージを表示
解答例の要件
- サーバーサイドバリデーションの実装
- エラーメッセージの表示
- フラッシュメッセージの使用
中級問題(6問)
中級1: ファイルアップロード
問題
画像ファイルをアップロードするフォームを作成してください。
- アップロード可能なファイル形式はjpg, png, gifのみ
- ファイルサイズは2MB以下に制限
- アップロード成功時はファイル名を表示
解答例の要件
- enctype=”multipart/form-data”の設定
- ファイル形式のチェック
- 安全なファイル名の処理
中級2: クライアントサイドバリデーション
問題
ユーザー登録フォームにHTML5のクライアントサイドバリデーションを追加してください。
- ユーザー名:必須、3文字以上
- メールアドレス:必須、email形式
- パスワード:必須、8文字以上
- Bootstrapのバリデーションスタイルを適用
解答例の要件
- HTML5バリデーション属性の使用
- Bootstrapのwas-validatedクラスの適用
- カスタムバリデーションメッセージ
中級3: セッションを使ったフォームデータの保持
問題
マルチページフォームを作成し、セッションを使ってデータを保持してください。
- 1ページ目:基本情報(名前、メール)
- 2ページ目:詳細情報(年齢、職業)
- 確認ページ:全データの表示と送信
解答例の要件
- Flaskセッションの使用
- 複数ページでのデータ保持
- データの確認表示
中級4: WTFormsの基本
問題
WTFormsを使用して以下のフォームを作成してください。
- 商品名(product_name):必須、最大100文字
- 価格(price):必須、数値、0以上
- 在庫数(stock):必須、整数、0以上
- カテゴリ(category):選択式(電子機器、書籍、衣類)
解答例の要件
- FlaskFormクラスの定義
- 各種バリデーターの使用
- テンプレートでのフォームレンダリング
中級5: 動的フォーム要素
問題
JavaScriptを使用して動的にフォーム要素を追加できる機能を実装してください。
- 「スキル追加」ボタンをクリックすると新しいスキル入力欄を追加
- 各スキル入力欄には削除ボタンを設置
- 最小1つ、最大5つのスキルを入力可能
解答例の要件
- JavaScriptでのDOM操作
- 動的な要素の追加・削除
- サーバーサイドでの配列データ処理
中級6: 条件付き表示フォーム
問題
ユーザータイプによって表示するフォーム項目を変更してください。
- ユーザータイプ(個人/企業)を選択
- 個人選択時:個人情報(姓名、年齢)を表示
- 企業選択時:企業情報(会社名、業種、従業員数)を表示
解答例の要件
- JavaScriptでの表示切り替え
- 条件に応じたフォーム項目の動的表示
- サーバーサイドでの条件分岐処理
上級問題(3問)
上級1: 非同期フォーム送信(Ajax)
問題
Ajaxを使用して非同期でフォームを送信する機能を実装してください。
- フォーム送信時にページ遷移しない
- 送信中はローディング表示を出す
- 成功/エラーに応じてメッセージを表示
- バリデーションエラーはフォームに表示
解答例の要件
- Fetch APIまたはjQuery Ajaxの使用
- JSONレスポンスの処理
- 動的なエラーメッセージ表示
上級2: カスタムバリデーション
問題
WTFormsで以下のカスタムバリデーションを作成してください。
- ユーザー名の重複チェック(仮想的なデータベースを想定)
- パスワードの強度チェック(大文字、小文字、数字を含む)
- 開始日と終了日の期間チェック
解答例の要件
- カスタムバリデーター関数の作成
- データベースチェックの模擬
- 複数フィールドにまたがるバリデーション
上級3: フォームセキュリティ対策
問題
以下のセキュリティ対策を実装したフォームを作成してください。
- CSRFトークンの実装
- XSS対策(入力値のエスケープ)
- クリックジャッキング対策
- レート制限(1分間に3回まで送信可能)
解答例の要件
- Flask-WTFでのCSRF保護
- テンプレートエンジンでの自動エスケープ
- ヘッダー設定によるクリックジャッキング対策
- Flask-Limiterによるレート制限
演習問題 解答例
初級問題 解答例
初級1: 基本的なフォーム作成
app.py
from flask import Flask, render_template, request, redirect, url_for
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/submit', methods=['POST'])
def submit():
name = request.form.get('name')
email = request.form.get('email')
print(f"受信したデータ: 名前={name}, メール={email}")
return render_template('thankyou.html', name=name)
if __name__ == '__main__':
app.run(debug=True)
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>コンタクトフォーム</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">お問い合わせ</h1>
<form method="POST" action="/submit">
<div class="mb-3">
<label for="name" class="form-label">お名前</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary w-100">送信</button>
</form>
</div>
</div>
</div>
</body>
</html>
templates/thankyou.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>送信完了</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-success text-center">
<h2>ありがとうございます</h2>
<p class="mb-0">{{ name }}様、お問い合わせを受け付けました。</p>
</div>
<a href="/" class="btn btn-secondary w-100">戻る</a>
</div>
</div>
</div>
</body>
</html>
初級2: フォームデータの取得
app.py
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/')
def registration_form():
return render_template('registration.html')
@app.route('/register', methods=['POST'])
def register():
# フォームデータの取得
username = request.form.get('username')
password = request.form.get('password')
age = request.form.get('age')
gender = request.form.get('gender')
# データの検証と変換
try:
age = int(age) if age else 0
except ValueError:
age = 0
# コンソールに表示
print("=== ユーザー登録情報 ===")
print(f"ユーザー名: {username}")
print(f"パスワード: {'*' * len(password)}")
print(f"年齢: {age}")
print(f"性別: {gender}")
return f"""
<div class='container mt-5'>
<h2>登録完了</h2>
<p>ユーザー名: {username}</p>
<p>年齢: {age}</p>
<p>性別: {gender}</p>
<a href='/' class='btn btn-secondary'>戻る</a>
</div>
"""
if __name__ == '__main__':
app.run(debug=True)
templates/registration.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ユーザー登録</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">ユーザー登録</h1>
<form method="POST" action="/register">
<div class="mb-3">
<label for="username" class="form-label">ユーザー名</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">パスワード</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="age" class="form-label">年齢</label>
<input type="number" class="form-control" id="age" name="age" min="0" max="120">
</div>
<div class="mb-3">
<label class="form-label">性別</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="male" name="gender" value="male">
<label class="form-check-label" for="male">男性</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="female" name="gender" value="female">
<label class="form-check-label" for="female">女性</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="other" name="gender" value="other">
<label class="form-check-label" for="other">その他</label>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">登録</button>
</form>
</div>
</div>
</div>
</body>
</html>
初級3: 基本的なバリデーション
app.py
from flask import Flask, render_template, request, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def contact_form():
return render_template('contact_validation.html')
@app.route('/submit', methods=['POST'])
def submit_contact():
name = request.form.get('name', '').strip()
email = request.form.get('email', '').strip()
# バリデーション
errors = []
# 名前のバリデーション
if not name:
errors.append('名前を入力してください')
elif len(name) < 2:
errors.append('名前は2文字以上で入力してください')
# メールアドレスのバリデーション
if not email:
errors.append('メールアドレスを入力してください')
elif '@' not in email:
errors.append('有効なメールアドレスを入力してください')
# エラーがある場合
if errors:
for error in errors:
flash(error, 'error')
return redirect(url_for('contact_form'))
# 成功時の処理
flash('お問い合わせありがとうございます。正常に送信されました。', 'success')
return redirect(url_for('contact_form'))
if __name__ == '__main__':
app.run(debug=True)
templates/contact_validation.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>コンタクトフォーム(バリデーション付き)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">お問い合わせ</h1>
<!-- フラッシュメッセージの表示 -->
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST" action="/submit">
<div class="mb-3">
<label for="name" class="form-label">お名前</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ request.form.name if request.form }}" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email"
value="{{ request.form.email if request.form }}" required>
</div>
<button type="submit" class="btn btn-primary w-100">送信</button>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
中級問題 解答例
中級1: ファイルアップロード
app.py
import os
from flask import Flask, render_template, request, flash, redirect, url_for
from werkzeug.utils import secure_filename
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
# 設定
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024 # 2MB
app.config['ALLOWED_EXTENSIONS'] = {'jpg', 'jpeg', 'png', 'gif'}
# アップロードフォルダの作成
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
@app.route('/')
def upload_form():
return render_template('upload.html')
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
flash('ファイルが選択されていません', 'error')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('ファイルが選択されていません', 'error')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
flash(f'ファイル "{filename}" が正常にアップロードされました', 'success')
return redirect(url_for('upload_form'))
else:
flash('許可されていないファイル形式です。jpg, png, gif形式のみアップロードできます。', 'error')
return redirect(request.url)
if __name__ == '__main__':
app.run(debug=True)
templates/upload.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ファイルアップロード</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">画像アップロード</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST" action="/upload" enctype="multipart/form-data">
<div class="mb-3">
<label for="file" class="form-label">画像ファイルを選択</label>
<input type="file" class="form-control" id="file" name="file" accept=".jpg,.jpeg,.png,.gif" required>
<div class="form-text">
対応形式: JPG, PNG, GIF(最大2MB)
</div>
</div>
<button type="submit" class="btn btn-primary w-100">アップロード</button>
</form>
</div>
</div>
</div>
</body>
</html>
中級2: クライアントサイドバリデーション
app.py
from flask import Flask, render_template, request, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def registration_form():
return render_template('client_validation.html')
@app.route('/register', methods=['POST'])
def register():
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
# サーバーサイドバリデーション(念のため)
errors = []
if len(username) < 3:
errors.append('ユーザー名は3文字以上で入力してください')
if '@' not in email:
errors.append('有効なメールアドレスを入力してください')
if len(password) < 8:
errors.append('パスワードは8文字以上で入力してください')
if errors:
for error in errors:
flash(error, 'error')
return redirect(url_for('registration_form'))
flash('登録が完了しました', 'success')
return redirect(url_for('registration_form'))
if __name__ == '__main__':
app.run(debug=True)
templates/client_validation.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ユーザー登録(クライアントバリデーション)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">ユーザー登録</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST" action="/register" class="needs-validation" novalidate>
<div class="mb-3">
<label for="username" class="form-label">ユーザー名</label>
<input type="text" class="form-control" id="username" name="username"
minlength="3" required>
<div class="invalid-feedback">
ユーザー名は3文字以上で入力してください
</div>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback">
有効なメールアドレスを入力してください
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">パスワード</label>
<input type="password" class="form-control" id="password" name="password"
minlength="8" required>
<div class="invalid-feedback">
パスワードは8文字以上で入力してください
</div>
</div>
<button type="submit" class="btn btn-primary w-100">登録</button>
</form>
</div>
</div>
</div>
<script>
// クライアントサイドバリデーション
(function() {
'use strict'
const forms = document.querySelectorAll('.needs-validation')
Array.from(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault()
event.stopPropagation()
}
form.classList.add('was-validated')
}, false)
})
})()
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
中級3: セッションを使ったフォームデータの保持
app.py
from flask import Flask, render_template, request, session, redirect, url_for
app = Flask(__name__)
app.secret_key = 'your_very_secret_key_here'
@app.route('/')
def index():
session.clear() # セッションをクリア
return redirect(url_for('step1'))
@app.route('/step1', methods=['GET', 'POST'])
def step1():
if request.method == 'POST':
# 基本情報をセッションに保存
session['name'] = request.form.get('name', '')
session['email'] = request.form.get('email', '')
return redirect(url_for('step2'))
return render_template('step1.html')
@app.route('/step2', methods=['GET', 'POST'])
def step2():
if request.method == 'POST':
# 詳細情報をセッションに保存
session['age'] = request.form.get('age', '')
session['occupation'] = request.form.get('occupation', '')
return redirect(url_for('confirm'))
return render_template('step2.html')
@app.route('/confirm', methods=['GET', 'POST'])
def confirm():
if request.method == 'POST':
# 最終送信処理
print("最終データ:", dict(session))
return redirect(url_for('complete'))
return render_template('confirm.html', data=session)
@app.route('/complete')
def complete():
data = dict(session)
session.clear() # セッションをクリア
return render_template('complete.html', data=data)
if __name__ == '__main__':
app.run(debug=True)
templates/step1.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ステップ1 - 基本情報</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="progress mb-4">
<div class="progress-bar" style="width: 25%">25%</div>
</div>
<h1 class="text-center mb-4">基本情報</h1>
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label">お名前</label>
<input type="text" class="form-control" id="name" name="name"
value="{% raw %}{{ session.name if session.name }}{% endraw %}" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email"
value="{% raw %}{{ session.email if session.email }}{% endraw %}" required>
</div>
<button type="submit" class="btn btn-primary w-100">次へ</button>
</form>
</div>
</div>
</div>
</body>
</html>
templates/step2.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ステップ2 - 詳細情報</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="progress mb-4">
<div class="progress-bar" style="width: 50%">50%</div>
</div>
<h1 class="text-center mb-4">詳細情報</h1>
<form method="POST">
<div class="mb-3">
<label for="age" class="form-label">年齢</label>
<input type="number" class="form-control" id="age" name="age"
value="{% raw %}{{ session.age if session.age }}{% endraw %}">
</div>
<div class="mb-3">
<label for="occupation" class="form-label">職業</label>
<select class="form-select" id="occupation" name="occupation">
<option value="">選択してください</option>
<option value="student" {% raw %}{{ 'selected' if session.occupation == 'student' }}{% endraw %}>学生</option>
<option value="company" {% raw %}{{ 'selected' if session.occupation == 'company' }}{% endraw %}>会社員</option>
<option value="self" {% raw %}{{ 'selected' if session.occupation == 'self' }}{% endraw %}>自営業</option>
<option value="other" {% raw %}{{ 'selected' if session.occupation == 'other' }}{% endraw %}>その他</option>
</select>
</div>
<div class="d-flex gap-2">
<a href="{% raw %}{{ url_for('step1') }}{% endraw %}" class="btn btn-secondary w-50">戻る</a>
<button type="submit" class="btn btn-primary w-50">次へ</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
templates/confirm.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>確認画面</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="progress mb-4">
<div class="progress-bar" style="width: 75%">75%</div>
</div>
<h1 class="text-center mb-4">入力内容確認</h1>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">基本情報</h5>
<p><strong>名前:</strong> {% raw %}{{ data.name }}{% endraw %}</p>
<p><strong>メール:</strong> {% raw %}{{ data.email }}{% endraw %}</p>
</div>
</div>
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">詳細情報</h5>
<p><strong>年齢:</strong> {% raw %}{{ data.age }}{% endraw %}</p>
<p><strong>職業:</strong> {% raw %}{{ data.occupation }}{% endraw %}</p>
</div>
</div>
<form method="POST">
<div class="d-flex gap-2">
<a href="{% raw %}{{ url_for('step2') }}{% endraw %}" class="btn btn-secondary w-50">戻る</a>
<button type="submit" class="btn btn-success w-50">送信</button>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
templates/complete.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>完了</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="progress mb-4">
<div class="progress-bar" style="width: 100%">100%</div>
</div>
<div class="alert alert-success text-center">
<h2>登録完了</h2>
<p class="mb-0">登録が正常に完了しました</p>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">登録内容</h5>
{% raw %}{% for key, value in data.items() %}{% endraw %}
<p><strong>{% raw %}{{ key }}{% endraw %}:</strong> {% raw %}{{ value }}{% endraw %}</p>
{% raw %}{% endfor %}{% endraw %}
</div>
</div>
<a href="{% raw %}{{ url_for('index') }}{% endraw %}" class="btn btn-primary w-100 mt-3">最初から</a>
</div>
</div>
</div>
</body>
</html>
中級4: WTFormsの基本
app.py
from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, DecimalField, IntegerField, SelectField, SubmitField
from wtforms.validators import DataRequired, NumberRange, Length
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
class ProductForm(FlaskForm):
product_name = StringField('商品名', validators=[
DataRequired(message='商品名は必須です'),
Length(max=100, message='商品名は100文字以内で入力してください')
])
price = DecimalField('価格', validators=[
DataRequired(message='価格は必須です'),
NumberRange(min=0, message='価格は0以上で入力してください')
], places=2)
stock = IntegerField('在庫数', validators=[
DataRequired(message='在庫数は必須です'),
NumberRange(min=0, message='在庫数は0以上で入力してください')
])
category = SelectField('カテゴリ', choices=[
('electronics', '電子機器'),
('books', '書籍'),
('clothing', '衣類'),
('other', 'その他')
], validators=[DataRequired(message='カテゴリを選択してください')])
submit = SubmitField('登録')
@app.route('/', methods=['GET', 'POST'])
def add_product():
form = ProductForm()
if form.validate_on_submit():
# フォームデータの処理
product_data = {
'product_name': form.product_name.data,
'price': float(form.price.data),
'stock': form.stock.data,
'category': form.category.data
}
print("商品データ:", product_data)
flash('商品が正常に登録されました', 'success')
return redirect(url_for('add_product'))
return render_template('product_form.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
templates/product_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>商品登録</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">商品登録</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{% raw %}{{ message }}{% endraw %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST">
{% raw %}{{ form.hidden_tag() }}{% endraw %}
<div class="mb-3">
{% raw %}{{ form.product_name.label(class="form-label") }}{% endraw %}
{% raw %}{{ form.product_name(class="form-control") }}{% endraw %}
{% raw %}{% if form.product_name.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.product_name.errors %}{% endraw %}
<small>{% raw %}{{ error }}{% endraw %}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
<div class="mb-3">
{% raw %}{{ form.price.label(class="form-label") }}{% endraw %}
{% raw %}{{ form.price(class="form-control", step="0.01") }}{% endraw %}
{% raw %}{% if form.price.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.price.errors %}{% endraw %}
<small>{% raw %}{{ error }}{% endraw %}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
<div class="mb-3">
{% raw %}{{ form.stock.label(class="form-label") }}{% endraw %}
{% raw %}{{ form.stock(class="form-control") }}{% endraw %}
{% raw %}{% if form.stock.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.stock.errors %}{% endraw %}
<small>{% raw %}{{ error }}{% endraw %}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
<div class="mb-3">
{% raw %}{{ form.category.label(class="form-label") }}{% endraw %}
{% raw %}{{ form.category(class="form-select") }}{% endraw %}
{% raw %}{% if form.category.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.category.errors %}{% endraw %}
<small>{% raw %}{{ error }}{% endraw %}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
{% raw %}{{ form.submit(class="btn btn-primary w-100") }}{% endraw %}
</form>
</div>
</div>
</div>
</body>
</html>
中級5: 動的フォーム要素
app.py
from flask import Flask, render_template, request, flash, redirect, url_for
import json
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def skill_form():
return render_template('dynamic_form.html')
@app.route('/submit-skills', methods=['POST'])
def submit_skills():
# スキルデータの取得(配列として送信される)
skills = request.form.getlist('skills[]')
# 空の要素を除去
skills = [skill.strip() for skill in skills if skill.strip()]
# バリデーション
if len(skills) == 0:
flash('少なくとも1つのスキルを入力してください', 'error')
return redirect(url_for('skill_form'))
if len(skills) > 5:
flash('スキルは最大5つまでです', 'error')
return redirect(url_for('skill_form'))
# 処理
print("登録されたスキル:", skills)
flash(f'{len(skills)}個のスキルが登録されました: {", ".join(skills)}', 'success')
return redirect(url_for('skill_form'))
if __name__ == '__main__':
app.run(debug=True)
templates/dynamic_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>スキル登録</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="text-center mb-4">スキル登録</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{% raw %}{{ message }}{% endraw %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST" action="/submit-skills" id="skills-form">
<div id="skills-container">
<div class="skill-group mb-3">
<label class="form-label">スキル 1</label>
<div class="input-group">
<input type="text" class="form-control" name="skills[]" placeholder="例: Python, JavaScript, デザイン...">
<button type="button" class="btn btn-outline-danger remove-skill" disabled>削除</button>
</div>
</div>
</div>
<div class="mb-3">
<button type="button" id="add-skill" class="btn btn-outline-primary">スキルを追加</button>
<small class="text-muted">(最大5つまで)</small>
</div>
<button type="submit" class="btn btn-primary w-100">登録</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const skillsContainer = document.getElementById('skills-container');
const addSkillButton = document.getElementById('add-skill');
let skillCount = 1;
const maxSkills = 5;
addSkillButton.addEventListener('click', function() {
if (skillCount >= maxSkills) {
alert(`スキルは最大${maxSkills}つまでです`);
return;
}
skillCount++;
const newSkillGroup = document.createElement('div');
newSkillGroup.className = 'skill-group mb-3';
newSkillGroup.innerHTML = `
<label class="form-label">スキル ${skillCount}</label>
<div class="input-group">
<input type="text" class="form-control" name="skills[]" placeholder="例: Python, JavaScript, デザイン...">
<button type="button" class="btn btn-outline-danger remove-skill">削除</button>
</div>
`;
skillsContainer.appendChild(newSkillGroup);
updateRemoveButtons();
});
skillsContainer.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-skill')) {
if (skillCount > 1) {
e.target.closest('.skill-group').remove();
skillCount--;
updateLabels();
updateRemoveButtons();
}
}
});
function updateLabels() {
const groups = skillsContainer.querySelectorAll('.skill-group');
groups.forEach((group, index) => {
group.querySelector('label').textContent = `スキル ${index + 1}`;
});
}
function updateRemoveButtons() {
const removeButtons = skillsContainer.querySelectorAll('.remove-skill');
removeButtons.forEach(button => {
button.disabled = removeButtons.length === 1;
});
addSkillButton.disabled = skillCount >= maxSkills;
}
updateRemoveButtons();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
中級6: 条件付き表示フォーム
app.py
from flask import Flask, render_template, request, flash, redirect, url_for
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def user_form():
return render_template('conditional_form.html')
@app.route('/submit-user', methods=['POST'])
def submit_user():
user_type = request.form.get('user_type')
if user_type == 'personal':
# 個人情報の処理
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
age = request.form.get('age', '').strip()
if not first_name or not last_name:
flash('姓名を入力してください', 'error')
return redirect(url_for('user_form'))
user_data = {
'type': '個人',
'first_name': first_name,
'last_name': last_name,
'age': age
}
elif user_type == 'business':
# 企業情報の処理
company_name = request.form.get('company_name', '').strip()
industry = request.form.get('industry', '').strip()
employees = request.form.get('employees', '').strip()
if not company_name:
flash('会社名を入力してください', 'error')
return redirect(url_for('user_form'))
user_data = {
'type': '企業',
'company_name': company_name,
'industry': industry,
'employees': employees
}
else:
flash('ユーザータイプを選択してください', 'error')
return redirect(url_for('user_form'))
print("登録データ:", user_data)
flash('登録が完了しました', 'success')
return redirect(url_for('user_form'))
if __name__ == '__main__':
app.run(debug=True)
templates/conditional_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ユーザー登録</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-8">
<h1 class="text-center mb-4">ユーザー登録</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{% raw %}{{ message }}{% endraw %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST" action="/submit-user">
<!-- ユーザータイプ選択 -->
<div class="mb-4">
<label class="form-label fw-bold">ユーザータイプ</label>
<div class="row">
<div class="col-md-6">
<div class="form-check card">
<input class="form-check-input" type="radio" name="user_type"
id="personal" value="personal" onchange="toggleForm()">
<label class="form-check-label card-body" for="personal">
<h5 class="card-title">個人</h5>
<p class="card-text text-muted">個人でご利用の場合</p>
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check card">
<input class="form-check-input" type="radio" name="user_type"
id="business" value="business" onchange="toggleForm()">
<label class="form-check-label card-body" for="business">
<h5 class="card-title">企業</h5>
<p class="card-text text-muted">法人でご利用の場合</p>
</label>
</div>
</div>
</div>
</div>
<!-- 個人情報フォーム -->
<div id="personal-form" class="form-section" style="display: none;">
<h4 class="mb-3">個人情報</h4>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="first_name" class="form-label">姓</label>
<input type="text" class="form-control" id="first_name" name="first_name">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="last_name" class="form-label">名</label>
<input type="text" class="form-control" id="last_name" name="last_name">
</div>
</div>
</div>
<div class="mb-3">
<label for="age" class="form-label">年齢</label>
<input type="number" class="form-control" id="age" name="age" min="0" max="120">
</div>
</div>
<!-- 企業情報フォーム -->
<div id="business-form" class="form-section" style="display: none;">
<h4 class="mb-3">企業情報</h4>
<div class="mb-3">
<label for="company_name" class="form-label">会社名</label>
<input type="text" class="form-control" id="company_name" name="company_name">
</div>
<div class="mb-3">
<label for="industry" class="form-label">業種</label>
<select class="form-select" id="industry" name="industry">
<option value="">選択してください</option>
<option value="it">IT・テクノロジー</option>
<option value="manufacturing">製造業</option>
<option value="retail">小売業</option>
<option value="service">サービス業</option>
<option value="other">その他</option>
</select>
</div>
<div class="mb-3">
<label for="employees" class="form-label">従業員数</label>
<select class="form-select" id="employees" name="employees">
<option value="">選択してください</option>
<option value="1-10">1-10名</option>
<option value="11-50">11-50名</option>
<option value="51-200">51-200名</option>
<option value="201-500">201-500名</option>
<option value="501+">501名以上</option>
</select>
</div>
</div>
<button type="submit" class="btn btn-primary w-100 mt-4" id="submit-btn" style="display: none;">
登録
</button>
</form>
</div>
</div>
</div>
<script>
function toggleForm() {
const personalRadio = document.getElementById('personal');
const businessRadio = document.getElementById('business');
const personalForm = document.getElementById('personal-form');
const businessForm = document.getElementById('business-form');
const submitBtn = document.getElementById('submit-btn');
if (personalRadio.checked) {
personalForm.style.display = 'block';
businessForm.style.display = 'none';
submitBtn.style.display = 'block';
} else if (businessRadio.checked) {
personalForm.style.display = 'none';
businessForm.style.display = 'block';
submitBtn.style.display = 'block';
} else {
personalForm.style.display = 'none';
businessForm.style.display = 'none';
submitBtn.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', function() {
toggleForm();
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
上級問題 解答例
上級1: 非同期フォーム送信(Ajax)
app.py
from flask import Flask, render_template, request, jsonify
import time
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/')
def ajax_form():
return render_template('ajax_form.html')
@app.route('/api/register', methods=['POST'])
def api_register():
# 擬似的な処理時間
time.sleep(1)
data = request.get_json()
username = data.get('username', '').strip()
email = data.get('email', '').strip()
password = data.get('password', '')
# バリデーション
errors = {}
if not username:
errors['username'] = 'ユーザー名を入力してください'
elif len(username) < 3:
errors['username'] = 'ユーザー名は3文字以上で入力してください'
if not email:
errors['email'] = 'メールアドレスを入力してください'
elif '@' not in email:
errors['email'] = '有効なメールアドレスを入力してください'
if not password:
errors['password'] = 'パスワードを入力してください'
elif len(password) < 8:
errors['password'] = 'パスワードは8文字以上で入力してください'
if errors:
return jsonify({'success': False, 'errors': errors})
# 成功時の処理
return jsonify({
'success': True,
'message': f'ユーザー {username} が登録されました'
})
if __name__ == '__main__':
app.run(debug=True)
templates/ajax_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>非同期フォーム</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">非同期ユーザー登録</h1>
<div id="message-container"></div>
<form id="register-form">
<div class="mb-3">
<label for="username" class="form-label">ユーザー名</label>
<input type="text" class="form-control" id="username" name="username" required>
<div class="invalid-feedback" id="username-error"></div>
</div>
<div class="mb-3">
<label for="email" class="form-label">メールアドレス</label>
<input type="email" class="form-control" id="email" name="email" required>
<div class="invalid-feedback" id="email-error"></div>
</div>
<div class="mb-3">
<label for="password" class="form-label">パスワード</label>
<input type="password" class="form-control" id="password" name="password" required>
<div class="invalid-feedback" id="password-error"></div>
</div>
<button type="submit" class="btn btn-primary w-100" id="submit-btn">
登録
</button>
<div class="text-center mt-3">
<div class="spinner-border text-primary" role="status" id="loading-spinner" style="display: none;">
<span class="visually-hidden">読み込み中...</span>
</div>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('register-form');
const submitBtn = document.getElementById('submit-btn');
const loadingSpinner = document.getElementById('loading-spinner');
const messageContainer = document.getElementById('message-container');
form.addEventListener('submit', async function(e) {
e.preventDefault();
submitBtn.disabled = true;
loadingSpinner.style.display = 'block';
submitBtn.innerHTML = '送信中...';
clearErrors();
clearMessage();
const formData = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
password: document.getElementById('password').value
};
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showMessage(result.message, 'success');
form.reset();
} else {
showErrors(result.errors);
showMessage('入力内容にエラーがあります', 'error');
}
} catch (error) {
showMessage('通信エラーが発生しました', 'error');
console.error('Error:', error);
} finally {
submitBtn.disabled = false;
loadingSpinner.style.display = 'none';
submitBtn.innerHTML = '登録';
}
});
function showErrors(errors) {
for (const field in errors) {
const input = document.getElementById(field);
const errorDiv = document.getElementById(field + '-error');
if (input && errorDiv) {
input.classList.add('is-invalid');
errorDiv.textContent = errors[field];
}
}
}
function clearErrors() {
const inputs = form.querySelectorAll('input');
inputs.forEach(input => input.classList.remove('is-invalid'));
const errorDivs = form.querySelectorAll('.invalid-feedback');
errorDivs.forEach(div => div.textContent = '');
}
function showMessage(message, type) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
messageContainer.innerHTML = `
<div class="alert ${alertClass} alert-dismissible fade show">
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
}
function clearMessage() {
messageContainer.innerHTML = '';
}
});
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
上級2: カスタムバリデーション
app.py
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, DateField, SubmitField
from wtforms.validators import DataRequired, ValidationError
from datetime import datetime, date
import re
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
# 仮想的なユーザーデータベース
existing_usernames = ['admin', 'user123', 'testuser']
def validate_username_availability(form, field):
"""ユーザー名の重複チェック"""
if field.data in existing_usernames:
raise ValidationError('このユーザー名は既に使用されています')
def validate_password_strength(form, field):
"""パスワードの強度チェック"""
password = field.data
if len(password) < 8:
raise ValidationError('パスワードは8文字以上で入力してください')
if not re.search(r'[A-Z]', password):
raise ValidationError('パスワードには大文字を含めてください')
if not re.search(r'[a-z]', password):
raise ValidationError('パスワードには小文字を含めてください')
if not re.search(r'[0-9]', password):
raise ValidationError('パスワードには数字を含めてください')
def validate_date_range(form, field):
"""開始日と終了日の期間チェック"""
if form.start_date.data and form.end_date.data:
if form.start_date.data >= form.end_date.data:
raise ValidationError('終了日は開始日より後に設定してください')
# 期間が長すぎないかチェック
days_diff = (form.end_date.data - form.start_date.data).days
if days_diff > 365:
raise ValidationError('期間は1年以内に設定してください')
class AdvancedForm(FlaskForm):
username = StringField('ユーザー名', validators=[
DataRequired(message='ユーザー名は必須です'),
validate_username_availability
])
password = PasswordField('パスワード', validators=[
DataRequired(message='パスワードは必須です'),
validate_password_strength
])
start_date = DateField('開始日', validators=[
DataRequired(message='開始日は必須です')
], format='%Y-%m-%d')
end_date = DateField('終了日', validators=[
DataRequired(message='終了日は必須です'),
validate_date_range
], format='%Y-%m-%d')
submit = SubmitField('登録')
@app.route('/', methods=['GET', 'POST'])
def advanced_form():
form = AdvancedForm()
if form.validate_on_submit():
# フォームデータの処理
user_data = {
'username': form.username.data,
'start_date': form.start_date.data.isoformat(),
'end_date': form.end_date.data.isoformat()
}
print("登録データ:", user_data)
return render_template('advanced_success.html', data=user_data)
return render_template('advanced_form.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
templates/advanced_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高度なバリデーション</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">高度なバリデーション</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control", placeholder="ユーザー名を入力") }}
{% raw %}{% if form.username.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.username.errors %}{% endraw %}
<small>{{ error }}</small><br>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
<div class="form-text">
既存のユーザー名は使用できません
</div>
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control", placeholder="パスワードを入力") }}
{% raw %}{% if form.password.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.password.errors %}{% endraw %}
<small>{{ error }}</small><br>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
<div class="form-text">
パスワードは8文字以上で、大文字・小文字・数字を含めてください
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
{{ form.start_date.label(class="form-label") }}
{{ form.start_date(class="form-control", type="date") }}
{% raw %}{% if form.start_date.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.start_date.errors %}{% endraw %}
<small>{{ error }}</small><br>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
{{ form.end_date.label(class="form-label") }}
{{ form.end_date(class="form-control", type="date") }}
{% raw %}{% if form.end_date.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.end_date.errors %}{% endraw %}
<small>{{ error }}</small><br>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
</div>
</div>
{% raw %}{% if form.end_date.errors %}{% endraw %}
<div class="text-danger mb-3">
{% raw %}{% for error in form.end_date.errors %}{% endraw %}
{% raw %}{% if "開始日" in error %}{% endraw %}
<small>{{ error }}</small><br>
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
{{ form.submit(class="btn btn-primary w-100") }}
</form>
</div>
</div>
</div>
</body>
</html>
templates/advanced_success.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登録完了</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-success text-center">
<h2>登録完了</h2>
<p class="mb-0">登録が正常に完了しました</p>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">登録内容</h5>
<p><strong>ユーザー名:</strong> {{ data.username }}</p>
<p><strong>開始日:</strong> {{ data.start_date }}</p>
<p><strong>終了日:</strong> {{ data.end_date }}</p>
</div>
</div>
<a href="/" class="btn btn-primary w-100 mt-3">最初から</a>
</div>
</div>
</div>
</body>
</html>
上級3: フォームセキュリティ対策
app.py
from flask import Flask, render_template
from flask_wtf import FlaskForm, CSRFProtect
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
from markupsafe import escape
app = Flask(__name__)
app.secret_key = 'your_very_secret_key_here_12345'
# CSRF保護の設定
csrf = CSRFProtect()
csrf.init_app(app)
# レート制限の設定
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri="memory://",
)
class SecureForm(FlaskForm):
name = StringField('お名前', validators=[
DataRequired(message='名前は必須です'),
Length(max=50, message='名前は50文字以内で入力してください')
])
message = TextAreaField('メッセージ', validators=[
DataRequired(message='メッセージは必須です'),
Length(max=1000, message='メッセージは1000文字以内で入力してください')
])
submit = SubmitField('送信')
@app.route('/')
def secure_form():
form = SecureForm()
return render_template('secure_form.html', form=form)
@app.route('/submit', methods=['POST'])
@limiter.limit("3 per minute") # 1分間に3回まで
def submit_secure():
form = SecureForm()
if form.validate_on_submit():
# XSS対策: 入力値のエスケープ
safe_name = escape(form.name.data)
safe_message = escape(form.message.data)
# 安全なデータの処理
print(f"安全なデータ - 名前: {safe_name}, メッセージ: {safe_message}")
return render_template('secure_success.html',
name=safe_name,
message=safe_message)
return render_template('secure_form.html', form=form)
# クリックジャッキング対策
@app.after_request
def set_security_headers(response):
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '1; mode=block'
return response
if __name__ == '__main__':
app.run(debug=True)
templates/secure_form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>セキュアなフォーム</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<h1 class="text-center mb-4">セキュアなコンタクトフォーム</h1>
{% raw %}{% with messages = get_flashed_messages(with_categories=true) %}{% endraw %}
{% raw %}{% if messages %}{% endraw %}
{% raw %}{% for category, message in messages %}{% endraw %}
<div class="alert alert-{{ 'danger' if category == 'error' else 'success' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% raw %}{% endfor %}{% endraw %}
{% raw %}{% endif %}{% endraw %}
{% raw %}{% endwith %}{% endraw %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% raw %}{% if form.name.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.name.errors %}{% endraw %}
<small>{{ error }}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
<div class="mb-3">
{{ form.message.label(class="form-label") }}
{{ form.message(class="form-control", rows="5") }}
{% raw %}{% if form.message.errors %}{% endraw %}
<div class="text-danger">
{% raw %}{% for error in form.message.errors %}{% endraw %}
<small>{{ error }}</small>
{% raw %}{% endfor %}{% endraw %}
</div>
{% raw %}{% endif %}{% endraw %}
</div>
{{ form.submit(class="btn btn-primary w-100") }}
<div class="text-center mt-3">
<small class="text-muted">
セキュリティ対策: CSRF保護, XSS対策, クリックジャッキング対策, レート制限
</small>
</div>
</form>
</div>
</div>
</div>
</body>
</html>
templates/secure_success.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>送信完了</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="alert alert-success text-center">
<h2>送信完了</h2>
<p class="mb-0">メッセージが正常に送信されました</p>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title">送信内容</h5>
<p><strong>名前:</strong> {{ name }}</p>
<p><strong>メッセージ:</strong></p>
<div class="border p-3 bg-light">
{{ message|safe }}
</div>
</div>
</div>
<a href="/" class="btn btn-primary w-100 mt-3">戻る</a>
</div>
</div>
</div>
</body>
</html>
requirements.txt
Flask==2.3.3
Flask-WTF==1.1.1
Flask-Limiter==3.3.0
WTForms==3.0.1