Skip to content

Commit

Permalink
Add submission/report endpoint (#317)
Browse files Browse the repository at this point in the history
* Simplify building of Slack report message

* Move function to send reports to Slack to the slack_helpers

* Add submission/report endpoint

* Add tests for submission/report endpoint

* Add submission fields for reports

* Save report reason and Slack message ID when reporting

* Only report posts that don't already have a report

* Fix submission report tests

* Adjust submission report Slack fields to match what we need

* Update implementation of report sending to new Slack data
  • Loading branch information
TimJentzsch committed Jan 16, 2022
1 parent 470c55b commit 99a871e
Show file tree
Hide file tree
Showing 8 changed files with 268 additions and 86 deletions.
23 changes: 23 additions & 0 deletions api/migrations/0017_auto_20220116_1355.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.10 on 2022-01-16 13:55

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0016_submission_nsfw"),
]

operations = [
migrations.AddField(
model_name="submission",
name="report_reason",
field=models.CharField(blank=True, max_length=300, null=True),
),
migrations.AddField(
model_name="submission",
name="report_slack_id",
field=models.CharField(blank=True, max_length=50, null=True),
),
]
23 changes: 23 additions & 0 deletions api/migrations/0018_auto_20220116_2114.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.10 on 2022-01-16 21:14

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("api", "0017_auto_20220116_1355"),
]

operations = [
migrations.RenameField(
model_name="submission",
old_name="report_slack_id",
new_name="report_slack_message_ts",
),
migrations.AddField(
model_name="submission",
name="report_slack_channel_id",
field=models.CharField(blank=True, max_length=50, null=True),
),
]
7 changes: 7 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ class Meta:
# specifically.
removed_from_queue = models.BooleanField(default=False)

# If the submission has been reported, this contains the report reason
report_reason = models.CharField(max_length=300, null=True, blank=True)
# If the submission has been reported, this contains the info to get
# the report message on Slack
report_slack_channel_id = models.CharField(max_length=50, null=True, blank=True)
report_slack_message_ts = models.CharField(max_length=50, null=True, blank=True)

# If we get errors back from our OCR solution or if a given submission cannot
# be run through OCR, this flag should be set.
cannot_ocr = models.BooleanField(default=False)
Expand Down
63 changes: 63 additions & 0 deletions api/tests/submissions/test_submission_report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import json
from unittest.mock import MagicMock

from django.test import Client
from django.urls import reverse
from rest_framework import status

from api.views.slack_helpers import client as slack_client
from utils.test_helpers import create_submission, setup_user_client


class TestSubmissionReport:
"""Tests validating the behavior of the Submission report process."""

def test_report_already_removed(self, client: Client) -> None:
"""Verify that reporting an already removed submission doesn't do anything."""
slack_client.chat_postMessage = MagicMock()
client, headers, user = setup_user_client(client)

submission = create_submission(id=3, removed_from_queue=True)
assert submission.removed_from_queue

data = {"reason": "Violation of ALL the rules"}

result = client.patch(
reverse("submission-report", args=[submission.id]),
json.dumps(data),
content_type="application/json",
**headers
)

submission.refresh_from_db()

assert result.status_code == status.HTTP_201_CREATED
assert submission.removed_from_queue
assert slack_client.chat_postMessage.call_count == 0

def test_report_not_removed(self, client: Client) -> None:
"""Verify that reporting sends a message to Slack."""
mock = MagicMock()
slack_client.chat_postMessage = mock
client, headers, user = setup_user_client(client)

submission = create_submission(id=3)
assert not submission.removed_from_queue
assert not submission.report_reason
assert not submission.report_slack_channel_id
assert not submission.report_slack_message_ts

data = {"reason": "Violation of ALL the rules"}

result = client.patch(
reverse("submission-report", args=[submission.id]),
json.dumps(data),
content_type="application/json",
**headers
)

submission.refresh_from_db()

assert result.status_code == status.HTTP_201_CREATED
assert not submission.removed_from_queue
assert submission.report_reason == "Violation of ALL the rules"
79 changes: 79 additions & 0 deletions api/views/slack_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from authentication.models import BlossomUser
from blossom.errors import ConfigurationError
from blossom.strings import translation
from utils.workers import send_to_worker

logger = logging.getLogger("api.views.slack_helpers")

Expand Down Expand Up @@ -500,3 +501,81 @@ def _send_transcription_to_slack(
)
except: # noqa
logger.warning(f"Cannot post message to slack. Msg: {msg}")


@send_to_worker
def ask_about_removing_post(submission: Submission, reason: str) -> None:
"""Ask Slack if we want to remove a reported submission or not."""
# Check if this got already sent to mod chat, we don't want duplicates
if (
submission.report_slack_channel_id is not None
or submission.report_slack_message_ts is not None
):
return

report_text = (
"Submission: <{url}|{title}> | <{tor_url}|ToR Post>\nReport reason: {reason}"
).format(
url=submission.url,
title=submission.title,
tor_url=submission.tor_url,
reason=reason,
)
keep_submission = f"keep_submission_{submission.id}"
remove_submission = f"remove_submission_{submission.id}"

