feat: add join form settings
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Simon 2026-03-10 14:29:49 +01:00
parent b7a83d9298
commit fa738aae88
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
12 changed files with 846 additions and 54 deletions

View file

@ -89,7 +89,8 @@ lib/
│ ├── join_request/ # JoinRequest changes (SetConfirmationToken, ConfirmRequest) │ ├── join_request/ # JoinRequest changes (SetConfirmationToken, ConfirmRequest)
│ ├── custom_field.ex # Custom field (definition) resource │ ├── custom_field.ex # Custom field (definition) resource
│ ├── custom_field_value.ex # Custom field value resource │ ├── custom_field_value.ex # Custom field value resource
│ ├── setting.ex # Global settings (singleton resource) │ ├── setting.ex # Global settings (singleton resource; incl. join form config)
│ ├── setting/ # Setting changes (NormalizeJoinFormSettings, etc.)
│ ├── group.ex # Group resource │ ├── group.ex # Group resource
│ ├── member_group.ex # MemberGroup join table resource │ ├── member_group.ex # MemberGroup join table resource
│ └── email.ex # Email custom type │ └── email.ex # Email custom type

View file

@ -809,8 +809,13 @@ end
- **PR review follow-ups (Join confirmation):** Join confirmation email uses `Mailer.deliver/1` and returns `{:ok, email}` \| `{:error, reason}`; domain logs delivery errors but still returns `{:ok, request}` so the user sees success. Comment in `submit_join_request/2` clarifies that the raw token is hashed by `JoinRequest.Changes.SetConfirmationToken`. Cleanup task uses `Ash.bulk_destroy` and logs partial errors without halting. Layout uses assigns `app_name` and `locale` (from config/Gettext) instead of hardcoded "Mila" and `lang="de"`. Production `runtime.exs` sets `:mail_from` from ENV (`MAIL_FROM_NAME`, `MAIL_FROM_EMAIL`). Layout reference unified to `"layout.html"`; redundant `put_layout` removed from senders. - **PR review follow-ups (Join confirmation):** Join confirmation email uses `Mailer.deliver/1` and returns `{:ok, email}` \| `{:error, reason}`; domain logs delivery errors but still returns `{:ok, request}` so the user sees success. Comment in `submit_join_request/2` clarifies that the raw token is hashed by `JoinRequest.Changes.SetConfirmationToken`. Cleanup task uses `Ash.bulk_destroy` and logs partial errors without halting. Layout uses assigns `app_name` and `locale` (from config/Gettext) instead of hardcoded "Mila" and `lang="de"`. Production `runtime.exs` sets `:mail_from` from ENV (`MAIL_FROM_NAME`, `MAIL_FROM_EMAIL`). Layout reference unified to `"layout.html"`; redundant `put_layout` removed from senders.
- Tests: `join_request_test.exs`, `join_request_submit_email_test.exs`, `join_confirm_controller_test.exs` all pass. - Tests: `join_request_test.exs`, `join_request_submit_email_test.exs`, `join_confirm_controller_test.exs` all pass.
**Subtask 3 Admin: Join form settings (TDD tests only):** **Subtask 3 Admin: Join form settings (done):**
- Test file: `test/membership/setting_join_form_test.exs` TDD tests for join form settings (persistence, validation, allowlist, defaults, robustness). Tests are red until Setting gains `join_form_enabled`, `join_form_field_ids`, `join_form_field_required` and `Mv.Membership.get_join_form_allowlist/0` is implemented. No functionality implemented yet. - **Setting resource** (`lib/membership/setting.ex`): 3 new attributes `join_form_enabled` (boolean, default false), `join_form_field_ids` ({:array, :string} ordered list of member field names or custom field UUIDs), `join_form_field_required` (:map field ID → boolean). Added to `:create` and `:update` accept lists. Validation rejects field IDs that are neither valid member field names nor UUID format. Migration: `20260310114701_add_join_form_settings_to_settings.exs`.
- **NormalizeJoinFormSettings** (`lib/membership/setting/changes/normalize_join_form_settings.ex`): Change applied on create/update whenever join form attrs are changing. Ensures email is always in `join_form_field_ids`, forces `join_form_field_required["email"] = true`, drops required flags for fields not in `join_form_field_ids`.
- **Domain** (`lib/membership/membership.ex`): `Mv.Membership.get_join_form_allowlist/0` returns `[]` when join form is disabled, otherwise a list of `%{id, required, type}` maps (type = `:member_field` or `:custom_field` based on ID format).
- **GlobalSettingsLive** (`lib/mv_web/live/global_settings_live.ex`): New "Join Form" / "Beitrittsformular" section between Club Settings and Vereinfacht. Checkbox to enable/disable, table of selected fields (with Required checkbox per field email always checked/disabled, other fields can be toggled), "Add field" dropdown with all unselected member fields and custom fields, "Save Join Form Settings" button. State is managed locally (not via AshPhoenix.Form); saved on explicit save click.
- **Translations**: 14 new German strings in `priv/gettext/de/LC_MESSAGES/default.po` (Beitrittsformular, Felder im Beitrittsformular, Feld hinzufügen, etc.).
- Tests: All 13 tests in `test/membership/setting_join_form_test.exs` pass; full test suite 1900 tests, 0 failures.
### Test Data Management ### Test Data Management

View file

