Skip to content

Commit b0831fd

Browse files
authored
Merge branch 'rubyforgood:main' into 4681-donation-sites-export-csv
2 parents e647821 + 4986d86 commit b0831fd

File tree

10 files changed

+191
-61
lines changed

10 files changed

+191
-61
lines changed

app/controllers/concerns/importable.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ def import_csv
2727
data = File.read(params[:file].path, encoding: "BOM|UTF-8")
2828
csv = CSV.parse(data, headers: true).reject { |row| row.to_hash.values.any?(&:nil?) }
2929
if csv.count.positive? && csv.first.headers.all? { |header| !header.nil? }
30-
resource_model.import_csv(csv, current_organization.id)
31-
flash[:notice] = "#{resource_model_humanized} were imported successfully!"
30+
errors = resource_model.import_csv(csv, current_organization.id)
31+
if errors.empty?
32+
flash[:notice] = "#{resource_model_humanized} were imported successfully!"
33+
else
34+
flash[:error] = "The following #{resource_model_humanized} did not import successfully:\n#{errors.join("\n")}"
35+
end
3236
else
3337
flash[:error] = "Check headers in file!"
3438
end

app/models/concerns/provideable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def self.import_csv(csv, organization)
1919

2020
loc.save!
2121
end
22+
[]
2223
end
2324

2425
def self.csv_export_headers

app/models/donation_site.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def self.import_csv(csv, organization)
4646
loc.organization_id = organization
4747
loc.save!
4848
end
49+
[]
4950
end
5051

5152
def self.csv_export_headers

app/models/partner.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,19 @@ def approvable?
158158

159159
# better to extract this outside of the model
160160
def self.import_csv(csv, organization_id)
161+
errors = []
161162
organization = Organization.find(organization_id)
162163

163164
csv.each do |row|
164165
hash_rows = Hash[row.to_hash.map { |k, v| [k.downcase, v] }]
165166

166167
svc = PartnerCreateService.new(organization: organization, partner_attrs: hash_rows)
167168
svc.call
169+
if svc.errors.present?
170+
errors << "#{svc.partner.name}: #{svc.partner.errors.full_messages.to_sentence}"
171+
end
168172
end
173+
errors
169174
end
170175

171176
def self.csv_export_headers

app/models/storage_location.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def self.import_csv(csv, organization)
121121
loc.organization_id = organization
122122
loc.save!
123123
end
124+
[]
124125
end
125126

126127
# NOTE: We should generalize this elsewhere -- Importable concern?

app/services/partner_create_service.rb

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,27 @@ def initialize(organization:, partner_attrs:)
1111
def call
1212
@partner = organization.partners.build(partner_attrs)
1313

14-
unless @partner.valid?
14+
if @partner.valid?
15+
ActiveRecord::Base.transaction do
16+
@partner.save!
17+
18+
Partners::Profile.create!({
19+
partner_id: @partner.id,
20+
name: @partner.name,
21+
enable_child_based_requests: organization.enable_child_based_requests,
22+
enable_individual_requests: organization.enable_individual_requests,
23+
enable_quantity_based_requests: organization.enable_quantity_based_requests
24+
})
25+
rescue StandardError => e
26+
errors.add(:base, e.message)
27+
raise ActiveRecord::Rollback
28+
end
29+
else
1530
@partner.errors.each do |error|
1631
errors.add(error.attribute, error.message)
1732
end
1833
end
1934

20-
ActiveRecord::Base.transaction do
21-
@partner.save!
22-
23-
Partners::Profile.create!({
24-
partner_id: @partner.id,
25-
name: @partner.name,
26-
enable_child_based_requests: organization.enable_child_based_requests,
27-
enable_individual_requests: organization.enable_individual_requests,
28-
enable_quantity_based_requests: organization.enable_quantity_based_requests
29-
})
30-
rescue StandardError => e
31-
errors.add(:base, e.message)
32-
raise ActiveRecord::Rollback
33-
end
34-
3535
self
3636
end
3737

