diff --git a/app/assets/javascripts/admin/models/export_csv.js b/app/assets/javascripts/admin/models/export_csv.js new file mode 100644 index 000000000..3afe84a1c --- /dev/null +++ b/app/assets/javascripts/admin/models/export_csv.js @@ -0,0 +1,26 @@ +/** + Data model for representing an export + + @class ExportCsv + @extends Discourse.Model + @namespace Discourse + @module Discourse +**/ +Discourse.ExportCsv = Discourse.Model.extend({}); + +Discourse.ExportCsv.reopenClass({ + /** + Exports user list + + @method export_user_list + **/ + exportUserList: function() { + return Discourse.ajax("/admin/export_csv/users.json").then(function(result) { + if (result.success) { + bootbox.alert(I18n.t("admin.export_csv.success")); + } else { + bootbox.alert(I18n.t("admin.export_csv.failed")); + } + }); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin_users_list_routes.js b/app/assets/javascripts/admin/routes/admin_users_list_routes.js index 86b180d39..bc1516c18 100644 --- a/app/assets/javascripts/admin/routes/admin_users_list_routes.js +++ b/app/assets/javascripts/admin/routes/admin_users_list_routes.js @@ -9,6 +9,12 @@ Discourse.AdminUsersListRoute = Discourse.Route.extend({ renderTemplate: function() { this.render('admin/templates/users_list', {into: 'admin/templates/admin'}); + }, + + actions: { + exportUsers: function() { + Discourse.ExportCsv.exportUserList(); + } } }); @@ -57,7 +63,7 @@ Discourse.AdminUsersListNewRoute = Discourse.Route.extend({ /** Handles the route that lists pending users. - @class AdminUsersListNewRoute + @class AdminUsersListPendingRoute @extends Discourse.Route @namespace Discourse @module Discourse diff --git a/app/assets/javascripts/admin/templates/users_list.js.handlebars b/app/assets/javascripts/admin/templates/users_list.js.handlebars index 61cacc69f..131036732 100644 --- a/app/assets/javascripts/admin/templates/users_list.js.handlebars +++ b/app/assets/javascripts/admin/templates/users_list.js.handlebars @@ -15,6 +15,9 @@
{{text-field value=username placeholderKey="search_hint"}}
+
+ +
diff --git a/app/controllers/admin/export_csv_controller.rb b/app/controllers/admin/export_csv_controller.rb new file mode 100644 index 000000000..f0e574105 --- /dev/null +++ b/app/controllers/admin/export_csv_controller.rb @@ -0,0 +1,20 @@ +class Admin::ExportCsvController < Admin::AdminController + + skip_before_filter :check_xhr, only: [:download] + + def export_user_list + # export csv file in a background thread + Jobs.enqueue(:export_csv_file, entity: 'user', user_id: current_user.id) + render json: success_json + end + + def download + filename = params.fetch(:id) + if export_csv_path = ExportCsv.get_download_path(filename) + send_file export_csv_path + else + render nothing: true, status: 404 + end + end + +end diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb new file mode 100644 index 000000000..6e4a0b797 --- /dev/null +++ b/app/jobs/regular/export_csv_file.rb @@ -0,0 +1,75 @@ +require 'csv' +require_dependency 'system_message' + +module Jobs + + class ExportCsvFile < Jobs::Base + sidekiq_options retry: false + attr_accessor :current_user + + def initialize + @file_name = "" + end + + def execute(args) + entity = args[:entity] + @current_user = User.find_by(id: args[:user_id]) + + raise Discourse::InvalidParameters.new(:entity) if entity.blank? + + case entity + when 'user' + query = ::AdminUserIndexQuery.new + user_data = query.find_users_query.to_a + + data = Hash.new do |hash, key| + hash[key] = {} + end + + user_data.each do |user| + id = user['id'] + email = user['email'] + data[id] = email + end + data = data.to_a + end + + if data && data.length > 0 + set_file_path + write_csv_file(data) + end + + notify_user + end + + private + + def set_file_path + @file_name = "export_#{SecureRandom.hex(4)}.csv" + # ensure directory exists + dir = File.dirname("#{ExportCsv.base_directory}/#{@file_name}") + FileUtils.mkdir_p(dir) unless Dir.exists?(dir) + end + + def write_csv_file(data) + # write to CSV file + CSV.open(File.expand_path("#{ExportCsv.base_directory}/#{@file_name}", __FILE__), "w") do |csv| + data.each do |value| + csv << [value[1]] + end + end + end + + def notify_user + if @current_user + if @file_name != "" && File.exists?("#{ExportCsv.base_directory}/#{@file_name}") + SystemMessage.create_from_system_user(@current_user, :csv_export_succeeded, download_link: "#{Discourse.base_url}/admin/export_csv/#{@file_name}/download", file_name: @file_name) + else + SystemMessage.create_from_system_user(@current_user, :csv_export_failed) + end + end + end + + end + +end diff --git a/app/models/export_csv.rb b/app/models/export_csv.rb new file mode 100644 index 000000000..214d650de --- /dev/null +++ b/app/models/export_csv.rb @@ -0,0 +1,16 @@ +class ExportCsv + + def self.get_download_path(filename) + path = File.join(ExportCsv.base_directory, filename) + if File.exists?(path) + return path + else + nil + end + end + + def self.base_directory + File.join(Rails.root, "public", "uploads", "csv_exports", RailsMultisite::ConnectionManagement.current_db) + end + +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8f30a7276..d25b0bbd4 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1603,6 +1603,13 @@ en: title: "Rollback the database to previous working state" confirm: "Are your sure you want to rollback the database to the previous working state?" + export_csv: + users: + text: "Export Users" + title: "Export user list in a CSV file." + success: "Export has been initiated, you will be notified shortly with progress." + failed: "Export failed. Please check the logs." + customize: title: "Customize" long_title: "Site Customizations" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4acc4355f..512a8a276 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1387,6 +1387,17 @@ en: %{logs} ``` + csv_export_succeeded: + subject_template: "Data Export completed successfully" + text_body_template: | + The data export was successful. + + Download CSV file: %{file_name} + + csv_export_failed: + subject_template: "Export failed" + text_body_template: "The export has failed. Please check the logs." + email_reject_trust_level: subject_template: "Email issue -- Insufficient Trust Level" text_body_template: | diff --git a/config/routes.rb b/config/routes.rb index 89782fe4c..add9d1604 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -145,6 +145,15 @@ Discourse::Application.routes.draw do end end + resources :export_csv, constraints: AdminConstraint.new do + member do + get "download" => "export_csv#download", constraints: { id: /[^\/]+/ } + end + collection do + get "users" => "export_csv#export_user_list" + end + end + resources :badges, constraints: AdminConstraint.new do collection do get "types" => "badges#badge_types" diff --git a/spec/controllers/admin/export_csv_controller_spec.rb b/spec/controllers/admin/export_csv_controller_spec.rb new file mode 100644 index 000000000..90863df2b --- /dev/null +++ b/spec/controllers/admin/export_csv_controller_spec.rb @@ -0,0 +1,35 @@ +require "spec_helper" + +describe Admin::ExportCsvController do + + it "is a subclass of AdminController" do + (Admin::ExportCsvController < Admin::AdminController).should be_true + end + + let(:export_filename) { "export_b6a2bc87.csv" } + + context "while logged in as an admin" do + + before { @admin = log_in(:admin) } + + describe ".download" do + + it "uses send_file to transmit the export file" do + controller.stubs(:render) + export = ExportCsv.new() + ExportCsv.expects(:get_download_path).with(export_filename).returns(export) + subject.expects(:send_file).with(export) + get :download, id: export_filename + end + + it "returns 404 when the export file does not exist" do + ExportCsv.expects(:get_download_path).returns(nil) + get :download, id: export_filename + response.should be_not_found + end + + end + + end + +end diff --git a/spec/jobs/export_csv_file_spec.rb b/spec/jobs/export_csv_file_spec.rb new file mode 100644 index 000000000..269fb12b2 --- /dev/null +++ b/spec/jobs/export_csv_file_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Jobs::ExportCsvFile do + + context '.execute' do + + it 'raises an error when the entity is missing' do + lambda { Jobs::ExportCsvFile.new.execute(user_id: "1") }.should raise_error(Discourse::InvalidParameters) + end + + end +end +