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?