diff --git a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
index 517f0c0386e..b9492e30af4 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-user-index.js
@@ -121,6 +121,11 @@ export default Controller.extend(CanCheckEmails, {
}
},
+ @discourseComputed("model.username")
+ postEditsByEditorFilter(username) {
+ return { editor: username };
+ },
+
groupAdded(added) {
this.model
.groupAdded(added)
diff --git a/app/assets/javascripts/admin/addon/templates/user-index.hbs b/app/assets/javascripts/admin/addon/templates/user-index.hbs
index 41da0bff3dd..17350dc1f0f 100644
--- a/app/assets/javascripts/admin/addon/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/addon/templates/user-index.hbs
@@ -641,6 +641,20 @@
+
{{i18n "admin.user.post_edits_count" }}
+
+ {{if (gt model.post_edits_count 0) model.post_edits_count "0"}}
+
+
+ {{#if (gt model.post_edits_count 0) }}
+ {{#link-to "adminReports.show" "post_edits" (query-params filters=postEditsByEditorFilter) class="btn btn-icon"}}
+ {{d-icon "far-eye"}}
+ {{i18n "admin.user.view_edits"}}
+ {{/link-to}}
+ {{/if}}
+
+
{{#if model.single_sign_on_record}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js b/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js
index 296b8765057..3c399d2a819 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/admin-user-index-test.js
@@ -3,7 +3,7 @@ import {
exists,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
-import { click, fillIn, visit } from "@ember/test-helpers";
+import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import { test } from "qunit";
@@ -63,6 +63,40 @@ acceptance("Admin - User Index", function (needs) {
);
});
+ test("shows the number of post edits", async function (assert) {
+ await visit("/admin/users/1/eviltrout");
+
+ assert.equal(queryAll(".post-edits-count .value").text().trim(), "6");
+
+ assert.ok(
+ exists(".post-edits-count .controls .btn.btn-icon"),
+ "View edits button exists"
+ );
+ });
+
+ test("a link to view post edits report exists", async function (assert) {
+ await visit("/admin/users/1/eviltrout");
+
+ let filter = encodeURIComponent('{"editor":"eviltrout"}');
+
+ await click(".post-edits-count .controls .btn.btn-icon");
+
+ assert.equal(
+ currentURL(),
+ `/admin/reports/post_edits?filters=${filter}`,
+ "it redirects to the right admin report"
+ );
+ });
+
+ test("hides the 'view Edits' button if the count is zero", async function (assert) {
+ await visit("/admin/users/2/sam");
+
+ assert.ok(
+ !exists(".post-edits-count .controls .btn.btn-icon"),
+ "View Edits button not present"
+ );
+ });
+
test("will clear unsaved groups when switching user", async function (assert) {
await visit("/admin/users/2/sam");
diff --git a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
index 123eaa9c217..be32ddfcdf3 100644
--- a/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
+++ b/app/assets/javascripts/discourse/tests/helpers/create-pretender.js
@@ -746,6 +746,7 @@ export function applyDefaultHandlers(pretender) {
username: "eviltrout",
email: "eviltrout@example.com",
admin: true,
+ post_edits_count: 6,
});
});
diff --git a/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js b/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js
index df9a581b73b..b88b216dd1d 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/admin-report-test.js
@@ -166,6 +166,17 @@ discourseModule("Integration | Component | admin-report", function (hooks) {
},
});
+ componentTest("post edits", {
+ template: hbs`{{admin-report dataSourceName='post_edits'}}`,
+
+ test(assert) {
+ assert.ok(
+ exists(".admin-report.post-edits"),
+ "it displays the post edits report"
+ );
+ },
+ });
+
componentTest("not found", {
template: hbs`{{admin-report dataSourceName='not_found'}}`,
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss
index 4eeb0811424..45dac3e0050 100644
--- a/app/assets/stylesheets/common/admin/customize.scss
+++ b/app/assets/stylesheets/common/admin/customize.scss
@@ -1,4 +1,4 @@
-// Customise area
+// Customize area
.email-template {
input {
diff --git a/app/models/concerns/reports/post_edits.rb b/app/models/concerns/reports/post_edits.rb
index 21ba8665d18..554fea563af 100644
--- a/app/models/concerns/reports/post_edits.rb
+++ b/app/models/concerns/reports/post_edits.rb
@@ -6,6 +6,7 @@ module Reports::PostEdits
class_methods do
def report_post_edits(report)
category_id, include_subcategories = report.add_category_filter
+ editor_username = report.filters['editor']
report.modes = [:table]
@@ -89,7 +90,12 @@ module Reports::PostEdits
end
end
- builder.where("editor.id > 0 AND editor.id != author.id")
+ if editor_username
+ builder.where("editor.username = ?", editor_username)
+ else
+ builder.where("editor.id > 0 AND editor.id != author.id")
+ end
+
builder.where("pr.created_at >= :start_date", start_date: report.start_date)
builder.where("pr.created_at <= :end_date", end_date: report.end_date)
diff --git a/app/models/post_revision.rb b/app/models/post_revision.rb
index 4557f0f4daf..9d9d48c3195 100644
--- a/app/models/post_revision.rb
+++ b/app/models/post_revision.rb
@@ -59,4 +59,4 @@ end
#
# index_post_revisions_on_post_id (post_id)
# index_post_revisions_on_post_id_and_number (post_id,number)
-#
+# index_post_revisions_on_user_id (user_id)
diff --git a/app/models/user.rb b/app/models/user.rb
index 4f807458db6..5204aff66cc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -433,7 +433,6 @@ class User < ActiveRecord::Base
end
def created_topic_count
- stat = user_stat || create_user_stat
stat.topic_count
end
@@ -894,10 +893,17 @@ class User < ActiveRecord::Base
end
def post_count
- stat = user_stat || create_user_stat
stat.post_count
end
+ def post_edits_count
+ stat.post_edits_count
+ end
+
+ def increment_post_edits_count
+ stat.increment!(:post_edits_count)
+ end
+
def flags_given_count
PostAction.where(user_id: id, post_action_type_id: PostActionType.flag_types_without_custom.values).count
end
@@ -1468,9 +1474,7 @@ class User < ActiveRecord::Base
end
def create_user_stat
- stat = UserStat.new(new_since: Time.now)
- stat.user_id = id
- stat.save!
+ UserStat.create!(new_since: Time.zone.now, user_id: id)
end
def create_user_option
@@ -1659,6 +1663,10 @@ class User < ActiveRecord::Base
private
+ def stat
+ user_stat || create_user_stat
+ end
+
def trigger_user_automatic_group_refresh
if !staged
Group.user_trust_level_change!(id, trust_level)
diff --git a/app/models/user_stat.rb b/app/models/user_stat.rb
index 045dcaca703..33e01183fdf 100644
--- a/app/models/user_stat.rb
+++ b/app/models/user_stat.rb
@@ -321,4 +321,4 @@ end
# first_unread_pm_at :datetime not null
# digest_attempted_at :datetime
# draft_count :integer default(0), not null
-#
+# post_edits_count :integer
diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb
index b1c4c5ba72c..ed99812b4fa 100644
--- a/app/serializers/admin_detailed_user_serializer.rb
+++ b/app/serializers/admin_detailed_user_serializer.rb
@@ -12,6 +12,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:like_given_count,
:post_count,
:topic_count,
+ :post_edits_count,
:flags_given_count,
:flags_received_count,
:private_topics_count,
diff --git a/config/dev_defaults.yml b/config/dev_defaults.yml
index 4c5fbd6eaf8..978477e4860 100644
--- a/config/dev_defaults.yml
+++ b/config/dev_defaults.yml
@@ -14,6 +14,8 @@ group:
post:
include_images: false
max_likes_count: 10
+post_revisions:
+ count: 50
tag:
count: 30
topic:
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 34998f25d8b..475eba1919e 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4855,6 +4855,8 @@ en:
delete_all_posts: "Delete all posts"
delete_posts_progress: "Deleting posts..."
delete_posts_failed: "There was a problem deleting the posts."
+ post_edits: "Post Edits"
+ view_edits: "View Edits"
penalty_post_actions: "What would you like to do with the associated post?"
penalty_post_delete: "Delete the post"
penalty_post_delete_replies: "Delete the post + any replies"
@@ -4913,6 +4915,7 @@ en:
approve_success: "User approved and email sent with activation instructions."
approve_bulk_success: "Success! All selected users have been approved and notified."
time_read: "Read Time"
+ post_edits_count: "Post Edits"
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."
anonymize_yes: "Yes, anonymize this account"
diff --git a/db/migrate/20210708035525_add_user_id_index_to_post_revisions.rb b/db/migrate/20210708035525_add_user_id_index_to_post_revisions.rb
new file mode 100644
index 00000000000..410d451a957
--- /dev/null
+++ b/db/migrate/20210708035525_add_user_id_index_to_post_revisions.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUserIdIndexToPostRevisions < ActiveRecord::Migration[6.1]
+ def change
+ add_index :post_revisions, :user_id
+ end
+end
diff --git a/db/migrate/20210708035538_add_post_edits_count_to_user_stats.rb b/db/migrate/20210708035538_add_post_edits_count_to_user_stats.rb
new file mode 100644
index 00000000000..502783fcae4
--- /dev/null
+++ b/db/migrate/20210708035538_add_post_edits_count_to_user_stats.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class AddPostEditsCountToUserStats < ActiveRecord::Migration[6.1]
+ disable_ddl_transaction!
+ BATCH_SIZE = 30_000
+
+ def up
+ add_column :user_stats, :post_edits_count, :integer
+
+ loop do
+ count = DB.exec(<<~SQL, batch_size: BATCH_SIZE)
+ UPDATE user_stats us
+ SET post_edits_count = editor.edits_count
+ FROM (
+ SELECT COUNT(editor.id) AS edits_count, editor.id AS id
+ FROM post_revisions pr JOIN users editor ON editor.id = pr.user_id
+ JOIN user_stats us ON us.user_id = editor.id
+ WHERE us.post_edits_count IS NULL AND pr.user_id IS NOT NULL
+ GROUP BY editor.id
+ LIMIT :batch_size
+ ) editor
+ WHERE editor.id = us.user_id;
+ SQL
+ break if count == 0
+ end
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/lib/discourse_dev/post.rb b/lib/discourse_dev/post.rb
index 76c895406ba..ed2e2f65dbb 100644
--- a/lib/discourse_dev/post.rb
+++ b/lib/discourse_dev/post.rb
@@ -106,5 +106,9 @@ module DiscourseDev
puts "Done!"
end
+ def self.random
+ super(::Post)
+ end
+
end
end
diff --git a/lib/discourse_dev/post_revision.rb b/lib/discourse_dev/post_revision.rb
new file mode 100644
index 00000000000..7c1f1da644c
--- /dev/null
+++ b/lib/discourse_dev/post_revision.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'discourse_dev/record'
+require 'faker'
+
+module DiscourseDev
+ class PostRevision < Record
+
+ def initialize
+ super(::PostRevision, DiscourseDev.config.post_revisions[:count])
+ end
+
+ def create!
+ data = { raw: Faker::DiscourseMarkdown.sandwich(sentences: 5) }
+
+ ::PostRevisor.new(Post.random).revise!(User.random, data)
+ end
+
+ def populate!
+ @count.times { create! }
+ end
+ end
+end
diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb
index 1c51d75548e..eee848607e7 100644
--- a/lib/post_revisor.rb
+++ b/lib/post_revisor.rb
@@ -415,6 +415,8 @@ class PostRevisor
@post.link_post_uploads
@post.save_reply_relationships
+ @editor.increment_post_edits_count if @post_successfully_saved
+
# post owner changed
if prev_owner && new_owner && prev_owner != new_owner
likes = UserAction.where(target_post_id: @post.id)
diff --git a/lib/tasks/populate.rake b/lib/tasks/populate.rake
index 81a3bbae06c..6197bb684b8 100644
--- a/lib/tasks/populate.rake
+++ b/lib/tasks/populate.rake
@@ -25,6 +25,11 @@ task 'topics:populate' => ['db:load_config'] do |_, args|
DiscourseDev::Topic.populate!
end
+desc 'Create post revisions'
+task 'post_revisions:populate' => ['db:load_config'] do |_, args|
+ DiscourseDev::PostRevision.populate!
+end
+
desc 'Add replies to a topic'
task 'replies:populate', [:topic_id, :count] => ['db:load_config'] do |_, args|
DiscourseDev::Post.add_replies!(args)
diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb
index d3b0f229cd8..fb748471aba 100644
--- a/spec/components/post_revisor_spec.rb
+++ b/spec/components/post_revisor_spec.rb
@@ -606,6 +606,12 @@ describe PostRevisor do
expect(post.topic.word_count).to eq(5)
end
+ it 'increases the post_edits stat count' do
+ expect do
+ subject.revise!(post.user, { raw: "This is a new revision" })
+ end.to change { post.user.user_stat.post_edits_count.to_i }.by(1)
+ end
+
context 'second poster posts again quickly' do
it 'is a grace period edit, because the second poster posted again quickly' do
diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb
index 1c662889fb6..6c5d22d6129 100644
--- a/spec/models/report_spec.rb
+++ b/spec/models/report_spec.rb
@@ -578,6 +578,37 @@ describe Report do
expect(row[:topic_id]).to eq(post.topic.id)
end
end
+
+ context "with editor filter" do
+ fab!(:posts) { Fabricate.times(3, :post) }
+
+ fab!(:editor_with_two_edits) do
+ Fabricate(:user).tap do |user|
+ 2.times do |i|
+ posts[i].revise(user, { raw: "edit #{i + 1}" })
+ end
+ end
+ end
+
+ fab!(:editor_with_one_edit) do
+ Fabricate(:user).tap do |user|
+ posts.last.revise(user, { raw: "edit 3" })
+ end
+ end
+
+ let(:report_with_one_edit) do
+ Report.find('post_edits', { filters: { 'editor' => editor_with_one_edit.username } })
+ end
+
+ let(:report_with_two_edits) do
+ Report.find('post_edits', { filters: { 'editor' => editor_with_two_edits.username } })
+ end
+
+ it "returns a report for a given editor" do
+ expect(report_with_one_edit.data.count).to be(1)
+ expect(report_with_two_edits.data.count).to be(2)
+ end
+ end
end
describe 'moderator activity' do
diff --git a/spec/requests/api/schemas/json/admin_user_response.json b/spec/requests/api/schemas/json/admin_user_response.json
index aa2e04b80dd..fdaa7dc52ca 100644
--- a/spec/requests/api/schemas/json/admin_user_response.json
+++ b/spec/requests/api/schemas/json/admin_user_response.json
@@ -147,6 +147,12 @@
"silence_reason": {
"type": ["string", "null"]
},
+ "post_edits_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
"primary_group_id": {
"type": ["string", "null"]
},