mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: Allow admins to export users (#30918)
The GDPR requires all users to be able to export their data, or request an export of their data. This is fine for active users as we have a data export button on user profiles, but suspended users have no way of accessing the data export function, and the workaround for admins to export data for suspended users involves temporarily unsuspending them, then impersonating the user to export the data as them. Since suspended users no longer have access to their account, we can safely assume that the export request will be coming via a medium outside of Discourse (eg, email). This change is built with this workflow in mind. This change adds a new "User exports" section to the admin user page, allowing admins to start a new export, and to download the latest export file.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { notEmpty } from "@ember/object/computed";
|
||||
import { service } from "@ember/service";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import UserExport from "admin/models/user-export";
|
||||
|
||||
const EXPORT_PROGRESS_CHANNEL = "/user-export-progress";
|
||||
|
||||
export default class extends Component {
|
||||
@service dialog;
|
||||
@service messageBus;
|
||||
@service toasts;
|
||||
|
||||
@tracked userExport = null;
|
||||
@tracked userExportReloading = false;
|
||||
|
||||
@notEmpty("userExport") userExportAvailable;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.messageBus.subscribe(EXPORT_PROGRESS_CHANNEL, this.onExportProgress);
|
||||
|
||||
this.model = this.args.model;
|
||||
this.userExport = UserExport.create(this.model.latest_export?.user_export);
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
this.messageBus.unsubscribe(EXPORT_PROGRESS_CHANNEL, this.onExportProgress);
|
||||
}
|
||||
|
||||
@bind
|
||||
onExportProgress(data) {
|
||||
if (data.user_export_id === this.model.id) {
|
||||
this.userExportReloading = false;
|
||||
if (data.failed) {
|
||||
this.dialog.alert(i18n("admin.user.exports.download.export_failed"));
|
||||
} else {
|
||||
this.userExport = UserExport.create(data.export_data.user_export);
|
||||
this.toasts.success({
|
||||
autoClose: false,
|
||||
data: { message: i18n("admin.user.exports.download.success") },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
triggerUserExport() {
|
||||
this.dialog.yesNoConfirm({
|
||||
message: i18n("admin.user.exports.download.confirm"),
|
||||
didConfirm: () => {
|
||||
this.userExportReloading = true;
|
||||
try {
|
||||
exportEntity("user_archive", {
|
||||
export_user_id: this.model.id,
|
||||
});
|
||||
|
||||
this.toasts.success({
|
||||
duration: 3000,
|
||||
data: { message: i18n("admin.user.exports.download.started") },
|
||||
});
|
||||
} catch (err) {
|
||||
popupAjaxError(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get userExportExpiry() {
|
||||
return i18n("admin.user.exports.download.expires_in", {
|
||||
count: this.userExport.retain_hours,
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<section class="details">
|
||||
<h1>{{i18n "admin.user.exports.title"}}</h1>
|
||||
|
||||
<div class="display-row">
|
||||
<div class="field">{{i18n
|
||||
"admin.user.exports.download.description"
|
||||
}}</div>
|
||||
<div class="value">
|
||||
{{#if this.userExportAvailable}}
|
||||
<a
|
||||
class="download"
|
||||
href={{this.userExport.uri}}
|
||||
>{{this.userExport.filename}}</a><br />
|
||||
{{this.userExport.human_filesize}}<br />
|
||||
{{this.userExportExpiry}}
|
||||
{{else}}
|
||||
{{i18n "admin.user.exports.download.not_available"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="controls">
|
||||
<ConditionalLoadingSpinner @condition={{this.userExportReloading}}>
|
||||
|
||||
<DButton
|
||||
@action={{this.triggerUserExport}}
|
||||
@icon="download"
|
||||
@label="admin.user.exports.download.button"
|
||||
class="btn-default"
|
||||
/>
|
||||
</ConditionalLoadingSpinner>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
}
|
||||
13
app/assets/javascripts/admin/addon/models/user-export.js
Normal file
13
app/assets/javascripts/admin/addon/models/user-export.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import RestModel from "discourse/models/rest";
|
||||
|
||||
export default class UserExport extends RestModel {
|
||||
static async findLatest(user_id) {
|
||||
const result = await ajax(
|
||||
`/export_csv/latest_user_archive/${user_id}.json`
|
||||
);
|
||||
if (result !== null) {
|
||||
return UserExport.create(result.user_export);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -866,6 +866,10 @@
|
||||
</section>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.currentUser.admin}}
|
||||
<AdminUserExportsTable @model={{this.model}} />
|
||||
{{/if}}
|
||||
|
||||
<span>
|
||||
<PluginOutlet
|
||||
@name="after-user-details"
|
||||
|
||||
@@ -1372,6 +1372,10 @@ export function applyDefaultHandlers(pretender) {
|
||||
pretender.get("/session/passkey/challenge.json", () =>
|
||||
response({ challenge: "123" })
|
||||
);
|
||||
|
||||
pretender.get("/export_csv/latest_user_archive/:id.json", () =>
|
||||
response(null)
|
||||
);
|
||||
}
|
||||
|
||||
export function resetPretender() {
|
||||
|
||||
@@ -4,8 +4,9 @@ class ExportCsvController < ApplicationController
|
||||
skip_before_action :preload_json, :check_xhr, only: [:show]
|
||||
|
||||
def export_entity
|
||||
guardian.ensure_can_export_entity!(export_params[:entity])
|
||||
entity = export_params[:entity]
|
||||
entity_id = params.dig(:args, :export_user_id)&.to_i if entity == "user_archive"
|
||||
guardian.ensure_can_export_entity!(entity, entity_id)
|
||||
raise Discourse::InvalidParameters.new(:entity) unless entity.is_a?(String) && entity.size < 100
|
||||
|
||||
(export_params[:args] || {}).each do |key, value|
|
||||
@@ -15,7 +16,13 @@ class ExportCsvController < ApplicationController
|
||||
end
|
||||
|
||||
if entity == "user_archive"
|
||||
Jobs.enqueue(:export_user_archive, user_id: current_user.id, args: export_params[:args])
|
||||
requesting_user_id = current_user.id if entity_id
|
||||
Jobs.enqueue(
|
||||
:export_user_archive,
|
||||
user_id: entity_id || current_user.id,
|
||||
requesting_user_id:,
|
||||
args: export_params[:args],
|
||||
)
|
||||
else
|
||||
Jobs.enqueue(
|
||||
:export_csv_file,
|
||||
@@ -30,6 +37,19 @@ class ExportCsvController < ApplicationController
|
||||
render_json_error I18n.t("csv_export.rate_limit_error")
|
||||
end
|
||||
|
||||
def latest_user_archive
|
||||
user_id = params[:user_id].to_i
|
||||
# If we can't export the entity, we shouldn't be able to see it either
|
||||
guardian.ensure_can_export_entity!("user_archive", user_id)
|
||||
|
||||
render json:
|
||||
UserExport
|
||||
.where(user_id:)
|
||||
.where("created_at > ?", UserExport::DESTROY_CREATED_BEFORE.ago)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def export_params
|
||||
|
||||
@@ -18,7 +18,7 @@ module Jobs
|
||||
# "[%{export_title}] 資料匯出已完成" gets converted to "%-topic", do not match that slug.
|
||||
slugs = slugs.reject { |s| s == "%-topic" }
|
||||
|
||||
topics = Topic.with_deleted.where(<<~SQL, slugs, UserExport::DESTROY_CREATED_BEFORE)
|
||||
topics = Topic.with_deleted.where(<<~SQL, slugs, UserExport::DESTROY_CREATED_BEFORE.ago)
|
||||
slug LIKE ANY(ARRAY[?]) AND
|
||||
archetype = 'private_message' AND
|
||||
subtype = 'system_message' AND
|
||||
|
||||
@@ -184,9 +184,9 @@ module Jobs
|
||||
|
||||
begin
|
||||
# create upload
|
||||
upload = create_upload_for_user(user_export, zip_filename)
|
||||
create_upload_for_user(user_export, zip_filename)
|
||||
ensure
|
||||
post = notify_user(upload, export_title)
|
||||
post = notify_user(user_export, export_title)
|
||||
|
||||
if user_export.present? && post.present?
|
||||
topic = post.topic
|
||||
@@ -623,19 +623,34 @@ module Jobs
|
||||
%w[composer_open_duration_msecs is_poll reply_to_post_number tags title typing_duration_msecs]
|
||||
end
|
||||
|
||||
def notify_user(upload, export_title)
|
||||
def notify_user(export, export_title)
|
||||
post = nil
|
||||
|
||||
if @requesting_user
|
||||
post =
|
||||
if upload.persisted?
|
||||
if export.upload&.persisted?
|
||||
::MessageBus.publish(
|
||||
"/user-export-progress",
|
||||
{
|
||||
user_export_id: @archive_for_user.id,
|
||||
export_data: UserExportSerializer.new(export, scope: guardian).as_json,
|
||||
},
|
||||
user_ids: [@requesting_user.id],
|
||||
)
|
||||
|
||||
SystemMessage.create_from_system_user(
|
||||
@requesting_user,
|
||||
:csv_export_succeeded,
|
||||
download_link: UploadMarkdown.new(upload).attachment_markdown,
|
||||
download_link: UploadMarkdown.new(export.upload).attachment_markdown,
|
||||
export_title: export_title,
|
||||
)
|
||||
else
|
||||
::MessageBus.publish(
|
||||
"/user-export-progress",
|
||||
{ user_export_id: @archive_for_user.id, failed: true },
|
||||
user_ids: [@requesting_user.id],
|
||||
)
|
||||
|
||||
SystemMessage.create_from_system_user(@requesting_user, :csv_export_failed)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,11 +13,11 @@ class UserExport < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
DESTROY_CREATED_BEFORE = 2.days.ago
|
||||
DESTROY_CREATED_BEFORE = 2.days
|
||||
|
||||
def self.remove_old_exports
|
||||
UserExport
|
||||
.where("created_at < ?", DESTROY_CREATED_BEFORE)
|
||||
.where("created_at < ?", DESTROY_CREATED_BEFORE.ago)
|
||||
.find_each do |user_export|
|
||||
UserExport.transaction do
|
||||
begin
|
||||
@@ -32,6 +32,10 @@ class UserExport < ActiveRecord::Base
|
||||
end
|
||||
end
|
||||
|
||||
def retain_hours
|
||||
(created_at + DESTROY_CREATED_BEFORE - Time.zone.now).to_i / 1.hour
|
||||
end
|
||||
|
||||
def self.base_directory
|
||||
File.join(
|
||||
Rails.root,
|
||||
|
||||
@@ -36,7 +36,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
:can_delete_sso_record,
|
||||
:api_key_count,
|
||||
:external_ids,
|
||||
:similar_users_count
|
||||
:similar_users_count,
|
||||
:latest_export
|
||||
|
||||
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
|
||||
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
|
||||
@@ -167,4 +168,15 @@ class AdminDetailedUserSerializer < AdminUserSerializer
|
||||
def can_delete_sso_record
|
||||
scope.can_delete_sso_record?(object)
|
||||
end
|
||||
|
||||
def latest_export
|
||||
export =
|
||||
UserExport
|
||||
.where(user_id: object&.id)
|
||||
.where("created_at > ?", UserExport::DESTROY_CREATED_BEFORE.ago)
|
||||
.order(created_at: :desc)
|
||||
.first
|
||||
|
||||
UserExportSerializer.new(export, scope:).as_json if export
|
||||
end
|
||||
end
|
||||
|
||||
25
app/serializers/user_export_serializer.rb
Normal file
25
app/serializers/user_export_serializer.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserExportSerializer < ApplicationSerializer
|
||||
attributes :id, :filename, :uri, :filesize, :extension, :retain_hours, :human_filesize
|
||||
|
||||
def filename
|
||||
object.upload.original_filename
|
||||
end
|
||||
|
||||
def uri
|
||||
object.upload.short_path
|
||||
end
|
||||
|
||||
def filesize
|
||||
object.upload.filesize
|
||||
end
|
||||
|
||||
def extension
|
||||
object.upload.extension
|
||||
end
|
||||
|
||||
def human_filesize
|
||||
object.upload.human_filesize
|
||||
end
|
||||
end
|
||||
@@ -6963,6 +6963,19 @@ en:
|
||||
approve_bulk_success: "Success! All selected users have been approved and notified."
|
||||
time_read: "Read Time"
|
||||
post_edits_count: "Post Edits"
|
||||
exports:
|
||||
title: User exports
|
||||
download:
|
||||
description: Download latest export
|
||||
expires_in:
|
||||
one: Expires in %{count} hour
|
||||
other: Expires in %{count} hours
|
||||
not_available: No export available
|
||||
button: Request archive
|
||||
confirm: Do you really want to create an archive of this user's activity and preferences?
|
||||
started: We've started collecting collecting the archive, the download link will update when the process is complete.
|
||||
success: The archive is ready for download.
|
||||
export_failed: We're sorry, but the export failed. Please check the logs for further information.
|
||||
anonymize: "Anonymize User"
|
||||
anonymize_confirm: "Are you sure you want to anonymize this account? This will change the username and email, and reset all profile information."
|
||||
delete_associated_accounts_confirm: "Are you sure you want to delete all associated accounts from this account? The user may not be able to log in if their email has changed."
|
||||
|
||||
@@ -1517,6 +1517,7 @@ Discourse::Application.routes.draw do
|
||||
|
||||
post "/export_csv/export_entity" => "export_csv#export_entity",
|
||||
:as => "export_entity_export_csv_index"
|
||||
get "/export_csv/latest_user_archive/:user_id.json" => "export_csv#latest_user_archive"
|
||||
|
||||
get "onebox" => "onebox#show"
|
||||
get "inline-onebox" => "inline_onebox#show"
|
||||
|
||||
@@ -536,14 +536,15 @@ class Guardian
|
||||
@user.in_any_groups?(SiteSetting.send_email_messages_allowed_groups_map)
|
||||
end
|
||||
|
||||
def can_export_entity?(entity)
|
||||
def can_export_entity?(entity, entity_id = nil)
|
||||
return false if anonymous?
|
||||
return true if is_admin?
|
||||
return can_see_emails? if entity == "screened_email"
|
||||
return entity != "user_list" if is_moderator?
|
||||
return entity != "user_list" if is_moderator? && (entity != "user_archive" || entity_id.nil?)
|
||||
|
||||
# Regular users can only export their archives
|
||||
return false unless entity == "user_archive"
|
||||
return false unless entity_id == @user.id || entity_id.nil?
|
||||
UserExport.where(
|
||||
user_id: @user.id,
|
||||
created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day),
|
||||
|
||||
@@ -3532,6 +3532,12 @@ RSpec.describe Guardian do
|
||||
it "does not allow anonymous to export" do
|
||||
expect(anonymous_guardian.can_export_entity?("user_archive")).to be_falsey
|
||||
end
|
||||
|
||||
it "only allows admins to export user_archive of other users" do
|
||||
expect(user_guardian.can_export_entity?("user_archive", another_user.id)).to be_falsey
|
||||
expect(moderator_guardian.can_export_entity?("user_archive", another_user.id)).to be_falsey
|
||||
expect(admin_guardian.can_export_entity?("user_archive", another_user.id)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#can_ignore_user?" do
|
||||
|
||||
@@ -39,4 +39,22 @@ RSpec.describe UserExport do
|
||||
expect(Topic.exists?(id: topic_2.id)).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#retain_hours" do
|
||||
it "should return the correct number of hours" do
|
||||
csv_file_1 = Fabricate(:upload, created_at: 1.day.ago)
|
||||
topic_1 = Fabricate(:topic, created_at: 1.day.ago)
|
||||
Fabricate(:post, topic: topic_1)
|
||||
export =
|
||||
UserExport.create!(
|
||||
file_name: "test",
|
||||
user: user,
|
||||
upload_id: csv_file_1.id,
|
||||
topic_id: topic_1.id,
|
||||
created_at: 1.day.ago,
|
||||
)
|
||||
|
||||
expect(export.retain_hours).to eq(23)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -134,6 +134,9 @@
|
||||
"full_suspend_reason": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"latest_export": {
|
||||
"type": ["object", "null"]
|
||||
},
|
||||
"silence_reason": {
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
RSpec.describe ExportCsvController do
|
||||
context "while logged in as normal user" do
|
||||
fab!(:user)
|
||||
fab!(:user2) { Fabricate(:user) }
|
||||
before { sign_in(user) }
|
||||
|
||||
describe "#export_entity" do
|
||||
@@ -28,6 +29,18 @@ RSpec.describe ExportCsvController do
|
||||
expect(Jobs::ExportCsvFile.jobs.size).to eq(0)
|
||||
end
|
||||
|
||||
it "does not allow a normal user to export another user's archive" do
|
||||
post "/export_csv/export_entity.json",
|
||||
params: {
|
||||
entity: "user_archive",
|
||||
args: {
|
||||
export_user_id: user2.id,
|
||||
},
|
||||
}
|
||||
expect(response.status).to eq(422)
|
||||
expect(Jobs::ExportUserArchive.jobs.size).to eq(0)
|
||||
end
|
||||
|
||||
it "correctly logs the entity export" do
|
||||
post "/export_csv/export_entity.json", params: { entity: "user_archive" }
|
||||
|
||||
@@ -37,9 +50,32 @@ RSpec.describe ExportCsvController do
|
||||
expect(log_entry.subject).to eq("user_archive")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#latest_user_archive" do
|
||||
it "returns the latest user archive" do
|
||||
export = generate_exports(user)
|
||||
|
||||
get "/export_csv/latest_user_archive/#{user.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["user_export"]["id"]).to eq(export.id)
|
||||
end
|
||||
|
||||
it "returns nothing when the user has no archives" do
|
||||
get "/export_csv/latest_user_archive/#{user.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body).to eq(nil)
|
||||
end
|
||||
|
||||
it "does not allow a normal user to view another user's archive" do
|
||||
generate_exports(user2)
|
||||
get "/export_csv/latest_user_archive/#{user2.id}.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "while logged in as an admin" do
|
||||
fab!(:user)
|
||||
fab!(:admin)
|
||||
before { sign_in(admin) }
|
||||
|
||||
@@ -65,6 +101,21 @@ RSpec.describe ExportCsvController do
|
||||
expect(job_data["user_id"]).to eq(admin.id)
|
||||
end
|
||||
|
||||
it "allows user archives for other users" do
|
||||
post "/export_csv/export_entity.json",
|
||||
params: {
|
||||
entity: "user_archive",
|
||||
args: {
|
||||
export_user_id: user.id,
|
||||
},
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Jobs::ExportUserArchive.jobs.size).to eq(1)
|
||||
|
||||
job_data = Jobs::ExportUserArchive.jobs.first["args"].first
|
||||
expect(job_data["user_id"]).to eq(user.id)
|
||||
end
|
||||
|
||||
it "correctly logs the entity export" do
|
||||
post "/export_csv/export_entity.json", params: { entity: "user_list" }
|
||||
|
||||
@@ -84,9 +135,19 @@ RSpec.describe ExportCsvController do
|
||||
expect(response.status).to eq(400)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#latest_user_archive" do
|
||||
it "allows an admin to view another user's archive" do
|
||||
export = generate_exports(user)
|
||||
get "/export_csv/latest_user_archive/#{user.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["user_export"]["id"]).to eq(export.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "while logged in as a moderator" do
|
||||
fab!(:user)
|
||||
fab!(:moderator)
|
||||
|
||||
before { sign_in(moderator) }
|
||||
@@ -114,6 +175,18 @@ RSpec.describe ExportCsvController do
|
||||
expect(job_data["user_id"]).to eq(moderator.id)
|
||||
end
|
||||
|
||||
it "does not allow moderators to export another user's archive" do
|
||||
post "/export_csv/export_entity.json",
|
||||
params: {
|
||||
entity: "user_archive",
|
||||
args: {
|
||||
export_user_id: user.id,
|
||||
},
|
||||
}
|
||||
expect(response.status).to eq(422)
|
||||
expect(Jobs::ExportUserArchive.jobs.size).to eq(0)
|
||||
end
|
||||
|
||||
it "allows moderator to export other entities" do
|
||||
post "/export_csv/export_entity.json", params: { entity: "staff_action" }
|
||||
expect(response.status).to eq(200)
|
||||
@@ -124,5 +197,36 @@ RSpec.describe ExportCsvController do
|
||||
expect(job_data["user_id"]).to eq(moderator.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#latest_user_archive" do
|
||||
it "does not allow a moderator to view another user's archive" do
|
||||
generate_exports(user)
|
||||
get "/export_csv/latest_user_archive/#{user.id}.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def generate_exports(user)
|
||||
csv_file_1 = Fabricate(:upload, created_at: 1.day.ago)
|
||||
topic_1 = Fabricate(:topic, created_at: 1.day.ago)
|
||||
Fabricate(:post, topic: topic_1)
|
||||
UserExport.create!(
|
||||
file_name: "test",
|
||||
user: user,
|
||||
upload_id: csv_file_1.id,
|
||||
topic_id: topic_1.id,
|
||||
created_at: 1.day.ago,
|
||||
)
|
||||
|
||||
csv_file_2 = Fabricate(:upload, created_at: 12.hours.ago)
|
||||
topic_2 = Fabricate(:topic, created_at: 12.hours.ago)
|
||||
UserExport.create!(
|
||||
file_name: "test2",
|
||||
user: user,
|
||||
upload_id: csv_file_2.id,
|
||||
topic_id: topic_2.id,
|
||||
created_at: 12.hours.ago,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
31
spec/serializers/user_export_serializer_spec.rb
Normal file
31
spec/serializers/user_export_serializer_spec.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe UserExportSerializer do
|
||||
subject(:serializer) { UserExportSerializer.new(user_export, root: false) }
|
||||
|
||||
fab!(:user_export) do
|
||||
user = Fabricate(:user)
|
||||
csv_file_1 = Fabricate(:upload, created_at: 1.day.ago)
|
||||
topic_1 = Fabricate(:topic, created_at: 1.day.ago)
|
||||
Fabricate(:post, topic: topic_1)
|
||||
UserExport.create!(
|
||||
file_name: "test",
|
||||
user: user,
|
||||
upload_id: csv_file_1.id,
|
||||
topic_id: topic_1.id,
|
||||
created_at: 1.day.ago,
|
||||
)
|
||||
end
|
||||
|
||||
it "should render without errors" do
|
||||
json_data = JSON.parse(serializer.to_json)
|
||||
|
||||
expect(json_data["id"]).to eql user_export.id
|
||||
expect(json_data["filename"]).to eql user_export.upload.original_filename
|
||||
expect(json_data["uri"]).to eql user_export.upload.short_path
|
||||
expect(json_data["filesize"]).to eql user_export.upload.filesize
|
||||
expect(json_data["extension"]).to eql user_export.upload.extension
|
||||
expect(json_data["retain_hours"]).to eql user_export.retain_hours
|
||||
expect(json_data["human_filesize"]).to eql user_export.upload.human_filesize
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user