Show request publication validation errors
Some checks are pending
Quality / full-local-gates (push) Waiting to run
Some checks are pending
Quality / full-local-gates (push) Waiting to run
This commit is contained in:
parent
9853cc021c
commit
cf7bacdf61
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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…")}
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user