From c6009aa445a9d7994e7425cfcbc9d7b410563b63 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 14:42:27 +0100 Subject: [PATCH 01/10] Replace onboard Rake task This replaces the Rake task with a command, part of the Mavis CLI, that allows users to onboard new teams using a path to a YAML configuration file. --- app/lib/mavis_cli.rb | 1 + app/lib/mavis_cli/teams/onboard.rb | 31 ++++++++++ docs/managing-teams.md | 6 +- lib/tasks/onboard.rake | 14 ----- script/set_up_coventry.sh | 8 --- ...t_teams_spec.rb => cli_teams_list_spec.rb} | 2 +- spec/features/cli_teams_onboard_spec.rb | 62 +++++++++++++++++++ 7 files changed, 98 insertions(+), 26 deletions(-) create mode 100644 app/lib/mavis_cli/teams/onboard.rb delete mode 100644 lib/tasks/onboard.rake delete mode 100644 script/set_up_coventry.sh rename spec/features/{cli_list_teams_spec.rb => cli_teams_list_spec.rb} (98%) create mode 100644 spec/features/cli_teams_onboard_spec.rb diff --git a/app/lib/mavis_cli.rb b/app/lib/mavis_cli.rb index 477daf6503..208832a77c 100644 --- a/app/lib/mavis_cli.rb +++ b/app/lib/mavis_cli.rb @@ -32,5 +32,6 @@ def self.progress_bar(total) require_relative "mavis_cli/teams/add_programme" require_relative "mavis_cli/teams/create_sessions" require_relative "mavis_cli/teams/list" +require_relative "mavis_cli/teams/onboard" require_relative "mavis_cli/vaccination_records/generate_fhir" require_relative "mavis_cli/vaccination_records/sync" diff --git a/app/lib/mavis_cli/teams/onboard.rb b/app/lib/mavis_cli/teams/onboard.rb new file mode 100644 index 0000000000..e14cee67b0 --- /dev/null +++ b/app/lib/mavis_cli/teams/onboard.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module MavisCLI + module Teams + class Onboard < Dry::CLI::Command + desc "Onboard a new team" + + argument :path, + required: true, + desc: "The path to the onboarding configuration file" + + def call(path:) + MavisCLI.load_rails + + config = YAML.safe_load(File.read(path)) + + onboarding = Onboarding.new(config) + + if onboarding.valid? + onboarding.save! + else + onboarding.errors.full_messages.each { |message| puts message } + end + end + end + end + + register "teams" do |prefix| + prefix.register "onboard", Teams::Onboard + end +end diff --git a/docs/managing-teams.md b/docs/managing-teams.md index 9b71611903..ae07df45a3 100644 --- a/docs/managing-teams.md +++ b/docs/managing-teams.md @@ -46,12 +46,12 @@ clinics: [config-onboarding]: /config/onboarding -### Rake task +### Command -Once the file has been written you can use the `onboard` Rake task to set everything up in the service. +Once the file has been written you can use the `onboard` command to set everything up in the service. ```sh -$ bundle exec rails onboard[path/to/configuration.yaml] +$ bin/mavis teams onboard path/to/configuration.yaml ``` If any validation errors are detected in the file they will be output and nothing will be processed, only if the file is completely valid will anything be processed. diff --git a/lib/tasks/onboard.rake b/lib/tasks/onboard.rake deleted file mode 100644 index 60d3a18e3c..0000000000 --- a/lib/tasks/onboard.rake +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -desc "Onboard a team from a configuration file." -task :onboard, [:filename] => :environment do |_, args| - config = YAML.safe_load(File.read(args[:filename])) - - onboarding = Onboarding.new(config) - - if onboarding.valid? - onboarding.save! - else - onboarding.errors.full_messages.each { |message| puts message } - end -end diff --git a/script/set_up_coventry.sh b/script/set_up_coventry.sh deleted file mode 100644 index 51074ef2a7..0000000000 --- a/script/set_up_coventry.sh +++ /dev/null @@ -1,8 +0,0 @@ -set -eux - -bin/rails db:schema:load - -bin/rails vaccines:seed[hpv] -bin/rails gp_practices:import -bin/rails schools:import -bin/rails onboard[config/onboarding/coventry-model-office.yaml] diff --git a/spec/features/cli_list_teams_spec.rb b/spec/features/cli_teams_list_spec.rb similarity index 98% rename from spec/features/cli_list_teams_spec.rb rename to spec/features/cli_teams_list_spec.rb index 1a4220bad1..0729b7ca04 100644 --- a/spec/features/cli_list_teams_spec.rb +++ b/spec/features/cli_teams_list_spec.rb @@ -2,7 +2,7 @@ require_relative "../../app/lib/mavis_cli" -describe "mavis list teams" do +describe "mavis teams list" do it "lists all teams" do given_a_couple_organisations_exist and_there_are_teams_in_the_organisations diff --git a/spec/features/cli_teams_onboard_spec.rb b/spec/features/cli_teams_onboard_spec.rb new file mode 100644 index 0000000000..bda6359dc4 --- /dev/null +++ b/spec/features/cli_teams_onboard_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require_relative "../../app/lib/mavis_cli" + +describe "mavis teams onboard" do + context "with a valid configuration" do + it "runs successfully" do + given_programmes_and_schools_exist + when_i_run_the_valid_command + then_i_see_no_output + and_a_new_team_is_created + end + end + + context "with an invalid configuration" do + it "displays an error message" do + when_i_run_the_invalid_command + then_i_see_an_error_message + end + end + + def command_with_valid_configuration + Dry::CLI.new(MavisCLI).call( + arguments: %w[teams onboard spec/fixtures/files/onboarding/valid.yaml] + ) + end + + def command_with_invalid_configuration + Dry::CLI.new(MavisCLI).call( + arguments: %w[teams onboard spec/fixtures/files/onboarding/invalid.yaml] + ) + end + + def given_programmes_and_schools_exist + create(:programme, :hpv) + + create(:school, :secondary, :open, urn: "123456") + create(:school, :secondary, :open, urn: "234567") + create(:school, :secondary, :open, urn: "345678") + create(:school, :secondary, :open, urn: "456789") + end + + def when_i_run_the_valid_command + @output = capture_output { command_with_valid_configuration } + end + + def when_i_run_the_invalid_command + @output = capture_output { command_with_invalid_configuration } + end + + def then_i_see_no_output + expect(@output).to be_empty + end + + def and_a_new_team_is_created + expect(Team.count).to eq(1) + end + + def then_i_see_an_error_message + expect(@output).to include("Programmes can't be blank") + end +end From ec9a736bc62d6cebb4d79b2b879cf75e49c33d93 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 14:53:23 +0100 Subject: [PATCH 02/10] Fix syntax of onboarding configuration The ODS code now belongs in an `organisation` block and the `team` block needs a workgroup. --- config/onboarding/coventry-training.yaml | 5 ++++- config/onboarding/hertfordshire-training.yaml | 5 ++++- config/onboarding/leicestershire-training.yaml | 5 ++++- config/onboarding/smoke-test.yml | 4 +++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/config/onboarding/coventry-training.yaml b/config/onboarding/coventry-training.yaml index 2bd79391b9..1b5027ea6e 100644 --- a/config/onboarding/coventry-training.yaml +++ b/config/onboarding/coventry-training.yaml @@ -1,8 +1,11 @@ +organisation: + ods_code: RYG + team: + workgroup: coventrytraining name: Coventry and Warwickshire Partnership NHS Trust email: example@covwarkpt.nhs.uk phone: 07700 900815 - ods_code: RYG careplus_venue_code: CWPTSI privacy_notice_url: https://www.covwarkpt.nhs.uk/download.cfm?ver=8286 privacy_policy_url: https://www.covwarkpt.nhs.uk/privacy diff --git a/config/onboarding/hertfordshire-training.yaml b/config/onboarding/hertfordshire-training.yaml index 1bafdb21ac..42eba7e534 100644 --- a/config/onboarding/hertfordshire-training.yaml +++ b/config/onboarding/hertfordshire-training.yaml @@ -1,8 +1,11 @@ +organisation: + ods_code: RY4 + team: + workgroup: hertfordshiretraining name: Hertfordshire and East Anglia Community School Age Immunisation Service email: hct.csaisherts@nhs.net phone: 0300 555 5055 - ods_code: RY4 careplus_venue_code: UNUSED privacy_notice_url: https://www.hct.nhs.uk/privacy privacy_policy_url: https://www.hct.nhs.uk/privacy diff --git a/config/onboarding/leicestershire-training.yaml b/config/onboarding/leicestershire-training.yaml index b4f755d892..24146e342b 100644 --- a/config/onboarding/leicestershire-training.yaml +++ b/config/onboarding/leicestershire-training.yaml @@ -1,8 +1,11 @@ +organisation: + ods_code: RT5 + team: + workgroup: leicestershiretraining name: Leicestershire Partnership Trust School Aged Immunisation Service email: lpt.sais@nhs.net phone: 0300 3000 007 - ods_code: RT5 careplus_venue_code: UNUSED privacy_notice_url: https://www.leicspart.nhs.uk/privacy-policy/ privacy_policy_url: https://www.leicspart.nhs.uk/privacy-policy/ diff --git a/config/onboarding/smoke-test.yml b/config/onboarding/smoke-test.yml index 8c5fec7229..b7683834af 100644 --- a/config/onboarding/smoke-test.yml +++ b/config/onboarding/smoke-test.yml @@ -1,8 +1,10 @@ +organisation: + ods_code: Y90128 + team: name: XXX SMOKE TEST XXX email: england.mavis@nhs.net phone: "03003112233" - ods_code: Y90128 careplus_venue_code: SMOKE privacy_policy_url: https://example.com/privacy From 15288002d075322941e34b016f0fdb94f9b62b93 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:10:04 +0100 Subject: [PATCH 03/10] Add rollover training onboarding configuration This adds an onboarding configuration file that can be used to set up a team for end to end testing the rollover functionality. --- config/onboarding/rollover-training.yaml | 40 ++++++++++++++++++++++++ script/rollover_training/set_up.sh | 6 ++++ 2 files changed, 46 insertions(+) create mode 100644 config/onboarding/rollover-training.yaml create mode 100644 script/rollover_training/set_up.sh diff --git a/config/onboarding/rollover-training.yaml b/config/onboarding/rollover-training.yaml new file mode 100644 index 0000000000..eaa80a2b5d --- /dev/null +++ b/config/onboarding/rollover-training.yaml @@ -0,0 +1,40 @@ +organisation: + ods_code: RLVTRN + +team: + workgroup: rollovertraining + name: Rollover Training + email: rollover@example.com + phone: 01234 567890 + careplus_venue_code: ROLLOVER + privacy_notice_url: https://www.example.com/privacy-notice + privacy_policy_url: https://www.example.com/privacy-policy + +programmes: [flu, hpv, menacwy, td_ipv] + +subteams: + generic: + name: Rollover Training + email: rollover@example.com + phone: 01234 567890 + +users: + - email: nurse.rollover@example.com + password: nurse.rollover@example.com + given_name: Nurse + family_name: ROLLOVER + +schools: + generic: + - 115529 # primary + - 131806 # primary + - 145212 # primary + - 118853 # primary + - 145482 # secondary + - 137236 # secondary + - 136642 # secondary + - 135970 # secondary + +clinics: + generic: + - name: Rollover Training Hospital diff --git a/script/rollover_training/set_up.sh b/script/rollover_training/set_up.sh new file mode 100644 index 0000000000..b1a2f62d8f --- /dev/null +++ b/script/rollover_training/set_up.sh @@ -0,0 +1,6 @@ +set -eu + +bin/rails vaccines:seed +bin/mavis gias import +bin/rails gp_practices:import +bin/mavis teams onboard config/onboarding/rollover-training.yaml From 38008089df0951a3ddfb7331e1210a66fb70891e Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:18:21 +0100 Subject: [PATCH 04/10] Turn off preparation period in QA This will temporarily turn off the preparation period in QA to allow us to end-to-end test rollover by starting with the preparation period disabled. We could temporarily set the value in AWS, but this allows us to continue deploying changes to QA while the testing is happening. --- terraform/app/env/qa.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/app/env/qa.tfvars b/terraform/app/env/qa.tfvars index 3750bb4578..166a3a66fe 100644 --- a/terraform/app/env/qa.tfvars +++ b/terraform/app/env/qa.tfvars @@ -14,7 +14,7 @@ enable_cis2 = false enable_pds_enqueue_bulk_updates = false # Normally this is 31, but this gives us 2 weeks of additional testing. -academic_year_number_of_preparation_days = 45 +academic_year_number_of_preparation_days = 14 http_hosts = { MAVIS__HOST = "qa.mavistesting.com" From 72e934489aee2cebf8bfa7923f067fbc28950e87 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:31:10 +0100 Subject: [PATCH 05/10] Reduce generate class indentation By defining the classes as `Generate::Class`, we can avoid the indentation caused by the `module Generate` line. --- lib/generate/cohort_imports.rb | 295 ++++++++++++++-------------- lib/generate/consents.rb | 224 +++++++++++---------- lib/generate/triages.rb | 104 +++++----- lib/generate/vaccination_records.rb | 176 ++++++++--------- 4 files changed, 396 insertions(+), 403 deletions(-) diff --git a/lib/generate/cohort_imports.rb b/lib/generate/cohort_imports.rb index fe005c6660..0c21b02c1a 100644 --- a/lib/generate/cohort_imports.rb +++ b/lib/generate/cohort_imports.rb @@ -30,170 +30,169 @@ # team = org.teams.find_by(name: "") # team.locations.school.pluck(:urn, :year_groups) .to_h # -module Generate - class CohortImports - attr_reader :ods_code, - :team, - :programme, - :urns, - :patient_count, - :school_year_groups, - :progress_bar - - def initialize( - ods_code: "A9A5A", - programme: "hpv", - urns: nil, - school_year_groups: nil, - patient_count: 10, - progress_bar: nil - ) - @team = Team.joins(:organisation).find_by(organisation: { ods_code: }) - @programme = Programme.find_by(type: programme) - @urns = - urns || @team.locations.select { it.urn.present? }.sample(3).pluck(:urn) - @school_year_groups = school_year_groups - @patient_count = patient_count - @progress_bar = progress_bar - @nhs_numbers = Set.new - end - def self.call(...) = new(...).call +class Generate::CohortImports + def initialize( + ods_code: "A9A5A", + programme: "hpv", + urns: nil, + school_year_groups: nil, + patient_count: 10, + progress_bar: nil + ) + @team = Team.joins(:organisation).find_by(organisation: { ods_code: }) + @programme = Programme.find_by(type: programme) + @urns = + urns || @team.locations.select { it.urn.present? }.sample(3).pluck(:urn) + @school_year_groups = school_year_groups + @patient_count = patient_count + @progress_bar = progress_bar + @nhs_numbers = Set.new + end - def call - write_cohort_import_csv - end + def self.call(...) = new(...).call - def patients - patient_count.times.lazy.map { build_patient } - end + def call + write_cohort_import_csv + end - private - - delegate :organisation, to: :team - - def cohort_import_csv_filepath - timestamp = Time.current.strftime("%Y%m%d%H%M%S") - size = - ActiveSupport::NumberHelper.number_to_human( - @patient_count, - units: { - thousand: "k", - million: "m" - }, - format: "%n%u" - ) - Rails.root.join( - "tmp/cohort-import-" \ - "#{organisation.ods_code}-#{programme.type}-#{size}-#{timestamp}.csv" + def patients + patient_count.times.lazy.map { build_patient } + end + + private + + attr_reader :ods_code, + :team, + :programme, + :urns, + :patient_count, + :school_year_groups, + :progress_bar + + delegate :organisation, to: :team + + def cohort_import_csv_filepath + timestamp = Time.current.strftime("%Y%m%d%H%M%S") + size = + ActiveSupport::NumberHelper.number_to_human( + @patient_count, + units: { + thousand: "k", + million: "m" + }, + format: "%n%u" ) - end + Rails.root.join( + "tmp/cohort-import-" \ + "#{organisation.ods_code}-#{programme.type}-#{size}-#{timestamp}.csv" + ) + end - def write_cohort_import_csv - CSV.open(cohort_import_csv_filepath, "w") do |csv| - csv << %w[ - CHILD_ADDRESS_LINE_1 - CHILD_ADDRESS_LINE_2 - CHILD_POSTCODE - CHILD_TOWN - CHILD_PREFERRED_GIVEN_NAME - CHILD_DATE_OF_BIRTH - CHILD_FIRST_NAME - CHILD_LAST_NAME - CHILD_NHS_NUMBER - PARENT_1_EMAIL - PARENT_1_NAME - PARENT_1_PHONE - PARENT_1_RELATIONSHIP - PARENT_2_EMAIL - PARENT_2_NAME - PARENT_2_PHONE - PARENT_2_RELATIONSHIP - CHILD_SCHOOL_URN + def write_cohort_import_csv + CSV.open(cohort_import_csv_filepath, "w") do |csv| + csv << %w[ + CHILD_ADDRESS_LINE_1 + CHILD_ADDRESS_LINE_2 + CHILD_POSTCODE + CHILD_TOWN + CHILD_PREFERRED_GIVEN_NAME + CHILD_DATE_OF_BIRTH + CHILD_FIRST_NAME + CHILD_LAST_NAME + CHILD_NHS_NUMBER + PARENT_1_EMAIL + PARENT_1_NAME + PARENT_1_PHONE + PARENT_1_RELATIONSHIP + PARENT_2_EMAIL + PARENT_2_NAME + PARENT_2_PHONE + PARENT_2_RELATIONSHIP + CHILD_SCHOOL_URN + ] + + patients.each do |patient| + csv << [ + patient.address_line_1, + patient.address_line_2, + patient.address_postcode, + patient.address_town, + patient.preferred_given_name, + patient.date_of_birth, + patient.given_name, + patient.family_name, + patient.nhs_number, + patient.parents.first&.email, + patient.parents.first&.full_name, + patient.parents.first&.phone, + patient.parent_relationships.first&.type, + patient.parents.second&.email, + patient.parents.second&.full_name, + patient.parents.second&.phone, + patient.parent_relationships.second&.type, + patient.school.urn ] - - patients.each do |patient| - csv << [ - patient.address_line_1, - patient.address_line_2, - patient.address_postcode, - patient.address_town, - patient.preferred_given_name, - patient.date_of_birth, - patient.given_name, - patient.family_name, - patient.nhs_number, - patient.parents.first&.email, - patient.parents.first&.full_name, - patient.parents.first&.phone, - patient.parent_relationships.first&.type, - patient.parents.second&.email, - patient.parents.second&.full_name, - patient.parents.second&.phone, - patient.parent_relationships.second&.type, - patient.school.urn - ] - progress_bar&.increment - end + progress_bar&.increment end - cohort_import_csv_filepath.to_s end + cohort_import_csv_filepath.to_s + end - def schools_with_year_groups - @schools_with_year_groups ||= - begin - locations = - if school_year_groups.present? - urns.map do |urn| - Location.new(urn:, year_groups: school_year_groups[urn]) - end - else - team.locations.where(urn: urns).includes(:team, :sessions) + def schools_with_year_groups + @schools_with_year_groups ||= + begin + locations = + if school_year_groups.present? + urns.map do |urn| + Location.new(urn:, year_groups: school_year_groups[urn]) end - locations.select do - (it.year_groups & programme.default_year_groups).any? + else + team.locations.where(urn: urns).includes(:team, :sessions) end + locations.select do + (it.year_groups & programme.default_year_groups).any? end - end - - def build_patient - school = schools_with_year_groups.sample - year_group ||= (school.year_groups & programme.default_year_groups).sample - nhs_number = nil - loop do - nhs_number = Faker::NationalHealthService.british_number.gsub(" ", "") - break unless nhs_number.in? @nhs_numbers end - @nhs_numbers << nhs_number - - FactoryBot - .build( - :patient, - school:, - team:, - date_of_birth: date_of_birth_for_year(year_group), - nhs_number: - ) - .tap do |patient| - patient.parents = - FactoryBot.build_list(:parent, 2, family_name: patient.family_name) - patient.parent_relationships = - patient.parents.map do - FactoryBot.build(:parent_relationship, parent: it, patient:) - end - end - end + end - def date_of_birth_for_year(year_group, academic_year: AcademicYear.pending) - if year_group < 12 - rand( - year_group.to_birth_academic_year( - academic_year: - ).to_academic_year_date_range - ) - else - raise "Unknown year group: #{year_group}" + def build_patient + school = schools_with_year_groups.sample + year_group ||= (school.year_groups & programme.default_year_groups).sample + nhs_number = nil + loop do + nhs_number = Faker::NationalHealthService.british_number.gsub(" ", "") + break unless nhs_number.in? @nhs_numbers + end + @nhs_numbers << nhs_number + + FactoryBot + .build( + :patient, + school:, + team:, + date_of_birth: date_of_birth_for_year(year_group), + nhs_number: + ) + .tap do |patient| + patient.parents = + FactoryBot.build_list(:parent, 2, family_name: patient.family_name) + patient.parent_relationships = + patient.parents.map do + FactoryBot.build(:parent_relationship, parent: it, patient:) + end end + end + + def date_of_birth_for_year(year_group, academic_year: AcademicYear.pending) + if year_group < 12 + rand( + year_group.to_birth_academic_year( + academic_year: + ).to_academic_year_date_range + ) + else + raise "Unknown year group: #{year_group}" end end end diff --git a/lib/generate/consents.rb b/lib/generate/consents.rb index 037392b478..2d7d924c20 100644 --- a/lib/generate/consents.rb +++ b/lib/generate/consents.rb @@ -1,131 +1,129 @@ # frozen_string_literal: true -module Generate - class Consents - attr_reader :team, :programme - - def initialize( - team:, - programme: nil, - session: nil, - refused: 0, - given: 0, - given_needs_triage: 0 - ) - validate_programme_and_session(programme, session) if programme - - @team = team - @programme = programme || team.programmes.sample - @session = session - @refused = refused - @given = given - @given_needs_triage = given_needs_triage - @updated_patients = [] - @updated_sessions = Set.new - end +class Generate::Consents + def initialize( + team:, + programme: nil, + session: nil, + refused: 0, + given: 0, + given_needs_triage: 0 + ) + validate_programme_and_session(programme, session) if programme + + @team = team + @programme = programme || team.programmes.sample + @session = session + @refused = refused + @given = given + @given_needs_triage = given_needs_triage + @updated_patients = [] + @updated_sessions = Set.new + end - def call - create_consents(:refused, @refused) - create_consents(:given, @given) - create_consents(:needing_triage, @given_needs_triage) + def call + create_consents(:refused, @refused) + create_consents(:given, @given) + create_consents(:needing_triage, @given_needs_triage) - StatusUpdater.call(patient: @updated_patients, session: @updated_sessions) - end + StatusUpdater.call(patient: @updated_patients, session: @updated_sessions) + end - def self.call(...) = new(...).call - - private - - def patients - @patients ||= - begin - sessions = - if @session - [@session] - else - team - .sessions - .eager_load(:location) - .merge(Location.school) - .has_programmes([programme]) - end - - sessions.flat_map do |session| - session - .patients - .includes(:parents, :school, :consents, consents: :parent) - .select { it.consents.empty? && it.parents.any? } - end - end - end + def self.call(...) = new(...).call - def random_patients(count) - @patients_randomised ||= patients.shuffle - @patients_randomised - .shift(count) - .tap do - if it.size < count - raise "Only #{it.size} patients without consent in #{programme.type} programme" - end - end - end + private - def session_for(patient) - @session || - patient - .sessions - .eager_load(:location) - .merge(Location.school) - .has_programmes([programme]) - .sample - end + attr_reader :team, :programme - def create_consents(response, count) - available_patient_sessions = - random_patients(count).map { [it, session_for(it)] } + def patients + @patients ||= + begin + sessions = + if @session + [@session] + else + team + .sessions + .eager_load(:location) + .merge(Location.school) + .has_programmes([programme]) + end - if response == :needing_triage - response = :given - traits = %i[given needing_triage] - else - traits = [response] + sessions.flat_map do |session| + session + .patients + .includes(:parents, :school, :consents, consents: :parent) + .select { it.consents.empty? && it.parents.any? } + end end + end - consents = - available_patient_sessions.map do |patient, session| - school = session.location.school? ? session.location : patient.school - - @updated_patients << patient - @updated_sessions << session - - FactoryBot.build( - :consent, - *traits, - patient:, - programme:, - team:, - consent_form: - FactoryBot.build( - :consent_form, - team:, - programmes: [programme], - session:, - school:, - response: - ) - ) + def random_patients(count) + @patients_randomised ||= patients.shuffle + @patients_randomised + .shift(count) + .tap do + if it.size < count + raise "Only #{it.size} patients without consent in #{programme.type} programme" end - Consent.import!(consents, recursive: true) + end + end + + def session_for(patient) + @session || + patient + .sessions + .eager_load(:location) + .merge(Location.school) + .has_programmes([programme]) + .sample + end + + def create_consents(response, count) + available_patient_sessions = + random_patients(count).map { [it, session_for(it)] } + + if response == :needing_triage + response = :given + traits = %i[given needing_triage] + else + traits = [response] end - def validate_programme_and_session(programme, session) - if session - if session.programmes.exclude?(programme) - raise "Session does not support programme #{programme.type}" - end - elsif programme.sessions.none? { it.location.school? } - raise "Programme #{programme.type} does not have a school session" + consents = + available_patient_sessions.map do |patient, session| + school = session.location.school? ? session.location : patient.school + + @updated_patients << patient + @updated_sessions << session + + FactoryBot.build( + :consent, + *traits, + patient:, + programme:, + team:, + consent_form: + FactoryBot.build( + :consent_form, + team:, + programmes: [programme], + session:, + school:, + response: + ) + ) + end + Consent.import!(consents, recursive: true) + end + + def validate_programme_and_session(programme, session) + if session + if session.programmes.exclude?(programme) + raise "Session does not support programme #{programme.type}" end + elsif programme.sessions.none? { it.location.school? } + raise "Programme #{programme.type} does not have a school session" end end end diff --git a/lib/generate/triages.rb b/lib/generate/triages.rb index c3b44ad0ef..f06c9c69fb 100644 --- a/lib/generate/triages.rb +++ b/lib/generate/triages.rb @@ -1,68 +1,66 @@ # frozen_string_literal: true -module Generate - class Triages - attr_reader :config, :team, :programme +class Generate::Triages + def initialize( + team:, + programme: nil, + session: nil, + ready_to_vaccinate: 1, + do_not_vaccinate: 1 + ) + @team = team + @programme = programme || team.programmes.sample + @session = session + @ready_to_vaccinate = ready_to_vaccinate + @do_not_vaccinate = do_not_vaccinate + end - def initialize( - team:, - programme: nil, - session: nil, - ready_to_vaccinate: 1, - do_not_vaccinate: 1 - ) - @team = team - @programme = programme || team.programmes.sample - @session = session - @ready_to_vaccinate = ready_to_vaccinate - @do_not_vaccinate = do_not_vaccinate - end + def call + create_triage_with_status(:ready_to_vaccinate, @ready_to_vaccinate) + create_triage_with_status(:do_not_vaccinate, @do_not_vaccinate) + end - def call - create_triage_with_status(:ready_to_vaccinate, @ready_to_vaccinate) - create_triage_with_status(:do_not_vaccinate, @do_not_vaccinate) - end + def self.call(...) = new(...).call - def self.call(...) = new(...).call + private - private + attr_reader :team, :programme - def academic_year = Date.current.academic_year + def academic_year = Date.current.academic_year - def patients - (@session.presence || team) - .patients - .includes(:triage_statuses) - .appear_in_programmes([programme], academic_year:) - .select { it.triage_status(programme:, academic_year:).required? } - end + def patients + (@session.presence || team) + .patients + .includes(:triage_statuses) + .appear_in_programmes([programme], academic_year:) + .select { it.triage_status(programme:, academic_year:).required? } + end - def random_patients(count) - patients - .shuffle - .take(count) - .tap do - raise "Not enough patients to generate triages" if it.size < count - end - end + def random_patients(count) + patients + .shuffle + .take(count) + .tap do + raise "Not enough patients to generate triages" if it.size < count + end + end - def user - @user ||= team.users.includes(:teams).sample - end + def user + @user ||= team.users.includes(:teams).sample + end - def create_triage_with_status(status, count) - available_patients = random_patients(count) + def create_triage_with_status(status, count) + available_patients = random_patients(count) - available_patients.each do |patient| - FactoryBot.create( - :triage, - status, - patient:, - programme:, - performed_by: user, - team: - ) - end + available_patients.each do |patient| + FactoryBot.create( + :triage, + status, + patient:, + programme:, + performed_by: user, + team: + ) end end end diff --git a/lib/generate/vaccination_records.rb b/lib/generate/vaccination_records.rb index cd2cf1a44f..da86227814 100644 --- a/lib/generate/vaccination_records.rb +++ b/lib/generate/vaccination_records.rb @@ -1,112 +1,110 @@ # frozen_string_literal: true -module Generate - class VaccinationRecords - attr_reader :config, :team, :programme, :session, :administered - - def initialize(team:, programme: nil, session: nil, administered: nil) - @team = team - @programme = programme || team.programmes.includes(:teams).sample - @session = session - @administered = administered - end - - def call - create_vaccinations - end +class Generate::VaccinationRecords + def initialize(team:, programme: nil, session: nil, administered: nil) + @team = team + @programme = programme || team.programmes.includes(:teams).sample + @session = session + @administered = administered + end - def self.call(...) = new(...).call + def call + create_vaccinations + end - private + def self.call(...) = new(...).call - def create_vaccinations - session_attendances = [] - vaccination_records = [] + private - random_patient_sessions.each do |patient_session| - patient_session_id = patient_session.id - session_date_ids = patient_session.session.session_dates.pluck(:id) + attr_reader :config, :team, :programme, :session, :administered - unless SessionAttendance.exists?( - patient_session_id:, - session_date_id: session_date_ids - ) - session_attendances << FactoryBot.build( - :session_attendance, - :present, - patient_session: - ) - end + def create_vaccinations + session_attendances = [] + vaccination_records = [] - location_name = - patient_session.location.name if patient_session.session.clinic? + random_patient_sessions.each do |patient_session| + patient_session_id = patient_session.id + session_date_ids = patient_session.session.session_dates.pluck(:id) - vaccination_records << FactoryBot.build( - :vaccination_record, - :administered, - patient: patient_session.patient, - programme:, - team:, - performed_by:, - session: patient_session.session, - vaccine:, - batch:, - location_name: + unless SessionAttendance.exists?( + patient_session_id:, + session_date_id: session_date_ids + ) + session_attendances << FactoryBot.build( + :session_attendance, + :present, + patient_session: ) end - SessionAttendance.import!(session_attendances) - VaccinationRecord.import!(vaccination_records) - - StatusUpdater.call(patient: vaccination_records.map(&:patient)) + location_name = + patient_session.location.name if patient_session.session.clinic? + + vaccination_records << FactoryBot.build( + :vaccination_record, + :administered, + patient: patient_session.patient, + programme:, + team:, + performed_by:, + session: patient_session.session, + vaccine:, + batch:, + location_name: + ) end - def random_patient_sessions - if administered&.positive? - patient_sessions - .sample(administered) - .tap do |selected| - if selected.size < administered - info = - "#{selected.size} (patient_sessions) < #{administered} (administered)" - raise "Not enough patients to generate vaccinations: #{info}" - end + SessionAttendance.import!(session_attendances) + VaccinationRecord.import!(vaccination_records) + + StatusUpdater.call(patient: vaccination_records.map(&:patient)) + end + + def random_patient_sessions + if administered&.positive? + patient_sessions + .sample(administered) + .tap do |selected| + if selected.size < administered + info = + "#{selected.size} (patient_sessions) < #{administered} (administered)" + raise "Not enough patients to generate vaccinations: #{info}" end - else - patient_sessions - end + end + else + patient_sessions end + end - def patient_sessions - (session.presence || team) - .patient_sessions - .joins(:patient) - .includes( - :session, - :location, - session: :session_dates, - patient: %i[consent_statuses vaccination_statuses triage_statuses] + def patient_sessions + (session.presence || team) + .patient_sessions + .joins(:patient) + .includes( + :session, + :location, + session: :session_dates, + patient: %i[consent_statuses vaccination_statuses triage_statuses] + ) + .appear_in_programmes([programme]) + .has_consent_status("given", programme:) + .select do + it.patient.consent_given_and_safe_to_vaccinate?( + programme:, + academic_year: it.session.academic_year ) - .appear_in_programmes([programme]) - .has_consent_status("given", programme:) - .select do - it.patient.consent_given_and_safe_to_vaccinate?( - programme:, - academic_year: it.session.academic_year - ) - end - end + end + end - def vaccine - (@vaccines ||= programme.vaccines.includes(:batches).active).first - end + def vaccine + (@vaccines ||= programme.vaccines.includes(:batches).active).first + end - def batch - (@batches ||= vaccine.batches).sample - end + def batch + (@batches ||= vaccine.batches).sample + end - def performed_by - (@team_users ||= team.users.includes(:teams)).sample - end + def performed_by + (@team_users ||= team.users.includes(:teams)).sample end end From a1e6e1a762426c864eb1bd07229ca2f8f0110c44 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:43:43 +0100 Subject: [PATCH 06/10] Update Generate::CohortImports interface This updates the interface of the `Generate::CohortImports` class to match the other classes in the `Generate` module. Specifically it's updated to take a team and a programme, and the CLI class is responsible for parsing these values. --- app/lib/mavis_cli/generate/cohort_imports.rb | 28 +++++++++---- lib/generate/cohort_imports.rb | 42 +++---------------- .../cli_generate_cohort_imports_spec.rb | 20 ++++----- spec/lib/generate/cohort_imports_spec.rb | 9 ++-- 4 files changed, 38 insertions(+), 61 deletions(-) diff --git a/app/lib/mavis_cli/generate/cohort_imports.rb b/app/lib/mavis_cli/generate/cohort_imports.rb index 6f27b312d4..b3976ba482 100644 --- a/app/lib/mavis_cli/generate/cohort_imports.rb +++ b/app/lib/mavis_cli/generate/cohort_imports.rb @@ -6,28 +6,38 @@ module MavisCLI module Generate class CohortImports < Dry::CLI::Command desc "Generate cohort imports" - option :patients, + + option :team_workgroup, + aliases: ["-w"], + default: "A9A5A", + desc: "Workgroup of team to generate consents for" + + option :programme_type, + aliases: ["-p"], + default: "hpv", + desc: + "Programme type to generate consents for (hpv, menacwy, td_ipv, etc)" + + option :patient_count, + aliases: ["-c"], type: :integer, required: true, default: 10, desc: "Number of patients to create" - option :ods_code, - type: :string, - default: "A9A5A", - desc: "ODS code of the organisation to use for the cohort import" - def call(patients:, ods_code:) + def call(team_workgroup:, programme_type:, patient_count:) MavisCLI.load_rails - patient_count = patients.to_i + patient_count = patient_count.to_i progress_bar = MavisCLI.progress_bar(patient_count) - puts "Generating cohort import for ods code #{ods_code} with" \ + puts "Generating cohort import for team #{team_workgroup} with" \ " #{patient_count} patients..." result = ::Generate::CohortImports.call( - ods_code:, + team: Team.find_by(workgroup: team_workgroup), + programme: Programme.find_by(type: programme_type), patient_count:, progress_bar: ) diff --git a/lib/generate/cohort_imports.rb b/lib/generate/cohort_imports.rb index 0c21b02c1a..4058ba174c 100644 --- a/lib/generate/cohort_imports.rb +++ b/lib/generate/cohort_imports.rb @@ -4,44 +4,17 @@ Faker::Config.locale = "en-GB" -# Use this to generate a cohort import CSV file for performance testing. -# -# Usage from the Rails console: -# -# Create a cohort import of 1000 children for all the school sessions for the -# org A9A5A in the local db: -# -# Generate::CohortImports.call(patient_count: 1000) -# -# You can also generate a cohort import for sessions not in the local db. -# -# Generate::CohortImports.call( -# patient_count: 1000, -# urns: ["123456", "987654"], -# school_year_groups: { -# "123456" => [-2, -1, 0, 1, 2, 3, 4, 5, 6], -# "987654" => [9, 10, 11, 12, 13] -# } -# ) -# -# You can pull out the year groups with the following: -# -# org = Organisation.find_by(ods_code: "A9A5A") -# team = org.teams.find_by(name: "") -# team.locations.school.pluck(:urn, :year_groups) .to_h -# - class Generate::CohortImports def initialize( - ods_code: "A9A5A", - programme: "hpv", + team:, + programme: nil, urns: nil, school_year_groups: nil, patient_count: 10, progress_bar: nil ) - @team = Team.joins(:organisation).find_by(organisation: { ods_code: }) - @programme = Programme.find_by(type: programme) + @team = team + @programme = programme || team.programmes.sample @urns = urns || @team.locations.select { it.urn.present? }.sample(3).pluck(:urn) @school_year_groups = school_year_groups @@ -62,16 +35,13 @@ def patients private - attr_reader :ods_code, - :team, + attr_reader :team, :programme, :urns, :patient_count, :school_year_groups, :progress_bar - delegate :organisation, to: :team - def cohort_import_csv_filepath timestamp = Time.current.strftime("%Y%m%d%H%M%S") size = @@ -85,7 +55,7 @@ def cohort_import_csv_filepath ) Rails.root.join( "tmp/cohort-import-" \ - "#{organisation.ods_code}-#{programme.type}-#{size}-#{timestamp}.csv" + "#{team.workgroup}-#{programme.type}-#{size}-#{timestamp}.csv" ) end diff --git a/spec/features/cli_generate_cohort_imports_spec.rb b/spec/features/cli_generate_cohort_imports_spec.rb index 665d003718..f91513cb05 100644 --- a/spec/features/cli_generate_cohort_imports_spec.rb +++ b/spec/features/cli_generate_cohort_imports_spec.rb @@ -9,18 +9,12 @@ end def given_an_organisation_exists - @programme = Programme.hpv.first || create(:programme, :hpv) - @organisation = create(:organisation, ods_code: "R1Y") + @programme = create(:programme, :hpv) + @team = create(:team, workgroup: "r1y") end def and_there_are_three_sessions_in_the_organisation - @sessions = - create_list( - :session, - 3, - organisation: @organisation, - programmes: [@programme] - ) + @sessions = create_list(:session, 3, team: @team, programmes: [@programme]) end def when_i_run_the_generate_cohort_imports_command @@ -28,7 +22,7 @@ def when_i_run_the_generate_cohort_imports_command @output = capture_output do Dry::CLI.new(MavisCLI).call( - arguments: %w[generate cohort-imports -o R1Y -p 100] + arguments: %w[generate cohort-imports -w r1y -c 100] ) end @timestamp = Time.current.strftime("%Y%m%d%H%M%S") @@ -37,15 +31,15 @@ def when_i_run_the_generate_cohort_imports_command def then_a_cohort_import_csv_file_is_created expect(@output).to include( - "Generating cohort import for ods code R1Y with 100 patients" + "Generating cohort import for team r1y with 100 patients" ) expect(@output).to match( - /Cohort import CSV generated:.*cohort-import-R1Y-hpv-100-#{@timestamp}.csv/ + /Cohort import CSV generated:.*cohort-import-r1y-hpv-100-#{@timestamp}.csv/ ) expect( File.readlines( - Rails.root.join("tmp", "cohort-import-R1Y-hpv-100-#{@timestamp}.csv") + Rails.root.join("tmp", "cohort-import-r1y-hpv-100-#{@timestamp}.csv") ).length ).to eq 101 end diff --git a/spec/lib/generate/cohort_imports_spec.rb b/spec/lib/generate/cohort_imports_spec.rb index 100948834d..217b35631f 100644 --- a/spec/lib/generate/cohort_imports_spec.rb +++ b/spec/lib/generate/cohort_imports_spec.rb @@ -1,15 +1,18 @@ # frozen_string_literal: true describe Generate::CohortImports do + subject(:cohort_imports) { described_class.new(team:, programme:) } + + let(:programme) { create(:programme, :hpv) } + let(:team) { create(:team, programmes: [programme]) } + before do - team = create(:team, ods_code: "A9A5A") - programme = create(:programme, :hpv) location = create(:school, :secondary, team:, name: "Test School", urn: "31337") create(:session, team:, slug: "slug", location:, programmes: [programme]) end it "generates patients" do - expect(described_class.new.patients.count).to eq 10 + expect(cohort_imports.patients.count).to eq 10 end end From a0d0feb921d7640e72b5eef2aa8ad05d73881798 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:56:57 +0100 Subject: [PATCH 07/10] Generate cohort imports with multiple programmes This allows testing cohorts that cut across multiple programmes enabling a more complete end to end test. --- app/lib/mavis_cli/generate/cohort_imports.rb | 13 +++--- lib/generate/cohort_imports.rb | 44 +++++++++---------- .../cli_generate_cohort_imports_spec.rb | 2 +- spec/lib/generate/cohort_imports_spec.rb | 4 +- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/app/lib/mavis_cli/generate/cohort_imports.rb b/app/lib/mavis_cli/generate/cohort_imports.rb index b3976ba482..cbcf1f2244 100644 --- a/app/lib/mavis_cli/generate/cohort_imports.rb +++ b/app/lib/mavis_cli/generate/cohort_imports.rb @@ -12,9 +12,9 @@ class CohortImports < Dry::CLI::Command default: "A9A5A", desc: "Workgroup of team to generate consents for" - option :programme_type, + option :programme_types, aliases: ["-p"], - default: "hpv", + default: [], desc: "Programme type to generate consents for (hpv, menacwy, td_ipv, etc)" @@ -25,10 +25,13 @@ class CohortImports < Dry::CLI::Command default: 10, desc: "Number of patients to create" - def call(team_workgroup:, programme_type:, patient_count:) + def call(team_workgroup:, programme_types:, patient_count:) MavisCLI.load_rails + team = Team.find_by!(workgroup: team_workgroup) + programmes = Programme.where(type: programme_types) patient_count = patient_count.to_i + progress_bar = MavisCLI.progress_bar(patient_count) puts "Generating cohort import for team #{team_workgroup} with" \ @@ -36,8 +39,8 @@ def call(team_workgroup:, programme_type:, patient_count:) result = ::Generate::CohortImports.call( - team: Team.find_by(workgroup: team_workgroup), - programme: Programme.find_by(type: programme_type), + team:, + programmes:, patient_count:, progress_bar: ) diff --git a/lib/generate/cohort_imports.rb b/lib/generate/cohort_imports.rb index 4058ba174c..b295a5e4f1 100644 --- a/lib/generate/cohort_imports.rb +++ b/lib/generate/cohort_imports.rb @@ -7,16 +7,15 @@ class Generate::CohortImports def initialize( team:, - programme: nil, + programmes: nil, urns: nil, school_year_groups: nil, patient_count: 10, progress_bar: nil ) @team = team - @programme = programme || team.programmes.sample - @urns = - urns || @team.locations.select { it.urn.present? }.sample(3).pluck(:urn) + @programmes = programmes.presence || team.programmes + @urns = urns || @team.schools.pluck(:urn) @school_year_groups = school_year_groups @patient_count = patient_count @progress_bar = progress_bar @@ -25,9 +24,7 @@ def initialize( def self.call(...) = new(...).call - def call - write_cohort_import_csv - end + def call = write_cohort_import_csv def patients patient_count.times.lazy.map { build_patient } @@ -36,12 +33,18 @@ def patients private attr_reader :team, - :programme, + :programmes, :urns, :patient_count, :school_year_groups, :progress_bar + def academic_year = AcademicYear.current + + def all_year_groups + programmes.flat_map(&:default_year_groups).uniq + end + def cohort_import_csv_filepath timestamp = Time.current.strftime("%Y%m%d%H%M%S") size = @@ -55,7 +58,7 @@ def cohort_import_csv_filepath ) Rails.root.join( "tmp/cohort-import-" \ - "#{team.workgroup}-#{programme.type}-#{size}-#{timestamp}.csv" + "#{team.workgroup}-#{programmes.map(&:type).join("-")}-#{size}-#{timestamp}.csv" ) end @@ -120,15 +123,14 @@ def schools_with_year_groups else team.locations.where(urn: urns).includes(:team, :sessions) end - locations.select do - (it.year_groups & programme.default_year_groups).any? - end + + locations.select { (it.year_groups & all_year_groups).any? } end end def build_patient school = schools_with_year_groups.sample - year_group ||= (school.year_groups & programme.default_year_groups).sample + year_group ||= (school.year_groups & all_year_groups).sample nhs_number = nil loop do nhs_number = Faker::NationalHealthService.british_number.gsub(" ", "") @@ -154,15 +156,11 @@ def build_patient end end - def date_of_birth_for_year(year_group, academic_year: AcademicYear.pending) - if year_group < 12 - rand( - year_group.to_birth_academic_year( - academic_year: - ).to_academic_year_date_range - ) - else - raise "Unknown year group: #{year_group}" - end + def date_of_birth_for_year(year_group) + rand( + year_group.to_birth_academic_year( + academic_year: + ).to_academic_year_date_range + ) end end diff --git a/spec/features/cli_generate_cohort_imports_spec.rb b/spec/features/cli_generate_cohort_imports_spec.rb index f91513cb05..671602981f 100644 --- a/spec/features/cli_generate_cohort_imports_spec.rb +++ b/spec/features/cli_generate_cohort_imports_spec.rb @@ -10,7 +10,7 @@ def given_an_organisation_exists @programme = create(:programme, :hpv) - @team = create(:team, workgroup: "r1y") + @team = create(:team, workgroup: "r1y", programmes: [@programme]) end def and_there_are_three_sessions_in_the_organisation diff --git a/spec/lib/generate/cohort_imports_spec.rb b/spec/lib/generate/cohort_imports_spec.rb index 217b35631f..1993e79f62 100644 --- a/spec/lib/generate/cohort_imports_spec.rb +++ b/spec/lib/generate/cohort_imports_spec.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true describe Generate::CohortImports do - subject(:cohort_imports) { described_class.new(team:, programme:) } + subject(:cohort_imports) do + described_class.new(team:, programmes: [programme]) + end let(:programme) { create(:programme, :hpv) } let(:team) { create(:team, programmes: [programme]) } From 7e34cb899bcacc1d63065a4b3c538ebd97e6c529 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 15:58:41 +0100 Subject: [PATCH 08/10] Memoise cohort_import_csv_filepath This ensures that when printed at the end of the command it matches the time exactly and therefore the right path is printed. --- lib/generate/cohort_imports.rb | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/generate/cohort_imports.rb b/lib/generate/cohort_imports.rb index b295a5e4f1..d3438bb569 100644 --- a/lib/generate/cohort_imports.rb +++ b/lib/generate/cohort_imports.rb @@ -46,20 +46,23 @@ def all_year_groups end def cohort_import_csv_filepath - timestamp = Time.current.strftime("%Y%m%d%H%M%S") - size = - ActiveSupport::NumberHelper.number_to_human( - @patient_count, - units: { - thousand: "k", - million: "m" - }, - format: "%n%u" - ) - Rails.root.join( - "tmp/cohort-import-" \ - "#{team.workgroup}-#{programmes.map(&:type).join("-")}-#{size}-#{timestamp}.csv" - ) + @cohort_import_csv_filepath ||= + begin + timestamp = Time.current.strftime("%Y%m%d%H%M%S") + size = + ActiveSupport::NumberHelper.number_to_human( + @patient_count, + units: { + thousand: "k", + million: "m" + }, + format: "%n%u" + ) + Rails.root.join( + "tmp/cohort-import-" \ + "#{team.workgroup}-#{programmes.map(&:type).join("-")}-#{size}-#{timestamp}.csv" + ) + end end def write_cohort_import_csv From 9929988144dcf5a3ca407ccaa583a5443e92f219 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 16:01:31 +0100 Subject: [PATCH 09/10] Add script to generate cohort imports This adds a script that makes it possible to create cohort imports for the rollover training team. --- script/rollover_training/generate.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 script/rollover_training/generate.sh diff --git a/script/rollover_training/generate.sh b/script/rollover_training/generate.sh new file mode 100644 index 0000000000..9069b4cc1d --- /dev/null +++ b/script/rollover_training/generate.sh @@ -0,0 +1,3 @@ +set -eu + +bin/mavis generate cohort-imports -w rollovertraining -c 1000 From 30a5516dc9574cf0c3c147adb43ac183283e16f6 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Sun, 10 Aug 2025 16:01:57 +0100 Subject: [PATCH 10/10] Add script to create sessions This adds a script that makes it possible to create sessions for the rollover training team. --- script/rollover_training/create_sessions.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 script/rollover_training/create_sessions.sh diff --git a/script/rollover_training/create_sessions.sh b/script/rollover_training/create_sessions.sh new file mode 100644 index 0000000000..6fdda7f336 --- /dev/null +++ b/script/rollover_training/create_sessions.sh @@ -0,0 +1,3 @@ +set -eu + +bin/mavis teams create-sessions rollovertraining