diff --git a/phpbb-test-board/README.md b/phpbb-test-board/README.md index 885b8be..930949a 100644 --- a/phpbb-test-board/README.md +++ b/phpbb-test-board/README.md @@ -152,7 +152,7 @@ Saves PNG screenshots of an extension's or style's pages with [Playwright](https `phpbbmodders/genders` gives every member a gender and saves the UCP selector, a topic, a profile and the ACP settings with the defaults, then the same topic, profile and settings with gender text before the icon, the phpBB 3.0 MOD prosilver icon set and gender in the main profile details. Use `--seed` with seed-forum's standard fixtures. -`phpbbmodders/knowledgebase` makes its own categories, articles (live, waiting for review, with a revision, and a draft) and forum questions, so a board search for `backup` shows its block; `--seed` with seed-forum's standard fixtures adds members to write them. Its two ACP pages are taken in full, so a new setting is not cut off. +`phpbbmodders/knowledgebase` makes its own categories, articles (live, waiting for review, with a revision, and a draft) and forum questions, so a board search for `backup` shows its block; `--seed` with seed-forum's standard fixtures adds members to write them. Its two ACP pages are taken in full, so a new setting is not cut off. The last two shots switch on the introduction above the index, with the default text the extension stores on install, and show it on the index and in the ACP configuration; it is switched off again afterwards. `phpbbmodders/newspage` replaces the board's welcome topic with four example news topics, the last one opening with a picture the script draws (and deletes afterwards), and makes that forum the news forum. It saves the news page with the first image above the text and without poster information or post buttons, one news item as the admin, the ACP settings (in full), the news page on a phone and filtered by forum, then the news page with poster information and post buttons on, shortened to 120 characters, and without the filters. diff --git a/phpbb-test-board/feature_checks.py b/phpbb-test-board/feature_checks.py index 736aca9..12e5c75 100755 --- a/phpbb-test-board/feature_checks.py +++ b/phpbb-test-board/feature_checks.py @@ -207,6 +207,89 @@ def links_to(page: str, path: str) -> bool: ok(not links_to(guest.get(f"{b.base}/index.php").text, rss_path), "links go away when the forum is set to none") +def check_knowledgebase(b: Board) -> None: + """phpbbmodders/knowledgebase: the ACP adds a category (a forged form or move link is refused), a member + submits an article that stays hidden until a moderator approves it, then readers see and find it.""" + admin = requests.Session() + sid = b.login(admin, acp=True) + manage = f"{b.base}/adm/index.php?i=-phpbbmodders-knowledgebase-acp-manage_module&mode=manage&sid={sid}" + name = "KB check " + secrets.token_hex(3) + + def category() -> list: + return b.sql("SELECT category_id FROM phpbb_kb_categories WHERE category_name = ?", (name,)) + + page_url = f"{manage}&action=add&parent_id=0" + action, fields = form_fields(admin.get(page_url, timeout=20).text) + forged = {k: v for k, v in fields.items() if k not in ("form_token", "creation_time")} + body = post_back(admin, f"{b.base}/adm", page_url, action, {**forged, "category_name": name, "update": "Submit"}) + ok("The submitted form was invalid" in body and not category(), "the ACP refuses a category form without its token") + post_back(admin, f"{b.base}/adm", page_url, action, {**fields, "category_name": name, "update": "Submit"}) + rows = category() + ok(rows, "the ACP adds a category") + if not rows: + return + cat_id = int(rows[0][0]) + body = admin.get(f"{manage}&action=move_up&f={cat_id}", timeout=20).text + ok("The submitted form was invalid" in body, "a category move link without its hash is refused") + + # Members may view the Knowledge Base and add articles to this category + author_pw, reader_pw = secrets.token_urlsafe(12), secrets.token_urlsafe(12) + suffix = secrets.token_hex(2) + b.add_user("kbauthor" + suffix, author_pw) + b.add_user("kbreader" + suffix, reader_pw) + registered = int(b.sql("SELECT group_id FROM phpbb_groups WHERE group_name = 'REGISTERED'")[0][0]) + view = int(b.sql("SELECT auth_option_id FROM phpbb_acl_options WHERE auth_option = 'u_kb_view'")[0][0]) + add = int(b.sql("SELECT auth_option_id FROM phpbb_kb_options WHERE auth_option = 'kb_u_add'")[0][0]) + b.sql("DELETE FROM phpbb_acl_groups WHERE group_id = ? AND auth_option_id = ?", (registered, view)) + b.sql("INSERT INTO phpbb_acl_groups (group_id, forum_id, auth_option_id, auth_role_id, auth_setting) " + "VALUES (?, 0, ?, 0, 1)", (registered, view)) + b.sql("INSERT INTO phpbb_kb_groups (group_id, category_id, auth_option_id, auth_setting) VALUES (?, ?, ?, 1)", + (registered, cat_id, add)) + b.sql("UPDATE phpbb_users SET user_permissions = ''") + b.purge_cache() + + author, reader = requests.Session(), requests.Session() + b.login(author, "kbauthor" + suffix, author_pw) + b.login(reader, "kbreader" + suffix, reader_pw) + title, word = "Check article " + secrets.token_hex(3), "kbcheck" + secrets.token_hex(4) + page_url = f"{b.base}/{b.route('knowledgebase/posting')}?id={cat_id}" + # Required custom fields (seed-forum's fixtures have some) get their first enabled value + required = {f"field_values[{field_id}][]": str(value_id) for field_id, value_id in b.sql( + "SELECT f.field_id, MIN(v.value_id) FROM phpbb_kb_fields f JOIN phpbb_kb_field_values v " + "ON v.field_id = f.field_id WHERE f.field_required = 1 AND v.enabled = 1 GROUP BY f.field_id")} + action, fields = form_fields(author.get(page_url, timeout=20).text, "postform") + post_back(author, b.base, page_url, action, {**fields, **required, "subject": title, "descr": "A check", + "message": f"Body with the word {word}.", "submit": "Submit"}) + article = b.sql("SELECT article_id, approved FROM phpbb_kb_articles WHERE article_title = ?", (title,)) + ok(article and int(article[0][1]) == 0, "a member submits an article, which waits for approval") + if not article: + return + art_id = int(article[0][0]) + article_url = f"{b.base}/{b.route('knowledgebase/article')}?k={art_id}" + approve_url = f"{b.base}/{b.route('knowledgebase/approve')}?id={art_id}" + ok(title not in reader.get(article_url, timeout=20).text, "other members cannot read it before approval") + ok("You are not authorised" in reader.get(approve_url, timeout=20).text, + "other members cannot open its approve page") + + # An article is assigned to a KB team and marked reviewed before it can be approved; a board + # without a team gets the Administrators group as one + teams = b.sql("SELECT config_value FROM phpbb_config_text WHERE config_name = 'kb_team_groups'") + team_ids = json.loads(teams[0][0]) if teams and teams[0][0] else [] + if not team_ids: + team_ids = [int(b.sql("SELECT group_id FROM phpbb_groups WHERE group_name = 'ADMINISTRATORS'")[0][0])] + b.php("$phpbb_container->get('config_text')->set('kb_team_groups', " + json.dumps(json.dumps(team_ids)) + ");") + b.purge_cache() + for step in ({"action": "assign", "team_id": str(team_ids[0])}, {"action": "review"}, + {"action": "approve", "skip_message": "1"}): + action, fields = form_fields(admin.get(approve_url, timeout=20).text) + post_back(admin, b.base, approve_url, action, {**fields, **step}) + ok(int(b.sql("SELECT approved FROM phpbb_kb_articles WHERE article_id = ?", (art_id,))[0][0]) == 1, + "a moderator assigns it to a team, marks it reviewed and approves it") + ok(title in reader.get(article_url, timeout=20).text, "members can read it once approved") + search = reader.get(f"{b.base}/{b.route('knowledgebase/search')}?keywords={word}&sf=all&cid=0", timeout=20).text + ok(title in search, "the Knowledge Base search finds it") + + def check_topicclaim(b: Board) -> None: """phpbbmodders/topicclaim: a moderator claims a topic with a note, other members see nothing, and the claim can be released (after confirmation).""" @@ -1764,6 +1847,7 @@ def check_translationhelper(b: Board) -> None: CHECKS = { + "phpbbmodders/knowledgebase": check_knowledgebase, "phpbbmodders/translationhelper": check_translationhelper, "phpbbmodders/prunenotifications": check_prunenotifications, "phpbbmodders/feedlinks": check_feedlinks, diff --git a/phpbb-test-board/screenshots.py b/phpbb-test-board/screenshots.py index b0baddc..a33018a 100755 --- a/phpbb-test-board/screenshots.py +++ b/phpbb-test-board/screenshots.py @@ -658,7 +658,8 @@ def kb_article(key: str, category: str, title: str, description: str, text: str, def shots_knowledgebase(board: Board, shoot: Shooter, args: argparse.Namespace) -> None: - """phpbbmodders/knowledgebase: articles, the approval queue, search, the ACP and the recent changes page. + """phpbbmodders/knowledgebase: articles, the approval queue, search, the ACP, the recent changes page and the + index introduction. Makes its own categories, articles and forum questions, so --seed is only needed for more forum content. The ACP pages are taken in full, so a new setting is not cut off. @@ -688,6 +689,15 @@ def shots_knowledgebase(board: Board, shoot: Shooter, args: argparse.Namespace) shoot.shot("11-acp-search-settings", acp.format("search", "index"), admin=True, full_page=True) shoot.shot("12-recent-changes", f"{kb}/recent_changes?sid={{sid}}", admin=True) + # The introduction above the index is off until switched on; its text is the default one stored on install. + board.sql("UPDATE phpbb_config SET config_value = '1' WHERE config_name = 'kb_intro_enabled'") + board.purge_cache() + shoot.shot("17-index-introduction", f"{kb}?sid={{sid}}", admin=True) + shoot.shot("18-acp-introduction", acp.format("config", "settings"), admin=True, + selector="fieldset:has(#intro_enabled)") + board.sql("UPDATE phpbb_config SET config_value = '0' WHERE config_name = 'kb_intro_enabled'") + board.purge_cache() + # The category tree is the one page that needs hierarchical browsing switched on. board.sql("UPDATE phpbb_config SET config_value = '1' WHERE config_name = 'kb_hierarchical_index'") board.purge_cache()