Skip to content

Commit

Permalink
Merge pull request #3576 from webcompat/issue/3574/1
Browse files Browse the repository at this point in the history
Fixes #3574: Incorporate bugbug ml labelling into moderation process
  • Loading branch information
Karl Dubost authored May 31, 2021
2 parents 12031a8 + 3de4ec2 commit 81d7fd5
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 42 deletions.
3 changes: 3 additions & 0 deletions config/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
)
BUGBUG_HTTP_SERVER = "https://bugbug.herokuapp.com"
CLASSIFIER_PATH = "needsdiagnosis/predict/github/webcompat/web-bugs-private" # noqa
AUTOCLOSED_MILESTONE_ID = 15

if STAGING:
GITHUB_CLIENT_ID = os.environ.get('STAGING_GITHUB_CLIENT_ID')
Expand All @@ -59,6 +60,7 @@
)
BUGBUG_HTTP_SERVER = "https://bugbug.herokuapp.com"
CLASSIFIER_PATH = "needsdiagnosis/predict/github/webcompat/webcompat-tests-private" # noqa
AUTOCLOSED_MILESTONE_ID = 5

if LOCALHOST:
# for now we are using .env only on localhost
Expand All @@ -82,6 +84,7 @@
)
BUGBUG_HTTP_SERVER = "http://0.0.0.0:8000"
CLASSIFIER_PATH = "needsdiagnosis/predict/github/webcompat/webcompat-tests-private" # noqa
AUTOCLOSED_MILESTONE_ID = 5

# BUG STATUS
# The id will be initialized when the app is started.
Expand Down
3 changes: 2 additions & 1 deletion tests/fixtures/webhooks/private_issue_opened.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"description": "issue in the process of being moderated"
}
],
"state": "open"
"state": "open",
"html_url": "https://github.com/webcompat/webcompat-tests-private/issues/600"
}
}
1 change: 1 addition & 0 deletions tests/fixtures/webhooks/private_milestone_accepted.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"description": "issue in the process of being moderated"
}
],
"html_url": "https://github.com/webcompat/webcompat-tests-private/issues/600",
"state": "open",
"milestone": {
"url": "https://api.github.com/repos/webcompat/webcompat-tests-private/milestones/2",
Expand Down
42 changes: 42 additions & 0 deletions tests/fixtures/webhooks/private_milestone_ml_autoclosed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"action": "milestoned",
"issue": {
"title": "www.netflix.com - test private issue autoclosed",
"repository_url": "https://api.github.com/repos/webcompat/webcompat-tests-private",
"number": 600,
"html_url": "https://github.com/webcompat/webcompat-tests-private/issues/600",
"body": "<!-- @browser: Firefox 55.0 -->\n<!-- @ua_header: Mozilla/5.0 (X11; Linux x86_64; rv:55.0) Gecko/20100101 Firefox/55.0 -->\n<!-- @reported_with: web -->\n<!-- @public_url: https://github.com/webcompat/webcompat-tests/issues/1 -->\n\n**URL**: https://www.netflix.com/",
"labels": [
{
"id": 1788251357,
"node_id": "MDU6TGFiZWwxNzg4MjUxMzU3",
"url": "https://api.github.com/repos/webcompat/webcompat-tests/labels/action-needsmoderation",
"name": "action-needsmoderation",
"color": "d36200",
"default": false,
"description": "issue in the process of being moderated"
}
],
"state": "open",
"milestone": {
"url": "https://api.github.com/repos/webcompat/webcompat-tests-private/milestones/5",
"html_url": "https://github.com/webcompat/webcompat-tests-private/milestone/5",
"labels_url": "https://api.github.com/repos/webcompat/webcompat-tests-private/milestones/5/labels",
"id": 6795520,
"node_id": "MDk6TWlsZXN0b25lNjc5NTUyMA==",
"number": 5,
"title": "ml-autoclosed",
"description": "Issues closed automatically based on ml prediction",
"open_issues": 1,
"closed_issues": 3,
"state": "open",
"created_at": "2021-05-26T20:24:14Z",
"updated_at": "2021-05-27T16:56:08Z",
"due_on": null,
"closed_at": null
}
},
"milestone": {
"title": "ml-autoclosed"
}
}
14 changes: 10 additions & 4 deletions tests/unit/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ def test_moderation_template_rejected(setup):
assert 'Its original content has been deleted' in actual['body']


