-
Notifications
You must be signed in to change notification settings - Fork 0
CP-14012 - Show checkbox and selection marks on PDFs #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,15 +18,22 @@ | |
| { attachment_uuid => doc } | ||
| end | ||
|
|
||
| # Point the submitter's submission at a single text field whose one area is | ||
| # `area`, so `fill_submitter_fields` reaches the page lookup for that area. | ||
| def assign_field_area(area) | ||
| submitter.submission.update!( | ||
| template_fields: [ | ||
| { 'uuid' => SecureRandom.uuid, 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'text', 'areas' => [area] } | ||
| ] | ||
| ) | ||
| def base_area(overrides = {}) | ||
| { | ||
| 'x' => 0.1, | ||
| 'y' => 0.1, | ||
| 'w' => 0.05, | ||
| 'h' => 0.05, | ||
| 'attachment_uuid' => attachment_uuid, | ||
| 'page' => 0 | ||
| }.merge(overrides) | ||
| end | ||
|
|
||
| # Point the submitter's submission at fields so fill_submitter_fields reaches | ||
| # the page lookup and drawing branches under test. | ||
| def assign_fields(fields, values = {}) | ||
| submitter.submission.update!(template_fields: fields) | ||
| submitter.update!(values: values) | ||
| end | ||
|
|
||
| def fill | ||
|
|
@@ -36,9 +43,34 @@ def fill | |
| ) | ||
| end | ||
|
|
||
| def image_xobject_count | ||
| page = pdfs_index[attachment_uuid].pages[0] | ||
| xobjects = page.resources[:XObject] | ||
| return 0 if xobjects.nil? | ||
|
|
||
| count = 0 | ||
| # HexaPDF::Dictionary supports each but not each_value | ||
| xobjects.each { |_name, obj| count += 1 if obj[:Subtype] == :Image } # rubocop:disable Style/HashEachMethods | ||
| count | ||
| end | ||
|
|
||
| def page_content_has_text? | ||
| page = pdfs_index[attachment_uuid].pages[0] | ||
| content = page.contents | ||
| content = content.data if content.respond_to?(:data) | ||
| content.to_s.match?(/Tj|TJ/) | ||
| end | ||
|
|
||
| before { allow(Rails.logger).to receive(:warn) } | ||
|
|
||
| describe '.fill_submitter_fields with a missing area page' do | ||
| def assign_field_area(area) | ||
| assign_fields( | ||
| [{ 'uuid' => SecureRandom.uuid, 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'text', 'areas' => [area] }] | ||
| ) | ||
| end | ||
|
|
||
| context 'when the area omits the page key' do | ||
| before { assign_field_area('attachment_uuid' => attachment_uuid) } | ||
|
|
||
|
|
@@ -78,4 +110,219 @@ def fill | |
| end | ||
| end | ||
| end | ||
|
|
||
| describe '.fill_submitter_fields selection rendering' do | ||
| let(:field_uuid) { SecureRandom.uuid } | ||
|
|
||
| context 'when the field is a checkbox' do | ||
| def assign_checkbox(value) | ||
| assign_fields( | ||
| [{ 'uuid' => field_uuid, 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'checkbox', 'areas' => [base_area] }], | ||
| { field_uuid => value } | ||
| ) | ||
| end | ||
|
|
||
| it 'draws a check when value is boolean true' do | ||
| assign_checkbox(true) | ||
| fill | ||
| expect(image_xobject_count).to eq(1) | ||
| end | ||
|
|
||
| it 'draws a check when value is the string "true" (prefill/API truthiness)' do | ||
| assign_checkbox('true') | ||
| fill | ||
| expect(image_xobject_count).to eq(1) | ||
| end | ||
|
|
||
| it 'draws a check for other truthy string values' do | ||
| %w[1 yes].each do |value| | ||
| doc = HexaPDF::Document.new | ||
| doc.pages.add | ||
| pdfs_index[attachment_uuid] = doc | ||
|
|
||
| assign_checkbox(value) | ||
| fill | ||
| expect(image_xobject_count).to eq(1), "expected check for #{value.inspect}" | ||
| end | ||
| end | ||
|
|
||
| it 'does not draw a check when value is false, "false", or nil' do | ||
| [false, 'false', nil].each do |value| | ||
| doc = HexaPDF::Document.new | ||
| doc.pages.add | ||
| pdfs_index[attachment_uuid] = doc | ||
|
|
||
| assign_checkbox(value) | ||
| fill | ||
| expect(image_xobject_count).to eq(0), "expected no check for #{value.inspect}" | ||
| end | ||
| end | ||
| end | ||
|
|
||
| context 'when the field is a radio with option areas' do | ||
| it 'draws a check only on the matching option area' do | ||
| yes_uuid = SecureRandom.uuid | ||
| no_uuid = SecureRandom.uuid | ||
| options = [ | ||
| { 'uuid' => yes_uuid, 'value' => 'Yes' }, | ||
| { 'uuid' => no_uuid, 'value' => 'No' } | ||
| ] | ||
|
|
||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'radio', | ||
| 'options' => options, | ||
| 'areas' => [ | ||
| base_area('option_uuid' => yes_uuid, 'x' => 0.1), | ||
| base_area('option_uuid' => no_uuid, 'x' => 0.3) | ||
| ] | ||
| }], | ||
| { field_uuid => 'Yes' } | ||
| ) | ||
|
|
||
| fill | ||
| expect(image_xobject_count).to eq(1) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. More of a question here, and not a blocker: the example says "only on the matching option area", but the assertion is just "the page has one image XObject", which would pass just as happily if we drew the check on the The two areas are at
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. Kept the “one check when both areas exist” case, then added a second step: value is Yes but only the No area is present → expect zero checks, so a wrong-option draw would fail. |
||
|
|
||
| # Selected Yes, but only the No area is on the page — must not draw a check. | ||
| doc = HexaPDF::Document.new | ||
| doc.pages.add | ||
| pdfs_index[attachment_uuid] = doc | ||
|
|
||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'radio', | ||
| 'options' => options, | ||
| 'areas' => [base_area('option_uuid' => no_uuid, 'x' => 0.3)] | ||
| }], | ||
| { field_uuid => 'Yes' } | ||
| ) | ||
|
|
||
| fill | ||
| expect(image_xobject_count).to eq(0) | ||
| end | ||
| end | ||
|
|
||
| context 'when the field is multiple with option areas' do | ||
| it 'draws a check on each selected option area' do | ||
| opt_a = SecureRandom.uuid | ||
| opt_b = SecureRandom.uuid | ||
| opt_c = SecureRandom.uuid | ||
|
|
||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'multiple', | ||
| 'options' => [ | ||
| { 'uuid' => opt_a, 'value' => 'A' }, | ||
| { 'uuid' => opt_b, 'value' => 'B' }, | ||
| { 'uuid' => opt_c, 'value' => 'C' } | ||
| ], | ||
| 'areas' => [ | ||
| base_area('option_uuid' => opt_a, 'x' => 0.1), | ||
| base_area('option_uuid' => opt_b, 'x' => 0.2), | ||
| base_area('option_uuid' => opt_c, 'x' => 0.3) | ||
| ] | ||
| }], | ||
| { field_uuid => %w[A B] } | ||
| ) | ||
|
|
||
| fill | ||
| expect(image_xobject_count).to eq(2) | ||
| end | ||
| end | ||
|
|
||
| context 'when a radio option area has a stale option_uuid' do | ||
| let(:stale_option_uuid) { SecureRandom.uuid } | ||
|
|
||
| before do | ||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'radio', | ||
| 'options' => [{ 'uuid' => SecureRandom.uuid, 'value' => 'Yes' }], | ||
| 'areas' => [base_area('option_uuid' => stale_option_uuid)] | ||
| }], | ||
| { field_uuid => 'Yes' } | ||
| ) | ||
| end | ||
|
|
||
| it 'skips the area instead of raising' do | ||
| expect { fill }.not_to raise_error | ||
| expect(image_xobject_count).to eq(0) | ||
| end | ||
|
|
||
| it 'logs that the option area was skipped' do | ||
| fill | ||
| expect(Rails.logger).to have_received(:warn).with( | ||
| /Skipping option area with unknown option_uuid.*option_uuid=#{stale_option_uuid}/ | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| context 'when radio/multiple has a single area without option_uuid' do | ||
| it 'draws selected radio value as text without raising' do | ||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'radio', | ||
| 'options' => [ | ||
| { 'uuid' => SecureRandom.uuid, 'value' => 'Yes' }, | ||
| { 'uuid' => SecureRandom.uuid, 'value' => 'No' } | ||
| ], | ||
| 'areas' => [base_area('w' => 0.3, 'h' => 0.04)] | ||
| }], | ||
| { field_uuid => 'Yes' } | ||
| ) | ||
|
|
||
| expect { fill }.not_to raise_error | ||
| expect(image_xobject_count).to eq(0) | ||
| expect(page_content_has_text?).to be true | ||
| end | ||
|
|
||
| it 'draws selected multiple values as joined text without raising' do | ||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'multiple', | ||
| 'options' => [ | ||
| { 'uuid' => SecureRandom.uuid, 'value' => 'A' }, | ||
| { 'uuid' => SecureRandom.uuid, 'value' => 'B' } | ||
| ], | ||
| 'areas' => [base_area('w' => 0.4, 'h' => 0.04)] | ||
| }], | ||
| { field_uuid => %w[A B] } | ||
| ) | ||
|
|
||
| expect { fill }.not_to raise_error | ||
| expect(image_xobject_count).to eq(0) | ||
| expect(page_content_has_text?).to be true | ||
| end | ||
| end | ||
|
|
||
| context 'when the field is text (regression)' do | ||
| it 'still renders typed text' do | ||
| assign_fields( | ||
| [{ | ||
| 'uuid' => field_uuid, | ||
| 'submitter_uuid' => submitter.uuid, | ||
| 'type' => 'text', | ||
| 'areas' => [base_area('w' => 0.4, 'h' => 0.04)] | ||
| }], | ||
| { field_uuid => 'Hello world' } | ||
| ) | ||
|
|
||
| expect { fill }.not_to raise_error | ||
| expect(page_content_has_text?).to be true | ||
| end | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reusing
NormalizeValues::TRUE_VALUESis the right call here, it's the exact tablenormalize_valueuses for checkboxes so the PDF and the normalized data agree on what "checked" means. 👍🏽Small thought, not a blocker: any reason not to call
Submitters::NormalizeValues.normalize_value(field, value) == trueinstead of reaching for the constant? Keeps the truth table behind one door if it ever grows. Totally your call, the constant reads fine as-is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — keeping the cast on the PDF path for this PR. Write-time normalization for checkbox defaults (and the related condition / nil-guard gaps) is a good follow-up so we don’t expand scope here.