app/services/reports/period_supply_report_service.rb

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ def initialize(year:, organization:)
1414
def report
1515
@report ||= {name: "Period Supplies",
1616
entries: {
17-
"Period supplies distributed" => number_with_delimiter(distributed_supplies),
18-
"Period supplies per adult per month" => monthly_supplies&.round || 0,
17+
"Period supplies distributed" => number_with_delimiter(total_distributed_period_supplies),
1918
"Period supplies" => types_of_supplies,
2019
"% period supplies donated" => "#{percent_donated.round}%",
2120
"% period supplies bought" => "#{percent_bought.round}%",
@@ -24,7 +23,7 @@ def report
2423
end
2524

2625
# @return [Integer]
27-
def distributed_supplies
26+
def distributed_loose_period_supplies
2827
@distributed_supplies ||= organization
2928
.distributions
3029
.for_year(year)
@@ -33,17 +32,12 @@ def distributed_supplies
3332
.sum("line_items.quantity")
3433
end
3534

36-
# @return [Integer]
37-
def monthly_supplies
38-
# NOTE: This is asking "per adult per month" but there doesn't seem to be much difference
39-
# in calculating per month or per any other time frame, since all it's really asking
40-
# is the value of the `distribution_quantity` field for the items we're giving out.
41-
organization
42-
.distributions
43-
.for_year(year)
44-
.joins(line_items: :item)
45-
.merge(Item.period_supplies)
46-
.average("COALESCE(items.distribution_quantity, 50)")
35+
def distributed_period_supplies_from_kits
36+
kit_items_calculation("distributions", "Distribution")
37+
end
38+
39+
def total_distributed_period_supplies
40+
distributed_loose_period_supplies + distributed_period_supplies_from_kits
4741
end
4842

4943
def types_of_supplies
@@ -54,14 +48,14 @@ def types_of_supplies
5448
def percent_donated
5549
return 0.0 if total_supplies.zero?
5650

57-
(donated_supplies / total_supplies.to_f) * 100
51+
(total_donated_supplies / total_supplies.to_f) * 100
5852
end
5953

6054
# @return [Float]
6155
def percent_bought
6256
return 0.0 if total_supplies.zero?
6357

64-
(purchased_supplies / total_supplies.to_f) * 100
58+
(total_purchased_supplies / total_supplies.to_f) * 100
6559
end
6660

6761
# @return [String]
@@ -72,24 +66,67 @@ def money_spent_on_supplies
7266
###### HELPER METHODS ######
7367

7468
# @return [Integer]
75-
def purchased_supplies
69+
def total_purchased_supplies
7670
@purchased_supplies ||= LineItem.joins(:item)
7771
.merge(Item.period_supplies)
7872
.where(itemizable: organization.purchases.for_year(year))
7973
.sum(:quantity)
74+
75+
@purchased_supplies + purchased_supplies_from_kits
76+
end
77+
78+
def purchased_supplies_from_kits
79+
kit_items_calculation("purchases", "Purchase")
8080
end
8181

8282
# @return [Integer]
8383
def total_supplies
84-
@total_supplies ||= purchased_supplies + donated_supplies
84+
@total_supplies ||= total_purchased_supplies + total_donated_supplies
8585
end
8686

8787
# @return [Integer]
88-
def donated_supplies
89-
@donated_supplies ||= LineItem.joins(:item)
88+
def total_donated_supplies
89+
loose_donated_supplies = LineItem.joins(:item)
9090
.merge(Item.period_supplies)
9191
.where(itemizable: organization.donations.for_year(year))
9292
.sum(:quantity)
93+
94+
loose_donated_supplies + donated_supplies_from_kits
95+
end
96+
97+
def donated_supplies_from_kits
98+
kit_items_calculation("donations", "Donation")
99+
end
100+
101+
private
102+
103+
def kit_items_calculation(itemizable_type, string_itemizable_type)
104+
organization_id = @organization.id
105+
year = @year
106+
107+
# Sanitize and validate inputs
108+
itemizable_type = ActiveRecord::Base.connection.quote_table_name(itemizable_type)
109+
string_itemizable_type = ActiveRecord::Base.connection.quote(string_itemizable_type)
110+
111+
sql_query = <<-SQL
112+
SELECT SUM(line_items.quantity * kit_line_items.quantity)
113+
FROM #{itemizable_type}
114+
INNER JOIN line_items ON line_items.itemizable_type = #{string_itemizable_type} AND line_items.itemizable_id = #{itemizable_type}.id
115+
INNER JOIN items ON items.id = line_items.item_id
116+
INNER JOIN kits ON kits.id = items.kit_id
117+
INNER JOIN line_items AS kit_line_items ON kits.id = kit_line_items.itemizable_id
118+
INNER JOIN items AS kit_items ON kit_items.id = kit_line_items.item_id
119+
INNER JOIN base_items ON base_items.partner_key = kit_items.partner_key
120+
WHERE #{itemizable_type}.organization_id = ?
121+
AND EXTRACT(year FROM issued_at) = ?
122+
AND LOWER(base_items.category) LIKE '%menstrual supplies%'
123+
AND NOT (LOWER(base_items.category) LIKE '%diaper%' OR LOWER(base_items.name) LIKE '%cloth%')
124+
AND kit_line_items.itemizable_type = 'Kit';
125+
SQL
126+
127+
sanitized_sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql_query, organization_id, year])
128+
result = ActiveRecord::Base.connection.execute(sanitized_sql)
129+
result.first["sum"].to_i
93130
end
94131
end
95132
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name,email
2+
3+
Partner 2,this is not an email address
4+

