mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 15:06:26 +08:00
UX: Update topics stats automatically (#17135)
Updates automatically data on the stats section of the topic. It will update automatically the following information: likes, replies and last reply (timestamp and user)
This commit is contained in:
parent
31a0bf11f2
commit
5840fb5c62
|
@ -1700,6 +1700,30 @@ export default Controller.extend(bufferedProperty("model"), {
|
||||||
topic.set("message_archived", true);
|
topic.set("message_archived", true);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stats": {
|
||||||
|
let updateStream = false;
|
||||||
|
["last_posted_at", "like_count", "posts_count"].forEach(
|
||||||
|
(property) => {
|
||||||
|
const value = data[property];
|
||||||
|
if (typeof value !== "undefined") {
|
||||||
|
topic.set(property, value);
|
||||||
|
updateStream = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data["last_poster"]) {
|
||||||
|
topic.details.set("last_poster", data["last_poster"]);
|
||||||
|
updateStream = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateStream) {
|
||||||
|
postStream
|
||||||
|
.triggerChangedTopicStats()
|
||||||
|
.then((firstPostId) => refresh({ id: firstPostId }));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
let callback = customPostMessageCallbacks[data.type];
|
let callback = customPostMessageCallbacks[data.type];
|
||||||
if (callback) {
|
if (callback) {
|
||||||
|
|
|
@ -872,6 +872,17 @@ export default RestModel.extend({
|
||||||
return resolved;
|
return resolved;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
triggerChangedTopicStats() {
|
||||||
|
if (this.firstPostNotLoaded) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve().then(() => {
|
||||||
|
const firstPost = this.posts.findBy("post_number", 1);
|
||||||
|
return firstPost.id;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
postForPostNumber(postNumber) {
|
postForPostNumber(postNumber) {
|
||||||
if (!this.hasPosts) {
|
if (!this.hasPosts) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import {
|
||||||
chromeTest,
|
chromeTest,
|
||||||
count,
|
count,
|
||||||
exists,
|
exists,
|
||||||
|
publishToMessageBus,
|
||||||
query,
|
query,
|
||||||
queryAll,
|
queryAll,
|
||||||
selectText,
|
selectText,
|
||||||
|
@ -12,6 +13,7 @@ import {
|
||||||
click,
|
click,
|
||||||
currentURL,
|
currentURL,
|
||||||
fillIn,
|
fillIn,
|
||||||
|
settled,
|
||||||
triggerKeyEvent,
|
triggerKeyEvent,
|
||||||
visit,
|
visit,
|
||||||
} from "@ember/test-helpers";
|
} from "@ember/test-helpers";
|
||||||
|
@ -651,3 +653,136 @@ acceptance("Navigating between topics", function (needs) {
|
||||||
assert.ok(currentURL().includes("/280"));
|
assert.ok(currentURL().includes("/280"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("Topic stats update automatically", function () {
|
||||||
|
test("Likes count updates automatically", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
const likesDisplay = query("#post_1 .topic-map .likes .number");
|
||||||
|
const oldLikes = likesDisplay.textContent;
|
||||||
|
|
||||||
|
const likesChangedFixture = {
|
||||||
|
id: 280,
|
||||||
|
type: "stats",
|
||||||
|
like_count: 999,
|
||||||
|
};
|
||||||
|
|
||||||
|
// simulate the topic like_count being changed
|
||||||
|
publishToMessageBus("/topic/280", likesChangedFixture);
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
const newLikes = likesDisplay.textContent;
|
||||||
|
|
||||||
|
assert.notEqual(
|
||||||
|
oldLikes,
|
||||||
|
newLikes,
|
||||||
|
"it updates the like count on the topic stats"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
newLikes,
|
||||||
|
likesChangedFixture.like_count,
|
||||||
|
"it updates the like count with the expected value"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const postsChangedFixture = {
|
||||||
|
id: 280,
|
||||||
|
type: "stats",
|
||||||
|
posts_count: 999,
|
||||||
|
last_posted_at: "2022-06-20T21:01:45.844Z",
|
||||||
|
last_poster: {
|
||||||
|
id: 1,
|
||||||
|
username: "test",
|
||||||
|
name: "Mr. Tester",
|
||||||
|
avatar_template: "http://www.example.com/avatar/updated_avatar.png",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test("Replies count updates automatically", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
const repliesDisplay = query("#post_1 .topic-map .replies .number");
|
||||||
|
const oldReplies = repliesDisplay.textContent;
|
||||||
|
|
||||||
|
// simulate the topic posts_count being changed
|
||||||
|
publishToMessageBus("/topic/280", postsChangedFixture);
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
const newLikes = repliesDisplay.textContent;
|
||||||
|
|
||||||
|
assert.notEqual(
|
||||||
|
oldReplies,
|
||||||
|
newLikes,
|
||||||
|
"it updates the replies count on the topic stats"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
newLikes,
|
||||||
|
postsChangedFixture.posts_count - 1, // replies = posts_count - 1
|
||||||
|
"it updates the replies count with the expected value"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Last replier avatar updates automatically", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
const avatarImg = query("#post_1 .topic-map .last-reply .avatar");
|
||||||
|
const oldAvatarTitle = avatarImg.title;
|
||||||
|
const oldAvatarSrc = avatarImg.src;
|
||||||
|
|
||||||
|
// simulate the topic posts_count being changed
|
||||||
|
publishToMessageBus("/topic/280", postsChangedFixture);
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
const newAvatarTitle = avatarImg.title;
|
||||||
|
const newAvatarSrc = avatarImg.src;
|
||||||
|
|
||||||
|
assert.notEqual(
|
||||||
|
oldAvatarTitle,
|
||||||
|
newAvatarTitle,
|
||||||
|
"it updates the last poster avatar title on the topic stats"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
newAvatarTitle,
|
||||||
|
postsChangedFixture.last_poster.name,
|
||||||
|
"it updates the last poster avatar title with the expected value"
|
||||||
|
);
|
||||||
|
assert.notEqual(
|
||||||
|
oldAvatarSrc,
|
||||||
|
newAvatarSrc,
|
||||||
|
"it updates the last poster avatar src on the topic stats"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
newAvatarSrc,
|
||||||
|
postsChangedFixture.last_poster.avatar_template,
|
||||||
|
"it updates the last poster avatar src with the expected value"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Last replied at updates automatically", async function (assert) {
|
||||||
|
await visit("/t/internationalization-localization/280");
|
||||||
|
|
||||||
|
const lastRepliedAtDisplay = query(
|
||||||
|
"#post_1 .topic-map .last-reply .relative-date"
|
||||||
|
);
|
||||||
|
const oldTime = lastRepliedAtDisplay.dataset.time;
|
||||||
|
|
||||||
|
// simulate the topic posts_count being changed
|
||||||
|
publishToMessageBus("/topic/280", postsChangedFixture);
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
const newTime = lastRepliedAtDisplay.dataset.time;
|
||||||
|
|
||||||
|
assert.notEqual(
|
||||||
|
oldTime,
|
||||||
|
newTime,
|
||||||
|
"it updates the last posted time on the topic stats"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
newTime,
|
||||||
|
new Date(postsChangedFixture.last_posted_at).getTime(),
|
||||||
|
"it updates the last posted time with the expected value"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -198,6 +198,8 @@ class Post < ActiveRecord::Base
|
||||||
# but message is safe to skip
|
# but message is safe to skip
|
||||||
return unless topic
|
return unless topic
|
||||||
|
|
||||||
|
skip_topic_stats = opts.delete(:skip_topic_stats)
|
||||||
|
|
||||||
message = {
|
message = {
|
||||||
id: id,
|
id: id,
|
||||||
post_number: post_number,
|
post_number: post_number,
|
||||||
|
@ -209,19 +211,14 @@ class Post < ActiveRecord::Base
|
||||||
}.merge(opts)
|
}.merge(opts)
|
||||||
|
|
||||||
publish_message!("/topic/#{topic_id}", message)
|
publish_message!("/topic/#{topic_id}", message)
|
||||||
|
Topic.publish_stats_to_clients!(topic.id, type) unless skip_topic_stats
|
||||||
end
|
end
|
||||||
|
|
||||||
def publish_message!(channel, message, opts = {})
|
def publish_message!(channel, message, opts = {})
|
||||||
return unless topic
|
return unless topic
|
||||||
|
|
||||||
if Topic.visible_post_types.include?(post_type)
|
if Topic.visible_post_types.include?(post_type)
|
||||||
if topic.private_message?
|
opts.merge!(topic.secure_audience_publish_messages)
|
||||||
opts[:user_ids] = User.human_users.where("admin OR moderator").pluck(:id)
|
|
||||||
opts[:user_ids] |= topic.allowed_users.pluck(:id)
|
|
||||||
opts[:user_ids] |= topic.allowed_group_users.pluck(:id)
|
|
||||||
else
|
|
||||||
opts[:group_ids] = topic.secure_group_ids
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
opts[:user_ids] = User.human_users
|
opts[:user_ids] = User.human_users
|
||||||
.where("admin OR moderator OR id = ?", user_id)
|
.where("admin OR moderator OR id = ?", user_id)
|
||||||
|
|
|
@ -783,7 +783,7 @@ class Topic < ActiveRecord::Base
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
# If a post is deleted we have to update our highest post counters
|
# If a post is deleted we have to update our highest post counters and last post information
|
||||||
def self.reset_highest(topic_id)
|
def self.reset_highest(topic_id)
|
||||||
archetype = Topic.where(id: topic_id).pluck_first(:archetype)
|
archetype = Topic.where(id: topic_id).pluck_first(:archetype)
|
||||||
|
|
||||||
|
@ -818,7 +818,16 @@ class Topic < ActiveRecord::Base
|
||||||
deleted_at IS NULL AND
|
deleted_at IS NULL AND
|
||||||
post_type <> 4
|
post_type <> 4
|
||||||
#{post_type}
|
#{post_type}
|
||||||
)
|
),
|
||||||
|
last_post_user_id = COALESCE((
|
||||||
|
SELECT user_id FROM posts
|
||||||
|
WHERE topic_id = :topic_id AND
|
||||||
|
deleted_at IS NULL AND
|
||||||
|
post_type <> 4
|
||||||
|
#{post_type}
|
||||||
|
ORDER BY created_at desc
|
||||||
|
LIMIT 1
|
||||||
|
), last_post_user_id)
|
||||||
WHERE id = :topic_id
|
WHERE id = :topic_id
|
||||||
RETURNING highest_post_number
|
RETURNING highest_post_number
|
||||||
SQL
|
SQL
|
||||||
|
@ -1805,6 +1814,49 @@ class Topic < ActiveRecord::Base
|
||||||
self.allowed_groups.where(smtp_enabled: true).first
|
self.allowed_groups.where(smtp_enabled: true).first
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def secure_audience_publish_messages
|
||||||
|
target_audience = {}
|
||||||
|
|
||||||
|
if private_message?
|
||||||
|
target_audience[:user_ids] = User.human_users.where("admin OR moderator").pluck(:id)
|
||||||
|
target_audience[:user_ids] |= allowed_users.pluck(:id)
|
||||||
|
target_audience[:user_ids] |= allowed_group_users.pluck(:id)
|
||||||
|
else
|
||||||
|
target_audience[:group_ids] = secure_group_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
target_audience
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.publish_stats_to_clients!(topic_id, type, opts = {})
|
||||||
|
topic = Topic.find_by(id: topic_id)
|
||||||
|
return unless topic.present?
|
||||||
|
|
||||||
|
case type
|
||||||
|
when :liked, :unliked
|
||||||
|
stats = { like_count: topic.like_count }
|
||||||
|
when :created, :destroyed, :deleted, :recovered
|
||||||
|
stats = { posts_count: topic.posts_count,
|
||||||
|
last_posted_at: topic.last_posted_at.as_json,
|
||||||
|
last_poster: BasicUserSerializer.new(topic.last_poster, root: false).as_json }
|
||||||
|
else
|
||||||
|
stats = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if stats
|
||||||
|
secure_audience = topic.secure_audience_publish_messages
|
||||||
|
|
||||||
|
if secure_audience[:user_ids] != [] && secure_audience[:group_ids] != []
|
||||||
|
message = stats.merge({
|
||||||
|
id: topic_id,
|
||||||
|
updated_at: Time.now,
|
||||||
|
type: :stats,
|
||||||
|
})
|
||||||
|
MessageBus.publish("/topic/#{topic_id}", message, opts.merge(secure_audience))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def invite_to_private_message(invited_by, target_user, guardian)
|
def invite_to_private_message(invited_by, target_user, guardian)
|
||||||
|
|
|
@ -633,7 +633,7 @@ class PostCreator
|
||||||
|
|
||||||
def publish
|
def publish
|
||||||
return if @opts[:import_mode] || @post.post_number == 1
|
return if @opts[:import_mode] || @post.post_number == 1
|
||||||
@post.publish_change_to_clients! :created
|
@post.publish_change_to_clients! :created, { skip_topic_stats: @post.post_number == 1 }
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_links
|
def extract_links
|
||||||
|
|
|
@ -98,6 +98,7 @@ class PostDestroyer
|
||||||
topic.update_column(:user_id, Discourse::SYSTEM_USER_ID) if !topic.user_id
|
topic.update_column(:user_id, Discourse::SYSTEM_USER_ID) if !topic.user_id
|
||||||
topic.recover!(@user) if @post.is_first_post?
|
topic.recover!(@user) if @post.is_first_post?
|
||||||
topic.update_statistics
|
topic.update_statistics
|
||||||
|
Topic.publish_stats_to_clients!(topic.id, :recovered)
|
||||||
|
|
||||||
UserActionManager.post_created(@post)
|
UserActionManager.post_created(@post)
|
||||||
DiscourseEvent.trigger(:post_recovered, @post, @opts, @user)
|
DiscourseEvent.trigger(:post_recovered, @post, @opts, @user)
|
||||||
|
@ -135,7 +136,8 @@ class PostDestroyer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@post.publish_change_to_clients! :recovered
|
# skip also publishing topic stats because they weren't updated yet
|
||||||
|
@post.publish_change_to_clients! :recovered, { skip_topic_stats: true }
|
||||||
TopicTrackingState.publish_recover(@post.topic) if @post.topic && @post.is_first_post?
|
TopicTrackingState.publish_recover(@post.topic) if @post.topic && @post.is_first_post?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -67,12 +67,28 @@ describe PostActionCreator do
|
||||||
PostActionCreator.new(user, post, like_type_id).perform
|
PostActionCreator.new(user, post, like_type_id).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
message = messages.last.data
|
message = messages.find { |msg| msg.data[:type] === :liked }.data
|
||||||
|
expect(message).to be_present
|
||||||
expect(message[:type]).to eq(:liked)
|
expect(message[:type]).to eq(:liked)
|
||||||
expect(message[:likes_count]).to eq(1)
|
expect(message[:likes_count]).to eq(1)
|
||||||
expect(message[:user_id]).to eq(user.id)
|
expect(message[:user_id]).to eq(user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'notifies updated topic stats to subscribers' do
|
||||||
|
topic = Fabricate(:topic)
|
||||||
|
post = Fabricate(:post, topic: topic)
|
||||||
|
|
||||||
|
expect(post.reload.like_count).to eq(0)
|
||||||
|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostActionCreator.new(user, post, like_type_id).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:like_count]).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not create an invalid post action' do
|
it 'does not create an invalid post action' do
|
||||||
result = PostActionCreator.new(user, nil, like_type_id).perform
|
result = PostActionCreator.new(user, nil, like_type_id).perform
|
||||||
expect(result.failed?).to eq(true)
|
expect(result.failed?).to eq(true)
|
||||||
|
|
|
@ -25,11 +25,28 @@ describe PostActionDestroyer do
|
||||||
PostActionDestroyer.destroy(user, post, :like)
|
PostActionDestroyer.destroy(user, post, :like)
|
||||||
end
|
end
|
||||||
|
|
||||||
message = messages.last.data
|
message = messages.find { |msg| msg.data[:type] === :unliked }.data
|
||||||
|
expect(message).to be_present
|
||||||
expect(message[:type]).to eq(:unliked)
|
expect(message[:type]).to eq(:unliked)
|
||||||
expect(message[:likes_count]).to eq(0)
|
expect(message[:likes_count]).to eq(0)
|
||||||
expect(message[:user_id]).to eq(user.id)
|
expect(message[:user_id]).to eq(user.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'notifies updated topic stats to subscribers' do
|
||||||
|
topic = Fabricate(:topic)
|
||||||
|
post = Fabricate(:post, topic: topic)
|
||||||
|
PostActionCreator.new(user, post, PostActionType.types[:like]).perform
|
||||||
|
|
||||||
|
expect(post.reload.like_count).to eq(1)
|
||||||
|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostActionDestroyer.destroy(user, post, :like)
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:like_count]).to eq(0)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'post action doesn’t exist' do
|
context 'post action doesn’t exist' do
|
||||||
|
|
|
@ -162,7 +162,7 @@ describe PostCreator do
|
||||||
|
|
||||||
channels = messages.map { |m| m.channel }.sort
|
channels = messages.map { |m| m.channel }.sort
|
||||||
|
|
||||||
# 2 for topic, one to notify of new topic another for tracking state
|
# 3 for topic, one to notify of new topic, one for topic stats and another for tracking state
|
||||||
expect(channels).to eq(
|
expect(channels).to eq(
|
||||||
[
|
[
|
||||||
"/new",
|
"/new",
|
||||||
|
@ -174,6 +174,7 @@ describe PostCreator do
|
||||||
"/latest",
|
"/latest",
|
||||||
"/topic/#{created_post.topic_id}",
|
"/topic/#{created_post.topic_id}",
|
||||||
"/topic/#{created_post.topic_id}",
|
"/topic/#{created_post.topic_id}",
|
||||||
|
"/topic/#{created_post.topic_id}",
|
||||||
"/user-drafts/#{admin.id}",
|
"/user-drafts/#{admin.id}",
|
||||||
"/user-drafts/#{admin.id}",
|
"/user-drafts/#{admin.id}",
|
||||||
"/user-drafts/#{admin.id}",
|
"/user-drafts/#{admin.id}",
|
||||||
|
@ -208,6 +209,9 @@ describe PostCreator do
|
||||||
draft_count = messages.find { |m| m.channel == "/user-drafts/#{p.user_id}" }
|
draft_count = messages.find { |m| m.channel == "/user-drafts/#{p.user_id}" }
|
||||||
expect(draft_count).not_to eq(nil)
|
expect(draft_count).not_to eq(nil)
|
||||||
|
|
||||||
|
topics_stats = messages.find { |m| m.channel == "/topic/#{p.topic.id}" && m.data[:type] == :stats }
|
||||||
|
expect(topics_stats).to eq(nil)
|
||||||
|
|
||||||
expect(messages.filter { |m| m.channel != "/distributed_hash" }.length).to eq(7)
|
expect(messages.filter { |m| m.channel != "/distributed_hash" }.length).to eq(7)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -851,6 +855,26 @@ describe PostCreator do
|
||||||
expect(topic.word_count).to eq(6)
|
expect(topic.word_count).to eq(6)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "publishes updates to topic stats" do
|
||||||
|
reply_timestamp = 1.day.from_now.round
|
||||||
|
|
||||||
|
# tests if messages of type :stats are published and the relevant data is fetched from the topic
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostCreator.new(
|
||||||
|
evil_trout,
|
||||||
|
raw: 'other post in topic',
|
||||||
|
topic_id: topic.id,
|
||||||
|
created_at: reply_timestamp
|
||||||
|
).create
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:posts_count]).to eq(2)
|
||||||
|
expect(stats_message.data[:last_posted_at]).to eq(reply_timestamp.as_json)
|
||||||
|
expect(stats_message.data[:last_poster]).to eq(BasicUserSerializer.new(evil_trout, root: false).as_json)
|
||||||
|
end
|
||||||
|
|
||||||
it "updates topic stats even when topic fails validation" do
|
it "updates topic stats even when topic fails validation" do
|
||||||
topic.update_columns(title: 'below 15 chars')
|
topic.update_columns(title: 'below 15 chars')
|
||||||
|
|
||||||
|
|
|
@ -1103,4 +1103,67 @@ describe PostDestroyer do
|
||||||
expect { regular_post.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
expect { regular_post.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "publishes messages to subscribers" do
|
||||||
|
# timestamps are rounded because postgres truncates the timestamp. that would cause the comparison if we compared
|
||||||
|
# these timestamps with the one read from the database
|
||||||
|
fab!(:first_post) { Fabricate(:post, created_at: 10.days.ago.round) }
|
||||||
|
fab!(:walter_white) { Fabricate(:walter_white) }
|
||||||
|
let!(:topic) { first_post.topic }
|
||||||
|
let!(:reply) { Fabricate(:post, topic: topic, created_at: 5.days.ago.round, user: coding_horror) }
|
||||||
|
let!(:expendable_reply) { Fabricate(:post, topic: topic, created_at: 2.days.ago.round, user: walter_white) }
|
||||||
|
|
||||||
|
it 'when a post is destroyed publishes updated topic stats' do
|
||||||
|
expect(topic.reload.posts_count).to eq(3)
|
||||||
|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostDestroyer.new(moderator, expendable_reply, force_destroy: true).destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { expendable_reply.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:posts_count]).to eq(2)
|
||||||
|
expect(stats_message.data[:last_posted_at]).to eq(reply.created_at.as_json)
|
||||||
|
expect(stats_message.data[:last_poster]).to eq(BasicUserSerializer.new(reply.user, root: false).as_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when a post is deleted publishes updated topic stats' do
|
||||||
|
expect(topic.reload.posts_count).to eq(3)
|
||||||
|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostDestroyer.new(moderator, expendable_reply).destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(expendable_reply.reload.deleted_at).not_to eq(nil)
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:posts_count]).to eq(2)
|
||||||
|
expect(stats_message.data[:last_posted_at]).to eq(reply.created_at.as_json)
|
||||||
|
expect(stats_message.data[:last_poster]).to eq(BasicUserSerializer.new(reply.user, root: false).as_json)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'when a post is recovered publishes update topic stats' do
|
||||||
|
expect(topic.reload.posts_count).to eq(3)
|
||||||
|
|
||||||
|
PostDestroyer.new(moderator, expendable_reply).destroy
|
||||||
|
expect(topic.reload.posts_count).to eq(2)
|
||||||
|
|
||||||
|
expendable_reply.reload
|
||||||
|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
PostDestroyer.new(admin, expendable_reply).recover
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(topic.reload.posts_count).to eq(3)
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:posts_count]).to eq(3)
|
||||||
|
expect(stats_message.data[:last_posted_at]).to eq(expendable_reply.created_at.as_json)
|
||||||
|
expect(stats_message.data[:last_poster]).to eq(BasicUserSerializer.new(expendable_reply.user, root: false).as_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1826,10 +1826,41 @@ describe Post do
|
||||||
version: post.version
|
version: post.version
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageBus.expects(:publish).once.with("/topic/#{topic.id}", message, is_a(Hash)) do |_, _, options|
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
options[:user_ids].sort == [user1.id, user2.id, user3.id].sort
|
post.publish_change_to_clients!(:created)
|
||||||
end
|
end
|
||||||
post.publish_change_to_clients!(:created)
|
|
||||||
|
created_message = messages.select { |msg| msg.data[:type] == :created }.first
|
||||||
|
expect(created_message).to be_present
|
||||||
|
expect(created_message.data).to eq(message)
|
||||||
|
expect(created_message.user_ids.sort).to eq([user1.id, user2.id, user3.id].sort)
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :created }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.user_ids.sort).to eq([user1.id, user2.id, user3.id].sort)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'also publishes topic stats' do
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
post.publish_change_to_clients!(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips publishing topic stats when requested' do
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
post.publish_change_to_clients!(:anything, { skip_topic_stats: true })
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_blank
|
||||||
|
|
||||||
|
# ensure that :skip_topic_stats did not get merged with the message
|
||||||
|
other_message = messages.select { |msg| msg.data[:type] == :anything }.first
|
||||||
|
expect(other_message).to be_present
|
||||||
|
expect(other_message.data.key?(:skip_topic_stats)).to be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3051,4 +3051,53 @@ describe Topic do
|
||||||
expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(nil)
|
expect(topic.reload.cannot_permanently_delete_reason(Fabricate(:admin))).to eq(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#publish_stats_to_clients!" do
|
||||||
|
fab!(:user1) { Fabricate(:user) }
|
||||||
|
fab!(:user2) { Fabricate(:user) }
|
||||||
|
fab!(:topic) { Fabricate(:topic, user: user1) }
|
||||||
|
fab!(:post1) { Fabricate(:post, topic: topic, user: user1) }
|
||||||
|
fab!(:post2) { Fabricate(:post, topic: topic, user: user2) }
|
||||||
|
fab!(:like1) { Fabricate(:like, post: post1, user: user2) }
|
||||||
|
|
||||||
|
it "it is triggered when a post publishes a message of type :liked or :unliked" do
|
||||||
|
[:liked, :unliked].each do |action|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
post1.publish_change_to_clients!(action)
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:like_count]).to eq(topic.like_count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "it is triggered when a post publishes a message of type :created, :destroyed, :deleted, :recovered" do
|
||||||
|
freeze_time Date.today
|
||||||
|
|
||||||
|
[:created, :destroyed, :deleted, :recovered].each do |action|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
post1.publish_change_to_clients!(action)
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_present
|
||||||
|
expect(stats_message.data[:posts_count]).to eq(topic.posts_count)
|
||||||
|
expect(stats_message.data[:last_posted_at]).to eq(topic.last_posted_at.as_json)
|
||||||
|
expect(stats_message.data[:last_poster]).to eq(BasicUserSerializer.new(topic.last_poster, root: false).as_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "it is not triggered when a post publishes an unhandled kind of message" do
|
||||||
|
[:unhandled, :unknown, :dont_care].each do |action|
|
||||||
|
messages = MessageBus.track_publish("/topic/#{topic.id}") do
|
||||||
|
post1.publish_change_to_clients!(action)
|
||||||
|
end
|
||||||
|
|
||||||
|
stats_message = messages.select { |msg| msg.data[:type] == :stats }.first
|
||||||
|
expect(stats_message).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue
Block a user