Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/reviews/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ class WikiAdmin(admin.ModelAdmin):

@admin.register(WikiConfiguration)
class WikiConfigurationAdmin(admin.ModelAdmin):
list_display = ("wiki", "updated_at")
list_display = ("wiki", "revertrisk_threshold", "updated_at")
search_fields = ("wiki__name", "wiki__code")
fields = ["wiki", "revertrisk_threshold"]


@admin.register(PendingPage)
Expand Down
269 changes: 218 additions & 51 deletions app/reviews/autoreview.py

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions app/reviews/migrations/0011_add_revertrisk_threshold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.25 on 2025-10-17

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("reviews", "0010_modelscores"),
]

operations = [
migrations.AddField(
model_name="wikiconfiguration",
name="revertrisk_threshold",
field=models.FloatField(
blank=True,
help_text="Revert risk threshold (0.0-1.0). Set to None to disable check.",
null=True,
),
),
]
6 changes: 6 additions & 0 deletions app/reviews/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ class WikiConfiguration(models.Model):
)
updated_at = models.DateTimeField(auto_now=True)

revertrisk_threshold = models.FloatField(
null=True,
blank=True,
help_text="Revert risk threshold (0.0-1.0). Set to None to disable check."
)

def __str__(self) -> str: # pragma: no cover - debug helper
return f"Configuration for {self.wiki.code}"

Expand Down
83 changes: 82 additions & 1 deletion app/reviews/tests/test_autoreview.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python manage.py test result is FAILED (errors=7)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image Fixed!

Original file line number Diff line number Diff line change
@@ -1,20 +1,101 @@
from __future__ import annotations

import unittest
import logging
import json
from datetime import datetime, timedelta
from unittest.mock import MagicMock, Mock, patch
from unittest.mock import patch, MagicMock, Mock

from django.test import TestCase, override_settings

from reviews import autoreview
from reviews.autoreview import (
_evaluate_revision,
_get_revertrisk_score,
_check_ores_scores,
_find_invalid_isbns,
_validate_isbn_10,
_validate_isbn_13,
)
from reviews.services import was_user_blocked_after

logging.disable(logging.CRITICAL)


class DummyRevision:
def __init__(self, revid=12345, user_name="TestUser", superset_data=None):
self.revid = revid
self.user_name = user_name
self.page = MagicMock()
self.page.categories = []
self.page.wiki = MagicMock()
self.page.wiki.language_code = "en"
self.superset_data = superset_data or {}

def get_categories(self):
return []

class TestRevertrisk(unittest.TestCase):

@patch("reviews.autoreview.is_bot_edit", return_value=False)
@patch("reviews.autoreview._get_revertrisk_score")
def test_high_risk_blocks(self, mock_score, mock_bot_edit):
mock_score.return_value = 0.9
revision = DummyRevision()
result = _evaluate_revision(
revision, None, auto_groups={},
blocking_categories={}, revertrisk_threshold=0.8
)
self.assertEqual(result["decision"].status, "blocked")

@patch("reviews.autoreview.is_bot_edit", return_value=False)
@patch("reviews.autoreview._get_revertrisk_score")
def test_low_risk_continues(self, mock_score, mock_bot_edit):
mock_score.return_value = 0.5
revision = DummyRevision()
result = _evaluate_revision(
revision, None, auto_groups={},
blocking_categories={}, revertrisk_threshold=0.8
)
self.assertEqual(result["decision"].status, "manual")

@patch("reviews.autoreview.is_bot_edit", return_value=False)
@patch("reviews.autoreview._get_revertrisk_score")
def test_api_error_handled(self, mock_score, mock_bot_edit):
mock_score.return_value = None
revision = DummyRevision()
result = _evaluate_revision(
revision, None, auto_groups={},
blocking_categories={}, revertrisk_threshold=0.8
)
self.assertEqual(result["decision"].status, "manual")

@patch("reviews.autoreview._get_revertrisk_score")
def test_bot_bypasses_check(self, mock_score):
revision = DummyRevision(superset_data={"rc_bot": True})
result = _evaluate_revision(
revision, None, auto_groups={},
blocking_categories={}, revertrisk_threshold=0.8
)
self.assertEqual(result["decision"].status, "approve")
mock_score.assert_not_called()

@patch("reviews.autoreview.is_bot_edit", return_value=False)
def test_no_threshold_skips_check(self, mock_bot_edit):
revision = DummyRevision()
result = _evaluate_revision(
revision, None, auto_groups={},
blocking_categories={}, revertrisk_threshold=None
)
self.assertFalse(any(t["id"] == "revertrisk" for t in result["tests"]))

@patch("reviews.autoreview.http.fetch")
@patch("reviews.autoreview.is_bot_edit", return_value=False)
def test_api_exception_returns_none(self, mock_bot_edit, mock_fetch):
mock_fetch.side_effect = Exception("Network error")
score = _get_revertrisk_score(DummyRevision())
self.assertIsNone(score)


