Skip to content
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

Validate batch numbers using separate worksheet(s) #2848

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ group :test do
branch: "main"
gem "capybara-screenshot"
gem "cuprite"
gem "its"
gem "rspec"
gem "rspec-html-matchers"
gem "rubyXL"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ GEM
irb (1.14.3)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
its (0.2.0)
rspec-core
jaro_winkler (1.5.6)
jmespath (1.6.2)
jsbundling-rails (1.3.1)
Expand Down Expand Up @@ -736,6 +738,7 @@ DEPENDENCIES
govuk_design_system_formbuilder
govuk_markdown
hotwire-livereload
its
jsbundling-rails
jsonb_accessor
jwt
Expand Down
52 changes: 31 additions & 21 deletions app/lib/reports/offline_session_exporter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ def call
# stree-ignore
Axlsx::Package
.new { |package|
package.use_shared_strings = true

add_vaccinations_sheet(package)
add_performing_professionals_sheet(package)
add_reference_sheet package,
name: "Performing Professionals",
values_name: "EMAIL",
values: performing_professional_email_values
add_batch_numbers_sheets(package)
}
.to_stream
.read
Expand All @@ -33,8 +39,6 @@ def self.call(*args, **kwargs)
delegate :location, :organisation, to: :session

def add_vaccinations_sheet(package)
package.use_shared_strings = true

workbook = package.workbook

cached_styles = CachedStyles.new(workbook)
Expand All @@ -56,21 +60,23 @@ def add_vaccinations_sheet(package)
end
end

def add_performing_professionals_sheet(package)
package.use_shared_strings = true

def add_reference_sheet(package, name:, values_name:, values:)
workbook = package.workbook
workbook.add_worksheet(
name: "Performing Professionals",
state: :hidden
) do |sheet|
workbook.add_worksheet(name:, state: :hidden) do |sheet|
sheet.sheet_protection

sheet.add_row(%w[EMAIL])
sheet.add_row([values_name])

performing_professional_email_values.each do |email|
sheet.add_row([email])
end
values.each { |value| sheet.add_row([value]) }
end
end

def add_batch_numbers_sheets(package)
session.programmes.map do |programme|
add_reference_sheet package,
name: "#{programme.type} Batch Numbers",
values_name: "NUMBER",
values: batch_values_for_programme(programme)
end
end

