diff --git a/app/models/bubble.rb b/app/models/bubble.rb index 8c07b3e47..5c408211d 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -20,6 +20,7 @@ class Bubble < ApplicationRecord scope :indexed_by, ->(index) do case index when "most_active" then ordered_by_activity + when "most_stale" then ordered_by_staleness when "most_discussed" then ordered_by_comments when "most_boosted" then ordered_by_boosts when "newest" then reverse_chronologically diff --git a/app/models/bubble/scorable.rb b/app/models/bubble/scorable.rb index 90df3b9e2..fd188eb98 100644 --- a/app/models/bubble/scorable.rb +++ b/app/models/bubble/scorable.rb @@ -5,6 +5,13 @@ module Bubble::Scorable included do scope :ordered_by_activity, -> { order activity_score_order: :desc } + + # Staleness is measured by the amount of activity_score lost since it was last updated. + # The factor 0.9 is chosen to make the decay curve closer to linear; an exponential + # curve would make recent items appear stale very quickly, due to the sharp dropoff. + scope :ordered_by_staleness, -> { order Arel.sql( + "coalesce(activity_score * (1 - power(0.9, julianday('now') - julianday(activity_score_at))), 0) desc" + ) } end def rescore diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index 9c2c212b0..680058043 100644 --- a/app/models/filter/fields.rb +++ b/app/models/filter/fields.rb @@ -1,7 +1,7 @@ module Filter::Fields extend ActiveSupport::Concern - INDEXES = %w[ most_discussed most_boosted newest oldest popped ] + INDEXES = %w[ most_discussed most_boosted most_stale newest oldest popped ] delegate :default_value?, to: :class diff --git a/test/models/bubble/scorable_test.rb b/test/models/bubble/scorable_test.rb index e55241b07..997add0dd 100644 --- a/test/models/bubble/scorable_test.rb +++ b/test/models/bubble/scorable_test.rb @@ -50,7 +50,32 @@ class Bubble::ScorableTest < ActiveSupport::TestCase travel_back bubble_new.boost! - assert_equal [ bubble_new, bubble_mid, bubble_old ], Bubble.where(id: [ bubble_old, bubble_mid, bubble_new ]).ordered_by_activity + assert_equal %w[ new mid old ], Bubble.where(id: [ bubble_old, bubble_mid, bubble_new ]).ordered_by_activity.map(&:title) + end + end + + test "items with old activity are more stale than those with none, or with new activity" do + with_current_user :kevin do + travel_to 20.days.ago + bubble_old = buckets(:writebook).bubbles.create! status: :published, title: "old" + bubble_new = buckets(:writebook).bubbles.create! status: :published, title: "new" + bubble_none = buckets(:writebook).bubbles.create! status: :published, title: "none" + + bubble_old.boost! + bubble_old.boost! + + travel_back + travel_to 2.days.ago + bubble_new.boost! + bubble_new.boost! + + travel_back + + assert_equal %w[ old new none ], Bubble.where(id: [ bubble_none, bubble_old, bubble_new ]).ordered_by_staleness.map(&:title) + + bubble_old.boost! + + assert_equal %w[ new old none ], Bubble.where(id: [ bubble_none, bubble_old, bubble_new ]).ordered_by_staleness.map(&:title) end end diff --git a/test/models/filter_test.rb b/test/models/filter_test.rb index 435b2bb35..24dcab200 100644 --- a/test/models/filter_test.rb +++ b/test/models/filter_test.rb @@ -110,6 +110,9 @@ class FilterTest < ActiveSupport::TestCase filters(:jz_assignments).update!(stages: [], assignees: [], tags: [], buckets: [ buckets(:writebook) ]) assert_equal "Most discussed in Writebook", filters(:jz_assignments).summary + + filters(:jz_assignments).update!(indexed_by: "most_stale") + assert_equal "Most stale in Writebook", filters(:jz_assignments).summary end test "params without a key-value pair" do