Compare commits

...

7 commits

Author SHA1 Message Date
b5c97fdecd
fix: axe-core critical and major issues
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-26 20:34:45 +02:00
e9aad29cc8
feat: add translation 2025-09-26 20:34:45 +02:00
13c5c7b648
feat: make member emails unique 2025-09-26 20:34:45 +02:00
a5fc8d3fd3
feat: seed member user relations 2025-09-26 20:34:44 +02:00
c9e8737829
feat: add member-user link in member view and user view 2025-09-26 20:34:44 +02:00
b4fe4e4858
feat: member user relation 2025-09-26 20:34:44 +02:00
ccac004fa4
feat: Add tests for user-member relationship 2025-09-26 20:34:44 +02:00
17 changed files with 1119 additions and 159 deletions

View file

@ -60,15 +60,54 @@ defmodule Mv.Accounts.User do
end end
actions do actions do
defaults [:read, :create, :destroy, :update] defaults [:read, :create, :destroy]
update :update do
primary? true
require_atomic? false
end
create :create_user do create :create_user do
# Only accept email directly - member_id is NOT in accept list
# This prevents direct foreign key manipulation, forcing use of manage_relationship
accept [:email] accept [:email]
# Allow member to be passed as argument for relationship management
argument :member, :map, allow_nil?: true
upsert? true upsert? true
# Manage the member relationship during user creation
change manage_relationship(:member, :member,
# Look up existing member and relate to it
on_lookup: :relate,
# Error if member doesn't exist in database
on_no_match: :error,
# If member already linked to this user, ignore (shouldn't happen in create)
on_match: :ignore,
# If no member provided, that's fine (optional relationship)
on_missing: :ignore
)
end end
update :update_user do update :update_user do
# Only accept email directly - member_id is NOT in accept list
# This prevents direct foreign key manipulation, forcing use of manage_relationship
accept [:email] accept [:email]
# Allow member to be passed as argument for relationship management
argument :member, :map, allow_nil?: true
# Required because custom validation function cannot be done atomically
require_atomic? false
# Manage the member relationship during user update
change manage_relationship(:member, :member,
# Look up existing member and relate to it
on_lookup: :relate,
# Error if member doesn't exist in database
on_no_match: :error,
# If same member provided, that's fine (allows updates with same member)
on_match: :ignore,
# If no member provided, remove existing relationship (allows member removal)
on_missing: :unrelate
)
end end
# Admin action for direct password changes in admin panel # Admin action for direct password changes in admin panel
@ -76,6 +115,7 @@ defmodule Mv.Accounts.User do
update :admin_set_password do update :admin_set_password do
accept [:email] accept [:email]
argument :password, :string, allow_nil?: false, sensitive?: true argument :password, :string, allow_nil?: false, sensitive?: true
require_atomic? false
# Set the strategy context that HashPasswordChange expects # Set the strategy context that HashPasswordChange expects
change set_context(%{strategy_name: :password}) change set_context(%{strategy_name: :password})
@ -125,6 +165,28 @@ defmodule Mv.Accounts.User do
validate string_length(:password, min: 8) do validate string_length(:password, min: 8) do
where action_is([:register_with_password, :admin_set_password]) where action_is([:register_with_password, :admin_set_password])
end end
# Prevent overwriting existing member relationship
# This validation ensures race condition safety by requiring explicit two-step process:
# 1. Remove existing member (set member to nil)
# 2. Add new member
# This prevents accidental overwrites when multiple admins work simultaneously
validate fn changeset, _context ->
member_arg = Ash.Changeset.get_argument(changeset, :member)
current_member_id = changeset.data.member_id
# Only trigger if:
# - member argument is provided AND has an ID
# - user currently has a member
# - the new member ID is different from current member ID
if member_arg && member_arg[:id] && current_member_id &&
member_arg[:id] != current_member_id do
{:error,
field: :member, message: "User already has a member. Remove existing member first."}
else
:ok
end
end
end end
def validate_oidc_id_present(changeset, _context) do def validate_oidc_id_present(changeset, _context) do
@ -146,12 +208,16 @@ defmodule Mv.Accounts.User do
end end
relationships do relationships do
# 1:1 relationship - User can optionally belong to one Member
# This automatically creates a `member_id` attribute in the User table
# The relationship is optional (allow_nil? true by default)
belongs_to :member, Mv.Membership.Member belongs_to :member, Mv.Membership.Member
end end
identities do identities do
identity :unique_email, [:email] identity :unique_email, [:email]
identity :unique_oidc_id, [:oidc_id] identity :unique_oidc_id, [:oidc_id]
identity :unique_member, [:member_id]
end end
# You can customize this if you wish, but this is a safe default that # You can customize this if you wish, but this is a safe default that

View file

