mirror of
https://github.com/discourse/discourse.git
synced 2024-12-30 08:43:53 +08:00
322a3be2db
Constants should always be only assigned once. The logical OR assignment of a constant is a relic of the past before we used zeitwerk for autoloading and had bugs where a file could be loaded twice resulting in constant redefinition warnings.
467 lines
13 KiB
Ruby
467 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "csv"
|
|
|
|
module Jobs
|
|
class ExportCsvFile < ::Jobs::Base
|
|
sidekiq_options retry: false
|
|
|
|
attr_accessor :extra
|
|
attr_accessor :current_user
|
|
attr_accessor :entity
|
|
|
|
HEADER_ATTRS_FOR =
|
|
HashWithIndifferentAccess.new(
|
|
user_list: %w[
|
|
id
|
|
name
|
|
username
|
|
email
|
|
title
|
|
created_at
|
|
last_seen_at
|
|
last_posted_at
|
|
last_emailed_at
|
|
trust_level
|
|
approved
|
|
suspended_at
|
|
suspended_till
|
|
silenced_till
|
|
active
|
|
admin
|
|
moderator
|
|
ip_address
|
|
staged
|
|
secondary_emails
|
|
],
|
|
user_stats: %w[
|
|
topics_entered
|
|
posts_read_count
|
|
time_read
|
|
topic_count
|
|
post_count
|
|
likes_given
|
|
likes_received
|
|
],
|
|
user_profile: %w[location website views],
|
|
user_sso: %w[
|
|
external_id
|
|
external_email
|
|
external_username
|
|
external_name
|
|
external_avatar_url
|
|
],
|
|
staff_action: %w[staff_user action subject created_at details context],
|
|
screened_email: %w[email action match_count last_match_at created_at ip_address],
|
|
screened_ip: %w[ip_address action match_count last_match_at created_at],
|
|
screened_url: %w[domain action match_count last_match_at created_at],
|
|
report: %w[date value],
|
|
)
|
|
|
|
def execute(args)
|
|
@entity = args[:entity]
|
|
@extra = HashWithIndifferentAccess.new(args[:args]) if args[:args]
|
|
@current_user = User.find_by(id: args[:user_id])
|
|
|
|
entity = { name: @entity }
|
|
entity[:method] = :"#{entity[:name]}_export"
|
|
raise Discourse::InvalidParameters.new(:entity) unless respond_to?(entity[:method])
|
|
|
|
@timestamp ||= Time.now.strftime("%y%m%d-%H%M%S")
|
|
entity[:filename] = if entity[:name] == "report" && @extra[:name].present?
|
|
"#{@extra[:name].dasherize}-#{@timestamp}"
|
|
else
|
|
"#{entity[:name].dasherize}-#{@timestamp}"
|
|
end
|
|
|
|
export_title =
|
|
if @entity == "report" && @extra[:name].present?
|
|
I18n.t("reports.#{@extra[:name]}.title")
|
|
else
|
|
@entity.gsub("_", " ").titleize
|
|
end
|
|
|
|
filename = entity[:filename]
|
|
user_export = UserExport.create(file_name: filename, user_id: @current_user.id)
|
|
filename = "#{filename}-#{user_export.id}"
|
|
|
|
zip_filename = write_to_csv_and_zip(filename, entity)
|
|
|
|
# create upload
|
|
upload = nil
|
|
|
|
if File.exist?(zip_filename)
|
|
File.open(zip_filename) do |file|
|
|
upload =
|
|
UploadCreator.new(
|
|
file,
|
|
File.basename(zip_filename),
|
|
type: "csv_export",
|
|
for_export: "true",
|
|
).create_for(@current_user.id)
|
|
|
|
if upload.persisted?
|
|
user_export.update_columns(upload_id: upload.id)
|
|
else
|
|
Rails.logger.warn(
|
|
"Failed to upload the file #{zip_filename}: #{upload.errors.full_messages}",
|
|
)
|
|
end
|
|
end
|
|
|
|
File.delete(zip_filename)
|
|
end
|
|
ensure
|
|
post = notify_user(upload, export_title)
|
|
|
|
if user_export.present? && post.present?
|
|
topic = post.topic
|
|
user_export.update_columns(topic_id: topic.id)
|
|
topic.update_status("closed", true, Discourse.system_user)
|
|
end
|
|
end
|
|
|
|
def user_list_export
|
|
user_field_ids = UserField.pluck(:id)
|
|
|
|
condition = {}
|
|
if @extra && @extra[:trust_level] &&
|
|
trust_level = TrustLevel.levels[@extra[:trust_level].to_sym]
|
|
condition = { trust_level: trust_level }
|
|
end
|
|
|
|
includes = %i[user_profile user_stat groups user_emails]
|
|
includes << [:single_sign_on_record] if SiteSetting.enable_discourse_connect
|
|
|
|
User
|
|
.where(condition)
|
|
.includes(*includes)
|
|
.find_each do |user|
|
|
user_info_array = get_base_user_array(user)
|
|
if SiteSetting.enable_discourse_connect
|
|
user_info_array = add_single_sign_on(user, user_info_array)
|
|
end
|
|
user_info_array = add_custom_fields(user, user_info_array, user_field_ids)
|
|
user_info_array = add_group_names(user, user_info_array)
|
|
yield user_info_array
|
|
end
|
|
end
|
|
|
|
def staff_action_export
|
|
staff_action_data =
|
|
if @current_user.admin?
|
|
UserHistory.only_staff_actions
|
|
else
|
|
UserHistory.where(admin_only: false).only_staff_actions
|
|
end
|
|
|
|
staff_action_data.find_each(order: :desc) do |staff_action|
|
|
yield get_staff_action_fields(staff_action)
|
|
end
|
|
end
|
|
|
|
def screened_email_export
|
|
ScreenedEmail.find_each(order: :desc) do |screened_email|
|
|
yield get_screened_email_fields(screened_email)
|
|
end
|
|
end
|
|
|
|
def screened_ip_export
|
|
ScreenedIpAddress.find_each(order: :desc) do |screened_ip|
|
|
yield get_screened_ip_fields(screened_ip)
|
|
end
|
|
end
|
|
|
|
def screened_url_export
|
|
ScreenedUrl
|
|
.select(
|
|
"domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at",
|
|
)
|
|
.group(:domain)
|
|
.order("last_match_at DESC")
|
|
.each { |screened_url| yield get_screened_url_fields(screened_url) }
|
|
end
|
|
|
|
def report_export
|
|
# If dates are invalid consider then `nil`
|
|
if @extra[:start_date].is_a?(String)
|
|
@extra[:start_date] = begin
|
|
@extra[:start_date].to_date.beginning_of_day
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
if @extra[:end_date].is_a?(String)
|
|
@extra[:end_date] = begin
|
|
@extra[:end_date].to_date.end_of_day
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
end
|
|
|
|
@extra[:filters] = {}
|
|
@extra[:filters][:category] = @extra[:category].to_i if @extra[:category].present?
|
|
@extra[:filters][:group] = @extra[:group].to_i if @extra[:group].present?
|
|
@extra[:filters][:include_subcategories] = !!ActiveRecord::Type::Boolean.new.cast(
|
|
@extra[:include_subcategories],
|
|
) if @extra[:include_subcategories].present?
|
|
|
|
report = Report.find(@extra[:name], @extra)
|
|
|
|
header = []
|
|
titles = {}
|
|
|
|
report.labels.each do |label|
|
|
if label[:type] == :user
|
|
titles[label[:properties][:username]] = label[:title]
|
|
header << label[:properties][:username]
|
|
elsif label[:type] == :topic
|
|
titles[label[:properties][:id]] = label[:title]
|
|
header << label[:properties][:id]
|
|
else
|
|
titles[label[:property]] = label[:title]
|
|
header << label[:property]
|
|
end
|
|
end
|
|
|
|
if report.modes == [:stacked_chart]
|
|
header = [:x]
|
|
data = {}
|
|
|
|
report.data.map do |series|
|
|
header << series[:label]
|
|
series[:data].each do |datapoint|
|
|
data[datapoint[:x]] ||= { x: datapoint[:x] }
|
|
data[datapoint[:x]][series[:label]] = datapoint[:y]
|
|
end
|
|
end
|
|
|
|
data = data.values
|
|
else
|
|
data = report.data
|
|
end
|
|
|
|
yield header.map { |k| titles[k] || k }
|
|
data.each { |row| yield row.values_at(*header).map(&:to_s) }
|
|
end
|
|
|
|
def get_header(entity)
|
|
if entity == "user_list"
|
|
header_array =
|
|
HEADER_ATTRS_FOR["user_list"] + HEADER_ATTRS_FOR["user_stats"] +
|
|
HEADER_ATTRS_FOR["user_profile"]
|
|
header_array.concat(HEADER_ATTRS_FOR["user_sso"]) if SiteSetting.enable_discourse_connect
|
|
user_custom_fields = UserField.all
|
|
if user_custom_fields.present?
|
|
user_custom_fields.each do |custom_field|
|
|
header_array.push("#{custom_field.name} (custom user field)")
|
|
end
|
|
end
|
|
header_array.push("group_names")
|
|
else
|
|
header_array = HEADER_ATTRS_FOR[entity]
|
|
end
|
|
|
|
header_array
|
|
end
|
|
|
|
private
|
|
|
|
def escape_comma(string)
|
|
string&.include?(",") ? %Q|"#{string}"| : string
|
|
end
|
|
|
|
def get_base_user_array(user)
|
|
# preloading scopes is hard, do this by hand
|
|
secondary_emails = []
|
|
primary_email = nil
|
|
|
|
user.user_emails.each do |user_email|
|
|
if user_email.primary?
|
|
primary_email = user_email.email
|
|
else
|
|
secondary_emails << user_email.email
|
|
end
|
|
end
|
|
|
|
[
|
|
user.id,
|
|
escape_comma(user.name),
|
|
user.username,
|
|
primary_email,
|
|
escape_comma(user.title),
|
|
user.created_at,
|
|
user.last_seen_at,
|
|
user.last_posted_at,
|
|
user.last_emailed_at,
|
|
user.trust_level,
|
|
user.approved,
|
|
user.suspended_at,
|
|
user.suspended_till,
|
|
user.silenced_till,
|
|
user.active,
|
|
user.admin,
|
|
user.moderator,
|
|
user.ip_address,
|
|
user.staged,
|
|
secondary_emails.join(";"),
|
|
user.user_stat.topics_entered,
|
|
user.user_stat.posts_read_count,
|
|
user.user_stat.time_read,
|
|
user.user_stat.topic_count,
|
|
user.user_stat.post_count,
|
|
user.user_stat.likes_given,
|
|
user.user_stat.likes_received,
|
|
escape_comma(user.user_profile.location),
|
|
user.user_profile.website,
|
|
user.user_profile.views,
|
|
]
|
|
end
|
|
|
|
def add_single_sign_on(user, user_info_array)
|
|
if user.single_sign_on_record
|
|
user_info_array.push(
|
|
user.single_sign_on_record.external_id,
|
|
user.single_sign_on_record.external_email,
|
|
user.single_sign_on_record.external_username,
|
|
escape_comma(user.single_sign_on_record.external_name),
|
|
user.single_sign_on_record.external_avatar_url,
|
|
)
|
|
else
|
|
user_info_array.push(nil, nil, nil, nil, nil)
|
|
end
|
|
user_info_array
|
|
end
|
|
|
|
def add_custom_fields(user, user_info_array, user_field_ids)
|
|
if user_field_ids.present?
|
|
user.user_fields.each { |custom_field| user_info_array << escape_comma(custom_field[1]) }
|
|
end
|
|
user_info_array
|
|
end
|
|
|
|
def add_group_names(user, user_info_array)
|
|
group_names = user.groups.map { |g| g.name }.join(";")
|
|
if group_names.present?
|
|
user_info_array << escape_comma(group_names)
|
|
else
|
|
user_info_array << nil
|
|
end
|
|
user_info_array
|
|
end
|
|
|
|
def get_staff_action_fields(staff_action)
|
|
staff_action_array = []
|
|
|
|
HEADER_ATTRS_FOR["staff_action"].each do |attr|
|
|
data =
|
|
if attr == "action"
|
|
UserHistory.actions.key(staff_action.attributes[attr]).to_s
|
|
elsif attr == "staff_user"
|
|
user = User.find_by(id: staff_action.attributes["acting_user_id"])
|
|
user.username if !user.nil?
|
|
elsif attr == "subject"
|
|
user = User.find_by(id: staff_action.attributes["target_user_id"])
|
|
if user.nil?
|
|
staff_action.attributes[attr]
|
|
else
|
|
"#{user.username} #{staff_action.attributes[attr]}"
|
|
end
|
|
else
|
|
staff_action.attributes[attr]
|
|
end
|
|
|
|
staff_action_array.push(data)
|
|
end
|
|
staff_action_array
|
|
end
|
|
|
|
def get_screened_email_fields(screened_email)
|
|
screened_email_array = []
|
|
|
|
HEADER_ATTRS_FOR["screened_email"].each do |attr|
|
|
data =
|
|
if attr == "action"
|
|
ScreenedEmail.actions.key(screened_email.attributes["action_type"]).to_s
|
|
else
|
|
screened_email.attributes[attr]
|
|
end
|
|
|
|
screened_email_array.push(data)
|
|
end
|
|
|
|
screened_email_array
|
|
end
|
|
|
|
def get_screened_ip_fields(screened_ip)
|
|
screened_ip_array = []
|
|
|
|
HEADER_ATTRS_FOR["screened_ip"].each do |attr|
|
|
data =
|
|
if attr == "action"
|
|
ScreenedIpAddress.actions.key(screened_ip.attributes["action_type"]).to_s
|
|
else
|
|
screened_ip.attributes[attr]
|
|
end
|
|
|
|
screened_ip_array.push(data)
|
|
end
|
|
|
|
screened_ip_array
|
|
end
|
|
|
|
def get_screened_url_fields(screened_url)
|
|
screened_url_array = []
|
|
|
|
HEADER_ATTRS_FOR["screened_url"].each do |attr|
|
|
data =
|
|
if attr == "action"
|
|
action = ScreenedUrl.actions.key(screened_url.attributes["action_type"]).to_s
|
|
action = "do nothing" if action.blank?
|
|
else
|
|
screened_url.attributes[attr]
|
|
end
|
|
|
|
screened_url_array.push(data)
|
|
end
|
|
|
|
screened_url_array
|
|
end
|
|
|
|
def notify_user(upload, export_title)
|
|
post = nil
|
|
|
|
if @current_user
|
|
post =
|
|
if upload&.errors&.empty?
|
|
SystemMessage.create_from_system_user(
|
|
@current_user,
|
|
:csv_export_succeeded,
|
|
download_link: UploadMarkdown.new(upload).attachment_markdown,
|
|
export_title: export_title,
|
|
)
|
|
else
|
|
SystemMessage.create_from_system_user(@current_user, :csv_export_failed)
|
|
end
|
|
end
|
|
|
|
post
|
|
end
|
|
|
|
def write_to_csv_and_zip(filename, entity)
|
|
dirname = "#{UserExport.base_directory}/#{filename}"
|
|
FileUtils.mkdir_p(dirname) unless Dir.exist?(dirname)
|
|
begin
|
|
CSV.open("#{dirname}/#{entity[:filename]}.csv", "w") do |csv|
|
|
csv << get_header(entity[:name]) if entity[:name] != "report"
|
|
public_send(entity[:method]) { |d| csv << d }
|
|
end
|
|
|
|
Compression::Zip.new.compress(UserExport.base_directory, filename)
|
|
ensure
|
|
FileUtils.rm_rf(dirname)
|
|
end
|
|
end
|
|
end
|
|
end
|