@ -455,6 +455,56 @@ defmodule Mv.Membership do
end end
end end
@doc """
Returns the allowlist of fields configured for the public join form.
Reads the current settings. When the join form is disabled (or no settings exist),
returns an empty list. When enabled, returns each configured field as a map with:
- `:id` - field identifier string (member field name or custom field UUID)
- `:required` - boolean; email is always true
- `:type` - `:member_field` or `:custom_field`
This is the server-side allowlist used by the join form submit action (Subtask 4)
to enforce which fields are accepted from user input.
## Returns
- `[%{id: String.t(), required: boolean(), type: :member_field | :custom_field}]`
- `[]` when join form is disabled or settings are missing
## Examples
iex> Mv.Membership.get_join_form_allowlist()
[%{id: "email", required: true, type: :member_field},
%{id: "first_name", required: false, type: :member_field}]
"""
def get_join_form_allowlist do
case get_settings() do
{:ok, settings} ->
if settings.join_form_enabled do
build_join_form_allowlist(settings)
else
[]
end
{:error, _} ->
[]
end
end
defp build_join_form_allowlist(settings) do
field_ids = settings.join_form_field_ids || []
required_config = settings.join_form_field_required || %{}
member_field_names = Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
Enum.map(field_ids, fn id ->
type = if id in member_field_names, do: :member_field, else: :custom_field
required = Map.get(required_config, id, false)
%{id: id, required: required, type: type}
end)
end
defp expired?(nil), do: true defp expired?(nil), do: true
defp expired?(expires_at), do: DateTime.compare(expires_at, DateTime.utc_now()) == :lt defp expired?(expires_at), do: DateTime.compare(expires_at, DateTime.utc_now()) == :lt
end end

View file

@ -15,6 +15,12 @@ defmodule Mv.Membership.Setting do
(e.g., `%{"first_name" => true, "last_name" => true}`). Email is always required; other fields default to optional. (e.g., `%{"first_name" => true, "last_name" => true}`). Email is always required; other fields default to optional.
- `include_joining_cycle` - Whether to include the joining cycle in membership fee generation (default: true) - `include_joining_cycle` - Whether to include the joining cycle in membership fee generation (default: true)
- `default_membership_fee_type_id` - Default membership fee type for new members (optional) - `default_membership_fee_type_id` - Default membership fee type for new members (optional)
- `join_form_enabled` - Whether the public /join page is active (default: false)
- `join_form_field_ids` - Ordered list of field IDs shown on the join form. Each entry is
either a member field name string (e.g. "email") or a custom field UUID. Email is always
included and always required; normalization enforces this automatically.
- `join_form_field_required` - Map of field ID => required boolean for the join form.
Email is always forced to true.
## Singleton Pattern ## Singleton Pattern
This resource uses a singleton pattern - there should only be one settings record. This resource uses a singleton pattern - there should only be one settings record.
@ -86,8 +92,13 @@ defmodule Mv.Membership.Setting do
:oidc_client_secret, :oidc_client_secret,
:oidc_admin_group_name, :oidc_admin_group_name,
:oidc_groups_claim, :oidc_groups_claim,
:oidc_only :oidc_only,
:join_form_enabled,
:join_form_field_ids,
:join_form_field_required
] ]
change Mv.Membership.Setting.Changes.NormalizeJoinFormSettings
end end
update :update do update :update do
@ -110,8 +121,13 @@ defmodule Mv.Membership.Setting do
:oidc_client_secret, :oidc_client_secret,
:oidc_admin_group_name, :oidc_admin_group_name,
:oidc_groups_claim, :oidc_groups_claim,
:oidc_only :oidc_only,
:join_form_enabled,
:join_form_field_ids,
:join_form_field_required
] ]
change Mv.Membership.Setting.Changes.NormalizeJoinFormSettings
end end
update :update_member_field_visibility do update :update_member_field_visibility do
@ -232,6 +248,39 @@ defmodule Mv.Membership.Setting do
end, end,
on: [:create, :update] on: [:create, :update]
# Validate join_form_field_ids: each entry must be a known member field name
# or a UUID-format string (custom field ID). Normalization (NormalizeJoinFormSettings
# change) runs before validations, so email is already present when this runs.
validate fn changeset, _context ->
field_ids = Ash.Changeset.get_attribute(changeset, :join_form_field_ids)
if is_list(field_ids) and field_ids != [] do
valid_member_fields =
Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
uuid_pattern =
~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
invalid_ids =
Enum.reject(field_ids, fn id ->
is_binary(id) and
(id in valid_member_fields or Regex.match?(uuid_pattern, id))
end)
if Enum.empty?(invalid_ids) do
:ok
else
{:error,
field: :join_form_field_ids,
message:
"Invalid field identifiers: #{inspect(invalid_ids)}. Use member field names or custom field UUIDs."}
end
else
:ok
end
end,
on: [:create, :update]
# Validate default_membership_fee_type_id exists if set # Validate default_membership_fee_type_id exists if set
validate fn changeset, context -> validate fn changeset, context ->
fee_type_id = fee_type_id =
@ -382,6 +431,29 @@ defmodule Mv.Membership.Setting do
description "When true and OIDC is configured, sign-in shows only OIDC (password login hidden)" description "When true and OIDC is configured, sign-in shows only OIDC (password login hidden)"
end end
# Join form (Beitrittsformular) settings
attribute :join_form_enabled, :boolean do
allow_nil? false
default false
public? true
description "When true, the public /join page is active and new members can submit a request."
end
attribute :join_form_field_ids, {:array, :string} do
allow_nil? true
public? true
description "Ordered list of field IDs shown on the join form. Each entry is a member field name (e.g. 'email') or a custom field UUID. Email is always present after normalization."
end
attribute :join_form_field_required, :map do
allow_nil? true
public? true
description "Map of field ID => required boolean for the join form. Email is always true after normalization."
end
timestamps() timestamps()
end end

