Skip to content

Commit b36408c

Browse files
authored
Merge pull request #2651 from sciencehistory/export_csv_from_cart
CSV report from cart
2 parents fb0f062 + bd09995 commit b36408c

File tree

6 files changed

+279
-1
lines changed

6 files changed

+279
-1
lines changed

app/controllers/admin/cart_items_controller.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
class Admin::CartItemsController < AdminController
22
before_action :authenticate_user! # need to be logged in
3+
require 'csv'
4+
35

46
# GET /admin/cart_items
57
# GET /admin/cart_items.json
@@ -69,4 +71,13 @@ def clear
6971
redirect_to admin_cart_items_url, notice: "emptied cart"
7072
end
7173

74+
def report
75+
begin
76+
serializer = WorkCartSerializer.new(current_user.works_in_cart)
77+
output_csv_file = serializer.csv_tempfile
78+
send_file output_csv_file.path, filename: "cart-report-#{Date.today.to_s}.csv"
79+
ensure
80+
output_csv_file.close
81+
end
82+
end
7283
end
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Serializes one work to a hash, or a Tempfile.new containing CSV info
2+
#
3+
# The serializer will use these associations, so they should be eager-loaded:
4+
# * leaf_representative
5+
# * contained_by (collection)
6+
#
7+
# serializer = WorkCartSerializer.new(columns)
8+
# serializer.row(work) # returns an array you can use in a report.
9+
10+
class WorkCartSerializer
11+
12+
def initialize(scope, columns: nil, extra_separator: '|')
13+
@scope = scope
14+
@extra_separator = extra_separator
15+
@column_keys = if columns.nil?
16+
all_columns.keys
17+
else
18+
columns.select { |c| all_columns.keys.include? c }
19+
end
20+
end
21+
22+
# Does not close the tempfile - that's your responsibility.
23+
def csv_tempfile
24+
output_csv_file = Tempfile.new
25+
CSV.open(output_csv_file, "wb") do |csv|
26+
self.to_a.each { |row| csv << row }
27+
end
28+
output_csv_file
29+
end
30+
31+
def to_a
32+
data = []
33+
data << title_row
34+
@scope.includes(:leaf_representative, :contained_by).find_each do |work|
35+
data << row(work)
36+
end
37+
data
38+
end
39+
40+
def title_row
41+
@column_keys.map {|k| all_columns[k]}
42+
end
43+
44+
def row(work)
45+
@column_keys.map do |k|
46+
column_to_string(
47+
column_methods[k].call(work)
48+
)
49+
end
50+
end
51+
52+
# A hash of possible columns (and their titles)
53+
# you can use in the report.
54+
# By default, the report contains all these columns,
55+
# but you can pass `columns` to return fewer.
56+
def all_columns
57+
@all_columns ||= {
58+
title: 'Title',
59+
additional_title: 'Additional title',
60+
url: 'URL',
61+
external_id: 'External ID',
62+
department: 'Department',
63+
creator: 'Creator',
64+
date: 'Date',
65+
medium: 'Medium',
66+
extent: 'Extent',
67+
place: 'Place',
68+
genre: 'Genre',
69+
description: 'Description',
70+
subject: 'Subject/s',
71+
series_arrangement: 'Series Arrangement',
72+
physical_container: 'Physical Container',
73+
collection: 'Collection',
74+
rights: 'Rights',
75+
rights_holder: 'Rights Holder',
76+
additional_credit: 'Additional Credit',
77+
digitization_funder: 'Digitization Funder',
78+
admin_note: 'Admin Note',
79+
created: 'Created',
80+
last_modified: 'Last Modified'
81+
}
82+
end
83+
84+
# Returns a hash.
85+
# The keys of the hash are the same as @column_keys .
86+
# The values of the hash are procs.
87+
#
88+
# Each proc
89+
# takes the work as an argument and
90+
# returns the metadata we want.
91+
#
92+
# { :title => method(work), :additional_title => method(work), ... }
93+
def column_methods
94+
@column_methods ||= @column_keys.map do |column_label|
95+
96+
# create the proc
97+
new_proc = if self.respond_to? column_label
98+
# If k is defined in this class, use that (e.g. :created)
99+
Proc.new { |some_work| self.send column_label, some_work }
100+
elsif Work.method_defined? column_label
101+
# Or, if k is defined as a method on work, use that (e.g. :title)
102+
Proc.new { |some_work| some_work.send column_label }
103+
else
104+
raise "Unknown column: #{column_label}"
105+
end
106+
107+
[column_label, new_proc]
108+
end.to_h
109+
end
110+
111+
def column_to_string(arr_or_string)
112+
return '' if arr_or_string.nil?
113+
return arr_or_string.join(@extra_separator) if arr_or_string.is_a?(Array)
114+
arr_or_string
115+
end
116+
117+
def url(work)
118+
app_url_base + Rails.application.routes.url_helpers.work_path(work.friendlier_id)
119+
end
120+
121+
def external_id(work)
122+
work.external_id.map(&:value)
123+
end
124+
125+
def creator(work)
126+
work.creator.map(&:value)
127+
end
128+
129+
def place(work)
130+
work.place.map(&:value)
131+
end
132+
133+
def collection(work)
134+
work.contained_by.map(&:title)
135+
end
136+
137+
def date(work)
138+
DateDisplayFormatter.new(work.date_of_work).display_dates
139+
end
140+
141+
def description(work)
142+
DescriptionDisplayFormatter.new(work.description).format_plain
143+
end
144+
145+
def physical_container(work)
146+
return nil if work.physical_container.nil?
147+
work.physical_container.attributes.map {|l, v | "#{l.humanize}: #{v}" if v.present? }.compact
148+
end
149+
150+
def additional_credit(work)
151+
work.additional_credit.map{ |item| "#{item.role}:#{item.name}" }
152+
end
153+
154+
def created(work)
155+
I18n.l work.created_at, format: :admin
156+
end
157+
158+
def last_modified(work)
159+
I18n.l work.updated_at, format: :admin
160+
end
161+
162+
protected
163+
164+
def app_url_base
165+
@app_url_base ||= ScihistDigicoll::Env.lookup!(:app_url_base)
166+
end
167+
end