class ISBNValidationTests(TestCase):
"""Test ISBN-10 and ISBN-13 checksum validation."""
Expand Down
34 changes: 33 additions & 1 deletion app/reviews/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,20 +383,51 @@ def api_clear_cache(request: HttpRequest, pk: int) -> JsonResponse:
@csrf_exempt
@require_http_methods(["GET", "PUT"])
def api_configuration(request: HttpRequest, pk: int) -> JsonResponse:
"""Get or update wiki configuration including revertrisk threshold."""
wiki = _get_wiki(pk)
configuration = wiki.configuration

if request.method == "PUT":
if request.content_type == "application/json":
payload = json.loads(request.body.decode("utf-8")) if request.body else {}
else:
payload = request.POST.dict()

blocking_categories = payload.get("blocking_categories", [])
auto_groups = payload.get("auto_approved_groups", [])

if isinstance(blocking_categories, str):
blocking_categories = [blocking_categories]
if isinstance(auto_groups, str):
auto_groups = [auto_groups]

# Handle revertrisk_threshold: empty string or null becomes None
revertrisk_threshold = payload.get("revertrisk_threshold")
if revertrisk_threshold is not None:
if revertrisk_threshold == "" or revertrisk_threshold == "null":
revertrisk_threshold = None
else:
try:
revertrisk_threshold = float(revertrisk_threshold)
if not (0.0 <= revertrisk_threshold <= 1.0):
return JsonResponse(
{"error": "revertrisk_threshold must be between 0.0 and 1.0"},
status=400
)
except (ValueError, TypeError):
return JsonResponse(
{"error": "revertrisk_threshold must be a valid number or null"},
status=400
)

configuration.blocking_categories = blocking_categories
configuration.auto_approved_groups = auto_groups
configuration.revertrisk_threshold = revertrisk_threshold
configuration.save(
update_fields=["blocking_categories", "auto_approved_groups",
"revertrisk_threshold", "updated_at"]
)


ores_damaging_threshold = payload.get("ores_damaging_threshold")
ores_goodfaith_threshold = payload.get("ores_goodfaith_threshold")
Expand Down Expand Up @@ -465,6 +496,7 @@ def validate_threshold(value, name):
{
"blocking_categories": configuration.blocking_categories,
"auto_approved_groups": configuration.auto_approved_groups,
"revertrisk_threshold": configuration.revertrisk_threshold,
"ores_damaging_threshold": configuration.ores_damaging_threshold,
"ores_goodfaith_threshold": configuration.ores_goodfaith_threshold,
"ores_damaging_threshold_living": configuration.ores_damaging_threshold_living,
Expand Down
13 changes: 13 additions & 0 deletions app/static/reviews/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ createApp({
const forms = reactive({
blockingCategories: "",
autoApprovedGroups: "",
revertriskThreshold: "",
oresDamagingThreshold: 0.0,
oresGoodfaithThreshold: 0.0,
oresDamagingThresholdLiving: 0.0,
Expand Down Expand Up @@ -241,6 +242,7 @@ createApp({
if (!currentWiki.value) {
forms.blockingCategories = "";
forms.autoApprovedGroups = "";
forms.revertriskThreshold = "";
forms.oresDamagingThreshold = 0.0;
forms.oresGoodfaithThreshold = 0.0;
forms.oresDamagingThresholdLiving = 0.0;
Expand All @@ -249,6 +251,9 @@ createApp({
}
forms.blockingCategories = (currentWiki.value.configuration.blocking_categories || []).join("\n");
forms.autoApprovedGroups = (currentWiki.value.configuration.auto_approved_groups || []).join("\n");
forms.revertriskThreshold = currentWiki.value.configuration.revertrisk_threshold !== null
? String(currentWiki.value.configuration.revertrisk_threshold)
: "";
forms.oresDamagingThreshold = currentWiki.value.configuration.ores_damaging_threshold || 0.0;
forms.oresGoodfaithThreshold = currentWiki.value.configuration.ores_goodfaith_threshold || 0.0;
forms.oresDamagingThresholdLiving = currentWiki.value.configuration.ores_damaging_threshold_living || 0.0;
Expand Down Expand Up @@ -382,6 +387,13 @@ createApp({
return;
}

let revertriskValue = null;
if (forms.revertriskThreshold && forms.revertriskThreshold.trim() !== "") {
revertriskValue = parseFloat(forms.revertriskThreshold);
if (isNaN(revertriskValue)) {
state.error = "Revertrisk threshold must be a valid number";
return;
}
const validationErrors = [];
const damagingError = validateOresThreshold(forms.oresDamagingThreshold, "Damaging threshold");
if (damagingError) validationErrors.push(damagingError);
Expand All @@ -403,6 +415,7 @@ createApp({
const payload = {
blocking_categories: parseTextarea(forms.blockingCategories),
auto_approved_groups: parseTextarea(forms.autoApprovedGroups),
revertrisk_threshold: revertriskValue,
ores_damaging_threshold: forms.oresDamagingThreshold,
ores_goodfaith_threshold: forms.oresGoodfaithThreshold,
ores_damaging_threshold_living: forms.oresDamagingThresholdLiving,
Expand Down
Loading