From 35a31e32032f7f05fa1bc247b587e19e32b14ed6 Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Wed, 23 Oct 2024 14:10:04 -0600 Subject: [PATCH] Model threadables --- app/models/bubble/thread.rb | 31 +++------- app/models/bubble/thread/rollup.rb | 60 ------------------- app/models/bubble/threaded.rb | 10 +++- app/models/comment.rb | 2 +- app/models/concerns/.keep | 0 app/models/concerns/threadable.rb | 11 ++++ app/models/event.rb | 7 ++- app/models/rollup.rb | 7 +++ app/models/thread_entry.rb | 7 +++ .../20241022174801_create_thread_entries.rb | 10 ++++ db/migrate/20241022180042_create_rollups.rb | 9 +++ ...22180133_add_rollup_reference_to_events.rb | 6 ++ db/schema.rb | 24 +++++++- test/fixtures/comments.yml | 1 + test/fixtures/events.yml | 12 ++++ test/fixtures/rollups.yml | 20 +++++++ test/fixtures/thread_entries.yml | 28 +++++++++ 17 files changed, 156 insertions(+), 89 deletions(-) delete mode 100644 app/models/bubble/thread/rollup.rb delete mode 100644 app/models/concerns/.keep create mode 100644 app/models/concerns/threadable.rb create mode 100644 app/models/rollup.rb create mode 100644 app/models/thread_entry.rb create mode 100644 db/migrate/20241022174801_create_thread_entries.rb create mode 100644 db/migrate/20241022180042_create_rollups.rb create mode 100644 db/migrate/20241022180133_add_rollup_reference_to_events.rb create mode 100644 test/fixtures/rollups.yml create mode 100644 test/fixtures/thread_entries.yml diff --git a/app/models/bubble/thread.rb b/app/models/bubble/thread.rb index 8b0e28be5..9495c4bc8 100644 --- a/app/models/bubble/thread.rb +++ b/app/models/bubble/thread.rb @@ -1,35 +1,20 @@ class Bubble::Thread - attr_reader :bubble - def initialize(bubble) @bubble = bubble end def entries - sorted_entries.chunk_while { |a, b| consecutive_events?(a, b) }.map.with_index { |entries, index| roll_up(entries, index) } + @entries ||= bubble.thread_entries end - def to_partial_path - "bubbles/threads/thread" + def latest_rollup + if entries.last&.rollup? + entries.last.rollup + else + Rollup.new bubble: bubble + end end private - delegate :events, :comments, to: :bubble, private: true - - def sorted_entries - (events + comments).sort_by(&:created_at) - end - - def consecutive_events?(a, b) - [ a, b ] in [ Event, Event ] - end - - def roll_up(entries, position) - case entries.first - when Comment - entries.sole - when Event - Rollup.new self, entries, first_position: position.zero? - end - end + attr_reader :bubble end diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb deleted file mode 100644 index daa25c32b..000000000 --- a/app/models/bubble/thread/rollup.rb +++ /dev/null @@ -1,60 +0,0 @@ -class Bubble::Thread::Rollup - def initialize(thread, entries, first_position: false) - @thread = thread - @entries = entries - @first_position = first_position - end - - def body - collapsed_entries.map { |entry, chunk_size| summarize(entry, chunk_size) }.to_sentence.upcase_first - end - - def to_partial_path - "bubbles/threads/rollup" - end - - private - attr_reader :thread, :entries, :first_position - - delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true - - def collapsed_entries - sorted_entries.chunk_while { |a, b| repeated_boosts?(a, b) }.map { |chunk| [ chunk.last, chunk.size ] } - end - - def sorted_entries - entries.sort_by do |entry| - case entry.action - when "created" then [ 1, entry.created_at ] - when "assigned" then [ 2, entry.created_at ] - when "staged", "unstaged" then [ 3, entry.created_at ] - when "boosted" then [ 4, entry.creator, entry.created_at ] - end - end - end - - def repeated_boosts?(a, b) - a.action == "boosted" && a.slice(:action, :creator_id) == b.slice(:action, :creator_id) - end - - def summarize(entry, chunk_size) - case entry.action - when "created" - "added by #{entry.creator.name} #{time_ago_in_words(entry.created_at)} ago" - when "assigned" - summary = "assigned to #{entry.assignees.map(&:name).to_sentence}" - summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? - summary - when "staged" - "#{entry.creator.name} moved this to '#{entry.stage.name}'" - when "unstaged" - "#{entry.creator.name} removed this from '#{entry.stage.name}'" - when "boosted" - "#{entry.creator.name} +#{chunk_size}" - end - end - - def first_position? - first_position - end -end diff --git a/app/models/bubble/threaded.rb b/app/models/bubble/threaded.rb index 22885f62a..15c2bedd8 100644 --- a/app/models/bubble/threaded.rb +++ b/app/models/bubble/threaded.rb @@ -1,5 +1,13 @@ module Bubble::Threaded + extend ActiveSupport::Concern + + included do + has_many :thread_entries, -> { chronologically }, dependent: :destroy + + delegate :latest_rollup, to: :thread + end + def thread - Bubble::Thread.new self + @thread ||= Bubble::Thread.new self end end diff --git a/app/models/comment.rb b/app/models/comment.rb index b4a342e6b..7b2da9c82 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,5 @@ class Comment < ApplicationRecord - include Searchable + include Searchable, Threadable belongs_to :bubble, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/models/concerns/threadable.rb b/app/models/concerns/threadable.rb new file mode 100644 index 000000000..10759aa98 --- /dev/null +++ b/app/models/concerns/threadable.rb @@ -0,0 +1,11 @@ +module Threadable + extend ActiveSupport::Concern + + included do + has_one :thread_entry, as: :threadable, dependent: :destroy + + after_create { create_thread_entry! bubble: bubble } + after_update { bubble.touch } + after_touch { bubble.touch } + end +end diff --git a/app/models/event.rb b/app/models/event.rb index 7946e79fe..9c578f24d 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,12 +1,13 @@ class Event < ApplicationRecord - THREADABLE_ACTIONS = %w[ assigned boosted created staged unstaged ] - include Assignments, Stages belongs_to :creator, class_name: "User" belongs_to :bubble, touch: true + belongs_to :rollup, default: -> { bubble.latest_rollup } has_one :account, through: :creator - scope :threadable, -> { where action: THREADABLE_ACTIONS } + store_accessor :particulars, :creator_name + + scope :chronologically, -> { order created_at: :asc, id: :desc } end diff --git a/app/models/rollup.rb b/app/models/rollup.rb new file mode 100644 index 000000000..1968e4ce8 --- /dev/null +++ b/app/models/rollup.rb @@ -0,0 +1,7 @@ +class Rollup < ApplicationRecord + include Threadable + + belongs_to :bubble + + has_many :events, -> { chronologically } +end diff --git a/app/models/thread_entry.rb b/app/models/thread_entry.rb new file mode 100644 index 000000000..3efa6b97f --- /dev/null +++ b/app/models/thread_entry.rb @@ -0,0 +1,7 @@ +class ThreadEntry < ApplicationRecord + belongs_to :bubble + + delegated_type :threadable, types: %w[ Comment Rollup ] + + scope :chronologically, -> { order created_at: :asc, id: :desc } +end diff --git a/db/migrate/20241022174801_create_thread_entries.rb b/db/migrate/20241022174801_create_thread_entries.rb new file mode 100644 index 000000000..73b3a9e3b --- /dev/null +++ b/db/migrate/20241022174801_create_thread_entries.rb @@ -0,0 +1,10 @@ +class CreateThreadEntries < ActiveRecord::Migration[8.0] + def change + create_table :thread_entries do |t| + t.references :bubble, null: false, foreign_key: true + t.references :threadable, polymorphic: true, null: false, index: { unique: true } + + t.timestamps + end + end +end diff --git a/db/migrate/20241022180042_create_rollups.rb b/db/migrate/20241022180042_create_rollups.rb new file mode 100644 index 000000000..b42f1689c --- /dev/null +++ b/db/migrate/20241022180042_create_rollups.rb @@ -0,0 +1,9 @@ +class CreateRollups < ActiveRecord::Migration[8.0] + def change + create_table :rollups do |t| + t.references :bubble, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241022180133_add_rollup_reference_to_events.rb b/db/migrate/20241022180133_add_rollup_reference_to_events.rb new file mode 100644 index 000000000..0f5697d9a --- /dev/null +++ b/db/migrate/20241022180133_add_rollup_reference_to_events.rb @@ -0,0 +1,6 @@ +class AddRollupReferenceToEvents < ActiveRecord::Migration[8.0] + def change + # FIXME: add null: false in another migration after data migrations happen + add_reference :events, :rollup, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 49c2ebde3..2fe46fbf7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do +ActiveRecord::Schema[8.0].define(version: 2024_10_22_180133) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -117,8 +117,10 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do t.string "action", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "rollup_id" t.index ["bubble_id", "action"], name: "index_events_on_bubble_id_and_action" t.index ["creator_id"], name: "index_events_on_creator_id" + t.index ["rollup_id"], name: "index_events_on_rollup_id" end create_table "pops", force: :cascade do |t| @@ -130,6 +132,13 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do t.index ["user_id"], name: "index_pops_on_user_id" end + create_table "rollups", force: :cascade do |t| + t.integer "bubble_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bubble_id"], name: "index_rollups_on_bubble_id" + end + create_table "sessions", force: :cascade do |t| t.integer "user_id", null: false t.string "ip_address" @@ -156,6 +165,16 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do t.index ["account_id"], name: "index_tags_on_account_id" end + create_table "thread_entries", force: :cascade do |t| + t.integer "bubble_id", null: false + t.string "threadable_type", null: false + t.integer "threadable_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bubble_id"], name: "index_thread_entries_on_bubble_id" + t.index ["threadable_type", "threadable_id"], name: "index_thread_entries_on_threadable", unique: true + end + create_table "users", force: :cascade do |t| t.integer "account_id", null: false t.string "name", null: false @@ -187,11 +206,14 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_18_223027) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "bubbles", "workflow_stages", column: "stage_id" + add_foreign_key "events", "rollups" add_foreign_key "pops", "bubbles" add_foreign_key "pops", "users" + add_foreign_key "rollups", "bubbles" add_foreign_key "sessions", "users" add_foreign_key "taggings", "bubbles" add_foreign_key "taggings", "tags" + add_foreign_key "thread_entries", "bubbles" add_foreign_key "users", "accounts" add_foreign_key "workflow_stages", "workflows" add_foreign_key "workflows", "accounts" diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml index 10ac4cca7..f0abd276c 100644 --- a/test/fixtures/comments.yml +++ b/test/fixtures/comments.yml @@ -8,6 +8,7 @@ logo_agreement_kevin: body: Same, let’s do it. creator: kevin bubble: logo + created_at: <%= 2.hours.ago %> layout_overflowing_david: body: The text is overflowing the container. diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml index 2cae139cb..ff649e9c6 100644 --- a/test/fixtures/events.yml +++ b/test/fixtures/events.yml @@ -4,6 +4,7 @@ logo_created: action: created particulars: <%= { creator_name: "David" }.to_json %> created_at: <%= 1.week.ago %> + rollup: logo_initial_activity logo_assignment_jz: creator: david @@ -11,6 +12,7 @@ logo_assignment_jz: action: assigned particulars: <%= { assignee_names: [ "JZ" ], creator_name: "David" }.to_json %> created_at: <%= 1.week.ago + 1.hour %> + rollup: logo_initial_activity logo_boost_dhh: creator: david @@ -18,6 +20,7 @@ logo_boost_dhh: action: boosted particulars: <%= { creator_name: "David" }.to_json %> created_at: <%= 1.week.ago + 2.hours %> + rollup: logo_initial_activity logo_assignment_km: creator: david @@ -25,6 +28,7 @@ logo_assignment_km: action: assigned particulars: <%= { assignee_names: [ "Kevin" ], creator_name: "David" }.to_json %> created_at: <%= 1.day.ago %> + rollup: logo_second_activity logo_boost_km1: creator: kevin @@ -32,6 +36,7 @@ logo_boost_km1: action: boosted particulars: <%= { creator_name: "Kevin" }.to_json %> created_at: <%= 1.day.ago + 1.hour %> + rollup: logo_second_activity logo_boost_km2: creator: kevin @@ -39,6 +44,7 @@ logo_boost_km2: action: boosted particulars: <%= { creator_name: "Kevin" }.to_json %> created_at: <%= 1.day.ago + 2.hours %> + rollup: logo_second_activity logo_boost_jz1: creator: jz @@ -46,6 +52,7 @@ logo_boost_jz1: action: boosted particulars: <%= { creator_name: "JZ" }.to_json %> created_at: <%= 1.day.ago + 3.hours %> + rollup: logo_second_activity logo_boost_jz2: creator: jz @@ -53,6 +60,7 @@ logo_boost_jz2: action: boosted particulars: <%= { creator_name: "JZ" }.to_json %> created_at: <%= 1.hour.ago %> + rollup: logo_third_activity layout_created: creator: david @@ -60,6 +68,7 @@ layout_created: action: created particulars: <%= { creator_name: "David" }.to_json %> created_at: <%= 1.week.ago %> + rollup: layout_initial_activity layout_assignment_jz: creator: david @@ -67,6 +76,7 @@ layout_assignment_jz: action: assigned particulars: <%= { assignee_names: [ "JZ" ], creator_name: "David" }.to_json %> created_at: <%= 1.hour.ago %> + rollup: layout_initial_activity text_created: creator: kevin @@ -74,6 +84,7 @@ text_created: action: created particulars: <%= { creator_name: "Kevin" }.to_json %> created_at: <%= 1.week.ago %> + rollup: text_initial_activity shipping_created: creator: kevin @@ -81,3 +92,4 @@ shipping_created: action: created particulars: <%= { creator_name: "Kevin" }.to_json %> created_at: <%= 1.week.ago %> + rollup: shipping_initial_activity diff --git a/test/fixtures/rollups.yml b/test/fixtures/rollups.yml new file mode 100644 index 000000000..abf326997 --- /dev/null +++ b/test/fixtures/rollups.yml @@ -0,0 +1,20 @@ +logo_initial_activity: + bubble: logo + created_at: <%= 1.week.ago %> + +logo_second_activity: + bubble: logo + created_at: <%= 1.day.ago %> + +logo_third_activity: + bubble: logo + created_at: <%= 1.hour.ago %> + +layout_initial_activity: + bubble: layout + +text_initial_activity: + bubble: text + +shipping_initial_activity: + bubble: shipping diff --git a/test/fixtures/thread_entries.yml b/test/fixtures/thread_entries.yml new file mode 100644 index 000000000..4137af55b --- /dev/null +++ b/test/fixtures/thread_entries.yml @@ -0,0 +1,28 @@ +logo_1: + bubble: logo + threadable: logo_initial_activity (Rollup) + created_at: <%= 1.week.ago %> + +logo_2: + bubble: logo + threadable: logo_agreement_jz (Comment) + created_at: <%= 2.days.ago %> + +logo_3: + bubble: logo + threadable: logo_second_activity (Rollup) + created_at: <%= 1.day.ago %> + +logo_4: + bubble: logo + threadable: logo_agreement_kevin (Comment) + created_at: <%= 2.hours.ago %> + +logo_5: + bubble: logo + threadable: logo_third_activity (Rollup) + created_at: <%= 1.hour.ago %> + +layout_1: + bubble: layout + threadable: layout_overflowing_david (Comment)