Skip to content

Commit

Permalink
Add EY CSV data import
Browse files Browse the repository at this point in the history
  • Loading branch information
vacabor committed Jul 29, 2024
1 parent 56cf939 commit a486907
Show file tree
Hide file tree
Showing 11 changed files with 305 additions and 1 deletion.
25 changes: 25 additions & 0 deletions app/controllers/admin/early_years_data_uploads_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Admin
class EarlyYearsDataUploadsController < BaseAdminController
before_action :ensure_service_operator

def new
end

def create
file = params[:file]
@importer = EarlyYearsDataImporter.new(file)

if @importer.errors.any?
render :new
else
file_upload = FileUpload.create(uploaded_by: admin_user, body: File.read(file))
ImportEarlyYearsDataJob.perform_later(file_upload.id)

redirect_to admin_claims_path, notice: "EY file uploaded and queued to be imported"
end
rescue => e
Rollbar.error(e)
redirect_to new_admin_early_years_data_upload_path, alert: "There was a problem, please try again"
end
end
end
6 changes: 6 additions & 0 deletions app/jobs/import_early_years_data_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ImportEarlyYearsDataJob < FileImporterJob
import_with EarlyYearsDataImporter do
Rails.logger.info "EY data imported"
end
rescue_with -> { EarlyYearsData.delete_all }
end
2 changes: 2 additions & 0 deletions app/models/early_years_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class EarlyYearsData < ApplicationRecord
end
32 changes: 32 additions & 0 deletions app/models/early_years_data_importer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class EarlyYearsDataImporter < CsvImporter::Base
import_options(
target_data_model: EarlyYearsData,
transform_rows_with: :row_to_hash,
skip_row_if: :skip_row_conditions?,
mandatory_headers: [
"Nursery Name",
"EYURN / Ofsted URN",
"LA Code",
"Nursery Address",
"Primary Key Contact Email Address",
"Secondary Contact Email Address (Optional)"
]
)

private

def skip_row_conditions?(row)
row.fetch("EYURN / Ofsted URN").blank? || row.fetch("Primary Key Contact Email Address").blank?
end

def row_to_hash(row)
{
nursery_name: row.fetch("Nursery Name"),
urn: row.fetch("EYURN / Ofsted URN"),
local_authority_id: LocalAuthority.find_by(code: row.fetch("LA Code")).try(:id),
nursery_address: row.fetch("Nursery Address"),
primary_key_contact_email_address: row.fetch("Primary Key Contact Email Address"),
secondary_contact_email_address: row.fetch("Secondary Contact Email Address (Optional)")
}
end
end
1 change: 1 addition & 0 deletions app/views/admin/claims/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<%= link_to "Upload School Workforce Census data", new_admin_school_workforce_census_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %>
<%= link_to "Upload TPS data", new_admin_tps_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %>
<%= link_to "Upload SLC data", new_admin_student_loans_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %>
<%= link_to "Upload EY data", new_admin_early_years_data_upload_path, class: "govuk-button govuk-button--secondary", data: { module: "govuk-button" }, role: :button %>

<%= render "allocations_form" %>

Expand Down
34 changes: 34 additions & 0 deletions app/views/admin/early_years_data_uploads/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<h1 class="govuk-heading-xl">
Upload Early Years data
</h1>

<p class="govuk-body">You are uploading a list of eligible nurseries and whitelisted email addresses for the EY Policy.</p>

<p class="govuk-body">
Before uploading please note the following:
</p>

<ul class="govuk-list govuk-list--bullet">
<li>All previously uploaded EY data will be wiped.</li>
<li>Previously uploaded data will remain wiped if there are any errors processing the uploaded file.</li>
<li>You can wipe all data by uploading a CSV file with just the header row and no data rows.</li>
</ul>

<%= form_with url: admin_early_years_data_uploads_path, multipart: true do |f| %>
<div class="govuk-form-group">
<%= f.label "file", "Upload a CSV file", class: "govuk-label" %>

