Avoid self-link suggestions for redirect targets

This commit is contained in:
Edward Betts 2026-07-22 10:57:28 +01:00
parent d1efae7745
commit e022872767
2 changed files with 94 additions and 4 deletions

View file

@ -55,6 +55,61 @@ class ArticlePageSkipTests(unittest.TestCase):
) )
self.assertIn("Useful candidate", body) self.assertIn("Useful candidate", body)
def test_redirect_target_is_left_out_of_candidates(self) -> None:
hits = [
hit("Documentary film"),
hit("Useful candidate"),
]
with (
patch("web_view.api.get_wiki_info", return_value="Documentary film"),
patch("web_view.search_count", return_value=2),
patch("web_view.search_count_with_link", return_value=0),
patch("web_view.search_no_link", return_value=(2, hits)),
):
response = self.client.get("/link/DVD_documentary")
body = response.get_data(as_text=True)
self.assertEqual(response.status_code, 200)
self.assertNotIn('data-title="Documentary film"', body)
self.assertIn("Useful candidate", body)
def test_redirect_target_case_variant_is_left_out_of_candidates(self) -> None:
hits = [hit("documentary film")]
with (
patch("web_view.api.get_wiki_info", return_value="Documentary film"),
patch("web_view.search_count", return_value=1),
patch("web_view.search_count_with_link", return_value=0),
patch("web_view.search_no_link", return_value=(1, hits)),
):
response = self.client.get("/link/DVD_documentary")
body = response.get_data(as_text=True)
self.assertEqual(response.status_code, 200)
self.assertIn("No more candidates found for this article.", body)
class ValidHitTests(unittest.TestCase):
def setUp(self) -> None:
web_view.app.config["TESTING"] = True
self.client = web_view.app.test_client()
def test_redirect_target_is_not_valid_hit(self) -> None:
with patch("web_view.get_diff") as get_diff:
response = self.client.get(
"/api/1/valid_hit",
query_string={
"link_to": "DVD documentary",
"link_from": "Documentary film",
"redirect_to": "Documentary film",
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.get_json(), {"valid": False})
get_diff.assert_not_called()
class ProtectedCandidateTests(unittest.TestCase): class ProtectedCandidateTests(unittest.TestCase):
def test_edit_protected_hits_are_filtered_out(self) -> None: def test_edit_protected_hits_are_filtered_out(self) -> None:

View file

@ -104,6 +104,28 @@ def article_url(title: str) -> str:
return flask.url_for("article_page", url_title=title.replace(" ", "_")) return flask.url_for("article_page", url_title=title.replace(" ", "_"))
def canonical_title_key(title: str) -> str:
"""Normalize a Wikipedia article title for same-page comparisons."""
title = title.replace("_", " ").strip()
return title[:1].casefold() + title[1:]
def same_article_title(a: str | None, b: str | None) -> bool:
"""Compare titles using Wikipedia's first-letter case-insensitive rule."""
if not a or not b:
return False
return canonical_title_key(a) == canonical_title_key(b)
def is_self_link_candidate(
hit_title: str, from_title: str, redirect_to: str | None = None
) -> bool:
"""Return true when a candidate is the source article or its redirect target."""
return same_article_title(hit_title, from_title) or same_article_title(
hit_title, redirect_to
)
def get_hit_count(q: str) -> int: def get_hit_count(q: str) -> int:
"""Search Wikipedia and return hit count.""" """Search Wikipedia and return hit count."""
return typing.cast(int, run_search(q, limit=0)["searchinfo"]["totalhits"]) return typing.cast(int, run_search(q, limit=0)["searchinfo"]["totalhits"])
@ -391,6 +413,8 @@ def handle_post(url_title: str) -> Response:
except mediawiki_oauth.LoginNeeded: except mediawiki_oauth.LoginNeeded:
next_url = flask.url_for("article_page", url_title=url_title, title=hit_title) next_url = flask.url_for("article_page", url_title=url_title, title=hit_title)
return flask.redirect(flask.url_for("start_oauth", next=next_url)) return flask.redirect(flask.url_for("start_oauth", next=next_url))
except NoMatch:
return flask.make_response(render_error("No valid unlinked mention found."), 400)
except (mediawiki_api.APIError, api.MediawikiError) as e: except (mediawiki_api.APIError, api.MediawikiError) as e:
return flask.make_response( return flask.make_response(
render_mediawiki_error(e, prefix="Save failed"), 502 render_mediawiki_error(e, prefix="Save failed"), 502
@ -437,8 +461,12 @@ def article_page(url_title: str) -> str | Response:
hits = [h for h in hits if h["title"] == title_param] + \ hits = [h for h in hits if h["title"] == title_param] + \
[h for h in hits if h["title"] != title_param] [h for h in hits if h["title"] != title_param]
hits = [h for h in hits if h["title"] not in session_skipped hits = [
and h["title"] != from_title and h["title"] != case_flip_first(from_title)] h
for h in hits
if h["title"] not in session_skipped
and not is_self_link_candidate(h["title"], from_title, redirect_to)
]
if not hits: if not hits:
return flask.render_template("all_done.html") return flask.render_template("all_done.html")
@ -460,13 +488,16 @@ def article_page(url_title: str) -> str | Response:
def do_save(title: str, hit_title: str) -> str: def do_save(title: str, hit_title: str) -> str:
"""Update page on Wikipedia.""" """Update page on Wikipedia."""
token = mediawiki_oauth.get_token()
try: try:
redirect_to = api.get_wiki_info(title) redirect_to = api.get_wiki_info(title)
except (api.MissingPage, api.MultipleRedirects, api.MediawikiError): except (api.MissingPage, api.MultipleRedirects, api.MediawikiError):
redirect_to = None redirect_to = None
if is_self_link_candidate(hit_title, title, redirect_to):
raise NoMatch
token = mediawiki_oauth.get_token()
found = get_match(title, hit_title, redirect_to) found = get_match(title, hit_title, redirect_to)
summary = ( summary = (
@ -510,6 +541,10 @@ def api_valid_hit() -> werkzeug.wrappers.response.Response:
link_from = flask.request.args["link_from"] link_from = flask.request.args["link_from"]
redirect_to = flask.request.args.get("redirect_to") or None redirect_to = flask.request.args.get("redirect_to") or None
if is_self_link_candidate(link_from, link_to, redirect_to):
_record_skip(link_to, link_from)
return flask.jsonify(valid=False)
try: try:
found = get_diff(link_to, link_from, redirect_to) found = get_diff(link_to, link_from, redirect_to)
except NoMatch: except NoMatch: