Compare commits

...

6 commits

Author SHA1 Message Date
8e33eea9de chore: updated env example
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-27 15:56:20 +01:00
2027c993b4 test updated
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-27 15:49:29 +01:00
24b0faf95a adds translation 2025-11-27 15:37:42 +01:00
f8c2c7bbe3 feat: adds settings live view and updated seeds 2025-11-27 15:34:32 +01:00
936ed0ace1 chore: adds settings ressource and migration
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-27 14:56:50 +01:00
8503c085cb adds tests
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-27 14:56:13 +01:00
17 changed files with 707 additions and 34 deletions

View file

@ -8,6 +8,9 @@ TOKEN_SIGNING_SECRET=changeme-run-mix-phx.gen.secret
# Required: Hostname for URL generation
PHX_HOST=localhost
# Recommended: Association settings
ASSOCIATION_NAME="Sportsclub XYZ"
# Optional: OIDC Configuration
# These have defaults in docker-compose.prod.yml, only override if needed
# OIDC_CLIENT_ID=mv

View file

@ -6,12 +6,14 @@ defmodule Mv.Membership do
- `Member` - Club members with personal information and custom field values
- `CustomFieldValue` - Dynamic custom field values attached to members
- `CustomField` - Schema definitions for custom fields
- `Setting` - Global application settings (singleton)
## Public API
The domain exposes these main actions:
- Member CRUD: `create_member/1`, `list_members/0`, `update_member/2`, `destroy_member/1`
- Custom field value management: `create_custom_field_value/1`, `list_custom_field_values/0`, etc.
- Custom field management: `create_custom_field/1`, `list_custom_fields/0`, etc.
- Settings management: `get_settings/0`, `update_settings/2`
## Admin Interface
The domain is configured with AshAdmin for management UI.
@ -45,5 +47,80 @@ defmodule Mv.Membership do
define :destroy_custom_field, action: :destroy_with_values
define :prepare_custom_field_deletion, action: :prepare_deletion, args: [:id]
end
resource Mv.Membership.Setting do
# Note: create action exists but is not exposed via code interface
# It's only used internally as fallback in get_settings/0
# Settings should be created via seed script
define :update_settings, action: :update
end
end
# Singleton pattern: Get the single settings record
@doc """
Gets the global settings.
Settings should normally be created via the seed script (`priv/repo/seeds.exs`).
If no settings exist, this function will create them as a fallback using the
`ASSOCIATION_NAME` environment variable or "Club Name" as default.
## Returns
- `{:ok, settings}` - The settings record
- `{:ok, nil}` - No settings exist (should not happen if seeds were run)
- `{:error, error}` - Error reading settings
## Examples
iex> {:ok, settings} = Mv.Membership.get_settings()
iex> settings.club_name
"My Club"
"""
def get_settings do
# Try to get the first (and only) settings record
case Ash.read_one(Mv.Membership.Setting, domain: __MODULE__) do
{:ok, nil} ->
# No settings exist - create as fallback (should normally be created via seed script)
default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name"
Mv.Membership.Setting
|> Ash.Changeset.for_create(:create, %{club_name: default_club_name})
|> Ash.create!(domain: __MODULE__)
|> then(fn settings -> {:ok, settings} end)
{:ok, settings} ->
{:ok, settings}
{:error, error} ->
{:error, error}
end
end
@doc """
Updates the global settings.
## Parameters
- `settings` - The settings record to update
- `attrs` - A map of attributes to update (e.g., `%{club_name: "New Name"}`)
## Returns
- `{:ok, updated_settings}` - Successfully updated settings
- `{:error, error}` - Validation or update error
## Examples
iex> {:ok, settings} = Mv.Membership.get_settings()
iex> {:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Club"})
iex> updated.club_name
"New Club"
"""
def update_settings(settings, attrs) do
settings
|> Ash.Changeset.for_update(:update, attrs)
|> Ash.update(domain: __MODULE__)
end
end

80
lib/membership/setting.ex Normal file
View file

