FEATURE: Add `Top Uploads` report (#6825)

FEATURE: Add Top Uploads report (#6825)

Co-Authored-By: I am very Pro-Grammer. khalilovcmded@users.noreply.github.com

diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6
index 6fc812f..68c8281 100644
--- a/app/assets/javascripts/admin/models/report.js.es6
+++ b/app/assets/javascripts/admin/models/report.js.es6
@@ -272,6 +272,7 @@ const Report = Discourse.Model.extend({
           if (type === "seconds") return this._secondsLabel(value);
           if (type === "link") return this._linkLabel(label.properties, row);
           if (type === "percent") return this._percentLabel(value);
+          if (type === "bytes") return this._bytesLabel(value);
           if (type === "number") {
             return this._numberLabel(value, opts);
           }
@@ -381,6 +382,13 @@ const Report = Discourse.Model.extend({
     };
   },
 
+  _bytesLabel(value) {
+    return {
+      value,
+      formatedValue: I18n.toHumanSize(value)
+    };
+  },
+
   _dateLabel(value, date, format = "LL") {
     return {
       value,
diff --git a/app/models/report.rb b/app/models/report.rb
index 8ddd46e..82f23ca 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -1433,6 +1433,72 @@ class Report
     }
   end
 
+  def self.report_top_uploads(report)
+    report.modes = [:table]
+
+    report.labels = [
+      {
+        type: :link,
+        properties: [
+          :file_url,
+          :file_name,
+        ],
+        title: I18n.t("reports.top_uploads.labels.filename")
+      },
+      {
+        type: :user,
+        properties: {
+          username: :author_username,
+          id: :author_id,
+          avatar: :author_avatar_template,
+        },
+        title: I18n.t("reports.top_uploads.labels.author")
+      },
+      {
+        type: :text,
+        property: :extension,
+        title: I18n.t("reports.top_uploads.labels.extension")
+      },
+      {
+        type: :bytes,
+        property: :filesize,
+        title: I18n.t("reports.top_uploads.labels.filesize")
+      },
+    ]
+
+    report.data = []
+
+    sql = <<~SQL
+    SELECT
+    u.id as user_id,
+    u.username,
+    u.uploaded_avatar_id,
+    up.filesize,
+    up.original_filename,
+    up.extension,
+    up.url
+    FROM uploads up
+    JOIN users u
+    ON u.id = up.user_id
+    WHERE up.created_at >= '#{report.start_date}' AND up.created_at <= '#{report.end_date}'
+    ORDER BY up.filesize DESC
+    LIMIT #{report.limit || 250}
+    SQL
+
+    DB.query(sql).each do |row|
+      data = {}
+      data[:author_id] = row.user_id
+      data[:author_username] = row.username
+      data[:author_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id)
+      data[:filesize] = row.filesize
+      data[:extension] = row.extension
+      data[:file_url] = Discourse.store.cdn_url(row.url)
+      data[:file_name] = row.original_filename.truncate(25)
+
+      report.data << data
+    end
+  end
+
   DiscourseEvent.on(:site_setting_saved) do |site_setting|
     if ["backup_location", "s3_backup_bucket"].include?(site_setting.name.to_s)
       clear_cache(:storage_stats)
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 532a1c4..4508de6 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1171,6 +1171,14 @@ en:
         location: Location
         login_at: Login at
       description: "List of admin and moderator login times with locations."
+    top_uploads:
+      title: "Top Uploads"
+      labels:
+        filename: Filename
+        extension: Extension
+        author: Author
+        filesize: File size
+      description: "List all uploads by extension, filesize and author."
 
   dashboard:
     rails_env_warning: "Your server is running in %{env} mode."
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index 5866905..3001a64 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -1028,4 +1028,45 @@ describe Report do
       end
     end
   end
+
+  describe "report_top_uploads" do
+    let(:report) { Report.find("top_uploads") }
+    let(:tarek) { Fabricate(:admin, username: "tarek") }
+    let(:khalil) { Fabricate(:admin, username: "khalil") }
+
+    context "with data" do
+      let!(:tarek_upload) do
+        Fabricate(:upload, user: tarek,
+                           url: "/uploads/default/original/1X/tarek.jpg",
+                           extension: "jpg",
+                           original_filename: "tarek.jpg",
+                           filesize: 1000)
+      end
+      let!(:khalil_upload) do
+        Fabricate(:upload, user: khalil,
+                           url: "/uploads/default/original/1X/khalil.png",
+                           extension: "png",
+                           original_filename: "khalil.png",
+                           filesize: 2000)
+      end
+
+      it "works" do
+        expect(report.data.length).to eq(2)
+        expect_row_to_be_equal(report.data[0], khalil, khalil_upload)
+        expect_row_to_be_equal(report.data[1], tarek, tarek_upload)
+      end
+
+      def expect_row_to_be_equal(row, user, upload)
+        expect(row[:author_id]).to eq(user.id)
+        expect(row[:author_username]).to eq(user.username)
+        expect(row[:author_avatar_template]).to eq(User.avatar_template(user.username, user.uploaded_avatar_id))
+        expect(row[:filesize]).to eq(upload.filesize)
+        expect(row[:extension]).to eq(upload.extension)
+        expect(row[:file_url]).to eq(Discourse.store.cdn_url(upload.url))
+        expect(row[:file_name]).to eq(upload.original_filename.truncate(25))
+      end
+    end
+
+    include_examples "no data"
+  end
 end
diff --git a/test/javascripts/models/report-test.js.es6 b/test/javascripts/models/report-test.js.es6
index f3117d3..3f1dd7a 100644
--- a/test/javascripts/models/report-test.js.es6
+++ b/test/javascripts/models/report-test.js.es6
@@ -404,7 +404,8 @@ QUnit.test("computed labels", assert => {
       topic_id: 2,
       topic_title: "Test topic",
       post_number: 3,
-      post_raw: "This is the beginning of"
+      post_raw: "This is the beginning of",
+      filesize: 582641
     }
   ];
 
@@ -437,7 +438,8 @@ QUnit.test("computed labels", assert => {
         truncated_raw: "post_raw"
       },
       title: "Post"
-    }
+    },
+    { type: "bytes", property: "filesize", title: "Filesize" }
   ];
 
   const report = Report.create({
@@ -516,6 +518,15 @@ QUnit.test("computed labels", assert => {
   );
   assert.equal(computedPostLabel.value, "This is the beginning of");
 
+  const filesizeLabel = computedLabels[6];
+  assert.equal(filesizeLabel.mainProperty, "filesize");
+  assert.equal(filesizeLabel.sortProperty, "filesize");
+  assert.equal(filesizeLabel.title, "Filesize");
+  assert.equal(filesizeLabel.type, "bytes");
+  const computedFilesizeLabel = filesizeLabel.compute(row);
+  assert.equal(computedFilesizeLabel.formatedValue, "569.0 KB");
+  assert.equal(computedFilesizeLabel.value, 582641);
+
   // subfolder support
   Discourse.BaseUri = "/forum";

GitHub sha: f1269fa8