fix(moderation): expose reported parent actions

This commit is contained in:
SimpleTest 2026-07-24 21:02:39 +03:00
parent 504c0af867
commit 50be881601
6 changed files with 169 additions and 11 deletions

View File

@ -78,6 +78,15 @@ defmodule WhoNeedHelp.Activities.Activity do
activity
|> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000)
|> require_hidden_reason()
end
defp require_hidden_reason(changeset) do
if get_field(changeset, :hidden_at) do
validate_required(changeset, :hidden_reason)
else
changeset
end
end
def public_coordinates(%__MODULE__{

View File

@ -87,6 +87,15 @@ defmodule WhoNeedHelp.Help.HelpRequest do
request
|> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000)
|> require_hidden_reason()
end
defp require_hidden_reason(changeset) do
if get_field(changeset, :hidden_at) do
validate_required(changeset, :hidden_reason)
else
changeset
end
end
def cancellation_changeset(request, attrs) do

View File

@ -345,10 +345,10 @@ defmodule WhoNeedHelp.Trust do
:reporter,
:reviewed_by,
request: :requester,
assignment: :helper,
message: :sender,
assignment: [:helper, :request],
message: [:sender, assignment: :request],
activity: :creator,
activity_message: :sender
activity_message: [:sender, :activity]
])
|> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id})

View File

