Ecto has_many nested cast_assoc not updating

Hello all,

I’m triing to create kind of feedback system with map, skipping some details I have campaign which has_many question_categories which has_many questions which has_many options (they are optional in case of open answer). On answer side, campaign has_many entities (point on map) which has_many responses, which has_many answer_categories (I would like to get rid of this table) which has_many answers that belnogs_to question and option (in case closed answer). I draw this schema here.

Now I need 4 forms,

  1. I need form for create entity that would contain also answers for first responses from author of entity
  2. then I need form to add new response to existing entity
  3. form to update entity (with author response)
  4. form to update response

I two things:

  1. Create form directly without AnswerCategories, but it turn out that I cant pass string to phoenix html form functions so it is imposible to select campaign a structure form in template, so form needs to be build to desired structure in changeset (this actuali kind of work but it was a lot of incredibli ugly code)

  2. Also without AnswerCategories I made separate ecto structure named response form that took to changeset function as parameter campaign and created form structure from it, it looked good for case of editing responses, but then I wanted to integrate this response form to entity, I also need to add separate update entity form and separate validation and again it ended up quite big and mostly not working

  3. Now I’m triing to get work structure with answer_category table (basicali table schema is dictate by UI needs) connect it throught has_many, and use cast_assoc, forms composition was easi, I just added has many to entity -> responses and when entity was creating, I cast first response form, there I hit the block because for some reason answers are not updating, they update only if I also somehow change answer_category it is like updates don’t propagate from 3# level nested associations and only thing that is updated is update time at response

Response schema

schema "responses" do
    belongs_to :entity, Entity
    has_many :answer_categories, Category, on_replace: :update

    timestamps()
  end

Response answer category

schema "response_answer_categories" do
  field :answered?, :boolean, default: false, virtual: true

  belongs_to :category, Category
  belongs_to :response, Response

  has_many :answers, Answer, on_replace: :delete

  timestamps()
end

Response answer

schema "response_answers" do
  field :answer, :string

  belongs_to :question, Question
  belongs_to :category, Category
  belongs_to :option, Option

  timestamps()
end

Changeset for response form

def create_changeset(entity, attrs) do
  entity.campaign
  |> put_change(:entity_id, entity.id)
  |> cast_assoc(:answer_categories, with: & Category.changeset(&1, entity.campaign.question_categories, &2))
  |> validate_required([:entity_id])
  |> prepare_changes(& filter_answered_categories/1) # I filter out categories with answer? true 
end

Changeset of answer category

def changeset(category, question_categories, attrs) do
  category
  |> cast(attrs, [:answered?, :response_id, :category_id])
  |> validate_inclusion(:category_id, Enum.map(question_categories, & &1.id))
  |> cast_answers(question_categories)
  |> unique_constraint([:response_id, :category_id])
end

defp cast_answers(changeset, question_categories) do
  category_id = get_field(changeset, :category_id)
  question_category = Enum.find(question_categories, & &1.id == category_id)

  cast_assoc(
    changeset,
    :answers,
    with: &Answer.changeset(&1, get_field(changeset, :answered?), question_category.questions, &2)
  )
end

Question changeset

def changeset(answer, validate, questions, attrs) do
  answer
  |> cast(attrs, [:answer, :question_id, :option_id, :category_id])
  |> validate_inclusion(:question_id, Enum.map(questions, & &1.id))
  |> validate_question(validate, questions)
  |> unique_constraint([:category_id, :question_id])
end
  1. I was thinking about kind of wrapping structure that would validate and then result would by saved to database using ecto multi, for instance for create entity
embeded_schema do
  field :entity, Entity,
  field :response, Response
end

And then in context

def create_entity(form, attrs) do
  changeset = Form.changeset(form, attrs)

  if changeset.valid? do
    Multi.new()
    |> Multi.insert(:entity, get_field(changeset, :entity))
    |> Multi.insert(:response, get_field(changeset, :response))
    |> Repo.transaction()
  else
    apply_action(changeset, :insert)
  end
end

But before I create pletohra custom forms that would write some other structures I would like to know your opinion, or if anybady solved similiar situation and know some more elegant solution. Part of problem that I somehov solved was also that structure of form is saved in database, which is solved by passing campaign to changeset and conditionali doing validation, I also tried to first create structure with all the data prefilled but cast_assoc was throving something like structure with empty primary key. But that is probably adept for another question (I ended up building everithing in changeset using put_change and changes)

Thank you for any help

The question is quite long, but I think You can simplify.

You should consider using entity as an aggregate, that would be managed as a unit, then You would need one form instead of 4.

At least if You use cast, instead of put. And no need for Multi…

I also try to avoid casting foreign keys, they are ways to avoid this.

idea with aggregate would not work since onli author of entity shoul by able to edit attributes in entity, other users can onli edit their answer in responses, but yeah, edit/create forms could by reduced, they are used mainli because create command is also writing authors which should not by influenced during update

You are also adding authorization concern, it should probably not be mixed in, but could be managed by an authorization service. You can create specific changesets, depending on the authorization status and set what users are allowed to change.

Authorization is solved elsewhere, I just want to point out that entity response relation is one to many and as such I don’t want to merge them, also, if I have just one structure for them both, I would just merge complexity from multiple smaller modules in to one big, but maybe that will work, for now I would invest little more time to why 3 level assoc are not updated, I found one place in code base where it is working

I found out two things

  1. on create I had response |> changes() |> cast_assoc so no changes was picked up after submit
  2. in function for filtering before insert to database (in changeset call |> prepare_changes(& filter_answered_categories/1) put_assoc for some reason returned empty changeset, old function definition that did not work on update (only on update for some reason):
defp filter_answered_categories(changeset) do
    answer_categories =
      changeset
      |> get_field(:answer_categories)
      |> Enum.filter(& &1.answered?)

    put_assoc(changeset, :answer_categories, answer_categories)
  end

But when I do filtering with category changesets and then put them to changeset with put_change, everithing started to work

defp filter_answered_categories(changeset) do
    answer_categories_changesets =
      changeset.changes.answer_categories
      |> Enum.filter(& get_field(&1, :answered?))

    put_change(changeset, :answer_categories, answer_categories_changesets)
  end

I’m still curious about why put_assoc behave like that…