1
votes

I have a web-application made in elixir/phoenix. I will keep my explanation focussed around the heading. So I have something in the system called roles and there is something called permissions.

The roles have many-to-many relationship with permissions via a middle table role_permissions.

The schema for the three is something along the lines of:

roles

schema "roles" do
    # associations
    has_many(:users, User)
    many_to_many(:permissions, Permission, join_through: "role_permissions")

    field(:name, :string)
    field(:description, :string)

    timestamps()
  end 

@required_params ~w(name)a
@optional_params ~w(description)a
@create_params @required_params ++ @optional_params
@update_params @required_params ++ @optional_params

@doc """
Returns a changeset to create a new `role`.
"""
@spec create_changeset(t, map) :: Ecto.Changeset.t()
def create_changeset(%__MODULE__{} = role, params) do
 role
 |> cast(params, @create_params)
 |> common_changeset()
end

permissions

schema "permissions" do
    field(:code, :string)
    field(:description, :string)

    timestamps()
  end

role_permissions

 schema "permissions" do
    belongs_to(:role, Role)
    belongs_to(:permission, Permission)

    timestamps()
  end

permissions can be created separately from roles so I have CRUD functions to create them.

However, while creating roles they should be associated with the permissions. In order to do that I have used a create function shown below

def create(%{permissions: permissions} = params) when permissions != [] do
    Multi.new()
    |> Multi.run(:role, fn _ ->
      QH.create(Role, params, Repo)
    end)
    |> permissions_multi(params)
    |> persist()
  end

defp permissions_multi(multi, params) do
    Multi.run(multi, :permission, fn %{role: role} ->
      role_permissions = associate_role_permissions(role, params[:permissions])
      {count, _} = Repo.insert_all(RolePermission, role_permissions)
      {:ok, count}
    end)
  end

  defp associate_role_permissions(role, permissions) do
    Enum.map(permissions, fn permission ->
      [permission_id: permission,
      role_id: role.id,
      inserted_at: DateTime.utc_now,
      updated_at: DateTime.utc_now]
    end)
  end

  # Run the accumulated multi struct
  defp persist(multi) do
    case Repo.transaction(multi) do
      {:ok, %{role: role}} ->
        {:ok, role}

      {:error, _, _, _} = error ->
        error
    end
  end

The create/1 takes name, description and permissions which is a list of permission_ids.

Role is inserted into the db first. Upon insertion, I receive {:ok, role} I use the associate_role_permission to pair the role_id with all the permission ids and use Repo.insert_all to make an insertion in the middle table.

All this works fine, I have written tests for this and it works.

The problem arises when I am moving to UI with phoenix.

The controller

def new(conn, _params) do
  changeset = RoleSchema.create_changeset(%RoleSchema{}, %{permissions: nil})
  render(conn, "new.html", changeset: changeset)
end

"new.html" is as follows:

<%= form_for @changeset, @action, [as: :role], fn f -> %>
  <%= input f, :name %>
  <%= input f, :description %>
  <%= multiple_select(f, :permissions, formatted_list(:permissions))%>
  <%= submit "Submit", class: "btn btn-primary submit-btn" %>
  <%= link("Cancel", to: role_path(@conn, :index), class: "btn btn-primary") %>
<% end %>

What I am trying to do here is create a dropdown for multi-select from the list of permissions I am loading in the format [{:code, :id}] which works fine.

But I am constantly receiving the error

protocol Phoenix.HTML.Safe not implemented for #Ecto.Association.NotLoaded<association :permissions is not loaded>.

I know the error is because in the changeset the data field has permissions not loaded.

What should be a workaround for this problem?

Is there a way to modify a particular field in the changeset.data field, is this even the right way?

1

1 Answers

1
votes

You have to explicitly Ecto.Query.preload/3 each association, Ecto won’t do it silently. Your RoleSchema has an association there in :permissions and since it’s not preloaded in the newly created changeset, it returns the Ecto.Association.NotLoaded struct, which is temporarily substituting the real value until the association is loaded.

Since you are creating a changeset here, just explicitly update it’s data like:

changeset = RoleSchema.create_changeset(%RoleSchema{})
changeset = update_in(changeset.data, &Repo.preload(&1, :permissions))
...