コンテンツにスキップ

サイト更新

概要

APIを使用してサイト設定を更新することができます。

事前準備

APIの操作を行う前にAPIキーの作成を実施してください。また、この機能は「サイトの管理」権限をもつユーザでなければ使用できません。

リクエスト

下記のリクエスト形式で、jsonデータを送信します。

設定項目 値
HTTPメソッド POST
Content-Type application/json
文字コード UTF-8
URL http://{サーバ名}/api/items/{サイトID}/updatesite(※1)
Body 以下のjsonデータを参考のこと

(※1){サーバ名}、{サイトID}の部分は、適宜、環境に合わせて編集してください。
  pleasanter.netの場合は以下の形式になります。
  https://pleasanter.net/fs/api/items/{サイトID}/updatesite
  {サイトID}には、更新対象のサイトIDを指定してください。

JSON

{
    "ApiVersion": "1.1",
    "ApiKey": "345yuAjA6789dA09d8uj6...",
    "TenantId": 1,
    "Title": "サイト名",
    "ReferenceType": "Issues",
    "InheritPermission": 99999,
    "SiteSettings": {
        "Version": 1.017,
        "ReferenceType": "Issues",
        "GridColumns": [
            "IssueId",
            "TitleBody",
            "Comments",
            "StartTime",
            "CompletionTime",
            "WorkValue",
            "ProgressRate",
            "RemainingWorkValue",
            "Status",
            "Manager",
            "Owner",
            "Updator",
            "UpdatedTime"
        ],
        "EditorColumnHash": {
            "General": [
                "IssueId",
                "Ver",
                "Title",
                "Body",
                "StartTime",
                "CompletionTime",
                "WorkValue",
                "ProgressRate",
                "RemainingWorkValue",
                "Status",
                "Manager",
                "Owner",
                "Comments"
            ]
        }
    },
    "VerUp": true
}

※TenantID以降のパラメータについては、サイトパッケージをエクスポートした際の、"Site”パラメータと同等の値を設定してください。

サンプルコード

Note

コード内の【 ... 】は適宜修正してください。

1. 組織改編後にサイトのアクセス権を旧組織から新組織へまとめて付け替える

組織の統合・分割・改称のあと、旧組織に付いていたサイトのアクセス権を新組織へまとめて付け替えます。「旧組織コード→新組織コード」の対応表と、対象サイトのID一覧をCSVで用意して実行すると、サイトごとにサイト取得APIでアクセス権を読み、旧組織の分を新組織に置き換えて、サイト更新APIで書き戻します。結果(変更前後のアクセス権)はCSVに出力します。

実務での使いどころ

組織改編は年度替わりなどに毎年起き、そのたびに「旧組織に付いていたアクセス権を新組織に付け直す」作業が発生します。アクセス権はサイトごとに「テーブルの管理」の「アクセス制御」タブで設定するため、サイトが多いと付け替え漏れが起きがちです。

  • 統合(例:営業1課・営業2課→営業部)は、対応表で複数の旧組織を同じ新組織に対応させます。権限が異なる場合は、両方の権限を合わせた(ビットの論理和をとった)権限を付けます。
  • 分割(例:開発部→開発1課・開発2課)は、対応表で1つの旧組織を複数の新組織に対応させます。それぞれに同じ権限を付けます。
  • 結果CSVには変更前(Before)のアクセス権も残るため、誤りがあった場合はそれをもとに手で戻せます。

なお、全サイトがフォルダからアクセス権を継承している環境なら、継承元のフォルダを1か所直せば済みます。本サンプルは、個別にアクセス権を設定したサイトが多い環境を想定しています。

事前準備

1:テナント管理者権限のAPIキーを用意してください。
2:旧組織は削除せず、無効化しておいてください(無効化した組織も、組織コードからIDを引けます)。
3:以下の2つのCSVファイル(UTF-8。BOMの有無は問いません)をinputフォルダに用意してください。

対象サイト(sites.csv):見出しをSiteIdとし、1行に1つサイトIDを書きます。フォルダ配下のサイトはAPIで列挙できないため、対象のサイトIDは利用者が用意します。

SiteId
101
102
103

組織の対応表(dept_mapping.csv):

OldDeptCode,NewDeptCode
D100,D300
D200,D300
D400,D410
D400,D420

上の例では、D100とD200をD300に統合し、D400をD410とD420に分割します。