@ -13,7 +13,11 @@ defmodule Mv.Membership.Member do
create :create_member do create :create_member do
primary? true primary? true
# Properties can be created along with member
argument :properties, {:array, :map} argument :properties, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
accept [ accept [
:first_name, :first_name,
@ -32,12 +36,29 @@ defmodule Mv.Membership.Member do
] ]
change manage_relationship(:properties, type: :create) change manage_relationship(:properties, type: :create)
# Manage the user relationship during member creation
change manage_relationship(:user, :user,
# Look up existing user and relate to it
on_lookup: :relate,
# Error if user doesn't exist in database
on_no_match: :error,
# Error if user is already linked to another member (prevents "stealing")
on_match: :error,
# If no user provided, that's fine (optional relationship)
on_missing: :ignore
)
end end
update :update_member do update :update_member do
primary? true primary? true
# Required because custom validation function cannot be done atomically
require_atomic? false require_atomic? false
# Properties can be updated or created along with member
argument :properties, {:array, :map} argument :properties, {:array, :map}
# Allow user to be passed as argument for relationship management
# user_id is NOT in accept list to prevent direct foreign key manipulation
argument :user, :map, allow_nil?: true
accept [ accept [
:first_name, :first_name,
@ -56,6 +77,18 @@ defmodule Mv.Membership.Member do
] ]
change manage_relationship(:properties, on_match: :update, on_no_match: :create) change manage_relationship(:properties, on_match: :update, on_no_match: :create)
# Manage the user relationship during member update
change manage_relationship(:user, :user,
# Look up existing user and relate to it
on_lookup: :relate,
# Error if user doesn't exist in database
on_no_match: :error,
# Error if user is already linked to another member (prevents "stealing")
on_match: :error,
# If no user provided, remove existing relationship (allows user removal)
on_missing: :unrelate
)
end end
end end
@ -67,6 +100,40 @@ defmodule Mv.Membership.Member do
validate present(:last_name) validate present(:last_name)
validate present(:email) validate present(:email)
# Prevent linking to a user that already has a member
# This validation prevents "stealing" users from other members by checking
# if the target user is already linked to a different member
# This is necessary because manage_relationship's on_match: :error only checks
# if the user is already linked to THIS specific member, not ANY member
validate fn changeset, _context ->
user_arg = Ash.Changeset.get_argument(changeset, :user)
if user_arg && user_arg[:id] do
user_id = user_arg[:id]
current_member_id = changeset.data.id
# Check the current state of the user in the database
case Ash.get(Mv.Accounts.User, user_id) do
# User is free to be linked
{:ok, %{member_id: nil}} ->
:ok
# User already linked to this member (update scenario)
{:ok, %{member_id: ^current_member_id}} ->
:ok
{:ok, %{member_id: _other_member_id}} ->
# User is linked to a different member - prevent "stealing"
{:error, field: :user, message: "User is already linked to another member"}
{:error, _} ->
{:error, field: :user, message: "User not found"}
end
else
:ok
end
end
# Birth date not in the future # Birth date not in the future
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0), validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
where: [present(:birth_date)], where: [present(:birth_date)],
@ -170,5 +237,14 @@ defmodule Mv.Membership.Member do
relationships do relationships do
has_many :properties, Mv.Membership.Property has_many :properties, Mv.Membership.Property
# 1:1 relationship - Member can optionally have one User
# This references the User's member_id attribute
# The relationship is optional (allow_nil? true by default)
has_one :user, Mv.Accounts.User
end
# Define identities for upsert operations
identities do
identity :unique_email, [:email]
end end
end end

View file

@ -18,13 +18,20 @@ defmodule MvWeb.Layouts.Navbar do
<div class="flex gap-2"> <div class="flex gap-2">
<form method="post" action="/set_locale" class="mr-4"> <form method="post" action="/set_locale" class="mr-4">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} /> <input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<select name="locale" onchange="this.form.submit()" class="select select-sm"> <label class="sr-only" for="locale-select">{gettext("Select language")}</label>
<select
id="locale-select"
name="locale"
onchange="this.form.submit()"
class="select select-sm"
aria-label={gettext("Select language")}
>
<option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option> <option value="de" selected={Gettext.get_locale() == "de"}>Deutsch</option>
<option value="en" selected={Gettext.get_locale() == "en"}>English</option> <option value="en" selected={Gettext.get_locale() == "en"}>English</option>
</select> </select>
</form> </form>
<!-- Daisy UI Theme Toggle for dark and light mode--> <!-- Daisy UI Theme Toggle for dark and light mode-->
<label class="flex cursor-pointer gap-2"> <label class="flex cursor-pointer gap-2" aria-label={gettext("Toggle dark mode")}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@ -35,11 +42,17 @@ defmodule MvWeb.Layouts.Navbar do
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
aria-hidden="true"
> >
<circle cx="12" cy="12" r="5" /> <circle cx="12" cy="12" r="5" />
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" /> <path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" />
</svg> </svg>
<input type="checkbox" value="dark" class="toggle theme-controller" /> <input
type="checkbox"
value="dark"
class="toggle theme-controller"
aria-label={gettext("Toggle dark mode")}
/>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@ -50,6 +63,7 @@ defmodule MvWeb.Layouts.Navbar do
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
aria-hidden="true"
> >
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg> </svg>

View file

