Avoid self-link suggestions for redirect targets
This commit is contained in:
parent
d1efae7745
commit
e022872767
2 changed files with 94 additions and 4 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
43
web_view.py
43
web_view.py
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue