Beacon submission approval workflow
Non-admin users can now submit beacons but they are held in an unapproved state until an admin approves them. Only approved beacons appear in the public list; pending submissions are shown in a separate admin-only section on /beacons with Approve and Delete actions. The show page surfaces a pending badge and an admin-only Approve button when viewing an unapproved beacon. Also formats EIRP (mW) on the index page without scientific notation.
This commit is contained in:
parent
f8763feb38
commit
80f2725cd5
9 changed files with 304 additions and 42 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule Microwaveprop.Beacons do
|
defmodule Microwaveprop.Beacons do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
The Beacons context. Beacon records are public (anyone can read)
|
The Beacons context. Any authenticated user can submit a beacon,
|
||||||
but only admin users can create/update/delete them — that gating
|
but submissions are held as unapproved until an admin approves
|
||||||
is enforced by the router's live_session for the form views.
|
them. Only approved beacons appear in the public list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
|
|
@ -28,9 +28,22 @@ defmodule Microwaveprop.Beacons do
|
||||||
Phoenix.PubSub.broadcast(Microwaveprop.PubSub, @topic, message)
|
Phoenix.PubSub.broadcast(Microwaveprop.PubSub, @topic, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Returns all beacons ordered by frequency."
|
@doc "Returns approved beacons ordered by frequency."
|
||||||
def list_beacons do
|
def list_beacons do
|
||||||
Repo.all(from b in Beacon, order_by: [asc: b.frequency_mhz, asc: b.callsign])
|
Repo.all(
|
||||||
|
from b in Beacon,
|
||||||
|
where: b.approved == true,
|
||||||
|
order_by: [asc: b.frequency_mhz, asc: b.callsign]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Returns unapproved beacons awaiting admin review."
|
||||||
|
def list_pending_beacons do
|
||||||
|
Repo.all(
|
||||||
|
from b in Beacon,
|
||||||
|
where: b.approved == false,
|
||||||
|
order_by: [asc: b.inserted_at]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Gets a single beacon. Raises if not found."
|
@doc "Gets a single beacon. Raises if not found."
|
||||||
|
|
@ -54,6 +67,14 @@ defmodule Microwaveprop.Beacons do
|
||||||
|> broadcast_if_ok(:updated)
|
|> broadcast_if_ok(:updated)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc "Marks a beacon as approved, making it visible in the public list."
|
||||||
|
def approve_beacon(%Beacon{} = beacon) do
|
||||||
|
beacon
|
||||||
|
|> Ecto.Changeset.change(approved: true)
|
||||||
|
|> Repo.update()
|
||||||
|
|> broadcast_if_ok(:updated)
|
||||||
|
end
|
||||||
|
|
||||||
@doc "Deletes a beacon."
|
@doc "Deletes a beacon."
|
||||||
def delete_beacon(%Beacon{} = beacon) do
|
def delete_beacon(%Beacon{} = beacon) do
|
||||||
case Repo.delete(beacon) do
|
case Repo.delete(beacon) do
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ defmodule Microwaveprop.Beacons.Beacon do
|
||||||
field :power_mw, :float
|
field :power_mw, :float
|
||||||
field :height_ft, :float
|
field :height_ft, :float
|
||||||
field :on_the_air, :boolean, default: true
|
field :on_the_air, :boolean, default: true
|
||||||
|
field :approved, :boolean, default: false
|
||||||
field :user_id, :binary_id
|
field :user_id, :binary_id
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,7 @@ defmodule Microwaveprop.Beacons.RangeEstimate do
|
||||||
"""
|
"""
|
||||||
@spec nearest_band_mhz(number()) :: integer()
|
@spec nearest_band_mhz(number()) :: integer()
|
||||||
def nearest_band_mhz(freq_mhz) when is_number(freq_mhz) do
|
def nearest_band_mhz(freq_mhz) when is_number(freq_mhz) do
|
||||||
BandConfig.all_freqs()
|
Enum.min_by(BandConfig.all_freqs(), fn b -> abs(b - freq_mhz) end)
|
||||||
|> Enum.min_by(fn b -> abs(b - freq_mhz) end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,12 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
Beacons
|
Beacons
|
||||||
<:subtitle>Microwave beacons tracked by NTMS.</:subtitle>
|
<:subtitle>Microwave beacons tracked by NTMS.</:subtitle>
|
||||||
<:actions>
|
<:actions>
|
||||||
<.button :if={admin?(@current_scope)} variant="primary" navigate={~p"/beacons/new"}>
|
<.button
|
||||||
<.icon name="hero-plus" /> New Beacon
|
:if={authenticated?(@current_scope)}
|
||||||
|
variant="primary"
|
||||||
|
navigate={~p"/beacons/new"}
|
||||||
|
>
|
||||||
|
<.icon name="hero-plus" /> Submit Beacon
|
||||||
</.button>
|
</.button>
|
||||||
</:actions>
|
</:actions>
|
||||||
</.header>
|
</.header>
|
||||||
|
|
@ -28,10 +32,10 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Lat">{beacon.lat}</:col>
|
<:col :let={{_id, beacon}} label="Lat">{beacon.lat}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Lon">{beacon.lon}</:col>
|
<:col :let={{_id, beacon}} label="Lon">{beacon.lon}</:col>
|
||||||
<:col :let={{_id, beacon}} label="EIRP (mW)">{beacon.power_mw}</:col>
|
<:col :let={{_id, beacon}} label="EIRP (mW)">{format_mw(beacon.power_mw)}</:col>
|
||||||
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||||
<:col :let={{_id, beacon}} label="On air">
|
<:col :let={{_id, beacon}} label="On air">
|
||||||
<span class={["badge badge-sm", beacon.on_the_air && "badge-success" || "badge-ghost"]}>
|
<span class={["badge badge-sm", (beacon.on_the_air && "badge-success") || "badge-ghost"]}>
|
||||||
{if beacon.on_the_air, do: "Yes", else: "No"}
|
{if beacon.on_the_air, do: "Yes", else: "No"}
|
||||||
</span>
|
</span>
|
||||||
</:col>
|
</:col>
|
||||||
|
|
@ -51,6 +55,44 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
</.link>
|
</.link>
|
||||||
</:action>
|
</:action>
|
||||||
</.table>
|
</.table>
|
||||||
|
|
||||||
|
<div :if={admin?(@current_scope) and @pending != []} class="mt-10">
|
||||||
|
<.header>
|
||||||
|
Pending approval
|
||||||
|
<:subtitle>Submitted beacons waiting for admin review.</:subtitle>
|
||||||
|
</.header>
|
||||||
|
|
||||||
|
<.table
|
||||||
|
id="pending-beacons"
|
||||||
|
rows={@streams.pending}
|
||||||
|
row_click={fn {_id, beacon} -> JS.navigate(~p"/beacons/#{beacon}") end}
|
||||||
|
>
|
||||||
|
<:col :let={{_id, beacon}} label="Frequency (MHz)">{beacon.frequency_mhz}</:col>
|
||||||
|
<:col :let={{_id, beacon}} label="Call">{beacon.callsign}</:col>
|
||||||
|
<:col :let={{_id, beacon}} label="Grid">{beacon.grid}</:col>
|
||||||
|
<:col :let={{_id, beacon}} label="EIRP (mW)">{format_mw(beacon.power_mw)}</:col>
|
||||||
|
<:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}</:col>
|
||||||
|
<:col :let={{_id, beacon}} label="Submitted">
|
||||||
|
{Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")}
|
||||||
|
</:col>
|
||||||
|
<:action :let={{_id, beacon}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("approve", value: %{id: beacon.id})}
|
||||||
|
data-confirm="Approve this beacon?"
|
||||||
|
>
|
||||||
|
Approve
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
<:action :let={{id, beacon}}>
|
||||||
|
<.link
|
||||||
|
phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")}
|
||||||
|
data-confirm="Delete this beacon?"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</.link>
|
||||||
|
</:action>
|
||||||
|
</.table>
|
||||||
|
</div>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
@ -59,10 +101,19 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
if connected?(socket), do: Beacons.subscribe_beacons()
|
if connected?(socket), do: Beacons.subscribe_beacons()
|
||||||
|
|
||||||
|
pending =
|
||||||
|
if admin?(socket.assigns.current_scope) do
|
||||||
|
Beacons.list_pending_beacons()
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "Beacons")
|
|> assign(:page_title, "Beacons")
|
||||||
|> stream(:beacons, Beacons.list_beacons())}
|
|> assign(:pending, pending)
|
||||||
|
|> stream(:beacons, Beacons.list_beacons())
|
||||||
|
|> stream(:pending, pending)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -70,7 +121,26 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
if admin?(socket.assigns.current_scope) do
|
if admin?(socket.assigns.current_scope) do
|
||||||
beacon = Beacons.get_beacon!(id)
|
beacon = Beacons.get_beacon!(id)
|
||||||
{:ok, _} = Beacons.delete_beacon(beacon)
|
{:ok, _} = Beacons.delete_beacon(beacon)
|
||||||
{:noreply, stream_delete(socket, :beacons, beacon)}
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> stream_delete(:beacons, beacon)
|
||||||
|
|> stream_delete(:pending, beacon)}
|
||||||
|
else
|
||||||
|
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("approve", %{"id" => id}, socket) do
|
||||||
|
if admin?(socket.assigns.current_scope) do
|
||||||
|
beacon = Beacons.get_beacon!(id)
|
||||||
|
{:ok, approved} = Beacons.approve_beacon(beacon)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> put_flash(:info, "Approved #{approved.callsign}.")
|
||||||
|
|> stream_delete(:pending, beacon)
|
||||||
|
|> stream_insert(:beacons, approved)}
|
||||||
else
|
else
|
||||||
{:noreply, put_flash(socket, :error, "Admins only.")}
|
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||||
end
|
end
|
||||||
|
|
@ -78,9 +148,45 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({type, %Microwaveprop.Beacons.Beacon{}}, socket) when type in [:created, :updated, :deleted] do
|
def handle_info({type, %Microwaveprop.Beacons.Beacon{}}, socket) when type in [:created, :updated, :deleted] do
|
||||||
{:noreply, stream(socket, :beacons, Beacons.list_beacons(), reset: true)}
|
pending =
|
||||||
|
if admin?(socket.assigns.current_scope) do
|
||||||
|
Beacons.list_pending_beacons()
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:pending, pending)
|
||||||
|
|> stream(:beacons, Beacons.list_beacons(), reset: true)
|
||||||
|
|> stream(:pending, pending, reset: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp admin?(%{user: %{is_admin: true}}), do: true
|
defp admin?(%{user: %{is_admin: true}}), do: true
|
||||||
defp admin?(_), do: false
|
defp admin?(_), do: false
|
||||||
|
|
||||||
|
defp authenticated?(%{user: %{}}), do: true
|
||||||
|
defp authenticated?(_), do: false
|
||||||
|
|
||||||
|
# Format a milliwatt power value without scientific notation.
|
||||||
|
# Keeps up to 3 decimal places, drops trailing zeros.
|
||||||
|
defp format_mw(nil), do: ""
|
||||||
|
|
||||||
|
defp format_mw(mw) when is_float(mw) do
|
||||||
|
if mw == trunc(mw) do
|
||||||
|
Integer.to_string(trunc(mw))
|
||||||
|
else
|
||||||
|
mw |> :erlang.float_to_binary(decimals: 3) |> trim_trailing_zeros()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp format_mw(mw), do: to_string(mw)
|
||||||
|
|
||||||
|
defp trim_trailing_zeros(str) do
|
||||||
|
if String.contains?(str, ".") do
|
||||||
|
str |> String.trim_trailing("0") |> String.trim_trailing(".")
|
||||||
|
else
|
||||||
|
str
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,20 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
<.header>
|
<.header>
|
||||||
{@beacon.callsign}
|
{@beacon.callsign}
|
||||||
<:subtitle>{@beacon.frequency_mhz} MHz · {@beacon.grid}</:subtitle>
|
<:subtitle>
|
||||||
|
{@beacon.frequency_mhz} MHz · {@beacon.grid}
|
||||||
|
<span :if={not @beacon.approved} class="badge badge-warning badge-sm ml-2">
|
||||||
|
Pending approval
|
||||||
|
</span>
|
||||||
|
</:subtitle>
|
||||||
<:actions>
|
<:actions>
|
||||||
|
<.button
|
||||||
|
:if={admin?(@current_scope) and not @beacon.approved}
|
||||||
|
variant="primary"
|
||||||
|
phx-click="approve"
|
||||||
|
>
|
||||||
|
<.icon name="hero-check" /> Approve
|
||||||
|
</.button>
|
||||||
<.button navigate={~p"/beacons"}>
|
<.button navigate={~p"/beacons"}>
|
||||||
<.icon name="hero-arrow-left" />
|
<.icon name="hero-arrow-left" />
|
||||||
</.button>
|
</.button>
|
||||||
|
|
@ -54,7 +66,8 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
<% else %>
|
<% else %>
|
||||||
(no HRRR data — defaulting cells to 50)
|
(no HRRR data — defaulting cells to 50)
|
||||||
<% end %>
|
<% end %>
|
||||||
· <strong>{length(@estimate.cells)}</strong> cells rendered
|
· <strong>{length(@estimate.cells)}</strong>
|
||||||
|
cells rendered
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -67,8 +80,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{tier.label}</strong>
|
<strong>{tier.label}</strong> (≥ {tier.min_dbm} dBm)
|
||||||
(≥ {tier.min_dbm} dBm)
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
@ -101,6 +113,20 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
|
||||||
|> assign(:estimate, RangeEstimate.estimate(beacon))}
|
|> assign(:estimate, RangeEstimate.estimate(beacon))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("approve", _params, socket) do
|
||||||
|
if admin?(socket.assigns.current_scope) do
|
||||||
|
{:ok, approved} = Beacons.approve_beacon(socket.assigns.beacon)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:beacon, approved)
|
||||||
|
|> put_flash(:info, "Beacon approved.")}
|
||||||
|
else
|
||||||
|
{:noreply, put_flash(socket, :error, "Admins only.")}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do
|
def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,17 @@ defmodule MicrowavepropWeb.Router do
|
||||||
# Health check — no pipeline, minimal overhead
|
# Health check — no pipeline, minimal overhead
|
||||||
get "/health", MicrowavepropWeb.HealthController, :check, log: false
|
get "/health", MicrowavepropWeb.HealthController, :check, log: false
|
||||||
|
|
||||||
# Admin-only routes must come first so that literal segments like
|
# Authenticated (non-admin) and admin-only routes must come first so that
|
||||||
# `/beacons/new` are registered before the public `/beacons/:id`.
|
# literal segments like `/beacons/new` are registered before the public
|
||||||
|
# `/beacons/:id`.
|
||||||
scope "/", MicrowavepropWeb do
|
scope "/", MicrowavepropWeb do
|
||||||
pipe_through [:browser, :require_authenticated_user]
|
pipe_through [:browser, :require_authenticated_user]
|
||||||
|
|
||||||
live_session :admin, on_mount: [{MicrowavepropWeb.UserAuth, :require_admin}] do
|
live_session :authenticated, on_mount: [{MicrowavepropWeb.UserAuth, :default}] do
|
||||||
live "/beacons/new", BeaconLive.Form, :new
|
live "/beacons/new", BeaconLive.Form, :new
|
||||||
|
end
|
||||||
|
|
||||||
|
live_session :admin, on_mount: [{MicrowavepropWeb.UserAuth, :require_admin}] do
|
||||||
live "/beacons/:id/edit", BeaconLive.Form, :edit
|
live "/beacons/:id/edit", BeaconLive.Form, :edit
|
||||||
|
|
||||||
live "/users", UserManagementLive.Index, :index
|
live "/users", UserManagementLive.Index, :index
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
defmodule Microwaveprop.Repo.Migrations.AddApprovedToBeacons do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:beacons) do
|
||||||
|
add :approved, :boolean, null: false, default: false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Existing beacons were admin-created, so mark them approved.
|
||||||
|
execute "UPDATE beacons SET approved = true", ""
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -18,12 +18,36 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "list_beacons/0" do
|
describe "list_beacons/0" do
|
||||||
test "returns all beacons regardless of creator" do
|
test "returns only approved beacons" do
|
||||||
u1 = user_fixture()
|
u1 = user_fixture()
|
||||||
u2 = user_fixture()
|
u2 = user_fixture()
|
||||||
b1 = beacon_fixture(u1)
|
{:ok, b1} = Beacons.create_beacon(u1, valid_beacon_attrs())
|
||||||
b2 = beacon_fixture(u2, callsign: "W5TX")
|
{:ok, b1} = Beacons.approve_beacon(b1)
|
||||||
assert [^b1, ^b2] = Enum.sort_by(Beacons.list_beacons(), & &1.callsign)
|
_pending = beacon_fixture(u2, callsign: "W5TX")
|
||||||
|
assert [^b1] = Beacons.list_beacons()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "list_pending_beacons/0" do
|
||||||
|
test "returns only unapproved beacons" do
|
||||||
|
u1 = user_fixture()
|
||||||
|
u2 = user_fixture()
|
||||||
|
{:ok, approved} = Beacons.create_beacon(u1, valid_beacon_attrs())
|
||||||
|
{:ok, _approved} = Beacons.approve_beacon(approved)
|
||||||
|
pending = beacon_fixture(u2, callsign: "W5TX")
|
||||||
|
assert [^pending] = Beacons.list_pending_beacons()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "approve_beacon/1" do
|
||||||
|
test "marks the beacon as approved and broadcasts an update" do
|
||||||
|
user = user_fixture()
|
||||||
|
beacon = beacon_fixture(user)
|
||||||
|
refute beacon.approved
|
||||||
|
Beacons.subscribe_beacons()
|
||||||
|
assert {:ok, approved} = Beacons.approve_beacon(beacon)
|
||||||
|
assert approved.approved == true
|
||||||
|
assert_receive {:updated, %Beacon{approved: true}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -50,6 +74,19 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
assert beacon.user_id == user.id
|
assert beacon.user_id == user.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "new beacons default to unapproved" do
|
||||||
|
user = user_fixture()
|
||||||
|
assert {:ok, beacon} = Beacons.create_beacon(user, valid_beacon_attrs())
|
||||||
|
assert beacon.approved == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignores client-supplied approved value" do
|
||||||
|
user = user_fixture()
|
||||||
|
attrs = Map.put(valid_beacon_attrs(), :approved, true)
|
||||||
|
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
|
||||||
|
assert beacon.approved == false
|
||||||
|
end
|
||||||
|
|
||||||
test "derives grid from lat/lon when grid is omitted" do
|
test "derives grid from lat/lon when grid is omitted" do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
# DFW is EM12
|
# DFW is EM12
|
||||||
|
|
@ -61,7 +98,8 @@ defmodule Microwaveprop.BeaconsTest do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
|
|
||||||
attrs =
|
attrs =
|
||||||
valid_beacon_attrs(grid: "EM12kp")
|
[grid: "EM12kp"]
|
||||||
|
|> valid_beacon_attrs()
|
||||||
|> Map.drop([:lat, :lon])
|
|> Map.drop([:lat, :lon])
|
||||||
|
|
||||||
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
|
assert {:ok, beacon} = Beacons.create_beacon(user, attrs)
|
||||||
|
|
|
||||||
|
|
@ -16,63 +16,112 @@ defmodule MicrowavepropWeb.BeaconLiveTest do
|
||||||
promote_to_admin(user_fixture())
|
promote_to_admin(user_fixture())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp approved_beacon_fixture(user, attrs \\ %{}) do
|
||||||
|
beacon = beacon_fixture(user, attrs)
|
||||||
|
{:ok, beacon} = Microwaveprop.Beacons.approve_beacon(beacon)
|
||||||
|
beacon
|
||||||
|
end
|
||||||
|
|
||||||
describe "Index (public)" do
|
describe "Index (public)" do
|
||||||
test "lists beacons without logging in", %{conn: conn} do
|
test "lists approved beacons without logging in", %{conn: conn} do
|
||||||
beacon = beacon_fixture(user_fixture())
|
beacon = approved_beacon_fixture(user_fixture())
|
||||||
{:ok, _view, html} = live(conn, ~p"/beacons")
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
assert html =~ "Beacons"
|
assert html =~ "Beacons"
|
||||||
assert html =~ beacon.callsign
|
assert html =~ beacon.callsign
|
||||||
end
|
end
|
||||||
|
|
||||||
test "non-admin does not see the New Beacon button", %{conn: conn} do
|
test "hides unapproved beacons from the public list", %{conn: conn} do
|
||||||
|
_pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
|
refute html =~ "W5PEN"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "anonymous does not see the Submit Beacon button", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
|
refute html =~ "Submit Beacon"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "authenticated user sees the Submit Beacon button", %{conn: conn} do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
conn = log_in_user(conn, user)
|
conn = log_in_user(conn, user)
|
||||||
{:ok, _view, html} = live(conn, ~p"/beacons")
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
refute html =~ "New Beacon"
|
assert html =~ "Submit Beacon"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin sees the New Beacon button", %{conn: conn} do
|
test "admin sees pending beacons section", %{conn: conn} do
|
||||||
|
pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
||||||
conn = log_in_user(conn, admin_user_fixture())
|
conn = log_in_user(conn, admin_user_fixture())
|
||||||
{:ok, _view, html} = live(conn, ~p"/beacons")
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
assert html =~ "New Beacon"
|
assert html =~ "Pending approval"
|
||||||
|
assert html =~ pending.callsign
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin does not see pending beacons section", %{conn: conn} do
|
||||||
|
_pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
||||||
|
conn = log_in_user(conn, user_fixture())
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/beacons")
|
||||||
|
refute html =~ "Pending approval"
|
||||||
|
refute html =~ "W5PEN"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can approve a pending beacon", %{conn: conn} do
|
||||||
|
pending = beacon_fixture(user_fixture(), callsign: "W5PEN")
|
||||||
|
conn = log_in_user(conn, admin_user_fixture())
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/beacons")
|
||||||
|
|
||||||
|
html =
|
||||||
|
view
|
||||||
|
|> element("a[phx-click][data-confirm='Approve this beacon?']")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert html =~ "Approved #{pending.callsign}"
|
||||||
|
assert Microwaveprop.Beacons.get_beacon!(pending.id).approved == true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Show (public)" do
|
describe "Show (public)" do
|
||||||
test "renders beacon details without logging in", %{conn: conn} do
|
test "renders beacon details without logging in", %{conn: conn} do
|
||||||
beacon = beacon_fixture(user_fixture())
|
beacon = approved_beacon_fixture(user_fixture())
|
||||||
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
||||||
assert html =~ beacon.callsign
|
assert html =~ beacon.callsign
|
||||||
assert html =~ "#{beacon.frequency_mhz}"
|
assert html =~ "#{beacon.frequency_mhz}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "shows pending badge on unapproved beacons", %{conn: conn} do
|
||||||
|
beacon = beacon_fixture(user_fixture())
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/beacons/#{beacon}")
|
||||||
|
assert html =~ "Pending approval"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "Form (admin only)" do
|
describe "Form" do
|
||||||
test "redirects non-admin away from /beacons/new", %{conn: conn} do
|
test "authenticated non-admin can access /beacons/new", %{conn: conn} do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
conn = log_in_user(conn, user)
|
conn = log_in_user(conn, user)
|
||||||
|
{:ok, _view, html} = live(conn, ~p"/beacons/new")
|
||||||
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/beacons/new")
|
assert html =~ "New beacon"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects anonymous user away from /beacons/new", %{conn: conn} do
|
test "redirects anonymous user away from /beacons/new", %{conn: conn} do
|
||||||
assert {:error, {:redirect, %{to: "/users/log-in"}}} = live(conn, ~p"/beacons/new")
|
assert {:error, {:redirect, %{to: "/users/log-in"}}} = live(conn, ~p"/beacons/new")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin creates a beacon", %{conn: conn} do
|
test "non-admin submission creates an unapproved beacon", %{conn: conn} do
|
||||||
conn = log_in_user(conn, admin_user_fixture())
|
user = user_fixture()
|
||||||
|
conn = log_in_user(conn, user)
|
||||||
{:ok, form_live, _html} = live(conn, ~p"/beacons/new")
|
{:ok, form_live, _html} = live(conn, ~p"/beacons/new")
|
||||||
|
|
||||||
attrs = valid_beacon_attrs()
|
attrs = valid_beacon_attrs()
|
||||||
|
|
||||||
assert {:ok, _view, html} =
|
assert {:ok, _view, _html} =
|
||||||
form_live
|
form_live
|
||||||
|> form("#beacon-form", beacon: attrs)
|
|> form("#beacon-form", beacon: attrs)
|
||||||
|> render_submit()
|
|> render_submit()
|
||||||
|> follow_redirect(conn, ~p"/beacons")
|
|> follow_redirect(conn, ~p"/beacons")
|
||||||
|
|
||||||
assert html =~ "Beacon created"
|
beacon = List.first(Microwaveprop.Beacons.list_pending_beacons())
|
||||||
assert html =~ attrs.callsign
|
assert beacon.callsign == attrs.callsign
|
||||||
|
assert beacon.approved == false
|
||||||
end
|
end
|
||||||
|
|
||||||
test "admin edits a beacon", %{conn: conn} do
|
test "admin edits a beacon", %{conn: conn} do
|
||||||
|
|
@ -90,5 +139,11 @@ defmodule MicrowavepropWeb.BeaconLiveTest do
|
||||||
|
|
||||||
assert html =~ "Beacon updated"
|
assert html =~ "Beacon updated"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot access /beacons/:id/edit", %{conn: conn} do
|
||||||
|
beacon = beacon_fixture(user_fixture())
|
||||||
|
conn = log_in_user(conn, user_fixture())
|
||||||
|
assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/beacons/#{beacon}/edit")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue