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