spec/requests/partners_requests_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,26 @@
222222
expect(response).to have_error "Check headers in file!"
223223
end
224224
end
225+
226+
context "csv file with invalid email address" do
227+
let(:file) { fixture_file_upload("partners_with_invalid_email.csv", "text/csv") }
228+
subject { post import_csv_partners_path, params: { file: file } }
229+
230+
it "invokes .import_csv" do
231+
expect(model_class).to respond_to(:import_csv).with(2).arguments
232+
end
233+
234+
it "redirects to :index" do
235+
subject
236+
expect(response).to be_redirect
237+
end
238+
239+
it "presents a flash notice message displaying the import errors" do
240+
subject
241+
expect(response).to have_error(/The following #{model_class.name.underscore.humanize.pluralize} did not import successfully:/)
242+
expect(response).to have_error(/Partner 2: Email is invalid/)
243+
end
244+
end
225245
end
226246

227247
describe "POST #create" do

spec/services/reports/period_supply_report_service_spec.rb

Lines changed: 80 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@
77
end
88

99
describe "#report" do
10-
it "should report zero values" do
11-
expect(report.report[:entries]).to match(hash_including({
12-
"% period supplies bought" => "0%",
13-
"% period supplies donated" => "0%",
14-
"Period supplies distributed" => "0",
15-
"Period supplies per adult per month" => 0,
16-
"Money spent purchasing period supplies" => "$0.00"
17-
}))
18-
expect(report.report[:entries]["Period supplies"].split(", "))
19-
.to contain_exactly("Tampons", "Pads", "Liners (Menstrual)")
10+
context "with no values" do
11+
it "should report zero values" do
12+
expect(report.report[:entries]).to match(hash_including({
13+
"% period supplies bought" => "0%",
14+
"% period supplies donated" => "0%",
15+
"Period supplies distributed" => "0",
16+
"Money spent purchasing period supplies" => "$0.00"
17+
}))
18+
expect(report.report[:entries]["Period supplies"].split(", "))
19+
.to contain_exactly("Tampons", "Pads", "Liners (Menstrual)")
20+
end
2021
end
2122

22-
describe "with values" do
23+
context "with values" do
2324
before(:each) do
2425
Organization.seed_items(organization)
2526

@@ -32,6 +33,49 @@
3233
# We will create data both within and outside our date range, and both period_supplies and non period_supplies.
3334
# Spec will ensure that only the required data is included.
3435

36+
# Kits
37+
period_supplies_kit = create(:kit, :with_item, organization: organization)
38+
another_period_supply_kit = create(:kit, :with_item, organization: organization)
39+
donated_period_supply_kit = create(:kit, :with_item, organization: organization)
40+
purchased_period_supply_kit = create(:kit, :with_item, organization: organization)
41+
pad_and_tampon_kit = create(:kit, :with_item, organization: organization)
42+
43+
create(:base_item, name: "Adult Pads", partner_key: "adult pads", category: "Menstrual Supplies")
44+
create(:base_item, name: "Adult Tampons", partner_key: "adult tampons", category: "Menstrual Supplies")
45+
46+
period_supplies_kit_item = create(:item, name: "Adult Pads", partner_key: "adult pads")
47+
another_period_supplies_kit_item = create(:item, name: "Adult Tampons", partner_key: "adult tampons")
48+
purchased_period_supplies_kit_item = create(:item, name: "Liners", partner_key: "adult tampons")
49+
50+
period_supplies_kit.line_items.first.update!(item_id: period_supplies_kit_item.id, quantity: 5)
51+
another_period_supply_kit.line_items.first.update!(item_id: another_period_supplies_kit_item.id, quantity: 5)
52+
donated_period_supply_kit.line_items.first.update!(item_id: another_period_supplies_kit_item.id, quantity: 5)
53+
purchased_period_supply_kit.line_items.first.update!(item_id: purchased_period_supplies_kit_item.id, quantity: 5)
54+
55+
pad_and_tampon_kit.line_items.first.update!(item_id: period_supplies_kit_item.id, quantity: 10)
56+
pad_and_tampon_kit.line_items.first.update!(item_id: another_period_supplies_kit_item.id, quantity: 10)
57+
58+
period_supplies_kit_distribution = create(:distribution, organization: organization, issued_at: within_time)
59+
another_period_supplies_kit_distribution = create(:distribution, organization: organization, issued_at: within_time)
60+
pad_and_tampon_kit_distribution = create(:distribution, organization: organization, issued_at: within_time)
61+
62+
kit_donation = create(:donation, product_drive: nil, issued_at: within_time, money_raised: 1000, organization: organization)
63+
64+
kit_purchase = create(:purchase, issued_at: within_time, organization: organization, purchased_from: "TikTok Shop", amount_spent_in_cents: 1000, amount_spent_on_period_supplies_cents: 1000, line_items: [
65+
create(:line_item, :purchase, item: period_supplies_kit_item, quantity: 5),
66+
create(:line_item, :purchase, item: purchased_period_supplies_kit_item, quantity: 5)
67+
])
68+
69+
create(:line_item, :distribution, quantity: 10, item: period_supplies_kit.item, itemizable: period_supplies_kit_distribution)
70+
create(:line_item, :distribution, quantity: 10, item: another_period_supply_kit.item, itemizable: another_period_supplies_kit_distribution)
71+
72+
create(:line_item, :distribution, quantity: 10, item: pad_and_tampon_kit.item, itemizable: pad_and_tampon_kit_distribution)
73+
create(:line_item, :distribution, quantity: 10, item: pad_and_tampon_kit.item, itemizable: pad_and_tampon_kit_distribution)
74+
75+
create(:line_item, :donation, quantity: 10, item: donated_period_supply_kit.item, itemizable: kit_donation)
76+
77+
create(:line_item, :purchase, quantity: 30, item: purchased_period_supply_kit.item, itemizable: kit_purchase)
78+
3579
# Distributions
3680
distributions = create_list(:distribution, 2, issued_at: within_time, organization: organization)
3781
outside_distributions = create_list(:distribution, 2, issued_at: outside_time, organization: organization)
@@ -84,19 +128,32 @@
84128
end
85129
end
86130

87-
it "should report normal values" do
88-
organization.items.period_supplies.first.update!(distribution_quantity: 20)
131+
describe "with values" do
132+
it "should report normal values" do
133+
organization.items.period_supplies.first.update!(distribution_quantity: 20)
89134

90-
expect(report.report[:name]).to eq("Period Supplies")
91-
expect(report.report[:entries]).to match(hash_including({
92-
"% period supplies bought" => "60%",
93-
"% period supplies donated" => "40%",
94-
"Period supplies distributed" => "2,000",
95-
"Period supplies per adult per month" => 20,
96-
"Money spent purchasing period supplies" => "$30.00"
97-
}))
98-
expect(report.report[:entries]["Period supplies"].split(", "))
99-
.to contain_exactly("Tampons", "Pads", "Liners (Menstrual)")
135+
expect(report.report[:name]).to eq("Period Supplies")
136+
expect(report.report[:entries]).to match(hash_including({
137+
"% period supplies bought" => "67%",
138+
"% period supplies donated" => "33%",
139+
"Period supplies distributed" => "2,300",
140+
"Money spent purchasing period supplies" => "$40.00"
141+
}))
142+
expect(report.report[:entries]["Period supplies"].split(", "))
143+
.to contain_exactly("Adult Pads", "Adult Tampons", "Liners", "Liners (Menstrual)", "Pads", "Tampons")
144+
end
145+
146+
it "returns the correct quantity of period supplies from kits" do
147+
expect(report.distributed_period_supplies_from_kits).to eq(300)
148+
end
149+
150+
it "returns the correct quantity of donated period supplies from kits" do
151+
expect(report.donated_supplies_from_kits).to eq(50)
152+
end
153+
154+
it "returns the correct quantity of purchased items in kits" do
155+
expect(report.purchased_supplies_from_kits).to eq(150)
156+
end
100157
end
101158
end
102159
end

0 commit comments

Comments
 (0)