8000 Render the form error summary from one shared partial by dhoelzer · Pull Request #37 · dhoelzer/DAD · GitHub
[go: up one dir, main page]

Skip to content

Render the form error summary from one shared partial - #37

Merged
dhoelzer merged 1 commit into
masterfrom
dedupe-crud-views
Nov 11, 2026
Merged

dhoelzer merged 1 commit into
masterfrom
dedupe-crud-views

Conversation

@dhoelzer
Copy link
Copy Markdown
Owner

What

The form error summary was copy-pasted into all 14 _form.html.erb files. It is now one shared partial. 154 lines removed, 14 added.

The duplication

Normalising the model name out of all 14 blocks left one distinct shape across all 14. The only differences were the instance variable and the loop variable — and those had already drifted: 8 forms used |msg|, 6 used |message|.

<%= render 'shared/error_explanation', record: @user %>

The h2 noun (prohibited this *user* from being saved) was a hardcoded string in each copy. It is now record.model_name.human.downcase, checked against all 14 before the change — every one matched the string that had been written out by hand.

The finding that matters more than the duplication: 12 of the 14 blocks are dead code

Only User (username presence) and Job (frequency numericality) have validations. The other twelve models have none, and config/application.rb deliberately does not call config.load_defaults, so belongs_to_required_by_default stays off. A blank Alert, Display, System or Service therefore saves successfully — errors.any? is false and the block cannot render.

Confirmed, not inferred:

Alert        save=true   errors.any?=false
Display      save=true   errors.any?=false
... 12 models ...
User         save=false  errors.any?=true   msgs=["Username can't be blank"]

So this was never 14 copies of working code — it was 14 copies of a template, 12 of which decorate a form that cannot report an error. Worth recording because it changes what future work should assume: adding a validation to Display would light up a summary that has never rendered in production.

Nothing tested any of it

No test in the suite referenced error_explanation or the summary text, before or after. A partial that raised would have left every test green. test/system/form_errors_test.rb now covers:

  • the two reachable paths (invalid user, invalid job), asserting the summary text and .field_with_errors (which Rails emits itself)
  • that a valid form renders no summary — this is what proves the errors.any? guard survived extraction
  • all 14 models, by rendering the partial directly, since the other twelve cannot reach it through a form without inventing validations they do not have

Both guards checked by mutation:

Mutation Result
model_name.human.downcase → class.name 3 failures — shared partial names the model wrong for Alert
if record.errors.any? → if true 1 failure — the no-summary test

Evidence the refactor is output-preserving

Rather than eyeballing the diff, rendered HTML for every reachable form path (new + edit for 14 models, plus the two invalid-submit responses) was captured before and after and compared:

Result Count
Byte-identical 4
Differ only by a whitespace-only line 21
Real content differences 2
  • The 21 are forms with no error block: the removed <% if %> left one fewer blank line.
  • The 2 "real" differences are the datetime_select hour rolling from 1 8000 8 to 19 between captures — the clock advanced, not the code.
  • For the two renders that do contain a summary, only the indentation of <h2>/<ul>/<li>/</div> changed, and the h2 text is identical — confirming the model_name.human.downcase derivation produces the same wording.

Deliberately not deduplicated

The 16 index.html.erb views look similar but are not. The action columns vary by what each controller permits:

View Show Edit Destroy
comments, exclusions, hunks, preferences, rights, roles, searches, services, statistics, users ✓ ✓ ✓
sessions ✓ — ✓
displays, jobs — — ✓
alerts, events, systems — — —

That is the rights model showing through, not copy-paste drift — a shared action-cell partial would need per-view conditionals as numerous as the views. The new/edit wrappers are 4 lines each and differ in all three lines; extracting them trades 14 short readable files for one file with parameters. Left as is.

Verification

Gate Result
rake test 437 runs / 1259 assertions / 0 failures / 0 errors
rake test:system 36 runs / 193 assertions / 0 failures (up from 32/127)
cucumber 5 scenarios / 18 steps passed
brakeman 0 warnings
bundle-audit no vulnerabilities

Docs: RECOMMENDATIONS.md §5.13, and AGENTS.md records the shared partial plus the "most models have no validations" fact so nobody re-inlines the block or assumes a form can report an error.

This PR was created by an AI agent (OpenHands) on behalf of the user.

All 14 _form.html.erb files carried the same 11-line error_explanation
block. Normalising the model name out left one distinct shape across all
14; the only differences were the instance variable and the loop variable,
which had already drifted (8 forms used |msg|, 6 used |message|).

Replaced with app/views/shared/_error_explanation.html.erb. 154 lines
removed, 14 added. The h2 noun was a hardcoded string in each copy and is
now record.model_name.human.downcase -- checked against all 14 before the
change, every one matched what had been written out by hand.

The bigger finding: 12 of the 14 blocks are dead code. Only User and Job
have validations, and config/application.rb deliberately omits
config.load_defaults, so belongs_to_required_by_default stays off and a
blank Alert/Display/System/Service saves cleanly -- errors.any? is false
and the block cannot render. Confirmed by building each model with no
attributes and calling save: 12 returned true, only User returned false.

Nothing tested any of it. test/system/form_errors_test.rb now covers the
two reachable paths, asserts a valid form renders no summary (which is
what proves the errors.any? guard survived extraction), and renders the
partial directly for all 14 models. Verified by mutation: swapping
model_name.human.downcase for class.name fails 3 tests; forcing the guard
to `if true` fails the no-summary test.

Output-preserving, shown rather than asserted: rendered HTML for every
reachable form path captured before and after. 4 byte-identical, 21
differing only by a whitespace-only line (the removed <% if %> left one
fewer blank line), 2 real differences -- both the datetime_select hour
rolling from 18 to 19 as the clock advanced between captures. For the two
renders that do contain a summary, only indentation changed and the h2
text is identical.

Index views deliberately left alone: the action columns vary by what each
controller permits (displays/jobs have Destroy but no Show or Edit,
sessions has no Edit, alerts/events/systems have none). That is the rights
model showing through, not copy-paste drift.

Co-authored-by: openhands <openhands@all-hands.dev>
@dhoelzer
dhoelzer merged commit c0c6e2b into master Nov 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0