Skip to content

feat(gradebook): import header tolerance, column ordering, reassigned-identifier detection, and batched writes#8459

Closed
LWS49 wants to merge 1 commit into
lws49/ext-pr3-grade-validationfrom
lws49/ext-pr4a-import-correctness
Closed

feat(gradebook): import header tolerance, column ordering, reassigned-identifier detection, and batched writes#8459
LWS49 wants to merge 1 commit into
lws49/ext-pr3-grade-validationfrom
lws49/ext-pr4a-import-correctness

Conversation

@LWS49

@LWS49 LWS49 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Backend correctness and performance for the external grade importer. Headers are accepted in any order with duplicate-header reporting and near-miss ("did you mean") suggestions; the preview reports columns in the file's own order plus a row count. Conflicts are regrouped into per-student changed-cell rows, and reassigned identifiers (an External ID now pointing at a different student) are detected. Write performance is improved by preloading student emails (N+1 fix) and batching grade writes.

Design decisions

  • The weightage zeroing guard is placed in the controller, not the import service - the service stays a pure writer, matching where the manual-add path enforces the same rule.
  • Out-of-range detection and structured conflict rows are computed in the service and emitted in the preview payload, keeping the wizard a thin renderer of backend-decided state.

Regression prevention

Covers (Ruby): header order tolerance, duplicate-header reporting, near-miss header suggestion, column ordering from preview, reassigned-identifier detection, email-preload N+1, and batched writes (import service spec + imports controller spec). Backend-only - no frontend change. Specs run on push/CI.

it 'overwrites an existing grade when onConflict is replace' do
post :create, params: base_params.merge(onConflict: 'replace')
post :create, params: base_params.merge(onConflict: 'replace',
csvData: "External ID,Midterm\nA001,99\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Layout/ArgumentAlignment: Align the arguments of a method call if they span more than one line.
[Correctable] Layout/HashAlignment: Align the keys of a hash literal if they span more than one line.

sql.include?(table)
end

ActiveSupport::Notifications.subscribed(counter, 'sql.active_record') { yield }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Style/ExplicitBlockArgument: Consider using explicit block argument in the surrounding method's signature over yield.

expect(error.payload[:suggestions]).to be_empty
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Layout/TrailingWhitespace: Trailing whitespace detected.

expect do
expect do
service(csv_data: csv, components: components).commit(on_conflict: 'replace')
end.to raise_error(described_class::ImportError) do |error|

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/MultilineBlockChain: Avoid multi-line chains of blocks.

component: "Midterms",
identifier: "S123",
grade: -2.0,
kind: "below",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

expect(result[:out_of_range]).to include(
a_hash_including(
component: "Midterms",
identifier: "S123",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

expect(result[:out_of_range]).to include(a_string_matching(/Midterms: 105.* exceeds 100/))
expect(result[:out_of_range]).to include(
a_hash_including(
component: "Midterms",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

it 'caps the sample at 5 rows but reports the true total in total_rows' do
extra = (1..5).map { |i| create(:course_student, course: course, external_id: "X00#{i}") }
ids = ['A001', 'A002'] + extra.map(&:external_id)
csv = "External ID,Midterm\n" + ids.map { |id| "#{id},10" }.join("\n") + "\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style/StringConcatenation: Prefer string interpolation to string concatenation.

csv = "External ID,Midterm\nA001,41\nA002,37\n"
expect { service(csv_data: csv, components: components).preview }.
not_to(change { Course::ExternalAssessmentGrade.count })
to not_change { Course::ExternalAssessmentGrade.count }.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint/AmbiguousBlockAssociation: Parenthesize the param not_change { Course::ExternalAssessment.count } to make sure that the block will be associated with the not_change method call.

describe '#preview' do
it 'writes nothing (dry-run)' do
csv = "External ID,Midterm\nA001,41\nA002,37\n"
csv = "External ID,Midterm\nA001,41\nA002,37\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Correctable] Layout/IndentationWidth: Use 2 (not 1) spaces for indentation.

@LWS49

LWS49 commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #8465 (import correctness + UX consolidated into a single import PR).

@LWS49 LWS49 closed this Jun 28, 2026
@LWS49 LWS49 deleted the lws49/ext-pr4a-import-correctness branch June 28, 2026 14:34
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.

1 participant