refactor: adds schemales changeset and validation constant
This commit is contained in:
parent
14a8417fdf
commit
7da037d81d
6 changed files with 158 additions and 34 deletions
|
|
@ -19,6 +19,8 @@ defmodule Mv.Constants do
|
|||
|
||||
@custom_field_prefix "custom_field_"
|
||||
|
||||
@email_validator_checks [:html_input, :pow]
|
||||
|
||||
def member_fields, do: @member_fields
|
||||
|
||||
@doc """
|
||||
|
|
@ -30,4 +32,23 @@ defmodule Mv.Constants do
|
|||
"custom_field_"
|
||||
"""
|
||||
def custom_field_prefix, do: @custom_field_prefix
|
||||
|
||||
@doc """
|
||||
Returns the email validator checks used for EctoCommons.EmailValidator.
|
||||
|
||||
We use both `:html_input` and `:pow` checks:
|
||||
- `:html_input` - Pragmatic validation matching browser `<input type="email">` behavior
|
||||
- `:pow` - Stricter validation following email spec, supports internationalization (Unicode)
|
||||
|
||||
Using both ensures:
|
||||
- Compatibility with common email providers (html_input)
|
||||
- Compliance with email standards (pow)
|
||||
- Support for international email addresses (pow)
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Mv.Constants.email_validator_checks()
|
||||
[:html_input, :pow]
|
||||
"""
|
||||
def email_validator_checks, do: @email_validator_checks
|
||||
end
|
||||
|
|
|
|||
|
|
@ -334,45 +334,80 @@ defmodule Mv.Membership.Import.MemberCSV do
|
|||
member_attrs = Map.get(row_map, :member, %{})
|
||||
custom_attrs = Map.get(row_map, :custom, %{})
|
||||
|
||||
# Trim all string values in member map
|
||||
trimmed_member = trim_string_values(member_attrs)
|
||||
# Validate email using schemaless changeset
|
||||
changeset =
|
||||
{%{}, %{email: :string}}
|
||||
|> Ecto.Changeset.cast(%{email: Map.get(member_attrs, :email)}, [:email])
|
||||
|> Ecto.Changeset.update_change(:email, &String.trim/1)
|
||||
|> Ecto.Changeset.validate_required([:email])
|
||||
|> EctoCommons.EmailValidator.validate_email(:email,
|
||||
checks: Mv.Constants.email_validator_checks()
|
||||
)
|
||||
|
||||
# Validate email presence (after trim)
|
||||
email = Map.get(trimmed_member, :email)
|
||||
|
||||
cond do
|
||||
is_nil(email) or email == "" ->
|
||||
{:error,
|
||||
%Error{
|
||||
csv_line_number: csv_line_number,
|
||||
field: :email,
|
||||
message: gettext("Email is required.")
|
||||
}}
|
||||
|
||||
not valid_email_format?(email) ->
|
||||
{:error,
|
||||
%Error{
|
||||
csv_line_number: csv_line_number,
|
||||
field: :email,
|
||||
message: gettext("Email is invalid.")
|
||||
}}
|
||||
|
||||
true ->
|
||||
{:ok, %{member: trimmed_member, custom: custom_attrs}}
|
||||
if changeset.valid? do
|
||||
# Apply trimmed email back to member_attrs
|
||||
trimmed_email = Ecto.Changeset.get_change(changeset, :email)
|
||||
trimmed_member = Map.put(member_attrs, :email, trimmed_email) |> trim_string_values()
|
||||
{:ok, %{member: trimmed_member, custom: custom_attrs}}
|
||||
else
|
||||
# Extract first error
|
||||
error = extract_changeset_error(changeset, csv_line_number)
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
# Validates email format using EctoCommons.EmailValidator
|
||||
defp valid_email_format?(email) when is_binary(email) do
|
||||
changeset =
|
||||
{%{}, %{email: :string}}
|
||||
|> Ecto.Changeset.cast(%{email: email}, [:email])
|
||||
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
|
||||
# Extracts the first error from a changeset and converts it to a MemberCSV.Error struct
|
||||
defp extract_changeset_error(changeset, csv_line_number) do
|
||||
case Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
|
||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||
String.replace(acc, "%{#{key}}", to_string(value))
|
||||
end)
|
||||
end) do
|
||||
%{email: [message | _]} ->
|
||||
# Email-specific error
|
||||
%Error{
|
||||
csv_line_number: csv_line_number,
|
||||
field: :email,
|
||||
message: gettext_error_message(message)
|
||||
}
|
||||
|
||||
changeset.valid?
|
||||
errors when map_size(errors) > 0 ->
|
||||
# Get first error (any field)
|
||||
{field, [message | _]} = Enum.at(Enum.to_list(errors), 0)
|
||||
|
||||
%Error{
|
||||
csv_line_number: csv_line_number,
|
||||
field: String.to_existing_atom(to_string(field)),
|
||||
message: gettext_error_message(message)
|
||||
}
|
||||
|
||||
_ ->
|
||||
# Fallback
|
||||
%Error{
|
||||
csv_line_number: csv_line_number,
|
||||
field: :email,
|
||||
message: gettext("Email is invalid.")
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_email_format?(_), do: false
|
||||
# Maps changeset error messages to appropriate Gettext messages
|
||||
defp gettext_error_message(message) when is_binary(message) do
|
||||
cond do
|
||||
String.contains?(String.downcase(message), "required") or
|
||||
String.contains?(String.downcase(message), "can't be blank") ->
|
||||
gettext("Email is required.")
|
||||
|
||||
String.contains?(String.downcase(message), "invalid") or
|
||||
String.contains?(String.downcase(message), "not a valid") ->
|
||||
gettext("Email is invalid.")
|
||||
|
||||
true ->
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
defp gettext_error_message(_), do: gettext("Email is invalid.")
|
||||
|
||||
# Processes a single row and creates member with custom field values
|
||||
defp process_row(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue