diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 6fc812f349..68c82810f9 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 8ddd46e17e..82f23caec4 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 532a1c42c7..4508de60d7 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 5866905b7a..3001a647bf 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 f3117d3a3e..3f1dd7a40d 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";