@ -0,0 +1,80 @@
defmodule Mv.Membership.Setting do
@moduledoc """
Ash resource representing global application settings.
## Overview
Settings is a singleton resource that stores global configuration for the association,
such as the club name and branding information. There should only ever be one settings
record in the database.
## Attributes
- `club_name` - The name of the association/club (required, cannot be empty)
## Singleton Pattern
This resource uses a singleton pattern - there should only be one settings record.
The resource is designed to be read and updated, but not created or destroyed
through normal CRUD operations. Initial settings should be seeded.
## Environment Variable Support
The `club_name` can be set via the `ASSOCIATION_NAME` environment variable.
If set, the environment variable value is used as a fallback when no database
value exists. Database values always take precedence over environment variables.
## Examples
# Get current settings
{:ok, settings} = Mv.Membership.get_settings()
settings.club_name # => "My Club"
# Update club name
{:ok, updated} = Mv.Membership.update_settings(settings, %{club_name: "New Name"})
"""
use Ash.Resource,
domain: Mv.Membership,
data_layer: AshPostgres.DataLayer
postgres do
table "settings"
repo Mv.Repo
end
resource do
description "Global application settings (singleton resource)"
end
actions do
defaults [:read]
# Internal create action - not exposed via code interface
# Used only as fallback in get_settings/0 if settings don't exist
# Settings should normally be created via seed script
create :create do
accept [:club_name]
end
update :update do
primary? true
accept [:club_name]
end
end
validations do
validate present(:club_name), on: [:create, :update]
validate string_length(:club_name, min: 1), on: [:create, :update]
end
attributes do
uuid_primary_key :id
attribute :club_name, :string,
allow_nil?: false,
public?: true,
description: "The name of the association/club",
constraints: [
trim?: true,
min_length: 1
]
timestamps()
end
end

View file

@ -6,15 +6,21 @@ defmodule MvWeb.Layouts.Navbar do
use Gettext, backend: MvWeb.Gettext
use MvWeb, :verified_routes
alias Mv.Membership
attr :current_user, :map,
required: true,
doc: "The current user - navbar is only shown when user is present"
def navbar(assigns) do
club_name = get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H"""
<header class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<a class="btn btn-ghost text-xl">Mitgliederverwaltung</a>
<a class="btn btn-ghost text-xl">{@club_name}</a>
<ul class="menu menu-horizontal bg-base-200">
<li><.link navigate="/members">{gettext("Members")}</.link></li>
<li><.link navigate="/custom_fields">{gettext("Custom Fields")}</.link></li>
@ -89,7 +95,9 @@ defmodule MvWeb.Layouts.Navbar do
{gettext("Profil")}
</.link>
</li>
<li><a>{gettext("Settings")}</a></li>
<li>
<.link navigate={~p"/settings"}>{gettext("Settings")}</.link>
</li>
<li>
<.link href={~p"/sign-out"}>{gettext("Logout")}</.link>
</li>
@ -99,4 +107,13 @@ defmodule MvWeb.Layouts.Navbar do
</header>
"""
end
# Helper function to get club name from settings
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
defp get_club_name do
case Membership.get_settings() do
{:ok, settings} -> settings.club_name
_ -> "Mitgliederverwaltung"
end
end
end

View file