4:下記サンプルコードのAPI_KEYを環境にあわせて書き換えてください。
5:まずDRY_RUN = Trueのまま実行し、output/result.csvで予定の変更を確認してから、DRY_RUN = Falseにして実行してください。

結果(output/result.csv。BOM付きUTF-8)の列は次のとおりです。

列名 内容
SiteId/Title 対象サイト
Before/After 変更前後のアクセス権(種別,ID,権限値を;で区切ったもの)
Result 更新/変更なし/継承のためスキップ(継承元ID)/取得失敗(HTTPステータス)/更新失敗(HTTPステータス)/要確認(読み直すと表示されない権限)。DRY_RUNのときは、更新する予定のサイトが「更新予定(DRY_RUN)」になります
Python
# ==========================================================
# 組織の統合・分割・改称のあと、旧組織に付いていたサイトのアクセス権を
# 新組織へまとめて付け替えるサンプルです。
#
# 「旧組織コード→新組織コード」の対応表(dept_mapping.csv)と、
# 対象サイトのID一覧(sites.csv)を読み込み、サイトごとに
# サイト取得API(getsite)でアクセス権を読み、旧組織の分を新組織に置き換えて、
# サイト更新API(updatesite)で書き戻します。結果は result.csv に出力します。
#
# 【対応表の書き方】
#   ・同じ新組織に複数の旧組織を対応させると「統合」(権限値はビットの論理和でまとめる)
#   ・同じ旧組織に複数の新組織を対応させると「分割」(それぞれに同じ権限を付ける)
#
# 【注意】
#   ・APIキーはテナント管理者のユーザーで発行したものを使用してください。
#   ・旧組織は削除せず、無効化しておいてください(無効化した組織も組織コードからIDを引けます)。
#   ・アクセス権を継承しているサイトは更新しません(継承元のサイトを一覧に入れてください)。
#   ・updatesite には InheritPermission と Permissions だけを送ります。
#     SiteSettings を含めて送ると、一覧・エディタ・サーバスクリプトなどの設定全体が
#     置き換わってしまうためです。
#   ・まず DRY_RUN = True で予定の変更を result.csv で確認してから、False にして実行してください。
#   ・組織をAPIやインポートで続けて作成・更新した直後は、Pleasanter内部の組織情報が
#     古いままになり、一部の組織の権限がサイト取得API(getsite)に表示されないことが
#     あります(権限そのものは保存されています)。表示されない権限は、読み取って
#     書き戻すと消えてしまいます。組織をまとめて作成・更新した直後に実行するときは、
#     先にいずれかの組織を1件更新(画面で保存し直すだけでよい)してから実行してください。
#     時間をおくだけでは解消しません。
#     更新後に読み直して、送った権限が表示されないサイトは「要確認」として出力します。
# ==========================================================
import csv
import sys
from pathlib import Path

import requests

# ==============================
# Pleasanter 接続設定
# ==============================
BASE_URL = "【URL】"
API_KEY = "【APIキー】"

API_VERSION = 1.1

# ==============================
# ファイル設定(環境にあわせ適宜変更してください)
# ==============================
INPUT_DIR = Path("./input")
SITES_CSV = INPUT_DIR / "sites.csv"                # 見出し SiteId、1行1サイトID
MAPPING_CSV = INPUT_DIR / "dept_mapping.csv"       # 見出し OldDeptCode,NewDeptCode
RESULT_CSV = Path("./output") / "result.csv"       # 結果(BOM付きUTF-8)

# True の場合、実際の更新は行わず予定の変更だけを result.csv に出力する
DRY_RUN = True


# ==============================
# CSV入力
# ==============================
def read_csv(path):
    # BOMの有無どちらでも読めるよう utf-8-sig で開く
    with open(path, "r", encoding="utf-8-sig", newline="") as f:
        return [
            {k.strip(): (v or "").strip() for k, v in row.items()}
            for row in csv.DictReader(f)
        ]


def write_result_csv(path, rows):
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(
            f, fieldnames=["SiteId", "Title", "Before", "After", "Result"]
        )
        writer.writeheader()
        writer.writerows(rows)


# ==============================
# Pleasanter API操作
# ==============================
def post(url, payload):
    """ステータスコードと応答本文(JSONでなければNone)を返す。404等でも例外にしない。"""
    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


