Update data exporter to handle admin send (#10274)

* Update data exporter to handle admin send

* Match button with everything else

* Use proper redirect path

* Stub SiteConfig definition instead of setting it

Co-authored-by: Mac Siri <krairit.siri@gmail.com>

* Removed if statement by accident oops

* Remove non-functional boolean param and pass email directly

* Use refinement to conv to boolean instead of JSON.parse

* Rename to StringToBoolean

* Use 'using' in proper scope (not in method)

* Rename to_bool to to_boolean

* Refactor if statement, thanks rhymes!

* Fix small bugs in tests

* Remove tracking for export_email b/c no @user

Co-authored-by: Mac Siri <krairit.siri@gmail.com>
This commit is contained in:
Andy Zhao 2020-10-26 18:00:56 -04:00 committed by GitHub
parent c817737748
commit b80dae1436
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 161 additions and 33 deletions

View file

@ -1,6 +1,7 @@
module Admin
class UsersController < Admin::ApplicationController
layout "admin"
using StringToBoolean
after_action only: %i[update user_status banish full_delete merge] do
Audit::Logger.log(:moderator, current_user, params.dup)
@ -50,6 +51,21 @@ module Admin
redirect_to "/admin/users/#{@user.id}/edit"
end
def export_data
user = User.find(params[:id])
send_to_admin = params[:send_to_admin].to_boolean
if send_to_admin
email = SiteConfig.email_addresses[:default]
receiver = "admin"
else
email = user.email
receiver = "user"
end
ExportContentWorker.perform_async(user.id, email)
flash[:success] = "Data exported to the #{receiver}. The job will complete momentarily."
redirect_to edit_admin_user_path(user.id)
end
def banish
Moderator::BanishUserWorker.perform_async(current_user.id, params[:id].to_i)
flash[:success] = "This user is being banished in the background. The job will complete soon."

View file

@ -47,7 +47,7 @@ class UsersController < ApplicationController
end
if @user.export_requested?
notice += " The export will be emailed to you shortly."
ExportContentWorker.perform_async(@user.id)
ExportContentWorker.perform_async(@user.id, @user.email)
end
cookies.permanent[:user_experience_level] = @user.experience_level.to_s if @user.experience_level.present?
flash[:settings_notice] = notice

View file

@ -116,12 +116,11 @@ class NotifyMailer < ApplicationMailer
end
def export_email
@user = params[:user]
attachment = params[:attachment]
export_filename = "devto-export-#{Date.current.iso8601}.zip"
attachments[export_filename] = attachment
mail(to: @user.email, subject: "The export of your content is ready")
mail(to: params[:email], subject: "The export of your content is ready")
end
def tag_moderator_confirmation_email

View file

@ -0,0 +1,7 @@
module StringToBoolean
refine String do
def to_boolean
ActiveModel::Type::Boolean.new.cast(self)
end
end
end

View file

@ -13,7 +13,7 @@ module Exporter
@user = user
end
def export(send_email: false, config: {})
def export(email, config: {})
exports = {}
# export content with filenames
@ -26,7 +26,7 @@ module Exporter
zipped_exports = zip_exports(exports)
send_exports_by_email(zipped_exports) if send_email
send_exports_by_email(zipped_exports, email)
update_user_export_fields
@ -53,9 +53,9 @@ module Exporter
buffer
end
def send_exports_by_email(zipped_exports)
def send_exports_by_email(zipped_exports, email)
zipped_exports.rewind
NotifyMailer.with(user: user, attachment: zipped_exports.read).export_email.deliver_now
NotifyMailer.with(email: email, attachment: zipped_exports.read).export_email.deliver_now
end
def update_user_export_fields

View file

@ -0,0 +1,55 @@
<h2 class="crayons-title mb-6">Create Event</h2>
<div class="crayons-card p-6">
<%# form_with url: admin_ %>
</div>
<div class="form-group">
<%= f.label :cover_image %>:
<%= f.file_field :cover_image, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :profile_image %> (for live notification):
<%= f.file_field :profile_image, class: "form-control" %>
<img src="<%= event.profile_image_url %>" style="width: 25%;" alt="event profile image">
</div>
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, maxlength: 90, size: 40, required: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :host_name %>
<%= f.text_field :host_name, size: 40, required: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :category %>
<%= f.select :category, ["AMA", "Workshop", "Talk", "Town Hall"], required: true %>
</div>
<div class="form-group">
<%= f.label :starts_at %>
<%= f.datetime_select :starts_at, required: true, include_blank: true, start_year: Time.current.year, end_year: Time.current.year + 2, class: "form-control" %> UTC Time Only (4 hours ahead of eastern time)
</div>
<div class="form-group">
<%= f.label :ends_at %>
<%= f.datetime_select :ends_at, required: true, include_blank: true, start_year: Time.current.year, end_year: Time.current.year + 2, class: "form-control" %> UTC Time Only (4 hours ahead of eastern time)
</div>
<div class="form-group">
<%= f.label :location_name %>
<%= f.text_field :location_name, required: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :location_url %>
<%= f.text_field :location_url, required: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :description_markdown %>
<%= f.text_area :description_markdown, size: "45x10", required: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :published %>
<%= f.check_box :published %>
</div>
<div class="form-group">
<%= f.label :live_now %>
<%= f.check_box :live_now %>
</div>
<%= f.submit class: "btn btn-primary" %>

View file

@ -8,7 +8,7 @@
<%= paginate @moderator_actions %>
<table class="crayons-table">
<table class="crayons-table" width="100%">
<thead>
<tr>
<th scope="col">ID</th>

View file

@ -0,0 +1,21 @@
<div class="crayons-card p-6">
<div>
<h2 class="d-inline">Data Export</h2>
<button type="button" data-toggle="collapse" data-target="#data-export-row" class="crayons-btn float-right">Toggle</button>
</div>
<div class="crayons-card__body collapse" id="data-export-row">
<p>
You can export a user's data here. Currently we only support exporting a user's posts and comments.
</p>
<p>
You have the option of exporting to your admin account to send to the user, or exporting to the user directly.
</p>
<p>
Exporting the data to your admin account will send it to your default admin email (<%= SiteConfig.email_addresses[:default] %>), and exporting to the user will send it to their email (<%= @user.email %>).
</p>
<div style="display: flex;">
<%= button_to "Export to Admin", export_data_admin_user_path(@user.id), data: { confirm: "Are you sure you want to export this user's content to the ADMIN?" }, class: "crayons-btn", params: { send_to_admin: true }, style: "margin-right: 8px;" %>
<%= button_to "Export to User", export_data_admin_user_path(@user.id), data: { confirm: "Are you sure you want to export this user's content to the USER?" }, class: "crayons-btn", params: { send_to_admin: false } %>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div class="crayons-card p-6">
<h2 class="mb-6">Recent Notes (last 10)</h2>
<% unless @notes.load.empty? %>
<% @notes.each do |note| %>
<p>
@ -21,6 +21,6 @@
<%= f.label "Add new note: ", class: "d-block" %>
<%= f.text_area :new_note, class: "form-control" %>
</div>
<%= f.submit "Submit Note", class: "btn btn-primary float-right" %>
<%= f.submit "Submit Note", class: "crayons-btn float-right" %>
<% end %>
</div>

View file

@ -9,7 +9,7 @@
<%= link_to "Unlock access", unlock_access_admin_user_path(@user), method: :patch, class: "btn btn-success" %>
</div>
<% end %>
<a href="<%= admin_user_path(@user) %>" class="btn btn-primary float-right">Admin Profile</a>
<a href="<%= admin_user_path(@user) %>" class="crayons-btn float-right">Admin Profile</a>
<p class="font-italic">Member since <%= @user.created_at.strftime("%b %e '%y") %></p>
</div>
@ -43,18 +43,19 @@
<%= f.label "Reason for action:" %>
<%= f.text_area :note_for_current_role, required: true, class: "form-control" %>
</div>
<%= f.submit "Update User Status", class: "btn btn-primary float-right" %>
<%= f.submit "Update User Status", class: "crayons-btn float-right" %>
<% end %>
</div>
<%= render "notes" %>
<%= render "negative_reactions" %>
<%= render "reports" %>
<%= render "data_export" %>
<div class="crayons-card p-6">
<div>
<h2 class="d-inline">Remove Identity</h2>
<button type="button" data-toggle="collapse" data-target="#identity-row" class="btn btn-secondary float-right">Toggle</button>
<button type="button" data-toggle="collapse" data-target="#identity-row" class="crayons-btn float-right">Toggle</button>
</div>
<div class="pt-6 collapse" id="identity-row">
<p>Removing a social account identity can solve certain sign in issues, for example:</p>
@ -92,7 +93,7 @@
<div class="crayons-card p-6">
<div>
<h2 class="d-inline">Destructive Actions</h2>
<button type="button" data-toggle="collapse" data-target="#destructive-actions" class="btn btn-secondary float-right">Toggle</button>
<button type="button" data-toggle="collapse" data-target="#destructive-actions" class="crayons-btn float-right">Toggle</button>
</div>
<div id="destructive-actions" class="pt-6 collapse">

View file

@ -3,9 +3,9 @@ class ExportContentWorker
sidekiq_options queue: :medium_priority, retry: 10, lock: :until_executed
def perform(user_id)
def perform(user_id, email)
user = User.find_by(id: user_id)
Exporter::Service.new(user).export(send_email: true) if user
Exporter::Service.new(user).export(email) if user
end
end

View file

@ -103,6 +103,7 @@ Rails.application.routes.draw do
resources :users, only: %i[index show edit update] do
member do
post "banish"
post "export_data"
post "full_delete"
patch "user_status"
post "merge"

View file

@ -403,7 +403,7 @@ RSpec.describe NotifyMailer, type: :mailer do
end
describe "#export_email" do
let(:email) { described_class.with(user: user, attachment: "attachment").export_email }
let(:email) { described_class.with(email: user.email, attachment: "attachment").export_email }
it "renders proper subject" do
expect(email.subject).to include("export of your content is ready")
@ -431,12 +431,6 @@ RSpec.describe NotifyMailer, type: :mailer do
it "includes the tracking pixel" do
expect(email.html_part.body).to include("open.gif")
end
it "includes UTM params" do
expect(email.html_part.body).to include(CGI.escape("utm_medium=email"))
expect(email.html_part.body).to include(CGI.escape("utm_source=notify_mailer"))
expect(email.html_part.body).to include(CGI.escape("utm_campaign=export_email"))
end
end
describe "#tag_moderator_confirmation_email" do

View file

@ -0,0 +1,10 @@
require "rails_helper"
RSpec.describe StringToBoolean, type: :refinement do
describe "#to_boolean" do
using described_class
it "converts a string to a boolean" do
expect("true".to_boolean).to be true
end
end
end

View file

@ -11,7 +11,7 @@ RSpec.describe "admin/users", type: :request do
sign_in(admin)
end
describe "GETS /admin/users" do
describe "GET /admin/users" do
it "renders to appropriate page" do
get "/admin/users"
expect(response.body).to include(user.username)
@ -149,4 +149,12 @@ RSpec.describe "admin/users", type: :request do
end.to change { user.reload.access_locked? }.from(true).to(false)
end
end
describe "POST admin/users/:id/export_data" do
it "redirects properly to the user edit page" do
sign_in admin
post export_data_admin_user_path(user), params: { send_to_admin: "true" }
expect(response).to redirect_to edit_admin_user_path(user)
end
end
end

View file

@ -47,14 +47,14 @@ RSpec.describe Exporter::Service, type: :service do
describe "#export" do
it "exports a zip file with files" do
service = valid_instance(article.user)
zipped_exports = service.export
zipped_exports = service.export(article.user.email)
exports = extract_zipped_exports(zipped_exports)
expect(exports.keys).to eq(["articles.json", "comments.json"])
end
it "passes configuration to an exporter" do
service = valid_instance(article.user)
zipped_exports = service.export(config: { articles: { slug: article.slug } })
zipped_exports = service.export(article.user.email, config: { articles: { slug: article.slug } })
exports = extract_zipped_exports(zipped_exports)
expect(exports.length).to eq(described_class::EXPORTERS.size)
end
@ -64,20 +64,26 @@ RSpec.describe Exporter::Service, type: :service do
config = double
allow(config).to receive(:fetch).with(:articles, {}).and_return(slug: article.slug)
allow(config).to receive(:fetch).with(:comments, {}).and_return({})
service.export(config: config)
service.export(article.user.email, config: config)
expect(config).to have_received(:fetch).with(:articles, {})
end
context "when emailing the user" do
it "delivers one email" do
service = valid_instance(article.user)
service.export(send_email: true)
service.export(article.user.email)
expect(ActionMailer::Base.deliveries.count).to eq(1)
end
it "delivers one email to the user's email" do
service = valid_instance(article.user)
service.export(article.user.email)
expect(ActionMailer::Base.deliveries.last.to.first).to eq article.user.email
end
it "delivers an email with the export" do
service = valid_instance(article.user)
zipped_export = service.export(send_email: true)
zipped_export = service.export(article.user.email)
attachment = ActionMailer::Base.deliveries.last.attachments[0].decoded
exports = extract_zipped_exports(zipped_export)
@ -85,16 +91,26 @@ RSpec.describe Exporter::Service, type: :service do
end
end
context "when emailing an admin" do
it "delivers one email to the default admin email" do
admin_email = "admin@example.com"
allow(SiteConfig).to receive(:email_addresses).and_return({ default: admin_email })
service = valid_instance(article.user)
service.export(admin_email)
expect(ActionMailer::Base.deliveries.last.to.first).to eq admin_email
end
end
it "sets the requested flag as false" do
service = valid_instance(article.user)
service.export
service.export(article.user.email)
expect(user.export_requested).to be(false)
end
it "sets the exported at datetime as the current one" do
Timecop.freeze(Time.current) do
service = valid_instance(article.user)
service.export
service.export(article.user.email)
expect(user.exported_at).to eq(Time.current)
end
end

View file

@ -15,12 +15,12 @@ RSpec.describe ExportContentWorker, type: :worker do
end
it "calls the service" do
worker.perform(user.id)
worker.perform(user.id, user.email)
expect(exporter_service).to have_received(:export).once
end
it "doesn't call the service if non existent user ID is given" do
worker.perform(9999)
worker.perform(9999, user.email)
expect(exporter_service).not_to have_received(:export)
end
end