Skip to content

Commit

Permalink
Merge pull request #12 from cmc333333/182-admin-user-csv-download
Browse files Browse the repository at this point in the history
Admin users CSV download
  • Loading branch information
GUI committed May 19, 2015
2 parents 4773da8 + b0691c9 commit e4e2b68
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Admin.AdminsIndexController = Ember.ObjectController.extend({
queryParams: null,

downloadUrl: function() {
return '/api-umbrella/v1/admins.csv?' + $.param(this.get('queryParams')) + '&api_key=' + webAdminAjaxApiKey;
}.property('queryParams'),

actions: {
paramsChange: function(newParams) {
// Remove paging
delete newParams.start;
delete newParams.length;
this.set('queryParams', newParams);
}
}
});
4 changes: 4 additions & 0 deletions app/assets/javascripts/admin/templates/admins/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
<div id="results_table searchable-table-with-add">
{{view Admin.AdminsTableView dataBinding='model.logs' queryBinding='query'}}
</div>

<div class="results-table-actions">
<a {{bind-attr href='downloadUrl'}}>Download CSV</a>
</div>
11 changes: 10 additions & 1 deletion app/assets/javascripts/admin/views/admins/table_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Admin.AdminsTableView = Ember.View.extend({
classNames: ['table', 'table-striped', 'table-bordered', 'table-condensed'],

didInsertElement: function() {
this.$().DataTable({
var dataTable = this.$().DataTable({
serverSide: true,
ajax: '/api-umbrella/v1/admins.json',
pageLength: 50,
order: [[0, 'asc']],
columns: [
{
data: 'username',
name: 'Username',
title: 'Username',
defaultContent: '-',
render: _.bind(function(email, type, data) {
Expand All @@ -25,36 +26,44 @@ Admin.AdminsTableView = Ember.View.extend({
},
{
data: 'email',
name: 'E-mail',
title: 'E-mail',
defaultContent: '-',
render: Admin.DataTablesHelpers.renderEscaped,
},
{
data: 'name',
name: 'Name',
title: 'Name',
defaultContent: '-',
render: Admin.DataTablesHelpers.renderEscaped,
},
{
data: 'group_names',
name: 'Groups',
title: 'Groups',
render: Admin.DataTablesHelpers.renderListEscaped,
},
{
data: 'last_sign_in_at',
type: 'date',
name: 'Last Signed In',
title: 'Last Signed In',
defaultContent: '-',
render: Admin.DataTablesHelpers.renderTime,
},
{
data: 'created_at',
type: 'date',
name: 'Created',
title: 'Created',
defaultContent: '-',
render: Admin.DataTablesHelpers.renderTime,
}
]
});
dataTable.on('draw.dt', function() {
this.get('controller').send('paramsChange', dataTable.ajax.params());
}.bind(this));
},
});
1 change: 1 addition & 0 deletions app/controllers/api/v1/admins_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def index

@admins_count = @admins.count
@admins = @admins.to_a.select { |admin| Pundit.policy!(pundit_user, admin).show? }
self.respond_to_datatables(@admins, "admins #{Time.now.strftime("%b %-e %Y")}")
end

def show
Expand Down
20 changes: 1 addition & 19 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
class ApplicationController < ActionController::Base
include Pundit
include DatatablesHelper
protect_from_forgery

def datatables_sort
sort = []

if(params[:order].present?)
params[:order].each do |i, order|
column_index = order[:column]
column = params[:columns][column_index]
column_name = column[:data]
sort << { column_name => order[:dir] }
end
end

sort
end

def datatables_sort_array
datatables_sort.map { |sort| sort.to_a.flatten }
end

def pundit_user
current_admin
end
Expand Down
79 changes: 79 additions & 0 deletions app/helpers/datatables_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Methods relating to Datatables input handling
module DatatablesHelper
def datatables_sort
sort = []
columns = self.datatables_columns
param_index_array(:order).each do |order|
column_index = order[:column].to_i
if columns.length > column_index
field = columns[column_index][:field]
sort << { field => order[:dir] }
end
end

sort
end

def datatables_sort_array
datatables_sort.map { |sort| sort.to_a.flatten }
end

# convert from ?param[0]=a&param[1]=b to ?param[]=a&param[]=b
def param_index_array(key)
as_array = []
if params[key].is_a?(Array)
as_array = params[key]
elsif params[key].is_a?(Hash)
upper_bound = params[key].length - 1
(0..upper_bound).each do |idx|
if params[key].key?(idx.to_s)
as_array << params[key][idx.to_s]
end
end
elsif params.key?(key)
as_array = [params[key]]
end
as_array
end

# Parse the column request from a datatables query
def datatables_columns
columns = self.param_index_array(:columns)
columns = columns.select { |col| col[:data] }
columns.map do |col|
{ :name => (col[:name] || '-').to_s,
:field => col[:data].to_s
}
end
end

# Set download headers and join arrays
def csv_output(results, columns)
requested_fields = columns.map { |c| c[:field] }
CSV.generate do |csv|
csv << columns.map { |c| c[:name] }
results.each do |result|
result = requested_fields.map { |field| result[field] }
result = result.map { |cell| cell.is_a?(Array) ? cell.join(",") : cell }
csv << result
end
end
end

# Include only the requested columns
def respond_to_datatables(results, csv_filename)
columns = self.datatables_columns
requested_fields = columns.map { |c| c[:field] }
results = results.map do |result|
hash = result.serializable_hash
hash.select { |k, v| requested_fields.include? k }
end
respond_to do |format|
format.csv do
send_file_headers!(:disposition => "attachment", :filename => csv_filename + ".csv")
self.response_body = self.csv_output(results, columns)
end
format.json
end
end
end
8 changes: 8 additions & 0 deletions app/models/admin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ def disallowed_roles
@disallowed_roles
end

def serializable_hash(options = nil)
options ||= {}
options[:force_except] = options.fetch(:force_except, []) + [:authentication_token]
hash = super(options)
hash["group_names"] = self.group_names
hash
end

private

def generate_authentication_token
Expand Down
11 changes: 1 addition & 10 deletions app/views/api/v1/admins/index.rabl
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,4 @@ object false
node(:draw) { params[:draw].to_i }
node(:recordsTotal) { @admins_count }
node(:recordsFiltered) { @admins_count }
node :data do
@admins.map do |admin|
data = admin.serializable_hash(:force_except => [:authentication_token])
data.merge!({
"group_names" => admin.group_names,
})

data
end
end
node(:data) { @admins }
120 changes: 120 additions & 0 deletions spec/helpers/datatables_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
require 'spec_helper'

describe DatatablesHelper do
describe "#datatables_sort" do
it "uses defaults to no order" do
helper.stub(:params) { {} }
expect(helper.datatables_sort).to eq([])
end

it "orders per request" do
helper.stub(:params) do
{ :columns => { "0" => { :data => 'col0' }, "1" => { :data => 'col1' },
"2" => { :data => 'col2' }, "3" => { :data => 'col3' } },
:order => [{ :column => '2', :dir => 'asc' }, { :column => '0', :dir => 'desc' }]
}
end
expect(helper.datatables_sort).to eq([
{ 'col2' => 'asc' }, { 'col0' => 'desc' }])
end
end

describe "#param_index_array" do
it "doesn't touch a parameter that's already an array" do
helper.stub(:params) { { :key => ["a", "b", "c"] } }
expect(helper.param_index_array(:key)).to eq(["a", "b", "c"])
end

it "converts parameters to arrays" do
helper.stub(:params) { { :key => "value" } }
expect(helper.param_index_array(:key)).to eq(["value"])
end

it "given an empty array when the parameter is not present" do
helper.stub(:params) { {} }
expect(helper.param_index_array(:key)).to eq([])
end

it "converts arrays with object indexes" do
helper.stub(:params) { { :key => { "0" => "a", "1" => "b", "2" => "c" } } }
expect(helper.param_index_array(:key)).to eq(["a", "b", "c"])
end

it "does not explode with missing indexes" do
helper.stub(:params) { { :key => { "0" => "a", "2" => "c" } } }
expect(helper.param_index_array(:key)).to eq(["a"])
end

it "does not explode with mixed indexes" do
helper.stub(:params) { { :key => { "0" => "a", "str" => "b", "2" => "c" } } }
expect(helper.param_index_array(:key)).to eq(["a", "c"])
end
end

describe "#datatables_columns" do
it "pulls out column data under realistic conditions" do
helper.stub(:params) do
{ :columns => {
"0" => { :data => "username", :name => "Username", :searchable => true,
:orderable => true, :search => { :value => '', :regex => false } },
"1" => { :data => "email", :name => "E-mail", :searchable => true,
:orderable => true, :search => { :value => '', :regex => false } },
"2" => { :data => "name", :name => "Name", :searchable => true,
:orderable => true, :search => { :value => '', :regex => false } },
} }
end
expect(helper.datatables_columns).to eq([
{ :name => 'Username', :field => 'username' },
{ :name => 'E-mail', :field => 'email' },
{ :name => 'Name', :field => 'name' }])
end

it "accounts for data errors" do
helper.stub(:params) do
{ :columns => {
"0" => { :name => "Username", :searchable => true, # missing data
:orderable => true, :search => { :value => '', :regex => false } },
"1" => { :data => "email", :searchable => true, # missing name
:orderable => true, :search => { :value => '', :regex => false } },
"2" => { :data => ["a", "b", "c"], :name => "Name", :searchable => true, # data is an array
:orderable => true, :search => { :value => '', :regex => false } },
} }
end
expect(helper.datatables_columns).to eq([
{ :name => '-', :field => 'email' },
{ :name => 'Name', :field => '["a", "b", "c"]' }])
end
end

describe "#csv_output" do
it "generates a csv" do
results = [{ "a" => 1, "b" => 2, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }]
columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }, { :name => "C", :field => "c" }]
output = helper.csv_output(results, columns)
expect(output).to eq("A,B,C\n1,2,3\n4,5,6\n")
end

it "does not include fields not requested" do
results = [{ "a" => 1, "b" => 2, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }]
columns = [{ :name => "A", :field => "a" }, { :name => "C", :field => "c" }]
output = helper.csv_output(results, columns)
expect(output).to eq("A,C\n1,3\n4,6\n")
end

it "skips over missing fields" do
results = [{ "a" => 1, "c" => 3 }, { "a" => 4, "b" => 5, "c" => 6 }]
columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }, { :name => "C", :field => "c" }]
output = helper.csv_output(results, columns)
expect(output).to eq("A,B,C\n1,,3\n4,5,6\n")
end

it "converts lists to a string" do
results = [{ "a" => 1, "b" => [2, 3, 4] }]
columns = [{ :name => "A", :field => "a" }, { :name => "B", :field => "b" }]
output = helper.csv_output(results, columns)
expect(output).to eq("A,B\n1," + '"2,3,4"' + "\n")
end
end

describe "#respond_to_datatables" # @todo
end

0 comments on commit e4e2b68

Please sign in to comment.