From b790da9617b794afee7465e915689c91a2ea5a59 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Wed, 4 Jun 2025 14:13:12 +0200 Subject: [PATCH] WIP for stalled cards --- app/jobs/card/activity_spike/detection_job.rb | 5 ++ app/models/card.rb | 2 +- app/models/card/activity_spike.rb | 3 ++ app/models/card/activity_spike/detector.rb | 51 +++++++++++++++++++ app/models/card/assignable.rb | 4 ++ app/models/card/stallable.rb | 27 ++++++++++ .../20250604120033_create_activity_spikes.rb | 8 +++ db/schema.rb | 10 +++- db/schema_cache.yml | 26 +++++++++- 9 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 app/jobs/card/activity_spike/detection_job.rb create mode 100644 app/models/card/activity_spike.rb create mode 100644 app/models/card/activity_spike/detector.rb create mode 100644 app/models/card/stallable.rb create mode 100644 db/migrate/20250604120033_create_activity_spikes.rb diff --git a/app/jobs/card/activity_spike/detection_job.rb b/app/jobs/card/activity_spike/detection_job.rb new file mode 100644 index 000000000..2f5142d0d --- /dev/null +++ b/app/jobs/card/activity_spike/detection_job.rb @@ -0,0 +1,5 @@ +class Card::ActivitySpike::DetectionJob < ApplicationJob + def perform(card) + card.detect_activity_spikes + end +end diff --git a/app/models/card.rb b/app/models/card.rb index 0a109e5ba..5d4d357f1 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,7 +1,7 @@ class Card < ApplicationRecord include Assignable, Colored, Engageable, Entropic, Eventable, Golden, Mentions, Pinnable, Closeable, Readable, Searchable, - Staged, Statuses, Taggable, Watchable + Staged, Stallable, Statuses, Taggable, Watchable belongs_to :collection, touch: true belongs_to :creator, class_name: "User", default: -> { Current.user } diff --git a/app/models/card/activity_spike.rb b/app/models/card/activity_spike.rb new file mode 100644 index 000000000..cbfbe906a --- /dev/null +++ b/app/models/card/activity_spike.rb @@ -0,0 +1,3 @@ +class Card::ActivitySpike < ApplicationRecord + belongs_to :card, touch: true +end diff --git a/app/models/card/activity_spike/detector.rb b/app/models/card/activity_spike/detector.rb new file mode 100644 index 000000000..f879fadf7 --- /dev/null +++ b/app/models/card/activity_spike/detector.rb @@ -0,0 +1,51 @@ +class Card::ActivitySpike::Detector + attr_reader :card + + def initialize(card) + @card = card + end + + def detect + if has_activity_spike? + activity_spike = card.activity_spike || card.build_activity_spike + activity_spike.touch + true + else + false + end + end + + private + def has_activity_spike? + card.entropic? && (multiple_people_commented? || card_was_just_assigned?) + end + + def multiple_people_commented? + card.comments + .where("created_at >= ?", recent_period.ago) + .group(:card_id) + .having("COUNT(*) >= ?", minimum_comments) + .having("COUNT(DISTINCT creator_id) >= ?", minimum_participants) + .exists? + end + + def recent_period + card.entropy.auto_clean_period * 0.25 + end + + def minimum_participants + 3 + end + + def minimum_comments + 2 + end + + def card_was_just_assigned? + last_event&.action&.card_assigned? && card.assigned? && last_event.created_at > 1.minute.ago + end + + def last_event + card.events.last + end +end diff --git a/app/models/card/assignable.rb b/app/models/card/assignable.rb index 7ef0eb134..c43c50526 100644 --- a/app/models/card/assignable.rb +++ b/app/models/card/assignable.rb @@ -18,6 +18,10 @@ module Card::Assignable assignments.exists? assignee: user end + def assigned? + assignments.any? + end + private def assign(user) assignments.create! assignee: user, assigner: Current.user diff --git a/app/models/card/stallable.rb b/app/models/card/stallable.rb new file mode 100644 index 000000000..fefa222f1 --- /dev/null +++ b/app/models/card/stallable.rb @@ -0,0 +1,27 @@ +module Card::Stallable + extend ActiveSupport::Concern + + STALLED_AFTER_LAST_SPIKE_PERIOD = 14.days + + included do + has_one :activity_spike, class_name: "Card::ActivitySpike", dependent: :destroy + + scope :with_activity_spikes, -> { joins(:activity_spike) } + scope :stalled, -> { with_activity_spikes.where(updated_at: ..STALLED_AFTER_LAST_SPIKE_PERIOD.ago) } + + after_update_commit :detect_activity_spikes_later, if: :saved_change_to_last_active_at? + end + + def stalled? + activity_spike && activity_spike.updated_at < STALLED_AFTER_LAST_SPIKE_PERIOD.ago + end + + def detect_activity_spikes + Card::ActivitySpike::Detector.new(self).detect + end + + private + def detect_activity_spikes_later + Card::ActivitySpike::DetectionJob.perform_later(self) + end +end diff --git a/db/migrate/20250604120033_create_activity_spikes.rb b/db/migrate/20250604120033_create_activity_spikes.rb new file mode 100644 index 000000000..d78e19b10 --- /dev/null +++ b/db/migrate/20250604120033_create_activity_spikes.rb @@ -0,0 +1,8 @@ +class CreateActivitySpikes < ActiveRecord::Migration[8.1] + def change + create_table :card_activity_spikes do |t| + t.references :card, null: false, foreign_key: true, index: true + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8c1642ea4..ecffc5579 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.1].define(version: 2025_06_04_080022) do +ActiveRecord::Schema[8.1].define(version: 2025_06_04_120033) do create_table "accesses", force: :cascade do |t| t.integer "collection_id", null: false t.datetime "created_at", null: false @@ -93,6 +93,13 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_04_080022) do t.index ["card_id"], name: "index_assignments_on_card_id" end + create_table "card_activity_spikes", force: :cascade do |t| + t.integer "card_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["card_id"], name: "index_card_activity_spikes_on_card_id" + end + create_table "card_engagements", force: :cascade do |t| t.integer "card_id" t.datetime "created_at", null: false @@ -349,6 +356,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_04_080022) 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 "card_activity_spikes", "cards" add_foreign_key "card_goldnesses", "cards" add_foreign_key "cards", "workflow_stages", column: "stage_id" add_foreign_key "closures", "cards" diff --git a/db/schema_cache.yml b/db/schema_cache.yml index e29b9c902..73911606c 100644 --- a/db/schema_cache.yml +++ b/db/schema_cache.yml @@ -344,6 +344,11 @@ columns: - *5 - *6 - *9 + card_activity_spikes: + - *22 + - *5 + - *6 + - *9 card_engagements: - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column auto_increment: @@ -1021,6 +1026,7 @@ primary_keys: assignees_filters: assigners_filters: assignments: id + card_activity_spikes: id card_engagements: id card_goldnesses: id cards: id @@ -1060,6 +1066,7 @@ data_sources: assignees_filters: true assigners_filters: true assignments: true + card_activity_spikes: true card_engagements: true card_goldnesses: true cards: true @@ -1347,6 +1354,23 @@ indexes: nulls_not_distinct: comment: valid: true + card_activity_spikes: + - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition + table: card_activity_spikes + name: index_card_activity_spikes_on_card_id + unique: false + columns: + - card_id + lengths: {} + orders: {} + opclasses: {} + where: + type: + using: + include: + nulls_not_distinct: + comment: + valid: true card_engagements: - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition table: card_engagements @@ -2211,4 +2235,4 @@ indexes: comment: valid: true workflows: [] -version: 20250604080022 +version: 20250604120033