app/views/admin/cart_items/index.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<div class="d-flex justify-content-between">
44
<p>
55
<%= link_to "Batch Edit", batch_update_admin_works_path, class: "btn btn-primary" %>
6+
<%= link_to "Save a report", report_admin_cart_items_path, class: "btn btn-primary", data: { confirm: "Save a CSV report?"}, method: "post" %>
67
<%= link_to "Clear Cart", clear_admin_cart_items_path, class: "btn btn-outline-danger", data: { confirm: "Delete all items in cart?"}, method: "delete" %>
78
</p>
89

config/routes.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,9 +329,10 @@ def self.matches?(request)
329329
to: "digitization_queue_items#delete_comment",
330330
as: "delete_digitization_queue_item_comment"
331331

332-
resources :cart_items, param: :work_friendlier_id, only: [:index, :update, :destroy] do
332+
resources :cart_items, param: :work_friendlier_id, only: [:index, :update, :destroy, :report] do
333333
collection do
334334
delete 'clear'
335+
post 'report'
335336
end
336337
end
337338

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'rails_helper'
2+
RSpec.describe Admin::CartItemsController, :logged_in_user, type: :controller, queue_adapter: :test do
3+
context "smoke test for report" do
4+
# See also the test for the cart_exporter itself at
5+
# at spec/services/cart_exporter_spec.rb
6+
let(:work_1) { FactoryBot.create(:work, :with_assets)}
7+
let(:work_2) { FactoryBot.create(:work, :with_assets)}
8+
let(:work_3) { FactoryBot.create(:work, :with_assets)}
9+
before do
10+
controller.current_user.works_in_cart = [work_1, work_2, work_3]
11+
end
12+
13+
it "export smoke test" do
14+
post :report
15+
expect(response.status).to eq(200)
16+
expect(response.headers["Content-Type"]).
17+
to eq 'text/csv'
18+
expect(response.headers["Content-Disposition"]).
19+
to match %r{attachment; filename=\"cart-report-.*.csv\"}
20+
end
21+
end
22+
end
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'rails_helper'
2+
3+
describe WorkCartSerializer do
4+
let(:scope) { Work.all.includes(:leaf_representative, :contained_by) }
5+
let(:report) { WorkCartSerializer.new(scope).to_a }
6+
7+
context "smoke test with two works" do
8+
let!(:work_1) { create(:public_work, title: "Title of work 1") }
9+
let!(:work_2) { create(:public_work, title: "Title of work 2") }
10+
it "returns a report with correct titles and one row per work" do
11+
expect(report[0]).to eq [
12+
"Title", "Additional title",
13+
"URL", "External ID", "Department", "Creator",
14+
"Date", "Medium", "Extent", "Place", "Genre",
15+
"Description", "Subject/s", "Series Arrangement",
16+
"Physical Container", "Collection", "Rights",
17+
"Rights Holder", "Additional Credit",
18+
"Digitization Funder", "Admin Note",
19+
"Created", "Last Modified"
20+
]
21+
expect(report.length).to eq 3
22+
end
23+
end
24+
25+
context "work with all metadata in it" do
26+
let!(:work_1) { create(:public_work, :with_complete_metadata, :with_collection,
27+
place_attributes: {"0"=>{"category"=> "place_of_creation", "value"=>"Illinois--Peoria"}},
28+
subject: ['s1', 's2', 's3'],
29+
) }
30+
it "values are correct; all columns present" do
31+
expect(report.length).to eq 2
32+
expect(report[1][0]).to eq 'Test title'
33+
expect(report[1][1]).to eq 'Additional Title 1|Additional Title 2'
34+
expect(report[1][2]).to match /http.*works/
35+
expect(report[1][3]).to eq 'Past Perfect ID 1|Sierra Bib Number 1|Sierra Bib Number 2|Accession Number 1'
36+
expect(report[1][4]).to eq 'Center for Oral History'
37+
expect(report[1][5]).to eq 'After 1|Author 1|Contributor 1'
38+
expect(report[1][6]).to eq 'Before 2014-Jan-01 – circa 2014-Jan-02 (Note 1)|Before 2014-Feb-03 – circa 2014-Feb-04 (Note 2)|Before 2014-Mar-05 – circa 2014-Mar-06 (Note 3)'
39+
expect(report[1][7]).to eq 'Audiocassettes|Celluloid|Dye'
40+
expect(report[1][8]).to eq '0.75 in. H x 2.5 in. W|80 cm L x 22 cm Diam.'
41+
expect(report[1][9]).to eq 'Illinois--Peoria' # place
42+
expect(report[1][10]).to eq 'Lithographs' # genre
43+
expect(report[1][11]).to eq 'Description 1' # description
44+
expect(report[1][12]).to eq 's1|s2|s3' # subject
45+
expect(report[1][13]).to eq 'Series arrangement 1|Series arrangement 2'
46+
expect(report[1][14]).to eq 'Box: Box|Page: Page|Part: Part|Reel: Reel|Folder: Folder|Volume: Volume|Shelfmark: Shelfmark'
47+
expect(report[1][15]).to eq 'Test title' # collection
48+
expect(report[1][16]).to eq "http://rightsstatements.org/vocab/NoC-US/1.0/"
49+
expect(report[1][17]).to eq "Rights Holder"
50+
expect(report[1][18]).to eq "photographed_by:Douglas Lockard|photographed_by:Mark Backrath"
51+
expect(report[1][19]).to eq "Daniel Sanford"
52+
expect(report[1][20]).to eq "Admin Note"
53+
expect(Date.parse(report[1][21])).to be_a Date
54+
expect(Date.parse(report[1][22])).to be_a Date
55+
end
56+
end
57+
58+
context "do not include child works; respect order of columns" do
59+
let!(:parent) { create(:public_work,
60+
title: "The parent",
61+
members: [child],
62+
creator_attributes: {
63+
"0"=>{"category"=> "author", "value"=>"creator1"},
64+
"1"=>{"category"=> "publisher", "value" => "publisher1 " }
65+
},
66+
)
67+
}
68+
let(:child) { create(:public_work, title: "child title") }
69+
let(:scope) { Work.where(title: 'The parent') }
70+
let(:report) { WorkCartSerializer.new(scope, columns: [:creator, :title]).to_a }
71+
it "returns a report" do
72+
expect(report).to match_array([["Creator", "Title"], ["creator1|publisher1 ", "The parent"]])
73+
end
74+
end
75+
76+
end

0 commit comments

Comments
 (0)