- {gettext(
- "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of memberdate (like e-mail or first name). Unknown data field columns will be ignored with a warning."
- )}
-
+ {gettext(
+ "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
+ )}
+
+ <%= for warning <- @import_progress.warnings do %>
+
{warning}
+ <% end %>
+
+
+
+ <% end %>
+
+
+ """
+ end
+
+ @impl true
+ def handle_event("validate_csv_upload", _params, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("start_import", _params, socket) do
+ case check_import_prerequisites(socket) do
+ {:error, message} ->
+ {:noreply, put_flash(socket, :error, message)}
+
+ :ok ->
+ process_csv_upload(socket)
+ end
+ end
+
+ # Checks if all prerequisites for starting an import are met.
+ #
+ # Validates:
+ # - User has admin permissions
+ # - No import is currently running
+ # - CSV file is uploaded and ready
+ #
+ # Returns `:ok` if all checks pass, `{:error, message}` otherwise.
+ #
+ # Note: on_mount :ensure_user_role_loaded already guarantees the role is loaded,
+ # so ensure_actor_loaded is primarily for clarity.
+ @spec check_import_prerequisites(Phoenix.LiveView.Socket.t()) ::
+ :ok | {:error, String.t()}
+ defp check_import_prerequisites(socket) do
+ # on_mount already ensures role is loaded, but we keep this for clarity
+ user_with_role = ensure_actor_loaded(socket)
+
+ cond do
+ not Authorization.can?(user_with_role, :create, Mv.Membership.Member) ->
+ {:error, gettext("Only administrators can import members from CSV files.")}
+
+ socket.assigns.import_status == :running ->
+ {:error, gettext("Import is already running. Please wait for it to complete.")}
+
+ Enum.empty?(socket.assigns.uploads.csv_file.entries) ->
+ {:error, gettext("Please select a CSV file to import.")}
+
+ not List.first(socket.assigns.uploads.csv_file.entries).done? ->
+ {:error,
+ gettext("Please wait for the file upload to complete before starting the import.")}
+
+ true ->
+ :ok
+ end
+ end
+
+ # Processes CSV upload and starts import process.
+ #
+ # Reads the uploaded CSV file, prepares it for import, and initiates
+ # the chunked processing workflow.
+ @spec process_csv_upload(Phoenix.LiveView.Socket.t()) ::
+ {:noreply, Phoenix.LiveView.Socket.t()}
+ defp process_csv_upload(socket) do
+ actor = MvWeb.LiveHelpers.current_actor(socket)
+
+ with {:ok, content} <- consume_and_read_csv(socket),
+ {:ok, import_state} <-
+ MemberCSV.prepare(content, max_rows: Config.csv_import_max_rows(), actor: actor) do
+ start_import(socket, import_state)
+ else
+ {:error, reason} when is_binary(reason) ->
+ {:noreply,
+ put_flash(
+ socket,
+ :error,
+ gettext("Failed to prepare CSV import: %{reason}", reason: reason)
+ )}
+
+ {:error, error} ->
+ error_message = format_error_message(error)
+
+ {:noreply,
+ put_flash(
+ socket,
+ :error,
+ gettext("Failed to prepare CSV import: %{reason}", reason: error_message)
+ )}
+ end
+ end
+
+ # Starts the import process by initializing progress tracking and scheduling the first chunk.
+ @spec start_import(Phoenix.LiveView.Socket.t(), map()) ::
+ {:noreply, Phoenix.LiveView.Socket.t()}
+ defp start_import(socket, import_state) do
+ progress = initialize_import_progress(import_state)
+
+ socket =
+ socket
+ |> assign(:import_state, import_state)
+ |> assign(:import_progress, progress)
+ |> assign(:import_status, :running)
+
+ send(self(), {:process_chunk, 0})
+
+ {:noreply, socket}
+ end
+
+ # Initializes the import progress tracking structure with default values.
+ @spec initialize_import_progress(map()) :: map()
+ defp initialize_import_progress(import_state) do
+ %{
+ inserted: 0,
+ failed: 0,
+ errors: [],
+ warnings: import_state.warnings || [],
+ status: :running,
+ current_chunk: 0,
+ total_chunks: length(import_state.chunks),
+ errors_truncated?: false
+ }
+ end
+
+ # Formats error messages for user-friendly display.
+ #
+ # Handles various error types including Ash errors, maps with message fields,
+ # lists of errors, and fallback formatting for unknown types.
+ @spec format_error_message(any()) :: String.t()
+ defp format_error_message(error) do
+ case error do
+ %Ash.Error.Invalid{} = ash_error ->
+ format_ash_error(ash_error)
+
+ %{message: msg} when is_binary(msg) ->
+ msg
+
+ %{errors: errors} when is_list(errors) ->
+ format_error_list(errors)
+
+ reason when is_binary(reason) ->
+ reason
+
+ other ->
+ format_unknown_error(other)
+ end
+ end
+
+ # Formats Ash validation errors for display
+ defp format_ash_error(%Ash.Error.Invalid{errors: errors}) when is_list(errors) do
+ Enum.map_join(errors, ", ", &format_single_error/1)
+ end
+
+ defp format_ash_error(error) do
+ format_unknown_error(error)
+ end
+
+ # Formats a list of errors into a readable string
+ defp format_error_list(errors) do
+ Enum.map_join(errors, ", ", &format_single_error/1)
+ end
+
+ # Formats a single error item
+ defp format_single_error(error) when is_map(error) do
+ Map.get(error, :message) || Map.get(error, :field) || inspect(error, limit: :infinity)
+ end
+
+ defp format_single_error(error) do
+ to_string(error)
+ end
+
+ # Formats unknown error types with truncation for very long messages
+ defp format_unknown_error(other) do
+ error_str = inspect(other, limit: :infinity, pretty: true)
+
+ if String.length(error_str) > @max_error_message_length do
+ String.slice(error_str, 0, @max_error_message_length - 3) <> "..."
+ else
+ error_str
+ end
+ end
+
+ @impl true
+ def handle_info({:process_chunk, idx}, socket) do
+ case socket.assigns do
+ %{import_state: import_state, import_progress: progress}
+ when is_map(import_state) and is_map(progress) ->
+ if idx < length(import_state.chunks) do
+ start_chunk_processing_task(socket, import_state, progress, idx)
+ else
+ handle_chunk_error(socket, :invalid_index, idx)
+ end
+
+ _ ->
+ # Missing required assigns - mark as error
+ handle_chunk_error(socket, :missing_state, idx)
+ end
+ end
+
+ @impl true
+ def handle_info({:chunk_done, idx, result}, socket) do
+ case socket.assigns do
+ %{import_state: import_state, import_progress: progress}
+ when is_map(import_state) and is_map(progress) ->
+ handle_chunk_result(socket, import_state, progress, idx, result)
+
+ _ ->
+ # Missing required assigns - mark as error
+ handle_chunk_error(socket, :missing_state, idx)
+ end
+ end
+
+ @impl true
+ def handle_info({:chunk_error, idx, reason}, socket) do
+ handle_chunk_error(socket, :processing_failed, idx, reason)
+ end
+
+ # Processes a chunk with error handling and sends result message to LiveView.
+ #
+ # Handles errors from MemberCSV.process_chunk and sends appropriate messages
+ # to the LiveView process for progress tracking.
+ @spec process_chunk_with_error_handling(
+ list(),
+ map(),
+ map(),
+ keyword(),
+ pid(),
+ non_neg_integer()
+ ) :: :ok
+ defp process_chunk_with_error_handling(
+ chunk,
+ column_map,
+ custom_field_map,
+ opts,
+ live_view_pid,
+ idx
+ ) do
+ result =
+ try do
+ MemberCSV.process_chunk(chunk, column_map, custom_field_map, opts)
+ rescue
+ e ->
+ {:error, Exception.message(e)}
+ catch
+ :exit, reason ->
+ {:error, inspect(reason)}
+
+ :throw, reason ->
+ {:error, inspect(reason)}
+ end
+
+ case result do
+ {:ok, chunk_result} ->
+ send(live_view_pid, {:chunk_done, idx, chunk_result})
+
+ {:error, reason} ->
+ send(live_view_pid, {:chunk_error, idx, reason})
+ end
+ end
+
+ # Starts async task to process a chunk of CSV rows.
+ #
+ # In tests (SQL sandbox mode), runs synchronously to avoid Ecto Sandbox issues.
+ @spec start_chunk_processing_task(
+ Phoenix.LiveView.Socket.t(),
+ map(),
+ map(),
+ non_neg_integer()
+ ) :: {:noreply, Phoenix.LiveView.Socket.t()}
+ defp start_chunk_processing_task(socket, import_state, progress, idx) do
+ chunk = Enum.at(import_state.chunks, idx)
+ actor = ensure_actor_loaded(socket)
+ live_view_pid = self()
+
+ # Process chunk with existing error count for capping
+ opts = [
+ custom_field_lookup: import_state.custom_field_lookup,
+ existing_error_count: length(progress.errors),
+ max_errors: @max_errors,
+ actor: actor
+ ]
+
+ # Get locale from socket for translations in background tasks
+ locale = socket.assigns[:locale] || "de"
+ Gettext.put_locale(MvWeb.Gettext, locale)
+
+ if Config.sql_sandbox?() do
+ # Run synchronously in tests to avoid Ecto Sandbox issues with async tasks
+ # In test mode, send the message - it will be processed when render() is called
+ # in the test. The test helper wait_for_import_completion() handles message processing
+ process_chunk_with_error_handling(
+ chunk,
+ import_state.column_map,
+ import_state.custom_field_map,
+ opts,
+ live_view_pid,
+ idx
+ )
+ else
+ # Start async task to process chunk in production
+ # Use start_child for fire-and-forget: no monitor, no Task messages
+ # We only use our own send/2 messages for communication
+ Task.Supervisor.start_child(Mv.TaskSupervisor, fn ->
+ # Set locale in task process for translations
+ Gettext.put_locale(MvWeb.Gettext, locale)
+
+ process_chunk_with_error_handling(
+ chunk,
+ import_state.column_map,
+ import_state.custom_field_map,
+ opts,
+ live_view_pid,
+ idx
+ )
+ end)
+ end
+
+ {:noreply, socket}
+ end
+
+ # Handles chunk processing result from async task and schedules the next chunk.
+ @spec handle_chunk_result(
+ Phoenix.LiveView.Socket.t(),
+ map(),
+ map(),
+ non_neg_integer(),
+ map()
+ ) :: {:noreply, Phoenix.LiveView.Socket.t()}
+ defp handle_chunk_result(socket, import_state, progress, idx, chunk_result) do
+ # Merge progress
+ new_progress = merge_progress(progress, chunk_result, idx)
+
+ socket =
+ socket
+ |> assign(:import_progress, new_progress)
+ |> assign(:import_status, new_progress.status)
+
+ # Schedule next chunk or mark as done
+ socket = schedule_next_chunk(socket, idx, length(import_state.chunks))
+
+ {:noreply, socket}
+ end
+
+ # Handles chunk processing errors and updates socket with error status.
+ @spec handle_chunk_error(
+ Phoenix.LiveView.Socket.t(),
+ :invalid_index | :missing_state | :processing_failed,
+ non_neg_integer(),
+ any()
+ ) :: {:noreply, Phoenix.LiveView.Socket.t()}
+ defp handle_chunk_error(socket, error_type, idx, reason \\ nil) do
+ error_message =
+ case error_type do
+ :invalid_index ->
+ gettext("Invalid chunk index: %{idx}", idx: idx)
+
+ :missing_state ->
+ gettext("Import state is missing. Cannot process chunk %{idx}.", idx: idx)
+
+ :processing_failed ->
+ gettext("Failed to process chunk %{idx}: %{reason}",
+ idx: idx,
+ reason: inspect(reason)
+ )
+ end
+
+ socket =
+ socket
+ |> assign(:import_status, :error)
+ |> put_flash(:error, error_message)
+
+ {:noreply, socket}
+ end
+
+ # Consumes uploaded CSV file entries and reads the file content.
+ #
+ # Returns the file content as a binary string or an error tuple.
+ @spec consume_and_read_csv(Phoenix.LiveView.Socket.t()) ::
+ {:ok, String.t()} | {:error, String.t()}
+ defp consume_and_read_csv(socket) do
+ raw = consume_uploaded_entries(socket, :csv_file, &read_file_entry/2)
+
+ case raw do
+ [{:ok, content}] when is_binary(content) ->
+ {:ok, content}
+
+ # Phoenix LiveView test (render_upload) can return raw content list when callback return is treated as value
+ [content] when is_binary(content) ->
+ {:ok, content}
+
+ [{:error, reason}] ->
+ {:error, gettext("Failed to read file: %{reason}", reason: reason)}
+
+ [] ->
+ {:error, gettext("No file was uploaded")}
+
+ _other ->
+ {:error, gettext("Failed to read uploaded file: unexpected format")}
+ end
+ end
+
+ # Reads a single file entry from the uploaded path
+ @spec read_file_entry(map(), map()) :: {:ok, String.t()} | {:error, String.t()}
+ defp read_file_entry(%{path: path}, _entry) do
+ case File.read(path) do
+ {:ok, content} ->
+ {:ok, content}
+
+ {:error, reason} when is_atom(reason) ->
+ # POSIX error atoms (e.g., :enoent) need to be formatted
+ {:error, :file.format_error(reason)}
+
+ {:error, %File.Error{reason: reason}} ->
+ # File.Error struct with reason atom
+ {:error, :file.format_error(reason)}
+
+ {:error, reason} ->
+ # Fallback for other error types
+ {:error, Exception.message(reason)}
+ end
+ end
+
+ # Merges chunk processing results into the overall import progress.
+ #
+ # Handles error capping, warning merging, and status updates.
+ @spec merge_progress(map(), map(), non_neg_integer()) :: map()
+ defp merge_progress(progress, chunk_result, current_chunk_idx) do
+ # Merge errors with cap of @max_errors overall
+ all_errors = progress.errors ++ chunk_result.errors
+ new_errors = Enum.take(all_errors, @max_errors)
+ errors_truncated? = length(all_errors) > @max_errors
+
+ # Merge warnings (optional dedupe - simple append for now)
+ new_warnings = progress.warnings ++ Map.get(chunk_result, :warnings, [])
+
+ # Update status based on whether we're done
+ # current_chunk_idx is 0-based, so after processing chunk 0, we've processed 1 chunk
+ chunks_processed = current_chunk_idx + 1
+ new_status = if chunks_processed >= progress.total_chunks, do: :done, else: :running
+
+ %{
+ inserted: progress.inserted + chunk_result.inserted,
+ failed: progress.failed + chunk_result.failed,
+ errors: new_errors,
+ warnings: new_warnings,
+ status: new_status,
+ current_chunk: chunks_processed,
+ total_chunks: progress.total_chunks,
+ errors_truncated?: errors_truncated? || chunk_result.errors_truncated?
+ }
+ end
+
+ # Schedules the next chunk for processing or marks import as complete.
+ @spec schedule_next_chunk(Phoenix.LiveView.Socket.t(), non_neg_integer(), non_neg_integer()) ::
+ Phoenix.LiveView.Socket.t()
+ defp schedule_next_chunk(socket, current_idx, total_chunks) do
+ next_idx = current_idx + 1
+
+ if next_idx < total_chunks do
+ # Schedule next chunk
+ send(self(), {:process_chunk, next_idx})
+ socket
+ else
+ # All chunks processed - status already set to :done in merge_progress
+ socket
+ end
+ end
+
+ # Determines if the import button should be disabled based on import status and upload state
+ @spec import_button_disabled?(:idle | :running | :done | :error, [map()]) :: boolean()
+ defp import_button_disabled?(:running, _entries), do: true
+ defp import_button_disabled?(_status, []), do: true
+ defp import_button_disabled?(_status, [entry | _]) when not entry.done?, do: true
+ defp import_button_disabled?(_status, _entries), do: false
+
+ # Ensures the actor (user with role) is loaded from socket assigns.
+ #
+ # Note: on_mount :ensure_user_role_loaded already guarantees the role is loaded,
+ # so this is primarily for clarity and defensive programming.
+ @spec ensure_actor_loaded(Phoenix.LiveView.Socket.t()) :: Mv.Accounts.User.t() | nil
+ defp ensure_actor_loaded(socket) do
+ user = socket.assigns[:current_user]
+ # on_mount already ensures role is loaded, but we keep this for clarity
+ Actor.ensure_loaded(user)
+ end
+end
diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex
index 2cbd6ab..b5bc616 100644
--- a/lib/mv_web/router.ex
+++ b/lib/mv_web/router.ex
@@ -88,6 +88,9 @@ defmodule MvWeb.Router do
live "/admin/roles/:id", RoleLive.Show, :show
live "/admin/roles/:id/edit", RoleLive.Form, :edit
+ # Import/Export (Admin only)
+ live "/admin/import-export", ImportExportLive
+
post "/set_locale", LocaleController, :set_locale
end
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po
index cdcc9ff..377c992 100644
--- a/priv/gettext/de/LC_MESSAGES/auth.po
+++ b/priv/gettext/de/LC_MESSAGES/auth.po
@@ -67,7 +67,7 @@ msgstr "Das Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "An account with email %{email} already exists. Please enter your password to link your OIDC account."
-msgstr "Ein Konto mit der E-Mail %{email} existiert bereits. Bitte geben Sie Ihr Passwort ein, um Ihr OIDC-Konto zu verknüpfen."
+msgstr "Ein Konto mit der E-Mail %{email} existiert bereits. Bitte gib dein Passwort ein, um dein OIDC-Konto zu verknüpfen."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
@@ -77,12 +77,12 @@ msgstr "Abbrechen"
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Incorrect password. Please try again."
-msgstr "Falsches Passwort. Bitte versuchen Sie es erneut."
+msgstr "Falsches Passwort. Bitte versuche es erneut."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Invalid session. Please try again."
-msgstr "Ungültige Sitzung. Bitte versuchen Sie es erneut."
+msgstr "Ungültige Sitzung. Bitte versuche es erneut."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
@@ -102,32 +102,32 @@ msgstr "Verknüpfen..."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Session expired. Please try again."
-msgstr "Sitzung abgelaufen. Bitte versuchen Sie es erneut."
+msgstr "Sitzung abgelaufen. Bitte versuche es erneut."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Your OIDC account has been successfully linked! Redirecting to complete sign-in..."
-msgstr "Ihr OIDC-Konto wurde erfolgreich verknüpft! Sie werden zur Anmeldung weitergeleitet..."
+msgstr "Dein OIDC-Konto wurde erfolgreich verknüpft! Du wirst zur Anmeldung weitergeleitet..."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Account activated! Redirecting to complete sign-in..."
-msgstr "Konto aktiviert! Sie werden zur Anmeldung weitergeleitet..."
+msgstr "Konto aktiviert! Du wirst zur Anmeldung weitergeleitet..."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to link account. Please try again or contact support."
-msgstr "Verknüpfung des Kontos fehlgeschlagen. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support."
+msgstr "Verknüpfung des Kontos fehlgeschlagen. Bitte versuche es erneut oder kontaktiere den Support."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "The email address from your OIDC provider is already registered to another account. Please change your email in the identity provider or contact support."
-msgstr "Die E-Mail-Adresse aus Ihrem OIDC-Provider ist bereits für ein anderes Konto registriert. Bitte ändern Sie Ihre E-Mail-Adresse im Identity-Provider oder kontaktieren Sie den Support."
+msgstr "Die E-Mail-Adresse aus deinem OIDC-Provider ist bereits für ein anderes Konto registriert. Bitte ändere deine E-Mail-Adresse im Identity-Provider oder kontaktiere den Support."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
msgid "This OIDC account is already linked to another user. Please contact support."
-msgstr "Dieses OIDC-Konto ist bereits mit einem anderen Benutzer verknüpft. Bitte kontaktieren Sie den Support."
+msgstr "Dieses OIDC-Konto ist bereits mit einem anderen Benutzer verknüpft. Bitte kontaktiere den Support."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
#, elixir-autogen, elixir-format
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index b732c4a..90dddc8 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -239,27 +239,27 @@ msgstr "Mitglied wurde erfolgreich %{action}"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "You are now signed in"
-msgstr "Sie sind jetzt angemeldet"
+msgstr "Du bist jetzt angemeldet"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "You are now signed out"
-msgstr "Sie sind jetzt abgemeldet"
+msgstr "Du bist jetzt abgemeldet"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n"
-msgstr "Sie haben sich bereits auf andere Weise angemeldet, aber Ihr Konto noch nicht bestätigt.\nSie können Ihr Konto über den Link bestätigen, den wir Ihnen gesendet haben, oder durch Zurücksetzen Ihres Passworts.\n"
+msgstr "Du hast dich bereits auf andere Weise angemeldet, aber dein Konto noch nicht bestätigt.\nDu kannst dein Konto über den Link bestätigen, den wir dir gesendet haben, oder durch Zurücksetzen deines Passworts.\n"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Your email address has now been confirmed"
-msgstr "Ihre E-Mail-Adresse wurde bestätigt"
+msgstr "Deine E-Mail-Adresse wurde bestätigt"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Your password has successfully been reset"
-msgstr "Ihr Passwort wurde erfolgreich zurückgesetzt"
+msgstr "Dein Passwort wurde erfolgreich zurückgesetzt"
#: lib/mv_web/live/custom_field_live/form_component.ex
#: lib/mv_web/live/custom_field_live/index_component.ex
@@ -399,7 +399,7 @@ msgstr "Dies ist ein Benutzer*innen-Datensatz aus Ihrer Datenbank."
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Use this form to manage user records in your database."
-msgstr "Verwenden Sie dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
+msgstr "Verwende dieses Formular, um Benutzer*innen-Datensätze zu verwalten."
#: lib/mv_web/live/user_live/form.ex
#: lib/mv_web/live/user_live/show.ex
@@ -439,7 +439,7 @@ msgstr "Administrator*innen-Hinweis"
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "As an administrator, you can directly set a new password for this user using the same secure Ash Authentication system."
-msgstr "Als Administrator*in können Sie direkt ein neues Passwort für diese*n Benutzer*in setzen."
+msgstr "Als Administrator*in kannst du direkt ein neues Passwort für diese*n Benutzer*in setzen."
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
@@ -454,7 +454,7 @@ msgstr "Passwort ändern"
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Check 'Change Password' above to set a new password for this user."
-msgstr "Aktivieren Sie 'Passwort ändern' oben, um ein neues Passwort für diese*n Benutzer*in zu setzen."
+msgstr "Aktiviere 'Passwort ändern' oben, um ein neues Passwort für diese*n Benutzer*in zu setzen."
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
@@ -500,7 +500,7 @@ msgstr "Passwort setzen"
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "User will be created without a password. Check 'Set Password' to add one."
-msgstr "Benutzer*in wird ohne Passwort erstellt. Aktivieren Sie 'Passwort setzen', um eines hinzuzufügen."
+msgstr "Benutzer*in wird ohne Passwort erstellt. Aktiviere 'Passwort setzen', um eines hinzuzufügen."
#: lib/mv_web/live/user_live/form.ex
#: lib/mv_web/live/user_live/index.html.heex
@@ -570,27 +570,27 @@ msgstr "Vorname"
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "An account with this email already exists. Please verify your password to link your OIDC account."
-msgstr "Ein Konto mit dieser E-Mail existiert bereits. Bitte verifizieren Sie Ihr Passwort, um Ihr OIDC-Konto zu verknüpfen."
+msgstr "Ein Konto mit dieser E-Mail existiert bereits. Bitte verifiziere dein Passwort, um dein OIDC-Konto zu verknüpfen."
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Unable to authenticate with OIDC. Please try again."
-msgstr "OIDC-Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut."
+msgstr "OIDC-Authentifizierung fehlgeschlagen. Bitte versuche es erneut."
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Unable to sign in. Please try again."
-msgstr "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
+msgstr "Anmeldung fehlgeschlagen. Bitte versuche es erneut."
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Authentication failed. Please try again."
-msgstr "Authentifizierung fehlgeschlagen. Bitte versuchen Sie es erneut."
+msgstr "Authentifizierung fehlgeschlagen. Bitte versuche es erneut."
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
msgid "Cannot update email: This email is already registered to another account. Please change your email in the identity provider."
-msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits für ein anderes Konto registriert. Bitte ändern Sie Ihre E-Mail-Adresse im Identity-Provider."
+msgstr "E-Mail kann nicht aktualisiert werden: Diese E-Mail-Adresse ist bereits für ein anderes Konto registriert. Bitte ändere deine E-Mail-Adresse im Identity-Provider."
#: lib/mv_web/controllers/auth_controller.ex
#, elixir-autogen, elixir-format
@@ -668,7 +668,7 @@ msgstr "Einstellungen erfolgreich gespeichert"
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "A member with this email already exists. To link with a different member, please change one of the email addresses first."
-msgstr "Ein Mitglied mit dieser E-Mail-Adresse existiert bereits. Um mit einem anderen Mitglied zu verknüpfen, ändern Sie bitte zuerst eine der E-Mail-Adressen."
+msgstr "Ein Mitglied mit dieser E-Mail-Adresse existiert bereits. Um mit einem anderen Mitglied zu verknüpfen, ändere bitte zuerst eine der E-Mail-Adressen."
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
@@ -1072,7 +1072,7 @@ msgstr "Ein Fehler ist aufgetreten"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete this cycle?"
-msgstr "Möchten Sie diesen Zyklus wirklich löschen?"
+msgstr "Möchtest du diesen Zyklus wirklich löschen?"
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -1092,7 +1092,7 @@ msgstr "Die Änderung des Betrags betrifft %{count} Mitglied(er)."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Click to edit amount"
-msgstr "Klicken Sie, um den Betrag zu bearbeiten"
+msgstr "Klicke, um den Betrag zu bearbeiten"
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1412,7 +1412,7 @@ msgstr "Zahlungsintervall"
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
msgid "Please confirm the amount change first"
-msgstr "Bitte bestätigen Sie zuerst die Betragsänderung"
+msgstr "Bitte bestätige zuerst die Betragsänderung"
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1442,7 +1442,7 @@ msgstr "Mitgliedsbeitragsart speichern"
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "Select a membership fee type for this member. Members can only switch between types with the same interval."
-msgstr "Wählen Sie eine Mitgliedsbeitragsart für dieses Mitglied. Mitglieder können nur zwischen Arten mit demselben Intervall wechseln."
+msgstr "Wähle eine Mitgliedsbeitragsart für dieses Mitglied. Mitglieder können nur zwischen Arten mit demselben Intervall wechseln."
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
@@ -1483,12 +1483,12 @@ msgstr "Art"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Type '%{confirmation}' to confirm"
-msgstr "Geben Sie '%{confirmation}' ein, um zu bestätigen"
+msgstr "Gib '%{confirmation}' ein, um zu bestätigen"
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
msgid "Use this form to manage membership fee types in your database."
-msgstr "Verwenden Sie dieses Formular, um Mitgliedsbeitragsarten in Ihrer Datenbank zu verwalten."
+msgstr "Verwende dieses Formular, um Mitgliedsbeitragsarten in deiner Datenbank zu verwalten."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
@@ -1499,7 +1499,7 @@ msgstr "Warnung"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "Warning: Changing from %{old_interval} to %{new_interval} is not allowed. Please select a membership fee type with the same interval."
-msgstr "Warnung: Wechsel von %{old_interval} zu %{new_interval} ist nicht erlaubt. Bitte wählen Sie eine Mitgliedsbeitragsart mit demselben Intervall."
+msgstr "Warnung: Wechsel von %{old_interval} zu %{new_interval} ist nicht erlaubt. Bitte wähle eine Mitgliedsbeitragsart mit demselben Intervall."
#: lib/mv_web/live/membership_fee_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1623,7 +1623,7 @@ msgstr "Verwalte Benutzer*innen-Rollen und ihre Berechtigungssätze."
#: lib/mv_web/live/role_live/show.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete role. %{count} user(s) are still assigned to this role. Please assign them to another role first."
-msgstr "Rolle kann nicht gelöscht werden. %{count} Benutzer*in(nen) sind dieser Rolle noch zugeordnet. Bitte weisen Sie sie zunächst einer anderen Rolle zu."
+msgstr "Rolle kann nicht gelöscht werden. %{count} Benutzer*in(nen) sind dieser Rolle noch zugeordnet. Bitte weise sie zunächst einer anderen Rolle zu."
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
@@ -1746,7 +1746,7 @@ msgstr "Sidebar umschalten"
#: lib/mv_web/live/role_live/form.ex
#, elixir-autogen, elixir-format
msgid "Use this form to manage roles in your database."
-msgstr "Verwenden Sie dieses Formular, um Rollen in Ihrer Datenbank zu verwalten."
+msgstr "Verwende dieses Formular, um Rollen in deiner Datenbank zu verwalten."
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
@@ -1776,7 +1776,7 @@ msgstr "read_only - Lesezugriff auf alle Daten"
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to %{action} members."
-msgstr "Sie haben keine Berechtigung, Mitglieder zu %{action}."
+msgstr "Du hast keine Berechtigung, Mitglieder zu %{action}."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
@@ -1821,22 +1821,22 @@ msgstr "Benutzer*in nicht gefunden"
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this membership fee type"
-msgstr "Sie haben keine Berechtigung, auf diese Mitgliedsbeitragsart zuzugreifen"
+msgstr "Du hast keine Berechtigung, auf diese Mitgliedsbeitragsart zuzugreifen"
#: lib/mv_web/live/user_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this user"
-msgstr "Sie haben keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
+msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to delete this membership fee type"
-msgstr "Sie haben keine Berechtigung, diese Mitgliedsbeitragsart zu löschen"
+msgstr "Du hast keine Berechtigung, diese Mitgliedsbeitragsart zu löschen"
#: lib/mv_web/live/user_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to delete this user"
-msgstr "Sie haben keine Berechtigung, diese*n Benutzer*in zu löschen"
+msgstr "Du hast keine Berechtigung, diese*n Benutzer*in zu löschen"
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1848,7 +1848,7 @@ msgstr "erstellt"
msgid "updated"
msgstr "aktualisiert"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@@ -1867,12 +1867,12 @@ msgstr "Mitglied nicht gefunden"
#: lib/mv_web/live/member_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this member"
-msgstr "Sie haben keine Berechtigung, auf dieses Mitglied zuzugreifen"
+msgstr "Du hast keine Berechtigung, auf dieses Mitglied zuzugreifen"
#: lib/mv_web/live/member_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to delete this member"
-msgstr "Sie haben keine Berechtigung, dieses Mitglied zu löschen"
+msgstr "Du hast keine Berechtigung, dieses Mitglied zu löschen"
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
@@ -1922,17 +1922,17 @@ msgstr "Fehler beim %{action} des Mitglieds."
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "Failed to save member. Please try again."
-msgstr "Fehler beim Speichern des Mitglieds. Bitte versuchen Sie es erneut."
+msgstr "Fehler beim Speichern des Mitglieds. Bitte versuche es erneut."
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "Please correct the errors in the form and try again."
-msgstr "Bitte korrigieren Sie die Fehler im Formular und versuchen Sie es erneut."
+msgstr "Bitte korrigiere die Fehler im Formular und versuche es erneut."
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
msgid "Validation failed. Please check your input."
-msgstr "Validierung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingabe."
+msgstr "Validierung fehlgeschlagen. Bitte überprüfe deine Eingabe."
#: lib/mv_web/live/member_live/form.ex
#, elixir-autogen, elixir-format
@@ -1969,147 +1969,137 @@ msgstr "Bezahlstatus"
msgid "Reset"
msgstr "Zurücksetzen"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr " (Datenfeld: %{field})"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr "CSV Datei"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr "CSV Vorlagen herunterladen:"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr "Englische Vorlage"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr "Liste der Fehler auf %{count} Einträge reduziert"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr "Fehler"
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to prepare CSV import: %{error}"
-msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{error}"
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr "Das Vorbereiten des CSV Imports ist gescheitert: %{reason}"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to process chunk %{idx}: %{reason}"
msgstr "Das Importieren von %{idx} ist gescheitert: %{reason}"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read file: %{reason}"
msgstr "Fehler beim Lesen der Datei: %{reason}"
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to read uploaded file"
-msgstr "Fehler beim Lesen der hochgeladenen Datei"
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr "Fehlgeschlagen: %{count} Zeile(n)"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr "Deutsche Vorlage"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr "Mitglieder importieren (CSV)"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr "Import-Ergebnisse"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
-msgstr "Import läuft bereits. Bitte warten Sie, bis er abgeschlossen ist."
+msgstr "Import läuft bereits. Bitte warte, bis er abgeschlossen ist."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import state is missing. Cannot process chunk %{idx}."
msgstr "Import-Status fehlt. Chunk %{idx} kann nicht verarbeitet werden."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Invalid chunk index: %{idx}"
msgstr "Ungültiger Chunk-Index: %{idx}"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr "Zeile %{line}: %{message}"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "No file was uploaded"
msgstr "Es wurde keine Datei hochgeladen"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr "Nur Administrator*innen können Mitglieder aus CSV-Dateien importieren."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Please select a CSV file to import."
-msgstr "Bitte wählen Sie eine CSV-Datei zum Importieren."
+msgstr "Bitte wähle eine CSV-Datei zum Importieren."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
-msgstr "Bitte warten Sie, bis der Datei-Upload abgeschlossen ist, bevor Sie den Import starten."
+msgstr "Bitte warte, bis der Datei-Upload abgeschlossen ist, bevor du den Import startest."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr "Verarbeite Chunk %{current} von %{total}..."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr "Import starten"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr "Import wird gestartet..."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr "Erfolgreich eingefügt: %{count} Mitglied(er)"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr "Zusammenfassung"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Warnings"
msgstr "Warnungen"
@@ -2255,9 +2245,9 @@ msgstr "Nicht berechtigt."
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions."
-msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfen Sie Ihre Berechtigungen."
+msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
msgstr "Nur CSV Dateien, maximal %{size} MB"
@@ -2282,20 +2272,51 @@ msgstr "Datenfeld: %{name} – erwartet %{type} %{details}, erhalten: %{value}"
msgid "custom_field: %{name} – expected %{type}, got: %{value}"
msgstr "Datenfeld: %{name} – erwartet %{type}, erhalten: %{value}"
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Manage Memberdata"
-msgstr "Mitgliederdaten verwalten"
-
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of memberdate (like e-mail or first name). Unknown data field columns will be ignored with a warning."
-msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Datenfelder müssen in Mila bereits angelegt sein, damit sie importiert werden können. Sie müssen in der Liste der Mitgliederdaten als Datenfeld enthalten sein (z.B. E-Mail). Spalten mit unbekannten Spaltenüberschriften werden mit einer Warnung ignoriert."
-
#: lib/mv/membership/import/member_csv.ex
#, elixir-autogen, elixir-format
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
-msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstellen Sie es in Mila vor dem Import."
+msgstr "Unbekannte Spalte '%{header}' wird ignoriert. Falls dies ein Datenfeld ist, erstelle es in Mila vor dem Import."
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Export Members (CSV)"
+msgstr "Mitglieder importieren (CSV)"
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Export functionality will be available in a future release."
+msgstr "Export-Funktionalität ist im nächsten release verfügbar."
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Failed to read uploaded file: unexpected format"
+msgstr "Fehler beim Lesen der hochgeladenen Datei"
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import members from CSV files or export member data."
+msgstr "Importiere Mitglieder aus CSV-Dateien oder exportiere Mitgliederdaten."
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import/Export"
+msgstr "Import/Export"
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "You do not have permission to access this page."
+msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Manage Member Data"
+msgstr "Mitgliederdaten verwalten"
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
+msgstr "Verwende die Namen der Datenfelder als Spaltennamen in der CSV Datei. Datenfelder müssen in Mila bereits angelegt sein, damit sie importiert werden können. sie müssen in der Liste der Mitgliederdaten als Datenfeld enthalten sein (z.B. E-Mail). Spalten mit unbekannten Spaltenüberschriften werden mit einer Warnung ignoriert."
#: lib/mv/membership/member/validations/email_change_permission.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -2343,3 +2364,8 @@ msgstr "SSO-/OIDC-Benutzer*in"
#, elixir-autogen, elixir-format, fuzzy
msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
msgstr "Dieser*e Benutzer*in ist per SSO (Single Sign-On) angebunden. Ein hier gesetztes oder geändertes Passwort betrifft nur die Anmeldung mit E-Mail und Passwort in dieser Anwendung. Es ändert nicht das Passwort beim Identity-Provider (z. B. Authentik). Zum Ändern des SSO-Passworts nutzen Sie den Identity-Provider oder die IT Ihrer Organisation."
+
+#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#~ #, elixir-autogen, elixir-format
+#~ msgid "Only administrators can regenerate cycles"
+#~ msgstr "Nur Administrator*innen können Zyklen regenerieren"
diff --git a/priv/gettext/de/LC_MESSAGES/errors.po b/priv/gettext/de/LC_MESSAGES/errors.po
index b1d359a..b1bdeea 100644
--- a/priv/gettext/de/LC_MESSAGES/errors.po
+++ b/priv/gettext/de/LC_MESSAGES/errors.po
@@ -123,7 +123,7 @@ msgstr "muss vorhanden sein"
## Custom validation messages from Mv.Accounts.User
msgid "User already has a member. Remove existing member first."
-msgstr "Benutzer*in hat bereits ein Mitglied. Entfernen Sie zuerst das vorhandene Mitglied."
+msgstr "Benutzer*in hat bereits ein Mitglied. Entferne zuerst das vorhandene Mitglied."
msgid "OIDC user_info must contain a non-empty 'sub' or 'id' field"
msgstr "OIDC user_info darf kein leeres 'sub' oder 'id' Feld enthalten"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 3c147ba..ace001a 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -1849,7 +1849,7 @@ msgstr ""
msgid "updated"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@@ -1970,147 +1970,137 @@ msgstr ""
msgid "Reset"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to prepare CSV import: %{error}"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to process chunk %{idx}: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to read file: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to read uploaded file"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import state is missing. Cannot process chunk %{idx}."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Invalid chunk index: %{idx}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "No file was uploaded"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Please select a CSV file to import."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Warnings"
msgstr ""
@@ -2258,7 +2248,7 @@ msgstr ""
msgid "Could not load data fields. Please check your permissions."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "CSV files only, maximum %{size} MB"
msgstr ""
@@ -2283,21 +2273,52 @@ msgstr ""
msgid "custom_field: %{name} – expected %{type}, got: %{value}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Manage Memberdata"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of memberdate (like e-mail or first name). Unknown data field columns will be ignored with a warning."
-msgstr ""
-
#: lib/mv/membership/import/member_csv.ex
#, elixir-autogen, elixir-format
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr ""
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Export Members (CSV)"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Export functionality will be available in a future release."
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Failed to read uploaded file: unexpected format"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import members from CSV files or export member data."
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import/Export"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "You do not have permission to access this page."
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Manage Member Data"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
+msgstr ""
+
#: lib/mv/membership/member/validations/email_change_permission.ex
#, elixir-autogen, elixir-format
msgid "Only administrators or the linked user can change the email for members linked to users"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 7aad814..510909c 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -1849,7 +1849,7 @@ msgstr ""
msgid "updated"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#: lib/mv_web/live/user_live/form.ex
#, elixir-autogen, elixir-format
msgid "Unknown error"
@@ -1970,147 +1970,137 @@ msgstr ""
msgid "Reset"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid " (Field: %{field})"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "CSV File"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Download CSV templates:"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "English Template"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Error list truncated to %{count} entries"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Errors"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to prepare CSV import: %{error}"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to prepare CSV import: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to process chunk %{idx}: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to read file: %{reason}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Failed to read uploaded file"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Failed: %{count} row(s)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "German Template"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Members (CSV)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import Results"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import is already running. Please wait for it to complete."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Import state is missing. Cannot process chunk %{idx}."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Invalid chunk index: %{idx}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Line %{line}: %{message}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "No file was uploaded"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Only administrators can import members from CSV files."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Please select a CSV file to import."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Please wait for the file upload to complete before starting the import."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Processing chunk %{current} of %{total}..."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Start Import"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Starting import..."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Successfully inserted: %{count} member(s)"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format
msgid "Summary"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Warnings"
msgstr ""
@@ -2258,7 +2248,7 @@ msgstr ""
msgid "Could not load data fields. Please check your permissions."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/import_export_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "CSV files only, maximum %{size} MB"
msgstr ""
@@ -2283,21 +2273,52 @@ msgstr ""
msgid "custom_field: %{name} – expected %{type}, got: %{value}"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Manage Memberdata"
-msgstr ""
-
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of memberdate (like e-mail or first name). Unknown data field columns will be ignored with a warning."
-msgstr ""
-
#: lib/mv/membership/import/member_csv.ex
#, elixir-autogen, elixir-format
msgid "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
msgstr "Unknown column '%{header}' will be ignored. If this is a custom field, create it in Mila before importing."
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Export Members (CSV)"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Export functionality will be available in a future release."
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Failed to read uploaded file: unexpected format"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import members from CSV files or export member data."
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format
+msgid "Import/Export"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "You do not have permission to access this page."
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Manage Member Data"
+msgstr ""
+
+#: lib/mv_web/live/import_export_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Use the data field name as the CSV column header in your file. Data fields must exist in Mila before importing, so they must be listed in the list of member data (like e-mail or first name). Unknown data field columns will be ignored with a warning."
+msgstr ""
+
#: lib/mv/membership/member/validations/email_change_permission.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Only administrators or the linked user can change the email for members linked to users"
@@ -2344,3 +2365,8 @@ msgstr ""
#, elixir-autogen, elixir-format, fuzzy
msgid "This user is linked via SSO (Single Sign-On). A password set or changed here only affects login with email and password in this application. It does not change the password in your identity provider (e.g. Authentik). To change the SSO password, use the identity provider or your organization's IT."
msgstr ""
+
+#~ #: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#~ #, elixir-autogen, elixir-format
+#~ msgid "Only administrators can regenerate cycles"
+#~ msgstr ""
diff --git a/test/accounts/user_authentication_test.exs b/test/accounts/user_authentication_test.exs
index e1dde87..d471b30 100644
--- a/test/accounts/user_authentication_test.exs
+++ b/test/accounts/user_authentication_test.exs
@@ -41,18 +41,6 @@ defmodule Mv.Accounts.UserAuthenticationTest do
assert is_nil(found_user.oidc_id)
end
- @tag :test_proposal
- test "password authentication uses email as identity_field" do
- # Verify the configuration: password strategy should use email as identity_field
- # This test checks the AshAuthentication configuration
-
- strategies = AshAuthentication.Info.authentication_strategies(Mv.Accounts.User)
- password_strategy = Enum.find(strategies, fn s -> s.name == :password end)
-
- assert password_strategy != nil
- assert password_strategy.identity_field == :email
- end
-
@tag :test_proposal
test "multiple users can exist with different emails" do
user1 =
diff --git a/test/membership/custom_field_slug_test.exs b/test/membership/custom_field_slug_test.exs
index 76ab5c7..aa8e649 100644
--- a/test/membership/custom_field_slug_test.exs
+++ b/test/membership/custom_field_slug_test.exs
@@ -1,13 +1,14 @@
defmodule Mv.Membership.CustomFieldSlugTest do
@moduledoc """
- Tests for automatic slug generation on CustomField resource.
+ Tests for CustomField slug business rules only.
- This test suite verifies:
- 1. Slugs are automatically generated from the name attribute
- 2. Slugs are unique (cannot have duplicates)
- 3. Slugs are immutable (don't change when name changes)
- 4. Slugs handle various edge cases (unicode, special chars, etc.)
- 5. Slugs can be used for lookups
+ We test our business logic, not Ash/slugify implementation details:
+ - Slug is generated from name on create (one smoke test)
+ - Slug is unique (business rule)
+ - Slug is immutable (does not change when name is updated; cannot be set manually)
+ - Slug cannot be empty (rejects name with only special characters)
+
+ We do not test: slugify edge cases (umlauts, truncation, etc.) or Ash/Ecto struct/load behavior.
"""
use Mv.DataCase, async: true
@@ -18,8 +19,8 @@ defmodule Mv.Membership.CustomFieldSlugTest do
%{actor: system_actor}
end
- describe "automatic slug generation on create" do
- test "generates slug from name with simple ASCII text", %{actor: actor} do
+ describe "slug generation (business rule)" do
+ test "slug is generated from name on create", %{actor: actor} do
{:ok, custom_field} =
CustomField
|> Ash.Changeset.for_create(:create, %{
@@ -30,78 +31,6 @@ defmodule Mv.Membership.CustomFieldSlugTest do
assert custom_field.slug == "mobile-phone"
end
-
- test "generates slug from name with German umlauts", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Café Müller",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "cafe-muller"
- end
-
- test "generates slug with lowercase conversion", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "TEST NAME",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "test-name"
- end
-
- test "generates slug by removing special characters", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "E-Mail & Address!",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "e-mail-address"
- end
-
- test "generates slug by replacing multiple spaces with single hyphen", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Multiple Spaces",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "multiple-spaces"
- end
-
- test "trims leading and trailing hyphens", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "-Test-",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "test"
- end
-
- test "handles unicode characters properly (ß becomes ss)", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Straße",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "strasse"
- end
end
describe "slug uniqueness" do
@@ -248,29 +177,8 @@ defmodule Mv.Membership.CustomFieldSlugTest do
end
end
- describe "slug edge cases" do
- test "handles very long names by truncating slug", %{actor: actor} do
- # Create a name at the maximum length (100 chars)
- long_name = String.duplicate("abcdefghij", 10)
- # 100 characters exactly
-
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: long_name,
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # Slug should be truncated to maximum 100 characters
- assert String.length(custom_field.slug) <= 100
- # Should be the full slugified version since name is exactly 100 chars
- assert custom_field.slug == long_name
- end
-
+ describe "slug cannot be empty (business rule)" do
test "rejects name with only special characters", %{actor: actor} do
- # When name contains only special characters, slug would be empty
- # This should fail validation
assert {:error, %Ash.Error.Invalid{} = error} =
CustomField
|> Ash.Changeset.for_create(:create, %{
@@ -279,107 +187,9 @@ defmodule Mv.Membership.CustomFieldSlugTest do
})
|> Ash.create(actor: actor)
- # Should fail because slug would be empty
error_message = Exception.message(error)
assert error_message =~ "Slug cannot be empty" or error_message =~ "is required"
end
-
- test "handles mixed special characters and text", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Test@#$%Name",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # slugify keeps the hyphen between words
- assert custom_field.slug == "test-name"
- end
-
- test "handles numbers in name", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Field 123 Test",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- assert custom_field.slug == "field-123-test"
- end
-
- test "handles consecutive hyphens in name", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Test---Name",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # Should reduce multiple hyphens to single hyphen
- assert custom_field.slug == "test-name"
- end
-
- test "handles name with dots and underscores", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "test.field_name",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # Dots and underscores should be handled (either kept or converted)
- assert custom_field.slug =~ ~r/^[a-z0-9-]+$/
- end
- end
-
- describe "slug in queries and responses" do
- test "slug is included in struct after create", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Test",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # Slug should be present in the struct
- assert Map.has_key?(custom_field, :slug)
- assert custom_field.slug != nil
- end
-
- test "can load custom field and slug is present", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Test",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- # Load it back
- loaded_custom_field = Ash.get!(CustomField, custom_field.id, actor: actor)
-
- assert loaded_custom_field.slug == "test"
- end
-
- test "slug is returned in list queries", %{actor: actor} do
- {:ok, custom_field} =
- CustomField
- |> Ash.Changeset.for_create(:create, %{
- name: "Test",
- value_type: :string
- })
- |> Ash.create(actor: actor)
-
- custom_fields = Ash.read!(CustomField, actor: actor)
-
- found = Enum.find(custom_fields, &(&1.id == custom_field.id))
- assert found.slug == "test"
- end
end
describe "slug-based lookup (future feature)" do
diff --git a/test/membership/group_test.exs b/test/membership/group_test.exs
index 1c84eeb..724d930 100644
--- a/test/membership/group_test.exs
+++ b/test/membership/group_test.exs
@@ -2,7 +2,7 @@ defmodule Mv.Membership.GroupTest do
@moduledoc """
Tests for Group resource validations, CRUD operations, and relationships.
"""
- use Mv.DataCase, async: true
+ use Mv.DataCase, async: false
alias Mv.Membership
@@ -232,23 +232,7 @@ defmodule Mv.Membership.GroupTest do
end
describe "Relationships & Deletion" do
- test "group has many_to_many members relationship (load with preloading)", %{actor: actor} do
- {:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor)
- {:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor)
-
- {:ok, _mg} =
- Membership.create_member_group(%{member_id: member.id, group_id: group.id},
- actor: actor
- )
-
- # Load group with members
- {:ok, group_with_members} =
- Ash.load(group, :members, actor: actor, domain: Mv.Membership)
-
- assert length(group_with_members.members) == 1
- assert hd(group_with_members.members).id == member.id
- end
-
+ # We test business/data rules (CASCADE), not Ash relationship loading (framework).
test "delete group cascades to member_groups (members remain intact)", %{actor: actor} do
{:ok, group} = Membership.create_group(%{name: "Test Group"}, actor: actor)
{:ok, member} = Membership.create_member(%{email: "test@test.com"}, actor: actor)
diff --git a/test/membership/member_group_test.exs b/test/membership/member_group_test.exs
index b3c048f..4dd4ae8 100644
--- a/test/membership/member_group_test.exs
+++ b/test/membership/member_group_test.exs
@@ -2,7 +2,7 @@ defmodule Mv.Membership.MemberGroupTest do
@moduledoc """
Tests for MemberGroup join table resource - validations and cascade delete behavior.
"""
- use Mv.DataCase, async: true
+ use Mv.DataCase, async: false
alias Mv.Membership
diff --git a/test/membership_fees/membership_fee_type_test.exs b/test/membership_fees/membership_fee_type_test.exs
index 7b6c39a..21f3100 100644
--- a/test/membership_fees/membership_fee_type_test.exs
+++ b/test/membership_fees/membership_fee_type_test.exs
@@ -1,6 +1,10 @@
defmodule Mv.MembershipFees.MembershipFeeTypeTest do
@moduledoc """
- Tests for MembershipFeeType resource.
+ Tests for MembershipFeeType business rules only.
+
+ We test: required fields, allowed interval values, uniqueness, amount constraints,
+ interval immutability, and referential integrity (cannot delete when in use).
+ We do not test: standard CRUD (create/update/delete when no constraints apply).
"""
use Mv.DataCase, async: true
@@ -11,34 +15,7 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
%{actor: system_actor}
end
- describe "create MembershipFeeType" do
- test "can create membership fee type with valid attributes", %{actor: actor} do
- attrs = %{
- name: "Standard Membership",
- amount: Decimal.new("120.00"),
- interval: :yearly,
- description: "Standard yearly membership fee"
- }
-
- assert {:ok, %MembershipFeeType{} = fee_type} =
- Ash.create(MembershipFeeType, attrs, actor: actor)
-
- assert fee_type.name == "Standard Membership"
- assert Decimal.equal?(fee_type.amount, Decimal.new("120.00"))
- assert fee_type.interval == :yearly
- assert fee_type.description == "Standard yearly membership fee"
- end
-
- test "can create membership fee type without description", %{actor: actor} do
- attrs = %{
- name: "Basic",
- amount: Decimal.new("60.00"),
- interval: :monthly
- }
-
- assert {:ok, %MembershipFeeType{}} = Ash.create(MembershipFeeType, attrs, actor: actor)
- end
-
+ describe "create MembershipFeeType - business rules" do
test "requires name", %{actor: actor} do
attrs = %{
amount: Decimal.new("100.00"),
@@ -69,28 +46,24 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
assert error_on_field?(error, :interval)
end
- test "validates interval enum values - monthly", %{actor: actor} do
- attrs = %{name: "Monthly", amount: Decimal.new("10.00"), interval: :monthly}
- assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
- assert fee_type.interval == :monthly
- end
+ test "accepts valid interval values (monthly, quarterly, half_yearly, yearly)", %{
+ actor: actor
+ } do
+ for {interval, name} <- [
+ monthly: "Monthly",
+ quarterly: "Quarterly",
+ half_yearly: "Half Yearly",
+ yearly: "Yearly"
+ ] do
+ attrs = %{
+ name: "#{name} #{System.unique_integer([:positive])}",
+ amount: Decimal.new("10.00"),
+ interval: interval
+ }
- test "validates interval enum values - quarterly", %{actor: actor} do
- attrs = %{name: "Quarterly", amount: Decimal.new("30.00"), interval: :quarterly}
- assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
- assert fee_type.interval == :quarterly
- end
-
- test "validates interval enum values - half_yearly", %{actor: actor} do
- attrs = %{name: "Half Yearly", amount: Decimal.new("60.00"), interval: :half_yearly}
- assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
- assert fee_type.interval == :half_yearly
- end
-
- test "validates interval enum values - yearly", %{actor: actor} do
- attrs = %{name: "Yearly", amount: Decimal.new("120.00"), interval: :yearly}
- assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
- assert fee_type.interval == :yearly
+ assert {:ok, fee_type} = Ash.create(MembershipFeeType, attrs, actor: actor)
+ assert fee_type.interval == interval
+ end
end
test "rejects invalid interval values", %{actor: actor} do
@@ -128,13 +101,13 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
end
end
- describe "update MembershipFeeType" do
+ describe "update MembershipFeeType - business rules" do
setup %{actor: actor} do
{:ok, fee_type} =
Ash.create(
MembershipFeeType,
%{
- name: "Original Name",
+ name: "Original Name #{System.unique_integer([:positive])}",
amount: Decimal.new("100.00"),
interval: :yearly,
description: "Original description"
@@ -145,28 +118,6 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
%{fee_type: fee_type}
end
- test "can update name", %{actor: actor, fee_type: fee_type} do
- assert {:ok, updated} = Ash.update(fee_type, %{name: "Updated Name"}, actor: actor)
- assert updated.name == "Updated Name"
- end
-
- test "can update amount", %{actor: actor, fee_type: fee_type} do
- assert {:ok, updated} = Ash.update(fee_type, %{amount: Decimal.new("150.00")}, actor: actor)
- assert Decimal.equal?(updated.amount, Decimal.new("150.00"))
- end
-
- test "can update description", %{actor: actor, fee_type: fee_type} do
- assert {:ok, updated} =
- Ash.update(fee_type, %{description: "Updated description"}, actor: actor)
-
- assert updated.description == "Updated description"
- end
-
- test "can clear description", %{actor: actor, fee_type: fee_type} do
- assert {:ok, updated} = Ash.update(fee_type, %{description: nil}, actor: actor)
- assert updated.description == nil
- end
-
test "interval immutability: update fails when interval is changed", %{
actor: actor,
fee_type: fee_type
@@ -179,7 +130,7 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
end
end
- describe "delete MembershipFeeType" do
+ describe "delete MembershipFeeType - business rules (referential integrity)" do
setup %{actor: actor} do
{:ok, fee_type} =
Ash.create(
@@ -195,12 +146,6 @@ defmodule Mv.MembershipFees.MembershipFeeTypeTest do
%{fee_type: fee_type}
end
- test "can delete when not in use", %{actor: actor, fee_type: fee_type} do
- result = Ash.destroy(fee_type, actor: actor)
- # Ash.destroy returns :ok or {:ok, _} depending on version
- assert result == :ok or match?({:ok, _}, result)
- end
-
test "cannot delete when members are assigned", %{actor: actor, fee_type: fee_type} do
alias Mv.Membership.Member
diff --git a/test/mv_web/live/global_settings_live_config_test.exs b/test/mv_web/live/global_settings_live_config_test.exs
index 1f06145..73f831f 100644
--- a/test/mv_web/live/global_settings_live_config_test.exs
+++ b/test/mv_web/live/global_settings_live_config_test.exs
@@ -39,9 +39,10 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do
original_config = Application.get_env(:mv, :csv_import, [])
try do
+ # Arrange: Set custom row limit to 500
Application.put_env(:mv, :csv_import, max_rows: 500)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/import-export")
# Generate CSV with 501 rows (exceeding custom limit of 500)
header = "first_name;last_name;email;street;postal_code;city\n"
@@ -53,17 +54,17 @@ defmodule MvWeb.GlobalSettingsLiveConfigTest do
large_csv = header <> Enum.join(rows)
- # Simulate file upload using helper function
+ # Act: Upload CSV and submit form
upload_csv_file(view, large_csv, "too_many_rows_custom.csv")
view
|> form("#csv-upload-form", %{})
|> render_submit()
+ # Assert: Import should be rejected with error message
html = render(view)
# Business rule: import should be rejected when exceeding configured limit
- assert html =~ "exceeds" or html =~ "maximum" or html =~ "limit" or
- html =~ "Failed to prepare"
+ assert html =~ "Failed to prepare CSV import"
after
# Restore original config
Application.put_env(:mv, :csv_import, original_config)
diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs
index 083c813..86680f3 100644
--- a/test/mv_web/live/global_settings_live_test.exs
+++ b/test/mv_web/live/global_settings_live_test.exs
@@ -3,22 +3,6 @@ defmodule MvWeb.GlobalSettingsLiveTest do
import Phoenix.LiveViewTest
alias Mv.Membership
- # Helper function to upload CSV file in tests
- # Reduces code duplication across multiple test cases
- defp upload_csv_file(view, csv_content, filename \\ "test_import.csv") do
- view
- |> file_input("#csv-upload-form", :csv_file, [
- %{
- last_modified: System.system_time(:second),
- name: filename,
- content: csv_content,
- size: byte_size(csv_content),
- type: "text/csv"
- }
- ])
- |> render_upload(filename)
- end
-
describe "Global Settings LiveView" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
@@ -97,595 +81,4 @@ defmodule MvWeb.GlobalSettingsLiveTest do
assert render(view) =~ "updated" or render(view) =~ "success"
end
end
-
- describe "CSV Import Section" do
- test "admin user sees import section", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
-
- # Check for import section heading or identifier
- assert html =~ "Import" or html =~ "CSV" or html =~ "member_import"
- end
-
- test "admin user sees custom fields notice", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
-
- # Check for custom fields notice text
- assert html =~ "Use the data field name"
- end
-
- test "admin user sees template download links", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- html = render(view)
-
- # Check for English template link
- assert html =~ "member_import_en.csv" or html =~ "/templates/member_import_en.csv"
-
- # Check for German template link
- assert html =~ "member_import_de.csv" or html =~ "/templates/member_import_de.csv"
- end
-
- test "template links use static path helper", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- html = render(view)
-
- # Check that links contain the static path pattern
- # Static paths typically start with /templates/ or contain the full path
- assert html =~ "/templates/member_import_en.csv" or
- html =~ ~r/href=["'][^"']*member_import_en\.csv["']/
-
- assert html =~ "/templates/member_import_de.csv" or
- html =~ ~r/href=["'][^"']*member_import_de\.csv["']/
- end
-
- test "admin user sees file upload input", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- html = render(view)
-
- # Check for file input element
- assert html =~ ~r/type=["']file["']/i or html =~ "phx-hook" or html =~ "upload"
- end
-
- test "file upload has CSV-only restriction", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- html = render(view)
-
- # Check for CSV file type restriction in help text or accept attribute
- assert html =~ ~r/\.csv/i or html =~ "CSV" or html =~ ~r/accept=["'][^"']*csv["']/i
- end
-
- test "non-admin user does not see import section", %{conn: conn} do
- # Member (own_data) is redirected when accessing /settings (no page permission)
- member_user = Mv.Fixtures.user_with_role_fixture("own_data")
- conn = MvWeb.ConnCase.conn_with_password_user(conn, member_user)
-
- assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/settings")
- assert to == "/users/#{member_user.id}"
- end
- end
-
- describe "CSV Import - Import" do
- setup %{conn: conn} do
- # Ensure admin user
- admin_user = Mv.Fixtures.user_with_role_fixture("admin")
- conn = MvWeb.ConnCase.conn_with_password_user(conn, admin_user)
-
- # Read valid CSV fixture
- csv_content =
- Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
- |> File.read!()
-
- {:ok, conn: conn, admin_user: admin_user, csv_content: csv_content}
- end
-
- test "admin can upload CSV and start import", %{conn: conn, csv_content: csv_content} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content)
-
- # Trigger start_import event via form submit
- assert view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Check that import has started or shows appropriate message
- html = render(view)
- # Either import started successfully OR we see a specific error (not admin error)
- import_started = html =~ "Import in progress" or html =~ "running" or html =~ "progress"
- no_admin_error = not (html =~ "Only administrators can import")
- # If import failed, it should be a CSV parsing error, not an admin error
- if html =~ "Failed to prepare CSV import" do
- # This is acceptable - CSV might have issues, but admin check passed
- assert no_admin_error
- else
- # Import should have started
- assert import_started or html =~ "CSV File"
- end
- end
-
- test "admin import initializes progress correctly", %{conn: conn, csv_content: csv_content} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content)
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Check that import has started or shows appropriate message
- html = render(view)
- # Either import started successfully OR we see a specific error (not admin error)
- import_started = html =~ "Import in progress" or html =~ "running" or html =~ "progress"
- no_admin_error = not (html =~ "Only administrators can import")
- # If import failed, it should be a CSV parsing error, not an admin error
- if html =~ "Failed to prepare CSV import" do
- # This is acceptable - CSV might have issues, but admin check passed
- assert no_admin_error
- else
- # Import should have started
- assert import_started or html =~ "CSV File"
- end
- end
-
- test "non-admin cannot start import", %{conn: conn} do
- # Member (own_data) is redirected when accessing /settings (no page permission)
- member_user = Mv.Fixtures.user_with_role_fixture("own_data")
- conn = MvWeb.ConnCase.conn_with_password_user(conn, member_user)
-
- assert {:error, {:redirect, %{to: to}}} = live(conn, ~p"/settings")
- assert to == "/users/#{member_user.id}"
- end
-
- test "invalid CSV shows user-friendly error", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Create invalid CSV (missing required fields)
- invalid_csv = "invalid_header\nincomplete_row"
-
- # Simulate file upload using helper function
- upload_csv_file(view, invalid_csv, "invalid.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Check for error message (flash)
- html = render(view)
- assert html =~ "error" or html =~ "failed" or html =~ "Failed to prepare"
- end
-
- @tag :skip
- test "empty CSV shows error", %{conn: conn} do
- # Skip this test - Phoenix LiveView has issues with empty file uploads in tests
- # The error is handled correctly in production, but test framework has limitations
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- empty_csv = " "
- csv_path = Path.join([System.tmp_dir!(), "empty_#{System.unique_integer()}.csv"])
- File.write!(csv_path, empty_csv)
-
- view
- |> file_input("#csv-upload-form", :csv_file, [
- %{
- last_modified: System.system_time(:second),
- name: "empty.csv",
- content: empty_csv,
- size: byte_size(empty_csv),
- type: "text/csv"
- }
- ])
- |> render_upload("empty.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Check for error message
- html = render(view)
- assert html =~ "error" or html =~ "empty" or html =~ "failed" or html =~ "Failed to prepare"
- end
- end
-
- describe "CSV Import - Step 3: Chunk Processing" do
- setup %{conn: conn} do
- # Ensure admin user
- admin_user = Mv.Fixtures.user_with_role_fixture("admin")
- conn = MvWeb.ConnCase.conn_with_password_user(conn, admin_user)
-
- # Read valid CSV fixture
- valid_csv_content =
- Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
- |> File.read!()
-
- # Read invalid CSV fixture
- invalid_csv_content =
- Path.join([__DIR__, "..", "..", "fixtures", "invalid_member_import.csv"])
- |> File.read!()
-
- {:ok,
- conn: conn,
- admin_user: admin_user,
- valid_csv_content: valid_csv_content,
- invalid_csv_content: invalid_csv_content}
- end
-
- test "happy path: valid CSV processes all chunks and shows done status", %{
- conn: conn,
- valid_csv_content: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content)
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for processing to complete
- # In test mode, chunks are processed synchronously and messages are sent via send/2
- # render(view) processes handle_info messages, so we call it multiple times
- # to ensure all messages are processed
- # Use the same approach as "success rendering" test which works
- Process.sleep(1000)
-
- html = render(view)
- # Should show success count (inserted count)
- assert html =~ "Inserted" or html =~ "inserted" or html =~ "2"
- # Should show completed status
- assert html =~ "completed" or html =~ "done" or html =~ "Import completed" or
- has_element?(view, "[data-testid='import-results-panel']")
- end
-
- test "error handling: invalid CSV shows errors with line numbers", %{
- conn: conn,
- invalid_csv_content: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content, "invalid_import.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for chunk processing
- Process.sleep(500)
-
- html = render(view)
- # Should show failure count > 0
- assert html =~ "failed" or html =~ "error" or html =~ "Failed"
-
- # Should show line numbers in errors (from service, not recalculated)
- # Line numbers should be 2, 3 (header is line 1)
- assert html =~ "2" or html =~ "3" or html =~ "line"
- end
-
- test "error cap: many failing rows caps errors at 50", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Generate CSV with 100 invalid rows (all missing email)
- header = "first_name;last_name;email;street;postal_code;city\n"
- invalid_rows = for i <- 1..100, do: "Row#{i};Last#{i};;Street#{i};12345;City#{i}\n"
- large_invalid_csv = header <> Enum.join(invalid_rows)
-
- # Simulate file upload using helper function
- upload_csv_file(view, large_invalid_csv, "large_invalid.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for chunk processing
- Process.sleep(1000)
-
- html = render(view)
- # Should show failed count == 100
- assert html =~ "100" or html =~ "failed"
-
- # Errors should be capped at 50 (but we can't easily check exact count in HTML)
- # The important thing is that processing completes without crashing
- assert html =~ "done" or html =~ "complete" or html =~ "finished"
- end
-
- test "chunk scheduling: progress updates show chunk processing", %{
- conn: conn,
- valid_csv_content: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content)
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait a bit for processing to start
- Process.sleep(200)
-
- # Check that status area exists (with aria-live for accessibility)
- html = render(view)
-
- assert html =~ "aria-live" or html =~ "status" or html =~ "progress" or
- html =~ "Processing" or html =~ "chunk"
-
- # Final state should be :done
- Process.sleep(500)
- final_html = render(view)
- assert final_html =~ "done" or final_html =~ "complete" or final_html =~ "finished"
- end
- end
-
- describe "CSV Import - Step 4: Results UI" do
- setup %{conn: conn} do
- # Ensure admin user
- admin_user = Mv.Fixtures.user_with_role_fixture("admin")
- conn = MvWeb.ConnCase.conn_with_password_user(conn, admin_user)
-
- # Read valid CSV fixture
- valid_csv_content =
- Path.join([__DIR__, "..", "..", "fixtures", "valid_member_import.csv"])
- |> File.read!()
-
- # Read invalid CSV fixture
- invalid_csv_content =
- Path.join([__DIR__, "..", "..", "fixtures", "invalid_member_import.csv"])
- |> File.read!()
-
- # Read CSV with unknown custom field
- unknown_custom_field_csv =
- Path.join([__DIR__, "..", "..", "fixtures", "csv_with_unknown_custom_field.csv"])
- |> File.read!()
-
- {:ok,
- conn: conn,
- admin_user: admin_user,
- valid_csv_content: valid_csv_content,
- invalid_csv_content: invalid_csv_content,
- unknown_custom_field_csv: unknown_custom_field_csv}
- end
-
- test "success rendering: valid CSV shows success count", %{
- conn: conn,
- valid_csv_content: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content)
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for processing to complete
- Process.sleep(1000)
-
- html = render(view)
- # Should show success count (inserted count)
- assert html =~ "Inserted" or html =~ "inserted" or html =~ "2"
- # Should show completed status
- assert html =~ "completed" or html =~ "done" or html =~ "Import completed"
- end
-
- test "error rendering: invalid CSV shows failure count and error list with line numbers", %{
- conn: conn,
- invalid_csv_content: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate file upload using helper function
- upload_csv_file(view, csv_content, "invalid_import.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for processing
- Process.sleep(1000)
-
- html = render(view)
- # Should show failure count
- assert html =~ "Failed" or html =~ "failed"
-
- # Should show error list with line numbers (from service, not recalculated)
- assert html =~ "Line" or html =~ "line" or html =~ "2" or html =~ "3"
- # Should show error messages
- assert html =~ "error" or html =~ "Error" or html =~ "Errors"
- end
-
- test "warning rendering: CSV with unknown custom field shows warnings block", %{
- conn: conn,
- unknown_custom_field_csv: csv_content
- } do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- csv_path =
- Path.join([System.tmp_dir!(), "unknown_custom_#{System.unique_integer()}.csv"])
-
- File.write!(csv_path, csv_content)
-
- view
- |> file_input("#csv-upload-form", :csv_file, [
- %{
- last_modified: System.system_time(:second),
- name: "unknown_custom.csv",
- content: csv_content,
- size: byte_size(csv_content),
- type: "text/csv"
- }
- ])
- |> render_upload("unknown_custom.csv")
-
- view
- |> form("#csv-upload-form", %{})
- |> render_submit()
-
- # Wait for processing
- Process.sleep(1000)
-
- html = render(view)
- # Should show warnings block (if warnings were generated)
- # Warnings are generated when unknown custom field columns are detected
- # Check if warnings section exists OR if import completed successfully
- has_warnings = html =~ "Warning" or html =~ "warning" or html =~ "Warnings"
- import_completed = html =~ "completed" or html =~ "done" or html =~ "Import Results"
-
- # If warnings exist, they should contain the column name
- if has_warnings do
- assert html =~ "UnknownCustomField" or html =~ "unknown" or html =~ "Unknown column" or
- html =~ "will be ignored"
- end
-
- # Import should complete (either with or without warnings)
- assert import_completed
- end
-
- test "A11y: file input has label", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
-
- # Check for label associated with file input
- assert html =~ ~r/