More dynamic activity scores
This introduces a more dynamic system of activity scoring, to improve the way bubbles "bubble up" due to their activity. There are a few different parts we can tune here, and it's likely we'll need to make adjustments once we get a feel for how this works in practice. The basic idea here is: - We assign points for certain types of event that happen on a bubble. A boost gets 1 point, a comment gets 10 points, and so on. - These points decay over time, at a rate of 50% per day. So old activity is worth much less than new activity. Bubbles should rise up quickly when acted upon, bit will float back down if left idle. - Some comments can score higher than others: the first comment from each person on a bubble is worth more (20) because it signals that more people are getting involved; and comments that follow a comment by a different author are also worth more (15) because that signals there's ongoing conversation between people, not just a series of notes being left by one individual. In terms of implementation, we persist the score on the bubble whenever it changes, but we handle the decay on the client side. That allows us to cache the bubble representation without having to continually change it while its activity decays. We also keep a separate `activity_score_order` attribute on the model. This can be used to sort the bubbles in order of "most active", without having to think about the decay.
This commit is contained in:
@@ -10,17 +10,4 @@ module BubblesHelper
|
|||||||
|
|
||||||
"--bubble-rotate: #{value}deg;"
|
"--bubble-rotate: #{value}deg;"
|
||||||
end
|
end
|
||||||
|
|
||||||
def bubble_size(bubble)
|
|
||||||
rank =
|
|
||||||
case bubble.activity_score
|
|
||||||
when 0..5 then "one"
|
|
||||||
when 6..10 then "two"
|
|
||||||
when 11..25 then "three"
|
|
||||||
when 26..50 then "four"
|
|
||||||
else "five"
|
|
||||||
end
|
|
||||||
|
|
||||||
"--bubble-size: var(--bubble-size-#{rank});"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
const SIZES = [ "one", "two", "three", "four", "five" ]
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = [ "bubble" ]
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
resize() {
|
||||||
|
const [ min, max ] = this.#getScoreRange()
|
||||||
|
|
||||||
|
this.bubbleTargets.forEach(bubble => {
|
||||||
|
const score = this.#currentBubbleScore(bubble)
|
||||||
|
const idx = Math.round((score - min) / (max - min) * (SIZES.length - 1))
|
||||||
|
|
||||||
|
bubble.style.setProperty("--bubble-size", `var(--bubble-size-${SIZES[idx]})`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#getScoreRange() {
|
||||||
|
var min = 0, max = 1;
|
||||||
|
|
||||||
|
this.bubbleTargets.forEach(bubble => {
|
||||||
|
const score = this.#currentBubbleScore(bubble)
|
||||||
|
|
||||||
|
min = Math.min(min, score)
|
||||||
|
max = Math.max(max, score)
|
||||||
|
})
|
||||||
|
|
||||||
|
return [ min, max ]
|
||||||
|
}
|
||||||
|
|
||||||
|
#currentBubbleScore(el) {
|
||||||
|
const score = el.dataset.activityScore
|
||||||
|
const scoreAt = el.dataset.activityScoreAt
|
||||||
|
const daysAgo = (Date.now() / 1000 - scoreAt) / (60 * 60 * 24)
|
||||||
|
|
||||||
|
return score / (2**daysAgo)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
class Bubble < ApplicationRecord
|
class Bubble < ApplicationRecord
|
||||||
include Assignable, Boostable, Colored, Commentable, Eventable,
|
include Assignable, Boostable, Colored, Commentable, Eventable,
|
||||||
Messages, Notifiable, Poppable, Searchable, Staged, Statuses, Taggable, Watchable
|
Messages, Notifiable, Poppable, Scorable, Searchable, Staged, Statuses, Taggable, Watchable
|
||||||
|
|
||||||
belongs_to :bucket, touch: true
|
belongs_to :bucket, touch: true
|
||||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||||
@@ -14,7 +14,6 @@ class Bubble < ApplicationRecord
|
|||||||
|
|
||||||
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
|
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
|
||||||
scope :chronologically, -> { order created_at: :asc, id: :asc }
|
scope :chronologically, -> { order created_at: :asc, id: :asc }
|
||||||
scope :ordered_by_activity, -> { order activity_score: :desc }
|
|
||||||
scope :in_bucket, ->(bucket) { where bucket: bucket }
|
scope :in_bucket, ->(bucket) { where bucket: bucket }
|
||||||
|
|
||||||
scope :indexed_by, ->(index) do
|
scope :indexed_by, ->(index) do
|
||||||
@@ -28,10 +27,6 @@ class Bubble < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rescore
|
|
||||||
update! activity_score: boosts_count + comments_count
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
def track_due_date_change
|
def track_due_date_change
|
||||||
if due_on.present?
|
if due_on.present?
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
module Bubble::Scorable
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
REFERENCE_DATE = Time.utc(2025, 1, 1)
|
||||||
|
|
||||||
|
included do
|
||||||
|
scope :ordered_by_activity, -> { order activity_score_order: :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.boosted? then 1
|
||||||
|
when event.comment&.first_by_author_on_bubble? 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 bubbles, since it represents the bubble'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(score) + days_diff
|
||||||
|
end
|
||||||
|
|
||||||
|
def last_scorable_activity_at
|
||||||
|
scorable_events.maximum(:created_at) || created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def scorable_events
|
||||||
|
events.where(action: [ :commented, :boosted ])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -10,6 +10,14 @@ class Comment < ApplicationRecord
|
|||||||
|
|
||||||
before_destroy :cleanup_events
|
before_destroy :cleanup_events
|
||||||
|
|
||||||
|
def first_by_author_on_bubble?
|
||||||
|
bubble_comments.many? && bubble_comments_prior.where(creator_id: creator_id).none?
|
||||||
|
end
|
||||||
|
|
||||||
|
def follows_comment_by_another_author?
|
||||||
|
bubble_comments.many? && bubble_comments_prior.last&.creator != creator
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def cleanup_events
|
def cleanup_events
|
||||||
@@ -21,4 +29,12 @@ class Comment < ApplicationRecord
|
|||||||
# Delete events that reference directly in particulars
|
# Delete events that reference directly in particulars
|
||||||
Event.where(particulars: { comment_id: id }).destroy_all
|
Event.where(particulars: { comment_id: id }).destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def bubble_comments_prior
|
||||||
|
bubble_comments.where(created_at: ...created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def bubble_comments
|
||||||
|
Comment.joins(:message).where(messages: { bubble: bubble })
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,9 +12,18 @@ class Event < ApplicationRecord
|
|||||||
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
scope :chronologically, -> { order created_at: :asc, id: :desc }
|
||||||
scope :non_boosts, -> { where.not action: :boosted }
|
scope :non_boosts, -> { where.not action: :boosted }
|
||||||
scope :boosts, -> { where action: :boosted }
|
scope :boosts, -> { where action: :boosted }
|
||||||
|
scope :comments, -> { where action: :commented }
|
||||||
|
|
||||||
after_create -> { bubble.update_auto_pop_at(created_at) }
|
after_create -> { bubble.update_auto_pop_at(created_at) }
|
||||||
|
|
||||||
|
def boosted?
|
||||||
|
action == "boosted"
|
||||||
|
end
|
||||||
|
|
||||||
|
def commented?
|
||||||
|
action == "commented"
|
||||||
|
end
|
||||||
|
|
||||||
def generate_notifications
|
def generate_notifications
|
||||||
Notifier.for(self)&.generate
|
Notifier.for(self)&.generate
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<% cache bubble do %>
|
<% cache bubble do %>
|
||||||
<div class="<%= class_names("bubble", drafted: bubble.drafted?, popped: bubble.popped?) %>"
|
<div class="<%= class_names("bubble", drafted: bubble.drafted?, popped: bubble.popped?) %>"
|
||||||
style="view-transition-name: bubble-<%= bubble.id -%>; --bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>"
|
style="view-transition-name: bubble-<%= bubble.id -%>; --bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %>"
|
||||||
|
data-bubble-size-target="bubble"
|
||||||
|
data-activity-score="<%= bubble.activity_score %>"
|
||||||
|
data-activity-score-at="<%= bubble.activity_score_at.to_i %>"
|
||||||
data-controller="animation upload-preview"
|
data-controller="animation upload-preview"
|
||||||
data-animation-play-class="bubble--wobble"
|
data-animation-play-class="bubble--wobble"
|
||||||
data-animation-play-on-load-value="true"
|
data-animation-play-on-load-value="true"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<section class="bubbles flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= @filter.id %>">
|
<section class="bubbles flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= @filter.id %>" data-controller="bubble-size" data-action="turbo:morph@window->bubble-size#resize">
|
||||||
<% if @bubbles.any? %>
|
<% if @bubbles.any? %>
|
||||||
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10), cached: true %>
|
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10), cached: true %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<%= link_to bubbles_path(bucket_ids: [ bucket ]), class: "border border-radius margin-block-end-half bubbles__container flex justify-center align-center position-relative" do %>
|
<%= link_to bubbles_path(bucket_ids: [ bucket ]), class: "border border-radius margin-block-end-half bubbles__container flex justify-center align-center position-relative" do %>
|
||||||
<div class="bubbles bucket__bubbles flex flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= bucket.id %>">
|
<div class="bubbles bucket__bubbles flex flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= bucket.id %>">
|
||||||
<% bucket.bubbles.active.published_or_drafted_by(Current.user).ordered_by_activity.limit(10).each do |bubble| %>
|
<% bucket.bubbles.active.published_or_drafted_by(Current.user).ordered_by_activity.limit(10).each do |bubble| %>
|
||||||
<div class="<%= class_names("bubble", drafted: bubble.drafted?) %>" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
<div class="<%= class_names("bubble", drafted: bubble.drafted?) %>" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %>"
|
||||||
|
data-bubble-size-target="bubble"
|
||||||
|
data-activity-score="<%= bubble.activity_score %>"
|
||||||
|
data-activity-score-at="<%= bubble.activity_score_at.to_i %>">
|
||||||
<span class="bubble__shape"></span>
|
<span class="bubble__shape"></span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="buckets margin-block-double unpad align-start justify-center flex flex-wrap gap">
|
<div class="buckets margin-block-double unpad align-start justify-center flex flex-wrap gap" data-controller="bubble-size">
|
||||||
<%= render @buckets, cached: true %>
|
<%= render @buckets, cached: true %>
|
||||||
</div>
|
</div>
|
||||||
<div class="buckets margin-block-double unpad align-start justify-center flex flex-wrap gap">
|
<div class="buckets margin-block-double unpad align-start justify-center flex flex-wrap gap" data-controller="bubble-size">
|
||||||
<%= render @filters %>
|
<%= render @filters %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<%= link_to bubbles_path(**filter.as_params), class: "border border-radius margin-block-end-half bubbles__container flex justify-center align-center position-relative" do %>
|
<%= link_to bubbles_path(**filter.as_params), class: "border border-radius margin-block-end-half bubbles__container flex justify-center align-center position-relative" do %>
|
||||||
<div class="bubbles bucket__bubbles flex flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= filter.id %>">
|
<div class="bubbles bucket__bubbles flex flex-wrap gap justify-center align-end" style="view-transition-name: bubbles_<%= filter.id %>">
|
||||||
<% filter.bubbles.ordered_by_activity.limit(10).each do |bubble| %>
|
<% filter.bubbles.ordered_by_activity.limit(10).each do |bubble| %>
|
||||||
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
<div class="<%= class_names("bubble", drafted: bubble.drafted?) %>" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %>"
|
||||||
|
data-bubble-size-target="bubble"
|
||||||
|
data-activity-score="<%= bubble.activity_score %>"
|
||||||
|
data-activity-score-at="<%= bubble.activity_score_at.to_i %>">
|
||||||
<span class="bubble__shape"></span>
|
<span class="bubble__shape"></span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
class AddActivityScoreAtToBubbles < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
change_table :bubbles do |t|
|
||||||
|
t.datetime :activity_score_at
|
||||||
|
t.float :activity_score_order, null: false, default: 0
|
||||||
|
t.change :activity_score, :float, null: false, default: 0
|
||||||
|
|
||||||
|
t.index :activity_score_order, order: :desc
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+5
-2
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.1].define(version: 2025_02_24_152047) do
|
ActiveRecord::Schema[8.1].define(version: 2025_03_04_140641) do
|
||||||
create_table "accesses", force: :cascade do |t|
|
create_table "accesses", force: :cascade do |t|
|
||||||
t.integer "bucket_id", null: false
|
t.integer "bucket_id", null: false
|
||||||
t.integer "user_id", null: false
|
t.integer "user_id", null: false
|
||||||
@@ -105,9 +105,12 @@ ActiveRecord::Schema[8.1].define(version: 2025_02_24_152047) do
|
|||||||
t.integer "boosts_count", default: 0, null: false
|
t.integer "boosts_count", default: 0, null: false
|
||||||
t.integer "stage_id"
|
t.integer "stage_id"
|
||||||
t.integer "comments_count", default: 0, null: false
|
t.integer "comments_count", default: 0, null: false
|
||||||
t.integer "activity_score", default: 0, null: false
|
t.float "activity_score", default: 0.0, null: false
|
||||||
t.text "status", default: "creating", null: false
|
t.text "status", default: "creating", null: false
|
||||||
t.datetime "auto_pop_at", null: false
|
t.datetime "auto_pop_at", null: false
|
||||||
|
t.datetime "activity_score_at"
|
||||||
|
t.float "activity_score_order", default: 0.0, null: false
|
||||||
|
t.index ["activity_score_order"], name: "index_bubbles_on_activity_score_order", order: :desc
|
||||||
t.index ["auto_pop_at"], name: "index_bubbles_on_auto_pop_at"
|
t.index ["auto_pop_at"], name: "index_bubbles_on_auto_pop_at"
|
||||||
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
|
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
|
||||||
t.index ["stage_id"], name: "index_bubbles_on_stage_id"
|
t.index ["stage_id"], name: "index_bubbles_on_stage_id"
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Bubble::ScorableTest < ActiveSupport::TestCase
|
||||||
|
test "a bubble has a score that increases with activity" do
|
||||||
|
bubble = bubbles(:logo)
|
||||||
|
|
||||||
|
score = bubble.activity_score
|
||||||
|
assert_operator score, :>, 0
|
||||||
|
|
||||||
|
with_current_user :kevin do
|
||||||
|
bubble.capture Comment.create(body: "This is exciting!")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_operator bubble.activity_score, :>, score
|
||||||
|
end
|
||||||
|
|
||||||
|
test "commenting on a bubble boosts its score more than boosting it" do
|
||||||
|
bubble = bubbles(:logo)
|
||||||
|
bubble.rescore
|
||||||
|
|
||||||
|
comment_change = capture_change -> { bubble.activity_score } do
|
||||||
|
with_current_user :kevin do
|
||||||
|
bubble.capture Comment.create(body: "This is exciting!")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
boost_change = capture_change -> { bubble.activity_score } do
|
||||||
|
with_current_user :kevin do
|
||||||
|
bubble.boost!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_operator comment_change, :>, boost_change
|
||||||
|
end
|
||||||
|
|
||||||
|
test "recent activity counts more than older activity in the ordering" do
|
||||||
|
with_current_user :kevin do
|
||||||
|
travel_to 5.days.ago
|
||||||
|
bubble_old = buckets(:writebook).bubbles.create! status: :published, title: "old"
|
||||||
|
bubble_mid = buckets(:writebook).bubbles.create! status: :published, title: "mid"
|
||||||
|
bubble_new = buckets(:writebook).bubbles.create! status: :published, title: "new"
|
||||||
|
|
||||||
|
bubble_old.boost!
|
||||||
|
bubble_old.boost!
|
||||||
|
|
||||||
|
travel_back
|
||||||
|
travel_to 2.days.ago
|
||||||
|
bubble_mid.boost!
|
||||||
|
|
||||||
|
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
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,8 +14,10 @@ class BubbleTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "boosting" do
|
test "boosting" do
|
||||||
assert_difference %w[ bubbles(:logo).boosts_count bubbles(:logo).activity_score Event.count ], +1 do
|
assert_changes -> { bubbles(:logo).activity_score } do
|
||||||
bubbles(:logo).boost!(bubbles(:logo).boosts_count+ 1)
|
assert_difference -> { bubbles(:logo).boosts_count }, +1 do
|
||||||
|
bubbles(:logo).boost!(bubbles(:logo).boosts_count+ 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -72,11 +74,6 @@ class BubbleTest < ActiveSupport::TestCase
|
|||||||
assert_includes Bubble.search("haggis"), bubble
|
assert_includes Bubble.search("haggis"), bubble
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ordering by activity" do
|
|
||||||
bubbles(:layout).tap { |b| b.update!(boosts_count: 1_000) }.rescore
|
|
||||||
assert_equal bubbles(:layout, :logo, :shipping, :text), Bubble.ordered_by_activity
|
|
||||||
end
|
|
||||||
|
|
||||||
test "ordering by comments" do
|
test "ordering by comments" do
|
||||||
assert_equal bubbles(:logo, :layout, :shipping, :text), Bubble.ordered_by_comments
|
assert_equal bubbles(:logo, :layout, :shipping, :text), Bubble.ordered_by_comments
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,12 +12,54 @@ class CommentTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "updating bubble counter" do
|
test "updating bubble counter" do
|
||||||
assert_difference %w[ bubbles(:logo).comments_count bubbles(:logo).activity_score ], +1 do
|
assert_difference -> { bubbles(:logo).comments_count } do
|
||||||
bubbles(:logo).capture Comment.new(body: "I'd prefer something more rustic")
|
assert_changes -> { bubbles(:logo).activity_score } do
|
||||||
|
bubbles(:logo).capture Comment.new(body: "I'd prefer something more rustic")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assert_difference %w[ bubbles(:logo).comments_count bubbles(:logo).activity_score ], -1 do
|
assert_difference -> { bubbles(:logo).comments_count }, -1 do
|
||||||
bubbles(:logo).messages.comments.last.destroy
|
assert_changes -> { bubbles(:logo).activity_score } do
|
||||||
|
bubbles(:logo).messages.comments.last.destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "first_by_author_on_bubble?" do
|
||||||
|
assert_not Comment.new.first_by_author_on_bubble?
|
||||||
|
|
||||||
|
with_current_user :david do
|
||||||
|
comment = Comment.new.tap { |c| bubbles(:logo).capture c }
|
||||||
|
assert comment.first_by_author_on_bubble?
|
||||||
|
|
||||||
|
comment = Comment.new.tap { |c| bubbles(:logo).capture c }
|
||||||
|
assert_not comment.first_by_author_on_bubble?
|
||||||
|
end
|
||||||
|
|
||||||
|
with_current_user :kevin do
|
||||||
|
comment = Comment.new.tap { |c| bubbles(:logo).capture c }
|
||||||
|
assert_not comment.first_by_author_on_bubble?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "follows_comment_by_another_author?" do
|
||||||
|
assert_not Comment.new.follows_comment_by_another_author?
|
||||||
|
|
||||||
|
bubble = buckets(:writebook).bubbles.create!
|
||||||
|
|
||||||
|
with_current_user :david do
|
||||||
|
comment = Comment.new.tap { |c| bubble.capture c }
|
||||||
|
assert_not comment.follows_comment_by_another_author?
|
||||||
|
end
|
||||||
|
|
||||||
|
with_current_user :kevin do
|
||||||
|
comment = Comment.new.tap { |c| bubble.capture c }
|
||||||
|
assert comment.follows_comment_by_another_author?
|
||||||
|
end
|
||||||
|
|
||||||
|
with_current_user :david do
|
||||||
|
comment = Comment.new.tap { |c| bubble.capture c }
|
||||||
|
assert comment.follows_comment_by_another_author?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class FilterTest < ActiveSupport::TestCase
|
|||||||
assert_equal [ @new_bubble ], filter.bubbles
|
assert_equal [ @new_bubble ], filter.bubbles
|
||||||
|
|
||||||
filter = users(:david).filters.new terms: [ "haggis" ]
|
filter = users(:david).filters.new terms: [ "haggis" ]
|
||||||
assert_equal bubbles(:logo, :layout), filter.bubbles
|
assert_equal bubbles(:logo, :layout).sort, filter.bubbles.sort
|
||||||
|
|
||||||
filter = users(:david).filters.new terms: [ "haggis", "love" ]
|
filter = users(:david).filters.new terms: [ "haggis", "love" ]
|
||||||
assert_equal [ bubbles(:logo) ], filter.bubbles
|
assert_equal [ bubbles(:logo) ], filter.bubbles
|
||||||
|
|||||||
+1
-1
@@ -10,6 +10,6 @@ module ActiveSupport
|
|||||||
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||||
fixtures :all
|
fixtures :all
|
||||||
|
|
||||||
include SessionTestHelper
|
include ChangeTestHelper, SessionTestHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
module ChangeTestHelper
|
||||||
|
def capture_change(target)
|
||||||
|
before = target.call
|
||||||
|
yield
|
||||||
|
after = target.call
|
||||||
|
after - before
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user