@ -11,8 +11,9 @@ defmodule MvWeb.MemberLive.Show do
<:subtitle>{gettext("This is a member record from your database.")}</:subtitle> <:subtitle>{gettext("This is a member record from your database.")}</:subtitle>
<:actions> <:actions>
<.button navigate={~p"/members"}> <.button navigate={~p"/members"} aria-label={gettext("Back to members list")}>
<.icon name="hero-arrow-left" /> <.icon name="hero-arrow-left" />
<span class="sr-only">{gettext("Back to members list")}</span>
</.button> </.button>
<.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}> <.button variant="primary" navigate={~p"/members/#{@member}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> {gettext("Edit Member")} <.icon name="hero-pencil-square" /> {gettext("Edit Member")}
@ -37,6 +38,19 @@ defmodule MvWeb.MemberLive.Show do
<:item title={gettext("Street")}>{@member.street}</:item> <:item title={gettext("Street")}>{@member.street}</:item>
<:item title={gettext("House Number")}>{@member.house_number}</:item> <:item title={gettext("House Number")}>{@member.house_number}</:item>
<:item title={gettext("Postal Code")}>{@member.postal_code}</:item> <:item title={gettext("Postal Code")}>{@member.postal_code}</:item>
<:item title={gettext("Linked User")}>
<%= if @member.user do %>
<.link
navigate={~p"/users/#{@member.user}"}
class="text-blue-600 hover:text-blue-800 underline"
>
<.icon name="hero-user" class="h-4 w-4 inline mr-1" />
{@member.user.email}
</.link>
<% else %>
<span class="text-gray-500 italic">{gettext("No user linked")}</span>
<% end %>
</:item>
</.list> </.list>
<h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3> <h3 class="mt-8 mb-2 text-lg font-semibold">{gettext("Custom Properties")}</h3>
@ -67,7 +81,7 @@ defmodule MvWeb.MemberLive.Show do
query = query =
Mv.Membership.Member Mv.Membership.Member
|> filter(id == ^id) |> filter(id == ^id)
|> load(properties: [:property_type]) |> load([:user, properties: [:property_type]])
member = Ash.read_one!(query) member = Ash.read_one!(query)

View file

@ -10,8 +10,9 @@ defmodule MvWeb.UserLive.Show do
<:subtitle>{gettext("This is a user record from your database.")}</:subtitle> <:subtitle>{gettext("This is a user record from your database.")}</:subtitle>
<:actions> <:actions>
<.button navigate={~p"/users"}> <.button navigate={~p"/users"} aria-label={gettext("Back to users list")}>
<.icon name="hero-arrow-left" /> <.icon name="hero-arrow-left" />
<span class="sr-only">{gettext("Back to users list")}</span>
</.button> </.button>
<.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}> <.button variant="primary" navigate={~p"/users/#{@user}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> {gettext("Edit User")} <.icon name="hero-pencil-square" /> {gettext("Edit User")}
@ -26,6 +27,19 @@ defmodule MvWeb.UserLive.Show do
<:item title={gettext("Password Authentication")}> <:item title={gettext("Password Authentication")}>
{if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")} {if @user.hashed_password, do: gettext("Enabled"), else: gettext("Not enabled")}
</:item> </:item>
<:item title={gettext("Linked Member")}>
<%= if @user.member do %>
<.link
navigate={~p"/members/#{@user.member}"}
class="text-blue-600 hover:text-blue-800 underline"
>
<.icon name="hero-users" class="h-4 w-4 inline mr-1" />
{@user.member.first_name} {@user.member.last_name}
</.link>
<% else %>
<span class="text-gray-500 italic">{gettext("No member linked")}</span>
<% end %>
</:item>
</.list> </.list>
</Layouts.app> </Layouts.app>
""" """
@ -33,9 +47,11 @@ defmodule MvWeb.UserLive.Show do
@impl true @impl true
def mount(%{"id" => id}, _session, socket) do def mount(%{"id" => id}, _session, socket) do
user = Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts, load: [:member])
{:ok, {:ok,
socket socket
|> assign(:page_title, gettext("Show User")) |> assign(:page_title, gettext("Show User"))
|> assign(:user, Ash.get!(Mv.Accounts.User, id, domain: Mv.Accounts))} |> assign(:user, user)}
end end
end end

View file

@ -61,6 +61,3 @@ msgstr "Anmelden..."
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "Das Passwort wurde erfolgreich zurückgesetzt" msgstr "Das Passwort wurde erfolgreich zurückgesetzt"
msgid "Sign in with Rauthy"
msgstr "Anmelden mit der Vereinscloud"

View file

@ -29,7 +29,7 @@ msgstr "Verbindung wird wiederhergestellt"
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62 #: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "Stadt" msgstr "Stadt"
@ -47,37 +47,37 @@ msgstr "Löschen"
msgid "Edit" msgid "Edit"
msgstr "Bearbeite" msgstr "Bearbeite"
#: lib/mv_web/live/member_live/show.ex:18 #: lib/mv_web/live/member_live/show.ex:19
#: lib/mv_web/live/member_live/show.ex:81 #: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "Mitglied bearbeiten" msgstr "Mitglied bearbeiten"
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "E-Mail" msgstr "E-Mail"
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:16
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "Vorname" msgstr "Vorname"
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64 #: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "Beitrittsdatum" msgstr "Beitrittsdatum"
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:17
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "Nachname" msgstr "Nachname"
@ -109,52 +109,52 @@ msgid "close"
msgstr "schließen" msgstr "schließen"
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:19
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "Geburtsdatum" msgstr "Geburtsdatum"
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:30
#: lib/mv_web/live/member_live/show.ex:42 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom Properties" msgid "Custom Properties"
msgstr "Eigene Eigenschaften" msgstr "Eigene Eigenschaften"
#: lib/mv_web/live/member_live/form.ex:23 #: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "Austrittsdatum" msgstr "Austrittsdatum"
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60 #: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "Hausnummer" msgstr "Hausnummer"
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:24
#: lib/mv_web/live/member_live/show.ex:35 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "Notizen" msgstr "Notizen"
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:20
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "Bezahlt" msgstr "Bezahlt"
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63 #: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/show.ex:32 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "Telefonnummer" msgstr "Telefonnummer"
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61 #: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "Postleitzahl" msgstr "Postleitzahl"
@ -174,7 +174,7 @@ msgstr "Speichern..."
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59 #: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "Straße" msgstr "Straße"
@ -184,17 +184,17 @@ msgstr "Straße"
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften."
#: lib/mv_web/live/member_live/show.ex:24 #: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "ID" msgstr "ID"
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "Nein" msgstr "Nein"
#: lib/mv_web/live/member_live/show.ex:80 #: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show Member" msgid "Show Member"
msgstr "Mitglied anzeigen" msgstr "Mitglied anzeigen"
@ -204,7 +204,7 @@ msgstr "Mitglied anzeigen"
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "Dies ist ein Mitglied aus deiner Datenbank." msgstr "Dies ist ein Mitglied aus deiner Datenbank."
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "Ja" msgstr "Ja"
@ -281,17 +281,17 @@ msgstr "Eigenschaftstyp auswählen"
msgid "Description" msgid "Description"
msgstr "Beschreibung" msgstr "Beschreibung"
#: lib/mv_web/live/user_live/show.ex:17 #: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit User" msgid "Edit User"
msgstr "Benutzer bearbeiten" msgstr "Benutzer bearbeiten"
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "Aktiviert" msgstr "Aktiviert"
#: lib/mv_web/live/user_live/show.ex:23 #: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "ID" msgstr "ID"
@ -301,7 +301,7 @@ msgstr "ID"
msgid "Immutable" msgid "Immutable"
msgstr "Unveränderlich" msgstr "Unveränderlich"
#: lib/mv_web/components/layouts/navbar.ex:73 #: lib/mv_web/components/layouts/navbar.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "Abmelden" msgstr "Abmelden"
@ -335,12 +335,12 @@ msgstr "Name"
msgid "New User" msgid "New User"
msgstr "Neuer Benutzer" msgstr "Neuer Benutzer"
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "Nicht aktiviert" msgstr "Nicht aktiviert"
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not set" msgid "Not set"
msgstr "Nicht gesetzt" msgstr "Nicht gesetzt"
@ -352,12 +352,12 @@ msgid "Note"
msgstr "Hinweis" msgstr "Hinweis"
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "OIDC ID" msgstr "OIDC ID"
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "Passwort-Authentifizierung" msgstr "Passwort-Authentifizierung"
@ -367,15 +367,15 @@ msgstr "Passwort-Authentifizierung"
msgid "Please select a property type first" msgid "Please select a property type first"
msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp" msgstr "Bitte wählen Sie zuerst einen Eigenschaftstyp"
#: lib/mv_web/components/layouts/navbar.ex:69 #: lib/mv_web/components/layouts/navbar.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "Profil" msgstr "Profil"
#: lib/mv_web/live/property_live/form.ex:207 #: lib/mv_web/live/property_live/form.ex:207
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format
msgid "Property %{action} successfully" msgid "Property %{action} successfully"
msgstr "Mitglied %{action} erfolgreich" msgstr "Eigenschaft %{action} erfolgreich"
#: lib/mv_web/live/property_live/form.ex:18 #: lib/mv_web/live/property_live/form.ex:18
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -412,7 +412,7 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member" msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/navbar.ex:72 #: lib/mv_web/components/layouts/navbar.ex:86
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
@ -422,7 +422,7 @@ msgstr "Einstellungen"
msgid "Save User" msgid "Save User"
msgstr "Benutzer speichern" msgstr "Benutzer speichern"
#: lib/mv_web/live/user_live/show.ex:38 #: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show User" msgid "Show User"
msgstr "Benutzer anzeigen" msgstr "Benutzer anzeigen"
@ -438,14 +438,14 @@ msgid "Unsupported value type: %{type}"
msgstr "Nicht unterstützter Wertetyp: %{type}" msgstr "Nicht unterstützter Wertetyp: %{type}"
#: lib/mv_web/live/property_live/form.ex:10 #: lib/mv_web/live/property_live/form.ex:10
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format
msgid "Use this form to manage property records in your database." msgid "Use this form to manage property records in your database."
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." msgstr "Dieses Formular dient zur Verwaltung von Eigenschaften in der Datenbank."
#: lib/mv_web/live/property_type_live/form.ex:11 #: lib/mv_web/live/property_type_live/form.ex:11
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format
msgid "Use this form to manage property_type records in your database." msgid "Use this form to manage property_type records in your database."
msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." msgstr "Dieses Formular dient zur Verwaltung von Eigenschaftstypen in der Datenbank."
#: lib/mv_web/live/user_live/form.ex:10 #: lib/mv_web/live/user_live/form.ex:10
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
@ -553,7 +553,46 @@ msgstr "Passwort setzen"
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen." msgstr "Benutzer wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/auth_overrides.ex:30 #: lib/mv_web/live/user_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "or" msgid "Linked Member"
msgstr "oder" msgstr "Verknüpftes Mitglied"
#: lib/mv_web/live/member_live/show.ex:41
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr "Verknüpfter Benutzer"
#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr "Kein Mitglied verknüpft"
#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr "Kein Benutzer verknüpft"
#: lib/mv_web/live/member_live/show.ex:14
#: lib/mv_web/live/member_live/show.ex:16
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr "Zurück zur Mitgliederliste"
#: lib/mv_web/live/user_live/show.ex:13
#: lib/mv_web/live/user_live/show.ex:15
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr "Zurück zur Benutzerliste"
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr "Sprache auswählen"
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/components/layouts/navbar.ex:54
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "Dunklen Modus umschalten"

View file

@ -12,101 +12,101 @@ msgstr ""
## From Ecto.Changeset.cast/4 ## From Ecto.Changeset.cast/4
msgid "can't be blank" msgid "can't be blank"
msgstr "" msgstr "darf nicht leer sein"
## From Ecto.Changeset.unique_constraint/3 ## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken" msgid "has already been taken"
msgstr "" msgstr "ist bereits vergeben"
## From Ecto.Changeset.put_change/3 ## From Ecto.Changeset.put_change/3
msgid "is invalid" msgid "is invalid"
msgstr "" msgstr "ist ungültig"
## From Ecto.Changeset.validate_acceptance/3 ## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted" msgid "must be accepted"
msgstr "" msgstr "muss akzeptiert werden"
## From Ecto.Changeset.validate_format/3 ## From Ecto.Changeset.validate_format/3
msgid "has invalid format" msgid "has invalid format"
msgstr "" msgstr "hat ein ungültiges Format"
## From Ecto.Changeset.validate_subset/3 ## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry" msgid "has an invalid entry"
msgstr "" msgstr "hat einen ungültigen Eintrag"
## From Ecto.Changeset.validate_exclusion/3 ## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved" msgid "is reserved"
msgstr "" msgstr "ist reserviert"
## From Ecto.Changeset.validate_confirmation/3 ## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation" msgid "does not match confirmation"
msgstr "" msgstr "stimmt nicht mit der Bestätigung überein"
## From Ecto.Changeset.no_assoc_constraint/3 ## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry" msgid "is still associated with this entry"
msgstr "" msgstr "ist noch mit diesem Eintrag verknüpft"
msgid "are still associated with this entry" msgid "are still associated with this entry"
msgstr "" msgstr "sind noch mit diesem Eintrag verknüpft"
## From Ecto.Changeset.validate_length/3 ## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)" msgid "should have %{count} item(s)"
msgid_plural "should have %{count} item(s)" msgid_plural "should have %{count} item(s)"
msgstr[0] "" msgstr[0] "sollte %{count} Element haben"
msgstr[1] "" msgstr[1] "sollte %{count} Elemente haben"
msgid "should be %{count} character(s)" msgid "should be %{count} character(s)"
msgid_plural "should be %{count} character(s)" msgid_plural "should be %{count} character(s)"
msgstr[0] "" msgstr[0] "sollte %{count} Zeichen haben"
msgstr[1] "" msgstr[1] "sollte %{count} Zeichen haben"
msgid "should be %{count} byte(s)" msgid "should be %{count} byte(s)"
msgid_plural "should be %{count} byte(s)" msgid_plural "should be %{count} byte(s)"
msgstr[0] "" msgstr[0] "sollte %{count} Byte haben"
msgstr[1] "" msgstr[1] "sollte %{count} Bytes haben"
msgid "should have at least %{count} item(s)" msgid "should have at least %{count} item(s)"
msgid_plural "should have at least %{count} item(s)" msgid_plural "should have at least %{count} item(s)"
msgstr[0] "" msgstr[0] "sollte mindestens %{count} Element haben"
msgstr[1] "" msgstr[1] "sollte mindestens %{count} Elemente haben"
msgid "should be at least %{count} character(s)" msgid "should be at least %{count} character(s)"
msgid_plural "should be at least %{count} character(s)" msgid_plural "should be at least %{count} character(s)"
msgstr[0] "" msgstr[0] "sollte mindestens %{count} Zeichen haben"
msgstr[1] "" msgstr[1] "sollte mindestens %{count} Zeichen haben"
msgid "should be at least %{count} byte(s)" msgid "should be at least %{count} byte(s)"
msgid_plural "should be at least %{count} byte(s)" msgid_plural "should be at least %{count} byte(s)"
msgstr[0] "" msgstr[0] "sollte mindestens %{count} Byte haben"
msgstr[1] "" msgstr[1] "sollte mindestens %{count} Bytes haben"
msgid "should have at most %{count} item(s)" msgid "should have at most %{count} item(s)"
msgid_plural "should have at most %{count} item(s)" msgid_plural "should have at most %{count} item(s)"
msgstr[0] "" msgstr[0] "sollte höchstens %{count} Element haben"
msgstr[1] "" msgstr[1] "sollte höchstens %{count} Elemente haben"
msgid "should be at most %{count} character(s)" msgid "should be at most %{count} character(s)"
msgid_plural "should be at most %{count} character(s)" msgid_plural "should be at most %{count} character(s)"
msgstr[0] "" msgstr[0] "sollte höchstens %{count} Zeichen haben"
msgstr[1] "" msgstr[1] "sollte höchstens %{count} Zeichen haben"
msgid "should be at most %{count} byte(s)" msgid "should be at most %{count} byte(s)"
msgid_plural "should be at most %{count} byte(s)" msgid_plural "should be at most %{count} byte(s)"
msgstr[0] "" msgstr[0] "sollte höchstens %{count} Byte haben"
msgstr[1] "" msgstr[1] "sollte höchstens %{count} Bytes haben"
## From Ecto.Changeset.validate_number/3 ## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}" msgid "must be less than %{number}"
msgstr "" msgstr "muss kleiner als %{number} sein"
msgid "must be greater than %{number}" msgid "must be greater than %{number}"
msgstr "" msgstr "muss größer als %{number} sein"
msgid "must be less than or equal to %{number}" msgid "must be less than or equal to %{number}"
msgstr "" msgstr "muss kleiner oder gleich %{number} sein"
msgid "must be greater than or equal to %{number}" msgid "must be greater than or equal to %{number}"
msgstr "" msgstr "muss größer oder gleich %{number} sein"
msgid "must be equal to %{number}" msgid "must be equal to %{number}"
msgstr "" msgstr "muss gleich %{number} sein"

View file

@ -30,7 +30,7 @@ msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62 #: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" msgstr ""
@ -48,37 +48,37 @@ msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:18 #: lib/mv_web/live/member_live/show.ex:19
#: lib/mv_web/live/member_live/show.ex:81 #: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:16
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64 #: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:17
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "" msgstr ""
@ -110,52 +110,52 @@ msgid "close"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:19
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:30
#: lib/mv_web/live/member_live/show.ex:42 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom Properties" msgid "Custom Properties"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:23 #: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60 #: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:24
#: lib/mv_web/live/member_live/show.ex:35 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:20
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63 #: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/show.ex:32 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61 #: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "" msgstr ""
@ -175,7 +175,7 @@ msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59 #: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "" msgstr ""
@ -185,17 +185,17 @@ msgstr ""
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:24 #: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:80 #: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show Member" msgid "Show Member"
msgstr "" msgstr ""
@ -205,7 +205,7 @@ msgstr ""
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
@ -282,17 +282,17 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:17 #: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit User" msgid "Edit User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:23 #: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "" msgstr ""
@ -302,7 +302,7 @@ msgstr ""
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:73 #: lib/mv_web/components/layouts/navbar.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -336,12 +336,12 @@ msgstr ""
msgid "New User" msgid "New User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
@ -353,12 +353,12 @@ msgid "Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
@ -368,7 +368,7 @@ msgstr ""
msgid "Please select a property type first" msgid "Please select a property type first"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:69 #: lib/mv_web/components/layouts/navbar.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
@ -413,7 +413,7 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:72 #: lib/mv_web/components/layouts/navbar.ex:86
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -423,7 +423,7 @@ msgstr ""
msgid "Save User" msgid "Save User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:38 #: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Show User" msgid "Show User"
msgstr "" msgstr ""
@ -554,7 +554,46 @@ msgstr ""
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "" msgstr ""
#: lib/mv_web/auth_overrides.ex:30 #: lib/mv_web/live/user_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "or" msgid "Linked Member"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:41
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:14
#: lib/mv_web/live/member_live/show.ex:16
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:13
#: lib/mv_web/live/user_live/show.ex:15
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/components/layouts/navbar.ex:54
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "" msgstr ""

View file

@ -58,6 +58,3 @@ msgstr ""
msgid "Your password has successfully been reset" msgid "Your password has successfully been reset"
msgstr "" msgstr ""
msgid "Sign in with Rauthy"
msgstr "Sign in with Vereinscloud"

View file

@ -30,7 +30,7 @@ msgstr ""
#: lib/mv_web/live/member_live/form.ex:25 #: lib/mv_web/live/member_live/form.ex:25
#: lib/mv_web/live/member_live/index.html.heex:62 #: lib/mv_web/live/member_live/index.html.heex:62
#: lib/mv_web/live/member_live/show.ex:36 #: lib/mv_web/live/member_live/show.ex:37
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "City" msgid "City"
msgstr "" msgstr ""
@ -48,37 +48,37 @@ msgstr ""
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:18 #: lib/mv_web/live/member_live/show.ex:19
#: lib/mv_web/live/member_live/show.ex:81 #: lib/mv_web/live/member_live/show.ex:95
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Edit Member" msgid "Edit Member"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:18 #: lib/mv_web/live/member_live/form.ex:18
#: lib/mv_web/live/member_live/index.html.heex:58 #: lib/mv_web/live/member_live/index.html.heex:58
#: lib/mv_web/live/member_live/show.ex:27 #: lib/mv_web/live/member_live/show.ex:28
#: lib/mv_web/live/user_live/form.ex:14 #: lib/mv_web/live/user_live/form.ex:14
#: lib/mv_web/live/user_live/index.html.heex:44 #: lib/mv_web/live/user_live/index.html.heex:44
#: lib/mv_web/live/user_live/show.ex:24 #: lib/mv_web/live/user_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Email" msgid "Email"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:16 #: lib/mv_web/live/member_live/form.ex:16
#: lib/mv_web/live/member_live/show.ex:25 #: lib/mv_web/live/member_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "First Name" msgid "First Name"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:22 #: lib/mv_web/live/member_live/form.ex:22
#: lib/mv_web/live/member_live/index.html.heex:64 #: lib/mv_web/live/member_live/index.html.heex:64
#: lib/mv_web/live/member_live/show.ex:33 #: lib/mv_web/live/member_live/show.ex:34
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Join Date" msgid "Join Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:17 #: lib/mv_web/live/member_live/form.ex:17
#: lib/mv_web/live/member_live/show.ex:26 #: lib/mv_web/live/member_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Last Name" msgid "Last Name"
msgstr "" msgstr ""
@ -110,52 +110,52 @@ msgid "close"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:19 #: lib/mv_web/live/member_live/form.ex:19
#: lib/mv_web/live/member_live/show.ex:28 #: lib/mv_web/live/member_live/show.ex:29
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Birth Date" msgid "Birth Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:30 #: lib/mv_web/live/member_live/form.ex:30
#: lib/mv_web/live/member_live/show.ex:42 #: lib/mv_web/live/member_live/show.ex:56
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Custom Properties" msgid "Custom Properties"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:23 #: lib/mv_web/live/member_live/form.ex:23
#: lib/mv_web/live/member_live/show.ex:34 #: lib/mv_web/live/member_live/show.ex:35
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Exit Date" msgid "Exit Date"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:27 #: lib/mv_web/live/member_live/form.ex:27
#: lib/mv_web/live/member_live/index.html.heex:60 #: lib/mv_web/live/member_live/index.html.heex:60
#: lib/mv_web/live/member_live/show.ex:38 #: lib/mv_web/live/member_live/show.ex:39
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "House Number" msgid "House Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:24 #: lib/mv_web/live/member_live/form.ex:24
#: lib/mv_web/live/member_live/show.ex:35 #: lib/mv_web/live/member_live/show.ex:36
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Notes" msgid "Notes"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:20 #: lib/mv_web/live/member_live/form.ex:20
#: lib/mv_web/live/member_live/show.ex:29 #: lib/mv_web/live/member_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Paid" msgid "Paid"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:21 #: lib/mv_web/live/member_live/form.ex:21
#: lib/mv_web/live/member_live/index.html.heex:63 #: lib/mv_web/live/member_live/index.html.heex:63
#: lib/mv_web/live/member_live/show.ex:32 #: lib/mv_web/live/member_live/show.ex:33
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Phone Number" msgid "Phone Number"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/form.ex:28 #: lib/mv_web/live/member_live/form.ex:28
#: lib/mv_web/live/member_live/index.html.heex:61 #: lib/mv_web/live/member_live/index.html.heex:61
#: lib/mv_web/live/member_live/show.ex:39 #: lib/mv_web/live/member_live/show.ex:40
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Postal Code" msgid "Postal Code"
msgstr "" msgstr ""
@ -175,7 +175,7 @@ msgstr ""
#: lib/mv_web/live/member_live/form.ex:26 #: lib/mv_web/live/member_live/form.ex:26
#: lib/mv_web/live/member_live/index.html.heex:59 #: lib/mv_web/live/member_live/index.html.heex:59
#: lib/mv_web/live/member_live/show.ex:37 #: lib/mv_web/live/member_live/show.ex:38
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Street" msgid "Street"
msgstr "" msgstr ""
@ -185,17 +185,17 @@ msgstr ""
msgid "Use this form to manage member records and their properties." msgid "Use this form to manage member records and their properties."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:24 #: lib/mv_web/live/member_live/show.ex:25
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Id" msgid "Id"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "No" msgid "No"
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:80 #: lib/mv_web/live/member_live/show.ex:94
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show Member" msgid "Show Member"
msgstr "" msgstr ""
@ -205,7 +205,7 @@ msgstr ""
msgid "This is a member record from your database." msgid "This is a member record from your database."
msgstr "" msgstr ""
#: lib/mv_web/live/member_live/show.ex:30 #: lib/mv_web/live/member_live/show.ex:31
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Yes" msgid "Yes"
msgstr "" msgstr ""
@ -282,17 +282,17 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:17 #: lib/mv_web/live/user_live/show.ex:18
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Edit User" msgid "Edit User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Enabled" msgid "Enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:23 #: lib/mv_web/live/user_live/show.ex:24
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "ID" msgid "ID"
msgstr "" msgstr ""
@ -302,7 +302,7 @@ msgstr ""
msgid "Immutable" msgid "Immutable"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:73 #: lib/mv_web/components/layouts/navbar.ex:87
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Logout" msgid "Logout"
msgstr "" msgstr ""
@ -336,12 +336,12 @@ msgstr ""
msgid "New User" msgid "New User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:27 #: lib/mv_web/live/user_live/show.ex:28
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Not enabled" msgid "Not enabled"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Not set" msgid "Not set"
msgstr "" msgstr ""
@ -353,12 +353,12 @@ msgid "Note"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/index.html.heex:52 #: lib/mv_web/live/user_live/index.html.heex:52
#: lib/mv_web/live/user_live/show.ex:25 #: lib/mv_web/live/user_live/show.ex:26
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "OIDC ID" msgid "OIDC ID"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:26 #: lib/mv_web/live/user_live/show.ex:27
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Password Authentication" msgid "Password Authentication"
msgstr "" msgstr ""
@ -368,7 +368,7 @@ msgstr ""
msgid "Please select a property type first" msgid "Please select a property type first"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:69 #: lib/mv_web/components/layouts/navbar.ex:83
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Profil" msgid "Profil"
msgstr "" msgstr ""
@ -413,7 +413,7 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:72 #: lib/mv_web/components/layouts/navbar.ex:86
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -423,7 +423,7 @@ msgstr ""
msgid "Save User" msgid "Save User"
msgstr "" msgstr ""
#: lib/mv_web/live/user_live/show.ex:38 #: lib/mv_web/live/user_live/show.ex:54
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Show User" msgid "Show User"
msgstr "" msgstr ""
@ -554,7 +554,46 @@ msgstr "Set Password"
msgid "User will be created without a password. Check 'Set Password' to add one." msgid "User will be created without a password. Check 'Set Password' to add one."
msgstr "User will be created without a password. Check 'Set Password' to add one." msgstr "User will be created without a password. Check 'Set Password' to add one."
#: lib/mv_web/auth_overrides.ex:30 #: lib/mv_web/live/user_live/show.ex:30
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format, fuzzy
msgid "or" msgid "Linked Member"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:41
#, elixir-autogen, elixir-format
msgid "Linked User"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:40
#, elixir-autogen, elixir-format
msgid "No member linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:51
#, elixir-autogen, elixir-format
msgid "No user linked"
msgstr ""
#: lib/mv_web/live/member_live/show.ex:14
#: lib/mv_web/live/member_live/show.ex:16
#, elixir-autogen, elixir-format
msgid "Back to members list"
msgstr ""
#: lib/mv_web/live/user_live/show.ex:13
#: lib/mv_web/live/user_live/show.ex:15
#, elixir-autogen, elixir-format
msgid "Back to users list"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:21
#: lib/mv_web/components/layouts/navbar.ex:27
#, elixir-autogen, elixir-format, fuzzy
msgid "Select language"
msgstr ""
#: lib/mv_web/components/layouts/navbar.ex:34
#: lib/mv_web/components/layouts/navbar.ex:54
#, elixir-autogen, elixir-format
msgid "Toggle dark mode"
msgstr "" msgstr ""

View file

@ -0,0 +1,17 @@
defmodule Mv.Repo.Migrations.MemberRelation 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 unique_index(:users, [:member_id], name: "users_unique_member_index")
end
def down do
drop_if_exists unique_index(:users, [:member_id], name: "users_unique_member_index")
end
end

View file

@ -0,0 +1,17 @@
defmodule Mv.Repo.Migrations.AddUniqueEmailToMembers 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 unique_index(:members, [:email], name: "members_unique_email_index")
end
def down do
drop_if_exists unique_index(:members, [:email], name: "members_unique_email_index")
end
end

View file

@ -48,7 +48,7 @@ Accounts.create_user!(%{email: "admin@mv.local"}, upsert?: true, upsert_identity
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|> Ash.update!() |> Ash.update!()
# Create sample members for testing # Create sample members for testing - use upsert to prevent duplicates
for member_attrs <- [ for member_attrs <- [
%{ %{
first_name: "Hans", first_name: "Hans",
@ -90,5 +90,96 @@ for member_attrs <- [
house_number: "8" house_number: "8"
} }
] do ] do
Membership.create_member!(member_attrs) # Use upsert to prevent duplicates based on email
Membership.create_member!(member_attrs, upsert?: true, upsert_identity: :unique_email)
end end
# Create additional users for user-member linking examples
additional_users = [
%{email: "hans.mueller@example.de"},
%{email: "greta.schmidt@example.de"},
%{email: "maria.weber@example.de"},
%{email: "thomas.klein@example.de"}
]
created_users =
Enum.map(additional_users, fn user_attrs ->
Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email)
|> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"})
|> Ash.update!()
end)
# Create members with linked users to demonstrate the 1:1 relationship
# Only create if users don't already have members
linked_members = [
%{
first_name: "Maria",
last_name: "Weber",
email: "maria.weber@example.de",
birth_date: ~D[1992-07-14],
join_date: ~D[2023-03-15],
paid: true,
phone_number: "+49301357924",
city: "Frankfurt",
street: "Goetheplatz",
house_number: "5",
postal_code: "60313",
notes: "Linked to user account",
# Link to the third user (maria.weber@example.de)
user: Enum.at(created_users, 2)
},
%{
first_name: "Thomas",
last_name: "Klein",
email: "thomas.klein@example.de",
birth_date: ~D[1988-12-03],
join_date: ~D[2023-04-01],
paid: false,
phone_number: "+49302468135",
city: "Köln",
street: "Rheinstraße",
house_number: "23",
postal_code: "50667",
notes: "Linked to user account - needs payment follow-up",
# Link to the fourth user (thomas.klein@example.de)
user: Enum.at(created_users, 3)
}
]
# Create the linked members - use upsert to prevent duplicates
Enum.each(linked_members, fn member_attrs ->
user = member_attrs.user
member_attrs_without_user = Map.delete(member_attrs, :user)
# Check if user already has a member
if user.member_id == nil do
# User is free, create member and link - use upsert to prevent duplicates
Membership.create_member!(
Map.put(member_attrs_without_user, :user, %{id: user.id}),
upsert?: true,
upsert_identity: :unique_email
)
else
# User already has a member, just create the member without linking - use upsert to prevent duplicates
Membership.create_member!(member_attrs_without_user,
upsert?: true,
upsert_identity: :unique_email
)
end
end)
IO.puts("✅ Seeds completed successfully!")
IO.puts("📝 Created sample data:")
IO.puts(" - Property types: String, Date, Boolean, Email")
IO.puts(" - Admin user: admin@mv.local (password: testpassword)")
IO.puts(" - Sample members: Hans, Greta, Friedrich")
IO.puts(
" - Additional users: hans.mueller@example.de, greta.schmidt@example.de, maria.weber@example.de, thomas.klein@example.de"
)
IO.puts(
" - Linked members: Maria Weber ↔ maria.weber@example.de, Thomas Klein ↔ thomas.klein@example.de"
)
IO.puts("🔗 Visit the application to see user-member relationships in action!")

View file

@ -0,0 +1,202 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"uuid_generate_v7()\")",
"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": "first_name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "last_name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "email",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "birth_date",
"type": "date"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "paid",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "phone_number",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "join_date",
"type": "date"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "exit_date",
"type": "date"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "notes",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "city",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "street",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "house_number",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "postal_code",
"type": "text"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "5F070A1E5BEE9883AE864FB5A4A5E81F487A1C57D41576C23BAC8D933005D565",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "members_unique_email_index",
"keys": [
{
"type": "atom",
"value": "email"
}
],
"name": "unique_email",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "members"
}

View file

@ -0,0 +1,141 @@
{
"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": "email",
"type": "citext"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "hashed_password",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "oidc_id",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "users_member_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "members"
},
"scale": null,
"size": null,
"source": "member_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "FDEBD4840449609DDA8B50D6741C2EEDE9D81DFBC1E26D4BC77DBD9B5A8EA4DC",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "users_unique_email_index",
"keys": [
{
"type": "atom",
"value": "email"
}
],
"name": "unique_email",
"nils_distinct?": true,
"where": null
},
{
"all_tenants?": false,
"base_filter": null,
"index_name": "users_unique_oidc_id_index",
"keys": [
{
"type": "atom",
"value": "oidc_id"
}
],
"name": "unique_oidc_id",
"nils_distinct?": true,
"where": null
},
{
"all_tenants?": false,
"base_filter": null,
"index_name": "users_unique_member_index",
"keys": [
{
"type": "atom",
"value": "member_id"
}
],
"name": "unique_member",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "users"
}

