mirror of
https://github.com/discourse/discourse-rewind.git
synced 2025-07-07 14:22:12 +00:00
caching and load order
This commit is contained in:
parent
141140a410
commit
a59d6c7931
@ -7,7 +7,7 @@ module ::DiscourseRewind
|
|||||||
requires_login
|
requires_login
|
||||||
|
|
||||||
def show
|
def show
|
||||||
DiscourseRewind::Rewind::Fetch.call(service_params) do
|
DiscourseRewind::FetchReports.call(service_params) do
|
||||||
on_model_not_found(:year) { raise Discourse::NotFound }
|
on_model_not_found(:year) { raise Discourse::NotFound }
|
||||||
on_model_not_found(:user) { raise Discourse::NotFound }
|
on_model_not_found(:user) { raise Discourse::NotFound }
|
||||||
on_success do |reports:|
|
on_success do |reports:|
|
||||||
|
43
app/services/discourse_rewind/action/activity_calendar.rb
Normal file
43
app/services/discourse_rewind/action/activity_calendar.rb
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# 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
|
18
app/services/discourse_rewind/action/base_report.rb
Normal file
18
app/services/discourse_rewind/action/base_report.rb
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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
|
21
app/services/discourse_rewind/action/best_posts.rb
Normal file
21
app/services/discourse_rewind/action/best_posts.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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
|
23
app/services/discourse_rewind/action/best_topics.rb
Normal file
23
app/services/discourse_rewind/action/best_topics.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 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
|
36
app/services/discourse_rewind/action/favorite_categories.rb
Normal file
36
app/services/discourse_rewind/action/favorite_categories.rb
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# 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)
|
||||||
|
.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
|
38
app/services/discourse_rewind/action/favorite_tags.rb
Normal file
38
app/services/discourse_rewind/action/favorite_tags.rb
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# 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)
|
||||||
|
.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
|
114
app/services/discourse_rewind/action/fbff.rb
Normal file
114
app/services/discourse_rewind/action/fbff.rb
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# 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
|
92
app/services/discourse_rewind/action/reactions.rb
Normal file
92
app/services/discourse_rewind/action/reactions.rb
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# 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
|
202
app/services/discourse_rewind/action/reading_time.rb
Normal file
202
app/services/discourse_rewind/action/reading_time.rb
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
# 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
|
@ -1,7 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DiscourseRewind
|
module DiscourseRewind
|
||||||
class Rewind::Action::TopWords < Rewind::Action::BaseReport
|
module Action
|
||||||
|
class TopWords < BaseReport
|
||||||
FakeData = {
|
FakeData = {
|
||||||
data: [
|
data: [
|
||||||
{ word: "what", score: 100 },
|
{ word: "what", score: 100 },
|
||||||
@ -84,3 +85,4 @@ module DiscourseRewind
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
@ -8,7 +8,7 @@ module DiscourseRewind
|
|||||||
# guardian: guardian
|
# guardian: guardian
|
||||||
# )
|
# )
|
||||||
#
|
#
|
||||||
class Rewind::Fetch
|
class FetchReports
|
||||||
include Service::Base
|
include Service::Base
|
||||||
|
|
||||||
# @!method self.call(guardian:, params:)
|
# @!method self.call(guardian:, params:)
|
||||||
@ -18,7 +18,20 @@ module DiscourseRewind
|
|||||||
# @option params [Integer] :username of the rewind
|
# @option params [Integer] :username of the rewind
|
||||||
# @return [Service::Base::Context]
|
# @return [Service::Base::Context]
|
||||||
|
|
||||||
CACHE_DURATION = 5.minutes
|
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,
|
||||||
|
]
|
||||||
|
|
||||||
model :year
|
model :year
|
||||||
model :user
|
model :user
|
||||||
@ -51,19 +64,15 @@ module DiscourseRewind
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_reports(date:, user:, guardian:, year:)
|
def fetch_reports(date:, user:, guardian:, year:)
|
||||||
# key = "rewind:#{guardian.user.username}:#{year}"
|
key = "rewind:#{guardian.user.username}:#{year}"
|
||||||
# reports = Discourse.redis.get(key)
|
reports = Discourse.redis.get(key)
|
||||||
|
|
||||||
# if Rails.env.development? || !reports
|
if !reports
|
||||||
reports =
|
reports = REPORTS.map { |report| report.call(date:, user:, guardian:) }
|
||||||
::DiscourseRewind::Rewind::Action::BaseReport
|
Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
|
||||||
.descendants
|
else
|
||||||
.filter { _1.enabled? }
|
reports = MultiJson.load(reports, symbolize_keys: true)
|
||||||
.map { |report| report.call(date:, user:, guardian:) }
|
end
|
||||||
# Discourse.redis.setex(key, CACHE_DURATION, MultiJson.dump(reports))
|
|
||||||
# else
|
|
||||||
# reports = MultiJson.load(reports.compact, symbolize_keys: true)
|
|
||||||
# end
|
|
||||||
|
|
||||||
reports
|
reports
|
||||||
end
|
end
|
@ -1,41 +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
|
|
||||||
class Rewind::Action::ActivityCalendar < Rewind::Action::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
|
|
@ -1,16 +0,0 @@
|
|||||||
# 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
|
|
@ -1,19 +0,0 @@
|
|||||||
# 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
|
|
@ -1,21 +0,0 @@
|
|||||||
# 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
|
|
@ -1,34 +0,0 @@
|
|||||||
# 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
|
|
@ -1,36 +0,0 @@
|
|||||||
# 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
|
|
@ -1,112 +0,0 @@
|
|||||||
# 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
|
|
@ -1,90 +0,0 @@
|
|||||||
# 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
|
|
@ -1,200 +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
|
|
||||||
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
|
|
0
spec/services/fetch_spec.rb
Normal file
0
spec/services/fetch_spec.rb
Normal file
Loading…
x
Reference in New Issue
Block a user