def test_moderation_template_autoclosed(setup):
"""Check the return values are for the rejected case."""
actual = moderation_template('autoclosed')
assert actual['title'] == 'Issue closed.'
assert 'We have closed this issue\nautomatically as we suspect it is invalid' in actual['body'] # noqa


def test_moderation_template_ongoing(setup):
"""Check the return values are for the needsmoderation case."""
# test the default
Expand All @@ -142,7 +149,6 @@ def test_moderation_template_ongoing(setup):
actual = moderation_template('ongoing')
assert actual['title'] == 'In the moderation queue.'
assert 'put in the moderation queue.' in actual['body']
# bad keyword, we go back to the default.
actual = moderation_template('punkcat')
assert actual['title'] == 'In the moderation queue.'
assert 'put in the moderation queue.' in actual['body']
# bad keyword causes error.
with pytest.raises(ValueError):
moderation_template('punkcat')
23 changes: 23 additions & 0 deletions tests/unit/test_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,21 @@ def test_prepare_rejected_issue(self):
self.assertEqual(type(actual), dict)
self.assertEqual(actual, expected)

def test_prepare_rejected_issue_autoclosed(self):
"""Test payload for the rejected issue that we want to auto close."""
expected = {
'body': '<p>Thanks for the report. We have closed this issue\n'
'automatically as we suspect it is invalid. If we made '
'a mistake, please\nfile a new issue and try to provide '
'more context.</p>',
'labels': ['bugbug-probability-high'],
'milestone': 8,
'state': 'closed',
'title': 'Issue closed.'}
actual = helpers.prepare_rejected_issue("autoclosed")
self.assertEqual(type(actual), dict)
self.assertEqual(actual, expected)

