はじめに
Flaskの基本文法を一通り学んだあと、多くの初学者がつまずくのが「実際のアプリでは、どの機能をどの順番で作ればよいのか」という点です。
ルーティング、テンプレート、フォーム、データベース、ログイン機能などは、それぞれ単体で学ぶこともできます。しかし、実務のWebアプリでは、それらを組み合わせて1つのシステムとして動かす必要があります。
そこで本演習では、社内向けの **備品貸出・在庫管理システム** を題材にします。
このアプリでは、社員がノートPC、モニター、ケーブル、プロジェクターなどの備品を検索し、必要な備品を借りる申請を行います。管理者は申請を確認し、承認または却下します。返却された備品の状態も管理できるため、単なる一覧アプリではなく、実務に近い業務システムとして学習できます。
作成するアプリの概要
アプリ名は仮に **EquipFlow** とします。
EquipFlowは、次のような機能を持つ社内用Webアプリです。
- – ユーザーがログインできる
- – 備品一覧を検索できる
- – 備品の詳細を確認できる
- – 備品の貸出申請ができる
- – 管理者が申請を承認・却下できる
- – ユーザーが返却登録できる
- – 管理者が備品マスタを登録・編集・削除できる
- – 備品写真をアップロードできる
- – APIで備品や申請情報を取得できる
- – 操作履歴を監査ログとして保存できる
- – テストとセキュリティ対策を実装できる
初学者向けに言い換えると、これは **ログイン付きの在庫管理アプリ** です。最初はログイン画面と一覧画面だけでも十分ですが、少しずつ機能を追加することで、実務アプリに必要な考え方をまとめて学べます。

