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 activity
|> cast(attrs, [:hidden_at, :hidden_reason]) |> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000) |> 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 end
def public_coordinates(%__MODULE__{ def public_coordinates(%__MODULE__{

View File

@ -87,6 +87,15 @@ defmodule WhoNeedHelp.Help.HelpRequest do
request request
|> cast(attrs, [:hidden_at, :hidden_reason]) |> cast(attrs, [:hidden_at, :hidden_reason])
|> validate_length(:hidden_reason, max: 1_000) |> 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 end
def cancellation_changeset(request, attrs) do def cancellation_changeset(request, attrs) do

View File

@ -345,10 +345,10 @@ defmodule WhoNeedHelp.Trust do
:reporter, :reporter,
:reviewed_by, :reviewed_by,
request: :requester, request: :requester,
assignment: :helper, assignment: [:helper, :request],
message: :sender, message: [:sender, assignment: :request],
activity: :creator, activity: :creator,
activity_message: :sender activity_message: [:sender, :activity]
]) ])
|> Repo.all() |> Repo.all()
|> Pagination.page(limit, &{&1.inserted_at, &1.id}) |> 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(:manual), do: gettext("Manual review")
defp signal_label(value), do: to_string(value) 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 @impl true
def render(assigns) do def render(assigns) do
~H""" ~H"""
@ -309,6 +341,8 @@ defmodule WhoNeedHelpWeb.ModerationLive do
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<article :for={report <- @reports} class="rounded-2xl border border-base-300 p-5"> <article :for={report <- @reports} class="rounded-2xl border border-base-300 p-5">
<% hide_form = scoped_form(%{"note" => ""}, :moderation, "report-hide-#{report.id}") %> <% hide_form = scoped_form(%{"note" => ""}, :moderation, "report-hide-#{report.id}") %>
<% reported_request = report_request(report) %>
<% reported_activity = report_activity(report) %>
<% report_form = <% report_form =
scoped_form( scoped_form(
%{"status" => to_string(report.status), "resolution_note" => ""}, %{"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"> <span :if={report.activity_message_id} class="badge badge-info badge-outline">
{gettext("activity message")} {gettext("activity message")}
</span> </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"> <span class="text-xs text-base-content/50">
{gettext("by %{name}", name: report.reporter.display_name)} {gettext("by %{name}", name: report.reporter.display_name)}
</span> </span>
@ -338,29 +378,55 @@ defmodule WhoNeedHelpWeb.ModerationLive do
{gettext("View scoped evidence")} {gettext("View scoped evidence")}
</button> </button>
<.form <.form
:if={report.request_id} :if={reported_request && is_nil(reported_request.hidden_at)}
for={hide_form} for={hide_form}
phx-submit="hide-request" phx-submit="hide-request"
phx-value-id={report.request_id} phx-value-id={reported_request.id}
class="flex gap-2" 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"> <.button class="btn btn-sm btn-error self-end">
{gettext("Hide request")} {gettext("Hide request")}
</.button> </.button>
</.form> </.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 <.form
:if={report.activity_id} :if={reported_activity && is_nil(reported_activity.hidden_at)}
for={hide_form} for={hide_form}
phx-submit="hide-activity" phx-submit="hide-activity"
phx-value-id={report.activity_id} phx-value-id={reported_activity.id}
class="flex gap-2" 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"> <.button class="btn btn-sm btn-error self-end">
{gettext("Hide activity")} {gettext("Hide activity")}
</.button> </.button>
</.form> </.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> </div>
<.form <.form
for={report_form} for={report_form}

View File

@ -153,6 +153,21 @@ defmodule WhoNeedHelp.TrustSafetyTest do
) )
end 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 test "category moderation cannot cross help and activity taxonomies", context do
moderator = moderator =
user_fixture(display_name: "Category 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, request} = Help.create_request(user_scope_fixture(requester), attrs)
{:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id) {:ok, assignment} = Help.accept_request(user_scope_fixture(helper), request.id)
{:ok, _message} = {:ok, message} =
Messaging.send_message(user_scope_fixture(helper), assignment, %{ Messaging.send_message(user_scope_fixture(helper), assignment, %{
"body" => "Evidence message" "body" => "Evidence message"
}) })
@ -1122,7 +1122,7 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
Trust.report(user_scope_fixture(requester), %{ Trust.report(user_scope_fixture(requester), %{
"reason" => "harassment", "reason" => "harassment",
"details" => "Please review the matched conversation.", "details" => "Please review the matched conversation.",
"assignment_id" => assignment.id "message_id" => message.id
}) })
{:ok, _proposal} = {:ok, _proposal} =
@ -1140,6 +1140,11 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert html =~ "Please review the matched conversation." assert html =~ "Please review the matched conversation."
assert html =~ "Bicycle repair" assert html =~ "Bicycle repair"
assert has_element?(
view,
"form[phx-submit='hide-request'][phx-value-id='#{request.id}']"
)
evidence_html = evidence_html =
view view
|> element("button[phx-click='view-evidence']") |> element("button[phx-click='view-evidence']")
@ -1148,6 +1153,60 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
assert evidence_html =~ "Evidence message" assert evidence_html =~ "Evidence message"
end 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", %{ test "abuse-signal pagination stays available without opening report evidence", %{
conn: conn, conn: conn,
user: user user: user