Skip to content

Commit

Permalink
Level B export fixes (non-budget)
Browse files Browse the repository at this point in the history
- For GCRF, remove "ODA or Non-ODA", "ISPF ODA partner countries",
  "ISPF themes", "Tags"
- For GCRF, add "GCRF Strategic Area", "GCRF Challenge Area"
- For ISPF, add "ISPF non-ODA partner countries"
- For Newton, remove "ODA or Non-ODA", "ISPF ODA partner countries",
  "ISPF themes", and "Tags"
- For Newton, add "Newton Fund Country Partner Organisations",
  "Newton Fund Pillar"
- For Other ODA, remove "ODA or Non-ODA", "ISPF ODA partner countries",
  "ISPF themes", "Tags"
  • Loading branch information
rgarner committed Feb 4, 2025
1 parent e783bce commit 1f48ef3
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 85 deletions.
165 changes: 82 additions & 83 deletions app/services/export/activities_level_b.rb
Original file line number Diff line number Diff line change
@@ -1,48 +1,91 @@
class Export::ActivitiesLevelB
HEADERS = [
"Partner Organisation",
"Activity level",
"Parent activity",
"ODA or Non-ODA",
"Partner organisation identifier",
"RODA identifier",
"IATI identifier",
"Linked activity",
"Activity title",
"Activity description",
"Aims or objectives",
"Sector",
"Original commitment figure",
"Activity status",
"Planned start date",
"Planned end date",
"Actual start date",
"Actual end date",
"ISPF ODA partner countries",
"Benefitting countries",
"Benefitting region",
"Global Development Impact",
"Sustainable Development Goals",
"ISPF themes",
"Aid type",
"ODA eligibility",
"Publish to IATI?",
"Tags",
"Budget 2023-2024",
"Budget 2024-2025",
"Budget 2025-2026",
"Budget 2026-2027",
"Budget 2027-2028",
"Budget 2028-2029",
"Comments"
Field = Data.define(:name, :fund, :value)

# A place to:
# - Name all the fields on the left
# - filter applicable fields on a per-fund basis in the middle
# - evaluate a row's values in the context of a fund's activity via a `value` Proc on the right
# - show all this on a line-by-line basis to avoid one tall export per fund, given
# there are many common fields
# standard:disable Layout/ExtraSpacing
FIELDS = [
Field.new("Partner Organisation", "ALL", -> { activity.organisation.name }),
Field.new("Activity level", "ALL", -> { activity.level }),
Field.new("Parent activity", "ALL", -> { activity.source_fund.name }),
Field.new("ODA or Non-ODA", "ISPF", -> { activity.is_oda }),
Field.new("Partner organisation identifier", "ALL", -> { activity.partner_organisation_identifier }),
Field.new("RODA identifier", "ALL", -> { activity.roda_identifier }),
Field.new("IATI identifier", "ALL", -> { activity.transparency_identifier }),
Field.new("Linked activity", "ALL", -> { activity.linked_activity_identifier }),
Field.new("Activity title", "ALL", -> { activity.title }),
Field.new("Activity description", "ALL", -> { activity.description }),
Field.new("Aims or objectives", "ALL", -> { activity.objectives }),
Field.new("Sector", "ALL", -> { activity.sector }),
Field.new("Original commitment figure", "ALL", -> { activity.commitment&.value }),
Field.new("Activity status", "ALL", -> { activity.programme_status }),
Field.new("Planned start date", "ALL", -> { activity.planned_start_date }),
Field.new("Planned end date", "ALL", -> { activity.planned_end_date }),
Field.new("Actual start date", "ALL", -> { activity.actual_start_date }),
Field.new("Actual end date", "ALL", -> { activity.actual_end_date }),
Field.new("ISPF ODA partner countries", "ISPF", -> { activity.ispf_oda_partner_countries }),
Field.new("ISPF non-ODA partner countries", "ISPF", -> { activity.ispf_non_oda_partner_countries }),
Field.new("GCRF Strategic Area", "GCRF", -> { activity.gcrf_strategic_area }),
Field.new("GCRF Challenge Area", "GCRF", -> { activity.gcrf_challenge_area }),
Field.new("Newton Fund Country Partner Organisations", "NF", -> { activity.country_partner_organisations }),
Field.new("Newton Fund Pillar", "NF", -> { activity.fund_pillar }),
Field.new("Benefitting countries", "ALL", -> { activity.benefitting_countries }),
Field.new("Benefitting region", "ALL", -> { activity.benefitting_region }),
Field.new("Global Development Impact", "ALL", -> { activity.gdi }),
Field.new("Sustainable Development Goals", "ALL", -> { activity.sustainable_development_goals }),
Field.new("ISPF themes", "ISPF", -> { activity.ispf_themes }),
Field.new("Aid type", "ALL", -> { activity.aid_type }),
Field.new("ODA eligibility", "ALL", -> { activity.oda_eligibility }),
Field.new("Publish to IATI?", "ALL", -> { activity.publish_to_iati }),
Field.new("Tags", "ISPF", -> { activity.tags }),
Field.new("Budget 2023-2024", "ALL", -> { budgets_by_year[2023]&.value }),
Field.new("Budget 2024-2025", "ALL", -> { budgets_by_year[2024]&.value }),
Field.new("Budget 2025-2026", "ALL", -> { budgets_by_year[2025]&.value }),
Field.new("Budget 2026-2027", "ALL", -> { budgets_by_year[2026]&.value }),
Field.new("Budget 2027-2028", "ALL", -> { budgets_by_year[2027]&.value }),
Field.new("Budget 2028-2029", "ALL", -> { budgets_by_year[2028]&.value }),
Field.new("Comments", "ALL", -> { activity.comments.map(&:body).join("|") })
].freeze
# standard:enable Layout/ExtraSpacing

# Given a fund and an activity (or nil for a header row), return all the cell values in a row
# via #to_a
Row = Struct.new(:fund, :activity) do
def budgets_by_year
@budgets_by_year ||= activity.budgets.each_with_object({}) do |budget, years|
years[budget.financial_year.start_year] = BudgetPresenter.new(budget)
end
end

def to_a
return applicable_fields.map(&:name) if header?

applicable_fields.map do |field|
instance_exec(&field.value) # get the field's value from its Proc in the context of this Row
end
end

private

def header?
activity.nil?
end

def applicable_fields
FIELDS.select { |field| field.fund.in? ["ALL", fund.short_name] }
end
end

def initialize(fund:)
@fund = fund
end

def headers
HEADERS
Row.new(fund: @fund, activity: nil).to_a
end

def filename
Expand All @@ -51,58 +94,14 @@ def filename

def rows
activities.map do |activity|
row_for(activity)
Row.new(fund: @fund, activity: ActivityCsvPresenter.new(activity)).to_a
end
end

private

def row_for(activity)
activity = ActivityCsvPresenter.new(activity)
budgets_by_year = activity.budgets.each_with_object({}) do |budget, hash|
hash[budget.financial_year.start_year] = BudgetPresenter.new(budget)
end
[
activity.organisation.name, # "Partner Organisation",
activity.level, # "Activity level",
@fund.name, # "Parent activity",
activity.is_oda, # "ODA or Non-ODA",
activity.partner_organisation_identifier, # "Partner organisation identifier",
activity.roda_identifier, # "RODA identifier", e.g. GCRF-LCXHF
activity.transparency_identifier, # "IATI identifier",
activity.linked_activity_identifier, # "Linked activity",
activity.title, # "Activity title",
activity.description, # "Activity description",
activity.objectives, # "Aims or objectives",
activity.sector, # "Sector",
activity.commitment&.value, # "Original commitment figure",
activity.programme_status, # "Activity status",
activity.planned_start_date, # "Planned start date",
activity.planned_end_date, # "Planned end date",
activity.actual_start_date, # "Actual start date",
activity.actual_end_date, # "Actual end date",
activity.ispf_oda_partner_countries, # "ISPF ODA partner countries",
activity.benefitting_countries, # "Benefitting countries",
activity.benefitting_region, # "Benefitting region",
activity.gdi, # "Global Development Impact",
activity.sustainable_development_goals, # "Sustainable Development Goals",
activity.ispf_themes, # "ISPF themes",
activity.aid_type, # "Aid type",
activity.oda_eligibility, # "ODA eligibility",
activity.publish_to_iati, # "Publish to IATI?",
activity.tags, # "Tags",
budgets_by_year[2023]&.value, # "Budget 2023-2024",
budgets_by_year[2024]&.value, # "Budget 2024-2025",
budgets_by_year[2025]&.value, # "Budget 2025-2026",
budgets_by_year[2026]&.value, # "Budget 2026-2027",
budgets_by_year[2027]&.value, # "Budget 2027-2028",
budgets_by_year[2028]&.value, # "Budget 2028-2029",
activity.comments.map(&:body).join("|") # "Comments"
]
end

def activities
@activities ||= @fund.activity.child_activities
.includes(:organisation, :linked_activity, :commitment, :budgets, :comments)
.includes(:organisation, :commitment, :budgets, :linked_activity, :comments)
end
end
1 change: 1 addition & 0 deletions config/environments/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
Bullet.add_safelist type: :unused_eager_loading, class_name: "User", association: :organisation
Bullet.add_safelist type: :unused_eager_loading, class_name: "Activity", association: :organisation
Bullet.add_safelist type: :unused_eager_loading, class_name: "Activity", association: :child_activities
Bullet.add_safelist type: :unused_eager_loading, class_name: "Activity", association: :linked_activity
Bullet.add_safelist type: :unused_eager_loading, class_name: "Transaction", association: :provider
Bullet.add_safelist type: :unused_eager_loading, class_name: "Transaction", association: :receiver
Bullet.add_safelist type: :unused_eager_loading, class_name: "Activity", association: :parent
Expand Down
3 changes: 2 additions & 1 deletion spec/controllers/exports_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@
end

it "returns a CSV of all of the exports" do
expect(CSV.parse(response.body.delete_prefix("\uFEFF")).first).to match_array(Export::ActivitiesLevelB::HEADERS)
header = Export::ActivitiesLevelB::Row.new(fund, nil).to_a
expect(CSV.parse(response.body.delete_prefix("\uFEFF")).first).to match_array(header)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/features/beis_users_can_download_exports_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@
"Budget 2027-2028" => nil,
"Budget 2028-2029" => nil,
"Comments" => "#{programme_1.comments.first.body}|#{programme_1.comments.last.body}"
})).and(have_attributes(length: 35))
})).and(have_attributes(length: 36))

