mirror of
https://github.com/discourse/discourse.git
synced 2025-05-30 07:11:34 +08:00
FEATURE: Topic view stats report (#27760)
Adds a report to show the top 100 most viewed topics in a date range, combining logged in and anonymous views. Can be filtered by category. This is a followup to 527f02e99fee782f53e206f739fb4f12d63b6d2a and d1191b7f5fbe9e8ea1b645d5a61c79fa93693c0c. We are also going to be able to see this data in a new topic map, but this admin report helps to see an overview across the forum for a date range.
This commit is contained in:
75
app/models/concerns/reports/topic_view_stats.rb
Normal file
75
app/models/concerns/reports/topic_view_stats.rb
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Reports::TopicViewStats
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def report_topic_view_stats(report)
|
||||||
|
report.modes = [:table]
|
||||||
|
|
||||||
|
category_id, include_subcategories = report.add_category_filter
|
||||||
|
|
||||||
|
category_ids = include_subcategories ? Category.subcategory_ids(category_id) : [category_id]
|
||||||
|
sql = <<~SQL
|
||||||
|
SELECT
|
||||||
|
topic_view_stats.topic_id,
|
||||||
|
topics.title AS topic_title,
|
||||||
|
SUM(topic_view_stats.anonymous_views) AS total_anonymous_views,
|
||||||
|
SUM(topic_view_stats.logged_in_views) AS total_logged_in_views,
|
||||||
|
SUM(topic_view_stats.anonymous_views + topic_view_stats.logged_in_views) AS total_views
|
||||||
|
FROM topic_view_stats
|
||||||
|
INNER JOIN topics ON topics.id = topic_view_stats.topic_id
|
||||||
|
WHERE viewed_at BETWEEN :start_date AND :end_date
|
||||||
|
#{category_ids.any? ? "AND topics.category_id IN (:category_ids)" : ""}
|
||||||
|
GROUP BY topic_view_stats.topic_id, topics.title
|
||||||
|
ORDER BY total_views DESC
|
||||||
|
LIMIT 100
|
||||||
|
SQL
|
||||||
|
|
||||||
|
data =
|
||||||
|
DB.query(
|
||||||
|
sql,
|
||||||
|
start_date: report.start_date,
|
||||||
|
end_date: report.end_date,
|
||||||
|
category_ids: category_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
report.labels = [
|
||||||
|
{
|
||||||
|
type: :topic,
|
||||||
|
properties: {
|
||||||
|
title: :topic_title,
|
||||||
|
id: :topic_id,
|
||||||
|
},
|
||||||
|
title: I18n.t("reports.topic_view_stats.labels.topic"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: :total_anonymous_views,
|
||||||
|
type: :number,
|
||||||
|
title: I18n.t("reports.topic_view_stats.labels.anon_views"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: :total_logged_in_views,
|
||||||
|
type: :number,
|
||||||
|
title: I18n.t("reports.topic_view_stats.labels.logged_in_views"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: :total_views,
|
||||||
|
type: :number,
|
||||||
|
title: I18n.t("reports.topic_view_stats.labels.total_views"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
report.data =
|
||||||
|
data.map do |row|
|
||||||
|
{
|
||||||
|
topic_id: row.topic_id,
|
||||||
|
topic_title: row.topic_title,
|
||||||
|
total_anonymous_views: row.total_anonymous_views,
|
||||||
|
total_logged_in_views: row.total_logged_in_views,
|
||||||
|
total_views: row.total_views,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -51,6 +51,7 @@ class Report
|
|||||||
include Reports::TopUsersByLikesReceivedFromInferiorTrustLevel
|
include Reports::TopUsersByLikesReceivedFromInferiorTrustLevel
|
||||||
include Reports::Topics
|
include Reports::Topics
|
||||||
include Reports::TopicsWithNoResponse
|
include Reports::TopicsWithNoResponse
|
||||||
|
include Reports::TopicViewStats
|
||||||
include Reports::TrendingSearch
|
include Reports::TrendingSearch
|
||||||
include Reports::TrustLevelGrowth
|
include Reports::TrustLevelGrowth
|
||||||
include Reports::UserFlaggingRatio
|
include Reports::UserFlaggingRatio
|
||||||
|
@ -1630,6 +1630,14 @@ en:
|
|||||||
user: User
|
user: User
|
||||||
qtt_like: Likes Received
|
qtt_like: Likes Received
|
||||||
description: "Top 10 users who have had likes from a wide range of people."
|
description: "Top 10 users who have had likes from a wide range of people."
|
||||||
|
topic_view_stats:
|
||||||
|
title: "Topic View Stats"
|
||||||
|
labels:
|
||||||
|
topic: Topic
|
||||||
|
logged_in_views: Logged In
|
||||||
|
anon_views: Anonymous
|
||||||
|
total_views: Total
|
||||||
|
description: "The top 100 most viewed topics in a date range, combining logged in and anonymous views. Can be filtered by category."
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
problem:
|
problem:
|
||||||
|
8
spec/fabricators/topic_view_stat_fabricator.rb
Normal file
8
spec/fabricators/topic_view_stat_fabricator.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:topic_view_stat) do
|
||||||
|
topic { Fabricate(:topic) }
|
||||||
|
viewed_at { Time.zone.now }
|
||||||
|
anonymous_views { 1 }
|
||||||
|
logged_in_views { 1 }
|
||||||
|
end
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
RSpec.describe Report do
|
RSpec.describe Report do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:c0) { Fabricate(:category, user: user) }
|
let(:category_1) { Fabricate(:category, user: user) }
|
||||||
let(:c1) { Fabricate(:category, parent_category: c0, user: user) } # id: 2
|
let(:category_2) { Fabricate(:category, parent_category: category_1, user: user) } # id: 2
|
||||||
let(:c2) { Fabricate(:category, user: user) }
|
let(:category_3) { Fabricate(:category, user: user) }
|
||||||
|
|
||||||
shared_examples "no data" do
|
shared_examples "no data" do
|
||||||
context "with no data" do
|
context "with no data" do
|
||||||
@ -894,7 +894,8 @@ RSpec.describe Report do
|
|||||||
user = Fabricate(:user, refresh_auto_groups: true)
|
user = Fabricate(:user, refresh_auto_groups: true)
|
||||||
topic = Fabricate(:topic, user: user)
|
topic = Fabricate(:topic, user: user)
|
||||||
post0 = Fabricate(:post, topic: topic, user: user)
|
post0 = Fabricate(:post, topic: topic, user: user)
|
||||||
post1 = Fabricate(:post, topic: Fabricate(:topic, category: c1, user: user), user: user)
|
post1 =
|
||||||
|
Fabricate(:post, topic: Fabricate(:topic, category: category_2, user: user), user: user)
|
||||||
post2 = Fabricate(:post, topic: topic, user: user)
|
post2 = Fabricate(:post, topic: topic, user: user)
|
||||||
post3 = Fabricate(:post, topic: topic, user: user)
|
post3 = Fabricate(:post, topic: topic, user: user)
|
||||||
PostActionCreator.off_topic(user, post0)
|
PostActionCreator.off_topic(user, post0)
|
||||||
@ -904,13 +905,13 @@ RSpec.describe Report do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "with category filtering" do
|
context "with category filtering" do
|
||||||
let(:report) { Report.find("flags", filters: { category: c1.id }) }
|
let(:report) { Report.find("flags", filters: { category: category_2.id }) }
|
||||||
|
|
||||||
include_examples "category filtering"
|
include_examples "category filtering"
|
||||||
|
|
||||||
context "with subcategories" do
|
context "with subcategories" do
|
||||||
let(:report) do
|
let(:report) do
|
||||||
Report.find("flags", filters: { category: c0.id, include_subcategories: true })
|
Report.find("flags", filters: { category: category_1.id, include_subcategories: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "category filtering on subcategories"
|
include_examples "category filtering on subcategories"
|
||||||
@ -930,19 +931,19 @@ RSpec.describe Report do
|
|||||||
before(:each) do
|
before(:each) do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
Fabricate(:topic, user: user)
|
Fabricate(:topic, user: user)
|
||||||
Fabricate(:topic, category: c1, user: user)
|
Fabricate(:topic, category: category_2, user: user)
|
||||||
Fabricate(:topic, user: user)
|
Fabricate(:topic, user: user)
|
||||||
Fabricate(:topic, created_at: 45.days.ago, user: user)
|
Fabricate(:topic, created_at: 45.days.ago, user: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with category filtering" do
|
context "with category filtering" do
|
||||||
let(:report) { Report.find("topics", filters: { category: c1.id }) }
|
let(:report) { Report.find("topics", filters: { category: category_2.id }) }
|
||||||
|
|
||||||
include_examples "category filtering"
|
include_examples "category filtering"
|
||||||
|
|
||||||
context "with subcategories" do
|
context "with subcategories" do
|
||||||
let(:report) do
|
let(:report) do
|
||||||
Report.find("topics", filters: { category: c0.id, include_subcategories: true })
|
Report.find("topics", filters: { category: category_1.id, include_subcategories: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "category filtering on subcategories"
|
include_examples "category filtering on subcategories"
|
||||||
@ -1017,7 +1018,7 @@ RSpec.describe Report do
|
|||||||
before(:each) do
|
before(:each) do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
topic = Fabricate(:topic, user: user)
|
topic = Fabricate(:topic, user: user)
|
||||||
topic_with_category_id = Fabricate(:topic, category: c1, user: user)
|
topic_with_category_id = Fabricate(:topic, category: category_2, user: user)
|
||||||
Fabricate(:post, topic: topic, user: user)
|
Fabricate(:post, topic: topic, user: user)
|
||||||
Fabricate(:post, topic: topic_with_category_id, user: user)
|
Fabricate(:post, topic: topic_with_category_id, user: user)
|
||||||
Fabricate(:post, topic: topic, user: user)
|
Fabricate(:post, topic: topic, user: user)
|
||||||
@ -1025,13 +1026,13 @@ RSpec.describe Report do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "with category filtering" do
|
context "with category filtering" do
|
||||||
let(:report) { Report.find("posts", filters: { category: c1.id }) }
|
let(:report) { Report.find("posts", filters: { category: category_2.id }) }
|
||||||
|
|
||||||
include_examples "category filtering"
|
include_examples "category filtering"
|
||||||
|
|
||||||
context "with subcategories" do
|
context "with subcategories" do
|
||||||
let(:report) do
|
let(:report) do
|
||||||
Report.find("posts", filters: { category: c0.id, include_subcategories: true })
|
Report.find("posts", filters: { category: category_1.id, include_subcategories: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "category filtering on subcategories"
|
include_examples "category filtering on subcategories"
|
||||||
@ -1052,14 +1053,16 @@ RSpec.describe Report do
|
|||||||
|
|
||||||
before(:each) do
|
before(:each) do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
Fabricate(:topic, category: c1, user: user)
|
Fabricate(:topic, category: category_2, user: user)
|
||||||
Fabricate(:post, topic: Fabricate(:topic, user: user), user: user)
|
Fabricate(:post, topic: Fabricate(:topic, user: user), user: user)
|
||||||
Fabricate(:topic, user: user)
|
Fabricate(:topic, user: user)
|
||||||
Fabricate(:topic, created_at: 45.days.ago, user: user)
|
Fabricate(:topic, created_at: 45.days.ago, user: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with category filtering" do
|
context "with category filtering" do
|
||||||
let(:report) { Report.find("topics_with_no_response", filters: { category: c1.id }) }
|
let(:report) do
|
||||||
|
Report.find("topics_with_no_response", filters: { category: category_2.id })
|
||||||
|
end
|
||||||
|
|
||||||
include_examples "category filtering"
|
include_examples "category filtering"
|
||||||
|
|
||||||
@ -1068,7 +1071,7 @@ RSpec.describe Report do
|
|||||||
Report.find(
|
Report.find(
|
||||||
"topics_with_no_response",
|
"topics_with_no_response",
|
||||||
filters: {
|
filters: {
|
||||||
category: c0.id,
|
category: category_1.id,
|
||||||
include_subcategories: true,
|
include_subcategories: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -1089,11 +1092,11 @@ RSpec.describe Report do
|
|||||||
include_examples "with data x/y"
|
include_examples "with data x/y"
|
||||||
|
|
||||||
before(:each) do
|
before(:each) do
|
||||||
topic = Fabricate(:topic, category: c1)
|
topic = Fabricate(:topic, category: category_2)
|
||||||
post = Fabricate(:post, topic: topic)
|
post = Fabricate(:post, topic: topic)
|
||||||
PostActionCreator.like(Fabricate(:user), post)
|
PostActionCreator.like(Fabricate(:user), post)
|
||||||
|
|
||||||
topic = Fabricate(:topic, category: c2)
|
topic = Fabricate(:topic, category: category_3)
|
||||||
post = Fabricate(:post, topic: topic)
|
post = Fabricate(:post, topic: topic)
|
||||||
PostActionCreator.like(Fabricate(:user), post)
|
PostActionCreator.like(Fabricate(:user), post)
|
||||||
PostActionCreator.like(Fabricate(:user), post)
|
PostActionCreator.like(Fabricate(:user), post)
|
||||||
@ -1105,13 +1108,13 @@ RSpec.describe Report do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context "with category filtering" do
|
context "with category filtering" do
|
||||||
let(:report) { Report.find("likes", filters: { category: c1.id }) }
|
let(:report) { Report.find("likes", filters: { category: category_2.id }) }
|
||||||
|
|
||||||
include_examples "category filtering"
|
include_examples "category filtering"
|
||||||
|
|
||||||
context "with subcategories" do
|
context "with subcategories" do
|
||||||
let(:report) do
|
let(:report) do
|
||||||
Report.find("likes", filters: { category: c0.id, include_subcategories: true })
|
Report.find("likes", filters: { category: category_1.id, include_subcategories: true })
|
||||||
end
|
end
|
||||||
|
|
||||||
include_examples "category filtering on subcategories"
|
include_examples "category filtering on subcategories"
|
||||||
@ -1732,4 +1735,90 @@ RSpec.describe Report do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "topic_view_stats" do
|
||||||
|
let(:report) { Report.find("topic_view_stats") }
|
||||||
|
|
||||||
|
fab!(:topic_1) { Fabricate(:topic) }
|
||||||
|
fab!(:topic_2) { Fabricate(:topic) }
|
||||||
|
|
||||||
|
include_examples "no data"
|
||||||
|
|
||||||
|
context "with data" do
|
||||||
|
before do
|
||||||
|
freeze_time_safe
|
||||||
|
|
||||||
|
Fabricate(
|
||||||
|
:topic_view_stat,
|
||||||
|
topic: topic_1,
|
||||||
|
anonymous_views: 4,
|
||||||
|
logged_in_views: 2,
|
||||||
|
viewed_at: Time.zone.now - 5.days,
|
||||||
|
)
|
||||||
|
Fabricate(
|
||||||
|
:topic_view_stat,
|
||||||
|
topic: topic_1,
|
||||||
|
anonymous_views: 5,
|
||||||
|
logged_in_views: 18,
|
||||||
|
viewed_at: Time.zone.now - 3.days,
|
||||||
|
)
|
||||||
|
Fabricate(
|
||||||
|
:topic_view_stat,
|
||||||
|
topic: topic_2,
|
||||||
|
anonymous_views: 14,
|
||||||
|
logged_in_views: 21,
|
||||||
|
viewed_at: Time.zone.now - 5.days,
|
||||||
|
)
|
||||||
|
Fabricate(
|
||||||
|
:topic_view_stat,
|
||||||
|
topic: topic_2,
|
||||||
|
anonymous_views: 9,
|
||||||
|
logged_in_views: 13,
|
||||||
|
viewed_at: Time.zone.now - 1.days,
|
||||||
|
)
|
||||||
|
Fabricate(
|
||||||
|
:topic_view_stat,
|
||||||
|
topic: Fabricate(:topic),
|
||||||
|
anonymous_views: 1,
|
||||||
|
logged_in_views: 34,
|
||||||
|
viewed_at: Time.zone.now - 40.days,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works" do
|
||||||
|
expect(report.data.length).to eq(2)
|
||||||
|
expect(report.data[0]).to include(
|
||||||
|
topic_id: topic_2.id,
|
||||||
|
topic_title: topic_2.title,
|
||||||
|
total_anonymous_views: 23,
|
||||||
|
total_logged_in_views: 34,
|
||||||
|
total_views: 57,
|
||||||
|
)
|
||||||
|
expect(report.data[1]).to include(
|
||||||
|
topic_id: topic_1.id,
|
||||||
|
topic_title: topic_1.title,
|
||||||
|
total_anonymous_views: 9,
|
||||||
|
total_logged_in_views: 20,
|
||||||
|
total_views: 29,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with category filtering" do
|
||||||
|
let(:report) { Report.find("topic_view_stats", filters: { category: category_1.id }) }
|
||||||
|
|
||||||
|
before { topic_1.update!(category: category_1) }
|
||||||
|
|
||||||
|
it "filters topics to that category" do
|
||||||
|
expect(report.data.length).to eq(1)
|
||||||
|
expect(report.data[0]).to include(
|
||||||
|
topic_id: topic_1.id,
|
||||||
|
topic_title: topic_1.title,
|
||||||
|
total_anonymous_views: 9,
|
||||||
|
total_logged_in_views: 20,
|
||||||
|
total_views: 29,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user