User export improvements 2 (#10560)

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

diff --git a/app/jobs/regular/export_user_archive.rb b/app/jobs/regular/export_user_archive.rb
index 4990c43..75b8e4a 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 35a51ea..8519f9d 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

GitHub sha: 5ec5fbd7

This commit appears in #10560 which was approved by eviltrout. It was merged by riking.