Expand Down Expand Up @@ -237,12 +243,11 @@ def add_existing_row_cells(row, vaccination_record:)
)
row[:performing_professional_email] = Cell.new(
vaccination_record.performed_by_user&.email,
allowed_formula: "=#{performing_professionals_range}"
allowed_formula: performing_professionals_range
)
row[:batch_number] = Cell.new(
batch&.name,
allowed_values:
batch_values_for_programme(programme, existing_batch: batch)
allowed_formula: batch_numbers_range_for_programme(programme)
)
row[:batch_expiry_date] = batch&.expiry
row[:anatomical_site] = Cell.new(
Expand Down Expand Up @@ -273,10 +278,10 @@ def add_new_row_cells(row, programme:)
allowed_values: vaccine_values_for_programme(programme)
)
row[:performing_professional_email] = Cell.new(
allowed_formula: "=#{performing_professionals_range}"
allowed_formula: performing_professionals_range
)
row[:batch_number] = Cell.new(
allowed_values: batch_values_for_programme(programme)
allowed_formula: batch_numbers_range_for_programme(programme)
)
row[:batch_expiry_date] = Cell.new(type: :date)
row[:anatomical_site] = Cell.new(
Expand Down Expand Up @@ -321,7 +326,12 @@ def performing_professional_email_values

def performing_professionals_range
count = performing_professional_email_values.count
"='Performing Professionals'!$A2:$A#{count + 1}"
"'Performing Professionals'!$A2:$A#{count + 1}"
end

def batch_numbers_range_for_programme(programme)
count = batch_values_for_programme(programme).count
"'#{programme.type} Batch Numbers'!$A2:$A#{count + 1}"
end

def clinic_name_values
Expand Down Expand Up @@ -406,7 +416,7 @@ def add_data_validation_to(sheet:, column_index:, row_index:)
if allowed_values.present?
"\"#{allowed_values.join(", ")}\""
elsif allowed_formula.present?
allowed_formula
"=#{allowed_formula}"
end

sheet.add_data_validation(
Expand Down
207 changes: 197 additions & 10 deletions spec/lib/reports/offline_session_exporter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ def worksheet_to_hashes(worksheet)
rows.compact
end

def validation_formula(worksheet:, column_name:, row: 1)
column = worksheet[0].cells.find_index { it.value == column_name.upcase }

# stree-ignore
worksheet
.data_validations
.find { |validation|
validation.sqref.any? do
it.col_range.include?(column) && it.row_range.include?(row)
end
}
.formula1
.expression
end

subject(:call) { described_class.call(session) }

let(:programme) { create(:programme, :hpv) }
Expand All @@ -21,13 +36,14 @@ def worksheet_to_hashes(worksheet)
let(:session) { create(:session, location:, organisation:, programme:) }

context "a school session" do
subject(:workbook) { RubyXL::Parser.parse_buffer(call) }

let(:location) { create(:school, team:) }

it { should_not be_blank }

describe "headers" do
subject(:headers) do
workbook = RubyXL::Parser.parse_buffer(call)
sheet = workbook.worksheets[0]
sheet[0].cells.map(&:value)
end
Expand Down Expand Up @@ -77,10 +93,7 @@ def worksheet_to_hashes(worksheet)
end

describe "rows" do
subject(:rows) do
workbook = RubyXL::Parser.parse_buffer(call)
worksheet_to_hashes(workbook.worksheets[0])
end
subject(:rows) { worksheet_to_hashes(workbook.worksheets[0]) }

let(:performed_at) { Time.zone.local(2024, 1, 1, 12, 5, 20) }
let(:batch) { create(:batch, vaccine: programme.vaccines.active.first) }
Expand Down Expand Up @@ -291,16 +304,105 @@ def worksheet_to_hashes(worksheet)
end
end
end

describe "cell validations" do
subject(:worksheet) { workbook.worksheets[0] }

before do
# Without a patient no validation will be setup.
create(:patient, session:)
end

describe "performing professional email" do
subject(:validation) do
create(:user, organisation:, email: "[email protected]")
validation_formula(
worksheet:,
column_name: "performing_professional_email"
)
end

it { should eq "='Performing Professionals'!$A2:$A2" }
end

describe "batch number" do
subject(:validation) do
create(
:batch,
name: "BATCH12345",
vaccine: programme.vaccines.active.first,
organisation:
)
validation_formula(worksheet:, column_name: "batch_number")
end

it { should eq "='hpv Batch Numbers'!$A2:$A2" }
end
end

describe "performing professionals sheet" do
subject(:worksheet) do
workbook.worksheets.find { it.sheet_name == "Performing Professionals" }
end

let!(:vaccinators) { create_list(:user, 2, organisation:) }

before do
create(:patient, session:)
create(
:user,
organisation: create(:organisation),
email: "[email protected]"
)
end

it "lists all the organisation users' emails" do
emails = worksheet[1..].map { it.cells.first.value }
expect(emails).to include(*vaccinators.map(&:email))
end

its(:state) { should eq "hidden" }
its(:sheet_protection) { should be_present }
end

describe "batch numbers sheet" do
subject(:worksheet) do
workbook.worksheets.find { it.sheet_name == "hpv Batch Numbers" }
end

let!(:batches) do
create_list(
:batch,
2,
vaccine: programme.vaccines.active.first,
organisation:
)
end

before do
create(:patient, session:)
create(:batch, name: "OTHERBATCH", vaccine: create(:vaccine, :flu))
end

it "lists all the batch numbers for the programme" do
batch_numbers = worksheet[1..].map { it.cells.first.value }
expect(batch_numbers).to include(*batches.map(&:name))
end

its(:state) { should eq "hidden" }
its(:sheet_protection) { should be_present }
end
end

context "a clinic session" do
subject(:workbook) { RubyXL::Parser.parse_buffer(call) }

let(:location) { create(:generic_clinic, team:) }

it { should_not be_blank }

describe "headers" do
subject(:headers) do
workbook = RubyXL::Parser.parse_buffer(call)
sheet = workbook.worksheets[0]
sheet[0].cells.map(&:value)
end
Expand Down Expand Up @@ -351,10 +453,7 @@ def worksheet_to_hashes(worksheet)
end

describe "rows" do
subject(:rows) do
workbook = RubyXL::Parser.parse_buffer(call)
worksheet_to_hashes(workbook.worksheets[0])
end
subject(:rows) { worksheet_to_hashes(workbook.worksheets[0]) }

it { should be_empty }

Expand Down Expand Up @@ -485,5 +584,93 @@ def worksheet_to_hashes(worksheet)
end
end
end

describe "cell validations" do
subject(:worksheet) { workbook.worksheets[0] }

before do
create(:patient, session:)
create(:user, organisation:, email: "[email protected]")
end

describe "performing professional email" do
subject(:validation) do
worksheet = workbook.worksheets[0]
validation_formula(
worksheet:,
column_name: "performing_professional_email"
)
end

it { should eq "='Performing Professionals'!$A2:$A2" }
end

describe "batch number" do
subject(:validation) do
create(
:batch,
name: "BATCH12345",
vaccine: programme.vaccines.active.first,
organisation:
)
validation_formula(worksheet:, column_name: "batch_number")
end

it { should eq "='hpv Batch Numbers'!$A2:$A2" }
end
end

describe "performing professionals sheet" do
subject(:worksheet) do
workbook.worksheets.find { it.sheet_name == "Performing Professionals" }
end

let!(:vaccinators) { create_list(:user, 2, organisation:) }

before do
create(:patient, session:)
create(
:user,
organisation: create(:organisation),
email: "[email protected]"
)
end

it "lists all the organisation users' emails" do
emails = worksheet[1..].map { it.cells.first.value }
expect(emails).to eq vaccinators.map(&:email)
end

its(:state) { should eq "hidden" }
its(:sheet_protection) { should be_present }
end

describe "batch numbers sheet" do
subject(:worksheet) do
workbook.worksheets.find { it.sheet_name == "hpv Batch Numbers" }
end

let!(:batches) do
create_list(
:batch,
2,
vaccine: programme.vaccines.active.first,
organisation:
)
end

before do
create(:patient, session:)
create(:batch, name: "OTHERBATCH", vaccine: create(:vaccine, :flu))
end

it "lists all the batch numbers for the programme" do
batch_numbers = worksheet[1..].map { it.cells.first.value }
expect(batch_numbers).to eq batches.map(&:name)
end

its(:state) { should eq "hidden" }
its(:sheet_protection) { should be_present }
end
end
end
Loading