I’d like to have opinnion of anyone experienced in using Ecto :drop_param
with nested changesets. Problem at hand is that, I would like to restrict the removal of certain association, if that association is referenced by another model in database.
I have structure similar to below, and when there are answers that belong to specific question or option removing that association from survey should not be possible.
Survey has_many Question
Question has_many Option
Option belongs_to Question
Answer belongs_to Question
Answer belongs_to Option
Form
<.form
for={@form}
id="survey-form"
phx-target={@target}
phx-change="validate"
phx-submit="save"
>
<.inputs_for :let={question} field={@form[:questions]}>
<label>
<input
type="checkbox"
name="event[questions_delete][]"
value={question.index}
class="hidden"
/>
<%= gettext("Remove question") %>
</label>
</.inputs_for>
</.form>
Changeset
def changeset(question, attrs \\ %{}, index) do
question
|> cast(attrs, [:id])
|> change(index: index)
|> cast_questions(attrs)
end
defp cast_questions(changeset, attrs) do
changeset
|> validate_question_removal(attrs)
|> case do
changeset ->
opts = questions_opts(changeset)
changeset
|> cast_assoc(:questions, opts)
|> validate_length(:questions, max: 25)
end
end
defp questions_opts(changeset) do
opts = [
with: &Question.changeset/3,
sort_param: :questions_order
]
if Enum.any?(changeset.errors, &drop_param_error?/1),
do: opts,
else: opts ++ [drop_param: :questions_delete]
end
defp drop_param_error?({:drop_param, _}), do: true
defp drop_param_error?(_), do: false
defp validate_question_removal(changeset, attrs) do
delete_index = attrs |> Map.get("questions_delete", []) |> List.first(nil)
question_id =
attrs
|> Map.get("questions", %{})
|> Map.get(delete_index, %{})
|> Map.get("id", nil)
if delete_index && question_id do
option_query =
from(a in Answer, where: a.question_id == ^question_id)
if Repo.Read.exists?(option_query),
do:
add_error(
changeset,
:drop_param,
"cannot remove a question that has associated answers"
),
else: changeset
else
changeset
end
end
This approach does work, and has behaviour I am expecting (instead of removing the association display error message on the form eg. questions has associated answers and cannot be removed).
This approach seems a little fragile though, and I have been thinking whether something like Ecto no_assoc_constraint/3
could be used instead? Ecto.Changeset — Ecto v3.10.3
Or are there some better streamlined approach to add constraint in case of using :drop_param
to do some validation before dropping the parameter.