From 5ec5fbd7ba029bfd7e918c02ed5af993d80dfe1b Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 31 Aug 2020 15:26:51 -0700 Subject: [PATCH] User export improvements 2 (#10560) * FEATURE: Use predictable filenames inside the user archive export * FEATURE: Include badges in user archive export * FEATURE: Add user_visits table to the user archive export --- app/jobs/regular/export_user_archive.rb | 47 ++++++++++++++++++- spec/jobs/export_user_archive_spec.rb | 61 ++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/app/jobs/regular/export_user_archive.rb b/app/jobs/regular/export_user_archive.rb index 4990c43cd27..75b8e4ae9ff 100644 --- a/app/jobs/regular/export_user_archive.rb +++ b/app/jobs/regular/export_user_archive.rb @@ -13,13 +13,17 @@ module Jobs COMPONENTS ||= %w( user_archive user_archive_profile + badges category_preferences + visits ) HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( user_archive: ['topic_title', 'categories', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'], user_archive_profile: ['location', 'website', 'bio', 'views'], + badges: ['badge_id', 'badge_name', 'granted_at', 'post_id', 'seq', 'granted_manually', 'notification_id', 'featured_rank'], category_preferences: ['category_id', 'category_names', 'notification_level', 'dismiss_new_timestamp'], + visits: ['visited_at', 'posts_read', 'mobile', 'time_read'], ) def execute(args) @@ -36,13 +40,13 @@ module Jobs if respond_to? filename_method h[:filename] = public_send(filename_method) else - h[:filename] = "#{name}-#{@current_user.username}-#{@timestamp}" + h[:filename] = name end components.push(h) end export_title = 'user_archive'.titleize - filename = components.first[:filename] + filename = "user_archive-#{@current_user.username}-#{@timestamp}" user_export = UserExport.create(file_name: filename, user_id: @current_user.id) filename = "#{filename}-#{user_export.id}" @@ -126,6 +130,29 @@ module Jobs end end + def badges_export + return enum_for(:badges_export) unless block_given? + + UserBadge + .where(user_id: @current_user.id) + .joins(:badge) + .select(:badge_id, :granted_at, :post_id, :seq, :granted_by_id, :notification_id, :featured_rank) + .order(:granted_at) + .each do |ub| + yield [ + ub.badge_id, + ub.badge.display_name, + ub.granted_at, + ub.post_id, + ub.seq, + # Hide the admin's identity, simply indicate human or system + User.human_user_id?(ub.granted_by_id), + ub.notification_id, + ub.featured_rank, + ] + end + end + def category_preferences_export return enum_for(:category_preferences_export) unless block_given? @@ -142,6 +169,22 @@ module Jobs end end + def visits_export + return enum_for(:visits_export) unless block_given? + + UserVisit + .where(user_id: @current_user.id) + .order(visited_at: :asc) + .each do |uv| + yield [ + uv.visited_at, + uv.posts_read, + uv.mobile, + uv.time_read, + ] + end + 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'] diff --git a/spec/jobs/export_user_archive_spec.rb b/spec/jobs/export_user_archive_spec.rb index 35a51ead004..8519f9d0a21 100644 --- a/spec/jobs/export_user_archive_spec.rb +++ b/spec/jobs/export_user_archive_spec.rb @@ -73,8 +73,8 @@ describe Jobs::ExportUserArchive do end expect(files.size).to eq(Jobs::ExportUserArchive::COMPONENTS.length) - expect(files.find { |f| f.match 'user_archive-john_doe-' }).to_not be_nil - expect(files.find { |f| f.match 'user_archive_profile-john_doe-' }).to_not be_nil + expect(files.find { |f| f == 'user_archive.csv' }).to_not be_nil + expect(files.find { |f| f == 'category_preferences.csv' }).to_not be_nil end end @@ -149,6 +149,37 @@ describe Jobs::ExportUserArchive do end end + context 'badges' do + let(:component) { 'badges' } + + let(:admin) { Fabricate(:admin) } + let(:badge1) { Fabricate(:badge) } + let(:badge2) { Fabricate(:badge, multiple_grant: true) } + let(:badge3) { Fabricate(:badge, multiple_grant: true) } + let(:day_ago) { 1.day.ago } + + it 'properly includes badge records' do + grant_start = Time.now.utc + BadgeGranter.grant(badge1, user) + BadgeGranter.grant(badge2, user) + BadgeGranter.grant(badge2, user, granted_by: admin) + BadgeGranter.grant(badge3, user, post_id: Fabricate(:post).id) + BadgeGranter.grant(badge3, user, post_id: Fabricate(:post).id) + BadgeGranter.grant(badge3, user, post_id: Fabricate(:post).id) + + data, csv_out = make_component_csv + expect(data.length).to eq(6) + + expect(data[0]['badge_id']).to eq(badge1.id.to_s) + expect(data[0]['badge_name']).to eq(badge1.display_name) + expect(data[0]['featured_rank']).to_not eq('') + expect(DateTime.parse(data[0]['granted_at'])).to be >= DateTime.parse(grant_start.to_s) + expect(data[2]['granted_manually']).to eq('true') + expect(Post.find(data[3]['post_id'])).to_not be_nil + end + + end + context 'category_preferences' do let(:component) { 'category_preferences' } @@ -201,4 +232,30 @@ describe Jobs::ExportUserArchive do end end + context 'visits' do + let(:component) { 'visits' } + let(:user2) { Fabricate(:user) } + + it 'correctly exports the UserVisit table' do + freeze_time '2017-03-01 12:00' + + UserVisit.create(user_id: user.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 10) + UserVisit.create(user_id: user.id, visited_at: 2.days.ago, posts_read: 2, mobile: false, time_read: 20) + UserVisit.create(user_id: user.id, visited_at: 1.week.ago, posts_read: 3, mobile: true, time_read: 30) + UserVisit.create(user_id: user.id, visited_at: 1.year.ago, posts_read: 4, mobile: false, time_read: 40) + UserVisit.create(user_id: user2.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 50) + + data, csv_out = make_component_csv + + # user2's data is not mixed in + expect(data.length).to eq(4) + expect(data.find { |r| r['time_read'] == 50 }).to be_nil + + expect(data[0]['visited_at']).to eq('2016-03-01') + expect(data[0]['posts_read']).to eq('4') + expect(data[0]['time_read']).to eq('40') + expect(data[1]['mobile']).to eq('true') + expect(data[3]['visited_at']).to eq('2017-03-01') + end + end end