サイト削除
概要¶
APIを使用してサイトを削除することができます。
事前準備¶
APIの操作を行う前にAPIキーの作成を実施してください。また、この機能は「サイトの管理」権限をもつユーザでなければ使用できません。
リクエスト¶
下記のリクエスト形式で、jsonデータを送信します。
| 設定項目 | 値 |
|---|---|
| HTTPメソッド | POST |
| Content-Type | application/json |
| 文字コード | UTF-8 |
| URL | http://{サーバ名}/api/items/{サイトID}/deletesite(※1) |
| Body | 以下のjsonデータを参考のこと |
(※1){サーバ名}、{サイトID}の部分は、適宜、環境に合わせて編集してください。
pleasanter.netの場合は以下の形式になります。
https://pleasanter.net/fs/api/items/{サイトID}/deletesite
{サイトID}には、削除対象のサイトIDを指定してください。
JSON¶
レスポンス¶
下記の形式のJSONデータが返却されます。
サンプルコード¶
Note
コード内の【 ... 】は適宜修正してください。
1. 廃止台帳で承認されたサイトを、対象を確かめてからまとめて削除する
使わなくなったサイトの廃止を「サイト廃止台帳」で申請・承認し、承認済みで廃止予定日を過ぎたサイトをまとめて削除します。削除の前に、サイト取得APIで対象がサイト(フォルダではない)であることと、タイトルが台帳と一致することを確かめ、確認できたものに限ってサイト削除APIで削除します。結果(削除済/削除失敗)は台帳の行に書き戻します。
実務での使いどころ¶
現場でアプリを作る運用が広がると、使い終わったサイトが放置されがちです。廃止の申請と承認を台帳のプロセスで行い、削除をこのスクリプトで定期的に行えば、「誰が・いつ・なぜ廃止したか」を台帳に残しながら、承認済みのサイトを漏れなく片付けられます。保存期間を満了したサイトの廃棄にも使えます。
画面からサイトを削除するには、サイトを1つずつ開いて、タイトル・ログインID・パスワードを入力する必要があります。本サンプルでは、台帳のタイトルとの照合を、画面でタイトルを入力させる確認の代わりにしています。
事前準備¶
1:テナント管理者権限のAPIキーを用意してください。
2:記録テーブルで「サイト廃止台帳」を作成し、以下の項目を有効化してください。
| 項目 | 内容 | 備考 |
|---|---|---|
| タイトル(Title) | 対象サイトのタイトル | 申請時に入力。削除前の照合に使います |
| 分類A(ClassA) | 対象サイトID | 申請時に入力 |
| 日付A(DateA) | 廃止予定日 | 申請時に入力 |
| 日付B(DateB) | 削除日時 | スクリプトが書き込みます |
| 状況(Status) | 申請中(100)/承認済(200)/削除済(900)/削除失敗(910) | 選択肢一覧に設定し、申請・承認はプロセスで行います |
| 内容(Body) | 廃止理由・結果メモ | 削除失敗の理由はスクリプトが追記します |
3:下記サンプルコードのAPI_KEY・LEDGER_SITE_ID(サイト廃止台帳のサイトID)を環境にあわせて書き換えてください。項目や状況の値を変えた場合は、先頭の定数もあわせて書き換えてください。
4:まずDRY_RUN = Trueのまま実行し、削除予定の一覧を確認してから、DRY_RUN = Falseにして実行してください。
Python¶
# ==========================================================
# 「サイト廃止台帳」で申請・承認されたサイトを、廃止予定日を過ぎたらまとめて削除するサンプルです。
#
# 台帳から状況が「承認済」で廃止予定日が実行日以前の行を取得し、行ごとに
# サイト取得API(getsite)で対象を確かめてから、サイト削除API(deletesite)で削除します。
# 結果は台帳の行に書き戻します(削除済/削除失敗)。承認は台帳のプロセスで行い、
# 削除だけをこのスクリプトで行う運用を想定しています。
#
# 【削除の前に必ず getsite で確かめる理由】
# ・deletesite にレコードのIDを渡すと、エラーにならず、そのレコードが属する
# サイトがまるごと削除されます。台帳の入力ミスで無関係なサイトを消さないよう、
# getsite(レコードのIDには404を返す)で対象がサイトであることを確かめます。
# ・フォルダを deletesite で削除すると、配下のサイトも一緒に削除されます。
# そのためフォルダは削除しません。
# ・画面からの削除でタイトルを入力させる確認の代わりに、台帳のタイトルと
# 対象サイトのタイトルが一致することを確かめます。
#
# 【注意】
# ・APIキーはテナント管理者のユーザーで発行したものを使用してください。
# ・deletesite はパスワードの入力なしで削除できます。まず DRY_RUN = True で
# 削除予定の一覧を確認してから、False にして実行してください。
# ・削除したサイトは親フォルダのごみ箱に入り、画面から復元できます。
# ==========================================================
import datetime
import requests
# ==============================
# Pleasanter 接続設定
# ==============================
BASE_URL = "【URL】"
API_KEY = "【APIキー】"
API_VERSION = 1.1
# 台帳の行の更新(items/{id}/update)だけは ApiVersion を 1(整数)にする。
# 1.1 だと成功の応答が返るのに拡張項目(DateB)が反映されないことがあるため
UPDATE_API_VERSION = 1
# ==============================
# サイト廃止台帳の設定(環境にあわせ適宜変更してください)
# ==============================
LEDGER_SITE_ID = "【サイトID】" # サイト廃止台帳(記録テーブル)のサイトID
# タイトル(Title)には対象サイトのタイトルを入力しておく(削除前の照合に使う)
TARGET_SITE_ID_COLUMN = "ClassA" # 対象サイトID
SCHEDULED_DATE_COLUMN = "DateA" # 廃止予定日
DELETED_AT_COLUMN = "DateB" # 削除日時(スクリプトが書き込む)
STATUS_APPROVED = 200 # 承認済
STATUS_DELETED = 900 # 削除済
STATUS_FAILED = 910 # 削除失敗
# True の場合、削除も台帳の更新もせず、削除予定の一覧だけを表示する
DRY_RUN = True
# ==============================
# Pleasanter API操作
# ==============================
def post(url, payload, api_version=API_VERSION):
"""ステータスコードと応答本文(JSONでなければNone)を返す。
deletesite は404のときJSONではなくHTMLのエラーページを返すため、
応答をJSONとして読む前にステータスコードを確かめられるようにしている。"""
body = {"ApiVersion": api_version, "ApiKey": API_KEY}
body.update(payload)
r = requests.post(
url,
json=body,
headers={"Content-Type": "application/json"},
timeout=60,
)
try:
return r.status_code, r.json()
except ValueError:
return r.status_code, None
# ==============================
# 台帳から「承認済」の行を全件取得する
# ==============================
def get_approved_rows():
rows = []
offset = 0
while True:
status, data = post(
f"{BASE_URL}/api/items/{LEDGER_SITE_ID}/get",
{
"Offset": offset,
"View": {"ColumnFilterHash": {"Status": f'["{STATUS_APPROVED}"]'}},
},
)
if status != 200:
raise RuntimeError(f"台帳の取得に失敗しました(HTTP {status})。")
page = data["Response"]["Data"]
rows.extend(page)
offset += len(page) # 取得件数に応じて進める
if len(page) == 0 or offset >= data["Response"]["TotalCount"]:
# 削除を伴うため、絞り込みが効かなかった場合に備えてコード側でも状況を確かめる
return [row for row in rows if row.get("Status") == STATUS_APPROVED]
def scheduled_date(row):
"""廃止予定日を返す。未入力(1899/12/30が返る)の場合はNone。"""
value = (row.get("DateHash") or {}).get(SCHEDULED_DATE_COLUMN)
if not value:
return None
date = datetime.datetime.fromisoformat(value).date()
return date if date.year > 1900 else None
# ==============================
# 削除してよいかの確認。問題があれば理由を、無ければNoneを返す
# ==============================
def check_target(row):
target = ((row.get("ClassHash") or {}).get(TARGET_SITE_ID_COLUMN) or "").strip()
if not target.isdigit():
return None, f"対象サイトID「{target}」が数値ではありません"
site_id = int(target)
# レコードのID・削除済み・存在しないIDは404になる(deletesiteを呼ぶ前に必ず確かめる)
status, data = post(f"{BASE_URL}/api/items/{site_id}/getsite", {})
if status != 200:
return site_id, f"サイトID {site_id} のサイトを取得できません(HTTP {status})"
site = data["Response"]["Data"]
if site.get("ReferenceType") == "Sites":
return site_id, f"サイトID {site_id} はフォルダのため削除しません(配下のサイトも削除されるため)"
if site.get("Title", "") != (row.get("Title") or "").strip():
return site_id, (
f"タイトルが一致しません(台帳:{row.get('Title')}/サイト:{site.get('Title')})"
)
return site_id, None
# ==============================
# 台帳の行の更新
# ==============================
def update_ledger_row(record_id, payload):
status, data = post(
f"{BASE_URL}/api/items/{record_id}/update", payload, api_version=UPDATE_API_VERSION
)
if status != 200:
print(f"[ERROR] 台帳の行 {record_id} を更新できませんでした(HTTP {status})")
def mark_failed(row, record_id, reason, now):
body = row.get("Body") or ""
note = f"[{now:%Y/%m/%d %H:%M}] 削除失敗:{reason}"
update_ledger_row(
record_id,
{"Status": STATUS_FAILED, "Body": f"{body}\n{note}" if body else note},
)
def mark_deleted(record_id, now):
update_ledger_row(
record_id,
{"Status": STATUS_DELETED, DELETED_AT_COLUMN: f"{now:%Y/%m/%d %H:%M:%S}"},
)
# ==============================
# メイン処理
# ==============================
def main():
now = datetime.datetime.now()
today = now.date()
counts = {"削除済": 0, "削除失敗": 0, "スキップ": 0}
if DRY_RUN:
counts = {"削除予定": 0, "削除失敗(予定)": 0, "スキップ": 0}
for row in get_approved_rows():
record_id = row.get("ResultId") or row.get("IssueId")
label = f"台帳 {record_id}「{row.get('Title')}」"
# 廃止予定日が未入力・まだ来ていない行は対象外
date = scheduled_date(row)
if date is None or date > today:
counts["スキップ"] += 1
continue
site_id, reason = check_target(row)
if reason:
if DRY_RUN:
print(f"[削除失敗(予定)] {label}:{reason}")
counts["削除失敗(予定)"] += 1
else:
print(f"[削除失敗] {label}:{reason}")
mark_failed(row, record_id, reason, now)
counts["削除失敗"] += 1
continue
if DRY_RUN:
print(f"[削除予定] {label}:サイトID {site_id}(廃止予定日 {date:%Y/%m/%d})")
counts["削除予定"] += 1
continue
status, data = post(f"{BASE_URL}/api/items/{site_id}/deletesite", {})
if status != 200:
reason = f"deletesite がHTTP {status}を返しました"
print(f"[削除失敗] {label}:{reason}")
mark_failed(row, record_id, reason, now)
counts["削除失敗"] += 1
continue
print(f"[削除済] {label}:サイトID {site_id}")
mark_deleted(record_id, now)
counts["削除済"] += 1
print("=== 結果 ===" + ("(DRY_RUN:削除・台帳の更新はしていません)" if DRY_RUN else ""))
for kind, count in counts.items():
print(f"{kind}: {count}件")
if __name__ == "__main__":
main()
- サイト削除API(
deletesite)にレコードのIDを渡すと、エラーにならず、そのレコードが属するサイトがまるごと削除されます。 台帳の入力ミスで無関係なサイトを消さないよう、削除の前に必ずサイト取得API(getsite)で確かめてください(getsiteはレコードのIDには404を返します)。本サンプルのcheck_targetを省かないでください。 - フォルダを
deletesiteで削除すると、配下のサイトも一緒に削除されます。そのため本サンプルはフォルダを削除しません。 deletesiteはパスワードの入力なしで削除できます。削除したサイトは親フォルダのごみ箱に入り、画面から復元できます。誤って削除した場合は、ごみ箱から復元してください。- 存在しないIDを
deletesiteに渡すと、404でJSONではなくHTMLのエラーページが返ります。そのため、応答をJSONとして読む前にステータスコードを確かめています。 - 台帳の行の更新(
items/{id}/update)では、ApiVersionを1(整数)にしています。1.1だと、成功の応答が返るのに日付Bなどの拡張項目が反映されないことがあるためです。 - 承認済みの行のうち、廃止予定日が未入力またはまだ来ていない行は「スキップ」として件数のみを数えます。
- 削除するサイトがほかのテーブルからリンクされている場合の扱いは、本サンプルでは考慮していません。申請時に利用者へ確認する運用としてください。
対応バージョン¶
| 対応バージョン | 内容 |
|---|---|
| 1.3.4.0 以降 | 機能追加 |