<% @importer && @importer.errors.each do |error| %>
<span class="govuk-error-message">
<span class="govuk-visually-hidden">Error:</span> <%= error %>
</span>
<% end %>

<%= f.file_field "file", class: "govuk-file-upload" %>
</div>
<%= f.submit "Upload", class: "govuk-button", data: {module: "govuk-button"} %>
<% end %>
</div>
</div>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def matches?(request)
patch "unhold"
end

resources :early_years_data_uploads, only: [:new, :create]
resources :qualification_report_uploads, only: [:new, :create]
resources :school_workforce_census_data_uploads, only: [:new, :create]
resources :student_loans_data_uploads, only: [:new, :create]
Expand Down
15 changes: 15 additions & 0 deletions db/migrate/20240729101656_create_early_years_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreateEarlyYearsData < ActiveRecord::Migration[7.0]
def change
create_table :early_years_data, id: :uuid do |t|
t.string :nursery_name
t.string :urn
t.references :local_authority, null: false, foreign_key: true, type: :uuid
t.string :nursery_address
t.string :primary_key_contact_email_address
t.string :secondary_contact_email_address

t.timestamps
end
add_index :early_years_data, :urn
end
end
16 changes: 15 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_07_10_080536) do
ActiveRecord::Schema[7.0].define(version: 2024_07_29_101656) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
enable_extension "pgcrypto"
Expand Down Expand Up @@ -175,6 +175,19 @@
t.index ["teacher_reference_number"], name: "index_ecp_eligibility_trn"
end

create_table "early_years_data", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "nursery_name"
t.string "urn"
t.uuid "local_authority_id", null: false
t.string "nursery_address"
t.string "primary_key_contact_email_address"
t.string "secondary_contact_email_address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["local_authority_id"], name: "index_early_years_data_on_local_authority_id"
t.index ["urn"], name: "index_early_years_data_on_urn"
end

create_table "eligible_fe_providers", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.integer "ukprn", null: false
t.text "academic_year", null: false
Expand Down Expand Up @@ -496,6 +509,7 @@
add_foreign_key "claims", "journeys_sessions"
add_foreign_key "decisions", "dfe_sign_in_users", column: "created_by_id"
add_foreign_key "early_career_payments_eligibilities", "schools", column: "current_school_id"
add_foreign_key "early_years_data", "local_authorities"
add_foreign_key "international_relocation_payments_eligibilities", "schools", column: "current_school_id"
add_foreign_key "levelling_up_premium_payments_eligibilities", "schools", column: "current_school_id"
add_foreign_key "notes", "claims"
Expand Down
51 changes: 51 additions & 0 deletions spec/jobs/import_early_years_data_job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
require "rails_helper"

RSpec.describe ImportEarlyYearsDataJob do
describe "#perform" do
subject(:upload) { described_class.new.perform(file_upload.id) }
let(:file_upload) { create(:file_upload, body: csv) }
let!(:local_authority) { create(:local_authority, code: "101") }
let(:csv) do
<<~CSV
Nursery Name,EYURN / Ofsted URN,LA Code,Nursery Address,Primary Key Contact Email Address,Secondary Contact Email Address (Optional)
Test Nursery,1234567,101,"123 Test Street, Test Town, TE1 5TT",[email protected],[email protected]
Other Nursery,9876543,101,"321 Test Street, Test Town, TE1 5TT",[email protected],
CSV
end

context "csv data processes successfully" do
it "imports early years data" do
expect { upload }.to change(EarlyYearsData, :count).by(2)
end

it "deletes the file upload" do
upload

expect(FileUpload.find_by_id(file_upload.id)).to be_nil
end

it "associates local authourity correctly" do
upload

expect(EarlyYearsData.first.nursery_name).to eq("Test Nursery")
expect(EarlyYearsData.first.local_authority).to eq(local_authority)
end
end

context "csv data encounters an error" do
before do
allow(EarlyYearsData).to receive(:insert_all).and_raise(ActiveRecord::RecordInvalid)
end

it "does not import early years data" do
expect { upload }.not_to change(EarlyYearsData, :count)
end

