<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="389541" data-post-id="389541">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="LostKobrakai" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/LostKobrakai/120/3072_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  LostKobrakai
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Ah. I was thinking the Form module would be the frontend not the struct of the changeset. Generally I’d argue that no matter how you scope your functions and modules you should always be able to create a changeset, where all the to be revalidated parts are on the changes side of the changeset(s) and not on the data side. If you do not start from blank data you might need to use force_change - but you can start from a blank schema struct and move all data over. There’s no object instances to loose or something. For as long as you keep ids the same they’ll be written/updated/kept as the same record.</p>
<p>Essentially on publish you need to split up the data existing on your records into what is considered already validated data / to be retained as is and what should be considered a change in need for validation - hence me suggesting to model that task explicitly.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="389541" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/idiomatic-way-to-re-validate-has-many-children-on-a-parent-state-transition-with-ecto/75501/12">Post #11</a>
	                </div>
	            </div>
              <div id="likers-container-389541" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="389541"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #11"></div>
  </section>
</div>
    <div class="postbit" id="389542" data-post-id="389542">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="jdiago" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/jdiago/120/31829_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  jdiago
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>The cleanest solution is probably removing this:</p>
<aside class="quote no-group" data-username="caioaao" data-post="1" data-topic="75501">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/caioaao/48/40488_2.png" class="avatar"> caioaao:</div>
<blockquote>
<p>To make matters worse, let’s say the <code>publish_form</code> API supports patching the form before publishing.</p>
</blockquote>
</aside>
<p>I used to work on an EHR rails app with this exact issue. It’s much easier to train non-technical users to click a validate button first before they could publish a form that would be used in the system. It put a fear in them that they could not change the form anymore once it was published and used at least once(which was the case).</p>
<p>This way, you have your publish changeset that validates the current state of the form separate from the edit form changeset and both code paths are simpler.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="389542" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/idiomatic-way-to-re-validate-has-many-children-on-a-parent-state-transition-with-ecto/75501/13">Post #12</a>
	                </div>
	            </div>
              <div id="likers-container-389542" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="389542"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #12"></div>
  </section>
</div>
    <div class="postbit" id="389871" data-post-id="389871">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="idmitrievskii" src="/assets/icons/user-9f439610.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  idmitrievskii
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p><a class="mention" href="/u/caioaao" rel="nofollow">@caioaao</a> could you please sum up the advice from this thread in a code snippet that shows how the code from the question could be improved? I feel like some of the discussion got over my head…</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="389871" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/idiomatic-way-to-re-validate-has-many-children-on-a-parent-state-transition-with-ecto/75501/14">Post #13</a>
	                </div>
	            </div>
              <div id="likers-container-389871" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="389871"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #13"></div>
  </section>
</div>
    <div class="postbit" id="390135" data-post-id="390135">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="caioaao" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/caioaao/120/40488_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  caioaao
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>Why do I feel like I’m an LLM being prompted? lol.</p>
<p>The winner advice is to include everything that needs to be revalidated as changes in the changeset. Something like this (untested):</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule Forms.Form do
  use Ecto.Schema
  import Ecto.Changeset

  schema "forms" do
    field :status, Ecto.Enum, values: [:draft, :published], default: :draft
    field :title, :string
    has_many :questions, Forms.Question, on_replace: :delete
  end

  @doc """
  More relaxed changeset. While in draft we allow things to be missing/invalid. Validation will happen in publishing
  """
  def draft_changeset(form \\ %__MODULE__{}, attrs) do
    form
    |&gt; cast(attrs, [:title])
    |&gt; cast_assoc(:questions)
    |&gt; put_change(:status, :draft)
  end

  @doc """
  Publish changeset validates everything again. Only complete forms can be published
  """
  def publish_changeset(form \\ %__MODULE__{}, attrs) do
    form
    |&gt; cast(attrs, [:title], force_changes: true) # Even if `title` is not present in `attrs`, it will be considered a change here
    |&gt; cast_assoc(:questions, with: &amp;Forms.Question.publish_changeset/2)
    |&gt; put_change(:status, :published)
    |&gt; validate_required([:title])
    |&gt; validate_length(:questions, min: 1)
  end
end

defmodule Forms.Question do
  use Ecto.Schema
  import Ecto.Changeset

  schema "questions" do
    field :prompt, :string
    field :type, Ecto.Enum, values: [:short_text, :multiple_choice]
    field :choices, {:array, :string}, default: []
    belongs_to :form, Forms.Form
  end

  @doc """
  More relaxed changeset. While in draft we allow things to be missing/invalid. Validation will happen in publishing
  """
  def draft_changeset(question \\ %__MODULE__{}, attrs) do
    question
    |&gt; cast(attrs, [:prompt, :type, :choices])
  end

  def publish_changeset(question \\ %__MODULE__{}, attrs) do
    question
    |&gt; cast(attrs, [:prompt, :type, :choices], force_changes: true) # same thing here: force changes to make sure everything is validated even if didn't change now
    |&gt; validate_required([:prompt, :type])
    |&gt; validate_choices()
  end
