diff --git a/app/mailers/export_mailer.rb b/app/mailers/export_mailer.rb
index 7f49c462..2df3d36f 100644
--- a/app/mailers/export_mailer.rb
+++ b/app/mailers/export_mailer.rb
@@ -42,4 +42,14 @@ class ExportMailer < BaseMailer
mail(to: @export.user.email, subject: 'Your supporters export has failed')
end
+
+ def export_supporter_notes_completed_notification(export)
+ @export = export
+ mail(to: @export.user.email, subject: 'Your supporter notes export is available!')
+ end
+
+ def export_supporter_notes_failed_notification(export)
+ @export = export
+ mail(to: @export.user.email, subject: 'Your supporter notes export has failed.')
+ end
end
diff --git a/app/views/export_mailer/export_supporter_notes_completed_notification.html.erb b/app/views/export_mailer/export_supporter_notes_completed_notification.html.erb
new file mode 100644
index 00000000..846ba002
--- /dev/null
+++ b/app/views/export_mailer/export_supporter_notes_completed_notification.html.erb
@@ -0,0 +1,9 @@
+
Your export from <%= Format::Date.simple @export.created_at %> was completed successfully.
+
+To view your exported CSV, visit: your export.
+
+Note: your generated CSV file will be automatically deleted from our servers after seven days. Don't worry; you can always visit your recurring donations panel and export a new CSV file.
+
+If you have any questions about this export, please contact <%= Settings.mailer.email %>.
+
+<%= render 'emails/sig' %>
\ No newline at end of file
diff --git a/app/views/export_mailer/export_supporter_notes_failed_notification.html.erb b/app/views/export_mailer/export_supporter_notes_failed_notification.html.erb
new file mode 100644
index 00000000..208412da
--- /dev/null
+++ b/app/views/export_mailer/export_supporter_notes_failed_notification.html.erb
@@ -0,0 +1,12 @@
+Your export from <%= Format::Date.simple @export.created_at %> did not succeed due to a technical error.
+While CommitChange engineers have been notified, don't hesitate to contact
+ <%= Settings.mailer.email %> with the following information:
+
+
+ - Export ID: <%= @export.id %>
+ - User ID: <%= @export.user_id %>
+ - Nonprofit ID: <%= @export.nonprofit_id %>
+ - Exception: <%= @export.exception %>
+
+
+<%= render 'emails/sig' %>
\ No newline at end of file
diff --git a/lib/export/export_supporter_notes.rb b/lib/export/export_supporter_notes.rb
new file mode 100644
index 00000000..83b9316e
--- /dev/null
+++ b/lib/export/export_supporter_notes.rb
@@ -0,0 +1,74 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+module ExportSupporterNotes
+ def self.initiate_export(npo_id, params, user_id)
+
+ ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id },
+ npo_id: { required: true, is_integer: true },
+ params: { required: true, is_hash: true },
+ user_id: { required: true, is_integer: true })
+ npo = Nonprofit.where('id = ?', npo_id).first
+ unless npo
+ raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id)
+ end
+ user = User.where('id = ?', user_id).first
+ unless user
+ raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id)
+ end
+
+ e = Export.create(nonprofit: npo, user: user, status: :queued, export_type: 'ExportSupporterNotes', parameters: params.to_json)
+
+ DelayedJobHelper.enqueue_job(ExportSupporterNotes, :run_export, [npo_id, params.to_json, user_id, e.id])
+ end
+
+ def self.run_export(npo_id, params, user_id, export_id)
+ # need to check that
+ ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id, export_id: export_id },
+ npo_id: { required: true, is_integer: true },
+ params: { required: true, is_json: true },
+ user_id: { required: true, is_integer: true },
+ export_id: { required: true, is_integer: true })
+
+ params = JSON.parse(params, :object_class=> HashWithIndifferentAccess)
+ # verify that it's also a hash since we can't do that at once
+ ParamValidation.new({ params: params },
+ params: { is_hash: true })
+ begin
+ export = Export.find(export_id)
+ rescue ActiveRecord::RecordNotFound
+ raise ParamValidation::ValidationError.new("Export #{export_id} doesn't exist!", key: :export_id)
+ end
+ export.status = :started
+ export.save!
+
+ unless Nonprofit.exists?(npo_id)
+ raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id)
+ end
+ user = User.where('id = ?', user_id).first
+ unless user
+ raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id)
+ end
+
+ file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S')
+ filename = "tmp/csv-exports/supporters-notes-#{file_date}.csv"
+
+ url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.supporter_note_export_enumerable(npo_id, params, 30000).map{|i| i.to_csv}, content_type: 'text/csv', content_disposition: 'attachment')
+ export.url = url
+ export.status = :completed
+ export.ended = Time.now
+ export.save!
+
+ EmailJobQueue.queue(JobTypes::ExportSupporterNotesCompletedJob, export)
+ rescue => e
+ if export
+ export.status = :failed
+ export.exception = e.to_s
+ export.ended = Time.now
+ export.save!
+ if user
+ EmailJobQueue.queue(JobTypes::ExportSupporterNotesFailedJob, export)
+ end
+ raise e
+ end
+ raise e
+ end
+end
\ No newline at end of file
diff --git a/lib/job_types/export_supporter_notes_completed_job.rb b/lib/job_types/export_supporter_notes_completed_job.rb
new file mode 100644
index 00000000..31ccd056
--- /dev/null
+++ b/lib/job_types/export_supporter_notes_completed_job.rb
@@ -0,0 +1,14 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+module JobTypes
+ class ExportSupporterNotesCompletedJob < EmailJob
+ attr_reader :export
+
+ def initialize(export)
+ @export = export
+ end
+
+ def perform
+ ExportMailer.export_supporter_notes_completed_notification(@export).deliver
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/job_types/export_supporter_notes_failed_job.rb b/lib/job_types/export_supporter_notes_failed_job.rb
new file mode 100644
index 00000000..40652dc1
--- /dev/null
+++ b/lib/job_types/export_supporter_notes_failed_job.rb
@@ -0,0 +1,14 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+module JobTypes
+ class ExportSupporterNotesFailedJob < EmailJob
+ attr_reader :export
+
+ def initialize(export)
+ @export = export
+ end
+
+ def perform
+ ExportMailer.export_supporter_notes_failed_notification(@export).deliver
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/query/query_payments.rb b/lib/query/query_payments.rb
index bef9a8bd..f535e791 100644
--- a/lib/query/query_payments.rb
+++ b/lib/query/query_payments.rb
@@ -363,7 +363,7 @@ module QueryPayments
'donations.anonymous',
'donations.comment',
"coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign",
- "campaigns_for_export.id AS campaign_id",
+ "campaigns_for_export.id AS \"Campaign Id\"",
"coalesce(nullif(campaigns_for_export.creator_email, ''), '') AS campaign_creator_email",
"coalesce(nullif(campaign_gift_options.name, ''), 'None') AS campaign_gift_level",
'events_for_export.name AS event_name',
diff --git a/spec/lib/export/export_payments_spec.rb b/spec/lib/export/export_payments_spec.rb
index f58fd814..13dca82d 100644
--- a/spec/lib/export/export_payments_spec.rb
+++ b/spec/lib/export/export_payments_spec.rb
@@ -57,11 +57,15 @@ describe ExportPayments do
stub_const("DelayedJobHelper", double('delayed'))
params = { param1: 'pp' }.with_indifferent_access
- expect(DelayedJobHelper).to receive(:enqueue_job).with(ExportPayments, :run_export, [nonprofit.id, params.to_json, user.id, 1])
+ expect(Export).to receive(:create).and_wrap_original {|m, *args|
+ e = m.call(*args) # get original create
+ expect(DelayedJobHelper).to receive(:enqueue_job).with(ExportPayments, :run_export, [nonprofit.id, params.to_json, user.id, e.id]) #add the enqueue
+ e
+ }
ExportPayments.initiate_export(nonprofit.id, params, user.id)
export = Export.first
- expected_export = { id: 1,
+ expected_export = { id: export.id,
user_id: user.id,
nonprofit_id: nonprofit.id,
status: 'queued',
diff --git a/spec/lib/export/export_supporter_notes_spec.rb b/spec/lib/export/export_supporter_notes_spec.rb
new file mode 100644
index 00000000..91d6fc7a
--- /dev/null
+++ b/spec/lib/export/export_supporter_notes_spec.rb
@@ -0,0 +1,232 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+require 'rails_helper'
+require 'support/test_chunked_uploader'
+
+describe ExportSupporterNotes do
+ before(:each) do
+ stub_const('CHUNKED_UPLOADER',TestChunkedUploader)
+ supporter_note_for_s1
+ supporter_note_1_for_s2
+ supporter_note_2_for_s2
+ CHUNKED_UPLOADER.clear
+
+ end
+
+ let(:nonprofit) { force_create(:nonprofit)}
+ let(:supporter1) { force_create(:supporter, nonprofit: nonprofit)}
+ let(:supporter2) { force_create(:supporter, nonprofit: nonprofit)}
+
+ let(:user) { force_create(:user, email: email) }
+ let(:email) {'example@example.com'}
+
+ let(:export_header) { ['Id', 'Email', 'Note Created At', 'Note Contents']}
+
+ let(:note_content_1) do
+ "CONTENT1"
+ end
+
+ let(:note_content_2) do
+ "CONTENT2"
+ end
+
+ let(:note_content_3) do
+ "CONTENT3"
+ end
+
+ let(:supporter_note_for_s1) do
+ force_create(:supporter_note, supporter: supporter1, created_at: DateTime.new(2018,1,5), content: note_content_1)
+ end
+
+ let(:supporter_note_1_for_s2) do
+ force_create(:supporter_note, supporter: supporter2, created_at: DateTime.new(2018,2,5), content: note_content_2)
+ end
+
+ let(:supporter_note_2_for_s2) do
+ force_create(:supporter_note, supporter: supporter2, created_at: DateTime.new(2020,4, 5), content: note_content_3)
+ end
+
+
+ context '.initiate_export' do
+ context 'param verification' do
+ it 'performs initial verification' do
+ expect { ExportSupporterNotes.initiate_export(nil, nil, nil) }.to(raise_error do |error|
+ expect(error).to be_a(ParamValidation::ValidationError)
+ expect(error.data.length).to eq(6)
+ expect_validation_errors(error.data, [{ key: 'npo_id', name: :required },
+ { key: 'npo_id', name: :is_integer },
+ { key: 'user_id', name: :required },
+ { key: 'user_id', name: :is_integer },
+ { key: 'params', name: :required },
+ { key: 'params', name: :is_hash }])
+ end)
+ end
+
+ it 'nonprofit doesnt exist' do
+ fake_npo = 8_888_881
+ expect { ExportSupporterNotes.initiate_export(fake_npo, {}, 8_888_883) }.to(raise_error do |error|
+ expect(error).to be_a(ParamValidation::ValidationError)
+ expect(error.message).to eq "Nonprofit #{fake_npo} doesn't exist!"
+ end)
+ end
+
+ it 'user doesnt exist' do
+ fake_user = 8_888_883
+ expect { ExportSupporterNotes.initiate_export(nonprofit.id, {}, fake_user) }.to(raise_error do |error|
+ expect(error).to be_a(ParamValidation::ValidationError)
+ expect(error.message).to eq "User #{fake_user} doesn't exist!"
+ end)
+ end
+ end
+
+ it 'creates an export object and schedules job' do
+ Timecop.freeze(2020, 4, 5) do
+ DelayedJobHelper = double('delayed')
+ params = { param1: 'pp', root_url: 'https://localhost:8080' }.with_indifferent_access
+
+ expect(Export).to receive(:create).and_wrap_original {|m, *args|
+ e = m.call(*args) # get original create
+ expect(DelayedJobHelper).to receive(:enqueue_job).with(ExportSupporterNotes, :run_export, [nonprofit.id, params.to_json, user.id, e.id]) #add the enqueue
+ e
+ }
+
+
+ ExportSupporterNotes.initiate_export(nonprofit.id, params, user.id)
+ export = Export.first
+ expected_export = { id: export.id,
+ user_id: user.id,
+ nonprofit_id: nonprofit.id,
+ status: 'queued',
+ export_type: 'ExportSupporterNotes',
+ parameters: params.to_json,
+ updated_at: Time.now,
+ created_at: Time.now,
+ url: nil,
+ ended: nil,
+ exception: nil }.with_indifferent_access
+ expect(export.attributes).to eq(expected_export)
+ end
+ end
+ end
+
+ context '.run_export' do
+ context 'param validation' do
+ it 'rejects basic invalid data' do
+ expect { ExportSupporterNotes.run_export(nil, nil, nil, nil) }.to(raise_error do |error|
+ expect(error).to be_a(ParamValidation::ValidationError)
+ expect_validation_errors(error, [{ key: 'npo_id', name: :required },
+ { key: 'npo_id', name: :is_integer },
+ { key: 'user_id', name: :required },
+ { key: 'user_id', name: :is_integer },
+ { key: 'params', name: :required },
+ { key: 'params', name: :is_json },
+ { key: 'export_id', name: :required },
+ { key: 'export_id', name: :is_integer }])
+ end)
+ end
+
+ it 'rejects json which isnt a hash' do
+ expect { ExportSupporterNotes.run_export(1, [{ item: '' }, { item: '' }].to_json, 1, 1) }.to(raise_error do |error|
+ expect(error).to be_a(ParamValidation::ValidationError)
+ expect_validation_errors(error, [
+ { key: :params, name: :is_hash }
+ ])
+ end)
+ end
+
+ it 'no export throw an exception' do
+ expect { ExportSupporterNotes.run_export(0, { x: 1 }.to_json, 0, 11_111) }.to(raise_error do |error|
+ expect(error).to be_a ParamValidation::ValidationError
+ expect(error.data[:key]).to eq :export_id
+ expect(error.message).to start_with('Export')
+ end)
+ end
+
+ it 'no nonprofit' do
+ Timecop.freeze(2020, 4, 5) do
+ @export = force_create(:export, user: user)
+ Timecop.freeze(2020, 4, 6) do
+ expect { ExportSupporterNotes.run_export(0, { x: 1 }.to_json, user.id, @export.id) }.to(raise_error do |error|
+ expect(error).to be_a ParamValidation::ValidationError
+ expect(error.data[:key]).to eq :npo_id
+ expect(error.message).to start_with('Nonprofit')
+
+ @export.reload
+ expect(@export.status).to eq 'failed'
+ expect(@export.exception).to eq error.to_s
+ expect(@export.ended).to eq Time.now
+ expect(@export.updated_at).to eq Time.now
+
+ end)
+ end
+ end
+ end
+
+ it 'no user' do
+ Timecop.freeze(2020, 4, 5) do
+ @export = force_create(:export, user: user)
+ Timecop.freeze(2020, 4, 6) do
+ expect { ExportSupporterNotes.run_export(nonprofit.id, { x: 1 }.to_json, 0, @export.id) }.to(raise_error do |error|
+ expect(error).to be_a ParamValidation::ValidationError
+ expect(error.data[:key]).to eq :user_id
+ expect(error.message).to start_with('User')
+
+ @export.reload
+ expect(@export.status).to eq 'failed'
+ expect(@export.exception).to eq error.to_s
+ expect(@export.ended).to eq Time.now
+ expect(@export.updated_at).to eq Time.now
+ end)
+ end
+ end
+ end
+ end
+
+ it 'handles exception in upload properly' do
+ Timecop.freeze(2020, 4, 5) do
+ @export = force_create(:export, user: user)
+ expect_email_queued.with(JobTypes::ExportSupporterNotesFailedJob, @export)
+ CHUNKED_UPLOADER.raise_error
+ Timecop.freeze(2020, 4, 6) do
+ expect { ExportSupporterNotes.run_export(nonprofit.id, {}.to_json, user.id, @export.id) }.to(raise_error do |error|
+ expect(error).to be_a StandardError
+ expect(error.message).to eq TestChunkedUploader::TEST_ERROR_MESSAGE
+
+ @export.reload
+ expect(@export.status).to eq 'failed'
+ expect(@export.exception).to eq error.to_s
+ expect(@export.ended).to eq Time.now
+ expect(@export.updated_at).to eq Time.now
+
+
+ end)
+ end
+ end
+ end
+
+ it 'uploads as expected' do
+ Timecop.freeze(2020, 4, 5) do
+ @export = create(:export, user: user, created_at: Time.now, updated_at: Time.now)
+ expect_email_queued.with(JobTypes::ExportSupporterNotesCompletedJob, @export)
+ Timecop.freeze(2020, 4, 6, 1, 2, 3) do
+ ExportSupporterNotes.run_export(nonprofit.id, {:root_url => "https://localhost:8080/"}.to_json, user.id, @export.id)
+
+ @export.reload
+
+ expect(@export.url).to eq 'http://fake.url/tmp/csv-exports/supporters-notes-04-06-2020--01-02-03.csv'
+ expect(@export.status).to eq 'completed'
+ expect(@export.exception).to be_nil
+ expect(@export.ended).to eq Time.now
+ expect(@export.updated_at).to eq Time.now
+ csv = CSV.parse(TestChunkedUploader.output)
+ expect(csv.length).to eq (4)
+
+ expect(csv[0]).to eq export_header
+
+ expect(TestChunkedUploader.options[:content_type]).to eq 'text/csv'
+ expect(TestChunkedUploader.options[:content_disposition]).to eq 'attachment'
+
+ end
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/lib/export/export_supporters_spec.rb b/spec/lib/export/export_supporters_spec.rb
index df55ac32..4205216f 100644
--- a/spec/lib/export/export_supporters_spec.rb
+++ b/spec/lib/export/export_supporters_spec.rb
@@ -10,7 +10,7 @@ describe ExportSupporters do
@supporters = 2.times { force_create(:supporter, nonprofit: @nonprofit)}
CHUNKED_UPLOADER.clear
end
- let(:export_header) { "Last Name,First Name,Full Name,Organization,Email,Phone,Address,City,State,Postal Code,Country,Anonymous?,Supporter Id,Total Contributed,Id,Last Payment Received,Notes".split(',')}
+ let(:export_header) { "Last Name,First Name,Full Name,Organization,Email,Phone,Address,City,State,Postal Code,Country,Anonymous?,Supporter Id,Total Contributed,Id,Last Payment Received,Notes,Tags".split(',')}
context '.initiate_export' do
context 'param verification' do
diff --git a/spec/lib/job_types/export_supporter_notes_completed_spec.rb b/spec/lib/job_types/export_supporter_notes_completed_spec.rb
new file mode 100644
index 00000000..b0721067
--- /dev/null
+++ b/spec/lib/job_types/export_supporter_notes_completed_spec.rb
@@ -0,0 +1,14 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+require 'rails_helper.rb'
+
+describe JobTypes::ExportSupporterNotesCompletedJob do
+ describe '.perform' do
+ it 'calls the correct active mailer' do
+ input = 1
+ expect(ExportMailer).to receive(:export_supporter_notes_completed_notification).with(input).and_wrap_original{|m, *args| mailer = double('object'); expect(mailer).to receive(:deliver).and_return(nil); mailer}
+
+ job = JobTypes::ExportSupporterNotesCompletedJob.new(1)
+ job.perform
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/lib/job_types/export_supporter_notes_failed_spec.rb b/spec/lib/job_types/export_supporter_notes_failed_spec.rb
new file mode 100644
index 00000000..d9460e06
--- /dev/null
+++ b/spec/lib/job_types/export_supporter_notes_failed_spec.rb
@@ -0,0 +1,14 @@
+# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later
+require 'rails_helper.rb'
+
+describe JobTypes::ExportSupporterNotesFailedJob do
+ describe '.perform' do
+ it 'calls the correct active mailer' do
+ input = 1
+ expect(ExportMailer).to receive(:export_supporter_notes_failed_notification).with(input).and_wrap_original{|m, *args| mailer = double('object'); expect(mailer).to receive(:deliver).and_return(nil); mailer}
+
+ job = JobTypes::ExportSupporterNotesFailedJob.new(1)
+ job.perform
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/support/mock_helpers.rb b/spec/support/mock_helpers.rb
index 7ad6d901..b41fd5f3 100644
--- a/spec/support/mock_helpers.rb
+++ b/spec/support/mock_helpers.rb
@@ -7,7 +7,7 @@ module MockHelpers
"Dedicated To: Email",
"Dedicated To: Phone",
"Dedicated To: Address",
- "Dedicated To: Note", 'Anonymous','Comment','Campaign', 'Campaign Gift Level', 'Event Name', 'Payment', 'Check Number', 'Donation Note']
+ "Dedicated To: Note", 'Anonymous','Comment','Campaign', 'Campaign Id', 'Campaign Creator Email', 'Campaign Gift Level', 'Event Name', 'Payment', 'Check Number', 'Donation Note']
end
def self.recurring_donation_export_headers()