diff --git a/app/assets/javascripts/discourse/components/csv-uploader.js.es6 b/app/assets/javascripts/discourse/components/csv-uploader.js.es6 new file mode 100644 index 00000000000..3e6505b1909 --- /dev/null +++ b/app/assets/javascripts/discourse/components/csv-uploader.js.es6 @@ -0,0 +1,17 @@ +import computed from "ember-addons/ember-computed-decorators"; +import UploadMixin from "discourse/mixins/upload"; + +export default Em.Component.extend(UploadMixin, { + type: "csv", + tagName: "span", + uploadUrl: "/invites/upload_csv", + + @computed("uploading") + uploadButtonText(uploading) { + return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text"); + }, + + uploadDone() { + bootbox.alert(I18n.t("user.invited.bulk_invite.success")); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 15d71343589..78c786847ec 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -18,8 +18,6 @@ export default Ember.Controller.extend({ this.set('searchTerm', ''); }, - uploadText: function() { return I18n.t("user.invited.bulk_invite.text"); }.property(), - /** Observe the search term box with a debouncer and change the results. diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index c9465fa7d14..9be0fc48234 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -33,14 +33,6 @@ export default Discourse.Route.extend({ showInvite() { showModal("invite", { model: this.currentUser }); this.controllerFor("invite").reset(); - }, - - uploadSuccess(filename) { - bootbox.alert(I18n.t("user.invited.bulk_invite.success", { filename: filename })); - }, - - uploadError(filename, message) { - bootbox.alert(I18n.t("user.invited.bulk_invite.error", { filename: filename, message: message })); } } }); diff --git a/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs new file mode 100644 index 00000000000..eab8d71fc96 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/csv-uploader.hbs @@ -0,0 +1,7 @@ + +{{#if uploading}} + {{i18n 'upload_selector.uploading'}} {{uploadProgress}}% +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index 6588a53e01f..fc8657b0d46 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -16,7 +16,7 @@
{{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}} {{#if canBulkInvite}} - {{resumable-upload target="/invites/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}} + {{csv-uploader uploading=uploading}} {{/if}} {{#if showReinviteAllButton}} {{#if reinvitedAll}} diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 01936f68719..ae62b9b8732 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,7 +6,7 @@ class InvitesController < ApplicationController skip_before_filter :check_xhr, :preload_json skip_before_filter :redirect_to_login_if_required - before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :check_csv_chunk, :upload_csv_chunk] + before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :upload_csv] before_filter :ensure_new_registrations_allowed, only: [:show, :redeem_disposable_invite] before_filter :ensure_not_logged_in, only: [:show, :redeem_disposable_invite] @@ -147,48 +147,29 @@ class InvitesController < ApplicationController render nothing: true end - def check_csv_chunk + def upload_csv guardian.ensure_can_bulk_invite_to_forum!(current_user) - filename = params.fetch(:resumableFilename) - identifier = params.fetch(:resumableIdentifier) - chunk_number = params.fetch(:resumableChunkNumber) - current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i + file = params[:file] || params[:files].first + name = params[:name] || File.basename(file.original_filename, ".*") + extension = File.extname(file.original_filename) - # path to chunk file - chunk = Invite.chunk_path(identifier, filename, chunk_number) - # check chunk upload status - status = HandleChunkUpload.check_chunk(chunk, current_chunk_size: current_chunk_size) - - render nothing: true, status: status - end - - def upload_csv_chunk - guardian.ensure_can_bulk_invite_to_forum!(current_user) - - filename = params.fetch(:resumableFilename) - return render status: 415, text: I18n.t("bulk_invite.file_should_be_csv") unless (filename.to_s.end_with?(".csv") || filename.to_s.end_with?(".txt")) - - file = params.fetch(:file) - identifier = params.fetch(:resumableIdentifier) - chunk_number = params.fetch(:resumableChunkNumber).to_i - chunk_size = params.fetch(:resumableChunkSize).to_i - total_size = params.fetch(:resumableTotalSize).to_i - current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i - - # path to chunk file - chunk = Invite.chunk_path(identifier, filename, chunk_number) - # upload chunk - HandleChunkUpload.upload_chunk(chunk, file: file) - - uploaded_file_size = chunk_number * chunk_size - # when all chunks are uploaded - if uploaded_file_size + current_chunk_size >= total_size - # handle bulk_invite processing in a background thread - Jobs.enqueue(:bulk_invite, filename: filename, identifier: identifier, chunks: chunk_number, current_user_id: current_user.id) + Scheduler::Defer.later("Upload CSV") do + begin + data = if extension == ".csv" + path = Invite.create_csv(file, name) + Jobs.enqueue(:bulk_invite, filename: "#{name}.csv", current_user_id: current_user.id) + {url: path} + else + failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")]) + end + rescue + failed_json.merge(errors: [I18n.t("bulk_invite.error")]) + end + MessageBus.publish("/uploads/csv", data.as_json, user_ids: [current_user.id]) end - render nothing: true + render json: success_json end def fetch_username diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb index 44b14abe642..6fb1eeb8637 100644 --- a/app/jobs/regular/bulk_invite.rb +++ b/app/jobs/regular/bulk_invite.rb @@ -14,21 +14,12 @@ module Jobs end def execute(args) - filename = args[:filename] - identifier = args[:identifier] - chunks = args[:chunks].to_i + filename = args[:filename] @current_user = User.find_by(id: args[:current_user_id]) - - raise Discourse::InvalidParameters.new(:filename) if filename.blank? - raise Discourse::InvalidParameters.new(:identifier) if identifier.blank? - raise Discourse::InvalidParameters.new(:chunks) if chunks <= 0 - - # merge chunks, and get csv path - csv_path = get_csv_path(filename, identifier, chunks) + raise Discourse::InvalidParameters.new(:filename) if filename.blank? # read csv file, and send out invitations - read_csv_file(csv_path) - + read_csv_file("#{Invite.base_directory}/#{filename}") ensure # send notification to user regarding progress notify_user @@ -37,17 +28,6 @@ module Jobs FileUtils.rm_rf(csv_path) rescue nil end - def get_csv_path(filename, identifier, chunks) - csv_path = "#{Invite.base_directory}/#{filename}" - tmp_csv_path = "#{csv_path}.tmp" - # path to tmp directory - tmp_directory = File.dirname(Invite.chunk_path(identifier, filename, 0)) - # merge all chunks - HandleChunkUpload.merge_chunks(chunks, upload_path: csv_path, tmp_upload_path: tmp_csv_path, model: Invite, identifier: identifier, filename: filename, tmp_directory: tmp_directory) - - return csv_path - end - def read_csv_file(csv_path) CSV.foreach(csv_path, encoding: "iso-8859-1:UTF-8") do |csv_info| if csv_info[0] diff --git a/app/models/invite.rb b/app/models/invite.rb index 989631d2bd3..8ecef5eda06 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -265,8 +265,12 @@ class Invite < ActiveRecord::Base File.join(Rails.root, "public", "uploads", "csv", RailsMultisite::ConnectionManagement.current_db) end - def self.chunk_path(identifier, filename, chunk_number) - File.join(Invite.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") + def self.create_csv(file, name) + extension = File.extname(file.original_filename) + path = "#{Invite.base_directory}/#{name}#{extension}" + FileUtils.mkdir_p(Pathname.new(path).dirname) + File.open(path, "wb") { |f| f << file.tempfile.read } + path end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e65c6f928cc..98074099f47 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -778,11 +778,9 @@ en: link_generated: "Invite link generated successfully!" valid_for: "Invite link is only valid for this email address: %{email}" bulk_invite: - none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a bulk invite file." + none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a CSV file." text: "Bulk Invite from File" - uploading: "Uploading..." success: "File uploaded successfully, you will be notified via message when the process is complete." - error: "There was an error uploading '{{filename}}': {{message}}" password: title: "Password" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 901c636852e..a9048c3a738 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -134,7 +134,8 @@ en: <<: *errors bulk_invite: - file_should_be_csv: "The uploaded file should be of csv or txt format." + file_should_be_csv: "The uploaded file should be of csv format." + error: "There was an error uploading that file. Please try again later." backup: operation_already_running: "An operation is currently running. Can't start a new job right now." diff --git a/config/routes.rb b/config/routes.rb index 770d8199455..5bffa05f745 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -617,12 +617,8 @@ Discourse::Application.routes.draw do resources :queued_posts, constraints: StaffConstraint.new get 'queued-posts' => 'queued_posts#index' - resources :invites do - collection do - get "upload" => "invites#check_csv_chunk" - post "upload" => "invites#upload_csv_chunk" - end - end + resources :invites + post "invites/upload_csv" => "invites#upload_csv" post "invites/reinvite" => "invites#resend_invite" post "invites/reinvite-all" => "invites#resend_all_invites" post "invites/link" => "invites#create_invite_link" diff --git a/config/site_settings.yml b/config/site_settings.yml index 76b29596660..5899f29c66c 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -686,7 +686,7 @@ files: default: 3072 authorized_extensions: client: true - default: 'jpg|jpeg|png|gif' + default: 'jpg|jpeg|png|gif|csv' refresh: true type: list crawl_images: diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index 745c781f9cb..be07bb78943 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -367,33 +367,10 @@ describe InvitesController do end - context '.check_csv_chunk' do + context '.upload_csv' do it 'requires you to be logged in' do expect { - post :check_csv_chunk - }.to raise_error(Discourse::NotLoggedIn) - end - - context 'while logged in' do - let(:resumableChunkNumber) { 1 } - let(:resumableCurrentChunkSize) { 46 } - let(:resumableIdentifier) { '46-discoursecsv' } - let(:resumableFilename) { 'discourse.csv' } - - it "fails if you can't bulk invite to the forum" do - log_in - post :check_csv_chunk, resumableChunkNumber: resumableChunkNumber, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename - expect(response).not_to be_success - end - - end - - end - - context '.upload_csv_chunk' do - it 'requires you to be logged in' do - expect { - post :upload_csv_chunk + xhr :post, :upload_csv }.to raise_error(Discourse::NotLoggedIn) end @@ -402,27 +379,19 @@ describe InvitesController do let(:file) do ActionDispatch::Http::UploadedFile.new({ filename: 'discourse.csv', tempfile: csv_file }) end - let(:resumableChunkNumber) { 1 } - let(:resumableChunkSize) { 1048576 } - let(:resumableCurrentChunkSize) { 46 } - let(:resumableTotalSize) { 46 } - let(:resumableType) { 'text/csv' } - let(:resumableIdentifier) { '46-discoursecsv' } - let(:resumableFilename) { 'discourse.csv' } - let(:resumableRelativePath) { 'discourse.csv' } + let(:filename) { 'discourse.csv' } it "fails if you can't bulk invite to the forum" do log_in - post :upload_csv_chunk, file: file, resumableChunkNumber: resumableChunkNumber.to_i, resumableChunkSize: resumableChunkSize.to_i, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableTotalSize: resumableTotalSize.to_i, resumableType: resumableType, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename + xhr :post, :upload_csv, file: file, name: filename expect(response).not_to be_success end - it "allows admins to bulk invite" do + it "allows admin to bulk invite" do log_in(:admin) - post :upload_csv_chunk, file: file, resumableChunkNumber: resumableChunkNumber.to_i, resumableChunkSize: resumableChunkSize.to_i, resumableCurrentChunkSize: resumableCurrentChunkSize.to_i, resumableTotalSize: resumableTotalSize.to_i, resumableType: resumableType, resumableIdentifier: resumableIdentifier, resumableFilename: resumableFilename + xhr :post, :upload_csv, file: file, name: filename expect(response).to be_success end - end end diff --git a/spec/jobs/bulk_invite_spec.rb b/spec/jobs/bulk_invite_spec.rb index 77629a6a82f..0739a415743 100644 --- a/spec/jobs/bulk_invite_spec.rb +++ b/spec/jobs/bulk_invite_spec.rb @@ -5,15 +5,8 @@ describe Jobs::BulkInvite do context '.execute' do it 'raises an error when the filename is missing' do - expect { Jobs::BulkInvite.new.execute(identifier: '46-discoursecsv', chunks: '1') }.to raise_error(Discourse::InvalidParameters) - end - - it 'raises an error when the identifier is missing' do - expect { Jobs::BulkInvite.new.execute(filename: 'discourse.csv', chunks: '1') }.to raise_error(Discourse::InvalidParameters) - end - - it 'raises an error when the chunks is missing' do - expect { Jobs::BulkInvite.new.execute(filename: 'discourse.csv', identifier: '46-discoursecsv') }.to raise_error(Discourse::InvalidParameters) + user = Fabricate(:user) + expect { Jobs::BulkInvite.new.execute(current_user_id: user.id) }.to raise_error(Discourse::InvalidParameters) end context '.read_csv_file' do