I’m trying to implement a form to edit permissions for roles, using checkboxes to grant permissions, with LiveView 0.19. I have a Role and a Permission schema with a many_to_many relationship like so:
defmodule MyApp.Authorizations.Role do
schema "roles" do
field(:description, :string, null: true)
field(:name, :string)
many_to_many(:permissions, Permission, join_through: "roles_permissions", on_replace: :delete)
timestamps()
end
def changeset(role, attrs) do
role
|> cast(attrs, [:name, :description])
|> put_assoc(:permissions, parse_permissions(attrs))
|> validate_required([:name])
|> unique_constraint(:name)
end
defp parse_permissions(%{"permissions" => permissions_ids}) do
permissions_ids
|> Enum.reject(& &1 == "")
|> Enum.map(&Authorizations.get_permission!/1)
end
defp parse_permissions(_), do: []
end
defmodule MyApp.Authorizations.Permission do
schema "permissions" do
field(:resource, EctoAtom)
field(:action, EctoAtom)
many_to_many(:roles, Role, join_through: "roles_permissions", on_replace: :delete)
end
end
perms being a list of {"permission name", "permission id"} tuples. The issue I have now is that, when I edit a role and check a permission, the checkboxes values are nested changesets because of the put_assoc:
Hmm, the example in the article is for an array of strings field. So since you’re using a many-to-many association, you’d probably want to use inputs_for/4.
inputs_for(assigns)
Renders nested form inputs for associations or embeds.
This code gives me a strange error coming from the inputs_for: construction of binary failed: segment 1 of type 'binary': expected a binary but got: nil.
This field will be used as the array we need for the checkboxes to work. Then, I changed the changeset to use this virtual field:
def changeset(role, attrs) do
role
|> cast(attrs, [:name, :description, :organization_id])
|> validate_required([:name, :organization_id])
|> put_assoc(:permissions, fetch_permissions(attrs))
|> unique_constraint(:name)
end
defp fetch_permissions(%{permission_list: ids}),
do: fetch_permissions(%{"permission_list" => ids})
defp fetch_permissions(%{"permission_list" => permission_ids}) do
permission_ids
|> Enum.reject(&(&1 == ""))
|> Authorizations.get_permissions()
end
defp fetch_permissions(_), do: []
Finally, since the permission_list field is virtual, we have to fill it when we get the role from the database. To do so, I implemented:
def get_role!(id) do
Role
|> Repo.get!(id)
|> Repo.preload(:permissions)
|> with_permission_list()
end
def with_permission_list(role) do
permissions =
role.permissions
|> Enum.map(& &1.id)
%Role{role | permission_list: permissions}
end
I’m not too happy with the fact I have to add some stuff in the schema only for the UI, but it works well anyway, that’s the important part.