Skip to content

Commit

Permalink
rudimentary reporting framework for custom reports
Browse files Browse the repository at this point in the history
  • Loading branch information
acoffman committed Apr 19, 2024
1 parent 2101c2d commit 80c982c
Show file tree
Hide file tree
Showing 14 changed files with 451 additions and 23 deletions.
58 changes: 58 additions & 0 deletions server/app/admin/reports_admin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Trestle.admin(:reports) do
menu do
item :reports, icon: "fas fa-file-alt"
end

controller do
def index
@reports = Report::AVAILABLE_REPORTS
end

def show
@report = Report::AVAILABLE_REPORTS.find { |x| params[:name] == x.name }
end

def generate_report
@report = Report::AVAILABLE_REPORTS.find { |x| params[:name] == x.name }
report_params = params.permit(@report.inputs.keys).to_h
report_instance = @report.new(report_params.symbolize_keys)
report_instance.perform unless report_instance.errors.any?

if report_instance.errors.any?
flash[:error] = report_instance.errors.join("\n")
render :show
else
if params[:format] == "download"
stream_table(report_instance)
else
@data = report_instance.data
@headers = report_instance.headers
render :result
end
end
end

private
def stream_table(report)
require 'csv'
headers.delete("Content-Length")
headers["Cache-Control"] = "no-cache"
headers["Content-Type"] = "text/csv"
headers["Content-Disposition"] = "attachment; filename=\"#{report.class.name}-#{Date.today}.tsv\""
headers["X-Accel-Buffering"] = "no"
response.status = 200

self.response_body = Enumerator.new do |stream|
stream << CSV.generate_line(report.headers, col_sep: "\t")
report.data.each do |row|
stream << CSV.generate_line(row, col_sep: "\t")
end
end
end
end

routes do
get '/:name', action: :show
post '/:name', action: :generate_report
end
end
35 changes: 35 additions & 0 deletions server/app/admin/utilities_admin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Trestle.admin(:utilities) do
menu do
item :utilities, icon: "fas fa-cogs"
end

controller do
def index
@utilities = ActionWrapper::AVAILABLE_ACTIONS
end

def show
@util = ActionWrapper::AVAILABLE_ACTIONS.find { |x| params[:name] == x.name }
end

def perform_action
@util = ActionWrapper::AVAILABLE_ACTIONS.find { |x| params[:name] == x.name }
util_params = params.permit(@util.inputs.keys).to_h
action = @util.new
res = action.perform(util_params.symbolize_keys)

if res.errors.any?
flash[:error] = res.errors.join("\n")
else
flash[:message] = "#{@util.name} Succeeded"
end

render :show
end
end

routes do
get '/:name', action: :show
post '/:name', action: :perform_action
end
end
44 changes: 21 additions & 23 deletions server/app/models/actions/merge_accounts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ module Actions
class MergeAccounts
include Actions::Transactional

attr_reader :account_to_keep, :accounts_to_merge, :old_account_ids
attr_reader :account_to_keep, :account_to_merge, :old_account_id

def initialize(account_to_keep:, accounts_to_merge_in:)
@account_to_keep = account_to_keep
@accounts_to_merge = Array(accounts_to_merge_in)
@old_account_ids = @accounts_to_merge.map(&:id)
def initialize(account_id_to_keep:, account_id_to_merge_in:)
@account_to_keep = User.find(account_id_to_keep)
@old_account_id = account_id_to_merge_in
@account_to_merge = User.find(account_id_to_merge_in)
end

def execute
Expand All @@ -24,58 +24,58 @@ def execute
merge_organizations
move_legacy_changes
account_to_keep.save!
accounts_to_merge.each { |u| u.destroy! }
account_to_merge.destroy!
end

private
def move_authorizations
auths = Authorization.where(user_id: old_account_ids)
auths = Authorization.where(user_id: old_account_id)
auths.each do |a|
a.user_id = account_to_keep.id
a.save!
end
end

def move_activities
activities = Activity.where(user_id: old_account_ids)
activities = Activity.where(user_id: old_account_id)
activities.each do |a|
a.user_id = account_to_keep.id
a.save!
end
end

def move_events
events = Event.where(originating_user_id: old_account_ids)
events = Event.where(originating_user_id: old_account_id)
events.each do |e|
e.originating_user_id = account_to_keep.id
e.save!
end
end

def move_comments
comments = Comment.where(user_id: old_account_ids)
comments = Comment.where(user_id: old_account_id)
comments.each do |c|
c.user_id = account_to_keep.id
c.save!
end
end

def move_flags
flagging = Flag.where(flagging_user_id: old_account_ids)
flagging = Flag.where(flagging_user_id: old_account_id)
flagging.each do |f|
f.flagging_user_id = account_to_keep.id
f.save!
end

resolving = Flag.where(resolving_user_id: old_account_ids)
resolving = Flag.where(resolving_user_id: old_account_id)
resolving.each do |f|
f.resolving_user_id = account_to_keep.id
f.save!
end
end

def move_subscriptions
subs = Subscription.where(user_id: old_account_ids)
subs = Subscription.where(user_id: old_account_id)
subs.each do |s|
if Subscription.where(subscribable: s.subscribable, user_id: account_to_keep.id).exists?
s.destroy!
Expand All @@ -87,7 +87,7 @@ def move_subscriptions
end