# ==============================
# 組織コード→組織IDの変換表(無効化された組織も含めて全件取得)
# ==============================
def get_dept_ids_by_code():
    dept_ids = {}
    offset = 0
    while True:
        status, data = post(f"{BASE_URL}/api/depts/get", {"Offset": offset})
        if status != 200:
            sys.exit(f"組織の取得に失敗しました(HTTP {status})。")
        page = data["Response"]["Data"]
        for dept in page:
            if dept.get("DeptCode"):
                dept_ids[dept["DeptCode"]] = dept["DeptId"]
        offset += len(page)  # 取得件数に応じて進める
        if len(page) == 0 or offset >= data["Response"]["TotalCount"]:
            return dept_ids


# ==============================
# 対応表を「旧組織ID→新組織IDの一覧」に変換する
# 存在しない組織コードがあれば、サイトを1件も更新せずに終了する
# ==============================
def build_dept_mapping(mapping_rows, dept_ids):
    unknown = sorted(
        {
            code
            for row in mapping_rows
            for code in (row["OldDeptCode"], row["NewDeptCode"])
            if code not in dept_ids
        }
    )
    if unknown:
        sys.exit(f"Pleasanterに存在しない組織コードがあります: {', '.join(unknown)}")

    mapping = {}
    for row in mapping_rows:
        old_id = dept_ids[row["OldDeptCode"]]
        new_id = dept_ids[row["NewDeptCode"]]
        mapping.setdefault(old_id, [])
        if new_id not in mapping[old_id]:
            mapping[old_id].append(new_id)
    return mapping


# ==============================
# アクセス権の組み立て直し
# Permissions は "Dept,23,1" のような「種別,ID,権限値」の文字列の配列。
# 権限値はビットの和(読取り=1、作成=2、更新=4、削除=8、メール送信=16、
# エクスポート=32、インポート=64、サイトの管理=128、権限の管理=256)。
# ==============================
def replace_permissions(permissions, mapping):
    merged = {}  # (種別, ID) -> 権限値
    for permission in permissions:
        kind, member_id, value = permission.split(",")
        member_id, value = int(member_id), int(value)
        if kind == "Dept" and member_id in mapping:
            # 旧組織の要素は外し、対応する新組織に同じ権限を付ける
            targets = [("Dept", new_id) for new_id in mapping[member_id]]
        else:
            targets = [(kind, member_id)]
        for key in targets:
            # 新組織の要素が既にある場合や、統合で重なった場合はビットの論理和でまとめる
            merged[key] = merged.get(key, 0) | value
    return [f"{kind},{member_id},{value}" for (kind, member_id), value in merged.items()]


# ==============================
# サイト1件の処理。結果CSVの1行と、件数集計用の結果の種類を返す
# ==============================
def process_site(site_id, mapping):
    row = {"SiteId": site_id, "Title": "", "Before": "", "After": "", "Result": ""}

    status, data = post(f"{BASE_URL}/api/items/{site_id}/getsite", {})
    if status != 200:
        # 削除済みのサイト・存在しないID・レコードのIDは404になる
        row["Result"] = f"取得失敗({status})"
        return row, "取得失敗"

    site = data["Response"]["Data"]
    row["Title"] = site.get("Title", "")

    # 継承しているサイトは Permissions が空で返る。空を「権限なし」として
    # 書き戻すと継承を壊すおそれがあるため、更新しない
    inherit_id = int(site["InheritPermission"])
    if inherit_id != site_id:
        row["Result"] = f"継承のためスキップ({inherit_id})"
        return row, "継承のためスキップ"

    before = site.get("Permissions") or []
    after = replace_permissions(before, mapping)
    row["Before"] = ";".join(before)
    row["After"] = ";".join(after)

    if sorted(before) == sorted(after):
        row["Result"] = "変更なし"
        return row, "変更なし"

    if DRY_RUN:
        row["Result"] = "更新予定(DRY_RUN)"
        return row, "更新予定(DRY_RUN)"

    # Permissions は送った内容でまるごと置き換わるため、組み立て直した全要素を送る。
    # SiteSettings は送らない(送るとサイト設定全体が置き換わる)
    status, data = post(
        f"{BASE_URL}/api/items/{site_id}/updatesite",
        {"InheritPermission": site_id, "Permissions": after},
    )
    if status != 200:
        message = (data or {}).get("Message", "")
        print(f"[ERROR] サイト{site_id}の更新に失敗しました(HTTP {status}){message}")
        row["Result"] = f"更新失敗({status})"
        return row, "更新失敗"

    # 読み直して、送った内容がそのまま返るか確かめる。組織の作成・更新が同じ秒のうちに
    # 続くと、Pleasanter内部の組織情報が読み直されず、保存された組織の権限が getsite に
    # 表示されないことがある(エラーにはならない)。その状態では、更新前に読み取った
    # アクセス権も欠けていた可能性があるため、要確認として知らせる
    status, data = post(f"{BASE_URL}/api/items/{site_id}/getsite", {})
    saved = (data or {}).get("Response", {}).get("Data", {}).get("Permissions") or []
    missing = sorted(set(after) - set(saved))
    if status != 200 or missing:
        detail = ";".join(missing) if status == 200 else f"読み直し失敗({status})"
        print(
            f"[WARN] サイト{site_id}を読み直すと、送ったアクセス権が表示されません: {detail}"
            "(組織の情報が古いままの可能性があります。いずれかの組織を1件更新してから、"
            "このサイトのアクセス権を確認してください)"
        )
        row["Result"] = f"要確認(表示されない:{detail})"
        return row, "要確認"

    row["Result"] = "更新"
    return row, "更新"


