FEATURE: Add post edits count to user activity (#13495)

This commit is contained in:
Jean 2021-08-02 10:15:53 -04:00 committed by GitHub
parent 7b56325f89
commit e7b8e75583
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 210 additions and 10 deletions

View File

@ -121,6 +121,11 @@ export default Controller.extend(CanCheckEmails, {
}
},
@discourseComputed("model.username")
postEditsByEditorFilter(username) {
return { editor: username };
},
groupAdded(added) {
this.model
.groupAdded(added)

View File

@ -641,6 +641,20 @@
<div class="field">{{i18n "user.invited.days_visited"}}</div>
<div class="value">{{html-safe model.days_visited}}</div>
</div>
<div class="display-row post-edits-count">
<div class="field">{{i18n "admin.user.post_edits_count" }}</div>
<div class="value">
{{if (gt model.post_edits_count 0) model.post_edits_count "0"}}
</div>
<div class="controls">
{{#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}}
</div>
</div>
</section>
{{#if model.single_sign_on_record}}

View File

@ -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");

View File

@ -746,6 +746,7 @@ export function applyDefaultHandlers(pretender) {
username: "eviltrout",
email: "eviltrout@example.com",
admin: true,
post_edits_count: 6,
});
});

View File

@ -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'}}`,

View File

@ -1,4 +1,4 @@
// Customise area
// Customize area
.email-template {
input {

View File

@ -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
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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -14,6 +14,8 @@ group:
post:
include_images: false
max_likes_count: 10
post_revisions:
count: 50
tag:
count: 30
topic:

View File

@ -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"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddUserIdIndexToPostRevisions < ActiveRecord::Migration[6.1]
def change
add_index :post_revisions, :user_id
end
end

View File

@ -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

View File

@ -106,5 +106,9 @@ module DiscourseDev
puts "Done!"
end
def self.random
super(::Post)
end
end
end

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -147,6 +147,12 @@
"silence_reason": {
"type": ["string", "null"]
},
"post_edits_count": {
"type": [
"integer",
"null"
]
},
"primary_group_id": {
"type": ["string", "null"]
},