@ -284,6 +284,38 @@ defmodule WhoNeedHelpWeb.ModerationLive do
defp signal_label(:manual), do: gettext("Manual review")
defp signal_label(value), do: to_string(value)
defp report_request(%{request: %WhoNeedHelp.Help.HelpRequest{} = request}), do: request
defp report_request(%{
assignment: %WhoNeedHelp.Help.Assignment{
request: %WhoNeedHelp.Help.HelpRequest{} = request
}
}),
do: request
defp report_request(%{
message: %WhoNeedHelp.Messaging.Message{
assignment: %WhoNeedHelp.Help.Assignment{
request: %WhoNeedHelp.Help.HelpRequest{} = request
}
}
}),
do: request
defp report_request(_report), do: nil
defp report_activity(%{activity: %WhoNeedHelp.Activities.Activity{} = activity}),
do: activity
defp report_activity(%{
activity_message: %WhoNeedHelp.Activities.Message{
activity: %WhoNeedHelp.Activities.Activity{} = activity
}
}),
do: activity
defp report_activity(_report), do: nil
@impl true
def render(assigns) do
~H"""
@ -309,6 +341,8 @@ defmodule WhoNeedHelpWeb.ModerationLive do
<div class="mt-4 space-y-4">
<article :for={report <- @reports} class="rounded-2xl border border-base-300 p-5">
<% hide_form = scoped_form(%{"note" => ""}, :moderation, "report-hide-#{report.id}") %>
<% reported_request = report_request(report) %>
<% reported_activity = report_activity(report) %>
<% report_form =
scoped_form(
%{"status" => to_string(report.status), "resolution_note" => ""},
@ -324,6 +358,12 @@ defmodule WhoNeedHelpWeb.ModerationLive do
<span :if={report.activity_message_id} class="badge badge-info badge-outline">
{gettext("activity message")}
</span>
<span :if={report.message_id} class="badge badge-info badge-outline">
{gettext("matched chat message")}
</span>
<span :if={report.assignment_id} class="badge badge-info badge-outline">
{gettext("matched conversation")}
</span>
<span class="text-xs text-base-content/50">
{gettext("by %{name}", name: report.reporter.display_name)}
</span>
@ -338,29 +378,55 @@ defmodule WhoNeedHelpWeb.ModerationLive do
{gettext("View scoped evidence")}
</button>
<.form
:if={report.request_id}
:if={reported_request && is_nil(reported_request.hidden_at)}
for={hide_form}
phx-submit="hide-request"
phx-value-id={report.request_id}
phx-value-id={reported_request.id}
class="flex gap-2"
>
<.input field={hide_form[:note]} placeholder={gettext("Hide reason")} />
<.input
field={hide_form[:note]}
placeholder={gettext("Required reason for hiding")}
required
/>
<.button class="btn btn-sm btn-error self-end">
{gettext("Hide request")}
</.button>
</.form>
<button
:if={reported_request && reported_request.hidden_at}
type="button"
phx-click="restore-request"
phx-value-id={reported_request.id}
class="btn btn-sm btn-outline"
>
{gettext("Restore request")}
</button>
<.form
:if={report.activity_id}
:if={reported_activity && is_nil(reported_activity.hidden_at)}
for={hide_form}
phx-submit="hide-activity"
phx-value-id={report.activity_id}
phx-value-id={reported_activity.id}
class="flex gap-2"
>
<.input field={hide_form[:note]} placeholder={gettext("Hide reason")} />
<.input
field={hide_form[:note]}
placeholder={gettext("Required reason for hiding")}
required
/>
<.button class="btn btn-sm btn-error self-end">
{gettext("Hide activity")}
</.button>
</.form>
<button
:if={reported_activity && reported_activity.hidden_at}
type="button"
phx-click="restore-activity"
phx-value-id={reported_activity.id}
class="btn btn-sm btn-outline"
>
{gettext("Restore activity")}
</button>
</div>
<.form
for={report_form}

View File

@ -153,6 +153,21 @@ defmodule WhoNeedHelp.TrustSafetyTest do
)
end
test "hiding a request requires a moderation reason", context do
moderator =
user_fixture(display_name: "Reason moderator")
|> Ecto.Changeset.change(role: :moderator)
|> Repo.update!()
{:ok, request} = Help.create_request(context.requester_scope, context.attrs)
assert {:error, %Ecto.Changeset{} = changeset} =
Trust.hide_request(user_scope_fixture(moderator), request.id, "")
assert "can't be blank" in errors_on(changeset).hidden_reason
refute Repo.get!(WhoNeedHelp.Help.HelpRequest, request.id).hidden_at
end
test "category moderation cannot cross help and activity taxonomies", context do
moderator =
user_fixture(display_name: "Category moderator")

View File

@ -1113,7 +1113,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
{:ok, request} = Help.create_request(user_scope_fixture(requester), attrs)
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
{:ok, _message} =
{:ok, message} =
Messaging.send_message(user_scope_fixture(helper), assignment, %{
"body" => "Evidence message"
})
@ -1122,7 +1122,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
Trust.report(user_scope_fixture(requester), %{
"reason" => "harassment",
"details" => "Please review the matched conversation.",
"assignment_id" => assignment.id
"message_id" => message.id
})
{:ok, _proposal} =
@ -1140,6 +1140,11 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert html =~ "Please review the matched conversation."
assert html =~ "Bicycle repair"
assert has_element?(
view,
"form[phx-submit='hide-request'][phx-value-id='#{request.id}']"
)
evidence_html =
view
|> element("button[phx-click='view-evidence']")
@ -1148,6 +1153,60 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert evidence_html =~ "Evidence message"
end
test "message reports expose parent activity moderation and switch to restore", %{
conn: conn,
user: user,
scope: participant_scope
} do
user |> Ecto.Changeset.change(role: :moderator) |> Repo.update!()
organizer = user_fixture(display_name: "Activity organizer")
Catalog.seed_defaults()
activity_category = Catalog.list_categories(:activity) |> List.first()
{:ok, activity} =
Activities.create_activity(
user_scope_fixture(organizer),
activity_attrs(activity_category)
)
{:ok, join_request} = Activities.request_to_join(participant_scope, activity.id)
{:ok, _approved} =
Activities.approve_participant(user_scope_fixture(organizer), join_request.id)
{:ok, message} =
Activities.send_message(user_scope_fixture(organizer), activity.id, %{
"body" => "Reported activity message"
})
{:ok, _report} =
Trust.report(participant_scope, %{
"reason" => "harassment",
"details" => "Please review this activity message.",
"activity_message_id" => message.id
})
{:ok, view, _html} = live(conn, ~p"/moderation")
assert has_element?(
view,
"form[phx-submit='hide-activity'][phx-value-id='#{activity.id}']"
)
view
|> form("form[phx-submit='hide-activity'][phx-value-id='#{activity.id}']", %{
"moderation" => %{"note" => "Unsafe public activity details"}
})
|> render_submit()
assert has_element?(
view,
"button[phx-click='restore-activity'][phx-value-id='#{activity.id}']"
)
assert Repo.get!(WhoNeedHelp.Activities.Activity, activity.id).hidden_at
end
test "abuse-signal pagination stays available without opening report evidence", %{
conn: conn,
user: user