1
Justfile
|
|
@ -9,6 +9,7 @@ migrate-database:
|
|||
|
||||
reset-database:
|
||||
mix ash.reset
|
||||
MIX_ENV=test mix ash.reset
|
||||
|
carla marked this conversation as resolved
Outdated
|
||||
|
||||
seed-database:
|
||||
mix run priv/repo/seeds.exs
|
||||
|
|
|
|||
|
|
@ -14,6 +14,23 @@ defmodule Mv.Membership.Member do
|
|||
create :create_member do
|
||||
primary? true
|
||||
argument :properties, {:array, :map}
|
||||
|
||||
accept [
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:birth_date,
|
||||
:paid,
|
||||
:phone_number,
|
||||
:join_date,
|
||||
:exit_date,
|
||||
:notes,
|
||||
:city,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code
|
||||
]
|
||||
|
||||
change manage_relationship(:properties, type: :create)
|
||||
end
|
||||
|
||||
|
|
@ -21,12 +38,138 @@ defmodule Mv.Membership.Member do
|
|||
primary? true
|
||||
require_atomic? false
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I think this will run using ash's "notifiers", and as such will run outside the transaction used for the destroy itself. This means that we could end up with errors if postgres detects that a property does not have an associated member. Also, I think the "references" block in the property's postgres configuration should take make this line obsolete because it tells postgres to cascade delete properties when a member is deleted? 🤔 I think this will run using ash's "notifiers", and as such [will run outside the transaction used for the destroy itself](https://hexdocs.pm/ash/notifiers.html#transactions). This means that we could end up with errors if postgres detects that a property does not have an associated member.
Also, I think the "references" block in the property's postgres configuration should take make this line obsolete because it tells postgres to cascade delete properties when a member is deleted? 🤔
moritz
commented
I tried different approaches to get the cascade deletion working. I think in the end the postgres reference block has done the job and this is still a leftover. I will try to run it without this change. I tried different approaches to get the cascade deletion working. I think in the end the postgres reference block has done the job and this is still a leftover. I will try to run it without this change.
carla
commented
Seems that properties are deleted using default :destroy with the reference block Seems that properties are deleted using default :destroy with the reference block
|
||||
argument :properties, {:array, :map}
|
||||
|
||||
accept [
|
||||
:first_name,
|
||||
:last_name,
|
||||
:email,
|
||||
:birth_date,
|
||||
:paid,
|
||||
:phone_number,
|
||||
:join_date,
|
||||
:exit_date,
|
||||
:notes,
|
||||
:city,
|
||||
:street,
|
||||
:house_number,
|
||||
:postal_code
|
||||
]
|
||||
|
||||
change manage_relationship(:properties, on_match: :update, on_no_match: :create)
|
||||
end
|
||||
end
|
||||
|
||||
validations do
|
||||
# Required fields are covered by allow_nil? false
|
||||
|
||||
# First name and last name must not be empty
|
||||
validate present(:first_name)
|
||||
validate present(:last_name)
|
||||
validate present(:email)
|
||||
|
||||
# Birth date not in the future
|
||||
validate compare(:birth_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||
where: [present(:birth_date)],
|
||||
message: "cannot be in the future"
|
||||
|
||||
# Join date not in the future
|
||||
validate compare(:join_date, less_than_or_equal_to: &Date.utc_today/0),
|
||||
where: [present(:join_date)],
|
||||
message: "cannot be in the future"
|
||||
|
||||
|
||||
# Exit date not before join date
|
||||
validate compare(:exit_date, greater_than: :join_date),
|
||||
where: [present([:join_date, :exit_date])],
|
||||
message: "cannot be before join date"
|
||||
|
||||
|
||||
# Phone number format (only if set)
|
||||
validate match(:phone_number, ~r/^\+?[0-9\- ]{6,20}$/),
|
||||
where: [present(:phone_number)],
|
||||
message: "is not a valid phone number"
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I'm fine with doing this using custom validations as I think "real" code is easier to read than DSLs - but I think if we wanted to, we could achieve this in a more concise way using ash's built-in compare validation, e.g.: Not sure if I got the syntax exactly right, but I hope you get what I mean. I think this approach would work for other custom validations in this file as well, e.g. using the built-in One advantage of using the built-in validations is that they are "atomic", meaning they can run in the database directly, which can improve performance. I'm fine with doing this using custom validations as I think "real" code is easier to read than DSLs - but I think if we wanted to, we could achieve this in a more concise way using ash's [built-in compare validation](https://hexdocs.pm/ash/Ash.Resource.Validation.Builtins.html#compare/2), e.g.:
```elixir
validate compare(:join_date, less_than: &Date.utc_today/0),
where: [present(:join_date)],
message: "cannot be in the future"
```
Not sure if I got the syntax exactly right, but I hope you get what I mean. I think this approach would work for other custom validations in this file as well, e.g. using the built-in `match` validation for regexes.
One advantage of using the built-in validations is that they are "atomic", meaning they can run in the database directly, which can improve performance.
moritz
commented
Thank you for the hint, I will apply it and test if it works. Thank you for the hint, I will apply it and test if it works.
carla
commented
Maybe not from a technical but from a user perspective: Why should it not be possible to add members already, where the membership starts maybe beginning of next month? Should we take the question to the Pilotvereine? Maybe not from a technical but from a user perspective: Why should it not be possible to add members already, where the membership starts maybe beginning of next month? Should we take the question to the Pilotvereine?
|
||||
|
||||
|
||||
# Postal code format (only if set)
|
||||
validate match(:postal_code, ~r/^\d{5}$/),
|
||||
where: [present(:postal_code)],
|
||||
message: "must consist of 5 digits"
|
||||
|
||||
|
||||
# Email validation with EctoCommons.EmailValidator
|
||||
validate fn changeset, _ ->
|
||||
email = Ash.Changeset.get_attribute(changeset, :email)
|
||||
|
||||
changeset2 =
|
||||
{%{}, %{email: :string}}
|
||||
|> Ecto.Changeset.cast(%{email: email}, [:email])
|
||||
|> EctoCommons.EmailValidator.validate_email(:email, checks: [:html_input, :pow])
|
||||
|
||||
if changeset2.valid? do
|
||||
:ok
|
||||
else
|
||||
{:error, field: :email, message: "is not a valid email"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
uuid_v7_primary_key :id
|
||||
|
||||
attribute :first_name, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 1
|
||||
end
|
||||
|
||||
attribute :last_name, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 1
|
||||
end
|
||||
|
||||
attribute :email, :string do
|
||||
allow_nil? false
|
||||
constraints min_length: 5, max_length: 254
|
||||
end
|
||||
|
||||
attribute :birth_date, :date do
|
||||
allow_nil? true
|
||||
|
carla marked this conversation as resolved
Outdated
rafael
commented
I don't think this works the way it appears: If I remove the
I don't think this works the way it appears: If I remove the `type="checkbox"` attribute in the paid form field, and e.g. enter "not a boolean" instead of "true" or "false", I get back an "is invalid" error. This hints to me that the validate function does not get called if the input does not cast to the required attribute type. There's a [hint buried in the ash docs](https://hexdocs.pm/ash/Ash.Resource.Validation.Builtins.html#absent/2):
> Keep in mind that some types cast certain values to nil, and validations are applied after all inputs have been cast. For example, a :string type attribute with the default constraints will cast "" as nil, meaning an input of "" would pass the absent validation.
carla
commented
Should we omit that validation? Because we define "paid" as boolean and I think using Should we omit that validation? Because we define "paid" as boolean and I think using `type="checkbox"` should not be removed anywhere or am I wrong?
rafael
commented
Just realized that my comment was very unspecific about what to change :D I think we can omit the validation, because ash already ensures that the attribute is a boolean, because the attribute definition marks it as the type Just realized that my comment was very unspecific about what to change :D I think we can omit the validation, because ash already ensures that the attribute is a boolean, because the attribute definition marks it as the type `:boolean`. We should not remove the `type="checkbox"`, that was just what I did to be able to send invalid data to the server.
|
||||
end
|
||||
|
||||
attribute :paid, :boolean do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :phone_number, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :join_date, :date do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :exit_date, :date do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :city, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :street, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :house_number, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :postal_code, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
end
|
||||
|
||||
relationships do
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ defmodule Mv.Membership.Property do
|
|||
postgres do
|
||||
table "properties"
|
||||
repo Mv.Repo
|
||||
|
||||
references do
|
||||
reference :member, on_delete: :delete
|
||||
end
|
||||
end
|
||||
|
||||
actions do
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ defmodule Mv.Membership.PropertyType do
|
|||
attribute :value_type, :atom,
|
||||
constraints: [one_of: [:string, :integer, :boolean, :date, :email]],
|
||||
allow_nil?: false,
|
||||
description: "Definies the datatype `Property.value` is interpreted as"
|
||||
description: "Defines the datatype `Property.value` is interpreted as"
|
||||
|
||||
attribute :description, :string, allow_nil?: true, public?: true
|
||||
|
||||
|
|
|
|||
|
|
@ -673,4 +673,28 @@ defmodule MvWeb.CoreComponents do
|
|||
def translate_errors(errors, field) when is_list(errors) do
|
||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders a list of items with name and value pairs.
|
||||
|
||||
## Examples
|
||||
<.generic_list items={[
|
||||
{item.name, item.value},
|
||||
{other.name, other.value}
|
||||
]} />
|
||||
"""
|
||||
attr :items, :list, required: true, doc: "List of {name, value} tuples"
|
||||
|
||||
def generic_list(assigns) do
|
||||
~H"""
|
||||
<div class="mt-14">
|
||||
<dl class="-my-4 divide-y divide-zinc-100">
|
||||
<div :for={{name, value} <- @items} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
|
||||
<dt class="w-1/4 flex-none text-zinc-500">{name}</dt>
|
||||
<dd class="text-zinc-700">{value}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,6 +36,21 @@ defmodule MvWeb.MemberLive.FormComponent do
|
|||
phx-change="validate"
|
||||
phx-submit="save"
|
||||
>
|
||||
<.input field={@form[:first_name]} label="First Name" required />
|
||||
<.input field={@form[:last_name]} label="Last Name" required />
|
||||
<.input field={@form[:email]} label="Email" required type="email" />
|
||||
<.input field={@form[:birth_date]} label="Birth Date" type="date" />
|
||||
<.input field={@form[:paid]} label="Paid" type="checkbox" />
|
||||
<.input field={@form[:phone_number]} label="Phone Number" />
|
||||
<.input field={@form[:join_date]} label="Join Date" type="date" />
|
||||
<.input field={@form[:exit_date]} label="Exit Date" type="date" />
|
||||
<.input field={@form[:notes]} label="Notes" />
|
||||
<.input field={@form[:city]} label="City" />
|
||||
<.input field={@form[:street]} label="Street" />
|
||||
<.input field={@form[:house_number]} label="House Number" />
|
||||
<.input field={@form[:postal_code]} label="Postal Code" />
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">Custom Properties</h3>
|
||||
<.inputs_for :let={f_property} field={@form[:properties]}>
|
||||
<% type = Enum.find(@property_types, &(&1.id == f_property[:property_type_id].value)) %>
|
||||
<.inputs_for :let={value_form} field={f_property[:value]}>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,12 @@ defmodule MvWeb.MemberLive.Index do
|
|||
rows={@streams.members}
|
||||
row_click={fn {_id, member} -> JS.navigate(~p"/members/#{member}") end}
|
||||
>
|
||||
<:col :let={{_id, member}} label="Id">{member.id}</:col>
|
||||
<!-- <:col :let={{_id, member}} label="Id">{member.id}</:col> -->
|
||||
<:col :let={{_id, member}} label="First Name">{member.first_name}</:col>
|
||||
<:col :let={{_id, member}} label="Last Name">{member.last_name}</:col>
|
||||
<:col :let={{_id, member}} label="Email">{member.email}</:col>
|
||||
<:col :let={{_id, member}} label="City">{member.city}</:col>
|
||||
<:col :let={{_id, member}} label="Join Date">{member.join_date}</:col>
|
||||
|
||||
<:action :let={{_id, member}}>
|
||||
<div class="sr-only">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
defmodule MvWeb.MemberLive.Show do
|
||||
use MvWeb, :live_view
|
||||
import Ash.Query
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.header>
|
||||
Member {@member.id}
|
||||
{@member.first_name} {@member.last_name}
|
||||
<:subtitle>This is a member record from your database.</:subtitle>
|
||||
|
||||
<:actions>
|
||||
|
|
@ -17,8 +18,35 @@ defmodule MvWeb.MemberLive.Show do
|
|||
|
||||
<.list>
|
||||
<:item title="Id">{@member.id}</:item>
|
||||
<:item title="First Name">{@member.first_name}</:item>
|
||||
<:item title="Last Name">{@member.last_name}</:item>
|
||||
<:item title="Email">{@member.email}</:item>
|
||||
<:item title="Birth Date">{@member.birth_date}</:item>
|
||||
<:item title="Paid">{if @member.paid, do: "Yes", else: "No"}</:item>
|
||||
<:item title="Phone Number">{@member.phone_number}</:item>
|
||||
<:item title="Join Date">{@member.join_date}</:item>
|
||||
<:item title="Exit Date">{@member.exit_date}</:item>
|
||||
<:item title="Notes">{@member.notes}</:item>
|
||||
<:item title="City">{@member.city}</:item>
|
||||
<:item title="Street">{@member.street}</:item>
|
||||
<:item title="House Number">{@member.house_number}</:item>
|
||||
<:item title="Postal Code">{@member.postal_code}</:item>
|
||||
</.list>
|
||||
|
||||
<h3 class="mt-8 mb-2 text-lg font-semibold">Custom Properties</h3>
|
||||
<.generic_list items={
|
||||
Enum.map(@member.properties, fn p ->
|
||||
{
|
||||
# name
|
||||
p.property_type && p.property_type.name,
|
||||
# value
|
||||
case p.value do
|
||||
%{value: v} -> v
|
||||
v -> v
|
||||
end
|
||||
}
|
||||
end)
|
||||
} />
|
||||
<.back navigate={~p"/members"}>Back to members</.back>
|
||||
|
||||
<.modal
|
||||
|
|
@ -46,10 +74,17 @@ defmodule MvWeb.MemberLive.Show do
|
|||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
query =
|
||||
Mv.Membership.Member
|
||||
|> filter(id == ^id)
|
||||
|> load(properties: [:property_type])
|
||||
|
||||
member = Ash.read_one!(query)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
||||
|> assign(:member, Ash.get!(Mv.Membership.Member, id))}
|
||||
|> assign(:member, member)}
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Member"
|
||||
|
|
|
|||
3
mix.exs
|
|
@ -69,7 +69,8 @@ defmodule Mv.MixProject do
|
|||
{:bandit, "~> 1.5"},
|
||||
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
|
||||
{:sobelow, "~> 0.14", only: [:dev, :test], runtime: false},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:ecto_commons, "~> 0.3"}
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
4
mix.lock
|
|
@ -13,9 +13,11 @@
|
|||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||
"ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"},
|
||||
"ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
|
||||
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||
"ex_phone_number": {:hex, :ex_phone_number, "0.4.5", "2065cc48c3e9d1ed9821f50877c32f2f6898362cb990f44147ca217c5d1374ed", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "67163f8706f8cbfef1b1f4b9230c461f19786d0d79fd0b22cbeeefc6f0b99d4a"},
|
||||
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
|
||||
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
|
||||
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||
"live_debugger": {:hex, :live_debugger, "0.2.4", "2e0b02874ca562ba2d8cebb9e024c25c0ae9c1f4ee499135a70814e1dea6183e", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "bfd0db143be54ccf2872f15bfd2209fbec1083d0b06b81b4cedeecb2fa9ac208"},
|
||||
"luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
|
||||
|
|
@ -58,6 +61,7 @@
|
|||
"splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"},
|
||||
"stream_data": {:hex, :stream_data, "1.2.0", "58dd3f9e88afe27dc38bef26fce0c84a9e7a96772b2925c7b32cd2435697a52b", [:mix], [], "hexpm", "eb5c546ee3466920314643edf68943a5b14b32d1da9fe01698dc92b73f89a9ed"},
|
||||
"swoosh": {:hex, :swoosh, "1.19.2", "b2325aa7cd2bcd63ba023fa07a73dfc4f80660a592d40912975a879966ed9b7b", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cab7ef7c2c94c68fe21d3da26f6b86db118fdf4e7024ccb5842a4972c1056837"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
|
||||
"tailwind": {:hex, :tailwind, "0.3.1", "a89d2835c580748c7a975ad7dd3f2ea5e63216dc16d44f9df492fbd12c094bed", [:mix], [], "hexpm", "98a45febdf4a87bc26682e1171acdedd6317d0919953c353fcd1b4f9f4b676a2"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||
|
|
|
|||
45
priv/repo/migrations/20250617090641_member_fields.exs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
defmodule Mv.Repo.Migrations.MemberFields do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:members) do
|
||||
add :first_name, :text, null: false
|
||||
add :last_name, :text, null: false
|
||||
add :email, :text, null: false
|
||||
add :birth_date, :date
|
||||
add :paid, :boolean
|
||||
add :phone_number, :text
|
||||
add :join_date, :date
|
||||
add :exit_date, :date
|
||||
add :notes, :text
|
||||
add :city, :text
|
||||
add :street, :text
|
||||
add :house_number, :text
|
||||
add :postal_code, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:members) do
|
||||
remove :postal_code
|
||||
remove :house_number
|
||||
remove :street
|
||||
remove :city
|
||||
remove :notes
|
||||
remove :exit_date
|
||||
remove :join_date
|
||||
remove :phone_number
|
||||
remove :paid
|
||||
remove :birth_date
|
||||
remove :email
|
||||
remove :last_name
|
||||
remove :first_name
|
||||
end
|
||||
end
|
||||
end
|
||||
38
priv/repo/migrations/20250617132424_member_delete.exs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
defmodule Mv.Repo.Migrations.MemberDelete do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
drop constraint(:properties, "properties_member_id_fkey")
|
||||
|
||||
alter table(:properties) do
|
||||
modify :member_id,
|
||||
references(:members,
|
||||
column: :id,
|
||||
name: "properties_member_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public",
|
||||
on_delete: :delete_all
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
drop constraint(:properties, "properties_member_id_fkey")
|
||||
|
||||
alter table(:properties) do
|
||||
modify :member_id,
|
||||
references(:members,
|
||||
column: :id,
|
||||
name: "properties_member_id_fkey",
|
||||
type: :uuid,
|
||||
prefix: "public"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,37 +7,30 @@ alias Mv.Membership
|
|||
|
||||
for attrs <- [
|
||||
%{
|
||||
name: "Vorname",
|
||||
name: "String Field",
|
||||
value_type: :string,
|
||||
description: "Vorname des Mitglieds",
|
||||
description: "Example for a field of type string",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Nachname",
|
||||
value_type: :string,
|
||||
description: "Nachname des Mitglieds",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Geburtsdatum",
|
||||
name: "Date Field",
|
||||
value_type: :date,
|
||||
description: "Geburtsdatum des Mitglieds",
|
||||
description: "Example for a field of type date",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Bezahlt",
|
||||
name: "Boolean Field",
|
||||
value_type: :boolean,
|
||||
description: "Status des Mitgliedsbeitrages des Mitglieds",
|
||||
description: "Example for a field of type boolean",
|
||||
immutable: true,
|
||||
required: true
|
||||
},
|
||||
%{
|
||||
name: "Email",
|
||||
name: "Email Field",
|
||||
value_type: :email,
|
||||
description: "Email-Adresse des Mitglieds",
|
||||
description: "Example for a field of type email",
|
||||
immutable: true,
|
||||
required: true
|
||||
}
|
||||
|
|
|
|||
187
priv/resource_snapshots/repo/members/20250617090641.json
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"uuid_generate_v7()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "first_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "last_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "email",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "birth_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "paid",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "phone_number",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "join_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "exit_date",
|
||||
"type": "date"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "notes",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "city",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "street",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "house_number",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "postal_code",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "CF80317E7EE409618E08458B10EE122FF605640DDA8CD6000B433F1979614F5D",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "members"
|
||||
}
|
||||
105
priv/resource_snapshots/repo/properties/20250617132424.json
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "value",
|
||||
"type": "map"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "properties_member_id_fkey",
|
||||
"on_delete": "delete",
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "members"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "member_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "properties_property_type_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": "public",
|
||||
"table": "property_types"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "property_type_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "4F17BE0106435A1D75D46A3ABDE6A3DA20FC9B1C43D101B6C310009279DD7CBA",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.Mv.Repo",
|
||||
"schema": null,
|
||||
"table": "properties"
|
||||
}
|
||||
110
test/membership/member_test.exs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
defmodule Mv.Membership.MemberTest do
|
||||
use Mv.DataCase, async: false
|
||||
alias Mv.Membership
|
||||
|
||||
describe "Fields and Validations" do
|
||||
@valid_attrs %{
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
birth_date: ~D[1990-01-01],
|
||||
paid: true,
|
||||
email: "john@example.com",
|
||||
phone_number: "+49123456789",
|
||||
join_date: ~D[2020-01-01],
|
||||
exit_date: nil,
|
||||
notes: "Test note",
|
||||
city: "Berlin",
|
||||
street: "Main Street",
|
||||
house_number: "1A",
|
||||
postal_code: "12345"
|
||||
}
|
||||
|
||||
test "First name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :first_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :first_name) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Last name is required and must not be empty" do
|
||||
attrs = Map.put(@valid_attrs, :last_name, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :last_name) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email is required" do
|
||||
attrs = Map.put(@valid_attrs, :email, "")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :email) =~ "must be present"
|
||||
end
|
||||
|
||||
test "Email must be valid" do
|
||||
attrs = Map.put(@valid_attrs, :email, "test@")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :email) =~ "is not a valid email"
|
||||
end
|
||||
|
||||
test "Birth date is optional but must not be in the future" do
|
||||
attrs = Map.put(@valid_attrs, :birth_date, Date.utc_today() |> Date.add(1))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :birth_date) =~ "cannot be in the future"
|
||||
end
|
||||
|
||||
test "Paid is optional but must be boolean if specified" do
|
||||
attrs = Map.put(@valid_attrs, :paid, nil)
|
||||
attrs2 = Map.put(@valid_attrs, :paid, "yes")
|
||||
assert {:ok, _member} = Membership.create_member(Map.delete(attrs, :paid))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs2)
|
||||
assert error_message(errors, :paid) =~ "is invalid"
|
||||
end
|
||||
|
||||
test "Phone number is optional but must have a valid format if specified" do
|
||||
attrs = Map.put(@valid_attrs, :phone_number, "abc")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :phone_number) =~ "is not a valid phone number"
|
||||
attrs2 = Map.delete(@valid_attrs, :phone_number)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Join date is optional but must not be in the future" do
|
||||
attrs = Map.put(@valid_attrs, :join_date, Date.utc_today() |> Date.add(1))
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :join_date) =~ "cannot be in the future"
|
||||
attrs2 = Map.delete(@valid_attrs, :join_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Exit date is optional but must not be before join date if both are specified" do
|
||||
attrs = Map.put(@valid_attrs, :exit_date, ~D[2010-01-01])
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :exit_date) =~ "cannot be before join date"
|
||||
attrs2 = Map.delete(@valid_attrs, :exit_date)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
|
||||
test "Notes is optional" do
|
||||
attrs = Map.delete(@valid_attrs, :notes)
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "City, street, house number are optional" do
|
||||
attrs = @valid_attrs |> Map.drop([:city, :street, :house_number])
|
||||
assert {:ok, _member} = Membership.create_member(attrs)
|
||||
end
|
||||
|
||||
test "Postal code is optional but must have 5 digits if specified" do
|
||||
attrs = Map.put(@valid_attrs, :postal_code, "1234")
|
||||
assert {:error, %Ash.Error.Invalid{errors: errors}} = Membership.create_member(attrs)
|
||||
assert error_message(errors, :postal_code) =~ "must consist of 5 digits"
|
||||
attrs2 = Map.delete(@valid_attrs, :postal_code)
|
||||
assert {:ok, _member} = Membership.create_member(attrs2)
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function for error evaluation
|
||||
defp error_message(errors, field) do
|
||||
errors
|
||||
|> Enum.filter(fn err -> Map.get(err, :field) == field end)
|
||||
|> Enum.map(&Map.get(&1, :message, ""))
|
||||
|> List.first()
|
||||
end
|
||||
end
|
||||
Why not use
ash.resethere as well?typo that works as well. but i change it