Использование вложенных форм в сочетании с Ecto.Multis и changesets - PullRequest
0 голосов
/ 18 февраля 2019

На странице регистрации моего приложения я пытаюсь использовать вложенные формы (используя form_for / 3 и input_for / 4 ) в сочетании с Ecto.Multis и наборами изменений.

Цель: Повторно отобразить форму с ранее введенной информацией, если одна или несколько проверок на стороне сервера не пройдены, чтобы пользователь мог вносить исправления, а не начинать с нуля.


Приложение имеет схему организации, в которой есть один пользователь, который является создателем / владельцем:

schema "organizations" do
  field :owner_id, :integer
  has_one :owner, MyApp.Accounts.User, references: :owner_id, foreign_key: :id
end

Контекст организации имеет такую ​​функцию create_organization:

def create_organization(org_attrs, user_attrs) do
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:organization, organization_changeset(%Organization{}, org_attrs))
  |> Ecto.Multi.run(:user, fn _repo, %{organization: organization} ->
    %User{organization_id: organization.id}
    |> Accounts.register_user_changeset(user_attrs)
    |> Repo.insert()
  end)
  |> Ecto.Multi.run(:update_organization, fn(_repo, %{user: user, organization: organization}) ->
    organization
    |> organization_creator_changeset(%{owner_id: user.id})
    |> Repo.update()
  end)
  |> Repo.transaction()
end

(Причина, по которой я использую Ecto.Multi для этого, заключается в том, что если, например, пользовательская вставка не удалась, я хочу, чтобы откатилась вся операция.)

Мои действия контроллера:

def new(conn, _params) do
  changeset = Organizations.create_organization_changeset(%Organization{owner: %User{}})
  conn
  |> render("new.html", changeset: changeset)
end


def create(conn, %{"organization" => %{"owner" => user_params}} = org_params) do
  case Organizations.create_organization(org_params, user_params) do
    {:ok, %{user: user}} ->
      conn
      |> put_status(:created)
      |> put_view(MyAppWeb.Accounts.AccountView)
      |> render("confirm.html", email: user.email)
    {:error, _resource, changeset, _changes} ->
      conn
      |> put_status(:unprocessable_entity)
      |> put_flash(:error, MyAppWeb.ErrorHelpers.transform_errors(changeset))
      |> render("new.html", changeset: changeset)
  end
end

И моя форма:

<%= form_for @changeset, account_path(@conn, :create), fn f -> %>

  <div class="row">
    <%= text_input :organization, :name, class: "form-control", required: true %>
    <%= label f, :organization_name %>
    <%= error_tag f, :name %>
  </div>

  <%= inputs_for f, :owner, fn u -> %>

    <div class="row">
      <%= email_input u, :email, class: "form-control", required: true %>
      <%= label u, :email %>
      <%= error_tag u, :email %>
    </div>

    <div class="row">
      <%= password_input u, :password, class: "form-control", pattern: ".{8,}", title: "Password must have 8 or more characters.", required: true %>
      <%= label u, :password %>
      <%= error_tag u, :password %>
    </div>

    <div class="row">
      <%= password_input u, :password_confirmation, class: "form-control", required: true %>
      <%= label u, :password_confirmation %>
      <%= error_tag u, :password_confirmation %>
    </div>

  <% end %>

<% end %>

Проблема: Если все идет хорошо, организация и пользователь успешно создаются и подтверждают .html рендерит,Однако, если есть ошибки набора изменений, я получаю это:

could not generate inputs for :owner from MyApp.Accounts.User. Check the field exists and it is one of embeds_one, embeds_many, has_one, has_many, belongs_to or many_to_many

Я считаю это потому, что Ecto.Changeset, который передается в new.html, является автономным пользователем%{} changeset:

#Ecto.Changeset<
  action: :insert,
  changes: %{
    email: "test@test.com",
    organization: #Ecto.Changeset<action: :update, changes: %{}, errors: [],
     data: #MyApp.Organizations.Organization<>, valid?: true>,
    password: "test",
    password_confirmation: "test",
    password_hash: "bla"
  },
  errors: [
    email: {"has already been taken",
     [constraint: :unique, constraint_name: "accounts_users_email_index"]}
  ],
  data: #MyApp.Accounts.User<>,
  valid?: false
>

Как лучше всего решить эту проблему?

1 Ответ

0 голосов
/ 18 февраля 2019

Вместо того, чтобы вводить свою собственную повторную реализацию для вставки нескольких вложенных / связанных записей в разные таблицы, вам, вероятно, лучше использовать то, что Ecto предоставляет из коробки: Ecto.Changeset.put_assoc/4 в User 's changeset.

Он выполнит все проверки для вас и вернет недействительный changeset, если что-то пошло не так.

...