Skip to content

Commit 1fc2f33

Browse files
authored
DEV-1241 SSD Proxy Reports from PT (#227)
1 parent 4f9e2c8 commit 1fc2f33

26 files changed

+822
-11
lines changed

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ gem "flag-icons-rails"
9292
gem "rails-i18n", "~> 6.0.0"
9393
gem "maxmind-geoip2"
9494
gem "jira-ruby"
95+
gem "ransack"
96+
gem "kaminari"
9597

9698
gem "canister"
9799
gem "ettin"

Gemfile.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,18 @@ GEM
181181
thor (>= 0.14, < 2.0)
182182
json (2.6.2)
183183
jwt (2.5.0)
184+
kaminari (1.2.2)
185+
activesupport (>= 4.1.0)
186+
kaminari-actionview (= 1.2.2)
187+
kaminari-activerecord (= 1.2.2)
188+
kaminari-core (= 1.2.2)
189+
kaminari-actionview (1.2.2)
190+
actionview
191+
kaminari-core (= 1.2.2)
192+
kaminari-activerecord (1.2.2)
193+
activerecord
194+
kaminari-core (= 1.2.2)
195+
kaminari-core (1.2.2)
184196
listen (3.0.8)
185197
rb-fsevent (~> 0.9, >= 0.9.4)
186198
rb-inotify (~> 0.9, >= 0.9.7)
@@ -280,6 +292,10 @@ GEM
280292
thor (~> 1.0)
281293
rainbow (3.1.1)
282294
rake (13.1.0)
295+
ransack (4.2.1)
296+
activerecord (>= 6.1.5)
297+
activesupport (>= 6.1.5)
298+
i18n
283299
rb-fsevent (0.11.2)
284300
rb-inotify (0.10.1)
285301
ffi (~> 1.0)
@@ -405,6 +421,7 @@ DEPENDENCIES
405421
jbuilder (~> 2.5)
406422
jira-ruby
407423
jquery-rails
424+
kaminari
408425
keycard!
409426
listen (>= 3.0.5, < 3.2)
410427
loofah (~> 2.19)
@@ -421,6 +438,7 @@ DEPENDENCIES
421438
rails (~> 6.1.7.7)
422439
rails-controller-testing
423440
rails-i18n (~> 6.0.0)
441+
ransack
424442
rubyzip (~> 2.0)
425443
sassc-rails
426444
selenium-webdriver

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,17 +122,22 @@ Contacts
122122
Contact Types
123123
Logs
124124
Registrations # for new users
125+
SSD Proxy Reports
125126
```
126127

127128
Most pages have the standard CRU(D) operations (not a lot of Deletes), Rails style.
128129

129130
## Design
130131

131132
There is very little branding, since it is not at all facing the public.
132-
There has been some adjustments to improve color contrast.
133+
There have been some adjustments to improve color contrast.
133134
Otis uses `select2.org` JavaScript library to make searchable lists of items (users, institutions).
134135
Also uses `Ckeditor` for rich text editing used in composing emails.
135136

137+
All index pages use Bootstrap Table (https://bootstrap-table.com) for data display. SSD Proxy Reports
138+
has advanced search features server-side using Ransack (https://github.com/activerecord-hackery/ransack).
139+
This approach is expected to be a model for updating the other index pages.
140+
136141
## Functionality
137142

138143
### Jira
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
3+
class HTSSDProxyReportsController < ApplicationController
4+
# This class is only responsible for an index page. There are no detail views or editing
5+
# capabilities.
6+
# Bootstrap Table gets all its data server-side, so most of the plumbing in this class
7+
# is in support of `format=json` queries.
8+
9+
# Extensive use is made of Ransack (https://github.com/activerecord-hackery/ransack)
10+
# which I chose because assembling LIKE queries in this controller appeared likely to
11+
# become a rabbit hole.
12+
13+
# Pagination is provided by Kaminari. See the `results.page(...).per(...)` calls.
14+
15+
# This is what a JSON request looks like when it comes in from Bootstrap Table:
16+
# ?format=json&pageSize=10&pageNumber=1&filter={"rights_code":"pdus"}&dateStart=2023-09-19&dateEnd=2024-09-19&sortName=inst_code&sortOrder=asc
17+
# - dateStart and dateEnd are the two date ranges initially populated by @date_start and @date_end
18+
# - sortName and sortOrder, if present, reflect the user's interaction with the column sort controls.
19+
# - filter={...} reflects the filters selected or typed into the column filters in the table.
20+
21+
# The `filter` keys come from HTSSDProxyReportPresenter::ALL_FIELDS which combines relevant
22+
# columns from the three associated database tables.
23+
24+
# Used by `#matchers` to translate `filter` keys into values that the `#ransack` method
25+
# can apply to the Active Record query. Many of these (the text input ones)
26+
# are of the form `*_i_cont` which is a case-insensitive contains equivalent to "LIKE '%value%'".
27+
# Those selectable by a dropdown menu can use an equality (`_eq`) matcher.
28+
RANSACK_MATCHERS = {
29+
"author" => :ht_hathifile_author_i_cont,
30+
"bib_num" => :ht_hathifile_bib_num_cont,
31+
"content_provider_code" => :ht_hathifile_content_provider_code_eq,
32+
"datetime" => :datetime_start,
33+
"digitization_agent_code" => :ht_hathifile_digitization_agent_code,
34+
"email" => :email_i_cont,
35+
"htid" => :htid_i_cont,
36+
"imprint" => :ht_hathifile_imprint_i_cont,
37+
"inst_code" => :inst_code_eq,
38+
"institution_name" => :ht_institution_name_i_cont,
39+
"rights_code" => :ht_hathifile_rights_code_eq,
40+
"rights_date_used" => :ht_hathifile_rights_date_used_eq,
41+
"title" => :ht_hathifile_title_i_cont
42+
}
43+
44+
# Translation table from params[:sortName] to a form Ransack can understand.
45+
RANSACK_ORDER = {
46+
"author" => :ht_hathifile_author,
47+
"bib_num" => :ht_hathifile_bib_num,
48+
"content_provider_code" => :ht_hathifile_content_provider_code,
49+
"datetime" => :datetime,
50+
"digitization_agent_code" => :ht_hathifile_digitization_agent_code,
51+
"email" => :email,
52+
"htid" => :htid,
53+
"imprint" => :ht_hathifile_imprint,
54+
"inst_code" => :inst_code,
55+
"institution_name" => :ht_institution_name,
56+
"rights_code" => :ht_hathifile_rights_code,
57+
"rights_date_used" => :ht_hathifile_rights_date_used,
58+
"title" => :ht_hathifile_title
59+
}
60+
61+
def index
62+
respond_to do |format|
63+
format.html do
64+
# Populate the date range fields with the latest datetime and
65+
# then the start date a year earlier
66+
@date_end = HTSSDProxyReport.maximum(:datetime).tap do |dt_end|
67+
@date_start = (dt_end - 1.year).to_date.to_s
68+
end.to_date.to_s
69+
end
70+
format.json do
71+
render json: json_query
72+
end
73+
end
74+
end
75+
76+
private
77+
78+
# @return [Hash] value to be returned to Bootstrap Table as JSON
79+
def json_query
80+
# Create a Ransack::Search with all of the filter fields translated into Ransack matchers.
81+
search = HTSSDProxyReport.includes(:ht_hathifile, :ht_institution)
82+
.ransack(matchers)
83+
# Apply the sort field and order, or default if not provided.
84+
# Ransack requires lower case sort direction.
85+
sort_name = RANSACK_ORDER.fetch(params[:sortName], "datetime")
86+
sort_order = params.fetch(:sortOrder, "asc")
87+
search.sorts = "#{sort_name} #{sort_order.downcase}"
88+
# Extract HTSSDProxyReport::ActiveRecord_Relation
89+
result = search.result
90+
# total is the number of results after user-selected filters e.g. {"rights_code":"pdus"}
91+
# totalNotFiltered (see a few lines below) is the SELECT * for the whole shebang
92+
total = result.count
93+
# Paginate using Kaminari. index UI is always paginated.
94+
# When exporting to Excel and the like, there is no pagination
95+
# (hence performance issues on large data sets).
96+
if params[:pageNumber] && params[:pageSize]
97+
result = result.page(params[:pageNumber]).per(params[:pageSize])
98+
end
99+
# Translate each row of the result into JSON and stick it into struct with totals.
100+
{
101+
total: total,
102+
totalNotFiltered: HTSSDProxyReport.count,
103+
rows: result.map { |line| line_to_json line }
104+
}
105+
end
106+
107+
# Use presenter to translate HTSSDProxyReport into JSON hash.
108+
# This is called for each object in the result.
109+
def line_to_json(report)
110+
report = presenter report
111+
HTSSDProxyReportPresenter::ALL_FIELDS.to_h do |field|
112+
[field, report.field_value(field)]
113+
end
114+
end
115+
116+
def presenter(report)
117+
HTSSDProxyReportPresenter.new(report, controller: self, action: params[:action].to_sym)
118+
end
119+
120+
# Filter param (if any) sent by Bootstrap Table translated into Hash.
121+
# This will be subsequently be translated into a form Ransack can understand.
122+
def filter
123+
@filter ||= JSON.parse(params.fetch("filter", "{}"))
124+
end
125+
126+
# Translate Bootstrap Table filter fields and date start/end fields
127+
# into Ransack matchers
128+
def matchers
129+
return @matchers if @matchers
130+
131+
@matchers = filter.transform_keys do |key|
132+
RANSACK_MATCHERS.fetch(key, key)
133+
end
134+
if params[:dateStart]
135+
@matchers[:datetime_gteq] = params[:dateStart]
136+
end
137+
if params[:dateEnd]
138+
@matchers[:datetime_lteq] = params[:dateEnd]
139+
end
140+
@matchers
141+
end
142+
end

app/helpers/application_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module ApplicationHelper
44
def nav_menu
5-
%w[users approval_requests institutions contacts contact_types logs registrations]
5+
%w[users approval_requests institutions contacts contact_types logs registrations ssd_proxy_reports]
66
.select { |item| can?(:index, "ht_#{item}") }
77
.map { |item| {item: item, path: send("ht_#{item}_path")} }
88
end

app/models/ht_hathifile.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
# Used as a secondary source of information for SSD Proxy Reports
4+
# Only fleshed out to the extent needed. There's a lot we could do here.
5+
class HTHathifile < ApplicationRecord
6+
self.table_name = "hathifiles.hf"
7+
self.primary_key = "htid"
8+
has_many :ht_ssd_proxy_report, foreign_key: :htid, primary_key: :htid
9+
10+
# SSD Proxy Reports uses Ransack gem to search by association
11+
def self.ransackable_attributes(auth_object = nil)
12+
%w[
13+
author bib_num content_provider_code digitization_agent_code htid imprint
14+
rights_code rights_date_used title
15+
]
16+
end
17+
18+
def self.ransackable_associations(auth_object = nil)
19+
%w[ht_ssd_proxy_report]
20+
end
21+
22+
private
23+
24+
# Ransack search matchers assume string values, so convert this integer
25+
ransacker :bib_num do
26+
Arel.sql("CONVERT(#{table_name}.bib_num, CHAR(9))")
27+
end
28+
end

app/models/ht_institution.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ class HTInstitution < ApplicationRecord
1616

1717
before_save :set_defaults
1818

19+
# SSD Proxy Reports uses Ransack gem to search by association
20+
def self.ransackable_attributes(auth_object = nil)
21+
%w[inst_id name]
22+
end
23+
1924
# https://stackoverflow.com/a/57485464
2025
attribute :enabled, ActiveRecord::Type::Integer.new
2126

app/models/ht_ssd_proxy_report.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
# Only used read-only in Otis for reporting
4+
class HTSSDProxyReport < ApplicationRecord
5+
self.table_name = "ht_web.reports_downloads_ssdproxy"
6+
# default_scope { order(:datetime) }
7+
has_one :ht_hathifile, foreign_key: :htid, primary_key: :htid
8+
has_one :ht_institution, foreign_key: :inst_id, primary_key: :inst_code
9+
10+
def self.ransackable_attributes(auth_object = nil)
11+
["datetime", "email", "htid", "id", "in_copyright", "inst_code", "is_partial", "sha", "yyyy", "yyyymm"]
12+
end
13+
14+
def self.ransackable_associations(auth_object = nil)
15+
["ht_hathifile", "ht_institution"]
16+
end
17+
18+
def institution_name
19+
institution&.name
20+
end
21+
22+
def institution
23+
ht_institution
24+
end
25+
26+
def hf
27+
ht_hathifile
28+
end
29+
30+
ransacker :datetime do
31+
Arel.sql("DATE(#{table_name}.datetime)")
32+
end
33+
end

0 commit comments

Comments
 (0)