# And that file should contain no level B activities for any other fund
expect(document.none? { |row| row["RODA Identifier"] == other_fund_programme.roda_identifier }).to be true
Expand Down
171 changes: 171 additions & 0 deletions spec/services/export/activities_level_b_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
require "rails_helper"

RSpec.describe Export::ActivitiesLevelB do
let(:export) { Export::ActivitiesLevelB.new(fund:) }

before do
Fund.all.each { |fund| create(:fund_activity, source_fund_code: fund.id, roda_identifier: fund.short_name) }
end

describe "#headers" do
subject(:headers) { export.headers }

context "fund is ISPF" do
let(:fund) { Fund.by_short_name("ISPF") }

it "has ISPF-only columns" do
expect(headers).to include("ODA or Non-ODA")
expect(headers).to include("ISPF ODA partner countries")
expect(headers).to include("ISPF themes")
expect(headers).to include("Tags")
end
end
context "fund is GCRF" do
let(:fund) { Fund.by_short_name("GCRF") }

it "has no ISPF-only columns" do
expect(headers).not_to include("ODA or Non-ODA")
expect(headers).not_to include("ISPF ODA partner countries")
expect(headers).not_to include("ISPF themes")
expect(headers).not_to include("Tags")
end

it "has GCRF-only columns" do
expect(headers).to include("GCRF Strategic Area")
expect(headers).to include("GCRF Challenge Area")
end
end
context "fund is Newton" do
let(:fund) { Fund.by_short_name("NF") }