View file

@ -0,0 +1,60 @@
defmodule Mv.Membership.Setting.Changes.NormalizeJoinFormSettings do
@moduledoc """
Ash change that normalizes join form field settings before persist.
Applied on create and update actions whenever join form attributes are present.
Rules enforced:
- Email is always added to join_form_field_ids if not already present.
- Email is always marked as required (true) in join_form_field_required.
- Keys in join_form_field_required that are not in join_form_field_ids are dropped.
Only runs when join_form_field_ids is being changed; if only
join_form_field_required changes, normalization still uses the current
(possibly changed) field_ids to strip orphaned required flags.
"""
use Ash.Resource.Change
def change(changeset, _opts, _context) do
changing_ids? = Ash.Changeset.changing_attribute?(changeset, :join_form_field_ids)
changing_required? = Ash.Changeset.changing_attribute?(changeset, :join_form_field_required)
if changing_ids? or changing_required? do
normalize(changeset)
else
changeset
end
end
defp normalize(changeset) do
field_ids = Ash.Changeset.get_attribute(changeset, :join_form_field_ids)
required_config = Ash.Changeset.get_attribute(changeset, :join_form_field_required)
field_ids = normalize_field_ids(field_ids)
required_config = normalize_required(field_ids, required_config)
changeset
|> Ash.Changeset.force_change_attribute(:join_form_field_ids, field_ids)
|> Ash.Changeset.force_change_attribute(:join_form_field_required, required_config)
end
defp normalize_field_ids(nil), do: ["email"]
defp normalize_field_ids(ids) when is_list(ids) do
if "email" in ids do
ids
else
["email" | ids]
end
end
defp normalize_field_ids(_), do: ["email"]
defp normalize_required(field_ids, required_config) do
base = if is_map(required_config), do: required_config, else: %{}
base
|> Map.filter(fn {key, _} -> key in field_ids end)
|> Map.put("email", true)
end
end

View file

@ -990,7 +990,7 @@ defmodule MvWeb.CoreComponents do
/> />
</th> </th>
<th :if={@action != []} class={table_th_sticky_class(@sticky_header)}> <th :if={@action != []} class={table_th_sticky_class(@sticky_header)}>
<span class="sr-only">{gettext("Actions")}</span> {gettext("Actions")}
</th> </th>
</tr> </tr>
</thead> </thead>

View file