# ==============================
# メイン処理
# ==============================
def main():
    site_ids = [int(row["SiteId"]) for row in read_csv(SITES_CSV) if row.get("SiteId")]
    mapping_rows = read_csv(MAPPING_CSV)

    # 先に組織コードをすべてIDに変換し、問題があればここで終了する(サイトは未更新)
    mapping = build_dept_mapping(mapping_rows, get_dept_ids_by_code())

    result_rows = []
    counts = {}
    for site_id in site_ids:
        row, kind = process_site(site_id, mapping)
        print(f"[{row['Result']}] {site_id} {row['Title']}")
        result_rows.append(row)
        counts[kind] = counts.get(kind, 0) + 1

    write_result_csv(RESULT_CSV, result_rows)

    print("=== 結果 ===" + ("(DRY_RUN:更新していません)" if DRY_RUN else ""))
    for kind, count in counts.items():
        print(f"{kind}: {count}件")
    print(f"結果ファイル: {RESULT_CSV}")


if __name__ == "__main__":
    main()
  • サイト更新APIにはInheritPermissionとPermissionsのみを送ります。SiteSettingsを含めて送ると、一覧・エディタ・サーバースクリプトなどのサイト設定全体が送った内容に置き換わるため、含めないでください。
  • Permissionsは送った内容でまるごと置き換わります(追記ではありません)。そのため、取得したアクセス権を全要素組み立て直してから送っています。
  • Permissionsは"Dept,23,1"のような「種別,ID,権限値」の文字列の配列です。権限値は、読取り=1、作成=2、更新=4、削除=8、メール送信=16、エクスポート=32、インポート=64、サイトの管理=128、権限の管理=256の和です。
  • アクセス権を継承しているサイトは、Permissionsが空で返ります。空のまま書き戻すと継承を壊すおそれがあるため、継承しているサイトは更新せず「継承のためスキップ」として継承元のサイトIDを記録します。継承元のサイトをsites.csvに入れれば、継承元の付け替えが配下のサイトにも効きます。
  • 削除済みのサイト・存在しないID・レコードのIDは、サイト取得APIが404を返すため「取得失敗(404)」になります。
  • 対応表に存在しない組織コードがあると、サイトを1件も更新せずにエラーで終了します。
  • グループやユーザーの付け替えは、replace_permissionsの種別(Dept)をGroup・Userに変えれば同じ形で書けます。
  • 組織をAPIやインポートで続けて作成・更新した直後は、プリザンター内部の組織情報が古いままになり、一部の組織の権限がサイト取得APIのPermissionsに表示されないことがあります(権限そのものは保存されています)。表示されない権限は、読み取って書き戻すと消えてしまうため、組織をまとめて作成・更新した直後に実行するときは、先にいずれかの組織を1件更新(画面で保存し直せば足ります)してから実行してください。時間をおいても解消しません。本サンプルは更新後にサイトを読み直し、送った権限が表示されないサイトを「要確認」として結果に出力します。

対応バージョン

対応バージョン 内容
1.3.4.0 以降 機能追加
1.5.6.0 以降 VerUpを追加