Skip to content
This repository has been archived by the owner on Jul 24, 2020. It is now read-only.

Commit

Permalink
Add user and equipment CSV export
Browse files Browse the repository at this point in the history
Resolves #1337
 - adds the module CsvExport to handle exporting CSV files
 - also supports exporting multiple CSVs at once in a zip file
  • Loading branch information
Sydney Young committed Feb 11, 2016
1 parent 0b02fff commit 2978340
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 1 deletion.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ gem 'net-ldap', '~> 0.13.0'
# attachments
gem 'paperclip', '~> 4.3.2'

# for exporting multiple files
gem 'rubyzip'

# soft deletion
gem 'permanent_records', '~> 4.1.0'
gem 'nilify_blanks', '~> 1.2.1'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ GEM
ruby-progressbar (1.7.5)
rubycas-client (2.3.9)
activesupport
rubyzip (1.1.6)
safe_yaml (1.0.4)
sass (3.4.21)
sass-rails (5.0.4)
Expand Down Expand Up @@ -474,6 +475,7 @@ DEPENDENCIES
rspec-rails (~> 3.4.0)
rubocop (~> 0.35.1)
ruby-progressbar (~> 1.7.5)
rubyzip
sass-rails (~> 5.0.4)
select2-rails (~> 4.0.1)
shoulda-matchers (~> 3.0.1)
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/categories_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class CategoriesController < ApplicationController
only: [:show, :edit, :update, :destroy, :deactivate]

include ActivationHelper
include CsvExport

# --------- before filter methods -------- #
def set_current_category
Expand All @@ -18,6 +19,10 @@ def index
else
@categories = Category.active
end
respond_to do |format|
format.html
format.zip { download_equipment_data }
end
end

def show
Expand Down
5 changes: 5 additions & 0 deletions app/controllers/equipment_models_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def index
else
@equipment_models = base.includes(:reservations).active
end

respond_to do |format|
format.html
format.zip { download_equipment_data }
end
end

def show # rubocop:disable AbcSize, MethodLength
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class UsersController < ApplicationController
before_action :check_cas_auth, only: [:show, :new, :create, :edit, :update]

include Autocomplete
include CsvExport

# ------------ before filter methods ------------ #

Expand All @@ -32,6 +33,14 @@ def index
else
@users = User.active.order('username ASC')
end

respond_to do |format|
format.html
format.csv do
col = %w(username first_name last_name nickname phone email affiliation)
download_csv(User.all, col, "users_#{Time.zone.today}")
end
end
end

def show
Expand Down
1 change: 1 addition & 0 deletions app/views/categories/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<% if can? :manage, Category %>
<%= link_to "New Category", new_category_path, :class => "btn btn-primary" %>
<%= link_to "Import Categories", equip_import_page_path, :class => "btn btn-default" %>
<%= link_to "Export Equipment Data", categories_path(format: 'zip'), :class => "btn btn-default" %>
<% end %>
<% if params[:show_deleted] %>
<%= link_to "Hide Old Categories from List", categories_path, :class => "btn btn-default" %>
Expand Down
1 change: 1 addition & 0 deletions app/views/equipment_items/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<% else %>
<%= link_to "New Equipment Item", new_equipment_item_path, class: 'btn btn-primary' %>
<%= link_to "Import Equipment Item", equip_import_page_path, class: 'btn btn-default' %>
<%= link_to "Export Equipment Data", categories_path(format: 'zip'), :class => "btn btn-default" %>
<% end %>
<% if params[:show_deleted] %>
Expand Down
3 changes: 2 additions & 1 deletion app/views/equipment_models/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
<%= link_to "View All Equipment Models", equipment_models_path, :class => 'btn btn-default' %>
<% else %>
<%= link_to "New Equipment Model", new_equipment_model_path, :class => "btn btn-primary" %>
<%= link_to "Import Equipment Models", equip_import_page_path, :class => "btn btn-default" %>
<%= link_to "Import Equipment Models", equip_import_page_path, :class => "btn btn-default" %>
<%= link_to "Export Equipment Data", categories_path(format: 'zip'), :class => "btn btn-default" %>
<% end %>
<% if params[:show_deleted] %>
<% if (@category.nil?) %>
Expand Down
1 change: 1 addition & 0 deletions app/views/users/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<%= link_to "New User", new_user_path, :class => "btn btn-primary" %>
<% if can? :manage, User %>
<%= link_to "Import Users", csv_import_page_path, :class => "btn btn-default" %>
<%= link_to "Export Users", users_path(format: 'csv'), :class => "btn btn-default" %>
<% end %>
<% if params[:show_banned] %>
<%= link_to "Hide Banned Users from List", users_path, :class => "btn btn-default" %>
Expand Down
73 changes: 73 additions & 0 deletions lib/csv_export.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module CsvExport
require 'csv'
require 'zip'

