Show request publication validation errors
Some checks are pending
Quality / full-local-gates (push) Waiting to run

This commit is contained in:
SimpleTest 2026-07-22 02:26:14 +03:00
parent 9853cc021c
commit cf7bacdf61
4 changed files with 127 additions and 17 deletions

View File

@ -175,14 +175,16 @@ defmodule WhoNeedHelp.Help do
end
def create_request(%Scope{user: user}, attrs) do
result =
with {:ok, _limit} <- Trust.authorize_action(Scope.for_user(user), :create_request) do
Repo.transact(fn ->
with {:ok, request} <-
changeset =
%HelpRequest{requester_id: user.id}
|> HelpRequest.create_changeset(attrs)
|> validate_structured_data()
|> Repo.insert(),
result =
if changeset.valid? do
with {:ok, _limit} <- Trust.authorize_action(Scope.for_user(user), :create_request) do
Repo.transact(fn ->
with {:ok, request} <- Repo.insert(changeset),
{:ok, _audit} <-
Trust.audit(user.id, "request.created", "request", request.id, %{
"urgency" => to_string(request.urgency),
@ -192,6 +194,9 @@ defmodule WhoNeedHelp.Help do
end
end)
end
else
{:error, changeset}
end
with {:ok, request} <- result do
broadcast({:request_created, get_request!(request.id)})

View File

@ -24,6 +24,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
|> assign(:categories, Catalog.list_categories())
|> assign(:selected_category, nil)
|> assign(:structured_fields, [])
|> assign(:submission_errors, [])
|> assign(:form, to_form(changeset))}
end
@ -39,6 +40,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
{:noreply,
socket
|> assign(:form, to_form(changeset))
|> maybe_refresh_submission_errors(changeset)
|> assign_category(params)}
end
@ -57,6 +59,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
{:noreply,
socket
|> assign(:form, to_form(changeset))
|> assign(:submission_errors, submission_errors(changeset))
|> assign_category(params)}
else
{:noreply, put_flash(socket, :error, error_message(changeset))}
@ -78,6 +81,34 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
defp normalize_expiry(params), do: params
defp maybe_refresh_submission_errors(%{assigns: %{submission_errors: []}} = socket, _changeset),
do: socket
defp maybe_refresh_submission_errors(socket, changeset),
do: assign(socket, :submission_errors, submission_errors(changeset))
defp submission_errors(changeset) do
changeset.errors
|> Enum.map(fn
{:location, _error} ->
{:location,
gettext("Choose a location using the button or enter both latitude and longitude.")}
{:expires_at, _error} ->
{:expires_at, gettext("Choose an expiry time in the future.")}
{field, error} ->
{field, "#{Phoenix.Naming.humanize(field)}: #{translate_error(error)}"}
end)
|> Enum.uniq()
end
defp minimum_expiry_value do
DateTime.utc_now(:second)
|> DateTime.add(60, :second)
|> Calendar.strftime("%Y-%m-%dT%H:%M")
end
defp assign_category(socket, params) do
category =
Enum.find(socket.assigns.categories, &(&1.id == params["category_id"]))
@ -304,6 +335,7 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
field={@form[:expires_at]}
type="datetime-local"
label={gettext("Request expires (UTC)")}
min={minimum_expiry_value()}
/>
</div>
<.input
@ -323,18 +355,21 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
]}
/>
<div class="rounded-2xl bg-base-200 p-5">
<h2 class="font-bold">{gettext("Location coordinates (required)")}</h2>
<button
type="button"
phx-hook="LocationPicker"
id="location-picker"
data-latitude-target="#request-latitude"
data-longitude-target="#request-longitude"
class="btn btn-outline btn-sm"
class="btn btn-outline btn-sm mt-3"
>
{gettext("Use my current foreground location")}
</button>
<p class="mt-2 text-xs text-base-content/60">
{gettext("You can also enter coordinates manually for local testing.")}
{gettext(
"Use the button or enter both coordinates. Public map precision follows the visibility choice above."
)}
</p>
<div class="mt-3 grid grid-cols-2 gap-3">
<.input
@ -352,6 +387,14 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
label={gettext("Longitude")}
/>
</div>
<p
:for={{:location, message} <- @submission_errors}
id="request-location-error"
class="mt-3 flex items-center gap-2 text-sm font-semibold text-error"
>
<.icon name="hero-exclamation-circle" class="size-5" />
{message}
</p>
</div>
<div class="rounded-2xl border border-base-300 p-4 text-sm">
<.input
@ -362,6 +405,22 @@ defmodule WhoNeedHelpWeb.RequestLive.New do
class="checkbox checkbox-success mt-0.5"
/>
</div>
<div
:if={@submission_errors != []}
id="request-submit-errors"
role="alert"
aria-live="assertive"
class="alert alert-error items-start"
>
<.icon name="hero-exclamation-triangle" class="mt-0.5 size-5 shrink-0" />
<div>
<h2 class="font-bold">{gettext("Request was not published")}</h2>
<p class="text-sm">{gettext("Fix the following details and publish again:")}</p>
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
<li :for={{_field, message} <- @submission_errors}>{message}</li>
</ul>
</div>
</div>
<.button
class="btn btn-primary btn-lg w-full"
phx-disable-with={gettext("Publishing…")}

View File

@ -305,6 +305,27 @@ defmodule WhoNeedHelp.TrustSafetyTest do
)
end
test "invalid request forms do not consume the publication rate limit", context do
old = Application.get_env(:who_need_help, :rate_limit_policies)
Application.put_env(:who_need_help, :rate_limit_policies, %{
"create_request" => %{"limit" => 1, "window_seconds" => 60}
})
on_exit(fn -> Application.put_env(:who_need_help, :rate_limit_policies, old) end)
invalid = Map.drop(context.attrs, ["latitude", "longitude"])
assert {:error, %Ecto.Changeset{}} =
Help.create_request(context.requester_scope, invalid)
assert {:ok, _request} =
Help.create_request(context.requester_scope, context.attrs)
assert {:error, :rate_limited} =
Help.create_request(context.requester_scope, context.attrs)
end
test "malformed runtime rate-limit configuration never crashes a request", context do
old = Application.get_env(:who_need_help, :rate_limit_policies)
Application.put_env(:who_need_help, :rate_limit_policies, ["not", "a", "map"])

View File

@ -267,6 +267,31 @@ defmodule WhoNeedHelpWeb.MutualAidLiveTest do
)
end
test "new request form explains location and expiry validation failures", %{conn: conn} do
category = Catalog.seed_defaults()
{:ok, view, _html} = live(conn, ~p"/requests/new")
params =
category
|> request_attrs()
|> Map.drop(["latitude", "longitude"])
|> Map.put(
"expires_at",
DateTime.utc_now(:second)
|> DateTime.add(-60, :second)
|> Calendar.strftime("%Y-%m-%dT%H:%M")
)
html = render_submit(view, "save", %{"help_request" => params})
assert html =~ "Request was not published"
assert html =~ "Choose a location using the button or enter both latitude and longitude."
assert html =~ "Choose an expiry time in the future."
assert has_element?(view, "#request-location-error")
assert has_element?(view, "#request-submit-errors[role='alert']")
assert has_element?(view, "#help_request_title[value='Medicine is ready at the pharmacy']")
end
test "activity form is category-driven and separate from urgent help", %{conn: conn} do
Catalog.seed_defaults()