@ -4,16 +4,24 @@ defmodule MvWeb.GlobalSettingsLive do
## Features ## Features
- Edit the association/club name - Edit the association/club name
- Configure the public join form (Beitrittsformular)
- Manage custom fields - Manage custom fields
- Real-time form validation - Real-time form validation
- Success/error feedback - Success/error feedback
## Settings ## Settings
- `club_name` - The name of the association/club (required) - `club_name` - The name of the association/club (required)
- `join_form_enabled` - Whether the public /join page is active
- `join_form_field_ids` - Ordered list of field IDs shown on the join form
- `join_form_field_required` - Map of field ID => required boolean
## Events ## Events
- `validate` - Real-time form validation - `validate` / `save` - Club settings form
- `save` - Save settings changes - `toggle_join_form_enabled` - Enable/disable the join form
- `add_join_form_field` / `remove_join_form_field` - Manage join form fields
- `toggle_join_form_field_required` - Toggle required flag per field
- `toggle_add_field_dropdown` / `hide_add_field_dropdown` - Dropdown visibility
- Join form changes (enable/disable, add/remove fields, required toggles) are persisted immediately
## Note ## Note
Settings is a singleton resource - there is only one settings record. Settings is a singleton resource - there is only one settings record.
@ -31,6 +39,7 @@ defmodule MvWeb.GlobalSettingsLive do
alias Mv.Membership alias Mv.Membership
alias Mv.Membership.Member, as: MemberResource alias Mv.Membership.Member, as: MemberResource
alias MvWeb.Helpers.MemberHelpers alias MvWeb.Helpers.MemberHelpers
alias MvWeb.Translations.MemberFields
on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded} on_mount {MvWeb.LiveHelpers, :ensure_user_role_loaded}
@ -42,6 +51,9 @@ defmodule MvWeb.GlobalSettingsLive do
locale = session["locale"] || Application.get_env(:mv, :default_locale, "de") locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
Gettext.put_locale(MvWeb.Gettext, locale) Gettext.put_locale(MvWeb.Gettext, locale)
actor = MvWeb.LiveHelpers.current_actor(socket)
custom_fields = load_custom_fields(actor)
socket = socket =
socket socket
|> assign(:page_title, gettext("Settings")) |> assign(:page_title, gettext("Settings"))
@ -65,6 +77,7 @@ defmodule MvWeb.GlobalSettingsLive do
|> assign(:oidc_only_env_set, Mv.Config.oidc_only_env_set?()) |> assign(:oidc_only_env_set, Mv.Config.oidc_only_env_set?())
|> assign(:oidc_configured, Mv.Config.oidc_configured?()) |> assign(:oidc_configured, Mv.Config.oidc_configured?())
|> assign(:oidc_client_secret_set, present?(settings.oidc_client_secret)) |> assign(:oidc_client_secret_set, present?(settings.oidc_client_secret))
|> assign_join_form_state(settings, custom_fields)
|> assign_form() |> assign_form()
{:ok, socket} {:ok, socket}
@ -103,6 +116,144 @@ defmodule MvWeb.GlobalSettingsLive do
</.button> </.button>
</.form> </.form>
</.form_section> </.form_section>
<%!-- Join Form Section (Beitrittsformular) --%>
<.form_section title={gettext("Join Form")}>
<p class="text-sm text-base-content/70 mb-4">
{gettext("Configure the public join form that allows new members to submit a join request.")}
</p>
<%!-- Enable/disable --%>
<div class="flex items-center gap-3 mb-3">
<input
type="checkbox"
id="join-form-enabled-checkbox"
class="checkbox checkbox-sm"
checked={@join_form_enabled}
phx-click="toggle_join_form_enabled"
aria-label={gettext("Join form enabled")}
/>
<label for="join-form-enabled-checkbox" class="cursor-pointer font-medium">
{gettext("Join form enabled")}
</label>
</div>
<%!-- Board approval (future feature) --%>
<div class="flex items-center gap-3 mb-6">
<input
type="checkbox"
id="join-form-board-approval-checkbox"
class="checkbox checkbox-sm"
checked={false}
disabled
aria-label={gettext("Board approval required (in development)")}
/>
<label for="join-form-board-approval-checkbox" class="text-base-content/60 font-medium">
{gettext("Board approval required (in development)")}
</label>
</div>
<div :if={@join_form_enabled}>
<%!-- Field list header + Add button (left-aligned) --%>
<h3 class="font-medium mb-3">{gettext("Fields on the join form")}</h3>
<div class="relative mb-3 w-fit" phx-click-away="hide_add_field_dropdown">
<.button
type="button"
variant="primary"
phx-click="toggle_add_field_dropdown"
disabled={Enum.empty?(@available_join_form_member_fields) and Enum.empty?(@available_join_form_custom_fields)}
aria-haspopup="listbox"
aria-expanded={to_string(@show_add_field_dropdown)}
>
<.icon name="hero-plus" class="size-4" />
{gettext("Add field")}
</.button>
<%!-- Available fields dropdown (sections: Personal data, Custom fields) --%>
<div
:if={@show_add_field_dropdown}
class="absolute left-0 mt-1 w-56 bg-base-100 border border-base-300 rounded-lg shadow-lg z-10 max-h-64 overflow-y-auto"
role="listbox"
aria-label={gettext("Available fields")}
>
<div :if={not Enum.empty?(@available_join_form_member_fields)} class="pt-2">
<div class="px-4 py-1 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
{gettext("Personal data")}
</div>
<div
:for={field <- @available_join_form_member_fields}
role="option"
tabindex="0"
class="px-4 py-2 cursor-pointer hover:bg-base-200 text-sm"
phx-click="add_join_form_field"
phx-value-field_id={field.id}
>
{field.label}
</div>
</div>
<div :if={not Enum.empty?(@available_join_form_custom_fields)} class={if(Enum.empty?(@available_join_form_member_fields), do: "pt-2", else: "border-t border-base-300")}>
<div class="px-4 py-1 text-xs font-semibold text-base-content/60 uppercase tracking-wide">
{gettext("Individual fields")}
</div>
<div
:for={field <- @available_join_form_custom_fields}
role="option"
tabindex="0"
class="px-4 py-2 cursor-pointer hover:bg-base-200 text-sm last:pb-2"
phx-click="add_join_form_field"
phx-value-field_id={field.id}
>
{field.label}
</div>
</div>
</div>
</div>
<%!-- Empty state --%>
<p :if={Enum.empty?(@join_form_fields)} class="text-sm text-base-content/60 italic mb-4">
{gettext("No fields selected. Add at least the email field.")}
</p>
<%!-- Fields table (compact width) --%>
<div :if={not Enum.empty?(@join_form_fields)} class="mb-4 max-w-2xl">
<.table
id="join-form-fields-table"
rows={@join_form_fields}
row_id={fn field -> "join-field-#{field.id}" end}
>
<:col :let={field} label={gettext("Field")} class="min-w-[14rem]">
{field.label}
</:col>
<:col :let={field} label={gettext("Required")} class="w-24 max-w-[9.375rem] text-center">
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={field.required}
disabled={not field.can_toggle_required}
phx-click={if field.can_toggle_required, do: "toggle_join_form_field_required"}
phx-value-field_id={field.id}
aria-label={gettext("Required")}
/>
</:col>
<:action :let={field}>
<.tooltip content={gettext("Remove")} position="left">
<.button
type="button"
variant="danger"
size="sm"
disabled={not field.can_remove}
class={if(not field.can_remove, do: "opacity-50 cursor-not-allowed", else: "")}
phx-click="remove_join_form_field"
phx-value-field_id={field.id}
aria-label={gettext("Remove field %{label}", label: field.label)}
>
<.icon name="hero-trash" class="size-4" />
</.button>
</.tooltip>
</:action>
</.table>
</div>
</div>
</.form_section>
<%!-- Vereinfacht Integration Section --%> <%!-- Vereinfacht Integration Section --%>
<.form_section title={gettext("Vereinfacht Integration")}> <.form_section title={gettext("Vereinfacht Integration")}>
<%= if @vereinfacht_env_configured do %> <%= if @vereinfacht_env_configured do %>
@ -426,6 +577,126 @@ defmodule MvWeb.GlobalSettingsLive do
end end
end end
# ---- Join form event handlers ----
@impl true
def handle_event("toggle_join_form_enabled", _params, socket) do
socket = assign(socket, :join_form_enabled, not socket.assigns.join_form_enabled)
{:noreply, persist_join_form_settings(socket)}
end
@impl true
def handle_event("toggle_add_field_dropdown", _params, socket) do
{:noreply,
assign(socket, :show_add_field_dropdown, not socket.assigns.show_add_field_dropdown)}
end
@impl true
def handle_event("hide_add_field_dropdown", _params, socket) do
{:noreply, assign(socket, :show_add_field_dropdown, false)}
end
@impl true
def handle_event("add_join_form_field", %{"field_id" => field_id}, socket) do
member_avail = socket.assigns.available_join_form_member_fields
custom_avail = socket.assigns.available_join_form_custom_fields
current = socket.assigns.join_form_fields
field_to_add =
Enum.find(member_avail, &(&1.id == field_id)) ||
Enum.find(custom_avail, &(&1.id == field_id))
socket =
if field_to_add do
full_field = %{
id: field_to_add.id,
label: field_to_add.label,
type: field_to_add.type,
required: false,
can_remove: field_to_add.id != "email",
can_toggle_required: field_to_add.id != "email"
}
new_fields = current ++ [full_field]
new_member = Enum.reject(member_avail, &(&1.id == field_id))
new_custom = Enum.reject(custom_avail, &(&1.id == field_id))
socket
|> assign(:join_form_fields, new_fields)
|> assign(:available_join_form_member_fields, new_member)
|> assign(:available_join_form_custom_fields, new_custom)
|> assign(:show_add_field_dropdown, false)
else
socket
end
{:noreply, persist_join_form_settings(socket)}
end
@impl true
def handle_event("remove_join_form_field", %{"field_id" => field_id}, socket) do
if field_id == "email" do
{:noreply, socket}
else
current = socket.assigns.join_form_fields
custom_fields = socket.assigns.join_form_custom_fields
new_fields = Enum.reject(current, &(&1.id == field_id))
new_ids = Enum.map(new_fields, & &1.id)
%{member_fields: new_member, custom_fields: new_custom} =
build_available_join_form_fields(new_ids, custom_fields)
socket =
socket
|> assign(:join_form_fields, new_fields)
|> assign(:available_join_form_member_fields, new_member)
|> assign(:available_join_form_custom_fields, new_custom)
|> persist_join_form_settings()
{:noreply, socket}
end
end
@impl true
def handle_event("toggle_join_form_field_required", %{"field_id" => "email"}, socket) do
{:noreply, socket}
end
@impl true
def handle_event("toggle_join_form_field_required", %{"field_id" => field_id}, socket) do
new_fields =
Enum.map(socket.assigns.join_form_fields, &toggle_required_if_matches(&1, field_id))
socket = assign(socket, :join_form_fields, new_fields) |> persist_join_form_settings()
{:noreply, socket}
end
defp persist_join_form_settings(socket) do
settings = socket.assigns.settings
field_ids = Enum.map(socket.assigns.join_form_fields, & &1.id)
required_map =
socket.assigns.join_form_fields
|> Map.new(fn field -> {field.id, field.required} end)
attrs = %{
join_form_enabled: socket.assigns.join_form_enabled,
join_form_field_ids: field_ids,
join_form_field_required: required_map
}
case Membership.update_settings(settings, attrs) do
{:ok, updated_settings} ->
custom_fields = socket.assigns.join_form_custom_fields
socket
|> assign(:settings, updated_settings)
|> assign_join_form_state(updated_settings, custom_fields)
{:error, _error} ->
put_flash(socket, :error, gettext("Could not save join form settings."))
end
end
@vereinfacht_param_keys ~w[vereinfacht_api_url vereinfacht_api_key vereinfacht_club_id vereinfacht_app_url] @vereinfacht_param_keys ~w[vereinfacht_api_url vereinfacht_api_key vereinfacht_club_id vereinfacht_app_url]
defp vereinfacht_params?(params) when is_map(params) do defp vereinfacht_params?(params) when is_map(params) do
@ -709,4 +980,94 @@ defmodule MvWeb.GlobalSettingsLive do
</div> </div>
""" """
end end
# ---- Join form helper functions ----
defp assign_join_form_state(socket, settings, custom_fields) do
enabled = settings.join_form_enabled || false
raw_ids = settings.join_form_field_ids || []
field_ids = if "email" in raw_ids, do: raw_ids, else: ["email" | raw_ids]
required_config = settings.join_form_field_required || %{}
join_form_fields = build_join_form_fields(field_ids, required_config, custom_fields)
%{member_fields: member_avail, custom_fields: custom_avail} =
build_available_join_form_fields(field_ids, custom_fields)
socket
|> assign(:join_form_enabled, enabled)
|> assign(:join_form_fields, join_form_fields)
|> assign(:available_join_form_member_fields, member_avail)
|> assign(:available_join_form_custom_fields, custom_avail)
|> assign(:show_add_field_dropdown, false)
|> assign(:join_form_custom_fields, custom_fields)
end
defp build_join_form_fields(field_ids, required_config, custom_fields) do
Enum.map(field_ids, fn id ->
label = join_form_field_label(id, custom_fields)
required = if id == "email", do: true, else: Map.get(required_config, id, false)
type = if id in member_field_id_strings(), do: :member_field, else: :custom_field
%{
id: id,
label: label,
required: required,
can_remove: id != "email",
can_toggle_required: id != "email",
type: type
}
end)
end
defp build_available_join_form_fields(selected_ids, custom_fields) do
member_fields =
Mv.Constants.member_fields()
|> Enum.reject(fn field -> Atom.to_string(field) in selected_ids end)
|> Enum.map(fn field ->
%{id: Atom.to_string(field), label: MemberFields.label(field), type: :member_field}
end)
custom_field_entries =
custom_fields
|> Enum.reject(fn cf -> cf.id in selected_ids end)
|> Enum.map(fn cf ->
%{id: cf.id, label: cf.name, type: :custom_field}
end)
|> Enum.sort_by(& &1.label)
%{member_fields: member_fields, custom_fields: custom_field_entries}
end
defp join_form_field_label(id, custom_fields) do
if id in member_field_id_strings() do
MemberFields.label(String.to_existing_atom(id))
else
case Enum.find(custom_fields, &(&1.id == id)) do
nil -> id
cf -> cf.name
end
end
end
defp toggle_required_if_matches(%{id: id} = field, id),
do: Map.put(field, :required, not field.required)
defp toggle_required_if_matches(field, _field_id), do: field
defp member_field_id_strings do
Mv.Constants.member_fields() |> Enum.map(&Atom.to_string/1)
end
defp load_custom_fields(nil), do: []
defp load_custom_fields(actor) do
case Ash.read(Mv.Membership.CustomField,
actor: actor,
domain: Mv.Membership,
authorize?: true
) do
{:ok, fields} -> fields
{:error, _} -> []
end
end
end end