View file

@ -0,0 +1,195 @@
defmodule Mv.Accounts.UserMemberRelationshipTest do
use Mv.DataCase, async: false
alias Mv.Accounts
alias Mv.Membership
describe "User-Member Relationship - Basic Tests" do
@valid_user_attrs %{
email: "test@example.com"
}
@valid_member_attrs %{
first_name: "John",
last_name: "Doe",
email: "john@example.com"
}
test "user can exist without member" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
assert user.member_id == nil
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
assert user_with_member.member == nil
end
test "member can exist without user" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
assert member.id != nil
assert member.first_name == "John"
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert member_with_user.user == nil
end
end
describe "User-Member Relationship - Linking Tests" do
@valid_user_attrs %{
email: "test1@example.com"
}
@valid_member_attrs %{
first_name: "Alice",
last_name: "Johnson",
email: "alice@example.com"
}
test "user can be linked to member during user creation" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
{:ok, user} = Accounts.create_user(user_attrs)
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
assert user_with_member.member.id == member.id
end
test "member can be linked to user during member creation using manage_relationship" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
member_attrs = Map.put(@valid_member_attrs, :user, %{id: user.id})
{:ok, member} = Membership.create_member(member_attrs)
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert member_with_user.user.id == user.id
end
test "user can be linked to member during update" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, updated_user} = Accounts.update_user(user, %{member: %{id: member.id}})
# Load the relationship to test it
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, updated_user.id, load: [:member])
assert user_with_member.member.id == member.id
end
test "member can be linked to user during update using manage_relationship" do
{:ok, user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, _updated_member} = Membership.update_member(member, %{user: %{id: user.id}})
# Load the relationship to test it
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert member_with_user.user.id == user.id
end
end
describe "User-Member Relationship - Inverse Relationship Tests" do
@valid_user_attrs %{
email: "test2@example.com"
}
@valid_member_attrs %{
first_name: "Bob",
last_name: "Smith",
email: "bob@example.com"
}
test "ash resolves inverse relationship automatically" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: member.id})
{:ok, user} = Accounts.create_user(user_attrs)
# Load relationships
{:ok, user_with_member} = Ash.get(Mv.Accounts.User, user.id, load: [:member])
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert user_with_member.member.id == member.id
assert member_with_user.user.id == user.id
end
test "member can find associated user" do
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, user} = Accounts.create_user(%{email: "test3@example.com", member: %{id: member.id}})
{:ok, member_with_user} = Ash.get(Mv.Membership.Member, member.id, load: [:user])
assert member_with_user.user.id == user.id
end
end
describe "User-Member Relationship - Preventing Duplicates" do
@valid_user_attrs %{
email: "test4@example.com"
}
@valid_member_attrs %{
first_name: "Charlie",
last_name: "Brown",
email: "charlie@example.com"
}
test "prevents overwriting a member of already linked user on update" do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
{:ok, user} = Accounts.create_user(user_attrs)
{:ok, member2} =
Membership.create_member(%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com"
})
assert {:error, %Ash.Error.Invalid{}} =
Accounts.update_user(user, %{member: %{id: member2.id}})
end
test "prevents linking user to already linked member on update" do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
{:ok, user2} = Accounts.create_user(%{email: "test5@example.com"})
assert {:error, %Ash.Error.Invalid{}} =
Accounts.update_user(user2, %{member: %{id: member.id}})
end
test "prevents linking member to already linked user on creation" do
{:ok, existing_member} = Membership.create_member(@valid_member_attrs)
user_attrs = Map.put(@valid_user_attrs, :member, %{id: existing_member.id})
{:ok, user} = Accounts.create_user(user_attrs)
assert {:error, %Ash.Error.Invalid{}} =
Membership.create_member(%{
first_name: "Dave",
last_name: "Wilson",
email: "dave@example.com",
user: %{id: user.id}
})
end
test "prevents linking user to already linked member on creation" do
{:ok, existing_user} = Accounts.create_user(@valid_user_attrs)
{:ok, member} = Membership.create_member(@valid_member_attrs)
{:ok, _updated_user} = Accounts.update_user(existing_user, %{member: %{id: member.id}})
assert {:error, %Ash.Error.Invalid{}} =
Accounts.create_user(%{
email: "test5@example.com",
member: %{id: member.id}
})
end
end
end