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 "user.invited.days_visited"}}
{{html-safe model.days_visited}}
+
+
{{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"] },