コンテンツにスキップ

サイト削除

概要

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
{
    "ApiVersion": "1.1",
    "ApiKey": "345yuAjA6789dA09d8uj6..."
}

レスポンス

下記の形式のJSONデータが返却されます。

{
    "Id": 12345,
    "StatusCode": 200,
    "Message": "\" 削除されたフォルダ \" を削除しました。"
}

サンプルコード

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 以降 機能追加