@patch('webcompat.webhooks.ml.make_classification_request')
def test_get_issue_classification(self, mock_class):
"""Make only one request if it returns 200 status code right away.
Expand All @@ -479,6 +494,14 @@ def test_get_issue_classification_exception(self, mock_class, mock_time):

assert mock_class.call_count == 4

def test_prepare_private_url(self):
"""Test private issue url is in the right format."""
expected = '\n<!-- @private_url: https://github.com/webcompat/webcompat-tests-private/issues/600 -->\n' # noqa
actual = helpers.prepare_private_url(
'https://github.com/webcompat/webcompat-tests-private/issues/600'
)
self.assertEqual(actual, expected)


if __name__ == '__main__':
unittest.main()
43 changes: 35 additions & 8 deletions tests/unit/test_webhook_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
from tests.unit.test_webhook import event_data
from webcompat.webhooks.model import WebHookIssue

AUTOCLOSED_MILESTONE_ID = webcompat.app.config['AUTOCLOSED_MILESTONE_ID']

# Some expected responses as tuples
accepted = ('Moderated issue accepted', 200, {'Content-Type': 'text/plain'})
rejected = ('Moderated issue rejected', 200, {'Content-Type': 'text/plain'})
incomplete = ('Moderated issue closed as incomplete',
200, {'Content-Type': 'text/plain'})
invalid = ('Moderated issue closed as invalid',
200, {'Content-Type': 'text/plain'})
autoclosed = ('Issue closed as invalid by ml bot',
200, {'Content-Type': 'text/plain'})
boring = ('Not an interesting hook', 403, {'Content-Type': 'text/plain'})
gracias = ('gracias, amigo.', 200, {'Content-Type': 'text/plain'})
wrong_repo = ('Wrong repository', 403, {'Content-Type': 'text/plain'})
Expand All @@ -40,7 +44,9 @@
'public_url': 'https://github.com/webcompat/webcompat-tests/issues/1',
'repository_url': 'https://api.github.com/repos/webcompat/webcompat-tests-private', # noqa
'title': 'www.netflix.com - test valid event',
'host_reported_from': ''}
'host_reported_from': '',
'html_url': 'https://github.com/webcompat/webcompat-tests-private/issues/600' # noqa
}

issue_info2 = {
'action': 'milestoned', 'state': 'open', 'milestoned_with': 'accepted',
Expand All @@ -50,7 +56,9 @@
'public_url': 'https://github.com/webcompat/webcompat-tests/issues/1',
'repository_url': 'https://api.github.com/repos/webcompat/webcompat-tests-private', # noqa
'title': 'www.netflix.com - test private issue accepted',
'host_reported_from': ''}
'host_reported_from': '',
'html_url': 'https://github.com/webcompat/webcompat-tests-private/issues/600' # noqa
}


def test_model_instance():
Expand Down Expand Up @@ -164,6 +172,21 @@ def test_closing_public_issues(mock_mr):
assert issue.get_public_issue_number() in uri


@patch('webcompat.webhooks.model.make_request')
def test_autoclose_public_issue(mock_mr):
"""Test API request that is sent to GitHub for auto closed issue."""
json_event, signature = event_data('private_milestone_ml_autoclosed.json')
payload = json.loads(json_event)
issue = WebHookIssue.from_dict(payload)
issue.close_public_issue(reason='autoclosed')
method, uri, data = mock_mr.call_args[0]
# make sure we sent a patch with the right data to GitHub
assert method == 'patch'
assert type(data) == dict
assert issue.get_public_issue_number() in uri
assert issue.html_url in data['body']


def test_prepare_public_comment():
"""Test we prepare the right comment body."""
expected_payload = '{"body": "[Original issue 1](https://github.com/webcompat/webcompat-tests/issues/1)"}' # noqa
Expand Down Expand Up @@ -306,9 +329,9 @@ def test_process_issue_action_scenarios(mock_mr, mock_classification):
('private_milestone_closed_unmoderated.json', rejected),
('private_milestone_accepted_incomplete.json', incomplete),
('private_milestone_accepted_invalid.json', invalid),
('private_milestone_accepted_closed.json', boring),
('private_issue_opened.json', comment_added),
('public_milestone_needscontact.json', outreach_comment_added)
('public_milestone_needscontact.json', outreach_comment_added),
('private_milestone_ml_autoclosed.json', autoclosed),
]
mock_classification.return_value = (
{'prob': [0.03385603427886963, 0.9661439657211304], 'class': 1}
Expand Down Expand Up @@ -349,6 +372,9 @@ def test_process_issue_action_github_api_exception(mock_mr, caplog):
'close_public_issue'),
('private_milestone_accepted_incomplete.json',
'private:closing public issue as incomplete failed',
'close_public_issue'),
('private_milestone_ml_autoclosed.json',
'private:closing public issue as invalid by ml-bot failed',
'close_public_issue')
]
for scenario in scenarios:
Expand Down Expand Up @@ -376,6 +402,7 @@ def test_process_issue_action_close_scenarios(mock_close, mock_mr):
('private_milestone_closed_unmoderated.json', 'rejected'),
('private_milestone_accepted_incomplete.json', 'incomplete'),
('private_milestone_accepted_invalid.json', 'invalid'),
('private_milestone_ml_autoclosed.json', 'autoclosed'),
]
for scenario in called:
issue_payload, arg = scenario
Expand Down Expand Up @@ -426,11 +453,11 @@ def test_classify_issue_probability_high(mock_mr, mock_classification):
issue.classify()
method, uri, data = mock_mr.call_args[0]

# make sure we set a bugbug-probability-high label and
# send a post request to Github
assert method == 'post'
# make sure we move the issue to ml-autoclosed milestone and
# send a patch request to Github
assert method == 'patch'
assert type(data) == dict
assert data.get('labels') == ['bugbug-probability-high']
assert data.get('milestone') == AUTOCLOSED_MILESTONE_ID


@patch('webcompat.webhooks.model.get_issue_classification')
Expand Down
49 changes: 37 additions & 12 deletions webcompat/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,33 @@
REJECTED_BODY = '''<p>The content of this issue doesn't meet our
<a href="https://webcompat.com/terms#acceptable-use">acceptable use</a>
guidelines. Its original content has been deleted.</p>'''
AUTOCLOSE_TITLE = 'Issue closed.'
AUTOCLOSE_BODY = '''<p>Thanks for the report. We have closed this issue
automatically as we suspect it is invalid. If we made a mistake, please
file a new issue and try to provide more context.</p>'''

TEMPLATE_CONFIG = {
'rejected': {
'title': REJECTED_TITLE,
'body': REJECTED_BODY
},
'invalid': {
'title': '',
'body': INVALID_BODY
},
'incomplete': {
'title': '',
'body': INCOMPLETE_BODY
},
'autoclosed': {
'title': AUTOCLOSE_TITLE,
'body': AUTOCLOSE_BODY
},
'ongoing': {
'title': ONGOING_TITLE,
'body': ONGOING_BODY
}
}


def moderation_template(choice='ongoing'):
Expand All @@ -53,22 +80,20 @@ def moderation_template(choice='ongoing'):
- rejected: the issue has been rejected.
- incomplete: the issue is incomplete (but not rejected)
- invalid: the issue is invalid (but not rejected)
- autoclosed: the issue is auto closed by ml bot
The default is 'ongoing' even with unknown keywords.
"""
if choice == 'rejected':
title = REJECTED_TITLE
body = REJECTED_BODY
elif choice == 'invalid':
title = ''
body = INVALID_BODY
elif choice == 'incomplete':
title = ''
body = INCOMPLETE_BODY
if choice in TEMPLATE_CONFIG:
return {
'title': TEMPLATE_CONFIG[choice].get('title'),
'body': TEMPLATE_CONFIG[choice].get('body')
}
else:
title = ONGOING_TITLE
body = ONGOING_BODY
return {'title': title, 'body': body}
temp = (", ".join(TEMPLATE_CONFIG.keys()))
raise ValueError(
f'Template choice must be one of the following: {temp}'
)