it "has no ISPF-only columns" do
expect(headers).not_to include("ODA or Non-ODA")
expect(headers).not_to include("ISPF ODA partner countries")
expect(headers).not_to include("ISPF themes")
expect(headers).not_to include("Tags")
end

it "has NF-only columns" do
expect(headers).to include("Newton Fund Country Partner Organisations")
expect(headers).to include("Newton Fund Pillar")
end
end
end

describe "#rows" do
subject(:rows) { export.rows }
let(:first_row) { export.headers.zip(export.rows.first).to_h } # express the first row as a hash of k/v
let(:common_expected_values) do
{
"Partner Organisation" => "Department for Business, Energy and Industrial Strategy",
"Activity level" => "Programme (level B)",
"Partner organisation identifier" => a_string_starting_with("GCRF-"),
"RODA identifier" => a_string_starting_with("#{fund.short_name}-"),
"IATI identifier" => a_string_starting_with("GB-GOV-26-"),
"Linked activity" => nil,
"Activity title" => programme_activity.title,
"Activity description" => programme_activity.description,
"Aims or objectives" => programme_activity.objectives,
"Sector" => "11110: Education policy and administrative management",
"Original commitment figure" => "£250,000.00",
"Activity status" => "Spend in progress",
"Planned start date" => "31 Jan 2025",
"Planned end date" => "1 Feb 2025",
"Actual start date" => "30 Jan 2025",
"Actual end date" => "31 Jan 2025",
"Benefitting countries" => "Argentina; Ecuador; Brazil",
"Benefitting region" => "South America, regional",
"Global Development Impact" => "GDI not applicable",
"Sustainable Development Goals" => "Not applicable",
"Aid type" => "D01: Donor country personnel",
"ODA eligibility" => "Eligible",
"Publish to IATI?" => "Yes"
}
end

