From 9fc42881aa8a0f395b33450239f0cf7b00952073 Mon Sep 17 00:00:00 2001
From: Caio <117518+caiosba@users.noreply.github.com>
Date: Thu, 22 Aug 2024 19:13:51 -0300
Subject: [PATCH] [WIP] Ticket CV2-5067: Refactoring code so it is not
media-specific and sending email
---
app/graph/mutations/export_mutations.rb | 9 +-
app/mailers/export_list_mailer.rb | 12 ++
.../export_list_mailer/send_csv.html.erb | 124 ++++++++++++++++++
.../export_list_mailer/send_csv.text.erb | 12 ++
config/initializers/plugins.rb | 2 +-
config/locales/en.yml | 5 +
lib/check_s3.rb | 4 +-
lib/check_search.rb | 13 +-
lib/list_export.rb | 49 +++++++
.../controllers/graphql_controller_11_test.rb | 6 +-
10 files changed, 215 insertions(+), 21 deletions(-)
create mode 100644 app/mailers/export_list_mailer.rb
create mode 100644 app/views/export_list_mailer/send_csv.html.erb
create mode 100644 app/views/export_list_mailer/send_csv.text.erb
create mode 100644 lib/list_export.rb
diff --git a/app/graph/mutations/export_mutations.rb b/app/graph/mutations/export_mutations.rb
index 97bc734d72..29cdcc150f 100644
--- a/app/graph/mutations/export_mutations.rb
+++ b/app/graph/mutations/export_mutations.rb
@@ -1,20 +1,21 @@
module ExportMutations
class ExportList < Mutations::BaseMutation
argument :query, GraphQL::Types::String, required: true
+ argument :type, GraphQL::Types::String, required: true # 'media', 'feed' or 'article'
field :success, GraphQL::Types::Boolean, null: true
- def resolve(query:)
+ def resolve(query:, type:)
ability = context[:ability]
team = Team.find_if_can(Team.current.id, ability)
if ability.cannot?(:export_list, team)
{ success: false }
else
- search = CheckSearch.new(query, nil, team.id)
- if search.number_of_results > CheckConfig.get(:export_csv_maximum_number_of_results, 10000, :integer)
+ export = ListExport.new(type.to_sym, query, team.id)
+ if export.number_of_rows > CheckConfig.get(:export_csv_maximum_number_of_results, 10000, :integer)
{ success: false }
else
- CheckSearch.delay.export_to_csv(query, team.id)
+ export.generate_csv_and_send_email_in_background(User.current)
{ success: true }
end
end
diff --git a/app/mailers/export_list_mailer.rb b/app/mailers/export_list_mailer.rb
new file mode 100644
index 0000000000..882eff3e02
--- /dev/null
+++ b/app/mailers/export_list_mailer.rb
@@ -0,0 +1,12 @@
+class ExportListMailer < ApplicationMailer
+ layout nil
+
+ def send_csv(csv_file_url, user)
+ @csv_file_url = csv_file_url
+ @user = user
+ @expire_in = CheckConfig.get('export_csv_expire', 7.days.to_i, :integer) / (60 * 60 * 24)
+ subject = I18n.t('mails_notifications.export_list.subject')
+ Rails.logger.info "Sending export e-mail to #{@user.email}"
+ mail(to: @user.email, email_type: 'export_list', subject: subject)
+ end
+end
diff --git a/app/views/export_list_mailer/send_csv.html.erb b/app/views/export_list_mailer/send_csv.html.erb
new file mode 100644
index 0000000000..ec3010e3b9
--- /dev/null
+++ b/app/views/export_list_mailer/send_csv.html.erb
@@ -0,0 +1,124 @@
+<%= render "shared/header" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%= I18n.t(:"mails_notifications.export_list.hello", name: @user.name) %>
+
+
+
+
+
+
+ <%= I18n.t(:"mails_notifications.export_list.body", days: @expire_in) %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <%=
+ link_to(I18n.t('mails_notifications.export_list.button_label'),
+ @csv_file_url,
+ :style => "text-decoration: none !important;color: #fff !important;"
+ )
+ %>
+
+ |
+
+ <%= image_tag("https://images.ctfassets.net/g118h5yoccvd/#{@direction[:arrow]}", width: "7", alt: "arrow-icon", style: "-ms-interpolation-mode: bicubic; border: 0 none; height: auto; line-height: 100%; outline: none; text-decoration: none;") %>
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+<%= render "shared/footer" %>
diff --git a/app/views/export_list_mailer/send_csv.text.erb b/app/views/export_list_mailer/send_csv.text.erb
new file mode 100644
index 0000000000..309fb33d66
--- /dev/null
+++ b/app/views/export_list_mailer/send_csv.text.erb
@@ -0,0 +1,12 @@
+<%= I18n.t('mails_notifications.export_list.hello', name: @user.name) %>
+
+<%= I18n.t('mails_notifications.export_list.subject') %>
+
+<%= I18n.t('mails_notifications.export_list.body', days: @expire_in ) %>
+
+<%= I18n.t('mails_notifications.export_list.button_label') %>: <%= @csv_file_url %>
+
+...
+
+<%= strip_tags I18n.t("mails_notifications.copyright_html", app_name: CheckConfig.get('app_name')) %>
+https://meedan.com
diff --git a/config/initializers/plugins.rb b/config/initializers/plugins.rb
index 056a5b61a5..b928f936ff 100644
--- a/config/initializers/plugins.rb
+++ b/config/initializers/plugins.rb
@@ -1,2 +1,2 @@
# Load classes on boot, in production, that otherwise wouldn't be auto-loaded by default
-CcDeville && Bot::Keep && Workflow::Workflow.workflows && CheckS3 && Bot::Tagger && Bot::Fetch && Bot::Smooch && Bot::Slack && Bot::Alegre && CheckChannels && RssFeed && UrlRewriter && ClusterTeam
+CcDeville && Bot::Keep && Workflow::Workflow.workflows && CheckS3 && Bot::Tagger && Bot::Fetch && Bot::Smooch && Bot::Slack && Bot::Alegre && CheckChannels && RssFeed && UrlRewriter && ClusterTeam && ListExport
diff --git a/config/locales/en.yml b/config/locales/en.yml
index aa441378fc..76c56b8868 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -476,6 +476,11 @@ en:
constitutes acceptance of our updated Terms of Service.
term_button: Terms of Service
more_info: This is a one-time required legal notice sent to all Check users, even those who have unsubscribed by optional announcements.
+ export_list:
+ subject: Your Check data export is ready
+ hello: Hello %{name}
+ body: Your data export is ready. Click on the button below to download it. Please note that the link is valid only for %{days} days.
+ button_label: Download export
mail_security:
device_subject: 'Security alert: New login to %{app_name} from %{browser} on %{platform}'
ip_subject: 'Security alert: New or unusual %{app_name} login'
diff --git a/lib/check_s3.rb b/lib/check_s3.rb
index 08015fb078..7989cefbb4 100644
--- a/lib/check_s3.rb
+++ b/lib/check_s3.rb
@@ -66,12 +66,12 @@ def self.delete(*paths)
client.delete_objects(bucket: CheckConfig.get('storage_bucket'), delete: { objects: objects })
end
- def self.write_presigned(path, content_type, content)
+ def self.write_presigned(path, content_type, content, expires_in)
self.write(path, content_type, content)
bucket = CheckConfig.get('storage_bucket')
client = Aws::S3::Client.new
s3 = Aws::S3::Resource.new(client: client)
obj = s3.bucket(bucket).object(path)
- obj.presigned_url(:get, expires_in: CheckConfig.get('export_csv_expire', 7.days.to_i, :integer))
+ obj.presigned_url(:get, expires_in: expires_in)
end
end
diff --git a/lib/check_search.rb b/lib/check_search.rb
index 77321b7743..d8707e31a2 100644
--- a/lib/check_search.rb
+++ b/lib/check_search.rb
@@ -333,7 +333,7 @@ def medias_get_search_result(query)
@options['es_id'] ? $repository.find([@options['es_id']]).compact : $repository.search(query: query, collapse: collapse, sort: sort, size: @options['eslimit'], from: @options['esoffset']).results
end
- def self.export_to_csv(query, team_id)
+ def self.get_exported_data(query, team_id)
team = Team.find(team_id)
search = CheckSearch.new(query, nil, team_id)
@@ -369,16 +369,7 @@ def self.export_to_csv(query, team_id)
end
data << row
end
-
- # Convert to CSV
- csv_string = CSV.generate do |csv|
- data.each do |row|
- csv << row
- end
- end
-
- # Save to S3
- CheckS3.write_presigned("export/item/#{team.slug}/#{Time.now.to_i}/#{Digest::MD5.hexdigest(query)}.csv", 'text/csv', csv_string)
+ data
end
private
diff --git a/lib/list_export.rb b/lib/list_export.rb
new file mode 100644
index 0000000000..96b3619c90
--- /dev/null
+++ b/lib/list_export.rb
@@ -0,0 +1,49 @@
+class ListExport
+ TYPES = [:article, :feed, :media]
+
+ def initialize(type, query, team_id)
+ @type = type
+ @query = query
+ @team_id = team_id
+ raise "Invalid export type '#{type}'. Should be one of: #{TYPES}" unless TYPES.include?(type)
+ end
+
+ def number_of_rows
+ case @type
+ when :media
+ CheckSearch.new(@query, nil, @team_id).number_of_results
+ end
+ end
+
+ def generate_csv_and_send_email_in_background(user)
+ ListExport.delay.generate_csv_and_send_email(self, user.id)
+ end
+
+ def generate_csv_and_send_email(user)
+ # Convert to CSV
+ csv_string = CSV.generate do |csv|
+ self.export_data.each do |row|
+ csv << row
+ end
+ end
+
+ # Save to S3
+ csv_file_url = CheckS3.write_presigned("export/#{@type}/#{@team_id}/#{Time.now.to_i}/#{Digest::MD5.hexdigest(@query)}.csv", 'text/csv', csv_string, CheckConfig.get('export_csv_expire', 7.days.to_i, :integer))
+
+ # Send to e-mail
+ ExportListMailer.delay.send_csv(csv_file_url, user)
+ end
+
+ def self.generate_csv_and_send_email(export, user_id)
+ export.generate_csv_and_send_email(User.find(user_id))
+ end
+
+ private
+
+ def export_data
+ case @type
+ when :media
+ CheckSearch.get_exported_data(@query, @team_id)
+ end
+ end
+end
diff --git a/test/controllers/graphql_controller_11_test.rb b/test/controllers/graphql_controller_11_test.rb
index a22852ebd3..de506ae659 100644
--- a/test/controllers/graphql_controller_11_test.rb
+++ b/test/controllers/graphql_controller_11_test.rb
@@ -166,7 +166,7 @@ def teardown
create_team_user team: t, user: u, role: 'admin'
authenticate_with_user(u)
- query = "mutation { exportList(input: { query: \"{}\" }) { success } }"
+ query = "mutation { exportList(input: { query: \"{}\", type: \"media\" }) { success } }"
post :create, params: { query: query, team: t.slug }
assert_response :success
assert JSON.parse(@response.body)['data']['exportList']['success']
@@ -178,7 +178,7 @@ def teardown
create_team_user team: t, user: u, role: 'editor'
authenticate_with_user(u)
- query = "mutation { exportList(input: { query: \"{}\" }) { success } }"
+ query = "mutation { exportList(input: { query: \"{}\", type: \"media\" }) { success } }"
post :create, params: { query: query, team: t.slug }
assert_response :success
assert !JSON.parse(@response.body)['data']['exportList']['success']
@@ -191,7 +191,7 @@ def teardown
authenticate_with_user(u)
stub_configs({ 'export_csv_maximum_number_of_results' => -1 }) do
- query = "mutation { exportList(input: { query: \"{}\" }) { success } }"
+ query = "mutation { exportList(input: { query: \"{}\", type: \"media\" }) { success } }"
post :create, params: { query: query, team: t.slug }
assert_response :success
assert !JSON.parse(@response.body)['data']['exportList']['success']
|