def report_private_issue(form, public_url):
Expand Down
28 changes: 23 additions & 5 deletions webcompat/webhooks/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from webcompat.helpers import proxy_request
from webcompat.helpers import to_bytes
from webcompat.issues import moderation_template
from webcompat.form import wrap_metadata

BROWSERS = ['blackberry', 'brave', 'chrome', 'edge', 'firefox', 'iceweasel', 'ie', 'lynx', 'myie', 'opera', 'puffin', 'qq', 'safari', 'samsung', 'seamonkey', 'uc', 'vivaldi'] # noqa
GECKO_BROWSERS = ['browser-android-components',
Expand All @@ -32,6 +33,16 @@
IOS_BROWSERS = ['browser-firefox-ios', ]
PUBLIC_REPO = app.config['ISSUES_REPO_URI']
PRIVATE_REPO = app.config['PRIVATE_REPO_URI']
REJECTED_CONFIG = {
'rejected': {
'template': 'rejected',
'labels': ['status-notacceptable']
},
'autoclosed': {
'template': 'autoclosed',
'labels': ['bugbug-probability-high']
}
}


def extract_metadata(body):
Expand Down Expand Up @@ -211,11 +222,12 @@ def msg_log(msg, issue_number):
log.info(msg)


def prepare_rejected_issue():
def prepare_rejected_issue(reason='rejected'):
"""Create the payload for the rejected moderated issue.
When the issue has been moderated as rejected,
we need to change a couple of things in the public space
When the issue has been moderated as rejected or labelled
invalid by the bot we need to change a couple of things
in the public space
- change Title
- change body
Expand All @@ -225,8 +237,14 @@ def prepare_rejected_issue():
"""
# Extract the relevant information
invalid_id = app.config['STATUSES']['invalid']['id']
payload_request = moderation_template('rejected')
payload_request['labels'] = ['status-notacceptable']
config = REJECTED_CONFIG[reason]
payload_request = moderation_template(config['template'])
payload_request['labels'] = config['labels']
payload_request['state'] = 'closed'
payload_request['milestone'] = invalid_id
return payload_request


def prepare_private_url(private_url):
"""Create metadata with private url"""
return "\n" + wrap_metadata(('private_url', private_url))
Loading

0 comments on commit 81d7fd5

Please sign in to comment.