From cf7bacdf61ffb171ebac85e32dfcf12bc6972d59 Mon Sep 17 00:00:00 2001 From: SimpleTest Date: Wed, 22 Jul 2026 02:26:14 +0300 Subject: [PATCH] Show request publication validation errors --- lib/who_need_help/help.ex | 35 ++++++----- .../live/request_live/new.ex | 63 ++++++++++++++++++- test/who_need_help/trust_safety_test.exs | 21 +++++++ .../live/mutual_aid_live_test.exs | 25 ++++++++ 4 files changed, 127 insertions(+), 17 deletions(-) diff --git a/lib/who_need_help/help.ex b/lib/who_need_help/help.ex index 49267c9..cff2fd0 100644 --- a/lib/who_need_help/help.ex +++ b/lib/who_need_help/help.ex @@ -175,22 +175,27 @@ defmodule WhoNeedHelp.Help do end def create_request(%Scope{user: user}, attrs) do + changeset = + %HelpRequest{requester_id: user.id} + |> HelpRequest.create_changeset(attrs) + |> validate_structured_data() + result = - with {:ok, _limit} <- Trust.authorize_action(Scope.for_user(user), :create_request) do - Repo.transact(fn -> - with {:ok, request} <- - %HelpRequest{requester_id: user.id} - |> HelpRequest.create_changeset(attrs) - |> validate_structured_data() - |> Repo.insert(), - {:ok, _audit} <- - Trust.audit(user.id, "request.created", "request", request.id, %{ - "urgency" => to_string(request.urgency), - "category_id" => request.category_id - }) do - {:ok, request} - end - end) + 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), + "category_id" => request.category_id + }) do + {:ok, request} + end + end) + end + else + {:error, changeset} end with {:ok, request} <- result do diff --git a/lib/who_need_help_web/live/request_live/new.ex b/lib/who_need_help_web/live/request_live/new.ex index 9b45498..300ef40 100644 --- a/lib/who_need_help_web/live/request_live/new.ex +++ b/lib/who_need_help_web/live/request_live/new.ex @@ -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()} /> <.input @@ -323,18 +355,21 @@ defmodule WhoNeedHelpWeb.RequestLive.New do ]} />
+

{gettext("Location coordinates (required)")}

- {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." + )}

<.input @@ -352,6 +387,14 @@ defmodule WhoNeedHelpWeb.RequestLive.New do label={gettext("Longitude")} />
+

+ <.icon name="hero-exclamation-circle" class="size-5" /> + {message} +

<.input @@ -362,6 +405,22 @@ defmodule WhoNeedHelpWeb.RequestLive.New do class="checkbox checkbox-success mt-0.5" />
+ <.button class="btn btn-primary btn-lg w-full" phx-disable-with={gettext("Publishing…")} diff --git a/test/who_need_help/trust_safety_test.exs b/test/who_need_help/trust_safety_test.exs index f8e3c9e..3096bbf 100644 --- a/test/who_need_help/trust_safety_test.exs +++ b/test/who_need_help/trust_safety_test.exs @@ -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"]) diff --git a/test/who_need_help_web/live/mutual_aid_live_test.exs b/test/who_need_help_web/live/mutual_aid_live_test.exs index e1eb03e..8da5775 100644 --- a/test/who_need_help_web/live/mutual_aid_live_test.exs +++ b/test/who_need_help_web/live/mutual_aid_live_test.exs @@ -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()