def move_notifications
notified = Notification.where(notified_user_id: old_account_ids)
notified = Notification.where(notified_user_id: old_account_id)
notified.each do |n|
if Notification.where(event_id: n.event_id, notified_user_id: account_to_keep.id).exists?
n.destroy!
Expand All @@ -97,7 +97,7 @@ def move_notifications
end
end

notifier = Notification.where(originating_user_id: old_account_ids)
notifier = Notification.where(originating_user_id: old_account_id)
notifier.each do |n|
if Notification.where(event_id: n.event_id, originating_user_id: account_to_keep.id).exists?
n.destroy!
Expand All @@ -109,7 +109,7 @@ def move_notifications
end

def move_mentions
mentions = UserMention.where(user_id: old_account_ids)
mentions = UserMention.where(user_id: old_account_id)
mentions.each do |m|
if UserMention.where(user_id: account_to_keep.id, comment_id: m.comment_id)
m.destroy!
Expand All @@ -121,30 +121,28 @@ def move_mentions
end

def merge_roles
account_to_keep.role = Role.highest_role_for_users(account_to_keep, *accounts_to_merge)
account_to_keep.role = Role.highest_role_for_users(account_to_keep, account_to_merge)
end

def move_source_suggestions
suggestions = SourceSuggestion.where(user_id: old_account_ids)
suggestions = SourceSuggestion.where(user_id: old_account_id)
suggestions.each do |s|
s.user_id = account_to_keep.id
s.save!
end
end

def merge_organizations
orgs_to_add = accounts_to_merge.flat_map(&:organizations)
accounts_to_merge.each do |user|
user.organizations = []
end
orgs_to_add = account_to_merge.organizations
account_to_merge.organizations = []
existing_orgs = account_to_keep.organizations
all_orgs = orgs_to_add + existing_orgs
account_to_keep.organizations = all_orgs.uniq
end

class SuggestedChange < ActiveRecord::Base; end
def move_legacy_changes
changes = SuggestedChange.where(user_id: old_account_ids)
changes = SuggestedChange.where(user_id: old_account_id)
changes.each do |sc|
sc.user_id = account_to_keep.id
sc.save!
Expand Down
58 changes: 58 additions & 0 deletions server/app/reports/clingen_counts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
class ClingenCounts < Report
attr_reader :start_date, :end_date, :all_org_ids
CLINGEN_ORG_ID = 2

def self.name
"ClinGen Contributions"
end

def self.description
"Count contributions from ClinGen member orgs over a specified timespan."
end

def self.inputs
{
start_date: :date,
end_date: :date,
include_suborgs: :boolean
}
end

def setup(start_date:, end_date:, include_suborgs:)
@start_date = Date.parse(start_date)
@end_date = Date.parse(end_date)

clingen_org = Organization.find(CLINGEN_ORG_ID)
if include_suborgs
sub_groups = clingen_org.groups
@all_org_ids = [clingen_org.id] + sub_groups.map(&:id)
else
@all_org_ids = clingen_org.id
end
end

def headers
["Contribution Type", "Count"]
end

def execute
data << ["Assertions Submitted", SubmitAssertionActivity.where(organization_id: all_org_ids, created_at: (start_date..end_date)).count]
data << ["Evidence Submitted", SubmitEvidenceItemActivity.where(organization_id: all_org_ids, created_at: (start_date..end_date)).count]
data << ["Comments Made", CommentActivity.where(organization_id: all_org_ids, created_at: (start_date..end_date)).count]
data << ["Revisions Suggested", Event.where(organization_id: all_org_ids, action: 'revision suggested', created_at: (start_date..end_date)).count]
data << ["Proposed Revisions Accepted", calculate_contribution('revision suggested', 'revision suggested')]
data << ["Submitted Evidence Accepted", calculate_contribution('accepted', 'submitted')]
data << ["Submitted Assertions Accepted", calculate_contribution('assertion accepted', 'assertion submitted')]
end

private
def calculate_contribution(submission_event_name, accept_event_name)
contributed_by_clingen = 0
Event.where(action: accept_event_name, created_at: (start_date..end_date)).find_each do |e|
if Event.where(action: submission_event_name, organization_id: all_org_ids, subject: e.subject).exists?
contributed_by_clingen += 1
end
end
contributed_by_clingen
end
end
70 changes: 70 additions & 0 deletions server/app/reports/report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
class Report
AVAILABLE_REPORTS = [
ClingenCounts
]

def initialize(params)
setup(**params)
rescue => e
errors << e.message
end

attr_reader :data, :headers, :errors

def self.name
raise NotImplementedError.new("Specify in subclass")
end

def self.description
raise NotImplementedError.new("Specify in subclass")
end

# Can users download this as a TSV
def self.downloadable?
true
end

# Can users view this directly in the admin UI
def self.viewable?
true
end

def self.inputs
#format input_name: :type
#supported types :text, :date, :boolean, :int
{}
end

# Column headers for the report
def headers
raise NotImplementedError.new("Specify in subclass")
end

# Data rows. #execute should append rows to this list
def data
@data ||= []
end

# Append any errors here
def errors
@errors ||= []
end

def perform
execute
rescue => e
errors << e.message
end

# Called from constructor.
# Will receive named arguments from the form inputs
# specified by self.inputs
def setup
raise NotImplementedError.new("Specify in subclass")
end

# Invoke the report logic. Must set data, headers, or errors
def execute
raise NotImplementedError.new("Specify in subclass")
end
end
Loading

0 comments on commit 80c982c

Please sign in to comment.