From ec7fc750547b2a6b253f3d7d698f0c15c9dfe2ec Mon Sep 17 00:00:00 2001 From: Jose Farias Date: Fri, 11 Oct 2024 20:14:09 -0500 Subject: [PATCH] Spike events system --- app/controllers/boosts_controller.rb | 2 +- app/controllers/comments_controller.rb | 7 +- app/helpers/bubbles_helper.rb | 2 +- app/helpers/comments_helper.rb | 80 ------------------- .../controllers/animation_controller.js | 2 +- .../controllers/filter_controller.js | 2 +- .../controllers/thread_controller.js | 14 ++++ app/javascript/helpers/current_helpers.js | 25 ++++++ app/javascript/helpers/index.js | 2 + app/models/boost.rb | 4 - app/models/bubble.rb | 7 +- app/models/bubble/assignable.rb | 7 +- app/models/bubble/boostable.rb | 10 +++ app/models/bubble/commentable.rb | 11 +++ app/models/bubble/eventable.rb | 14 ++++ app/models/bubble/thread.rb | 39 +++++++++ app/models/bubble/thread/rollup.rb | 59 ++++++++++++++ app/models/bubble/threaded.rb | 5 ++ app/models/current.rb | 8 +- app/models/event.rb | 10 +++ app/models/event/assignments.rb | 16 ++++ app/views/boosts/_boosts.html.erb | 8 +- app/views/boosts/create.turbo_stream.erb | 4 + app/views/bubbles/show.html.erb | 5 +- app/views/bubbles/threads/_entry.html.erb | 3 + app/views/bubbles/threads/_rollup.html.erb | 3 + app/views/bubbles/threads/_thread.html.erb | 13 +++ app/views/buckets/index.html.erb | 2 +- app/views/comments/_comment.html.erb | 2 +- app/views/layouts/application.html.erb | 4 + ...9181806_add_unique_index_to_assignments.rb | 5 ++ ...1009182303_make_comment_bodies_not_null.rb | 5 ++ db/migrate/20241009183354_create_events.rb | 14 ++++ db/migrate/20241009191253_drop_boosts.rb | 5 ++ ...241009191318_add_boost_count_to_bubbles.rb | 5 ++ ...remove_assignments_index_on_assignee_id.rb | 5 ++ db/schema.rb | 26 +++--- test/controllers/.keep | 0 test/controllers/boosts_controller_test.rb | 14 +++- test/controllers/comments_controller_test.rb | 14 +++- test/fixtures/assignments.yml | 7 ++ test/fixtures/boosts.yml | 15 ---- test/fixtures/bubbles.yml | 5 ++ test/fixtures/comments.yml | 1 + test/fixtures/events.yml | 66 +++++++++++++++ test/models/bubble_test.rb | 25 ++++++ 46 files changed, 436 insertions(+), 146 deletions(-) delete mode 100644 app/helpers/comments_helper.rb create mode 100644 app/javascript/controllers/thread_controller.js create mode 100644 app/javascript/helpers/current_helpers.js create mode 100644 app/javascript/helpers/index.js delete mode 100644 app/models/boost.rb create mode 100644 app/models/bubble/boostable.rb create mode 100644 app/models/bubble/commentable.rb create mode 100644 app/models/bubble/eventable.rb create mode 100644 app/models/bubble/thread.rb create mode 100644 app/models/bubble/thread/rollup.rb create mode 100644 app/models/bubble/threaded.rb create mode 100644 app/models/event.rb create mode 100644 app/models/event/assignments.rb create mode 100644 app/views/bubbles/threads/_entry.html.erb create mode 100644 app/views/bubbles/threads/_rollup.html.erb create mode 100644 app/views/bubbles/threads/_thread.html.erb create mode 100644 db/migrate/20241009181806_add_unique_index_to_assignments.rb create mode 100644 db/migrate/20241009182303_make_comment_bodies_not_null.rb create mode 100644 db/migrate/20241009183354_create_events.rb create mode 100644 db/migrate/20241009191253_drop_boosts.rb create mode 100644 db/migrate/20241009191318_add_boost_count_to_bubbles.rb create mode 100644 db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb delete mode 100644 test/controllers/.keep delete mode 100644 test/fixtures/boosts.yml create mode 100644 test/fixtures/events.yml diff --git a/app/controllers/boosts_controller.rb b/app/controllers/boosts_controller.rb index 166b7e634..5f5c95e93 100644 --- a/app/controllers/boosts_controller.rb +++ b/app/controllers/boosts_controller.rb @@ -2,6 +2,6 @@ class BoostsController < ApplicationController include BubbleScoped, BucketScoped def create - @bubble.boosts.create! + @bubble.boost! end end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 3a6a650ac..d9ce0616c 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -2,12 +2,7 @@ class CommentsController < ApplicationController include BubbleScoped, BucketScoped def create - @bubble.comments.create!(comment_params) + @bubble.comment! params.dig(:comment, :body).presence redirect_to bucket_bubble_url(@bucket, @bubble) end - - private - def comment_params - params.require(:comment).permit(:body) - end end diff --git a/app/helpers/bubbles_helper.rb b/app/helpers/bubbles_helper.rb index 4d47f7d11..e1d2fdc84 100644 --- a/app/helpers/bubbles_helper.rb +++ b/app/helpers/bubbles_helper.rb @@ -8,7 +8,7 @@ module BubblesHelper end def bubble_size(bubble) - activity = bubble.boosts.size + bubble.comments.size + activity = bubble.boost_count + bubble.comments.count rank = case activity when 0..5 then "one" diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb deleted file mode 100644 index 18586dbe5..000000000 --- a/app/helpers/comments_helper.rb +++ /dev/null @@ -1,80 +0,0 @@ -module CommentsHelper - def render_comments_and_boosts(bubble) - combined_collection = combine_and_sort_items(bubble) - - safe_join([ - render_creator_summary(bubble, combined_collection), - render_remaining_items(combined_collection) - ]) - end - - private - def combine_and_sort_items(bubble) - (bubble.comments + bubble.boosts + bubble.assignments).sort_by(&:created_at) - end - - def render_creator_summary(bubble, combined_collection) - content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do - [ - creator_info(bubble), - initial_assignment_info(combined_collection), - render_initial_boosts(combined_collection) - ].compact.join(", ").html_safe - end - end - - def creator_info(bubble) - "Added by #{bubble.creator.name} #{time_ago_in_words(bubble.created_at)} ago" - end - - def initial_assignment_info(combined_collection) - initial_assignment = combined_collection.find { |item| item.is_a?(Assignment) } - "assigned to #{initial_assignment.assignee.name}" if initial_assignment - end - - def render_initial_boosts(combined_collection) - grouped_boosts = combined_collection.take_while { |item| item.is_a?(Boost) } - return if grouped_boosts.empty? - - user_boosts = grouped_boosts.group_by(&:creator).transform_values(&:count) - boost_summaries = user_boosts.map { |user, count| "#{user.name} +#{count}" } - boost_summaries.to_sentence - end - - def render_remaining_items(combined_collection) - initial_count = combined_collection.take_while { |item| !item.is_a?(Comment) }.count - items = combined_collection.drop(initial_count) - - safe_join(items.chunk_while { |i, j| grouped_item?(i) && grouped_item?(j) }.map do |chunk| - if chunk.first.is_a?(Comment) - render "comments/comment", comment: chunk.first - else - render_grouped_items(chunk) - end - end) - end - - def grouped_item?(item) - item.is_a?(Boost) || item.is_a?(Assignment) - end - - def render_grouped_items(items) - return if items.empty? - - content_tag(:div, class: "comment--upvotes flex-inline flex-wrap align-start gap fill-white border-radius center position-relative") do - [ - render_grouped_boosts(items.select { |item| item.is_a?(Boost) }), - render_grouped_assignments(items.select { |item| item.is_a?(Assignment) }) - ].flatten.compact.to_sentence.html_safe - end - end - - def render_grouped_boosts(boosts) - return if boosts.empty? - boosts.group_by(&:creator).map { |user, user_boosts| "#{user.name} +#{user_boosts.count}" } - end - - def render_grouped_assignments(assignments) - assignments.map { |assignment| "Assigned to #{assignment.assignee.name} #{time_ago_in_words(assignment.created_at)} ago" } - end -end diff --git a/app/javascript/controllers/animation_controller.js b/app/javascript/controllers/animation_controller.js index a6fe306a2..9fd0998d8 100644 --- a/app/javascript/controllers/animation_controller.js +++ b/app/javascript/controllers/animation_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { nextFrame } from "helpers/timing_helpers" +import { nextFrame } from "helpers" export default class extends Controller { static classes = [ "play" ] diff --git a/app/javascript/controllers/filter_controller.js b/app/javascript/controllers/filter_controller.js index 815b05952..953ae3a29 100644 --- a/app/javascript/controllers/filter_controller.js +++ b/app/javascript/controllers/filter_controller.js @@ -1,5 +1,5 @@ import { Controller } from "@hotwired/stimulus" -import { debounce } from "helpers/timing_helpers" +import { debounce } from "helpers" export default class extends Controller { static targets = [ "list" ] diff --git a/app/javascript/controllers/thread_controller.js b/app/javascript/controllers/thread_controller.js new file mode 100644 index 000000000..1004505b3 --- /dev/null +++ b/app/javascript/controllers/thread_controller.js @@ -0,0 +1,14 @@ +import { Controller } from "@hotwired/stimulus" +import { current } from "helpers" + +export default class extends Controller { + static classes = [ "myComment" ] + + connect() { + this.#myComments.forEach(comment => comment.classList.add(this.myCommentClass)) + } + + get #myComments() { + return this.element.querySelectorAll(`.comment[data-creator-id='${current.user.id}']`) + } +} diff --git a/app/javascript/helpers/current_helpers.js b/app/javascript/helpers/current_helpers.js new file mode 100644 index 000000000..6d2d9fdb2 --- /dev/null +++ b/app/javascript/helpers/current_helpers.js @@ -0,0 +1,25 @@ +// On-demand JavaScript objects from "current" HTML elements. Example: +// +// +// +// +// >> current.identity +// => { id: "123", timeZoneName: "Central Time (US & Canada)" } +// +// >> current.foo +// => {} + export const current = new Proxy({}, { + get(_target, propertyName) { + const result = {} + const prefix = `current-${propertyName}-` + for (const { name, content } of document.head.querySelectorAll(`meta[name^=${prefix}]`)) { + const key = camelize(name.slice(prefix.length)) + result[key] = content + } + return result + } + }) + + function camelize(string) { + return string.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase()) + } diff --git a/app/javascript/helpers/index.js b/app/javascript/helpers/index.js new file mode 100644 index 000000000..634fd65a2 --- /dev/null +++ b/app/javascript/helpers/index.js @@ -0,0 +1,2 @@ +export * from "helpers/current_helpers" +export * from "helpers/timing_helpers" diff --git a/app/models/boost.rb b/app/models/boost.rb deleted file mode 100644 index 41b0edb7a..000000000 --- a/app/models/boost.rb +++ /dev/null @@ -1,4 +0,0 @@ -class Boost < ApplicationRecord - belongs_to :bubble - belongs_to :creator, class_name: "User", default: -> { Current.user } -end diff --git a/app/models/bubble.rb b/app/models/bubble.rb index 43a6d05f8..debbf14cd 100644 --- a/app/models/bubble.rb +++ b/app/models/bubble.rb @@ -1,12 +1,9 @@ class Bubble < ApplicationRecord - include Assignable, Colored, Poppable, Searchable, Taggable + include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded belongs_to :bucket belongs_to :creator, class_name: "User", default: -> { Current.user } - has_many :comments, dependent: :destroy - has_many :boosts, dependent: :destroy - has_one_attached :image, dependent: :purge_later searchable_by :title, using: :bubbles_search_index @@ -14,7 +11,7 @@ class Bubble < ApplicationRecord before_save :set_default_title scope :reverse_chronologically, -> { order(created_at: :desc, id: :desc) } - scope :ordered_by_activity, -> { left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")) } + scope :ordered_by_activity, -> { left_joins(:comments).group(:id).order(Arel.sql("COUNT(comments.id) + boost_count DESC")) } scope :mentioning, ->(query) do bubbles = search(query).select(:id).to_sql diff --git a/app/models/bubble/assignable.rb b/app/models/bubble/assignable.rb index 3abf18d39..828034dab 100644 --- a/app/models/bubble/assignable.rb +++ b/app/models/bubble/assignable.rb @@ -2,7 +2,7 @@ module Bubble::Assignable extend ActiveSupport::Concern included do - has_many :assignments, dependent: :destroy + has_many :assignments, dependent: :delete_all has_many :assignees, through: :assignments has_many :assigners, through: :assignments @@ -12,8 +12,9 @@ module Bubble::Assignable end def assign(users, assigner: Current.user) - (Array(users) - assignees).tap do |users| - users.each { |user| assignments.create!(assignee: user, assigner: assigner) } + transaction do + Assignment.insert_all Array(users).collect { |user| { assignee_id: user.id, assigner_id: assigner.id, bubble_id: id } } + track_event :assigned, assignee_ids: Array(users).map(&:id) end end end diff --git a/app/models/bubble/boostable.rb b/app/models/bubble/boostable.rb new file mode 100644 index 000000000..3c39d93fa --- /dev/null +++ b/app/models/bubble/boostable.rb @@ -0,0 +1,10 @@ +module Bubble::Boostable + extend ActiveSupport::Concern + + def boost! + transaction do + increment! :boost_count + track_event :boosted, boost_count: + end + end +end diff --git a/app/models/bubble/commentable.rb b/app/models/bubble/commentable.rb new file mode 100644 index 000000000..d0ff9c521 --- /dev/null +++ b/app/models/bubble/commentable.rb @@ -0,0 +1,11 @@ +module Bubble::Commentable + extend ActiveSupport::Concern + + included do + has_many :comments, dependent: :delete_all + end + + def comment!(body) + comments.create! body: + end +end diff --git a/app/models/bubble/eventable.rb b/app/models/bubble/eventable.rb new file mode 100644 index 000000000..e1137880a --- /dev/null +++ b/app/models/bubble/eventable.rb @@ -0,0 +1,14 @@ +module Bubble::Eventable + extend ActiveSupport::Concern + + included do + has_many :events, dependent: :delete_all + + after_create -> { track_event :created } + end + + private + def track_event(action, creator: Current.user, **particulars) + events.create! action:, creator:, particulars: + end +end diff --git a/app/models/bubble/thread.rb b/app/models/bubble/thread.rb new file mode 100644 index 000000000..1b8dda3c0 --- /dev/null +++ b/app/models/bubble/thread.rb @@ -0,0 +1,39 @@ +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) } + end + + def to_partial_path + "bubbles/threads/thread" + end + + def cache_key + ActiveSupport::Cache.expand_cache_key bubble, :thread + 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, index) + case entries.first + when Comment + entries.sole + when Event + Rollup.new self, entries, first_position: index.zero? + end + end +end diff --git a/app/models/bubble/thread/rollup.rb b/app/models/bubble/thread/rollup.rb new file mode 100644 index 000000000..be4a49bb2 --- /dev/null +++ b/app/models/bubble/thread/rollup.rb @@ -0,0 +1,59 @@ +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 + + def cache_key + ActiveSupport::Cache.expand_cache_key [ thread, entries.first, entries.last ], "rollup" + end + + private + attr_reader :thread, :entries, :first_position + + delegate :time_ago_in_words, to: "ApplicationController.helpers", private: true + + def first_position? + first_position + end + + def collapsed_entries + sorted_entries.chunk_while { |a, b| equivalent_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 "boosted" then [ 3, entry.creator, entry.created_at ] + end + end + end + + def equivalent_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.assignee_names.to_sentence}" + summary += " #{time_ago_in_words(entry.created_at)} ago" unless first_position? + summary + when "boosted" + "#{entry.creator.name} +#{chunk_size}" + end + end +end diff --git a/app/models/bubble/threaded.rb b/app/models/bubble/threaded.rb new file mode 100644 index 000000000..22885f62a --- /dev/null +++ b/app/models/bubble/threaded.rb @@ -0,0 +1,5 @@ +module Bubble::Threaded + def thread + Bubble::Thread.new self + end +end diff --git a/app/models/current.rb b/app/models/current.rb index e5546b93b..d534f3e58 100644 --- a/app/models/current.rb +++ b/app/models/current.rb @@ -1,6 +1,10 @@ class Current < ActiveSupport::CurrentAttributes - attribute :session + attribute :session, :user - delegate :user, to: :session, allow_nil: true delegate :account, to: :user, allow_nil: true + + def session=(session) + super + self.user = session.user + end end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 000000000..8994acc05 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,10 @@ +class Event < ApplicationRecord + THREADABLE_ACTIONS = %w[ assigned boosted created ] + + include Assignments + + belongs_to :creator, class_name: "User" + belongs_to :bubble + + scope :threadable, -> { where action: THREADABLE_ACTIONS } +end diff --git a/app/models/event/assignments.rb b/app/models/event/assignments.rb new file mode 100644 index 000000000..ade131d31 --- /dev/null +++ b/app/models/event/assignments.rb @@ -0,0 +1,16 @@ +module Event::Assignments + extend ActiveSupport::Concern + + included do + store_accessor :particulars, :assignee_ids + end + + def assignee_names + assignees.map &:name + end + + private + def assignees + @assignees ||= creator.account.users.find assignee_ids + end +end diff --git a/app/views/boosts/_boosts.html.erb b/app/views/boosts/_boosts.html.erb index 1e2b1998b..973107476 100644 --- a/app/views/boosts/_boosts.html.erb +++ b/app/views/boosts/_boosts.html.erb @@ -1,6 +1,6 @@ <%= button_to bucket_bubble_boosts_path(bubble.bucket, bubble), - class: "btn btn--plain", - data: { action: "toggle-class#toggle" }, - form_class: "full-width" do %> - + <%= bubble.boosts.size if bubble.boosts.any? %> + class: "btn btn--plain", + data: { action: "toggle-class#toggle" }, + form_class: "full-width" do %> + + <%= bubble.boost_count if bubble.boost_count.positive? %> <% end %> diff --git a/app/views/boosts/create.turbo_stream.erb b/app/views/boosts/create.turbo_stream.erb index 4481aa4bf..718c9cf0d 100644 --- a/app/views/boosts/create.turbo_stream.erb +++ b/app/views/boosts/create.turbo_stream.erb @@ -1,3 +1,7 @@ <%= turbo_stream.update dom_id(@bubble, :boosts) do %> <%= render "boosts/boosts", bubble: @bubble %> <% end %> + +<%= turbo_stream.replace dom_id(@bubble, :thread) do %> + <%= render @bubble.thread %> +<% end %> diff --git a/app/views/bubbles/show.html.erb b/app/views/bubbles/show.html.erb index ee9cfbad8..9d320c4f7 100644 --- a/app/views/bubbles/show.html.erb +++ b/app/views/bubbles/show.html.erb @@ -25,7 +25,4 @@ <%= render "bubbles/bubble", bubble: @bubble %> -
- <%= render_comments_and_boosts(@bubble) %> - <%= render "comments/new", bubble: @bubble %> -
+<%= render @bubble.thread %> diff --git a/app/views/bubbles/threads/_entry.html.erb b/app/views/bubbles/threads/_entry.html.erb new file mode 100644 index 000000000..f299a3e08 --- /dev/null +++ b/app/views/bubbles/threads/_entry.html.erb @@ -0,0 +1,3 @@ +<%# Template Dependency: comments/comment %> +<%# Template Dependency: bubbles/threads/rollup %> +<%= render entry %> diff --git a/app/views/bubbles/threads/_rollup.html.erb b/app/views/bubbles/threads/_rollup.html.erb new file mode 100644 index 000000000..665807798 --- /dev/null +++ b/app/views/bubbles/threads/_rollup.html.erb @@ -0,0 +1,3 @@ +
+ <%= rollup.body %> +
diff --git a/app/views/bubbles/threads/_thread.html.erb b/app/views/bubbles/threads/_thread.html.erb new file mode 100644 index 000000000..7573722e5 --- /dev/null +++ b/app/views/bubbles/threads/_thread.html.erb @@ -0,0 +1,13 @@ +<% bubble = thread.bubble %> + +
+ <% cache thread do %> + <%= render partial: "bubbles/threads/entry", collection: thread.entries, cache: true %> + <% end %> + + <%= render "comments/new", bubble: bubble %> +
diff --git a/app/views/buckets/index.html.erb b/app/views/buckets/index.html.erb index 08573bd09..8436c5658 100644 --- a/app/views/buckets/index.html.erb +++ b/app/views/buckets/index.html.erb @@ -20,7 +20,7 @@ <%= link_to bucket_bubbles_path(bucket), class: "windshield__container flex justify-center align-center position-relative" do %>
- <% bucket.bubbles.not_popped.left_joins(:comments, :boosts).group(:id).order(Arel.sql("COUNT(comments.id) + COUNT(boosts.id) DESC")).limit(10).each do |bubble| %> + <% bucket.bubbles.not_popped.ordered_by_activity.limit(10).each do |bubble| %>
diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb index 6d5892457..1492a6a23 100644 --- a/app/views/comments/_comment.html.erb +++ b/app/views/comments/_comment.html.erb @@ -1,4 +1,4 @@ -
" id="<%= dom_id(comment) %>"> +
<%= avatar_tag comment.creator, loading: :lazy %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f75b3e64c..84908c22d 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,6 +9,10 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <% if Current.user %> + + <% end %> + <%= yield :head %> <%= stylesheet_link_tag :all, "data-turbo-track": "reload" %> diff --git a/db/migrate/20241009181806_add_unique_index_to_assignments.rb b/db/migrate/20241009181806_add_unique_index_to_assignments.rb new file mode 100644 index 000000000..6600bef57 --- /dev/null +++ b/db/migrate/20241009181806_add_unique_index_to_assignments.rb @@ -0,0 +1,5 @@ +class AddUniqueIndexToAssignments < ActiveRecord::Migration[8.0] + def change + add_index :assignments, %i[ assignee_id bubble_id ], unique: true + end +end diff --git a/db/migrate/20241009182303_make_comment_bodies_not_null.rb b/db/migrate/20241009182303_make_comment_bodies_not_null.rb new file mode 100644 index 000000000..eebedd368 --- /dev/null +++ b/db/migrate/20241009182303_make_comment_bodies_not_null.rb @@ -0,0 +1,5 @@ +class MakeCommentBodiesNotNull < ActiveRecord::Migration[8.0] + def change + change_column_null :comments, :body, false + end +end diff --git a/db/migrate/20241009183354_create_events.rb b/db/migrate/20241009183354_create_events.rb new file mode 100644 index 000000000..19c0f66da --- /dev/null +++ b/db/migrate/20241009183354_create_events.rb @@ -0,0 +1,14 @@ +class CreateEvents < ActiveRecord::Migration[8.0] + def change + create_table :events do |t| + t.references :bubble, null: false, index: false + t.references :creator, null: false + t.json :particulars, default: {} + t.string :action, null: false + + t.timestamps + end + + add_index :events, %i[ bubble_id action ], name: "index_events_on_bubble_id_and_action" + end +end diff --git a/db/migrate/20241009191253_drop_boosts.rb b/db/migrate/20241009191253_drop_boosts.rb new file mode 100644 index 000000000..9ecfeeea5 --- /dev/null +++ b/db/migrate/20241009191253_drop_boosts.rb @@ -0,0 +1,5 @@ +class DropBoosts < ActiveRecord::Migration[8.0] + def change + drop_table :boosts + end +end diff --git a/db/migrate/20241009191318_add_boost_count_to_bubbles.rb b/db/migrate/20241009191318_add_boost_count_to_bubbles.rb new file mode 100644 index 000000000..55a215ed5 --- /dev/null +++ b/db/migrate/20241009191318_add_boost_count_to_bubbles.rb @@ -0,0 +1,5 @@ +class AddBoostCountToBubbles < ActiveRecord::Migration[8.0] + def change + add_column :bubbles, :boost_count, :integer, null: false, default: 0 + end +end diff --git a/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb b/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb new file mode 100644 index 000000000..5f41bcc3a --- /dev/null +++ b/db/migrate/20241009211414_remove_assignments_index_on_assignee_id.rb @@ -0,0 +1,5 @@ +class RemoveAssignmentsIndexOnAssigneeId < ActiveRecord::Migration[8.0] + def change + remove_index :assignments, :assignee_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 4724af881..0218e49b4 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_02_163242) do +ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do create_table "accesses", force: :cascade do |t| t.integer "bucket_id", null: false t.integer "user_id", null: false @@ -64,18 +64,10 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "assigner_id", null: false - t.index ["assignee_id"], name: "index_assignments_on_assignee_id" + t.index ["assignee_id", "bubble_id"], name: "index_assignments_on_assignee_id_and_bubble_id", unique: true t.index ["bubble_id"], name: "index_assignments_on_bubble_id" end - create_table "boosts", force: :cascade do |t| - t.integer "creator_id", null: false - t.integer "bubble_id", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["bubble_id"], name: "index_boosts_on_bubble_id" - end - create_table "bubbles", force: :cascade do |t| t.string "title" t.string "color" @@ -84,6 +76,7 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do t.integer "creator_id", null: false t.date "due_on" t.integer "bucket_id", null: false + t.integer "boost_count", default: 0, null: false t.index ["bucket_id"], name: "index_bubbles_on_bucket_id" end @@ -98,13 +91,24 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_02_163242) do end create_table "comments", force: :cascade do |t| - t.text "body" + t.text "body", null: false t.integer "creator_id", null: false t.integer "bubble_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end + create_table "events", force: :cascade do |t| + t.integer "bubble_id", null: false + t.integer "creator_id", null: false + t.json "particulars", default: {} + t.string "action", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["bubble_id", "action"], name: "index_events_on_bubble_id_and_action" + t.index ["creator_id"], name: "index_events_on_creator_id" + end + create_table "pops", force: :cascade do |t| t.integer "bubble_id", null: false t.integer "user_id" diff --git a/test/controllers/.keep b/test/controllers/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/controllers/boosts_controller_test.rb b/test/controllers/boosts_controller_test.rb index 051cf6298..ce8135c78 100644 --- a/test/controllers/boosts_controller_test.rb +++ b/test/controllers/boosts_controller_test.rb @@ -1,7 +1,15 @@ require "test_helper" class BoostsControllerTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end + setup do + sign_in_as :kevin + end + + test "create" do + assert_difference "bubbles(:logo).reload.boost_count", +1 do + post bucket_bubble_boosts_url(buckets(:writebook), bubbles(:logo), format: :turbo_stream) + end + + assert_turbo_stream action: :update, target: dom_id(bubbles(:logo), :boosts) + end end diff --git a/test/controllers/comments_controller_test.rb b/test/controllers/comments_controller_test.rb index 3f25e015e..9578a2ffb 100644 --- a/test/controllers/comments_controller_test.rb +++ b/test/controllers/comments_controller_test.rb @@ -1,7 +1,15 @@ require "test_helper" class CommentsControllerTest < ActionDispatch::IntegrationTest - # test "the truth" do - # assert true - # end + setup do + sign_in_as :kevin + end + + test "create" do + assert_difference "bubbles(:logo).comments.count", +1 do + post bucket_bubble_comments_url(buckets(:writebook), bubbles(:logo), params: { comment: { body: "Agreed." } }) + end + + assert_redirected_to bucket_bubble_url(buckets(:writebook), bubbles(:logo)) + end end diff --git a/test/fixtures/assignments.yml b/test/fixtures/assignments.yml index d7dd9e0a6..6ac04e823 100644 --- a/test/fixtures/assignments.yml +++ b/test/fixtures/assignments.yml @@ -1,7 +1,14 @@ +logo_jz: + assigner: david + assignee: jz + bubble: logo + created_at: <%= 1.week.ago %> + logo_kevin: assigner: david assignee: kevin bubble: logo + created_at: <%= 1.day.ago %> layout_jz: assigner: david diff --git a/test/fixtures/boosts.yml b/test/fixtures/boosts.yml deleted file mode 100644 index 5d46d0cce..000000000 --- a/test/fixtures/boosts.yml +++ /dev/null @@ -1,15 +0,0 @@ -logo_jz_one: - bubble: logo - creator: jz - -logo_jz_two: - bubble: logo - creator: jz - -logo_kevin: - bubble: logo - creator: kevin - -layout_jz: - bubble: layout - creator: jz diff --git a/test/fixtures/bubbles.yml b/test/fixtures/bubbles.yml index 23ad32219..f97105d5a 100644 --- a/test/fixtures/bubbles.yml +++ b/test/fixtures/bubbles.yml @@ -4,21 +4,26 @@ logo: title: The logo isn't big enough color: "#ED8008" due_on: <%= 3.days.from_now %> + created_at: <%= 1.week.ago %> + boost_count: 4 layout: bucket: writebook creator: david title: Layout is broken color: "#698F9C" + created_at: <%= 1.week.ago %> text: bucket: writebook creator: kevin title: The text is too small color: "#3B4B59" + created_at: <%= 1.week.ago %> shipping: bucket: writebook creator: kevin title: We need to ship the app color: "#ED8008" + created_at: <%= 1.week.ago %> diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml index 0444a5a43..10ac4cca7 100644 --- a/test/fixtures/comments.yml +++ b/test/fixtures/comments.yml @@ -2,6 +2,7 @@ logo_agreement_jz: body: I agree. creator: jz bubble: logo + created_at: <%= 2.days.ago %> logo_agreement_kevin: body: Same, let’s do it. diff --git a/test/fixtures/events.yml b/test/fixtures/events.yml new file mode 100644 index 000000000..04dfdf2b4 --- /dev/null +++ b/test/fixtures/events.yml @@ -0,0 +1,66 @@ +logo_created: + creator: david + bubble: logo + action: created + created_at: <%= 1.week.ago %> + +logo_assignment_jz: + creator: david + bubble: logo + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %> + created_at: <%= 1.week.ago %> + +logo_assignment_km: + creator: david + bubble: logo + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:kevin) ] }.to_json %> + created_at: <%= 1.day.ago %> + +logo_boost_km1: + creator: kevin + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_km2: + creator: kevin + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_jz1: + creator: jz + bubble: logo + action: boosted + created_at: <%= 1.day.ago %> + +logo_boost_jz2: + creator: jz + bubble: logo + action: boosted + +layout_created: + creator: david + bubble: layout + action: created + created_at: <%= 1.week.ago %> + +layout_assignment_jz: + creator: david + bubble: layout + action: assigned + particulars: <%= { assignee_ids: [ ActiveRecord::FixtureSet.identify(:jz) ] }.to_json %> + +text_created: + creator: kevin + bubble: text + action: created + created_at: <%= 1.week.ago %> + +shipping_created: + creator: kevin + bubble: shipping + action: created + created_at: <%= 1.week.ago %> diff --git a/test/models/bubble_test.rb b/test/models/bubble_test.rb index d22683df2..7e8447a38 100644 --- a/test/models/bubble_test.rb +++ b/test/models/bubble_test.rb @@ -1,6 +1,31 @@ require "test_helper" class BubbleTest < ActiveSupport::TestCase + setup do + Current.user = users(:kevin) + end + + test "boosting" do + assert_difference %w[ bubbles(:logo).boost_count bubbles(:logo).events.count ], +1 do + bubbles(:logo).boost! + end + end + + test "commenting" do + bubbles(:logo).comment! "Agreed." + + assert_equal "Agreed.", bubbles(:logo).comments.last.body + end + + test "assigning" do + bubbles(:logo).assign users(:david) + + assert_equal users(:kevin, :jz, :david), bubbles(:logo).assignees + assert_equal users(:david, :kevin), bubbles(:logo).assigners.uniq + assert_equal [ users(:david).id ], bubbles(:logo).events.last.assignee_ids + assert_equal [ "David" ], bubbles(:logo).events.last.assignee_names + end + test "searchable by title" do bubble = buckets(:writebook).bubbles.create! title: "Insufficient haggis", creator: users(:kevin)