it "keeps the file upload" do
upload

expect(FileUpload.find_by_id(file_upload.id)).to be_present
end
end
end
end
123 changes: 123 additions & 0 deletions spec/requests/admin_early_years_data_upload_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
require "rails_helper"

RSpec.describe "EY (Early Years) data upload " do
# TODO: Set up EY journeys/check service open as required
let!(:journey_configuration_tslr) { create(:journey_configuration, :student_loans) }
let!(:journey_configuration_ecp_lupp) { create(:journey_configuration, :additional_payments) }
let!(:local_authority) { create(:local_authority, code: "101") }

before { @signed_in_user = sign_in_as_service_operator }

describe "#new" do
it "shows the upload form" do
get new_admin_early_years_data_upload_path
expect(response.body).to include("You are uploading a list of eligible nurseries and whitelisted email addresses")
end
end

describe "#create" do
let(:file) { Rack::Test::UploadedFile.new(StringIO.new(csv), "text/csv", original_filename: "ey_data.csv") }

context "when an invalid CSV is uploaded" do
let(:csv) { "Malformed CSV File\"," }

it "displays an error" do
post admin_early_years_data_uploads_path, params: {file: file}

expect(response.body).to include("The selected file must be a CSV")
end
end

context "when no CSV file is uploaded" do
it "displays an error" do
post admin_early_years_data_uploads_path

expect(response.body).to include("Select a file")
end
end

[DfeSignIn::User::SUPPORT_AGENT_DFE_SIGN_IN_ROLE_CODE, DfeSignIn::User::PAYROLL_OPERATOR_DFE_SIGN_IN_ROLE_CODE].each do |role|
it "returns a unauthorized response for #{role} users" do
sign_in_to_admin_with_role(role)

post admin_early_years_data_uploads_path

expect(response).to have_http_status(:unauthorized)
end
end

context "when a valid CSV is uploaded" do
subject(:upload) do
post admin_early_years_data_uploads_path, params: {file: file}
end

let(:csv) do
<<~CSV
Nursery Name,EYURN / Ofsted URN,LA Code,Nursery Address,Primary Key Contact Email Address,Secondary Contact Email Address (Optional)
#{rows[0].values.join(",")}
#{rows[1].values.join(",")}
CSV
end

let(:rows) do
[
{
nursery_name: "Test Nursery",
urn: "1234567",
local_authority_id: "101",
nursery_address: "123 Test Street, Test Town, TE1 5ST",
primary_key_contact_email_address: "[email protected]",
secondary_contact_email_address: "[email protected]"
},
{
nursery_name: "Other Nursery",
urn: "9876543",
local_authority_id: "101",
nursery_address: "321 Test Street, Test Town, TE1 5ST",
primary_key_contact_email_address: "[email protected]",
secondary_contact_email_address: ""
}
]
end

let(:expected_records) do
rows.map do |row|
expected_row = row.dup
expected_row[:local_authourity] = local_authourity
expected_row.delete :local_authority_id
expected_row
end
end

it "enqueues a job to import the file asynchronously" do
expect { upload }.to have_enqueued_job(ImportEarlyYearsDataJob)
end

it "parses the rows and saves them as early years data records" do
aggregate_failures do
expect { perform_enqueued_jobs { upload } }.to change(EarlyYearsData, :count).by(2)
expect(EarlyYearsData.find(urn: "1234567")).to have_attributes(expected_records[0])
expect(EarlyYearsData.find(urn: "9876543")).to have_attributes(expected_records[1])
end
end

shared_examples :no_upload do
it "does not upload the rows" do
expect { perform_enqueued_jobs { upload } }.not_to change(EarlyYearsData, :count)
end
end

context "with rows with blank 'URN'" do
let(:rows) { super().map { |row| row.merge(urn: "") } }

include_examples :no_upload
end

context "with rows with invalid primary email" do
let(:rows) { super().map { |row| row.merge(primary_key_contact_email_address: "") } }

include_examples :no_upload
end
end
end
end

0 comments on commit a486907

Please sign in to comment.