I am writing a simple CRUD app in Phoenix, where admins, upon creating a new organisation are allowed to provision it with an initial staff member account.
Effectively the relationship between Organisations and Users is many to many.
I came up with the following:
User schema:
defmodule MyApp.User do
use MyApp.Web, :model
schema "users" do
field :name, :string
field :email, :string
field :password, :string, virtual: true
field :password_hash, :string
end
def changeset(...) # validate email, password confirmation etc.
Organisation schema:
defmodule MyApp.Org do
use MyApp.Web, :model
schema "orgs" do
field :official_name, :string
field :common_name, :string
has_many :org_staff_users, MyApp.OrgStaffUser
has_many :users, through: [:org_staff_users, :user]
end
def changeset(model, params \\ :empty) do
model
|> cast(params, ~w(official_name common_name), [])
end
def provisioning_changeset(model, params \\ :empty) do
model
|> changeset(params)
|> cast_assoc(:org_staff_users, required: true)
end
Junction table org_staff_users
and the corresponding Ecto Schema with
user_id
and org_id
Controller with the following new
action:
def new(conn, _params) do
data = %Org{org_staff_users: [%User{}]}
changeset = Org.provisioning_changeset(data)
render(conn, "new.html", changeset: changeset)
end
Template with the following excerpt:
<%= form_for @changeset, @action, fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below:</p>
<ul>
<%= for {attr, message} <- f.errors do %>
<li><%= humanize(attr) %> <%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= text_input f, :official_name, class: "form-control" %>
<%= text_input f, :common_name, class: "form-control" %>
<%= inputs_for f, :org_staff_users, fn i -> %>
<%= text_input f, :email, class: "form-control" %>
<%= text_input f, :password, class: "form-control" %>
<%= text_input f, :password_confirmation, class: "form-control" %>
<% end %>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
So far so good, the form displays nicely.
The problem is, I don't really understand what should be the canonical way of building the changeset I'm about to insert on create
, while being able
to pass it again to the view upon validation errors.
It is unclear whether I should use one changeset (and how?) or explicitly
three changesets per each entity (User
, Org
and the junction table).
How do I validate the changes for such combined form, given that each model / schema has its own specific validations defined?
The params I receieve upon submitting the form are all within %{"org" => ...}
map, including the ones that are in fact related to a user
. How should I
create the form properly?
I have read the recently updated http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/ but I remain confused regardless.
FWIW, I am on Phoenix 1.0.4, Phoenix Ecto 2.0 and Phoenix HTML 2.3.0.
Any tips would be greatly appreciated.
Right now, you don't have any other option besides doing everything in a transaction. You are going to create an organization inside the transaction, which its own changeset, and if it works you create each staff. Something like this:
if organization_changeset.valid? and Enum.all?(staff_changesets, & &1.valid?) do
Repo.transaction fn ->
Repo.insert!(organization_changeset)
Enum.each staff_changesets, &Repo.insert!/1)
end
end
Notice I am doing valid?
checks on the changesets which is non-ideal because it doesn't consider constraints. If there are constraints in the changesets though, you need to use Repo.insert
(without bang !
).
Keep in mind this will be much easier on Ecto 2.0. On Ecto master, we already support belongs_to via changesets, which means you would be able to do it explicitly by generating both intermediate and end associations:
<%= inputs_for f, :org_staff_users, fn org_staff -> %>
<%= inputs_for org_staff, :user, fn user -> %>
# Your user form here
<% end %>
<% end %>
However, we will also support many_to_many which will make it altogether straight-forward.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With