View file

@ -357,6 +357,7 @@ msgstr "Passwort-Authentifizierung"
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2149,6 +2150,7 @@ msgstr "Mitglied ist nicht in dieser Gruppe."
msgid "No email" msgid "No email"
msgstr "Keine E-Mail" msgstr "Keine E-Mail"
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Remove" msgid "Remove"
@ -3324,3 +3326,68 @@ msgstr "Unvollständig"
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "Diese Datenfelder sind für MILA notwendig, um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden." msgstr "Diese Datenfelder sind für MILA notwendig, um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Add field"
msgstr "Feld hinzufügen"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Available fields"
msgstr "Verfügbare Felder"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Configure the public join form that allows new members to submit a join request."
msgstr "Konfiguriere das öffentliche Beitrittsformular, über das neue Mitglieder einen Beitrittsantrag stellen können."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Could not save join form settings."
msgstr "Beitrittsformular-Einstellungen konnten nicht gespeichert werden."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Field"
msgstr "Feld"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Fields on the join form"
msgstr "Felder im Beitrittsformular"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join Form"
msgstr "Beitrittsformular"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join form enabled"
msgstr "Beitrittsformular aktiv"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "No fields selected. Add at least the email field."
msgstr "Keine Felder ausgewählt. Füge mindestens das E-Mail-Feld hinzu."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Remove field %{label}"
msgstr "Feld %{label} entfernen"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Board approval required (in development)"
msgstr "Bestätigung durch Vorstand erforderlich (in Entwicklung)"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Personal data"
msgstr "Persönliche Daten"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Individual fields"
msgstr "Individuelle Felder"

