import re
from collections.abc import Iterator
from dataclasses import asdict, dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Optional, Union
import httpx
from bs4 import BeautifulSoup
from ..common import exceptions
from ..util.parser import odate as odate_parser
from ..util.parser import user as user_parser
from ..util.requestutil import RequestUtil
from .page_revision import PageRevision, PageRevisionCollection
from .page_source import PageSource
from .page_votes import PageVote, PageVoteCollection
if TYPE_CHECKING:
from .site import Site
from .user import User
DEFAULT_MODULE_BODY = [
"fullname", # ページのフルネーム(str)
"category", # カテゴリ(str)
"name", # ページ名(str)
"title", # タイトル(str)
"created_at", # 作成日時(odate element)
"created_by_linked", # 作成者(user element)
"updated_at", # 更新日時(odate element)
"updated_by_linked", # 更新者(user element)
"commented_at", # コメント日時(odate element)
"commented_by_linked", # コメントしたユーザ(user element)
"parent_fullname", # 親ページのフルネーム(str)
"comments", # コメント数(int)
"size", # サイズ(int)
"children", # 子ページ数(int)
"rating_votes", # 投票数(int)
"rating", # レーティング(int or float)
"rating_percent", # 5つ星レーティング(%)
"revisions", # リビジョン数(int)
"tags", # タグのリスト(list of str)
"_tags", # 隠しタグのリスト(list of str)
]
[ドキュメント]
@dataclass
class SearchPagesQuery:
"""
ページ検索クエリを表すクラス
Wikidotのページ検索に使用される各種検索条件を定義する。
ListPagesModuleに渡すためのクエリパラメータをカプセル化している。
Attributes
----------
pagetype : str, default "*"
ページタイプ(例: "normal", "admin"等)
category : str, default "*"
カテゴリ名
tags : str | list[str] | None, default None
検索対象のタグ(文字列または文字列のリスト)
parent : str | None, default None
親ページ名
link_to : str | None, default None
リンク先ページ名
created_at : str | None, default None
作成日時の条件
updated_at : str | None, default None
更新日時の条件
created_by : User | str | None, default None
作成者による絞り込み
rating : str | None, default None
評価値による絞り込み
votes : str | None, default None
投票数による絞り込み
name : str | None, default None
ページ名による絞り込み
fullname : str | None, default None
フルネームによる絞り込み(完全一致)
range : str | None, default None
範囲指定
order : str, default "created_at desc"
ソート順(例: "created_at desc", "title asc"等)
offset : int, default 0
取得開始位置
limit : int | None, default None
取得件数制限
perPage : int, default 250
1ページあたりの表示件数
separate : str, default "no"
個別表示するかどうか
wrapper : str, default "no"
ラッパー要素を表示するかどうか
"""
# selecting pages
pagetype: Optional[str] = "*"
category: Optional[str] = "*"
tags: Optional[str | list[str]] = None
parent: Optional[str] = None
link_to: Optional[str] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
created_by: Optional[Union["User", str]] = None
rating: Optional[str] = None
votes: Optional[str] = None
name: Optional[str] = None
fullname: Optional[str] = None
range: Optional[str] = None
# ordering
order: str = "created_at desc"
# pagination
offset: Optional[int] = 0
limit: Optional[int] = None
perPage: Optional[int] = 250
# layout
separate: Optional[str] = "no"
wrapper: Optional[str] = "no"
[ドキュメント]
def as_dict(self) -> dict[str, Any]:
"""
クエリパラメータを辞書形式に変換する
タグがリスト形式の場合は空白区切りの文字列に変換する。
Returns
-------
dict[str, Any]
APIリクエスト用の辞書形式パラメータ
"""
res = {k: v for k, v in asdict(self).items() if v is not None}
if "tags" in res and isinstance(res["tags"], list):
res["tags"] = " ".join(res["tags"])
return res
[ドキュメント]
class PageCollection(list["Page"]):
"""
ページオブジェクトのコレクションを表すクラス
複数のページオブジェクトを格納し、一括して操作するための機能を提供する。
ページの検索、一括取得、一括操作などの機能を集約している。
"""
def __init__(self, site: Optional["Site"] = None, pages: Optional[list["Page"]] = None):
"""
初期化メソッド
Parameters
----------
site : Site | None, default None
ページが属するサイト。Noneの場合は最初のページから推測する
pages : list[Page] | None, default None
格納するページのリスト
"""
super().__init__(pages or [])
if site is not None:
self.site = site
else:
self.site = self[0].site
def __iter__(self) -> Iterator["Page"]:
"""
コレクション内のページを順に返すイテレータ
Returns
-------
Iterator[Page]
ページオブジェクトのイテレータ
"""
return super().__iter__()
[ドキュメント]
def find(self, fullname: str) -> Optional["Page"]:
"""
指定したフルネームのページを取得する
Parameters
----------
fullname : str
取得するページのフルネーム
Returns
-------
Page | None
指定したフルネームのページ。存在しない場合はNone
"""
for page in self:
if page.fullname == fullname:
return page
return None
@staticmethod
def _parse(site: "Site", html_body: BeautifulSoup):
"""
ListPagesModuleのレスポンスをパースしてページオブジェクトのリストを生成する
Parameters
----------
site : Site
ページが属するサイト
html_body : BeautifulSoup
パース対象のHTML
Returns
-------
PageCollection
パース結果のページコレクション
Raises
------
NoElementException
必要な要素が見つからない場合
"""
pages = []
for page_element in html_body.select("div.page"):
page_params = {}
# レーティング方式を判定
is_5star_rating = page_element.select_one("span.rating span.page-rate-list-pages-start") is not None
# 各値を取得
for set_element in page_element.select("span.set"):
key_element = set_element.select_one("span.name")
if key_element is None:
raise exceptions.NoElementException("Cannot find key element")
key = key_element.text.strip()
value_element = set_element.select_one("span.value")
if value_element is None:
value: Any = None
elif key in ["created_at", "updated_at", "commented_at"]:
odate_element = value_element.select_one("span.odate")
if odate_element is None:
value = None
else:
value = odate_parser(odate_element)
elif key in [
"created_by_linked",
"updated_by_linked",
"commented_by_linked",
]:
printuser_element = value_element.select_one("span.printuser")
if printuser_element is None:
value = None
else:
value = user_parser(site.client, printuser_element)
elif key in ["tags", "_tags"]:
value = value_element.text.split()
elif key in ["rating_votes", "comments", "size", "revisions"]:
value = int(value_element.text.strip())
elif key in ["rating"]:
if is_5star_rating:
value = float(value_element.text.strip())
else:
value = int(value_element.text.strip())
elif key in ["rating_percent"]:
if is_5star_rating:
value = float(value_element.text.strip()) / 100
else:
value = None
else:
value = value_element.text.strip()
# keyを変換
if "_linked" in key:
key = key.replace("_linked", "")
elif key in ["comments", "children", "revisions"]:
key = f"{key}_count"
elif key in ["rating_votes"]:
key = "votes_count"
page_params[key] = value
# タグのリストを統合
for key in ["tags", "_tags"]:
if key not in page_params or page_params[key] is None:
page_params[key] = []
page_params["tags"] = page_params["tags"] + page_params["_tags"]
del page_params["_tags"]
# ページオブジェクトを作成
pages.append(Page(site, **page_params))
return PageCollection(site, pages)
[ドキュメント]
@staticmethod
def search_pages(site: "Site", query: SearchPagesQuery = SearchPagesQuery()):
"""
サイト内のページを検索する
指定されたクエリに基づいてサイト内のページを検索し、結果を返す。
Wikidotの「ListPagesModule」を使用して検索を実行する。
Parameters
----------
site : Site
検索対象のサイト
query : SearchPagesQuery, default SearchPagesQuery()
検索条件
Returns
-------
PageCollection
検索結果のページコレクション
Raises
------
ForbiddenException
プライベートサイトでアクセスが拒否された場合
WikidotStatusCodeException
その他のAPIエラーが発生した場合
NoElementException
レスポンスからページ情報を抽出できない場合
"""
# 初回実行
query_dict = query.as_dict()
query_dict["moduleName"] = "list/ListPagesModule"
query_dict["module_body"] = (
'[[div class="page"]]\n'
+ "".join(
[
f'[[span class="set {key}"]]'
f'[[span class="name"]] {key} [[/span]]'
f'[[span class="value"]] %%{key}%% [[/span]]'
f"[[/span]]"
for key in DEFAULT_MODULE_BODY
]
)
+ "\n[[/div]]"
)
try:
response = site.amc_request([query_dict])[0]
except exceptions.WikidotStatusCodeException as e:
if e.status_code == "not_ok":
raise exceptions.ForbiddenException("Failed to get pages, target site may be private") from e
raise e
body = response.json()["body"]
first_page_html_body = BeautifulSoup(body, "lxml")
total = 1
html_bodies = [first_page_html_body]
# pagerが存在する
if first_page_html_body.select_one("div.pager") is not None:
# span.target[-2] > a から最大ページ数を取得
last_pager_element = first_page_html_body.select("div.pager span.target")[-2]
last_pager_link_element = last_pager_element.select_one("a")
if last_pager_link_element is None:
raise exceptions.NoElementException("Cannot find last pager link")
total = int(last_pager_link_element.text.strip())
if total > 1:
request_bodies = []
for i in range(1, total):
_query_dict = query_dict.copy()
_query_dict["offset"] = i * (query.perPage or 250)
request_bodies.append(_query_dict)
responses = site.amc_request(request_bodies)
html_bodies.extend([BeautifulSoup(response.json()["body"], "lxml") for response in responses])
pages = []
for html_body in html_bodies:
pages.extend(PageCollection._parse(site, html_body))
return PageCollection(site, pages)
@staticmethod
def _acquire_page_ids(site: "Site", pages: list["Page"]):
"""
ページIDを取得する内部メソッド
未取得のページIDを一括で取得する。norender/noredirectオプション付きで
ページにアクセスし、ページソースからIDを抽出する。
Parameters
----------
site : Site
ページが属するサイト
pages : list[Page]
対象ページのリスト
Returns
-------
list[Page]
ID情報が更新されたページのリスト
Raises
------
UnexpectedException
ページIDが見つからない場合や、予期しないレスポンスタイプの場合
"""
# pagesからidが設定されていないものを抽出
target_pages = [page for page in pages if not page.is_id_acquired()]
# なければ終了
if len(target_pages) == 0:
return pages
# norender, noredirectでアクセス
responses = RequestUtil.request(
site.client,
"GET",
[f"{page.get_url()}/norender/true/noredirect/true" for page in target_pages],
)
# "WIKIREQUEST.info.pageId = xxx;"の値をidに設定
for index, response in enumerate(responses):
if not isinstance(response, httpx.Response):
raise exceptions.UnexpectedException(f"Unexpected response type: {type(response)}")
source = response.text
id_match = re.search(r"WIKIREQUEST\.info\.pageId = (\d+);", source)
if id_match is None:
raise exceptions.UnexpectedException(f"Cannot find page id: {target_pages[index].fullname}")
target_pages[index].id = int(id_match.group(1))
return pages
[ドキュメント]
def get_page_ids(self):
"""
コレクション内の全ページのIDを取得する
IDが設定されていないページについて、一括でIDを取得する。
Returns
-------
PageCollection
自身(メソッドチェーン用)
"""
return PageCollection._acquire_page_ids(self.site, self)
@staticmethod
def _acquire_page_sources(site: "Site", pages: list["Page"]):
"""
ページソースを取得する内部メソッド
指定されたページのソースコード(Wikidot記法)を一括で取得する。
Parameters
----------
site : Site
ページが属するサイト
pages : list[Page]
対象ページのリスト
Returns
-------
list[Page]
ソース情報が更新されたページのリスト
Raises
------
NoElementException
ソース要素が見つからない場合
"""
if len(pages) == 0:
return pages
responses = site.amc_request(
[{"moduleName": "viewsource/ViewSourceModule", "page_id": page.id} for page in pages]
)
for page, responses in zip(pages, responses):
body = responses.json()["body"]
html = BeautifulSoup(body, "lxml")
source_element = html.select_one("div.page-source")
if source_element is None:
raise exceptions.NoElementException("Cannot find source element")
source = source_element.text.strip().removeprefix("\t")
page.source = PageSource(page, source)
return pages
[ドキュメント]
def get_page_sources(self):
"""
コレクション内の全ページのソースコードを取得する
ページのソースコード(Wikidot記法)を一括で取得し、各ページのsourceプロパティに設定する。
Returns
-------
PageCollection
自身(メソッドチェーン用)
"""
return PageCollection._acquire_page_sources(self.site, self)
@staticmethod
def _acquire_page_revisions(site: "Site", pages: list["Page"]):
"""
ページリビジョン履歴を取得する内部メソッド
指定されたページのリビジョン(編集履歴)を一括で取得する。
Parameters
----------
site : Site
ページが属するサイト
pages : list[Page]
対象ページのリスト
Returns
-------
list[Page]
リビジョン情報が更新されたページのリスト
Raises
------
NoElementException
必要な要素が見つからない場合
"""
if len(pages) == 0:
return pages
responses = site.amc_request(
[
{
"moduleName": "history/PageRevisionListModule",
"page_id": page.id,
"options": {"all": True},
"perpage": 100000000, # pagerを使わずに全て取得
}
for page in pages
]
)
for page, response in zip(pages, responses):
body = response.json()["body"]
revs = []
body_html = BeautifulSoup(body, "lxml")
for rev_element in body_html.select("table.page-history > tr[id^=revision-row-]"):
rev_id = int(str(rev_element["id"]).removeprefix("revision-row-"))
tds = rev_element.select("td")
rev_no = int(tds[0].text.strip().removesuffix("."))
created_by_elem = tds[4].select_one("span.printuser")
if created_by_elem is None:
raise exceptions.NoElementException("Cannot find created by element")
created_by = user_parser(page.site.client, created_by_elem)
created_at_elem = tds[5].select_one("span.odate")
if created_at_elem is None:
raise exceptions.NoElementException("Cannot find created at element")
created_at = odate_parser(created_at_elem)
comment = tds[6].text.strip()
revs.append(
PageRevision(
page=page,
id=rev_id,
rev_no=rev_no,
created_by=created_by,
created_at=created_at,
comment=comment,
)
)
page.revisions = PageRevisionCollection(page, revs)
return pages
[ドキュメント]
def get_page_revisions(self):
"""
コレクション内の全ページのリビジョン履歴を取得する
ページのリビジョン(編集履歴)を一括で取得し、各ページのrevisionsプロパティに設定する。
Returns
-------
PageCollection
自身(メソッドチェーン用)
"""
return PageCollection._acquire_page_revisions(self.site, self)
@staticmethod
def _acquire_page_votes(site: "Site", pages: list["Page"]):
"""
ページへの投票情報を取得する内部メソッド
指定されたページへの投票(レーティング)情報を一括で取得する。
Parameters
----------
site : Site
ページが属するサイト
pages : list[Page]
対象ページのリスト
Returns
-------
list[Page]
投票情報が更新されたページのリスト
Raises
------
UnexpectedException
ユーザー要素と投票値要素の数が一致しない場合
"""
if len(pages) == 0:
return pages
responses = site.amc_request(
[{"moduleName": "pagerate/WhoRatedPageModule", "pageId": page.id} for page in pages]
)
for page, response in zip(pages, responses):
body = response.json()["body"]
html = BeautifulSoup(body, "lxml")
user_elems = html.select("span.printuser")
value_elems = html.select("span[style^='color']")
if len(user_elems) != len(value_elems):
raise exceptions.UnexpectedException("User and value count mismatch")
users = [user_parser(site.client, user_elem) for user_elem in user_elems]
values = []
for value in value_elems:
_v = value.text.strip()
if _v == "+":
values.append(1)
elif _v == "-":
values.append(-1)
else:
values.append(int(_v))
votes = [PageVote(page, user, vote) for user, vote in zip(users, values)]
page._votes = PageVoteCollection(page, votes)
return pages
[ドキュメント]
def get_page_votes(self):
"""
コレクション内の全ページの投票情報を取得する
ページへの投票(レーティング)情報を一括で取得し、各ページのvotesプロパティに設定する。
Returns
-------
PageCollection
自身(メソッドチェーン用)
"""
return PageCollection._acquire_page_votes(self.site, self)
[ドキュメント]
@dataclass
class Page:
"""
Wikidotページを表すクラス
Wikidotサイト内の単一ページに関する情報と操作機能を提供する。
ページの基本情報、メタデータ、履歴、評価などを管理する。
Attributes
----------
site : Site
ページが存在するサイト
fullname : str
ページのフルネーム(例: "コンポーネント:scp-173")
name : str
ページ名(例: "scp-173")
category : str
カテゴリ(例: "コンポーネント")
title : str
ページのタイトル
children_count : int
子ページの数
comments_count : int
コメント数
size : int
ページのサイズ(バイト数)
rating : int | float
レーティング(+/-評価の場合はint、5つ星評価の場合はfloat)
votes_count : int
投票数
rating_percent : float
5つ星評価システムにおけるパーセンテージ値(0.0~1.0)
revisions_count : int
リビジョン(編集履歴)の数
parent_fullname : str | None
親ページのフルネーム(親がない場合はNone)
tags : list[str]
付けられたタグのリスト
created_by : User
ページの作成者
created_at : datetime
ページの作成日時
updated_by : User
最終更新者
updated_at : datetime
最終更新日時
commented_by : User | None
最後にコメントしたユーザー(コメントがない場合はNone)
commented_at : datetime | None
最後のコメント日時(コメントがない場合はNone)
_id : int | None
ページのID(内部識別子)
_source : PageSource | None
ページのソースコード(要求時に取得)
_revisions : PageRevisionCollection | None
ページのリビジョン履歴(要求時に取得)
_votes : PageVoteCollection | None
ページへの投票情報(要求時に取得)
_metas : dict[str, str] | None
ページのメタタグ情報(要求時に取得)
"""
site: "Site"
fullname: str
name: str
category: str
title: str
children_count: int
comments_count: int
size: int
rating: int | float
votes_count: int
rating_percent: float
revisions_count: int
parent_fullname: str | None
tags: list[str]
created_by: "User"
created_at: datetime
updated_by: "User"
updated_at: datetime
commented_by: Optional["User"]
commented_at: Optional[datetime]
_id: Optional[int] = None
_source: Optional[PageSource] = None
_revisions: Optional[PageRevisionCollection] = None
_votes: Optional[PageVoteCollection] = None
_metas: Optional[dict[str, str]] = None
[ドキュメント]
def get_url(self) -> str:
"""
ページの完全なURLを取得する
サイトのURLとページ名から完全なページURLを生成する。
Returns
-------
str
ページの完全なURL
"""
return f"{self.site.url}/{self.fullname}"
@property
def id(self) -> int:
"""
ページIDを取得する
IDが未取得の場合は自動的に取得処理を行う。
Returns
-------
int
ページID
Raises
------
NotFoundException
ページIDが見つからない場合
"""
if not self.is_id_acquired():
PageCollection(self.site, [self]).get_page_ids()
if self._id is None:
raise exceptions.NotFoundException("Cannot find page id")
return self._id
@id.setter
def id(self, value: int):
"""
ページIDを設定する
Parameters
----------
value : int
設定するページID
"""
self._id = value
[ドキュメント]
def is_id_acquired(self) -> bool:
"""
ページIDが既に取得済みかどうかを確認する
Returns
-------
bool
IDが取得済みの場合はTrue、未取得の場合はFalse
"""
return self._id is not None
@property
def source(self) -> PageSource:
"""
ページのソースコードを取得する
ソースコードが未取得の場合は自動的に取得処理を行う。
Returns
-------
PageSource
ページのソースコードオブジェクト
Raises
------
NotFoundException
ページソースが見つからない場合
"""
if self._source is None:
PageCollection(self.site, [self]).get_page_sources()
if self._source is None:
raise exceptions.NotFoundException("Cannot find page source")
return self._source
@source.setter
def source(self, value: PageSource):
"""
ページのソースコードを設定する
Parameters
----------
value : PageSource
設定するソースコードオブジェクト
"""
self._source = value
@property
def revisions(self) -> PageRevisionCollection:
"""
ページのリビジョン履歴を取得する
リビジョン履歴が未取得の場合は自動的に取得処理を行う。
Returns
-------
PageRevisionCollection
ページのリビジョン履歴コレクション
Raises
------
NotFoundException
リビジョン履歴が見つからない場合
"""
if self._revisions is None:
PageCollection(self.site, [self]).get_page_revisions()
return PageRevisionCollection(self, self._revisions)
@revisions.setter
def revisions(self, value: list["PageRevision"] | PageRevisionCollection):
"""
ページのリビジョン履歴を設定する
Parameters
----------
value : list[PageRevision] | PageRevisionCollection
設定するリビジョンのリストまたはコレクション
"""
if isinstance(value, list):
self._revisions = PageRevisionCollection(self, value)
else:
self._revisions = value
@property
def latest_revision(self) -> PageRevision:
"""
ページの最新リビジョンを取得する
revision_countとrev_noが一致するリビジョンを最新として返す。
Returns
-------
PageRevision
最新のリビジョンオブジェクト
Raises
------
NotFoundException
最新リビジョンが見つからない場合
"""
# revision_countとrev_noが一致するものを取得
for revision in self.revisions:
if revision.rev_no == self.revisions_count:
return revision
raise exceptions.NotFoundException("Cannot find latest revision")
@property
def votes(self) -> PageVoteCollection:
"""
ページへの投票情報を取得する
投票情報が未取得の場合は自動的に取得処理を行う。
Returns
-------
PageVoteCollection
ページへの投票情報コレクション
Raises
------
NotFoundException
投票情報が見つからない場合
"""
if self._votes is None:
PageCollection(self.site, [self]).get_page_votes()
if self._votes is None:
raise exceptions.NotFoundException("Cannot find page votes")
return self._votes
@votes.setter
def votes(self, value: PageVoteCollection):
"""
ページへの投票情報を設定する
Parameters
----------
value : PageVoteCollection
設定する投票情報コレクション
"""
self._votes = value
[ドキュメント]
def destroy(self):
"""
ページを削除する
ログイン状態でのみ実行可能。ページの完全削除を行う。
Raises
------
LoginRequiredException
ログインしていない場合
WikidotStatusCodeException
削除に失敗した場合
"""
self.site.client.login_check()
self.site.amc_request(
[
{
"action": "WikiPageAction",
"event": "deletePage",
"page_id": self.id,
"moduleName": "Empty",
}
]
)
@property
def metas(self) -> dict[str, str]:
"""
ページのメタタグ情報を取得する
メタタグ情報が未取得の場合は自動的に取得処理を行う。
Returns
-------
dict[str, str]
メタタグ名とその内容の辞書
"""
if self._metas is None:
response = self.site.amc_request(
[
{
"pageId": self.id,
"moduleName": "edit/EditMetaModule",
}
]
)
# レスポンス解析
body = response[0].json()["body"]
# <meta name="xxx" content="yyy"/> を正規表現で取得
metas = {}
for meta in re.findall(r'<meta name="([^"]+)" content="([^"]+)"/>', body):
metas[meta[0]] = meta[1]
self._metas = metas
return self._metas
@metas.setter
def metas(self, value: dict[str, str]):
"""
ページのメタタグ情報を設定する
現在のメタタグと比較し、削除されたものは削除、追加されたものは追加する。
Parameters
----------
value : dict[str, str]
設定するメタタグ名とその内容の辞書
Raises
------
LoginRequiredException
ログインしていない場合
WikidotStatusCodeException
メタタグの設定に失敗した場合
"""
self.site.client.login_check()
current_metas = self.metas
deleted_metas = {k: v for k, v in current_metas.items() if k not in value}
added_metas = {k: v for k, v in value.items() if k not in current_metas}
for name, content in deleted_metas.items():
self.site.amc_request(
[
{
"metaName": name,
"action": "WikiPageAction",
"event": "deleteMetaTag",
"pageId": self.id,
"moduleName": "edit/EditMetaModule",
}
]
)
for name, content in added_metas.items():
self.site.amc_request(
[
{
"metaName": name,
"metaContent": content,
"action": "WikiPageAction",
"event": "saveMetaTag",
"pageId": self.id,
"moduleName": "edit/EditMetaModule",
}
]
)
self._metas = value
[ドキュメント]
@staticmethod
def create_or_edit(
site: "Site",
fullname: str,
page_id: int | None = None,
title: str = "",
source: str = "",
comment: str = "",
force_edit: bool = False,
raise_on_exists: bool = False,
) -> "Page":
"""
ページを作成または編集する
新規ページの作成または既存ページの編集を行う。
編集の場合はページロックの取得とページ保存の処理を行う。
Parameters
----------
site : Site
ページが属するサイト
fullname : str
ページのフルネーム
page_id : int | None, default None
編集する場合のページID(新規作成時はNone)
title : str, default ""
ページのタイトル
source : str, default ""
ページのソースコード(Wikidot記法)
comment : str, default ""
編集コメント
force_edit : bool, default False
他のユーザーによるロックを強制的に解除するかどうか
raise_on_exists : bool, default False
ページが既に存在する場合に例外を送出するかどうか
Returns
-------
Page
作成または編集されたページオブジェクト
Raises
------
LoginRequiredException
ログインしていない場合
TargetErrorException
ページがロックされている場合
TargetExistsException
ページが既に存在し、raise_on_existsがTrueの場合
ValueError
既存ページの編集時にpage_idが指定されていない場合
WikidotStatusCodeException
ページの保存に失敗した場合
NotFoundException
ページの作成後に検索できない場合
"""
site.client.login_check()
# ページロックを取得しにいく
page_lock_request_body = {
"mode": "page",
"wiki_page": fullname,
"moduleName": "edit/PageEditModule",
}
if force_edit:
page_lock_request_body["force_lock"] = "yes"
page_lock_response = site.amc_request([page_lock_request_body])[0]
page_lock_response_data = page_lock_response.json()
if "locked" in page_lock_response_data or "other_locks" in page_lock_response_data:
raise exceptions.TargetErrorException(
f"Page {fullname} is locked or other locks exist",
)
# ページが存在するか(page_revision_idがあるか)確認
is_exist = "page_revision_id" in page_lock_response_data
if raise_on_exists and is_exist:
raise exceptions.TargetExistsException(f"Page {fullname} already exists")
if is_exist and page_id is None:
raise ValueError("page_id must be specified when editing existing page")
# lock_idとlock_secret、page_revision_id(あれば)を取得
lock_id = page_lock_response_data["lock_id"]
lock_secret = page_lock_response_data["lock_secret"]
page_revision_id = page_lock_response_data.get("page_revision_id")
# ページの作成または編集
edit_request_body = {
"action": "WikiPageAction",
"event": "savePage",
"moduleName": "Empty",
"mode": "page",
"lock_id": lock_id,
"lock_secret": lock_secret,
"revision_id": page_revision_id if page_revision_id is not None else "",
"wiki_page": fullname,
"page_id": page_id if page_id is not None else "",
"title": title,
"source": source,
"comments": comment,
}
response = site.amc_request([edit_request_body])[0]
if response.json()["status"] != "ok":
raise exceptions.WikidotStatusCodeException(
f"Failed to create or edit page: {fullname}", response.json()["status"]
)
res = PageCollection.search_pages(site, SearchPagesQuery(fullname=fullname))
if len(res) == 0:
raise exceptions.NotFoundException(f"Page creation failed: {fullname}")
return res[0]
[ドキュメント]
def edit(
self,
title: Optional[str] = None,
source: Optional[str] = None,
comment: Optional[str] = None,
force_edit: bool = False,
) -> "Page":
"""
このページを編集する
既存ページの内容を更新する。指定されていないパラメータは現在の値を維持する。
Parameters
----------
title : str | None, default None
新しいタイトル(Noneの場合は現在のタイトルを維持)
source : str | None, default None
新しいソースコード(Noneの場合は現在のソースを維持)
comment : str | None, default None
編集コメント
force_edit : bool, default False
他のユーザーによるロックを強制的に解除するかどうか
Returns
-------
Page
編集後のページオブジェクト
Raises
------
同上(create_or_editメソッドと同様)
"""
# Noneならそのままにする
title = title or self.title
source = source or self.source.wiki_text
comment = comment or ""
return Page.create_or_edit(
self.site,
self.fullname,
self.id,
title,
source,
comment,
force_edit,
)
[ドキュメント]
def commit_tags(self):
"""
ページのタグ情報を保存する
現在のtagsプロパティの内容をWikidotに保存する。
Returns
-------
Page
自身(メソッドチェーン用)
Raises
------
LoginRequiredException
ログインしていない場合
WikidotStatusCodeException
タグの保存に失敗した場合
"""
self.site.client.login_check()
self.site.amc_request(
[
{
"tags": " ".join(self.tags),
"action": "WikiPageAction",
"event": "saveTags",
"pageId": self.id,
"moduleName": "Empty",
}
]
)
return self