end
</code></pre>
<p>The other idea (a more verbose one) would be to split the schemas into a <code>DraftForm</code>/<code>DraftQuestion</code> and a <code>PublishedForm</code>/<code>PublishedQuestion</code>. They can both point to the same table, but have different requirements in their changesets. Also the <code>Draft</code> variants would have a <code>to_published_attrs</code> that returns the map to be fed into the published variant’s changeset, like so:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">defmodule Forms.DraftForm do
  use Ecto.Schema
  import Ecto.Changeset

  schema "forms" do
    field :status, Ecto.Enum, values: [:draft, :published], default: :draft
    field :title, :string
    has_many :questions, Forms.DraftQuestion, foreign_key: :form_id, on_replace: :delete
  end

  @doc """
  Relaxed changeset. While in draft we allow things to be missing/invalid.
  """
  def changeset(form \\ %__MODULE__{}, attrs) do
    form
    |&gt; cast(attrs, [:title])
    |&gt; cast_assoc(:questions)
    |&gt; put_change(:status, :draft)
  end

  @doc """
  Converts a draft into the attrs map expected by `Forms.PublishedForm.changeset/2`.
  Everything goes through the published changeset, so all fields get validated.
  """
  def to_published_attrs(%__MODULE__{} = form) do
    %{
      "title" =&gt; form.title,
      "questions" =&gt; Enum.map(form.questions, &amp;Forms.DraftQuestion.to_published_attrs/1)
    }
  end
end

defmodule Forms.DraftQuestion do
  use Ecto.Schema
  import Ecto.Changeset

  schema "questions" do
    field :prompt, :string
    field :type, Ecto.Enum, values: [:short_text, :multiple_choice]
    field :choices, {:array, :string}, default: []
    belongs_to :form, Forms.DraftForm
  end

  def changeset(question \\ %__MODULE__{}, attrs) do
    question
    |&gt; cast(attrs, [:prompt, :type, :choices])
  end

  def to_published_attrs(%__MODULE__{} = question) do
    %{
      "prompt" =&gt; question.prompt,
      "type" =&gt; question.type,
      "choices" =&gt; question.choices
    }
  end
end

defmodule Forms.PublishedForm do
  use Ecto.Schema
  import Ecto.Changeset

  schema "forms" do
    field :status, Ecto.Enum, values: [:draft, :published], default: :draft
    field :title, :string
    has_many :questions, Forms.PublishedQuestion, foreign_key: :form_id, on_replace: :delete
  end

  @doc """
  Strict changeset. Only complete forms can be published.
  Feed it `Forms.DraftForm.to_published_attrs/1` output - since every field
  arrives as an incoming change, everything is (re)validated.
  """
  def changeset(form \\ %__MODULE__{}, attrs) do
    form
    |&gt; cast(attrs, [:title])
    |&gt; cast_assoc(:questions)
    |&gt; put_change(:status, :published)
    |&gt; validate_required([:title])
    |&gt; validate_length(:questions, min: 1)
  end
end

defmodule Forms.PublishedQuestion do
  use Ecto.Schema
  import Ecto.Changeset

  schema "questions" do
    field :prompt, :string
    field :type, Ecto.Enum, values: [:short_text, :multiple_choice]
    field :choices, {:array, :string}, default: []
    belongs_to :form, Forms.PublishedForm
  end

  def changeset(question \\ %__MODULE__{}, attrs) do
    question
    |&gt; cast(attrs, [:prompt, :type, :choices])
    |&gt; validate_required([:prompt, :type])
    |&gt; validate_choices()
  end

  defp validate_choices(changeset) do
    case get_field(changeset, :type) do
      :multiple_choice -&gt;
        validate_length(changeset, :choices, min: 2)

      _ -&gt;
        changeset
    end
  end
end
</code></pre>
<p>Publishing then looks like:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">draft
|&gt; Forms.DraftForm.to_published_attrs()
|&gt; then(&amp;Forms.PublishedForm.changeset(%Forms.PublishedForm{id: draft.id}, &amp;1))
|&gt; Repo.update()
</code></pre> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="390135" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/idiomatic-way-to-re-validate-has-many-children-on-a-parent-state-transition-with-ecto/75501/15">Post #14</a>
	                </div>
	            </div>
              <div id="likers-container-390135" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="390135"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-standard-post cat-standard-post" title="Post #14"></div>
  </section>
</div>
    <div class="postbit" id="390177" data-post-id="390177">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="GrammAcc" src="https://forum.elixirforum.com/user_avatar/forum.elixirforum.com/GrammAcc/120/40380_2.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  GrammAcc
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<p>This is an interesting problem. It seems to me like the complexity is coming from the data model. The <code>:status</code> key is a column on the form table, but it’s actually representing the state of the questions table since the only thing that changes on a form is the associated questions, so I think adding the status column to the questions table and then just adding a validation that enforces the questions all have the same status as the form would simplify a lot of stuff cause then the validations could treat the question struct as a tagged union, and check constraints could also be added to the questions table to ensure data integrity for different statuses.</p>
<p>Ofc, that’s not an option for the OP since they mentioned they can’t change the data model. I would also assume the real use-case has a more complicated or messier schema, but it’s an interesting problem to think about at least. <img src="https://forum.elixirforum.com/images/emoji/apple/slight_smile.png?v=15" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"></p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="390177" data-batch-url="/posts/batch_likers">
                        0
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/idiomatic-way-to-re-validate-has-many-children-on-a-parent-state-transition-with-ecto/75501/16">Post #15</a>
	                </div>
	            </div>
              <div id="likers-container-390177" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="390177"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-last-post cat-last-post" title="Last post!"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <span class="all-loaded">— All posts loaded —</span>
</div></template></turbo-stream>