@ -0,0 +1,97 @@
defmodule MvWeb.GlobalSettingsLive do
@moduledoc """
LiveView for managing global application settings (Vereinsdaten).
## Features
- Edit the association/club name
- Real-time form validation
- Success/error feedback
## Settings
- `club_name` - The name of the association/club (required)
## Events
- `validate` - Real-time form validation
- `save` - Save settings changes
## Note
Settings is a singleton resource - there is only one settings record.
The club_name can also be set via the `ASSOCIATION_NAME` environment variable.
"""
use MvWeb, :live_view
alias Mv.Membership
@impl true
def mount(_params, _session, socket) do
{:ok, settings} = Membership.get_settings()
{:ok,
socket
|> assign(:page_title, gettext("Club Settings"))
|> assign(:settings, settings)
|> assign_form()}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<.header>
{gettext("Club Settings")}
<:subtitle>
{gettext("Manage global settings for the association.")}
</:subtitle>
</.header>
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
<.input
field={@form[:club_name]}
type="text"
label={gettext("Association Name")}
required
/>
<.button phx-disable-with={gettext("Saving...")} variant="primary">
{gettext("Save Settings")}
</.button>
</.form>
</Layouts.app>
"""
end
@impl true
def handle_event("validate", %{"setting" => setting_params}, socket) do
{:noreply,
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))}
end
def handle_event("save", %{"setting" => setting_params}, socket) do
case AshPhoenix.Form.submit(socket.assigns.form, params: setting_params) do
{:ok, updated_settings} ->
socket =
socket
|> assign(:settings, updated_settings)
|> put_flash(:info, gettext("Settings updated successfully"))
|> assign_form()
{:noreply, socket}
{:error, form} ->
{:noreply, assign(socket, form: form)}
end
end
defp assign_form(%{assigns: %{settings: settings}} = socket) do
form =
AshPhoenix.Form.for_update(
settings,
:update,
api: Membership,
as: "setting",
forms: [auto?: true]
)
assign(socket, form: to_form(form))
end
end

View file

@ -73,6 +73,8 @@ defmodule MvWeb.Router do
live "/users/:id", UserLive.Show, :show
live "/users/:id/show/edit", UserLive.Show, :edit
live "/settings", GlobalSettingsLive
post "/set_locale", LocaleController, :set_locale
end

View file

@ -160,6 +160,7 @@ msgstr "Mitglied speichern"
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
@ -291,7 +292,7 @@ msgstr "ID"
msgid "Immutable"
msgstr "Unveränderlich"
#: lib/mv_web/components/layouts/navbar.ex:94
#: lib/mv_web/components/layouts/navbar.ex:102
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr "Abmelden"
@ -307,7 +308,7 @@ msgstr "Benutzer*innen auflisten"
msgid "Member"
msgstr "Mitglied"
#: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
@ -351,7 +352,7 @@ msgstr "OIDC ID"
msgid "Password Authentication"
msgstr "Passwort-Authentifizierung"
#: lib/mv_web/components/layouts/navbar.ex:89
#: lib/mv_web/components/layouts/navbar.ex:95
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr "Profil"
@ -371,7 +372,7 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member"
msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex:92
#: lib/mv_web/components/layouts/navbar.ex:99
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr "Einstellungen"
@ -536,14 +537,14 @@ msgstr "Zurück zur Mitgliederliste"
msgid "Back to users list"
msgstr "Zurück zur Benutzer*innen-Liste"
#: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr "Sprache auswählen"
#: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:60
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"
@ -554,7 +555,7 @@ msgstr "Dunklen Modus umschalten"
msgid "Search..."
msgstr "Suchen..."
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format
msgid "Users"
msgstr "Benutzer*innen"
@ -647,7 +648,7 @@ msgstr "Benutzerdefinierten Feldwert speichern"
msgid "Use this form to manage custom_field records in your database."
msgstr "Verwende dieses Formular, um Benutzerdefinierte Felder in deiner Datenbank zu verwalten."
#: lib/mv_web/components/layouts/navbar.ex:20
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format
msgid "Custom Fields"
msgstr "Benutzerdefinierte Felder"
@ -694,6 +695,32 @@ msgstr "Obigen Text zur Bestätigung eingeben"
msgid "To confirm deletion, please enter this text:"
msgstr "Um die Löschung zu bestätigen, gib bitte folgenden Text ein:"
#: lib/mv_web/live/global_settings_live.ex:51
#, elixir-autogen, elixir-format
msgid "Association Name"
msgstr "Vereinsname"
#: lib/mv_web/live/global_settings_live.ex:31
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format, fuzzy
msgid "Club Settings"
msgstr "Vereinsdaten"
#: lib/mv_web/live/global_settings_live.ex:43
#, elixir-autogen, elixir-format
msgid "Manage global settings for the association."
msgstr "Passe übergreifende Einstellungen für den Verein an."
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings"
msgstr "Einstellungen speichern"
#: lib/mv_web/live/global_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Settings updated successfully"
msgstr "Einstellungen erfolgreich gespeichert"
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
#~ #, elixir-autogen, elixir-format
#~ msgid "To confirm deletion, please enter the custom field slug:"

View file

@ -161,6 +161,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
@ -292,7 +293,7 @@ msgstr ""
msgid "Immutable"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:94
#: lib/mv_web/components/layouts/navbar.ex:102
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@ -308,7 +309,7 @@ msgstr ""
msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
@ -352,7 +353,7 @@ msgstr ""
msgid "Password Authentication"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:89
#: lib/mv_web/components/layouts/navbar.ex:95
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@ -372,7 +373,7 @@ msgstr ""
msgid "Select member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:92
#: lib/mv_web/components/layouts/navbar.ex:99
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
@ -537,14 +538,14 @@ msgstr ""
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:60
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@ -555,7 +556,7 @@ msgstr ""
msgid "Search..."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format
msgid "Users"
msgstr ""
@ -648,7 +649,7 @@ msgstr ""
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format
msgid "Custom Fields"
msgstr ""
@ -694,3 +695,29 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "To confirm deletion, please enter this text:"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:51
#, elixir-autogen, elixir-format
msgid "Association Name"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:31
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format
msgid "Club Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:43
#, elixir-autogen, elixir-format
msgid "Manage global settings for the association."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format
msgid "Save Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Settings updated successfully"
msgstr ""

View file

@ -161,6 +161,7 @@ msgstr ""
#: lib/mv_web/live/custom_field_live/form.ex:64
#: lib/mv_web/live/custom_field_value_live/form.ex:74
#: lib/mv_web/live/global_settings_live.ex:55
#: lib/mv_web/live/member_live/form.ex:79
#: lib/mv_web/live/user_live/form.ex:124
#, elixir-autogen, elixir-format
@ -292,7 +293,7 @@ msgstr ""
msgid "Immutable"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:94
#: lib/mv_web/components/layouts/navbar.ex:102
#, elixir-autogen, elixir-format
msgid "Logout"
msgstr ""
@ -308,7 +309,7 @@ msgstr ""
msgid "Member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:19
#: lib/mv_web/components/layouts/navbar.ex:25
#: lib/mv_web/live/member_live/index.ex:39
#: lib/mv_web/live/member_live/index.html.heex:3
#, elixir-autogen, elixir-format
@ -352,7 +353,7 @@ msgstr ""
msgid "Password Authentication"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:89
#: lib/mv_web/components/layouts/navbar.ex:95
#, elixir-autogen, elixir-format
msgid "Profil"
msgstr ""
@ -372,7 +373,7 @@ msgstr ""
msgid "Select member"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:92
#: lib/mv_web/components/layouts/navbar.ex:99
#, elixir-autogen, elixir-format
msgid "Settings"
msgstr ""
@ -537,14 +538,14 @@ msgstr ""
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:27
#: lib/mv_web/components/layouts/navbar.ex:33
#: lib/mv_web/components/layouts/navbar.ex:39
#, elixir-autogen, elixir-format, fuzzy
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:40
#: lib/mv_web/components/layouts/navbar.ex:60
#: lib/mv_web/components/layouts/navbar.ex:46
#: lib/mv_web/components/layouts/navbar.ex:66
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr ""
@ -555,7 +556,7 @@ msgstr ""
msgid "Search..."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format, fuzzy
msgid "Users"
msgstr ""
@ -648,7 +649,7 @@ msgstr ""
msgid "Use this form to manage custom_field records in your database."
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:20
#: lib/mv_web/components/layouts/navbar.ex:26
#, elixir-autogen, elixir-format, fuzzy
msgid "Custom Fields"
msgstr ""
@ -695,6 +696,32 @@ msgstr ""
msgid "To confirm deletion, please enter this text:"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:51
#, elixir-autogen, elixir-format
msgid "Association Name"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:31
#: lib/mv_web/live/global_settings_live.ex:41
#, elixir-autogen, elixir-format, fuzzy
msgid "Club Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:43
#, elixir-autogen, elixir-format
msgid "Manage global settings for the association."
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:56
#, elixir-autogen, elixir-format, fuzzy
msgid "Save Settings"
msgstr ""
#: lib/mv_web/live/global_settings_live.ex:75
#, elixir-autogen, elixir-format
msgid "Settings updated successfully"
msgstr ""
#~ #: lib/mv_web/live/custom_field_live/index.ex:97
#~ #, elixir-autogen, elixir-format
#~ msgid "To confirm deletion, please enter the custom field slug:"

View file

@ -0,0 +1,31 @@
defmodule Mv.Repo.Migrations.AddSettingsTable do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
create table(:settings, primary_key: false) do
add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
add :club_name, :text, null: false
add :inserted_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
add :updated_at, :utc_datetime_usec,
null: false,
default: fragment("(now() AT TIME ZONE 'utc')")
end
# Note: Singleton pattern is enforced at application level via get_settings/0
# which creates the record if it doesn't exist and only allows updates
end
def down do
drop table(:settings)
end
end

View file

@ -323,8 +323,27 @@ if friedrich = find_member.("friedrich.wagner@example.de") do
end)
end
# Create or update global settings (singleton)
default_club_name = System.get_env("ASSOCIATION_NAME") || "Club Name"
case Membership.get_settings() do
{:ok, existing_settings} ->
# Settings exist, update if club_name is different from env var
if existing_settings.club_name != default_club_name do
{:ok, _updated} =
Membership.update_settings(existing_settings, %{club_name: default_club_name})
end
{:ok, nil} ->
# Settings don't exist, create them
Mv.Membership.Setting
|> Ash.Changeset.for_create(:create, %{club_name: default_club_name})
|> Ash.create!(domain: Mv.Membership)
end
IO.puts("✅ Seeds completed successfully!")
IO.puts("📝 Created sample data:")
IO.puts(" - Global settings: club_name = #{default_club_name}")
IO.puts(" - Custom fields: 12 fields (String, Date, Boolean, Email, + 8 realistic fields)")
IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
IO.puts(" - Sample members: Hans, Greta, Friedrich")

View file

@ -0,0 +1,67 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "club_name",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "353EB39F18B97C596A77A78A060FB9DE075AAD731F74F64AB62D357CBCDEC914",
"identities": [],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
}

View file

@ -0,0 +1,61 @@
defmodule Mv.Membership.SettingEnvTest do
use Mv.DataCase, async: false
alias Mv.Membership
describe "Settings with environment variable" do
test "club_name can be set via ASSOCIATION_NAME environment variable" do
# Set environment variable
System.put_env("ASSOCIATION_NAME", "Test Association from Env")
try do
# Get settings - should use environment variable if no DB value exists
{:ok, settings} = Membership.get_settings()
# If settings don't have a club_name in DB, it should use the env var
# This depends on implementation - we'll check that the env var is respected
assert settings.club_name != nil
after
# Clean up
System.delete_env("ASSOCIATION_NAME")
end
end
test "database value takes precedence over environment variable" do
# Set environment variable
System.put_env("ASSOCIATION_NAME", "Env Value")
try do
# Set a value in the database
{:ok, settings} = Membership.get_settings()
{:ok, _updated} = Membership.update_settings(settings, %{club_name: "DB Value"})
# Get settings again - should use DB value, not env var
{:ok, settings_after} = Membership.get_settings()
assert settings_after.club_name == "DB Value"
after
# Clean up
System.delete_env("ASSOCIATION_NAME")
end
end
test "uses environment variable when database value is not set" do
# Set environment variable
System.put_env("ASSOCIATION_NAME", "Default from Env")
try do
# Clear database value (if possible) or check that env var is used
{:ok, settings} = Membership.get_settings()
# If club_name is nil or empty in DB, should use env var
# This test depends on implementation details
# We're testing that the env var fallback works
club_name = settings.club_name || System.get_env("ASSOCIATION_NAME")
assert club_name != nil
assert club_name != ""
after
# Clean up
System.delete_env("ASSOCIATION_NAME")
end
end
end
end

View file

@ -0,0 +1,51 @@
defmodule Mv.Membership.SettingTest do
use Mv.DataCase, async: false
alias Mv.Membership
describe "Settings Resource" do
test "can read settings" do
# Settings should be a singleton resource
assert {:ok, _settings} = Membership.get_settings()
end
test "settings have club_name attribute" do
{:ok, settings} = Membership.get_settings()
assert Map.has_key?(settings, :club_name)
end
test "can update club_name" do
{:ok, settings} = Membership.get_settings()
assert {:ok, updated_settings} =
Membership.update_settings(settings, %{club_name: "New Club Name"})
assert updated_settings.club_name == "New Club Name"
end
test "club_name is required" do
{:ok, settings} = Membership.get_settings()
assert {:error, %Ash.Error.Invalid{errors: errors}} =
Membership.update_settings(settings, %{club_name: nil})
assert error_message(errors, :club_name) =~ "must be present"
end
test "club_name cannot be empty" do
{:ok, settings} = Membership.get_settings()
assert {:error, %Ash.Error.Invalid{errors: errors}} =
Membership.update_settings(settings, %{club_name: ""})
assert error_message(errors, :club_name) =~ "must be present"
end
end
# Helper function to extract error messages
defp error_message(errors, field) do
errors
|> Enum.filter(fn err -> Map.get(err, :field) == field end)
|> Enum.map(&Map.get(&1, :message, ""))
|> List.first() || ""
end
end

View file

@ -84,5 +84,23 @@ defmodule MvWeb.Layouts.NavbarTest do
# Check for correct logout path
assert html =~ ~s(href="/sign-out")
end
test "Settings link navigates to global settings page", %{conn: conn} do
user = create_test_user(%{email: "test@example.com"})
conn = conn_with_oidc_user(conn, user)
html =
render_component(&MvWeb.Layouts.Navbar.navbar/1, %{
current_user: user
})
# Check that Settings link exists and points to /settings
assert html =~ "Settings"
assert html =~ ~s(href="/settings") || html =~ ~s(navigate="/settings")
# Verify the link actually works by navigating to it
{:ok, _view, settings_html} = live(conn, ~p"/settings")
assert settings_html =~ "Club Settings"
end
end
end

View file

@ -1,10 +1,11 @@
defmodule MvWeb.PageControllerTest do
use MvWeb.ConnCase
use MvWeb.ConnCase, async: true
test "GET /", %{conn: conn} do
conn = conn_with_oidc_user(conn)
test "renders home template successfully with authenticated user", %{conn: conn} do
user = create_test_user(%{email: "test@example.com"})
conn = conn_with_oidc_user(conn, user)
conn = get(conn, "/")
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Mitgliederverwaltung"
assert html_response(conn, 200)
end
end

View file

@ -0,0 +1,68 @@
defmodule MvWeb.GlobalSettingsLiveTest do
use MvWeb.ConnCase, async: true
import Phoenix.LiveViewTest
alias Mv.Membership
describe "Global Settings LiveView" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
conn = conn_with_oidc_user(conn, user)
{:ok, conn: conn, user: user}
end
test "renders the global settings page", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "Club Settings"
assert html =~ "Settings"
end
test "displays current club name", %{conn: conn} do
# Set initial club name
{:ok, settings} = Membership.get_settings()
{:ok, _updated} = Membership.update_settings(settings, %{club_name: "Test Club"})
{:ok, _view, html} = live(conn, ~p"/settings")
assert html =~ "Test Club"
end
test "can update club name via form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with new club name
assert view
|> form("#settings-form", %{setting: %{club_name: "Updated Club Name"}})
|> render_submit()
# Check for success message
assert render(view) =~ "Settings updated successfully"
assert render(view) =~ "Updated Club Name"
end
test "shows error when club_name is empty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with empty club name
html =
view
|> form("#settings-form", %{setting: %{club_name: ""}})
|> render_submit()
assert html =~ "must be present"
end
test "shows error when club_name is missing", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# Submit form with club_name explicitly set to empty string
# (Phoenix forms will keep existing value if field is omitted)
html =
view
|> form("#settings-form", %{setting: %{club_name: ""}})
|> render_submit()
assert html =~ "must be present"
end
end
end