View file

@ -358,6 +358,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2150,6 +2151,7 @@ msgstr ""
msgid "No email" msgid "No email"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Remove" msgid "Remove"
@ -3324,3 +3326,58 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Add field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Available fields"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Configure the public join form that allows new members to submit a join request."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Could not save join form settings."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Fields on the join form"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join Form"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join form enabled"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "No fields selected. Add at least the email field."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Remove field %{label}"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Board approval required (in development)"
msgstr ""

View file

@ -358,6 +358,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form_component.ex #: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex #: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/member_field_live/form_component.ex #: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex #: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -2150,6 +2151,7 @@ msgstr ""
msgid "No email" msgid "No email"
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/group_live/show.ex #: lib/mv_web/live/group_live/show.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Remove" msgid "Remove"
@ -3324,3 +3326,68 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview." msgid "These fields are necessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "" msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Add field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Available fields"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Configure the public join form that allows new members to submit a join request."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Could not save join form settings."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Field"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Fields on the join form"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join Form"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Join form enabled"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "No fields selected. Add at least the email field."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Remove field %{label}"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Board approval required (in development)"
msgstr "Board approval required (in development)"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Personal data"
msgstr "Personal data"
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Individual fields"
msgstr "Individual fields"

View file

@ -0,0 +1,27 @@
defmodule Mv.Repo.Migrations.AddJoinFormSettingsToSettings do
@moduledoc """
Adds join form configuration columns to the settings table.
- join_form_enabled: whether the public /join page is active
- join_form_field_ids: ordered list of field IDs shown on the join form (JSONB array)
- join_form_field_required: map of field ID => required boolean (JSONB)
"""
use Ecto.Migration
def up do
alter table(:settings) do
add :join_form_enabled, :boolean, default: false, null: false
add :join_form_field_ids, {:array, :string}
add :join_form_field_required, :map
end
end
def down do
alter table(:settings) do
remove :join_form_enabled
remove :join_form_field_ids
remove :join_form_field_required
end
end
end