この演習で学ぶ技術
このアプリでは、Flaskの基本から実務寄りの機能まで段階的に扱います。
主な技術は以下です。
| 分野 | 使用する技術 | 役割 |
|---|---|---|
| Webアプリ本体 | Flask | URLと処理をつなぐ |
| 画面表示 | Jinja2 | HTMLテンプレートを作る |
| デザイン | Bootstrap / CSS | 管理画面を見やすくする |
| フォーム | Flask-WTF / WTForms | 入力フォームとバリデーションを作る |
| データベース | SQLAlchemy | ユーザー、備品、申請を保存する |
| DB変更管理 | Flask-Migrate | テーブル変更を履歴管理する |
| ログイン | Flask-Login | 認証状態を管理する |
| 画像処理 | Pillow | 備品写真のサムネイルを作る |
| 非同期処理 | Celery | メール通知や期限超過チェックを裏側で実行する |
| 外部連携 | requests | 地図APIなどからデータを取得する |
| API | FlaskのJSONレスポンス | 外部システム向けの入口を作る |
| テスト | pytest | 機能が壊れていないか確認する |
| セキュリティ | CSRF/XSS/SQLi対策 | 安全なアプリにする |
要件定義:何を作るのかを決める
目的
- 備品の所在・貸出状況を一元管理し、貸出手続きと監査性を向上する。
対象ユーザー
- 一般ユーザー:貸出申請、返却登録、自分の履歴閲覧
- 管理者:備品マスタ管理、申請承認、ユーザー/ロール管理、監査ログ確認
前提/制約
- 認証必須(ログインなしアクセス不可)
- 備品写真のアップロードは画像のみ(png/jpg/webp)
- 期限超過通知は定期タスクで送信
- 監査ログは改ざん防止のため更新不可(追記のみ)
- APIはJSON、認証はセッションクッキー(管理画面)+APIトークン(API)
非機能要件
- セキュリティ:CSRF、XSS、SQLi対策、パスワードハッシュ、権限分離
- 可用性:想定同時利用 50人、ピーク時も検索が2秒以内
- 保守性:マイグレーション管理、構造化ログ、テストカバレッジ70%以上
- 性能:備品一覧はページネーション、検索結果はキャッシュ(短時間)
画面遷移
- ログイン → ダッシュボード
- ダッシュボード
- 備品一覧 → 備品詳細 →(貸出申請)
- マイページ(自分の貸出履歴)→ 返却登録
- (管理者のみ)申請一覧 → 申請詳細 → 承認/却下
- (管理者のみ)備品管理(一覧→新規→編集)
- (管理者のみ)ユーザー管理(一覧→詳細→ロール変更)
- (管理者のみ)監査ログ閲覧
- エラーページ(403/404/500)は共通レイアウトで遷移
ログイン
↓
ダッシュボード
↓
備品一覧 → 備品詳細 → 貸出申請
↓
マイページ → 自分の貸出履歴 → 返却登録
```
管理者の場合は、さらに次のような画面も使います。
```text
管理者ダッシュボード
↓
申請一覧 → 申請詳細 → 承認 / 却下
↓
備品管理 → 新規登録 / 編集 / 削除
↓
ユーザー管理 → ロール変更
↓
監査ログ確認
機能要件
1. 認証・認可
認証とは「あなたは誰ですか?」を確認する仕組みです。ログイン機能がこれに当たります。認可とは「あなたはこの操作をしてよいですか?」を確認する仕組みです。管理者だけが備品を削除できる、といった制限がこれに当たります。
Flaskでは、認証には `Flask-Login` を使うと便利です。ユーザー登録時にはパスワードをそのまま保存せず、必ずハッシュ化して保存します。
たとえば、一般ユーザーが `/admin/items` にアクセスした場合は、403 Forbiddenとして拒否します。画面にボタンを表示しないだけでなく、URLを直接入力されても処理側で拒否することが重要です。
- ユーザー登録、ログイン、ログアウト
- パスワードはハッシュ化して保存
- ロール(user/admin)によるアクセス制御(デコレータ)
2. 備品管理(管理者)
備品管理は、管理者向けの中心機能です。登録する情報の例は次のとおりです。
- 管理番号
- 備品名
- カテゴリ
- 状態
- 保管場所
- 説明文
- 備品写真
状態には、たとえば次のような値を用意します。
| 状態 | 意味 |
|---|---|
| available | 利用可能 |
| borrowed | 貸出中 |
| repair | 修理中 |
| disposed | 廃棄済み |
備品写真を登録できるようにすると、利用者がどの備品か判断しやすくなります。画像はアップロード後にサムネイルを作成し、一覧画面では軽い画像を表示するようにします。
- 備品のCRUD
- 状態(利用可/貸出中/修理中/廃棄)管理
- 画像アップロード、サムネイル自動生成、表示最適化
3. 貸出フロー(一般+管理者)
このアプリの業務的な中心は、貸出フローです。一般ユーザーは、備品詳細画面から貸出申請を行います。申請には、開始日、返却予定日、理由を入力します。
管理者は申請一覧を見て、承認または却下を行います。承認された場合、備品の状態は `available` から `borrowed` に変わります。
返却時には、ユーザーまたは管理者が返却登録を行います。返却された備品が問題なければ `available` に戻し、故障していれば `repair` に変更します。このように、申請の状態と備品の状態を連動させることが大切です。
- 一般:貸出申請(開始日・返却予定日・理由)
- 管理者:承認/却下、却下理由登録
- 承認後、備品は「貸出中」に遷移
- 返却登録により「利用可」に戻す(破損時は「修理中」も選択可)
4. 検索・一覧
備品が増えてくると、単純な一覧だけでは使いづらくなります。
そこで、次のような検索機能を追加します。
- キーワード検索
- カテゴリ検索
- 状態検索
- 拠点検索
- ページネーション
ページネーションとは、一覧を1ページにすべて表示せず、10件ずつ、20件ずつのように分けて表示する仕組みです。また、データベースから関連データを取得するときには、N+1問題にも注意します。初学者の段階では難しく感じるかもしれませんが、「一覧画面が遅くなる原因の1つ」として覚えておくとよいです。
- 備品一覧:カテゴリ/状態/キーワードで検索、ページネーション
- 貸出履歴:ユーザー別、期間フィルタ
- クエリ最適化(N+1回避)、必要に応じてインデックス
5. 通知(非同期)
貸出申請が承認されたときや却下されたときには、ユーザーにメール通知を送ると便利です。ただし、メール送信を画面表示の処理と同時に行うと、ユーザーが待たされることがあります。そのため、Celeryなどを使って非同期処理にします。
非同期処理とは、画面の処理とは別に、裏側で時間のかかる処理を実行する方法です。このアプリでは、たとえば次のような処理を非同期化できます。
- 承認・却下時メール通知(Celery)
- 期限超過の確認
- 期限超過の定期チェック(1日1回)で督促メール
6. 外部API連携
備品には保管場所があります。たとえば「東京本社 3階」「福岡オフィス 倉庫A」などです。住所情報をもとに地図APIを使うと、緯度・経度を取得できます。この処理をジオコーディングと呼びます。
Flask側では `requests` を使って外部APIにアクセスします。APIキーはコードに直接書かず、環境変数で管理します。外部APIを使うときは、次の点に注意します。
- 拠点住所から地図ジオコーディング(requests)
- 通信に失敗する可能性がある
- タイムアウトを設定する
- 取得したデータを必要な形に整える
- 備品保管拠点の地図表示用に緯度経度を保持
- APIキーは環境変数で管理、ログに出さない
7. REST API
管理画面は人間がブラウザで使う画面です。一方、REST APIは外部システムやJavaScriptからデータを取得・操作するための入口です。このアプリでは、次のようなAPIを用意します。
- GET /api/items(検索+ページネーション)
- GET /api/items/
- POST /api/requests(申請)
- GET /api/requests/mine(自分の申請)
- 管理者向け:PATCH /api/requests/(承認/却下)
- エラー時はJSONで統一(コード、メッセージ、詳細)
APIでは、HTMLではなくJSONを返します。エラー時も形式を統一し、`code`、`message`、`details` のような情報を返すと扱いやすくなります。
8. エラーハンドリング/ログ
実務のアプリでは、エラーが起きないことよりも、エラーが起きたときに安全に処理できることが重要です。たとえば、存在しない備品IDにアクセスされた場合は404ページを表示します。権限がない画面にアクセスされた場合は403ページを表示します。予期しないエラーが起きた場合は500ページを表示します。
ユーザーには分かりやすいメッセージを表示し、開発者向けの詳しいエラー情報はログに残します。ログには、「発生時刻 / ユーザーID / URL / エラー内容 / IPアドレス 」のような情報を残すと調査しやすくなります。
- 403/404/500のカスタムページ
- 例外は適切に握り、監査ログ+アプリログへ記録
- ログはリクエストIDを付与
9. テスト
- 単体:モデル、サービス層、バリデーション
- 統合:主要画面・API・権限
- カバレッジ計測(閾値設定)
テーブル設計書(案)
このアプリでは、少なくとも次のテーブルを使います。たとえば、備品情報とユーザー情報と申請情報を1つのテーブルにまとめると、あとから検索や集計が難しくなります。ユーザー、備品、申請を分け、それぞれを外部キーでつなげることで、管理しやすいデータ構造になります。
users
- id(PK)
- email(Unique, NotNull)
- password_hash(NotNull)
- name(NotNull)
- is_active(Bool, default true)
- created_at, updated_at
roles
- id(PK)
- name(Unique) 例: user/admin
user_roles(多対多)
- user_id(FK users.id, PK)
- role_id(FK roles.id, PK)
locations(拠点)
- id(PK)
- name(NotNull)
- address(NotNull)
- latitude, longitude(ジオコーディング結果)
- created_at, updated_at
items(備品)
- id(PK)
- asset_tag(Unique, NotNull) 管理番号
- name(NotNull)
- category(NotNull) 例: pc/monitor/other
- status(NotNull) 例: available/borrowed/repair/disposed
- location_id(FK locations.id)
- description(Text)
- image_path
- thumb_path
- created_at, updated_at
- index: (status), (category), (location_id)
loan_requests(貸出申請)
- id(PK)
- user_id(FK users.id, NotNull)
- item_id(FK items.id, NotNull)
- start_date(NotNull)
- due_date(NotNull)
- reason(Text, NotNull)
- status(NotNull) 例: pending/approved/rejected/returned
- decision_by(FK users.id, Null) 承認者
- decision_note(Text, Null)
- decided_at(Null)
- returned_at(Null)
- created_at, updated_at
- index: (user_id, status), (item_id, status), (due_date)
audit_logs(監査ログ)
- id(PK)
- actor_user_id(FK users.id, Null) 未ログイン操作の可能性考慮
- action(NotNull) 例: ITEM_CREATE, REQUEST_APPROVE
- target_type(NotNull) 例: item/request/user
- target_id(NotNull)
- detail_json(JSON/Text)
- ip_address
- user_agent
- created_at
- 更新/削除禁止(アプリ側で制御)
結合テスト(主要観点)
単体テストは、1つの関数や1つのモデルが正しく動くかを確認します。一方、結合テストでは、複数の機能をつなげて確認します。たとえば、次のような流れをテストします。
IT-01 認証~一覧表示
- 前提:一般ユーザーが存在
- 手順:ログイン→備品一覧表示→検索→ページ遷移
- 期待:未ログイン時はログインへリダイレクト、検索条件が反映、ページネーション正常
IT-02 申請~承認~状態遷移
- 前提:備品がavailable、管理者が存在
- 手順:一般が申請→管理者が承認→備品詳細確認
- 期待:申請status=approved、備品status=borrowed、承認メールが非同期キューに積まれる
IT-03 却下フロー
- 手順:申請→却下(理由必須)
- 期待:status=rejected、備品statusはavailableのまま、却下理由が履歴に残る
IT-04 返却登録
- 手順:承認済み申請を返却登録(返却時状態選択)
- 期待:status=returned、returned_at設定、備品statusがavailableまたはrepairへ
IT-05 画像アップロード
- 手順:備品にjpgをアップロード→詳細表示
- 期待:拡張子/ContentTypeチェック、サムネ生成、パスがDBに保存、一覧はサムネ表示
IT-06 API認証と権限制御
- 手順:トークン無しでPOST /api/requests→失敗、一般トークンで管理PATCH→失敗、管理トークンで成功
- 期待:401/403が正しく返る、エラーJSON形式統一
IT-07 例外とエラーページ
- 手順:存在しない備品IDアクセス、権限無し画面アクセス
- 期待:404/403がカスタムページで表示、ログに記録される
IT-08 期限超過通知(疑似)
- 手順:due_dateが過去のapprovedを作成→定期タスク相当処理を実行
- 期待:対象抽出が正しい、通知が重複送信されない(送信済みフラグ or auditで制御)
受け入れテスト仕様書(抜粋)
受け入れテストでは、開発者目線ではなく利用者目線で確認します。たとえば、次のような観点です。
- – 一般ユーザーが迷わず備品を借りられるか
- – 管理者が備品台帳を管理できるか
- – 不正な操作が拒否されるか
- – 画面のエラー表示が分かりやすいか
- – 検索が遅すぎないか
- – 障害時にログから原因を追跡できるか
受け入れテストまで考えることで、単なる学習用アプリではなく、実務に近い完成度を目指せます。
UAT-01 基本業務が完遂できること
- シナリオ:一般ユーザーが備品を探し、申請し、承認され、返却まで完了
- 合格条件:各ステップで画面・メール通知・状態遷移が矛盾なく完了
UAT-02 管理者が備品台帳を維持できること
- シナリオ:備品の新規登録、画像登録、拠点紐付け、廃棄まで実行
- 合格条件:一覧/検索に反映、監査ログに記録、誤操作時もエラーが分かりやすい
UAT-03 セキュリティ要件を満たすこと
- シナリオ:CSRFが必要なフォームでトークン無し送信、XSSを含む入力、SQLi風入力
- 合格条件:送信拒否/無害化、エラーが適切、データ破壊が起きない、機密がログに出ない
UAT-04 性能要件を満たすこと
- シナリオ:備品1000件、貸出履歴5000件相当データで一覧検索
- 合格条件:主要検索が2秒以内、ページネーションが破綻しない、N+1が発生しない
UAT-05 運用要件(障害時の追跡)
- シナリオ:意図的に例外を起こす、404を発生させる
- 合格条件:ログにリクエストID・ユーザー・エラーが残り、原因追跡が可能
セキュリティと性能
最後に、セキュリティと性能を確認します。
代表的な対策は次の通りです。
| リスク | 対策 |
|---|---|
| CSRF | Flask-WTFのCSRFトークンを使う |
| XSS | Jinja2の自動エスケープを活かす |
| SQLインジェクション | SQLAlchemyの安全なクエリを使う |
| 権限突破 | サーバー側でロールチェックする |
| パスワード漏えい | ハッシュ化して保存する |
| 不正アップロード | 拡張子、MIMEタイプ、サイズを確認する |
| 検索遅延 | ページネーション、インデックス、キャッシュを使う |
Flask初学者の場合、最初からすべてを完璧に作る必要はありません。ただし、「あとで必要になる品質」を知ったうえで段階的に作ることが重要です。