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.subject") %> +
+ + + + +
 
+
+
+ <%= 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']