Merge pull request #414 from basecamp/drop-activity-score
Drop activity scoring
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
const SIZES = [ "one", "two", "three", "four", "five" ]
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "card" ]
|
||||
|
||||
connect() {
|
||||
this.resize()
|
||||
}
|
||||
|
||||
resize() {
|
||||
const [ min, max ] = this.#getScoreRange()
|
||||
|
||||
this.cardTargets.forEach(card => {
|
||||
const score = this.#currentCardScore(card)
|
||||
const idx = Math.round((score - min) / (max - min) * (SIZES.length - 1))
|
||||
|
||||
card.style.setProperty("--card-size", `var(--card-size-${SIZES[idx]})`)
|
||||
})
|
||||
}
|
||||
|
||||
#getScoreRange() {
|
||||
var min = 0, max = 1;
|
||||
|
||||
this.cardTargets.forEach(card => {
|
||||
const score = this.#currentCardScore(card)
|
||||
|
||||
min = Math.min(min, score)
|
||||
max = Math.max(max, score)
|
||||
})
|
||||
|
||||
return [ min, max ]
|
||||
}
|
||||
|
||||
#currentCardScore(el) {
|
||||
const score = el.dataset.activityScore
|
||||
const scoreAt = el.dataset.activityScoreAt
|
||||
const daysAgo = (Date.now() / 1000 - scoreAt) / (60 * 60 * 24)
|
||||
|
||||
return score / (2**daysAgo)
|
||||
}
|
||||
}
|
||||
+6
-7
@@ -1,6 +1,6 @@
|
||||
class Card < ApplicationRecord
|
||||
include Assignable, Colored, DraftCommenting, Engageable, Eventable, Golden,
|
||||
Messages, Notifiable, Pinnable, Closeable, Scorable, Searchable, Staged,
|
||||
Messages, Notifiable, Pinnable, Closeable, Searchable, Staged,
|
||||
Statuses, Taggable, Watchable
|
||||
|
||||
belongs_to :collection, touch: true
|
||||
@@ -21,12 +21,11 @@ class Card < ApplicationRecord
|
||||
|
||||
scope :indexed_by, ->(index) do
|
||||
case index
|
||||
when "most_active" then ordered_by_activity
|
||||
when "newest" then reverse_chronologically
|
||||
when "oldest" then chronologically
|
||||
when "latest" then latest
|
||||
when "stalled" then ordered_by_staleness
|
||||
when "closed" then closed
|
||||
when "newest" then reverse_chronologically
|
||||
when "oldest" then chronologically
|
||||
when "latest" then latest
|
||||
when "stalled" then chronologically
|
||||
when "closed" then closed
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
module Card::Scorable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
REFERENCE_DATE = Time.utc(2025, 1, 1)
|
||||
|
||||
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
|
||||
score = calculate_activity_score
|
||||
score_at = last_scorable_activity_at
|
||||
|
||||
update! \
|
||||
activity_score: score,
|
||||
activity_score_at: score_at,
|
||||
activity_score_order: event_score_reference(score, score_at)
|
||||
end
|
||||
|
||||
private
|
||||
def calculate_activity_score
|
||||
scorable_events.sum { |event| event_score(event) }
|
||||
end
|
||||
|
||||
def event_score(event)
|
||||
days_ago = (last_scorable_activity_at - event.created_at) / 1.day
|
||||
event_weight(event) / (2**days_ago)
|
||||
end
|
||||
|
||||
def event_weight(event)
|
||||
case
|
||||
when event.comment&.first_by_author_on_card? then 20
|
||||
when event.comment&.follows_comment_by_another_author? then 15
|
||||
when event.commented? then 10
|
||||
else 0
|
||||
end
|
||||
end
|
||||
|
||||
def event_score_reference(score, activity_at)
|
||||
# The reference score is used to make the activity score comparable
|
||||
# across different cards, since it represents the card's activity
|
||||
# level at a consistent point in time.
|
||||
#
|
||||
# We store this as log2 to tame the huge/tiny numbers we'd otherwise get
|
||||
# when activity is far from the reference date.
|
||||
days_diff = (activity_at - REFERENCE_DATE) / 1.day
|
||||
Math.log2(1.0 + score) + days_diff
|
||||
end
|
||||
|
||||
def last_scorable_activity_at
|
||||
scorable_events.maximum(:created_at) || created_at
|
||||
end
|
||||
|
||||
def scorable_events
|
||||
events.where(action: :commented)
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
<section id="doing-cards" class="cards cards--doing" style="view-transition-name: cards-container;" data-controller="card-size" data-action="turbo:morph@window->card-size#resize">
|
||||
<section id="doing-cards" class="cards cards--doing" style="view-transition-name: cards-container;">
|
||||
<% if workflow = filter.single_workflow %>
|
||||
<%= render "cards/index/workflow_filter", workflow: workflow, filter: filter %>
|
||||
<% else %>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
class RemoveActivityFromCards < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
remove_index :cards, :activity_score_order
|
||||
remove_column :cards, :activity_score_order
|
||||
remove_column :cards, :activity_score_at
|
||||
remove_column :cards, :activity_score
|
||||
end
|
||||
end
|
||||
Generated
+1
-5
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_04_15_131804) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_04_18_150259) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -108,9 +108,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_15_131804) do
|
||||
end
|
||||
|
||||
create_table "cards", force: :cascade do |t|
|
||||
t.float "activity_score", default: 0.0, null: false
|
||||
t.datetime "activity_score_at"
|
||||
t.float "activity_score_order", default: 0.0, null: false
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "creator_id", null: false
|
||||
@@ -120,7 +117,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_15_131804) do
|
||||
t.text "status", default: "creating", null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["activity_score_order"], name: "index_cards_on_activity_score_order"
|
||||
t.index ["collection_id"], name: "index_cards_on_collection_id"
|
||||
t.index ["last_active_at", "status"], name: "index_cards_on_last_active_at_and_status"
|
||||
t.index ["stage_id"], name: "index_cards_on_stage_id"
|
||||
|
||||
+33
-87
@@ -21,7 +21,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &24 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &22 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: collection_id
|
||||
cast_type: *1
|
||||
@@ -80,7 +80,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &28 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: user_id
|
||||
cast_type: *1
|
||||
@@ -374,47 +374,9 @@ columns:
|
||||
- *9
|
||||
cards:
|
||||
- *5
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: activity_score
|
||||
cast_type: &22 !ruby/object:ActiveModel::Type::Float
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &23 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: float
|
||||
type: :float
|
||||
limit:
|
||||
precision:
|
||||
scale:
|
||||
'null': false
|
||||
default: 0.0
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: activity_score_at
|
||||
cast_type: *3
|
||||
sql_type_metadata: *4
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: activity_score_order
|
||||
cast_type: *22
|
||||
sql_type_metadata: *23
|
||||
'null': false
|
||||
default: 0.0
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *24
|
||||
- *22
|
||||
- *6
|
||||
- &25 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &23 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: creator_id
|
||||
cast_type: *1
|
||||
@@ -427,12 +389,12 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: due_on
|
||||
cast_type: &26 !ruby/object:ActiveRecord::Type::Date
|
||||
cast_type: &24 !ruby/object:ActiveRecord::Type::Date
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
timezone:
|
||||
sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &25 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: date
|
||||
type: :date
|
||||
limit:
|
||||
@@ -473,7 +435,7 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- &32 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &30 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: title
|
||||
cast_type: *7
|
||||
@@ -528,11 +490,11 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: all_access
|
||||
cast_type: &33 !ruby/object:ActiveModel::Type::Boolean
|
||||
cast_type: &31 !ruby/object:ActiveModel::Type::Boolean
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &34 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &32 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: boolean
|
||||
type: :boolean
|
||||
limit:
|
||||
@@ -544,7 +506,7 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *6
|
||||
- *25
|
||||
- *23
|
||||
- *10
|
||||
- *9
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
@@ -558,15 +520,15 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
collections_filters:
|
||||
- *24
|
||||
- *22
|
||||
- *18
|
||||
comments:
|
||||
- *5
|
||||
- *6
|
||||
- *25
|
||||
- *23
|
||||
- *9
|
||||
creators_filters:
|
||||
- *25
|
||||
- *23
|
||||
- *18
|
||||
event_summaries:
|
||||
- *5
|
||||
@@ -586,12 +548,12 @@ columns:
|
||||
comment:
|
||||
- *21
|
||||
- *6
|
||||
- *25
|
||||
- *23
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: due_date
|
||||
cast_type: *26
|
||||
sql_type_metadata: *27
|
||||
cast_type: *24
|
||||
sql_type_metadata: *25
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
@@ -600,11 +562,11 @@ columns:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: particulars
|
||||
cast_type: &28 !ruby/object:ActiveRecord::Type::Json
|
||||
cast_type: &26 !ruby/object:ActiveRecord::Type::Json
|
||||
precision:
|
||||
scale:
|
||||
limit:
|
||||
sql_type_metadata: &29 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type_metadata: &27 !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata
|
||||
sql_type: json
|
||||
type: :json
|
||||
limit:
|
||||
@@ -629,12 +591,12 @@ columns:
|
||||
filters:
|
||||
- *5
|
||||
- *6
|
||||
- *25
|
||||
- *23
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: fields
|
||||
cast_type: *28
|
||||
sql_type_metadata: *29
|
||||
cast_type: *26
|
||||
sql_type_metadata: *27
|
||||
'null': false
|
||||
default: "{}"
|
||||
default_function:
|
||||
@@ -665,7 +627,7 @@ columns:
|
||||
comment:
|
||||
filters_tags:
|
||||
- *18
|
||||
- &31 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &29 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: tag_id
|
||||
cast_type: *1
|
||||
@@ -745,13 +707,13 @@ columns:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
- *30
|
||||
- *28
|
||||
pins:
|
||||
- *5
|
||||
- *21
|
||||
- *6
|
||||
- *9
|
||||
- *30
|
||||
- *28
|
||||
reactions:
|
||||
- *5
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
@@ -831,25 +793,25 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *30
|
||||
- *28
|
||||
taggings:
|
||||
- *5
|
||||
- *21
|
||||
- *6
|
||||
- *31
|
||||
- *29
|
||||
- *9
|
||||
tags:
|
||||
- *5
|
||||
- *6
|
||||
- *32
|
||||
- *30
|
||||
- *9
|
||||
users:
|
||||
- *5
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: active
|
||||
cast_type: *33
|
||||
sql_type_metadata: *34
|
||||
cast_type: *31
|
||||
sql_type_metadata: *32
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -893,12 +855,12 @@ columns:
|
||||
- *21
|
||||
- *6
|
||||
- *9
|
||||
- *30
|
||||
- *28
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: watching
|
||||
cast_type: *33
|
||||
sql_type_metadata: *34
|
||||
cast_type: *31
|
||||
sql_type_metadata: *32
|
||||
'null': false
|
||||
default: true
|
||||
default_function:
|
||||
@@ -1350,22 +1312,6 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: cards
|
||||
name: index_cards_on_activity_score_order
|
||||
unique: false
|
||||
columns:
|
||||
- activity_score_order
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
closure_reasons: []
|
||||
closures:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
@@ -1992,4 +1938,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250415131804
|
||||
version: 20250418150259
|
||||
|
||||
Vendored
-2
@@ -4,7 +4,6 @@ logo:
|
||||
title: The logo isn't big enough
|
||||
due_on: <%= 3.days.from_now %>
|
||||
created_at: <%= 1.week.ago %>
|
||||
activity_score: 7
|
||||
status: published
|
||||
last_active_at: <%= Time.current %>
|
||||
|
||||
@@ -13,7 +12,6 @@ layout:
|
||||
creator: david
|
||||
title: Layout is broken
|
||||
created_at: <%= 1.week.ago %>
|
||||
activity_score: 1
|
||||
status: published
|
||||
last_active_at: <%= Time.current %>
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Card::ScorableTest < ActiveSupport::TestCase
|
||||
test "cards with no activity have a valid activity_score_order" do
|
||||
card = Card.create! collection: collections(:writebook), creator: users(:kevin)
|
||||
|
||||
card.rescore
|
||||
|
||||
assert card.activity_score.zero?
|
||||
assert_not card.activity_score_order.infinite?
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user