はじめに
ExcelのWBSを進捗管理ツールに置き換えた話の2本目です。
- ExcelのWBSを捨てずに、AWSサーバーレスの進捗管理ツールへ置き換えた
- Excelとの往復を壊さない取込の作り方(下見・全件主義・列の持ち方)(この記事)
- 同じAWSアカウントに似た名前のスタックが同居しているときのデプロイの守り方
- テストが通っても安心できないので、実装をわざと壊して確かめる仕組みを作った
- 作った進捗管理ツールに入力が集まらなかったので、入れてもらうための機能を足した
このツールでは、Excel に出力して、直して、また取り込む、という往復ができます。往復のどこかで食い違うと、使う人には「なぜか取り込めない」「値が消えた」としか見えません。この記事では、往復を壊さないために決めたことと、実際に壊しかけたところを書きます。
往復のしくみ
出力する Excel には、画面に出さない列が2つあります。
| 隠し列 | 中身 | 使い道 |
|---|---|---|
| タスクID | タスクごとの不変の ID | 取り込むときに、元のタスクと突き合わせる |
| バージョン | 出力した時点の版数 | 出力したあとに画面で直されていないかを見る |
WBS 番号は行の挿入で振り直されるので、突き合わせには使えません。タスクIDが空の行は新規として扱います。
テンプレートから書いた Excel はタスクIDが空なので、何度取り込んでも新規になります。既存のタスクを直すときは「画面から出力したファイルを直す」のが決まりです。テンプレートを誤って取り込んだときに備えて、タスクIDが空なのに WBS 番号が既存と重なる行はエラーにしています。
1件でもエラーなら全件取り込まない
取込では、すべての行を検査してエラーを集め、1件でもあれば何も書き込みません。
途中まで書き込んで止まると、「どこまで入ったのか」を使う人が調べることになります。全部入るか、何も入らないかのどちらかにしておけば、エラーを直してもう一度取り込むだけで済みます。
エラーは行番号つきで全件返します。1件ずつ直して取り込み直す、を繰り返させないためです。
| 検査 | 内容 |
|---|---|
| WBS 番号 |
1 / 1.2 / 1.2.3 の形、重複なし、親の行があること |
| 日付 | 開始が終了より後になっていないこと。実績は終了日だけの入力を認めない |
| 進捗率 | 0〜100 の整数 |
| 競合 | 出力したあとに画面で直された行を、古い内容で上書きしようとしていないこと |
競合の検査では、隠し列のバージョンと DB の版数を比べます。DB のほうが進んでいて、しかも内容が違う行があれば、取込全体を止めます。バージョン列を持たない古いファイルは、この検査を飛ばします。
下見してから取り込む
以前は、ファイルを選ぶとそのまま反映していました。本番のタスクをまとめて書き換える操作なので、押す前に何が起きるかを見せる形に変えました。
下見で見せるのは次のものです。
| 項目 | 内容 |
|---|---|
| 件数 | 新規・更新・変更なし・Excel に無い |
| 明細 | 増える行と変わる行(WBS 番号つき、最大100件) |
| 自動採番の結果 | WBS 番号を空欄にした行が、何番で入るか |
WBS 番号を空欄にした行は「直前の行と同じ階層で、最大の番号 + 1」で採番します。この規則で期待どおりの番号になるかは、番号を見ないと分かりません。そのため、採番した結果を下見に出しています。
下見と実行は同じ関数を通す
下見と実行を別々に書くと、「下見では通ったのに、実行で落ちる」が起きます。そこで、両方とも同じ関数を通し、違いは書き込むかどうかだけにしました。
def 取り込む(project_id, import_id, bucket, key, 下見):
"""パースして突合する。下見なら書き込まない。
下見と実行で**同じ道を通る**ようにしてある。
別の道にすると、下見では通ったのに実行で落ちる、が起きる。
違うのは書き込むかどうかだけ。
"""
実行のときは、ファイルをもう一度読んで全部検査し直します。下見から時間がたって画面で直されていても、ここで競合として止まります。それでも、見せた内容と実際が離れすぎないように、下見は1時間で期限切れにしています。実行は1回しかできません。
取込の最中は画面から書き換えさせない
取込は、DB の内容を読んでから、まとめて書き込みます。その間に画面から更新が入ると、取込が読んだあと・書く前の値に、取込の内容が上書きされます。どちらの意図とも違う状態になります。
隠し列のバージョンによる競合検査は、取り込む Excel の側の話です。取込の最中に画面から入る更新は、この検査を通りません。そこで API 側で止めています。
def assert_no_import_running(project_id):
running = running_import(project_id)
if running is None:
return
raise ApiError(
409, "IMPORT_IN_PROGRESS",
f"Excel取込の処理中です({running.get('createdBy') or '実行者不明'})。"
"終わるまでタスクの追加・編集・削除はできません。"
"取込は最大5分ほどで終わります。",
)
止めるのは、追加・編集・削除・一括削除・新しい取込の開始の5つです。下見が終わって確認を待っている間は止めません。DB をまだ書いていないからです。
止めっぱなしにしない
この仕組みを入れるとき、取込の Lambda が途中で落ちた場合を考えました。状態が「処理中」のまま残ると、その案件は二度と編集できなくなります。
そこで、状態ごとに上限を置き、それを過ぎた取込は「動いていない」とみなすようにしました。
| 状態 | 上限 |
|---|---|
| アップロード待ち | 5分 |
| 下見の処理中 | 10分 |
守るための仕組みは、壊れたときに閉じ込める仕組みにもなります。止める仕組みを入れるときは、解ける仕組みも一緒に入れるようにしています。
列の位置を英字で書かない
工数の列を足す作業の途中で、出力の処理に「H列が進捗率」のような、列の位置を英字で直接書いたところが残っているのに気づきました。列を1つ足すと、日付の書式も入力規則も条件付き書式も、1列ずつずれます。cdk synth もテストも通り、開いた Excel だけがおかしくなります。
いまは、列の並びを1か所にだけ書き、位置はそこから作っています。
HEADERS = [
("WBS番号", 12, "wbsCode"),
("タスク名", 36, "name"),
("担当者", 14, "assignee"),
# …
]
列番号 = {field: i for i, (_, _, field) in enumerate(HEADERS, start=1)}
列 = {field: get_column_letter(i) for field, i in 列番号.items()}
書式を付けるときは 列["progress"] のように項目名で引きます。テストの側も同じで、ws.column_dimensions["K"] のような書き方はしていません。
ファイルに無い列は触らない
同じ作業の中で、もう1つ見つかりました。工数の列を足す前に出力した古いファイルを取り込むと、入っていた工数が消える作りになっていました。
ファイルに工数の列が無いので、読み取った値は空になります。それを「空欄に直された」と受け取って、DB の値を上書きしていました。
いまは、ファイルにあった列を覚えておき、無い列は更新の対象から外しています。
# 読み取り側:ファイルにあった列を覚える
file_meta["_columns"] = sorted(col_map.keys())
# 突き合わせ側:あった列だけを比べて書く
あった列 = (file_meta or {}).get("_columns")
取り込む列 = ([f for f in COMPARABLE_FIELDS if f in あった列]
if あった列 else list(COMPARABLE_FIELDS))
取り込んだ覚えのない項目が空になる、という壊れ方は、気づくまでに時間がかかります。
返さない項目は、保存のたびに消える
往復は Excel だけではありません。画面とも往復しています。
画面の編集モーダルは、編集できる項目を全部まとめて送ります。ところが、一覧を返す API が「備考」を返していませんでした。画面は備考を知らないまま空で送るので、保存するたびに備考が消えます。
本番の備考が0件だったので、誰も踏んでいませんでした。項目を足すときは、受け取る側と返す側の両方に足すことにしています。
往復そのものをテストする
いちばん頼りにしているのは、往復のテストです。
- テスト用のタスクを Excel に出力する
- そのファイルを、何も直さずに取込の処理へ読ませる
- 「変更なし」と判定されることを確かめる
出力と取込のどちらかだけを直して食い違うと、このテストが落ちます。列を足したときのずれも、ここで気づけるようにしています。
おわりに
Excel を捨てない設計にしたことで、取込と出力はこのツールでいちばん手のかかる部分になりました。ここで決めたことをまとめます。
| 決めたこと | 防いでいること |
|---|---|
| WBS 番号ではなく不変の ID で突き合わせる | 行の挿入で別のタスクに上書きされる |
| 1件でもエラーなら全件取り込まない | どこまで入ったか分からない状態 |
| 下見と実行を同じ関数で通す | 下見では通ったのに実行で落ちる |
| 取込中は画面からの書き換えを止め、上限で解く | 取込との上書き合い、落ちた取込への閉じ込め |
| 列の位置は1か所から作る | 列を足したときの書式のずれ |
| ファイルに無い列は触らない | 古いファイルで値が消える |
次の記事では、似た名前のスタックが同じ AWS アカウントに同居している中で、デプロイ先を取り違えないようにした話を書きます。