PROTECTED_COLS = %w(id encrypted_password reset_password_token
reset_password_sent_at)

# generates a csv from the given model data
# columns is optional; defaults to all columns except protected
def generate_csv(data, columns = [])
columns = data.first.attributes.keys if columns.empty?

PROTECTED_COLS.each { |col| columns.delete(col) }

CSV.generate(headers: true) do |csv|
csv << columns

data.each do |o|
csv << columns.map do |attr|
s = o.send(attr)
s.is_a?(ActiveRecord::Base) ? s.name : s
end
end
end
end

# generates a zip file containing multiple CSVs
# expects tables to be an array of arrays with the following format:
# [[objects, columns], ...]
# where columns is optional; defaults to all columns except protected
def generate_zip(tables)
# create the CSVs
csvs = tables.map { |model| generate_csv(*model) }

Zip::OutputStream.write_buffer do |stream|
csvs.each_with_index do |csv, i|
model_name = tables[i].first.first.class.name
stream.put_next_entry "#{model_name}_#{Time.zone.now.to_s(:number)}.csv"
stream.write csv
end
end.string
end

# downloads a csv of the given model table
# NOTE: this method depends on ActionController
def download_csv(data, columns, filename)
send_data(generate_csv(data, columns),
filename: "#{filename}_#{Time.zone.now.to_s(:number)}.csv")
end

# downloads a zip file containing multiple CSVs
# expects tables to be an array of arrays with the following format:
# [[objects, columns], ...]
# where columns is optional; defaults to all columns except protected
# NOTE: this method depends on ActionController
def download_zip(tables, filename)
send_data(generate_zip(tables), type: 'application/zip',
filename: "#{filename}.zip")
end

# NOTE: this method depends on ActionController
def download_equipment_data
categories = [Category.all, %w(name max_per_user max_checkout_length
max_renewal_times max_renewal_length
renewal_days_before_due sort_order)]
models = [EquipmentModel.all, %w(category name description late_fee
replacement_fee max_per_user
max_renewal_length)]
items = [EquipmentItem.all, %w(equipment_model name serial)]
download_zip([categories, models, items],
"EquipmentData_#{Time.zone.now.to_s(:number)}")
end
end
40 changes: 40 additions & 0 deletions spec/lib/csv_export_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'spec_helper'
include CsvExport

describe CsvExport do
before(:all) { FactoryGirl.create(:app_config) }

MODELS = [:user, :category, :equipment_model, :equipment_item]
PROTECTED_COLS = %w(id encrypted_password reset_password_token
reset_password_sent_at)

shared_examples 'builds a csv' do |model|
let(:csv) do
generate_csv(FactoryGirl.build_list(model, 5)).split("\n")
end

it 'has the appropriate length' do
expect(csv.size).to eq(6)
end

it 'has the appropriate columns' do
expect(csv.first.split(',')).to eq(
FactoryGirl.build(model).attributes.keys - PROTECTED_COLS)
end

it "doesn't include protected columns" do
PROTECTED_COLS.each do |col|
expect(csv.first.split(',')).not_to include(col)
end
end

it 'limits columns appropriately' do
cols = FactoryGirl.build(model).attributes.keys.sample(4)
cols.delete 'id' if cols.include? 'id'
csv = generate_csv(FactoryGirl.build_list(model, 5), cols).split("\n")
expect(csv.first.split(',')).to eq(cols)
end
end

MODELS.each { |m| it_behaves_like 'builds a csv', m }
end

0 comments on commit 2978340

Please sign in to comment.