before { travel_to Date.new(2025, 1, 31) } # Factories default to dates around today for actual/planned dates

context "fund is ISPF" do
let(:fund) { Fund.by_short_name("ISPF") }
let!(:programme_activity) do
create(
:programme_activity, :ispf_funded, commitment: create(:commitment, value: BigDecimal("250_000.00")),
benefitting_countries: %w[AR EC BR], tags: [4, 5], transparency_identifier: "GB-GOV-26-1234-5678-91011"
)
end

it "has a row with ISPF-specific and common values" do
expect(first_row).to match a_hash_including(
{
"Parent activity" => "International Science Partnerships Fund",
"ODA or Non-ODA" => "ODA",
"ISPF ODA partner countries" => "India (ODA)",
"ISPF non-ODA partner countries" => "India (non-ODA)",
"ISPF themes" => "Resilient Planet",
"Tags" => "Tactical Fund|Previously reported under OODA"
}.reverse_merge(common_expected_values)
)
end
end

context "fund is GCRF" do
let(:fund) { Fund.by_short_name("GCRF") }
let!(:programme_activity) do
create(
:programme_activity, :gcrf_funded, commitment: create(:commitment, value: BigDecimal("250_000.00")),
benefitting_countries: %w[AR EC BR], transparency_identifier: "GB-GOV-26-1234-5678-91011"
)
end

it "has a row with GCRF-specific and common values" do
expect(first_row).to match a_hash_including(
{
"Parent activity" => "Global Challenges Research Fund",
"GCRF Strategic Area" => "UKRI Collective Fund (2017 allocation) and Academies Collective Fund: Resilient Futures",
"GCRF Challenge Area" => "Not applicable"
}.reverse_merge(common_expected_values)
)
end
end

context "fund is OODA" do
let(:fund) { Fund.by_short_name("OODA") }
let!(:programme_activity) do
create(
:programme_activity, :ooda_funded, commitment: create(:commitment, value: BigDecimal("250_000.00")),
benefitting_countries: %w[AR EC BR], transparency_identifier: "GB-GOV-26-1234-5678-91011"
)
end

it "has a row with OODA-specific and common values" do
expect(first_row).to match a_hash_including(
{
"Parent activity" => "Other ODA"
}.reverse_merge(common_expected_values)
)
end
end

context "fund is Newton" do
let(:fund) { Fund.by_short_name("NF") }
let!(:programme_activity) do
create(
:programme_activity, :newton_funded, commitment: create(:commitment, value: BigDecimal("250_000.00")),
benefitting_countries: %w[AR EC BR], transparency_identifier: "GB-GOV-26-1234-5678-91011",
country_partner_organisations: ["National Council for the State Funding Agencies (CONFAP)", "Other"],
fund_pillar: "1" # People
)
end

it "has a row with Newton-specific and common values" do
expect(first_row).to match a_hash_including(
{
"Parent activity" => "Newton Fund",
"Newton Fund Country Partner Organisations" => "National Council for the State Funding Agencies (CONFAP)|Other",
"Newton Fund Pillar" => "People"
}.reverse_merge(common_expected_values)
)
end
end
end
end

0 comments on commit 1f48ef3

Please sign in to comment.