View file

@ -26,8 +26,17 @@ defmodule Mv.Membership.SettingJoinFormTest do
on_exit(fn -> on_exit(fn ->
{:ok, s} = Membership.get_settings() {:ok, s} = Membership.get_settings()
attrs = %{} attrs = %{}
attrs = if saved_enabled != nil, do: Map.put(attrs, :join_form_enabled, saved_enabled), else: attrs
attrs = if saved_ids != nil, do: Map.put(attrs, :join_form_field_ids, saved_ids || []), else: attrs attrs =
if saved_enabled != nil,
do: Map.put(attrs, :join_form_enabled, saved_enabled),
else: attrs
attrs =
if saved_ids != nil,
do: Map.put(attrs, :join_form_field_ids, saved_ids || []),
else: attrs
attrs = attrs =
if saved_required != nil, if saved_required != nil,
do: Map.put(attrs, :join_form_field_required, saved_required || %{}), do: Map.put(attrs, :join_form_field_required, saved_required || %{}),
@ -57,6 +66,7 @@ defmodule Mv.Membership.SettingJoinFormTest do
describe "join form settings persistence and loading" do describe "join form settings persistence and loading" do
test "save and load join_form_enabled plus field selection and required flags returns same config" do test "save and load join_form_enabled plus field selection and required flags returns same config" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
attrs = %{ attrs = %{
join_form_enabled: true, join_form_enabled: true,
join_form_field_ids: ["email", "first_name"], join_form_field_ids: ["email", "first_name"],
@ -78,17 +88,20 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "repeated save with changed field list overwrites config without leftovers" do test "repeated save with changed field list overwrites config without leftovers" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
assert {:ok, _} = update_join_form_settings(settings, %{
join_form_enabled: true,
join_form_field_ids: ["email", "first_name"],
join_form_field_required: %{"email" => true, "first_name" => false}
})
assert {:ok, updated} = update_join_form_settings(settings, %{ assert {:ok, _} =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: ["email", "last_name"], join_form_enabled: true,
join_form_field_required: %{"email" => true, "last_name" => false} join_form_field_ids: ["email", "first_name"],
}) join_form_field_required: %{"email" => true, "first_name" => false}
})
assert {:ok, updated} =
update_join_form_settings(settings, %{
join_form_enabled: true,
join_form_field_ids: ["email", "last_name"],
join_form_field_required: %{"email" => true, "last_name" => false}
})
assert updated.join_form_field_ids == ["email", "last_name"] assert updated.join_form_field_ids == ["email", "last_name"]
assert Map.has_key?(updated.join_form_field_required, "last_name") assert Map.has_key?(updated.join_form_field_required, "last_name")
@ -102,11 +115,12 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "only existing member fields or custom field ids are accepted; unknown field names rejected or sanitized" do test "only existing member fields or custom field ids are accepted; unknown field names rejected or sanitized" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
result = update_join_form_settings(settings, %{ result =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: ["email", "not_a_member_field"], join_form_enabled: true,
join_form_field_required: %{"email" => true, "not_a_member_field" => false} join_form_field_ids: ["email", "not_a_member_field"],
}) join_form_field_required: %{"email" => true, "not_a_member_field" => false}
})
# Until attributes exist we get NoSuchInput; once implemented we expect validation error # Until attributes exist we get NoSuchInput; once implemented we expect validation error
assert {:error, _} = result assert {:error, _} = result
@ -115,16 +129,18 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "config without email is rejected or email is auto-added and required" do test "config without email is rejected or email is auto-added and required" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
result = update_join_form_settings(settings, %{ result =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: ["first_name", "last_name"], join_form_enabled: true,
join_form_field_required: %{"first_name" => true, "last_name" => false} join_form_field_ids: ["first_name", "last_name"],
}) join_form_field_required: %{"first_name" => true, "last_name" => false}
})
# Either rejected or, when loaded, email must be present and required # Either rejected or, when loaded, email must be present and required
case result do case result do
{:error, _} -> {:error, _} ->
:ok :ok
{:ok, updated} -> {:ok, updated} ->
assert "email" in updated.join_form_field_ids assert "email" in updated.join_form_field_ids
assert updated.join_form_field_required["email"] == true assert updated.join_form_field_required["email"] == true
@ -134,11 +150,12 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "required false for email is ignored or forced to true when saved" do test "required false for email is ignored or forced to true when saved" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
{:ok, updated} = update_join_form_settings(settings, %{ {:ok, updated} =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: ["email", "first_name"], join_form_enabled: true,
join_form_field_required: %{"email" => false, "first_name" => false} join_form_field_ids: ["email", "first_name"],
}) join_form_field_required: %{"email" => false, "first_name" => false}
})
assert updated.join_form_field_required["email"] == true assert updated.join_form_field_required["email"] == true
end end
@ -146,15 +163,17 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "required flag for field not in join_form_field_ids is rejected or dropped" do test "required flag for field not in join_form_field_ids is rejected or dropped" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
result = update_join_form_settings(settings, %{ result =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: ["email"], join_form_enabled: true,
join_form_field_required: %{"email" => true, "first_name" => true} join_form_field_ids: ["email"],
}) join_form_field_required: %{"email" => true, "first_name" => true}
})
case result do case result do
{:error, _} -> {:error, _} ->
:ok :ok
{:ok, updated} -> {:ok, updated} ->
refute Map.has_key?(updated.join_form_field_required, "first_name") refute Map.has_key?(updated.join_form_field_required, "first_name")
end end
@ -166,6 +185,7 @@ defmodule Mv.Membership.SettingJoinFormTest do
describe "join form allowlist" do describe "join form allowlist" do
test "allowlist returns configured fields with required/optional when join form enabled" do test "allowlist returns configured fields with required/optional when join form enabled" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
update_join_form_settings(settings, %{ update_join_form_settings(settings, %{
join_form_enabled: true, join_form_enabled: true,
join_form_field_ids: ["email", "first_name"], join_form_field_ids: ["email", "first_name"],
@ -175,8 +195,8 @@ defmodule Mv.Membership.SettingJoinFormTest do
allowlist = Membership.get_join_form_allowlist() allowlist = Membership.get_join_form_allowlist()
assert length(allowlist) == 2 assert length(allowlist) == 2
email_entry = Enum.find(allowlist, &( &1.id == "email" )) email_entry = Enum.find(allowlist, &(&1.id == "email"))
first_name_entry = Enum.find(allowlist, &( &1.id == "first_name" )) first_name_entry = Enum.find(allowlist, &(&1.id == "first_name"))
assert email_entry.required == true assert email_entry.required == true
assert first_name_entry.required == false assert first_name_entry.required == false
assert email_entry.type == :member_field assert email_entry.type == :member_field
@ -185,6 +205,7 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "allowlist returns empty or defined default when join form disabled" do test "allowlist returns empty or defined default when join form disabled" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
update_join_form_settings(settings, %{ update_join_form_settings(settings, %{
join_form_enabled: false, join_form_enabled: false,
join_form_field_ids: ["email", "first_name"], join_form_field_ids: ["email", "first_name"],
@ -200,11 +221,13 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "allowlist distinguishes member fields and custom field identifiers" do test "allowlist distinguishes member fields and custom field identifiers" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
actor = SystemActor.get_system_actor() actor = SystemActor.get_system_actor()
{:ok, cf} = {:ok, cf} =
Membership.create_custom_field( Membership.create_custom_field(
%{name: "join_cf_#{System.unique_integer([:positive])}", value_type: :string}, %{name: "join_cf_#{System.unique_integer([:positive])}", value_type: :string},
actor: actor actor: actor
) )
update_join_form_settings(settings, %{ update_join_form_settings(settings, %{
join_form_enabled: true, join_form_enabled: true,
join_form_field_ids: ["email", cf.id], join_form_field_ids: ["email", cf.id],
@ -213,8 +236,8 @@ defmodule Mv.Membership.SettingJoinFormTest do
allowlist = Membership.get_join_form_allowlist() allowlist = Membership.get_join_form_allowlist()
email_entry = Enum.find(allowlist, &( &1.id == "email" )) email_entry = Enum.find(allowlist, &(&1.id == "email"))
cf_entry = Enum.find(allowlist, &( &1.id == cf.id )) cf_entry = Enum.find(allowlist, &(&1.id == cf.id))
assert email_entry.type == :member_field assert email_entry.type == :member_field
assert cf_entry.type == :custom_field assert cf_entry.type == :custom_field
end end
@ -252,11 +275,12 @@ defmodule Mv.Membership.SettingJoinFormTest do
test "invalid or unexpected payload structure yields clean error or ignores unknown keys" do test "invalid or unexpected payload structure yields clean error or ignores unknown keys" do
{:ok, settings} = Membership.get_settings() {:ok, settings} = Membership.get_settings()
result = update_join_form_settings(settings, %{ result =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: "not_a_list", join_form_enabled: true,
join_form_field_required: %{} join_form_field_ids: "not_a_list",
}) join_form_field_required: %{}
})
assert match?({:error, _}, result) or assert match?({:error, _}, result) or
(match?({:ok, _}, result) && elem(result, 1).join_form_field_ids != "not_a_list") (match?({:ok, _}, result) && elem(result, 1).join_form_field_ids != "not_a_list")
@ -267,11 +291,12 @@ defmodule Mv.Membership.SettingJoinFormTest do
all_member = Constants.member_fields() |> Enum.map(&to_string/1) all_member = Constants.member_fields() |> Enum.map(&to_string/1)
required_map = Map.new(all_member, fn f -> {f, f == "email"} end) required_map = Map.new(all_member, fn f -> {f, f == "email"} end)
assert {:ok, updated} = update_join_form_settings(settings, %{ assert {:ok, updated} =
join_form_enabled: true, update_join_form_settings(settings, %{
join_form_field_ids: all_member, join_form_enabled: true,
join_form_field_required: required_map join_form_field_ids: all_member,
}) join_form_field_required: required_map
})
assert length(updated.join_form_field_ids) == length(all_member) assert length(updated.join_form_field_ids) == length(all_member)
{:ok, reloaded} = Membership.get_settings() {:ok, reloaded} = Membership.get_settings()