From 26a77d4c69644814b5c1749f5f3323f5f6e69d3b Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Mon, 1 Dec 2025 10:08:30 +1000 Subject: [PATCH] DEV: Adding specs and refactors (#21) Add specs for the following reports: * Reading time * Activity calendar * Best posts * Best topics * Top words * Most viewed tags * Most viewed categories Did some minor UI and ruby refactors for related components. Also made a minor change to the Activity calendar, to show a title based on the number of posts or if the user was active on hover. Still missing specs for: * Reactions * FBFF And the newly added reports that don't yet have UI components. --- .../action/most_viewed_categories.rb | 10 +- .../action/most_viewed_tags.rb | 4 +- .../discourse_rewind/action/reactions.rb | 2 +- .../discourse_rewind/action/reading_time.rb | 305 +++++++++--------- .../discourse_rewind/action/top_words.rb | 9 +- .../discourse_rewind/fetch_reports.rb | 3 +- .../components/reports/activity-calendar.gjs | 28 +- .../components/reports/best-posts.gjs | 14 +- .../components/reports/best-topics.gjs | 16 +- .../components/reports/top-words.gjs | 41 ++- config/locales/client.en.yml | 6 + spec/actions/activity_calendar_spec.rb | 68 ++++ spec/actions/best_posts_spec.rb | 66 ++++ spec/actions/best_topics_spec.rb | 61 ++++ spec/actions/most_viewed_categories_spec.rb | 92 ++++++ spec/actions/most_viewed_tags_spec.rb | 117 +++++++ spec/actions/reading_time_spec.rb | 146 +++++++++ spec/actions/top_words_spec.rb | 135 ++++++++ spec/plugin_helper.rb | 15 + spec/services/fetch_reports_spec.rb | 11 + 20 files changed, 943 insertions(+), 206 deletions(-) create mode 100644 spec/actions/activity_calendar_spec.rb create mode 100644 spec/actions/best_posts_spec.rb create mode 100644 spec/actions/best_topics_spec.rb create mode 100644 spec/actions/most_viewed_categories_spec.rb create mode 100644 spec/actions/most_viewed_tags_spec.rb create mode 100644 spec/actions/reading_time_spec.rb create mode 100644 spec/actions/top_words_spec.rb create mode 100644 spec/plugin_helper.rb diff --git a/app/services/discourse_rewind/action/most_viewed_categories.rb b/app/services/discourse_rewind/action/most_viewed_categories.rb index d473306..5a3434b 100644 --- a/app/services/discourse_rewind/action/most_viewed_categories.rb +++ b/app/services/discourse_rewind/action/most_viewed_categories.rb @@ -20,9 +20,13 @@ module DiscourseRewind 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 }) + .where( + user: user, + viewed_at: date, + categories: { + id: user.guardian.allowed_category_ids, + }, + ) .group("categories.id, categories.name") .order("COUNT(*) DESC") .limit(4) diff --git a/app/services/discourse_rewind/action/most_viewed_tags.rb b/app/services/discourse_rewind/action/most_viewed_tags.rb index 0552961..789add2 100644 --- a/app/services/discourse_rewind/action/most_viewed_tags.rb +++ b/app/services/discourse_rewind/action/most_viewed_tags.rb @@ -22,9 +22,7 @@ module DiscourseRewind .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) }) + .where(user: user, viewed_at: date, tags: { id: Tag.visible(user.guardian).pluck(:id) }) .group("tags.id, tags.name") .order("COUNT(DISTINCT topic_views.topic_id) DESC") .limit(4) diff --git a/app/services/discourse_rewind/action/reactions.rb b/app/services/discourse_rewind/action/reactions.rb index d7e52db..22e53a7 100644 --- a/app/services/discourse_rewind/action/reactions.rb +++ b/app/services/discourse_rewind/action/reactions.rb @@ -85,7 +85,7 @@ module DiscourseRewind end def sort_and_limit(reactions) - reactions.sort_by { |_, v| -v }.first(5).reverse.to_h + reactions.sort_by { |_, value| -value }.take(5).reverse.to_h end end end diff --git a/app/services/discourse_rewind/action/reading_time.rb b/app/services/discourse_rewind/action/reading_time.rb index dfc6885..cdd8fb3 100644 --- a/app/services/discourse_rewind/action/reading_time.rb +++ b/app/services/discourse_rewind/action/reading_time.rb @@ -5,6 +5,144 @@ module DiscourseRewind module Action class ReadingTime < BaseReport + POPULAR_BOOKS = { + "The Metamorphosis" => { + reading_time: 3120, + isbn: "978-0553213690", + series: false, + }, + "The Little Prince" => { + reading_time: 5400, + isbn: "978-0156012195", + series: false, + }, + "Animal Farm" => { + reading_time: 7200, + isbn: "978-0451526342", + series: false, + }, + "The Alchemist" => { + reading_time: 10_800, + isbn: "978-0061122415", + series: false, + }, + "The Great Gatsby" => { + reading_time: 12_600, + isbn: "978-0743273565", + series: false, + }, + "The Hitchhiker's Guide to the Galaxy" => { + reading_time: 12_600, + isbn: "978-0345391803", + series: false, + }, + "Fahrenheit 451" => { + reading_time: 15_000, + isbn: "978-1451673319", + series: false, + }, + "And Then There Were None" => { + reading_time: 16_200, + isbn: "978-0062073488", + series: false, + }, + "1984" => { + reading_time: 16_800, + isbn: "978-0451524935", + series: false, + }, + "The Catcher in the Rye" => { + reading_time: 18_000, + isbn: "978-0316769488", + series: false, + }, + "The Hunger Games" => { + reading_time: 19_740, + isbn: "978-0439023481", + series: false, + }, + "To Kill a Mockingbird" => { + reading_time: 22_800, + isbn: "978-0061120084", + series: false, + }, + "Harry Potter and the Sorcerer's Stone" => { + reading_time: 24_600, + isbn: "978-0590353427", + series: true, + }, + "Pride and Prejudice" => { + reading_time: 25_200, + isbn: "978-1503290563", + series: false, + }, + "The Hobbit" => { + reading_time: 27_000, + isbn: "978-0547928227", + series: false, + }, + "Little Women" => { + reading_time: 30_000, + isbn: "978-0147514011", + series: false, + }, + "Jane Eyre" => { + reading_time: 34_200, + isbn: "978-0141441146", + series: false, + }, + "The Da Vinci Code" => { + reading_time: 37_800, + isbn: "978-0307474278", + series: false, + }, + "One Hundred Years of Solitude" => { + reading_time: 46_800, + isbn: "978-0060883287", + series: false, + }, + "The Lord of the Rings" => { + reading_time: 108_000, + isbn: "978-0544003415", + series: true, + }, + "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 + FakeData = { data: { reading_time: 2_880_000, @@ -26,7 +164,7 @@ module DiscourseRewind { data: { reading_time: reading_time, - book: book[:title], + book: book[:title].to_s, isbn: book[:isbn], series: book[:series], }, @@ -34,168 +172,15 @@ module DiscourseRewind } 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 = [] + best_fit = + POPULAR_BOOKS + .select { |_, v| v[:reading_time] > reading_time } + .min_by { |_, v| v[:reading_time] } - while reading_time_rest > 0 - best_fit = popular_books.min_by { |_, v| (v[:reading_time] - reading_time_rest).abs } - break if best_fit.nil? + return 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], - } + { title: best_fit.first, isbn: best_fit.last[:isbn], series: best_fit.last[:series] } end end end diff --git a/app/services/discourse_rewind/action/top_words.rb b/app/services/discourse_rewind/action/top_words.rb index 4851bf1..7517f5d 100644 --- a/app/services/discourse_rewind/action/top_words.rb +++ b/app/services/discourse_rewind/action/top_words.rb @@ -10,7 +10,6 @@ module DiscourseRewind { word: "you", score: 80 }, { word: "overachieved", score: 70 }, { word: "assume", score: 60 }, - { word: "week", score: 50 }, ], identifier: "top-words", } @@ -79,7 +78,13 @@ module DiscourseRewind LIMIT 100 SQL - word_score = words.map { { word: _1.original_word, score: _1.ndoc + _1.nentry } } + word_score = + words + .map do |word_data| + { word: word_data.original_word, score: word_data.ndoc + word_data.nentry } + end + .sort_by! { |w| -w[:score] } + .take(5) { data: word_score, identifier: "top-words" } end diff --git a/app/services/discourse_rewind/fetch_reports.rb b/app/services/discourse_rewind/fetch_reports.rb index 679b916..3b28f68 100644 --- a/app/services/discourse_rewind/fetch_reports.rb +++ b/app/services/discourse_rewind/fetch_reports.rb @@ -5,7 +5,8 @@ module DiscourseRewind # # @example # ::DiscourseRewind::Rewind::Fetch.call( - # guardian: guardian + # guardian: guardian, + # params: { year: 2023, username: 'codinghorror' } # ) # class FetchReports diff --git a/assets/javascripts/discourse/components/reports/activity-calendar.gjs b/assets/javascripts/discourse/components/reports/activity-calendar.gjs index 6e0ba0d..8a8563c 100644 --- a/assets/javascripts/discourse/components/reports/activity-calendar.gjs +++ b/assets/javascripts/discourse/components/reports/activity-calendar.gjs @@ -28,6 +28,32 @@ export default class ActivityCalendar extends Component { return moment().month(monthIndex).format("MMM"); } + @action + computeCellTitle(cell) { + if (!cell || !cell.date) { + return ""; + } + + const date = moment(cell.date).format("LL"); + + if (cell.visited && cell.post_count === 0) { + return i18n( + "discourse_rewind.reports.activity_calendar.cell_title.visited_no_posts", + { date } + ); + } else if (cell.post_count > 0) { + return i18n( + "discourse_rewind.reports.activity_calendar.cell_title.visited_with_posts", + { date, count: cell.post_count } + ); + } + + return i18n( + "discourse_rewind.reports.activity_calendar.cell_title.no_activity", + { date } + ); + } + @action computeClass(count) { if (!count) { @@ -107,7 +133,7 @@ export default class ActivityCalendar extends Component { {{#each row as |cell|}} {{#if @report.data.length}}
-

{{i18n +

+ {{i18n "discourse_rewind.reports.best_posts.title" count=@report.data.length - }}

+ }} +
{{#each @report.data as |post idx|}} -
+

{{htmlSafe diff --git a/assets/javascripts/discourse/components/reports/best-topics.gjs b/assets/javascripts/discourse/components/reports/best-topics.gjs index 19c8de1..b4a5cf9 100644 --- a/assets/javascripts/discourse/components/reports/best-topics.gjs +++ b/assets/javascripts/discourse/components/reports/best-topics.gjs @@ -1,27 +1,31 @@ import Component from "@glimmer/component"; import { concat } from "@ember/helper"; import { htmlSafe } from "@ember/template"; +import concatClass from "discourse/helpers/concat-class"; import replaceEmoji from "discourse/helpers/replace-emoji"; +import getURL from "discourse/lib/get-url"; import { i18n } from "discourse-i18n"; export default class BestTopics extends Component { - rank(idx) { - return idx + 1; + rankClass(idx) { + return `rank-${idx + 1}`; }