Link submissions to the logged-in user on /submit
Contacts now carry a user_id FK to users. When a logged-in user submits a contact (single or CSV upload), the server injects their user_id and the email from their account into the submission params, and the visible email input is replaced with a hidden input holding that value — logged-in users don't have to retype their address. The submission changeset now requires at least one of user_id or submitter_email (rather than hard-requiring email), and email format is only validated when one is actually present. Reworded the submit-page blurb from "10 GHz to 241 GHz" to "902 MHz and up" so it matches the actual band coverage of the project.
This commit is contained in:
parent
8637253fda
commit
5724b33e8f
3 changed files with 106 additions and 30 deletions
|
|
@ -39,13 +39,14 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
|
|
||||||
field :user_submitted, :boolean, default: false
|
field :user_submitted, :boolean, default: false
|
||||||
field :submitter_email, :string
|
field :submitter_email, :string
|
||||||
|
field :user_id, :binary_id
|
||||||
field :flagged_invalid, :boolean, default: false
|
field :flagged_invalid, :boolean, default: false
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
|
||||||
@required_fields ~w(station1 station2 qso_timestamp mode band)a
|
@required_fields ~w(station1 station2 qso_timestamp mode band)a
|
||||||
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km)a
|
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km user_id)a
|
||||||
|
|
||||||
def changeset(contact, attrs) do
|
def changeset(contact, attrs) do
|
||||||
contact
|
contact
|
||||||
|
|
@ -53,14 +54,16 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
end
|
end
|
||||||
|
|
||||||
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email)a
|
@submission_fields ~w(station1 station2 qso_timestamp mode band grid1 grid2 submitter_email user_id)a
|
||||||
|
@submission_required ~w(station1 station2 qso_timestamp mode band grid1 grid2)a
|
||||||
@allowed_modes ~w(CW SSB FM FT8 FT4)
|
@allowed_modes ~w(CW SSB FM FT8 FT4)
|
||||||
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1)
|
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 68000 75000 122000 134000 241000), &Decimal.new/1)
|
||||||
|
|
||||||
def submission_changeset(contact, attrs) do
|
def submission_changeset(contact, attrs) do
|
||||||
contact
|
contact
|
||||||
|> cast(attrs, @submission_fields)
|
|> cast(attrs, @submission_fields)
|
||||||
|> validate_required(@submission_fields)
|
|> validate_required(@submission_required)
|
||||||
|
|> validate_user_or_email()
|
||||||
|> sanitize_callsign(:station1)
|
|> sanitize_callsign(:station1)
|
||||||
|> sanitize_callsign(:station2)
|
|> sanitize_callsign(:station2)
|
||||||
|> sanitize_grid(:grid1)
|
|> sanitize_grid(:grid1)
|
||||||
|
|
@ -71,12 +74,33 @@ defmodule Microwaveprop.Radio.Contact do
|
||||||
|> validate_grid_format(:grid2)
|
|> validate_grid_format(:grid2)
|
||||||
|> validate_inclusion(:mode, @allowed_modes)
|
|> validate_inclusion(:mode, @allowed_modes)
|
||||||
|> validate_inclusion(:band, @allowed_bands)
|
|> validate_inclusion(:band, @allowed_bands)
|
||||||
|> validate_format(:submitter_email, ~r/^[^\s<>]+@[^\s<>]+\.[^\s<>]+$/)
|
|> validate_email_format()
|
||||||
|> validate_length(:submitter_email, max: 254)
|
|> validate_length(:submitter_email, max: 254)
|
||||||
|> validate_length(:station1, max: 20)
|
|> validate_length(:station1, max: 20)
|
||||||
|> validate_length(:station2, max: 20)
|
|> validate_length(:station2, max: 20)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Either a user_id (logged-in submitter) or a submitter_email (anonymous)
|
||||||
|
# must be present so we know who submitted the contact.
|
||||||
|
defp validate_user_or_email(changeset) do
|
||||||
|
user_id = get_field(changeset, :user_id)
|
||||||
|
email = get_field(changeset, :submitter_email)
|
||||||
|
|
||||||
|
if user_id || (email && email != "") do
|
||||||
|
changeset
|
||||||
|
else
|
||||||
|
add_error(changeset, :submitter_email, "can't be blank")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_email_format(changeset) do
|
||||||
|
case get_field(changeset, :submitter_email) do
|
||||||
|
nil -> changeset
|
||||||
|
"" -> changeset
|
||||||
|
_email -> validate_format(changeset, :submitter_email, ~r/^[^\s<>]+@[^\s<>]+\.[^\s<>]+$/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Strip whitespace and upcase callsigns
|
# Strip whitespace and upcase callsigns
|
||||||
defp sanitize_callsign(changeset, field) do
|
defp sanitize_callsign(changeset, field) do
|
||||||
case get_change(changeset, field) do
|
case get_change(changeset, field) do
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
if recently_submitted?(socket) do
|
if recently_submitted?(socket) do
|
||||||
{:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
|
{:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
|
||||||
else
|
else
|
||||||
|
contact_params = merge_user_params(contact_params, socket)
|
||||||
|
|
||||||
case Radio.create_contact(contact_params) do
|
case Radio.create_contact(contact_params) do
|
||||||
{:ok, contact} ->
|
{:ok, contact} ->
|
||||||
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
|
||||||
|
|
@ -82,8 +84,14 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("upload_csv", %{"submitter_email" => email}, socket) do
|
def handle_event("upload_csv", params, socket) do
|
||||||
if String.trim(email) == "" do
|
email =
|
||||||
|
case current_user(socket.assigns) do
|
||||||
|
%{email: user_email} -> user_email
|
||||||
|
_ -> params |> Map.get("submitter_email", "") |> String.trim()
|
||||||
|
end
|
||||||
|
|
||||||
|
if email == "" do
|
||||||
{:noreply, put_flash(socket, :error, "Email is required for CSV upload")}
|
{:noreply, put_flash(socket, :error, "Email is required for CSV upload")}
|
||||||
else
|
else
|
||||||
uploaded_contents =
|
uploaded_contents =
|
||||||
|
|
@ -144,6 +152,18 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp merge_user_params(params, socket) do
|
||||||
|
case current_user(socket.assigns) do
|
||||||
|
%{id: user_id, email: user_email} ->
|
||||||
|
params
|
||||||
|
|> Map.put("user_id", user_id)
|
||||||
|
|> Map.put("submitter_email", user_email)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
|
@ -156,10 +176,10 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
<div class="bg-info/10 border border-info/30 rounded-box p-4 mb-6 text-sm leading-relaxed">
|
<div class="bg-info/10 border border-info/30 rounded-box p-4 mb-6 text-sm leading-relaxed">
|
||||||
<p class="font-semibold text-info mb-1">Every contact matters</p>
|
<p class="font-semibold text-info mb-1">Every contact matters</p>
|
||||||
<p>
|
<p>
|
||||||
Our propagation model improves with real-world data. Each verified microwave contact
|
Our propagation model improves with real-world data. Each verified contact
|
||||||
you submit is matched against atmospheric conditions at the time of your contact,
|
you submit is matched against atmospheric conditions at the time of your contact,
|
||||||
helping us calibrate predictions for all bands from 10 GHz to 241 GHz. The more contacts
|
helping us calibrate predictions for all amateur bands from 902 MHz and up. The
|
||||||
we have, the better our forecasts get for everyone.
|
more contacts we have, the better our forecasts get for everyone.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -199,7 +219,12 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= if @active_tab == :single do %>
|
<%= if @active_tab == :single do %>
|
||||||
<.single_contact_form form={@form} band_options={@band_options} mode_options={@mode_options} />
|
<.single_contact_form
|
||||||
|
form={@form}
|
||||||
|
band_options={@band_options}
|
||||||
|
mode_options={@mode_options}
|
||||||
|
current_user={current_user(assigns)}
|
||||||
|
/>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= cond do %>
|
<%= cond do %>
|
||||||
<% @csv_result -> %>
|
<% @csv_result -> %>
|
||||||
|
|
@ -207,13 +232,17 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
<% @csv_preview -> %>
|
<% @csv_preview -> %>
|
||||||
<.csv_preview preview={@csv_preview} />
|
<.csv_preview preview={@csv_preview} />
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<.csv_upload_form uploads={@uploads} />
|
<.csv_upload_form uploads={@uploads} current_user={current_user(assigns)} />
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</Layouts.app>
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# In-template variant — takes an assigns map directly (not a socket).
|
||||||
|
defp current_user(%{current_scope: %{user: %{} = user}}), do: user
|
||||||
|
defp current_user(_), do: nil
|
||||||
|
|
||||||
defp single_contact_form(assigns) do
|
defp single_contact_form(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
|
<.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
|
||||||
|
|
@ -262,13 +291,21 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
<.input field={@form[:qso_timestamp]} type="datetime-local" label="Timestamp (UTC)" required />
|
<.input field={@form[:qso_timestamp]} type="datetime-local" label="Timestamp (UTC)" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.input
|
<%= if @current_user do %>
|
||||||
field={@form[:submitter_email]}
|
<input
|
||||||
type="email"
|
type="hidden"
|
||||||
label="Your Email"
|
name="contact[submitter_email]"
|
||||||
placeholder="you@example.com"
|
value={@current_user.email}
|
||||||
required
|
/>
|
||||||
/>
|
<% else %>
|
||||||
|
<.input
|
||||||
|
field={@form[:submitter_email]}
|
||||||
|
type="email"
|
||||||
|
label="Your Email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<.button phx-disable-with="Submitting..." class="btn btn-primary btn-lg w-full sm:w-auto">
|
<.button phx-disable-with="Submitting..." class="btn btn-primary btn-lg w-full sm:w-auto">
|
||||||
|
|
@ -311,18 +348,22 @@ defmodule MicrowavepropWeb.SubmitLive do
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="fieldset mb-2">
|
<%= if @current_user do %>
|
||||||
<label>
|
<input type="hidden" name="submitter_email" value={@current_user.email} />
|
||||||
<span class="label mb-1">Your Email</span>
|
<% else %>
|
||||||
<input
|
<div class="fieldset mb-2">
|
||||||
type="email"
|
<label>
|
||||||
name="submitter_email"
|
<span class="label mb-1">Your Email</span>
|
||||||
class="w-full input"
|
<input
|
||||||
placeholder="you@example.com"
|
type="email"
|
||||||
value=""
|
name="submitter_email"
|
||||||
/>
|
class="w-full input"
|
||||||
</label>
|
placeholder="you@example.com"
|
||||||
</div>
|
value=""
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-full sm:w-auto">
|
<button type="submit" class="btn btn-primary btn-lg w-full sm:w-auto">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule Microwaveprop.Repo.Migrations.AddUserIdToContacts do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:contacts) do
|
||||||
|
add :user_id, references(:users, type: :binary_id, on_delete: :nilify_all)
|
||||||
|
end
|
||||||
|
|
||||||
|
create index(:contacts, [:user_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue