feat: Improve handling of association name config
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
605a897045
commit
2443bc62ac
9 changed files with 179 additions and 4 deletions
|
|
@ -178,6 +178,18 @@ defmodule Mv.Membership do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Invalidates the global settings cache.
|
||||||
|
|
||||||
|
This should be used by callers that update settings through paths outside of
|
||||||
|
`update_settings/2` (for example, custom form submit flows) to keep reads via
|
||||||
|
`get_settings/0` consistent across views.
|
||||||
|
"""
|
||||||
|
@spec invalidate_settings_cache() :: :ok
|
||||||
|
def invalidate_settings_cache do
|
||||||
|
SettingsCache.invalidate()
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Lists only required custom fields.
|
Lists only required custom fields.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,27 @@ defmodule Mv.Config do
|
||||||
|> parse_and_validate_integer(default)
|
|> parse_and_validate_integer(default)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Association name
|
||||||
|
# ENV variable takes priority; fallback to Settings from database.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the association name.
|
||||||
|
|
||||||
|
Reads from `ASSOCIATION_NAME` env first, then from Settings.
|
||||||
|
"""
|
||||||
|
@spec association_name() :: String.t() | nil
|
||||||
|
def association_name do
|
||||||
|
env_or_setting("ASSOCIATION_NAME", :club_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns true if ASSOCIATION_NAME is set (field is read-only in Settings).
|
||||||
|
"""
|
||||||
|
@spec association_name_env_set?() :: boolean()
|
||||||
|
def association_name_env_set?, do: env_set?("ASSOCIATION_NAME")
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Vereinfacht accounting software integration
|
# Vereinfacht accounting software integration
|
||||||
# ENV variables take priority; fallback to Settings from database.
|
# ENV variables take priority; fallback to Settings from database.
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,11 @@ defmodule MvWeb.Layouts do
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
def app(assigns) do
|
def app(assigns) do
|
||||||
# Single get_settings() for layout; derive club_name and join_form_enabled to avoid duplicate query.
|
# Single settings read for layout defaults.
|
||||||
%{club_name: club_name, join_form_enabled: join_form_enabled} = get_layout_settings()
|
# Use an explicitly provided club_name as source of truth to avoid stale
|
||||||
|
# values from cache reads immediately after a settings update in LiveViews.
|
||||||
|
%{club_name: fallback_club_name, join_form_enabled: join_form_enabled} = get_layout_settings()
|
||||||
|
club_name = assigns[:club_name] || fallback_club_name
|
||||||
|
|
||||||
# NOTE: Unprocessed count runs on every page load when join form is enabled; consider
|
# NOTE: Unprocessed count runs on every page load when join form is enabled; consider
|
||||||
# loading only on navigation or caching briefly if performance becomes an issue.
|
# loading only on navigation or caching briefly if performance becomes an issue.
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
|> assign(:settings, settings)
|
|> assign(:settings, settings)
|
||||||
|> assign(:locale, locale)
|
|> assign(:locale, locale)
|
||||||
|> assign(:environment, environment)
|
|> assign(:environment, environment)
|
||||||
|
|> assign(:association_name_env_set, Mv.Config.association_name_env_set?())
|
||||||
|> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?())
|
|> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?())
|
||||||
|> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?())
|
|> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?())
|
||||||
|> assign(:vereinfacht_api_key_env_set, Mv.Config.vereinfacht_api_key_env_set?())
|
|> assign(:vereinfacht_api_key_env_set, Mv.Config.vereinfacht_api_key_env_set?())
|
||||||
|
|
@ -125,6 +126,13 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
<div class="mt-6 space-y-6 max-w-4xl px-4">
|
<div class="mt-6 space-y-6 max-w-4xl px-4">
|
||||||
<%!-- Club Settings Section --%>
|
<%!-- Club Settings Section --%>
|
||||||
<.form_section title={gettext("Club Settings")}>
|
<.form_section title={gettext("Club Settings")}>
|
||||||
|
<%= if @association_name_env_set do %>
|
||||||
|
<p class="text-sm text-base-content/70 mb-4">
|
||||||
|
{gettext(
|
||||||
|
"Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
|
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<.input
|
<.input
|
||||||
|
|
@ -132,10 +140,18 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
type="text"
|
type="text"
|
||||||
label={gettext("Association Name")}
|
label={gettext("Association Name")}
|
||||||
required
|
required
|
||||||
|
disabled={@association_name_env_set}
|
||||||
|
placeholder={
|
||||||
|
if(@association_name_env_set, do: gettext("From ASSOCIATION_NAME"), else: nil)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.button phx-disable-with={gettext("Saving...")} variant="primary">
|
<.button
|
||||||
|
:if={not @association_name_env_set}
|
||||||
|
phx-disable-with={gettext("Saving...")}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
{gettext("Save Name")}
|
{gettext("Save Name")}
|
||||||
</.button>
|
</.button>
|
||||||
</.form>
|
</.form>
|
||||||
|
|
@ -919,6 +935,7 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
# Never send blank API key / client secret / smtp password so we do not overwrite stored secrets
|
# Never send blank API key / client secret / smtp password so we do not overwrite stored secrets
|
||||||
setting_params_clean =
|
setting_params_clean =
|
||||||
setting_params
|
setting_params
|
||||||
|
|> drop_env_managed_association_name()
|
||||||
|> drop_blank_vereinfacht_api_key()
|
|> drop_blank_vereinfacht_api_key()
|
||||||
|> drop_blank_oidc_client_secret()
|
|> drop_blank_oidc_client_secret()
|
||||||
|> drop_blank_smtp_password()
|
|> drop_blank_smtp_password()
|
||||||
|
|
@ -927,6 +944,10 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
|
|
||||||
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
|
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
|
||||||
{:ok, updated_settings} ->
|
{:ok, updated_settings} ->
|
||||||
|
# Keep cross-view reads consistent after settings updates (layouts/sidebar
|
||||||
|
# read via Membership.get_settings/0).
|
||||||
|
Membership.invalidate_settings_cache()
|
||||||
|
|
||||||
# Use the returned record for the form so saved values show immediately;
|
# Use the returned record for the form so saved values show immediately;
|
||||||
# get_settings() can return cached data without the new attribute until reload.
|
# get_settings() can return cached data without the new attribute until reload.
|
||||||
test_result =
|
test_result =
|
||||||
|
|
@ -1195,10 +1216,19 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp drop_env_managed_association_name(params) when is_map(params) do
|
||||||
|
if Mv.Config.association_name_env_set?() do
|
||||||
|
Map.delete(params, "club_name")
|
||||||
|
else
|
||||||
|
params
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp assign_form(%{assigns: %{settings: settings}} = socket) do
|
defp assign_form(%{assigns: %{settings: settings}} = socket) do
|
||||||
# Show ENV values in disabled fields (Vereinfacht, OIDC, SMTP); never expose secrets in form
|
# Show ENV values in disabled fields (Association Name, Vereinfacht, OIDC, SMTP); never expose secrets in form
|
||||||
settings_display =
|
settings_display =
|
||||||
settings
|
settings
|
||||||
|
|> merge_association_env_values()
|
||||||
|> merge_vereinfacht_env_values()
|
|> merge_vereinfacht_env_values()
|
||||||
|> merge_oidc_env_values()
|
|> merge_oidc_env_values()
|
||||||
|> merge_smtp_env_values()
|
|> merge_smtp_env_values()
|
||||||
|
|
@ -1225,6 +1255,15 @@ defmodule MvWeb.GlobalSettingsLive do
|
||||||
defp put_if_env_set(map, _key, false, _value), do: map
|
defp put_if_env_set(map, _key, false, _value), do: map
|
||||||
defp put_if_env_set(map, key, true, value), do: Map.put(map, key, value)
|
defp put_if_env_set(map, key, true, value), do: Map.put(map, key, value)
|
||||||
|
|
||||||
|
defp merge_association_env_values(s) do
|
||||||
|
put_if_env_set(
|
||||||
|
s,
|
||||||
|
:club_name,
|
||||||
|
Mv.Config.association_name_env_set?(),
|
||||||
|
Mv.Config.association_name()
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
defp merge_vereinfacht_env_values(s) do
|
defp merge_vereinfacht_env_values(s) do
|
||||||
s
|
s
|
||||||
|> put_if_env_set(
|
|> put_if_env_set(
|
||||||
|
|
|
||||||
|
|
@ -3927,3 +3927,13 @@ msgstr "Die SMTP-Umgebungs-Konfiguration ist unvollständig. Fehlend: %{keys}"
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
||||||
msgstr "SMTP wird vollständig über Umgebungsvariablen verwaltet. Alle SMTP-Felder sind schreibgeschützt."
|
msgstr "SMTP wird vollständig über Umgebungsvariablen verwaltet. Alle SMTP-Felder sind schreibgeschützt."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
|
||||||
|
msgstr "Der Vereinsname wird über die Umgebungsvariable ASSOCIATION_NAME gesetzt. Dieses Feld ist schreibgeschützt."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "From ASSOCIATION_NAME"
|
||||||
|
msgstr "Aus ASSOCIATION_NAME"
|
||||||
|
|
|
||||||
|
|
@ -3927,3 +3927,13 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "From ASSOCIATION_NAME"
|
||||||
|
msgstr ""
|
||||||
|
|
|
||||||
|
|
@ -3927,3 +3927,13 @@ msgstr ""
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
|
||||||
|
msgstr "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
|
||||||
|
|
||||||
|
#: lib/mv_web/live/global_settings_live.ex
|
||||||
|
#, elixir-autogen, elixir-format
|
||||||
|
msgid "From ASSOCIATION_NAME"
|
||||||
|
msgstr "From ASSOCIATION_NAME"
|
||||||
|
|
|
||||||
26
test/mv_web/components/layouts_test.exs
Normal file
26
test/mv_web/components/layouts_test.exs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
defmodule MvWeb.LayoutsTest do
|
||||||
|
use MvWeb.ConnCase, async: false
|
||||||
|
|
||||||
|
import Phoenix.LiveViewTest
|
||||||
|
|
||||||
|
alias Mv.Membership
|
||||||
|
alias MvWeb.Layouts
|
||||||
|
|
||||||
|
describe "app/1" do
|
||||||
|
test "prefers provided club_name over settings fallback" do
|
||||||
|
{:ok, settings} = Membership.get_settings()
|
||||||
|
{:ok, _} = Membership.update_settings(settings, %{club_name: "Settings Club Name"})
|
||||||
|
|
||||||
|
html =
|
||||||
|
render_component(&Layouts.app/1, %{
|
||||||
|
flash: %{},
|
||||||
|
current_user: nil,
|
||||||
|
club_name: "Provided Club Name",
|
||||||
|
inner_block: [%{inner_block: fn _, _ -> "content" end}]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert html =~ "Provided Club Name"
|
||||||
|
refute html =~ "Settings Club Name"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -13,6 +13,7 @@ defmodule MvWeb.GlobalSettingsLiveTest do
|
||||||
"MAIL_FROM_NAME",
|
"MAIL_FROM_NAME",
|
||||||
"MAIL_FROM_EMAIL"
|
"MAIL_FROM_EMAIL"
|
||||||
]
|
]
|
||||||
|
@association_env_key "ASSOCIATION_NAME"
|
||||||
|
|
||||||
describe "Global Settings LiveView" do
|
describe "Global Settings LiveView" do
|
||||||
setup %{conn: conn} do
|
setup %{conn: conn} do
|
||||||
|
|
@ -51,6 +52,17 @@ defmodule MvWeb.GlobalSettingsLiveTest do
|
||||||
assert render(view) =~ "Updated Club Name"
|
assert render(view) =~ "Updated Club Name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "updated club name is shown after remount", %{conn: conn} do
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> form("#settings-form", %{setting: %{club_name: "Remount Club Name"}})
|
||||||
|
|> render_submit()
|
||||||
|
|
||||||
|
{:ok, _view_after_remount, html_after_remount} = live(conn, ~p"/settings")
|
||||||
|
assert html_after_remount =~ "Remount Club Name"
|
||||||
|
end
|
||||||
|
|
||||||
test "shows error when club_name is empty", %{conn: conn} do
|
test "shows error when club_name is empty", %{conn: conn} do
|
||||||
{:ok, view, _html} = live(conn, ~p"/settings")
|
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||||
|
|
||||||
|
|
@ -90,6 +102,34 @@ defmodule MvWeb.GlobalSettingsLiveTest do
|
||||||
"Open"
|
"Open"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "disables association name input when ASSOCIATION_NAME is set", %{conn: conn} do
|
||||||
|
clear_association_name_env()
|
||||||
|
System.put_env(@association_env_key, "Association Name from ENV")
|
||||||
|
on_exit(fn -> clear_association_name_env() end)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||||
|
|
||||||
|
assert has_element?(view, "#setting_club_name[disabled]")
|
||||||
|
assert has_element?(view, "#setting_club_name[placeholder='From ASSOCIATION_NAME']")
|
||||||
|
refute has_element?(view, "#settings-form button", "Save Name")
|
||||||
|
assert render(view) =~ "Association name is set via environment variable ASSOCIATION_NAME"
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :ui
|
||||||
|
test "keeps association name input editable when ASSOCIATION_NAME is not set", %{conn: conn} do
|
||||||
|
clear_association_name_env()
|
||||||
|
on_exit(fn -> clear_association_name_env() end)
|
||||||
|
|
||||||
|
{:ok, view, _html} = live(conn, ~p"/settings")
|
||||||
|
|
||||||
|
refute has_element?(view, "#setting_club_name[disabled]")
|
||||||
|
assert has_element?(view, "#settings-form button", "Save Name")
|
||||||
|
|
||||||
|
refute render(view) =~
|
||||||
|
"Association name is set via environment variable ASSOCIATION_NAME"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "SMTP / E-Mail section" do
|
describe "SMTP / E-Mail section" do
|
||||||
|
|
@ -270,4 +310,8 @@ defmodule MvWeb.GlobalSettingsLiveTest do
|
||||||
defp clear_smtp_env do
|
defp clear_smtp_env do
|
||||||
Enum.each(@smtp_env_keys, &System.delete_env/1)
|
Enum.each(@smtp_env_keys, &System.delete_env/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp clear_association_name_env do
|
||||||
|
System.delete_env(@association_env_key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue