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?
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))
...