# created using the Slack Block Kit Builder https://app.slack.com/block-kit-builder/
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"This submission was reported -- please investigate and decide"
" whether it should be removed."
),
},
},
{"type": "divider"},
{"type": "section", "text": {"type": "mrkdwn", "text": report_text}},
{"type": "divider"},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Keep"},
"value": keep_submission,
},
{
"type": "button",
"style": "danger",
"text": {"type": "plain_text", "text": "Remove"},
"value": remove_submission,
"confirm": {
"title": {"type": "plain_text", "text": "Are you sure?"},
"text": {
"type": "mrkdwn",
"text": "This will remove the submission from the queue.",
},
"confirm": {"type": "plain_text", "text": "Nuke it"},
"deny": {"type": "plain_text", "text": "Back"},
},
},
],
},
]

response = client.chat_postMessage(
channel=settings.SLACK_REPORTED_POST_CHANNEL, blocks=blocks
)
if not response["ok"]:
logger.warning(
f"Could not send report for submission {submission.id} to Slack!"
)
return

# TODO: Does this actually work?
submission.report_slack_channel_id = response["channel"]["id"]
submission.report_slack_message_ts = response["message"]["ts"]
submission.save()
46 changes: 45 additions & 1 deletion api/views/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@
from api.models import Source, Submission, Transcription
from api.pagination import StandardResultsSetPagination
from api.serializers import SubmissionSerializer
from api.views.slack_helpers import _send_transcription_to_slack
from api.views.slack_helpers import (
_send_transcription_to_slack,
ask_about_removing_post,
)
from api.views.slack_helpers import client as slack
from api.views.volunteer import VolunteerViewSet
from authentication.models import BlossomUser
Expand Down Expand Up @@ -867,6 +870,47 @@ def remove(self, request: Request, pk: int) -> Response:
data=self.serializer_class(submission, context={"request": request}).data,
)

@csrf_exempt
@swagger_auto_schema(
request_body=Schema(type="object", properties={"reason": Schema(type="str")}),
responses={
201: DocResponse("Successful report", schema=serializer_class),
404: "Submission not found.",
},
)
@validate_request(data_params={"reason"})
@action(detail=True, methods=["patch"])
def report(self, request: Request, pk: int, reason: str) -> Response:
"""Report the given submission.
This will send a message to the mods to review the submission.
"""
submission = get_object_or_404(Submission, id=pk)

if submission.removed_from_queue or submission.report_reason is not None:
# The submission is already removed or reported-- ignore the report
print("Already reported!")
return Response(
status=status.HTTP_201_CREATED,
data=self.serializer_class(
submission, context={"request": request}
).data,
)

print("Setting report reason")
# Save the report reason
submission.report_reason = reason
submission.save(skip_extras=True)

# Send the report to mod chat
ask_about_removing_post(submission, reason)
print("Asked for post removal")

return Response(
status=status.HTTP_201_CREATED,
data=self.serializer_class(submission, context={"request": request}).data,
)


def _is_returning_transcriber(volunteer: BlossomUser) -> bool:
"""Determine if the transcriber is returning.
Expand Down
32 changes: 21 additions & 11 deletions app/tests/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from unittest.mock import MagicMock

from django.test import RequestFactory

from api.models import Source
from api.views.slack_helpers import client as slack_client
from app.views import ask_about_removing_post, get_blossom_app_source
Expand All @@ -18,15 +16,27 @@ def test_get_blossom_app_source() -> None:
assert response.name == "TranscriptionApp"


def test_ask_about_removing_post(rf: RequestFactory) -> None:
def test_ask_about_removing_post() -> None:
"""Verify that block messages are handled appropriately."""
# Mock the Slack client to catch the sent messages by the function under test.
slack_client.chat_postMessage = MagicMock()
request = rf.get("asdf?reason=asdf")
submission = create_submission()
ask_about_removing_post(request, submission, worker_test_mode=True)

blocks = slack_client.chat_postMessage.call_args[1]["blocks"]
mock = MagicMock()
mock.return_value = {
"ok": True,
"message": {"ts": "12345"},
"channel": {"id": "6789"},
}
slack_client.chat_postMessage = mock

submission = create_submission(id=3)
assert not submission.report_slack_channel_id
assert not submission.report_slack_message_ts

ask_about_removing_post(submission, "asdf", worker_test_mode=True)
submission.refresh_from_db()

assert submission.report_slack_channel_id == "6789"
assert submission.report_slack_message_ts == "12345"
blocks = mock.call_args[1]["blocks"]
assert "asdf" in blocks[2]["text"]["text"]
assert "submission_1" in blocks[-1]["elements"][0]["value"]
assert "submission_1" in blocks[-1]["elements"][1]["value"]
assert "submission_3" in blocks[-1]["elements"][0]["value"]
assert "submission_3" in blocks[-1]["elements"][1]["value"]
Loading

0 comments on commit 99a871e

Please sign in to comment.