Skip to content

Commit 70b1336

Browse files
authored
feat(exports): Add ability to generate data exports for sponsors (#199)
Fixes #133
1 parent b8da138 commit 70b1336

File tree

14 files changed

+375
-1
lines changed

14 files changed

+375
-1
lines changed

app/controllers/manage/application_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ def logged_in
88
authenticate_user!
99
end
1010

11+
def require_full_admin
12+
return redirect_to root_path unless current_user.try(:admin?)
13+
end
14+
1115
def require_admin_or_limited_admin
1216
return redirect_to root_path unless current_user.try(:admin?) || current_user.try(:admin_limited_access?)
1317
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
class Manage::DataExportsController < Manage::ApplicationController
2+
skip_before_action :require_admin_or_limited_admin
3+
before_action :require_full_admin
4+
5+
before_action :set_data_export, only: [:destroy]
6+
7+
respond_to :html, :json
8+
9+
# GET /manage/data_exports
10+
def index
11+
@data_exports = DataExport.all.order(created_at: :desc)
12+
@params = {}
13+
if params[:export_type]
14+
@params = params.require(:data_export).permit(:export_type).reject { |_, v| v.blank? }
15+
@data_exports = @data_exports.where(@params)
16+
end
17+
respond_with(:manage, @data_exports)
18+
end
19+
20+
# GET /manage/data_exports/new
21+
def new
22+
export_type = params[:export_type]
23+
@data_export = DataExport.new(export_type: export_type)
24+
respond_with(:manage, @data_export)
25+
end
26+
27+
# POST /manage/data_exports
28+
def create
29+
@data_export = DataExport.new(data_export_params)
30+
31+
if @data_export.save
32+
@data_export.enqueue!
33+
respond_to do |format|
34+
format.html { redirect_to manage_data_exports_path, notice: "Data export was successfully created." }
35+
format.json { render json: @data_export }
36+
end
37+
else
38+
response_view_or_errors :new, @data_export
39+
end
40+
end
41+
42+
# DELETE /manage/data_exports/1
43+
def destroy
44+
@data_export.destroy
45+
respond_to do |format|
46+
format.html { redirect_to manage_data_exports_path, notice: "Data export was successfully destroyed." }
47+
format.json { render json: @data_export }
48+
end
49+
end
50+
51+
private
52+
53+
# Use callbacks to share common setup or constraints between actions.
54+
def set_data_export
55+
@data_export = DataExport.find(params[:id])
56+
end
57+
58+
# Only allow a trusted parameter "white list" through.
59+
def data_export_params
60+
params.require(:data_export).permit(:export_type)
61+
end
62+
end

app/jobs/generate_data_export_job.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
require "zip"
2+
3+
class GenerateDataExportJob < ApplicationJob
4+
queue_as :default
5+
6+
def perform(*args)
7+
data_export = args[0]
8+
# Prevent an already-started or already-completed job from running again
9+
return unless data_export.status == "queued"
10+
11+
data_export.update_attribute(:started_at, Time.now)
12+
13+
begin
14+
case data_export.export_type
15+
when "sponsor_dump_rsvp_confirmed"
16+
generate__sponsor_dump(data_export, "rsvp_confirmed")
17+
when "sponsor_dump_checked_in"
18+
generate__sponsor_dump(data_export, "checked_in")
19+
else
20+
raise "Unknown export type: #{data_export.export_type}"
21+
end
22+
23+
data_export.update_attribute(:finished_at, Time.now)
24+
rescue => ex
25+
data_export.update_attribute(:started_at, nil)
26+
data_export.update_attribute(:finished_at, nil)
27+
# Re-raise the original exception
28+
raise
29+
end
30+
end
31+
32+
private
33+
34+
def generate__sponsor_dump(data_export, attendee_type)
35+
print data_export.file.name
36+
37+
case attendee_type
38+
when "rsvp_confirmed"
39+
questionnaires = Questionnaire.where(acc_status: "rsvp_confirmed", can_share_info: true)
40+
when "checked_in"
41+
questionnaires = Questionnaire.where("checked_in_at > 0", can_share_info: true)
42+
else
43+
raise "Unknown attendee type: #{attendee_type}"
44+
end
45+
46+
Dir.mktmpdir("data-export") do |dir|
47+
folder_path = File.join(dir, data_export.file_basename)
48+
Dir.mkdir(folder_path)
49+
zipfile_name = "#{data_export.file_basename}.zip"
50+
zipfile_path = File.join(dir, zipfile_name)
51+
52+
# Download all of the resumes & generate CSV
53+
csv_data = []
54+
resume_paths = []
55+
questionnaires.each do |q|
56+
csv_row = [
57+
q.first_name,
58+
q.last_name,
59+
q.school_name,
60+
q.email,
61+
q.vcs_url,
62+
q.portfolio_url,
63+
]
64+
65+
if q.resume.attached?
66+
filename = "#{q.id}-#{q.resume.filename.sanitized}"
67+
puts "--> Downloading #{q.id} resume, filename '#{filename}'"
68+
path = File.join(folder_path, filename)
69+
File.open(path, "wb") do |file|
70+
file.write(q.resume.download)
71+
end
72+
resume_paths << { path: path, filename: filename }
73+
csv_row << filename
74+
else
75+
csv_row << "" # No resume file
76+
end
77+
78+
csv_data << csv_row
79+
end
80+
81+
csvfile_name = "000-Attendees.csv"
82+
csvfile_path = File.join(folder_path, csvfile_name)
83+
CSV.open(csvfile_path, "wb") do |csv|
84+
csv << ["Fist name", "Last name", "School", "Email", "VCS URL", "Portfolio URL", "Resume filename"]
85+
csv_data.each do |row|
86+
csv << row
87+
end
88+
end
89+
90+
# Zip up all of the files
91+
Zip::File.open(zipfile_path, Zip::File::CREATE) do |zipfile|
92+
# Add the CSV
93+
zipfile.add(csvfile_name, csvfile_path)
94+
# Add all resume files
95+
resume_paths.each do |resume|
96+
path = resume[:path]
97+
filename = resume[:filename]
98+
# Two arguments:
99+
# - The name of the file as it will appear in the archive
100+
# - The original file, including the path to find it
101+
zipfile.add(filename, path)
102+
end
103+
end
104+
105+
# Attach the zip file to the record
106+
data_export.file.attach(
107+
io: File.open(zipfile_path),
108+
filename: zipfile_name,
109+
content_type: "application/zip",
110+
)
111+
end
112+
end
113+
end

app/models/data_export.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
class DataExport < ApplicationRecord
2+
3+
# A DataExport is a generated .zip of data from HackathonManager, such as a .zip of
4+
# resumes & attendee data, or a .zip of the entire database is the form of multiple
5+
# CSVs.
6+
#
7+
# These should be generated asynchronously with a background job, and then stored as an
8+
# active storage attachment.
9+
10+
POSSIBLE_TYPES = [
11+
"sponsor_dump_rsvp_confirmed",
12+
"sponsor_dump_checked_in",
13+
].freeze
14+
15+
validates_presence_of :export_type
16+
validates_inclusion_of :export_type, in: POSSIBLE_TYPES
17+
18+
has_one_attached :file
19+
20+
strip_attributes
21+
22+
def file_basename
23+
time = created_at.strftime("%r").gsub(":", "-")
24+
date = created_at.strftime("%F")
25+
"#{export_type} #{date} #{time}"
26+
end
27+
28+
def finished?
29+
finished_at.present?
30+
end
31+
32+
def started?
33+
started_at.present?
34+
end
35+
36+
def queued?
37+
queued_at.present?
38+
end
39+
40+
def enqueue!
41+
raise "Data export has already been queued" unless status == "created_not_queued"
42+
43+
GenerateDataExportJob.perform_later(self)
44+
update_attribute(:queued_at, Time.now)
45+
end
46+
47+
def status
48+
return "finished" if finished?
49+
return "started" if started?
50+
return "queued" if queued?
51+
52+
"created_not_queued"
53+
end
54+
end

app/views/layouts/manage/application.html.haml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@
8989
Doorkeeper
9090
%span.fa.fa-external-link.icon-space-l-half
9191
.nav-item-description OAuth2 provider management
92+
%li.nav-item
93+
= active_link_to manage_data_exports_path, class: "nav-link" do
94+
.fa.fa-download.fa-fw.icon-space-r-half
95+
Data Exports
96+
.nav-item-description Generate & export data
9297
%main.col-md-10.ml-sm-auto.px-4{role: "main"}
9398
= render "layouts/manage/flashes"
9499
= yield
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.form-container
2+
= bs_horizontal_simple_form_for @data_export, url: url_for(action: @data_export.new_record? ? "create" : "update", controller: "data_exports") do |f|
3+
= f.error_notification
4+
5+
.form-inputs
6+
= f.input :export_type, as: :select, collection: DataExport::POSSIBLE_TYPES.map { |x| [x.titleize, x] }, include_blank: false
7+
8+
.form-actions.mb-3.mt-3
9+
= f.button :submit, class: 'btn-primary'
10+
11+
.mb-4
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
= render "layouts/manage/page_title", title: "Data Exports" do
2+
= link_to "New Data Export", new_manage_data_export_path, class: "btn btn-sm btn-outline-secondary"
3+
4+
%table.table.table-striped
5+
%thead
6+
%tr
7+
%th Type
8+
%th Created
9+
%th Timeline
10+
%th Download
11+
%th Delete
12+
13+
%tbody
14+
- if @data_exports.blank?
15+
%tr
16+
%td{colspan: 5} No data exports have been generated.
17+
- @data_exports.each do |data_export|
18+
%tr
19+
%td= data_export.export_type.titleize
20+
%td= display_datetime(data_export.created_at)
21+
%td
22+
%span
23+
Queued: #{data_export.queued_at || "n/a"}
24+
%br
25+
%span
26+
Started: #{data_export.started_at || "n/a"}
27+
%br
28+
%span
29+
Finished: #{data_export.finished_at || "n/a"}
30+
%td
31+
- if data_export.finished? && data_export.file.attached?
32+
= link_to "Download", rails_blob_path(data_export.file)
33+
- else
34+
Not available
35+
%br
36+
%small Please wait for generation to finish
37+
%td= link_to 'Delete', manage_data_export_path(data_export), method: :delete, data: { confirm: "Are you sure? The data export \"#{data_export.file_basename}\" will be permanently deleted. This action is irreversible." }, class: 'btn btn-sm btn-outline-secondary'
38+
39+
%br
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
= render "layouts/manage/page_title", title: "New Data Export"
2+
3+
= render 'form'

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,6 @@
9393
end
9494
resources :trackable_events
9595
resources :trackable_tags
96+
resources :data_exports
9697
end
9798
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class CreateDataExports < ActiveRecord::Migration[5.2]
2+
def change
3+
create_table :data_exports do |t|
4+
t.string :export_type, null: false
5+
t.datetime :queued_at
6+
t.datetime :started_at
7+
t.datetime :finished_at
8+
9+
t.timestamps
10+
end
11+
end
12+
end

0 commit comments

Comments
 (0)