Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b1af6f717 |
@@ -1,3 +0,0 @@
|
||||
< 3.6.0.beta1-dev: ca316389b51afeb17a697efaff041e425a0deac6
|
||||
< 3.5.0.beta5-dev: cbcbbd2e3316b06c91998c7a0fd493d89ab6d2a5
|
||||
< 3.5.0.beta1-dev: b3f205375a2f12287aa3338d8adc1afa98c4bfbb
|
||||
|
||||
+52
-67
@@ -1,95 +1,80 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (8.0.3)
|
||||
activesupport (7.1.3.3)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
uri (>= 0.13.1)
|
||||
ast (2.4.3)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.3.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.4)
|
||||
drb (2.2.3)
|
||||
i18n (1.14.7)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
ast (2.4.2)
|
||||
base64 (0.2.0)
|
||||
bigdecimal (3.1.8)
|
||||
concurrent-ruby (1.2.3)
|
||||
connection_pool (2.4.1)
|
||||
drb (2.2.1)
|
||||
i18n (1.14.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.15.1)
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
minitest (5.26.0)
|
||||
parallel (1.27.0)
|
||||
parser (3.3.9.0)
|
||||
json (2.7.2)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.23.1)
|
||||
mutex_m (0.2.0)
|
||||
parallel (1.24.0)
|
||||
parser (3.3.1.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
prettier_print (1.2.1)
|
||||
prism (1.5.1)
|
||||
racc (1.8.1)
|
||||
rack (3.2.3)
|
||||
racc (1.8.0)
|
||||
rack (3.0.11)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.11.3)
|
||||
rubocop (1.81.1)
|
||||
regexp_parser (2.9.2)
|
||||
rexml (3.3.9)
|
||||
rubocop (1.64.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.47.1, < 2.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.47.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-capybara (2.22.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-discourse (3.13.3)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-capybara (2.20.0)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-discourse (3.8.0)
|
||||
activesupport (>= 6.1)
|
||||
lint_roller (>= 1.1.0)
|
||||
rubocop (>= 1.73.2)
|
||||
rubocop-capybara (>= 2.22.0)
|
||||
rubocop-factory_bot (>= 2.27.0)
|
||||
rubocop-rails (>= 2.30.3)
|
||||
rubocop-rspec (>= 3.0.1)
|
||||
rubocop-rspec_rails (>= 2.31.0)
|
||||
rubocop-factory_bot (2.27.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rails (2.33.4)
|
||||
rubocop (>= 1.59.0)
|
||||
rubocop-capybara (>= 2.0.0)
|
||||
rubocop-factory_bot (>= 2.0.0)
|
||||
rubocop-rails (>= 2.25.0)
|
||||
rubocop-rspec (>= 2.25.0)
|
||||
rubocop-factory_bot (2.25.1)
|
||||
rubocop (~> 1.41)
|
||||
rubocop-rails (2.25.0)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.75.0, < 2.0)
|
||||
rubocop-ast (>= 1.44.0, < 2.0)
|
||||
rubocop-rspec (3.7.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec_rails (2.31.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rspec (~> 3.5)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (2.29.2)
|
||||
rubocop (~> 1.40)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
rubocop-rspec_rails (~> 2.28)
|
||||
rubocop-rspec_rails (2.28.3)
|
||||
rubocop (~> 1.40)
|
||||
ruby-progressbar (1.13.0)
|
||||
securerandom (0.4.1)
|
||||
syntax_tree (6.3.0)
|
||||
syntax_tree (6.2.0)
|
||||
prettier_print (>= 1.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (3.2.0)
|
||||
unicode-emoji (~> 4.1)
|
||||
unicode-emoji (4.1.0)
|
||||
uri (1.0.4)
|
||||
unicode-display_width (2.5.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@@ -99,4 +84,4 @@ DEPENDENCIES
|
||||
syntax_tree
|
||||
|
||||
BUNDLED WITH
|
||||
2.7.2
|
||||
2.5.10
|
||||
|
||||
@@ -7,12 +7,12 @@ module ::DiscourseRewind
|
||||
requires_login
|
||||
|
||||
def show
|
||||
DiscourseRewind::FetchReports.call(service_params) do
|
||||
DiscourseRewind::Rewind::Fetch.call(service_params) do
|
||||
on_model_not_found(:year) { raise Discourse::NotFound }
|
||||
on_model_not_found(:user) { raise Discourse::NotFound }
|
||||
on_success do |reports:|
|
||||
@reports = reports
|
||||
render json: MultiJson.dump(reports), status: :ok
|
||||
render json: MultiJson.dump(reports), status: 200
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For a GitHub like calendar
|
||||
# https://docs.github.com/assets/cb-35216/mw-1440/images/help/profile/contributions-graph.webp
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class ActivityCalendar < BaseReport
|
||||
FakeData = {
|
||||
data:
|
||||
(Date.new(2024, 1, 1)..Date.new(2024, 12, 31)).map do |date|
|
||||
{ date:, post_count: rand(0..20), visited: false }
|
||||
end,
|
||||
identifier: "activity-calendar",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
calendar =
|
||||
Post
|
||||
.unscoped
|
||||
.joins(<<~SQL)
|
||||
RIGHT JOIN
|
||||
generate_series('#{date.first}', '#{date.last}', '1 day'::interval) ON
|
||||
posts.created_at::date = generate_series::date AND
|
||||
posts.user_id = #{user.id} AND
|
||||
posts.deleted_at IS NULL
|
||||
SQL
|
||||
.joins(
|
||||
"LEFT JOIN user_visits ON generate_series::date = visited_at AND user_visits.user_id = #{user.id}",
|
||||
)
|
||||
.select(
|
||||
"generate_series::date as date, count(posts.id) as post_count, COUNT(visited_at) > 0 as visited",
|
||||
)
|
||||
.group("generate_series, user_visits.id")
|
||||
.order("generate_series")
|
||||
.map { |row| { date: row.date, post_count: row.post_count, visited: row.visited } }
|
||||
|
||||
{ data: calendar, identifier: "activity-calendar" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class BaseReport < Service::ActionBase
|
||||
option :user
|
||||
option :date
|
||||
|
||||
def call
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def self.enabled?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class BestPosts < BaseReport
|
||||
def call
|
||||
best_posts =
|
||||
Post
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.where("post_number > 1")
|
||||
.order("like_count DESC NULLS LAST")
|
||||
.limit(3)
|
||||
.pluck(:post_number, :topic_id, :like_count, :reply_count, :raw, :cooked)
|
||||
|
||||
{ data: best_posts, identifier: "best-posts" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class BestTopics < BaseReport
|
||||
def call
|
||||
best_topics =
|
||||
TopTopic
|
||||
.includes(:topic)
|
||||
.references(:topic)
|
||||
.where(topic: { deleted_at: nil, created_at: date, user_id: user.id })
|
||||
.order("yearly_score DESC NULLS LAST")
|
||||
.limit(3)
|
||||
.pluck(:topic_id, :title, :excerpt, :yearly_score)
|
||||
.map do |topic_id, title, excerpt, yearly_score|
|
||||
{ topic_id: topic_id, title: title, excerpt: excerpt, yearly_score: yearly_score }
|
||||
end
|
||||
|
||||
{ data: best_topics, identifier: "best-topics" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Topics visited grouped by category
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class FavoriteCategories < BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ category_id: 1, name: "cats" },
|
||||
{ category_id: 2, name: "dogs" },
|
||||
{ category_id: 3, name: "countries" },
|
||||
{ category_id: 4, name: "management" },
|
||||
{ category_id: 5, name: "things" },
|
||||
],
|
||||
identifier: "favorite-categories",
|
||||
}
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
favorite_categories =
|
||||
TopicViewItem
|
||||
.joins(:topic)
|
||||
.joins("INNER JOIN categories ON categories.id = topics.category_id")
|
||||
.where(user: user)
|
||||
.where(viewed_at: date)
|
||||
.where(categories: { id: user.guardian.allowed_category_ids })
|
||||
.group("categories.id, categories.name")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(5)
|
||||
.pluck("categories.id, categories.name")
|
||||
.map { |category_id, name| { category_id: category_id, name: name } }
|
||||
|
||||
{ data: favorite_categories, identifier: "favorite-categories" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Topics visited grouped by tag
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class FavoriteTags < BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ tag_id: 1, name: "cats" },
|
||||
{ tag_id: 2, name: "dogs" },
|
||||
{ tag_id: 3, name: "countries" },
|
||||
{ tag_id: 4, name: "management" },
|
||||
{ tag_id: 5, name: "things" },
|
||||
],
|
||||
identifier: "favorite-tags",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
favorite_tags =
|
||||
TopicViewItem
|
||||
.joins(:topic)
|
||||
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
|
||||
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
|
||||
.where(user: user)
|
||||
.where(viewed_at: date)
|
||||
.where(tags: { id: Tag.visible(user.guardian).pluck(:id) })
|
||||
.group("tags.id, tags.name")
|
||||
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
|
||||
.limit(5)
|
||||
.pluck("tags.id, tags.name")
|
||||
.map { |tag_id, name| { tag_id: tag_id, name: name } }
|
||||
|
||||
{ data: favorite_tags, identifier: "favorite-tags" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,114 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Forum Best Friend Forever ranking
|
||||
# Score is informative only, do not show in UI
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class Fbff < BaseReport
|
||||
MAX_SUMMARY_RESULTS = 50
|
||||
LIKE_SCORE = 1
|
||||
REPLY_SCORE = 10
|
||||
|
||||
def call
|
||||
most_liked_users =
|
||||
like_query(date)
|
||||
.where(acting_user_id: user.id)
|
||||
.group(:user_id)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("user_actions.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
most_liked_by_users =
|
||||
like_query(date)
|
||||
.where(user: user)
|
||||
.group(:acting_user_id)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("acting_user_id, COUNT(*)")
|
||||
.map { |acting_user_id, count| { acting_user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
users_who_most_replied_me =
|
||||
post_query(user, date)
|
||||
.where(posts: { user_id: user.id })
|
||||
.group("replies.user_id")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("replies.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
users_i_most_replied =
|
||||
post_query(user, date)
|
||||
.where("replies.user_id = ?", user.id)
|
||||
.group("posts.user_id")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("posts.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
#TODO include chat?
|
||||
|
||||
fbffs = [
|
||||
apply_score(most_liked_users, LIKE_SCORE),
|
||||
apply_score(most_liked_by_users, LIKE_SCORE),
|
||||
apply_score(users_who_most_replied_me, REPLY_SCORE),
|
||||
apply_score(users_i_most_replied, REPLY_SCORE),
|
||||
]
|
||||
|
||||
fbff_id =
|
||||
fbffs
|
||||
.flatten
|
||||
.inject { |h1, h2| h1.merge(h2) { |_, v1, v2| v1 + v2 } }
|
||||
&.sort_by { |_, v| -v }
|
||||
&.first
|
||||
&.first
|
||||
|
||||
return if !fbff_id
|
||||
|
||||
{
|
||||
data: {
|
||||
fbff: BasicUserSerializer.new(User.find(fbff_id), root: false).as_json,
|
||||
yourself: BasicUserSerializer.new(user, root: false).as_json,
|
||||
},
|
||||
identifier: "fbff",
|
||||
}
|
||||
end
|
||||
|
||||
def post_query(user, date)
|
||||
Post
|
||||
.joins(:topic)
|
||||
.includes(:topic)
|
||||
.where(
|
||||
"posts.post_type IN (?)",
|
||||
Topic.visible_post_types(user, include_moderator_actions: false),
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number",
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN topics ON replies.topic_id = topics.id AND topics.archetype <> 'private_message'",
|
||||
)
|
||||
.joins(
|
||||
"AND replies.post_type IN (#{Topic.visible_post_types(user, include_moderator_actions: false).join(",")})",
|
||||
)
|
||||
.where("replies.created_at BETWEEN ? AND ?", date.first, date.last)
|
||||
.where("replies.user_id <> posts.user_id")
|
||||
end
|
||||
|
||||
def like_query(date)
|
||||
UserAction
|
||||
.joins(:target_topic, :target_post)
|
||||
.where(created_at: date)
|
||||
.where(action_type: UserAction::WAS_LIKED)
|
||||
end
|
||||
|
||||
def apply_score(users, score)
|
||||
users.map { |user_id, count| { user_id => count * score } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,92 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For a most user / received reactions cards
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class Reactions < BaseReport
|
||||
FakeData = {
|
||||
data: {
|
||||
post_received_reactions: {
|
||||
"open_mouth" => 2,
|
||||
"cat" => 32,
|
||||
"dog" => 34,
|
||||
"heart" => 45,
|
||||
"grinning" => 82,
|
||||
},
|
||||
post_used_reactions: {
|
||||
"open_mouth" => 2,
|
||||
"cat" => 32,
|
||||
"dog" => 34,
|
||||
"heart" => 45,
|
||||
"grinning" => 82,
|
||||
},
|
||||
chat_used_reactions: {
|
||||
"open_mouth" => 2,
|
||||
"cat" => 32,
|
||||
"dog" => 34,
|
||||
"heart" => 45,
|
||||
"grinning" => 82,
|
||||
},
|
||||
chat_received_reactions: {
|
||||
"open_mouth" => 2,
|
||||
"cat" => 32,
|
||||
"dog" => 34,
|
||||
"heart" => 45,
|
||||
"grinning" => 82,
|
||||
},
|
||||
},
|
||||
identifier: "reactions",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
data = {}
|
||||
if defined?(DiscourseReactions::Reaction)
|
||||
# This is missing heart reactions (default like)
|
||||
data[:post_used_reactions] = sort_and_limit(
|
||||
DiscourseReactions::Reaction
|
||||
.by_user(user)
|
||||
.where(created_at: date)
|
||||
.group(:reaction_value)
|
||||
.count,
|
||||
)
|
||||
|
||||
data[:post_received_reactions] = sort_and_limit(
|
||||
DiscourseReactions::Reaction
|
||||
.includes(:post)
|
||||
.where(posts: { user_id: user.id })
|
||||
.where(created_at: date)
|
||||
.group(:reaction_value)
|
||||
.count,
|
||||
)
|
||||
end
|
||||
|
||||
if SiteSetting.chat_enabled
|
||||
data[:chat_used_reactions] = sort_and_limit(
|
||||
Chat::MessageReaction.where(user: user).where(created_at: date).group(:emoji).count,
|
||||
)
|
||||
|
||||
data[:chat_received_reactions] = sort_and_limit(
|
||||
Chat::MessageReaction
|
||||
.includes(:chat_message)
|
||||
.where(chat_message: { user_id: user.id })
|
||||
.where(created_at: date)
|
||||
.group(:emoji)
|
||||
.count,
|
||||
)
|
||||
end
|
||||
|
||||
{ data:, identifier: "reactions" }
|
||||
end
|
||||
|
||||
def enabled?
|
||||
SiteSetting.discourse_reactions_enabled || SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def sort_and_limit(reactions)
|
||||
reactions.sort_by { |_, v| -v }.first(5).reverse.to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,202 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For showcasing the reading time of a user
|
||||
# Should we show book covers or just the names?
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class ReadingTime < BaseReport
|
||||
FakeData = {
|
||||
data: {
|
||||
reading_time: 2_880_000,
|
||||
book: "The Combined Cosmere works + Wheel of Time",
|
||||
isbn: "978-0812511819",
|
||||
series: true,
|
||||
},
|
||||
identifier: "reading-time",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
|
||||
book = best_book_fit(reading_time)
|
||||
|
||||
return if book.nil?
|
||||
|
||||
{
|
||||
data: {
|
||||
reading_time: reading_time,
|
||||
book: book[:title],
|
||||
isbn: book[:isbn],
|
||||
series: book[:series],
|
||||
},
|
||||
identifier: "reading-time",
|
||||
}
|
||||
end
|
||||
|
||||
def popular_books
|
||||
{
|
||||
"The Hunger Games" => {
|
||||
reading_time: 19_740,
|
||||
isbn: "978-0439023481",
|
||||
series: false,
|
||||
},
|
||||
"The Metamorphosis" => {
|
||||
reading_time: 3120,
|
||||
isbn: "978-0553213690",
|
||||
series: false,
|
||||
},
|
||||
"To Kill a Mockingbird" => {
|
||||
reading_time: 22_800,
|
||||
isbn: "978-0061120084",
|
||||
series: false,
|
||||
},
|
||||
"Pride and Prejudice" => {
|
||||
reading_time: 25_200,
|
||||
isbn: "978-1503290563",
|
||||
series: false,
|
||||
},
|
||||
"1984" => {
|
||||
reading_time: 16_800,
|
||||
isbn: "978-0451524935",
|
||||
series: false,
|
||||
},
|
||||
"The Lord of the Rings" => {
|
||||
reading_time: 108_000,
|
||||
isbn: "978-0544003415",
|
||||
series: true,
|
||||
},
|
||||
"Harry Potter and the Sorcerer's Stone" => {
|
||||
reading_time: 24_600,
|
||||
isbn: "978-0590353427",
|
||||
series: true,
|
||||
},
|
||||
"The Great Gatsby" => {
|
||||
reading_time: 12_600,
|
||||
isbn: "978-0743273565",
|
||||
series: false,
|
||||
},
|
||||
"The Little Prince" => {
|
||||
reading_time: 5400,
|
||||
isbn: "978-0156012195",
|
||||
series: false,
|
||||
},
|
||||
"Animal Farm" => {
|
||||
reading_time: 7200,
|
||||
isbn: "978-0451526342",
|
||||
series: false,
|
||||
},
|
||||
"The Catcher in the Rye" => {
|
||||
reading_time: 18_000,
|
||||
isbn: "978-0316769488",
|
||||
series: false,
|
||||
},
|
||||
"Jane Eyre" => {
|
||||
reading_time: 34_200,
|
||||
isbn: "978-0141441146",
|
||||
series: false,
|
||||
},
|
||||
"Fahrenheit 451" => {
|
||||
reading_time: 15_000,
|
||||
isbn: "978-1451673319",
|
||||
series: false,
|
||||
},
|
||||
"The Hobbit" => {
|
||||
reading_time: 27_000,
|
||||
isbn: "978-0547928227",
|
||||
series: false,
|
||||
},
|
||||
"The Da Vinci Code" => {
|
||||
reading_time: 37_800,
|
||||
isbn: "978-0307474278",
|
||||
series: false,
|
||||
},
|
||||
"Little Women" => {
|
||||
reading_time: 30_000,
|
||||
isbn: "978-0147514011",
|
||||
series: false,
|
||||
},
|
||||
"One Hundred Years of Solitude" => {
|
||||
reading_time: 46_800,
|
||||
isbn: "978-0060883287",
|
||||
series: false,
|
||||
},
|
||||
"And Then There Were None" => {
|
||||
reading_time: 16_200,
|
||||
isbn: "978-0062073488",
|
||||
series: false,
|
||||
},
|
||||
"The Alchemist" => {
|
||||
reading_time: 10_800,
|
||||
isbn: "978-0061122415",
|
||||
series: false,
|
||||
},
|
||||
"The Hitchhiker's Guide to the Galaxy" => {
|
||||
reading_time: 12_600,
|
||||
isbn: "978-0345391803",
|
||||
series: false,
|
||||
},
|
||||
"The Complete works of Shakespeare" => {
|
||||
reading_time: 180_000,
|
||||
isbn: "978-1853268953",
|
||||
series: true,
|
||||
},
|
||||
"The Game of Thrones Series" => {
|
||||
reading_time: 360_000,
|
||||
isbn: "978-0007477159",
|
||||
series: true,
|
||||
},
|
||||
"Malazan Book of the Fallen" => {
|
||||
reading_time: 720_000,
|
||||
isbn: "978-0765348821",
|
||||
series: true,
|
||||
},
|
||||
"Terry Pratchett’s Discworld series" => {
|
||||
reading_time: 1_440_000,
|
||||
isbn: "978-9123684458",
|
||||
series: true,
|
||||
},
|
||||
"The Wandering Inn web series" => {
|
||||
reading_time: 2_160_000,
|
||||
isbn: "the-wandering-inn",
|
||||
series: true,
|
||||
},
|
||||
"The Combined Cosmere works + Wheel of Time" => {
|
||||
reading_time: 2_880_000,
|
||||
isbn: "978-0812511819",
|
||||
series: true,
|
||||
},
|
||||
"The Star Trek novels" => {
|
||||
reading_time: 3_600_000,
|
||||
isbn: "978-1852860691",
|
||||
series: true,
|
||||
},
|
||||
}.symbolize_keys
|
||||
end
|
||||
|
||||
def best_book_fit(reading_time)
|
||||
reading_time_rest = reading_time
|
||||
books = []
|
||||
|
||||
while reading_time_rest > 0
|
||||
best_fit = popular_books.min_by { |_, v| (v[:reading_time] - reading_time_rest).abs }
|
||||
break if best_fit.nil?
|
||||
|
||||
books << best_fit.first
|
||||
reading_time_rest -= best_fit.last[:reading_time]
|
||||
end
|
||||
|
||||
return if books.empty?
|
||||
|
||||
book_title =
|
||||
books.group_by { |book| book }.transform_values(&:count).max_by { |_, count| count }.first
|
||||
|
||||
{
|
||||
title: book_title,
|
||||
isbn: popular_books[book_title][:isbn],
|
||||
series: popular_books[book_title][:series],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For a GitHub like calendar
|
||||
# https://docs.github.com/assets/cb-35216/mw-1440/images/help/profile/contributions-graph.webp
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::ActivityCalendar < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data:
|
||||
(Date.new(2024, 1, 1)..Date.new(2024, 12, 31)).map do |date|
|
||||
{ date: date.strftime("%Y-%m-%d"), post_count: rand(0..20), visited: false }
|
||||
end,
|
||||
identifier: "activity-calendar",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
calendar =
|
||||
Post
|
||||
.unscoped
|
||||
.joins(<<~SQL)
|
||||
RIGHT JOIN
|
||||
generate_series('#{date.first}', '#{date.last}', '1 day'::interval) ON
|
||||
posts.created_at::date = generate_series::date AND
|
||||
posts.user_id = #{user.id} AND
|
||||
posts.deleted_at IS NULL
|
||||
SQL
|
||||
.joins(
|
||||
"LEFT JOIN user_visits ON generate_series::date = visited_at AND user_visits.user_id = #{user.id}",
|
||||
)
|
||||
.select(
|
||||
"generate_series::date as date, count(posts.id) as post_count, COUNT(visited_at) > 0 as visited",
|
||||
)
|
||||
.group("generate_series, user_visits.id")
|
||||
.order("generate_series")
|
||||
.map do |row|
|
||||
{
|
||||
date: row.date.strftime("%Y-%m-%d"),
|
||||
post_count: row.post_count,
|
||||
visited: row.visited,
|
||||
}
|
||||
end
|
||||
|
||||
{ data: calendar, identifier: "activity-calendar" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::BaseReport < Service::ActionBase
|
||||
option :user
|
||||
option :date
|
||||
|
||||
def call
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def self.enabled?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::BestPosts < Rewind::Action::BaseReport
|
||||
def call
|
||||
best_posts =
|
||||
Post
|
||||
.where(user_id: user.id)
|
||||
.where(created_at: date)
|
||||
.where(deleted_at: nil)
|
||||
.where("post_number > 1")
|
||||
.order("like_count DESC NULLS LAST")
|
||||
.limit(3)
|
||||
.pluck(:post_number, :topic_id, :like_count, :reply_count, :raw, :cooked)
|
||||
|
||||
{ data: best_posts, identifier: "best-posts" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::BestTopics < Rewind::Action::BaseReport
|
||||
def call
|
||||
best_topics =
|
||||
TopTopic
|
||||
.includes(:topic)
|
||||
.references(:topic)
|
||||
.where(topic: { deleted_at: nil, created_at: date, user_id: user.id })
|
||||
.order("yearly_score DESC NULLS LAST")
|
||||
.limit(3)
|
||||
.pluck(:topic_id, :title, :excerpt, :yearly_score)
|
||||
.map do |topic_id, title, excerpt, yearly_score|
|
||||
{ topic_id: topic_id, title: title, excerpt: excerpt, yearly_score: yearly_score }
|
||||
end
|
||||
|
||||
{ data: best_topics, identifier: "best-topics" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Topics visited grouped by category
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::FavoriteCategories < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ category_id: 1, name: "cats" },
|
||||
{ category_id: 2, name: "dogs" },
|
||||
{ category_id: 3, name: "countries" },
|
||||
{ category_id: 4, name: "management" },
|
||||
{ category_id: 5, name: "things" },
|
||||
],
|
||||
identifier: "favorite-categories",
|
||||
}
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
favorite_categories =
|
||||
TopicViewItem
|
||||
.joins(:topic)
|
||||
.joins("INNER JOIN categories ON categories.id = topics.category_id")
|
||||
.where(user: user)
|
||||
.where(viewed_at: date)
|
||||
.group("categories.id, categories.name")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(5)
|
||||
.pluck("categories.id, categories.name")
|
||||
.map { |category_id, name| { category_id: category_id, name: name } }
|
||||
|
||||
{ data: favorite_categories, identifier: "favorite-categories" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Topics visited grouped by tag
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::FavoriteTags < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ tag_id: 1, name: "cats" },
|
||||
{ tag_id: 2, name: "dogs" },
|
||||
{ tag_id: 3, name: "countries" },
|
||||
{ tag_id: 4, name: "management" },
|
||||
{ tag_id: 5, name: "things" },
|
||||
],
|
||||
identifier: "favorite-tags",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
favorite_tags =
|
||||
TopicViewItem
|
||||
.joins(:topic)
|
||||
.joins("INNER JOIN topic_tags ON topic_tags.topic_id = topics.id")
|
||||
.joins("INNER JOIN tags ON tags.id = topic_tags.tag_id")
|
||||
.where(user: user)
|
||||
.where(viewed_at: date)
|
||||
.group("tags.id, tags.name")
|
||||
.order("COUNT(DISTINCT topic_views.topic_id) DESC")
|
||||
.limit(5)
|
||||
.pluck("tags.id, tags.name")
|
||||
.map { |tag_id, name| { tag_id: tag_id, name: name } }
|
||||
|
||||
{ data: favorite_tags, identifier: "favorite-tags" }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,112 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Forum Best Friend Forever ranking
|
||||
# Score is informative only, do not show in UI
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::Fbff < Rewind::Action::BaseReport
|
||||
MAX_SUMMARY_RESULTS ||= 50
|
||||
LIKE_SCORE ||= 1
|
||||
REPLY_SCORE ||= 10
|
||||
|
||||
def call
|
||||
most_liked_users =
|
||||
like_query(date)
|
||||
.where(acting_user_id: user.id)
|
||||
.group(:user_id)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("user_actions.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
most_liked_by_users =
|
||||
like_query(date)
|
||||
.where(user: user)
|
||||
.group(:acting_user_id)
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("acting_user_id, COUNT(*)")
|
||||
.map { |acting_user_id, count| { acting_user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
users_who_most_replied_me =
|
||||
post_query(user, date)
|
||||
.where(posts: { user_id: user.id })
|
||||
.group("replies.user_id")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("replies.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
users_i_most_replied =
|
||||
post_query(user, date)
|
||||
.where("replies.user_id = ?", user.id)
|
||||
.group("posts.user_id")
|
||||
.order("COUNT(*) DESC")
|
||||
.limit(MAX_SUMMARY_RESULTS)
|
||||
.pluck("posts.user_id, COUNT(*)")
|
||||
.map { |user_id, count| { user_id => count } }
|
||||
.reduce({}, :merge)
|
||||
|
||||
#TODO include chat?
|
||||
|
||||
fbffs = [
|
||||
apply_score(most_liked_users, LIKE_SCORE),
|
||||
apply_score(most_liked_by_users, LIKE_SCORE),
|
||||
apply_score(users_who_most_replied_me, REPLY_SCORE),
|
||||
apply_score(users_i_most_replied, REPLY_SCORE),
|
||||
]
|
||||
|
||||
fbff_id =
|
||||
fbffs
|
||||
.flatten
|
||||
.inject { |h1, h2| h1.merge(h2) { |_, v1, v2| v1 + v2 } }
|
||||
&.sort_by { |_, v| -v }
|
||||
&.first
|
||||
&.first
|
||||
|
||||
return if !fbff_id
|
||||
|
||||
{
|
||||
data: {
|
||||
fbff: BasicUserSerializer.new(User.find(fbff_id), root: false).as_json,
|
||||
yourself: BasicUserSerializer.new(user, root: false).as_json,
|
||||
},
|
||||
identifier: "fbff",
|
||||
}
|
||||
end
|
||||
|
||||
def post_query(user, date)
|
||||
Post
|
||||
.joins(:topic)
|
||||
.includes(:topic)
|
||||
.where(
|
||||
"posts.post_type IN (?)",
|
||||
Topic.visible_post_types(user, include_moderator_actions: false),
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number",
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN topics ON replies.topic_id = topics.id AND topics.archetype <> 'private_message'",
|
||||
)
|
||||
.joins(
|
||||
"AND replies.post_type IN (#{Topic.visible_post_types(user, include_moderator_actions: false).join(",")})",
|
||||
)
|
||||
.where("replies.created_at BETWEEN ? AND ?", date.first, date.last)
|
||||
.where("replies.user_id <> posts.user_id")
|
||||
end
|
||||
|
||||
def like_query(date)
|
||||
UserAction
|
||||
.joins(:target_topic, :target_post)
|
||||
.where(created_at: date)
|
||||
.where(action_type: UserAction::WAS_LIKED)
|
||||
end
|
||||
|
||||
def apply_score(users, score)
|
||||
users.map { |user_id, count| { user_id => count * score } }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For a most user / received reactions cards
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::Reactions < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data: {
|
||||
post_received_reactions: {
|
||||
open_mouth: 2,
|
||||
cat: 32,
|
||||
dog: 34,
|
||||
heart: 45,
|
||||
grinning: 82,
|
||||
},
|
||||
post_used_reactions: {
|
||||
open_mouth: 2,
|
||||
cat: 32,
|
||||
dog: 34,
|
||||
heart: 45,
|
||||
grinning: 82,
|
||||
},
|
||||
chat_used_reactions: {
|
||||
open_mouth: 2,
|
||||
cat: 32,
|
||||
dog: 34,
|
||||
heart: 45,
|
||||
grinning: 82,
|
||||
},
|
||||
chat_received_reactions: {
|
||||
open_mouth: 2,
|
||||
cat: 32,
|
||||
dog: 34,
|
||||
heart: 45,
|
||||
grinning: 82,
|
||||
},
|
||||
},
|
||||
identifier: "reactions",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
data = {}
|
||||
if defined?(DiscourseReactions::Reaction)
|
||||
# This is missing heart reactions (default like)
|
||||
data[:post_used_reactions] = sort_and_limit(
|
||||
DiscourseReactions::Reaction
|
||||
.by_user(user)
|
||||
.where(created_at: date)
|
||||
.group(:reaction_value)
|
||||
.count,
|
||||
)
|
||||
|
||||
data[:post_received_reactions] = sort_and_limit(
|
||||
DiscourseReactions::Reaction
|
||||
.includes(:post)
|
||||
.where(posts: { user_id: user.id })
|
||||
.where(created_at: date)
|
||||
.group(:reaction_value)
|
||||
.count,
|
||||
)
|
||||
end
|
||||
|
||||
if SiteSetting.chat_enabled
|
||||
data[:chat_used_reactions] = sort_and_limit(
|
||||
Chat::MessageReaction.where(user: user).where(created_at: date).group(:emoji).count,
|
||||
)
|
||||
|
||||
data[:chat_received_reactions] = sort_and_limit(
|
||||
Chat::MessageReaction
|
||||
.includes(:chat_message)
|
||||
.where(chat_message: { user_id: user.id })
|
||||
.where(created_at: date)
|
||||
.group(:emoji)
|
||||
.count,
|
||||
)
|
||||
end
|
||||
|
||||
{ data:, identifier: "reactions" }
|
||||
end
|
||||
|
||||
def enabled?
|
||||
SiteSetting.discourse_reactions_enabled || SiteSetting.chat_enabled
|
||||
end
|
||||
|
||||
def sort_and_limit(reactions)
|
||||
reactions.sort_by { |_, v| -v }.first(5).reverse.to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,200 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# For showcasing the reading time of a user
|
||||
# Should we show book covers or just the names?
|
||||
module DiscourseRewind
|
||||
class Rewind::Action::ReadingTime < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data: {
|
||||
reading_time: 2_880_000,
|
||||
book: "The Combined Cosmere works + Wheel of Time",
|
||||
isbn: "978-0812511819",
|
||||
series: true,
|
||||
},
|
||||
identifier: "reading-time",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
reading_time = UserVisit.where(user_id: user.id).where(visited_at: date).sum(:time_read)
|
||||
book = best_book_fit(reading_time)
|
||||
|
||||
return if book.nil?
|
||||
|
||||
{
|
||||
data: {
|
||||
reading_time: reading_time,
|
||||
book: book[:title],
|
||||
isbn: book[:isbn],
|
||||
series: book[:series],
|
||||
},
|
||||
identifier: "reading-time",
|
||||
}
|
||||
end
|
||||
|
||||
def popular_books
|
||||
{
|
||||
"The Hunger Games" => {
|
||||
reading_time: 19_740,
|
||||
isbn: "978-0439023481",
|
||||
series: false,
|
||||
},
|
||||
"The Metamorphosis" => {
|
||||
reading_time: 3120,
|
||||
isbn: "978-0553213690",
|
||||
series: false,
|
||||
},
|
||||
"To Kill a Mockingbird" => {
|
||||
reading_time: 22_800,
|
||||
isbn: "978-0061120084",
|
||||
series: false,
|
||||
},
|
||||
"Pride and Prejudice" => {
|
||||
reading_time: 25_200,
|
||||
isbn: "978-1503290563",
|
||||
series: false,
|
||||
},
|
||||
"1984" => {
|
||||
reading_time: 16_800,
|
||||
isbn: "978-0451524935",
|
||||
series: false,
|
||||
},
|
||||
"The Lord of the Rings" => {
|
||||
reading_time: 108_000,
|
||||
isbn: "978-0544003415",
|
||||
series: true,
|
||||
},
|
||||
"Harry Potter and the Sorcerer's Stone" => {
|
||||
reading_time: 24_600,
|
||||
isbn: "978-0590353427",
|
||||
series: true,
|
||||
},
|
||||
"The Great Gatsby" => {
|
||||
reading_time: 12_600,
|
||||
isbn: "978-0743273565",
|
||||
series: false,
|
||||
},
|
||||
"The Little Prince" => {
|
||||
reading_time: 5400,
|
||||
isbn: "978-0156012195",
|
||||
series: false,
|
||||
},
|
||||
"Animal Farm" => {
|
||||
reading_time: 7200,
|
||||
isbn: "978-0451526342",
|
||||
series: false,
|
||||
},
|
||||
"The Catcher in the Rye" => {
|
||||
reading_time: 18_000,
|
||||
isbn: "978-0316769488",
|
||||
series: false,
|
||||
},
|
||||
"Jane Eyre" => {
|
||||
reading_time: 34_200,
|
||||
isbn: "978-0141441146",
|
||||
series: false,
|
||||
},
|
||||
"Fahrenheit 451" => {
|
||||
reading_time: 15_000,
|
||||
isbn: "978-1451673319",
|
||||
series: false,
|
||||
},
|
||||
"The Hobbit" => {
|
||||
reading_time: 27_000,
|
||||
isbn: "978-0547928227",
|
||||
series: false,
|
||||
},
|
||||
"The Da Vinci Code" => {
|
||||
reading_time: 37_800,
|
||||
isbn: "978-0307474278",
|
||||
series: false,
|
||||
},
|
||||
"Little Women" => {
|
||||
reading_time: 30_000,
|
||||
isbn: "978-0147514011",
|
||||
series: false,
|
||||
},
|
||||
"One Hundred Years of Solitude" => {
|
||||
reading_time: 46_800,
|
||||
isbn: "978-0060883287",
|
||||
series: false,
|
||||
},
|
||||
"And Then There Were None" => {
|
||||
reading_time: 16_200,
|
||||
isbn: "978-0062073488",
|
||||
series: false,
|
||||
},
|
||||
"The Alchemist" => {
|
||||
reading_time: 10_800,
|
||||
isbn: "978-0061122415",
|
||||
series: false,
|
||||
},
|
||||
"The Hitchhiker's Guide to the Galaxy" => {
|
||||
reading_time: 12_600,
|
||||
isbn: "978-0345391803",
|
||||
series: false,
|
||||
},
|
||||
"The Complete works of Shakespeare" => {
|
||||
reading_time: 180_000,
|
||||
isbn: "978-1853268953",
|
||||
series: true,
|
||||
},
|
||||
"The Game of Thrones Series" => {
|
||||
reading_time: 360_000,
|
||||
isbn: "978-0007477159",
|
||||
series: true,
|
||||
},
|
||||
"Malazan Book of the Fallen" => {
|
||||
reading_time: 720_000,
|
||||
isbn: "978-0765348821",
|
||||
series: true,
|
||||
},
|
||||
"Terry Pratchett’s Discworld series" => {
|
||||
reading_time: 1_440_000,
|
||||
isbn: "978-9123684458",
|
||||
series: true,
|
||||
},
|
||||
"The Wandering Inn web series" => {
|
||||
reading_time: 2_160_000,
|
||||
isbn: "the-wandering-inn",
|
||||
series: true,
|
||||
},
|
||||
"The Combined Cosmere works + Wheel of Time" => {
|
||||
reading_time: 2_880_000,
|
||||
isbn: "978-0812511819",
|
||||
series: true,
|
||||
},
|
||||
"The Star Trek novels" => {
|
||||
reading_time: 3_600_000,
|
||||
isbn: "978-1852860691",
|
||||
series: true,
|
||||
},
|
||||
}.symbolize_keys
|
||||
end
|
||||
|
||||
def best_book_fit(reading_time)
|
||||
reading_time_rest = reading_time
|
||||
books = []
|
||||
|
||||
while reading_time_rest > 0
|
||||
best_fit = popular_books.min_by { |_, v| (v[:reading_time] - reading_time_rest).abs }
|
||||
break if best_fit.nil?
|
||||
|
||||
books << best_fit.first
|
||||
reading_time_rest -= best_fit.last[:reading_time]
|
||||
end
|
||||
|
||||
return if books.empty?
|
||||
|
||||
book_title =
|
||||
books.group_by { |book| book }.transform_values(&:count).max_by { |_, count| count }.first
|
||||
|
||||
{
|
||||
title: book_title,
|
||||
isbn: popular_books[book_title][:isbn],
|
||||
series: popular_books[book_title][:series],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
+17
-19
@@ -1,24 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module DiscourseRewind
|
||||
module Action
|
||||
class TopWords < BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ word: "seven", score: 100 },
|
||||
{ word: "longest", score: 90 },
|
||||
{ word: "you", score: 80 },
|
||||
{ word: "overachieved", score: 70 },
|
||||
{ word: "assume", score: 60 },
|
||||
{ word: "week", score: 50 },
|
||||
],
|
||||
identifier: "top-words",
|
||||
}
|
||||
class Rewind::Action::TopWords < Rewind::Action::BaseReport
|
||||
FakeData = {
|
||||
data: [
|
||||
{ word: "what", score: 100 },
|
||||
{ word: "have", score: 90 },
|
||||
{ word: "you", score: 80 },
|
||||
{ word: "overachieved", score: 70 },
|
||||
{ word: "this", score: 60 },
|
||||
{ word: "week", score: 50 },
|
||||
],
|
||||
identifier: "top-words",
|
||||
}
|
||||
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
def call
|
||||
return FakeData if Rails.env.development?
|
||||
|
||||
words = DB.query(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
|
||||
words = DB.query(<<~SQL, user_id: user.id, date_start: date.first, date_end: date.last)
|
||||
WITH popular_words AS (
|
||||
SELECT
|
||||
*
|
||||
@@ -79,10 +78,9 @@ module DiscourseRewind
|
||||
LIMIT 100
|
||||
SQL
|
||||
|
||||
word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } }
|
||||
word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } }
|
||||
|
||||
{ data: word_score, identifier: "top-words" }
|
||||
end
|
||||
{ data: word_score, identifier: "top-words" }
|
||||
end
|
||||
end
|
||||
end
|
||||
+12
-21
@@ -8,7 +8,7 @@ module DiscourseRewind
|
||||
# guardian: guardian
|
||||
# )
|
||||
#
|
||||
class FetchReports
|
||||
class Rewind::Fetch
|
||||
include Service::Base
|
||||
|
||||
# @!method self.call(guardian:, params:)
|
||||
@@ -18,20 +18,7 @@ module DiscourseRewind
|
||||
# @option params [Integer] :username of the rewind
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
CACHE_DURATION = Rails.env.development? ? 10.seconds : 5.minutes
|
||||
|
||||
# order matters
|
||||
REPORTS = [
|
||||
Action::TopWords,
|
||||
Action::ReadingTime,
|
||||
Action::Reactions,
|
||||
Action::Fbff,
|
||||
Action::FavoriteTags,
|
||||
Action::FavoriteCategories,
|
||||
Action::BestTopics,
|
||||
Action::BestPosts,
|
||||
Action::ActivityCalendar,
|
||||
]
|
||||
CACHE_DURATION = 5.minutes
|
||||
|
||||
model :year
|
||||
model :user
|
||||
@@ -64,14 +51,18 @@ module DiscourseRewind
|
||||
end
|
||||
|
||||
def fetch_reports(date:, user:, guardian:, year:)
|
||||
key = "rewind:#{guardian.user.username}:#{year}"
|
||||
reports = Discourse.redis.get(key)
|
||||
key = "rewind:#{guardian.user.id}:#{year}"
|
||||
cached_reports = Discourse.redis.get(key)
|
||||
|
||||
if !reports
|
||||
reports = REPORTS.map { |report| report.call(date:, user:, guardian:) }
|
||||
Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
|
||||
if !cached_reports
|
||||
reports =
|
||||
::DiscourseRewind::Rewind::Action::BaseReport
|
||||
.descendants
|
||||
.filter { _1.enabled? }
|
||||
.map { |report| report.call(date:, user:, guardian:) }
|
||||
Discourse.redis.setex(key, CACHE_DURATION, reports.to_json)
|
||||
else
|
||||
reports = MultiJson.load(reports, symbolize_keys: true)
|
||||
reports = JSON.parse(cached_reports, symbolize_names: true)
|
||||
end
|
||||
|
||||
reports
|
||||
@@ -2,7 +2,7 @@ import Component from "@glimmer/component";
|
||||
import { concat } from "@ember/helper";
|
||||
import { get } from "@ember/object";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export default class BestPosts extends Component {
|
||||
|
||||
@@ -5,7 +5,7 @@ import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import emoji from "discourse/helpers/emoji";
|
||||
import discourseLater from "discourse/lib/later";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
|
||||
const MYSTERY_EMOJIS = [
|
||||
"mag", // 🔍
|
||||
@@ -47,7 +47,7 @@ export default class WordCard extends Component {
|
||||
}
|
||||
|
||||
get longWord() {
|
||||
return this.args.word.length >= 5;
|
||||
return this.args.word.length >= 7;
|
||||
}
|
||||
|
||||
get cardStyle() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { tracked } from "@glimmer/tracking";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { eq } from "truth-helpers";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
@@ -20,8 +21,12 @@ import ReadingTime from "discourse/plugins/discourse-rewind/discourse/components
|
||||
import TopWords from "discourse/plugins/discourse-rewind/discourse/components/reports/top-words";
|
||||
|
||||
export default class Rewind extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
@tracked rewind = [];
|
||||
|
||||
@tracked fullScreen = true;
|
||||
|
||||
@tracked loadingRewind = false;
|
||||
|
||||
@action
|
||||
|
||||
@@ -2,8 +2,8 @@ import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
|
||||
|
||||
export default class RewindTab extends Component {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import DNavigationItem from "discourse/components/d-navigation-item";
|
||||
import icon from "discourse/helpers/d-icon";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import isRewindActive from "discourse/plugins/discourse-rewind/discourse/lib/is-rewind-active";
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import RouteTemplate from "ember-route-template";
|
||||
import Rewind from "../../components/rewind";
|
||||
|
||||
export default RouteTemplate(<template><Rewind /></template>);
|
||||
@@ -0,0 +1 @@
|
||||
<Rewind />
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user