diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb
index f5eb8c9fc..aaf71e046 100644
--- a/app/controllers/collections_controller.rb
+++ b/app/controllers/collections_controller.rb
@@ -33,7 +33,7 @@ class CollectionsController < ApplicationController
end
def collection_params
- params.expect(collection: [ :name, :all_access, :auto_close_period ])
+ params.expect(collection: [ :name, :all_access, :auto_close_period, :auto_reconsider_period ])
end
def grantees
diff --git a/app/models/account.rb b/app/models/account.rb
index f562ef665..b2a4d71b5 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -1,5 +1,5 @@
class Account < ApplicationRecord
- include Joinable
+ include Entropy, Joinable
has_many_attached :uploads
end
diff --git a/app/models/account/entropy.rb b/app/models/account/entropy.rb
new file mode 100644
index 000000000..39c47f2b5
--- /dev/null
+++ b/app/models/account/entropy.rb
@@ -0,0 +1,18 @@
+module Account::Entropy
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :default_entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
+
+ before_save :set_default_entropy_configuration
+ end
+
+ private
+ DEFAULT_ENTROPY_PERIOD = 30.days
+
+ def set_default_entropy_configuration
+ self.default_entropy_configuration ||= build_default_entropy_configuration \
+ auto_close_period: DEFAULT_ENTROPY_PERIOD,
+ auto_reconsider_period: DEFAULT_ENTROPY_PERIOD
+ end
+end
diff --git a/app/models/card/entropy.rb b/app/models/card/entropy.rb
index 0b6f6e4aa..d5dd17268 100644
--- a/app/models/card/entropy.rb
+++ b/app/models/card/entropy.rb
@@ -8,9 +8,9 @@ module Card::Entropy
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
scope :stagnated, -> { doing.where(last_active_at: ..AUTO_RECONSIDER_PERIOD.ago) }
- scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
+ scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || entropy_configurations.auto_close_period || ' seconds')") }
- delegate :auto_close_period, to: :collection
+ delegate :auto_close_period, :auto_reconsider_period, to: :collection
end
class_methods do
diff --git a/app/models/collection.rb b/app/models/collection.rb
index 9b74d8524..0fc1e912e 100644
--- a/app/models/collection.rb
+++ b/app/models/collection.rb
@@ -1,11 +1,12 @@
class Collection < ApplicationRecord
- include AutoClosing, Accessible, Broadcastable, Filterable, Workflowing
+ include AutoClosing, Accessible, Broadcastable, Entropy, Filterable, Workflowing
belongs_to :creator, class_name: "User", default: -> { Current.user }
has_many :cards, dependent: :destroy
has_many :tags, -> { distinct }, through: :cards
has_many :events
+ has_one :entropy_configuration, class_name: "Entropy::Configuration", as: :container, dependent: :destroy
scope :alphabetically, -> { order("lower(name)") }
diff --git a/app/models/collection/auto_closing.rb b/app/models/collection/auto_closing.rb
index e3a9c8990..c8e52cb12 100644
--- a/app/models/collection/auto_closing.rb
+++ b/app/models/collection/auto_closing.rb
@@ -2,12 +2,12 @@ module Collection::AutoClosing
extend ActiveSupport::Concern
included do
- scope :auto_closing, -> { where.not(auto_close_period: nil) }
+ scope :auto_closing, -> { joins(:entropy_configuration).where.not("entropy_configurations.auto_close_period": nil) }
before_create :set_default_auto_close_period
end
def auto_closing?
- auto_close_period.present?
+ entropy_configuration.present?
end
private
diff --git a/app/models/collection/entropy.rb b/app/models/collection/entropy.rb
new file mode 100644
index 000000000..ddf0883f4
--- /dev/null
+++ b/app/models/collection/entropy.rb
@@ -0,0 +1,21 @@
+module Collection::Entropy
+ extend ActiveSupport::Concern
+
+ included do
+ delegate :auto_close_period, :auto_reconsider_period, to: :entropy_configuration
+ end
+
+ def entropy_configuration
+ super || Account.sole.default_entropy_configuration
+ end
+
+ def auto_close_period=(new_value)
+ entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration
+ entropy_configuration.update auto_close_period: new_value
+ end
+
+ def auto_reconsider_period=(new_value)
+ entropy_configuration ||= association(:entropy_configuration).reader || self.build_entropy_configuration
+ entropy_configuration.update auto_reconsider_period: new_value
+ end
+end
diff --git a/app/models/entropy.rb b/app/models/entropy.rb
new file mode 100644
index 000000000..42e730a09
--- /dev/null
+++ b/app/models/entropy.rb
@@ -0,0 +1,5 @@
+module Entropy
+ def self.table_name_prefix
+ "entropy_"
+ end
+end
diff --git a/app/models/entropy/configuration.rb b/app/models/entropy/configuration.rb
new file mode 100644
index 000000000..72370670f
--- /dev/null
+++ b/app/models/entropy/configuration.rb
@@ -0,0 +1,3 @@
+class Entropy::Configuration < ApplicationRecord
+ belongs_to :container, polymorphic: true
+end
diff --git a/app/views/collections/edit/_auto_close.html.erb b/app/views/collections/edit/_auto_close.html.erb
index 585627dd6..d4455e45b 100644
--- a/app/views/collections/edit/_auto_close.html.erb
+++ b/app/views/collections/edit/_auto_close.html.erb
@@ -5,12 +5,12 @@
Cards in Considering
Auto-close after…
<%= form_with model: collection, data: { controller: "form" } do |form| %>
- <%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-half txt-small fill-white txt-align-center", data: { action: "change->form#submit" } } %>
+ <%= form.select :auto_reconsider_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-half txt-small fill-white txt-align-center", data: { action: "change->form#submit" } } %>
<% end %>
-
Cards inDoing
+
Cards in Doing
Fall back to Considering after…
<%= form_with model: collection, data: { controller: "form" } do |form| %>
<%= form.select :auto_close_period, collection_auto_close_options, {}, { class: "input input--select full-width margin-block-half txt-small fill-white txt-align-center", data: { action: "change->form#submit" } } %>
@@ -18,4 +18,3 @@
-
\ No newline at end of file
diff --git a/db/migrate/20250604071718_create_entropy_configurations.rb b/db/migrate/20250604071718_create_entropy_configurations.rb
new file mode 100644
index 000000000..702d4d9cd
--- /dev/null
+++ b/db/migrate/20250604071718_create_entropy_configurations.rb
@@ -0,0 +1,15 @@
+class CreateEntropyConfigurations < ActiveRecord::Migration[8.1]
+ def change
+ create_table :entropy_configurations do |t|
+ t.references :container, polymorphic: true, null: false, index: { unique: true }
+
+ t.bigint :auto_close_period, null: false, default: 30.days.to_i
+ t.bigint :auto_reconsider_period, null: false, default: 30.days.to_i
+
+ t.timestamps
+
+ t.index %i[ container_type container_id auto_close_period ]
+ t.index %i[ container_type container_id auto_reconsider_period ]
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7acec01bd..8c1642ea4 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_01_161653) do
+ActiveRecord::Schema[8.1].define(version: 2025_06_04_080022) do
create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false
t.datetime "created_at", null: false
@@ -141,13 +141,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
create_table "collections", force: :cascade do |t|
t.boolean "all_access", default: false, null: false
- t.bigint "auto_close_period"
t.datetime "created_at", null: false
t.integer "creator_id", null: false
t.string "name", null: false
t.datetime "updated_at", null: false
t.integer "workflow_id"
- t.index ["auto_close_period"], name: "index_collections_on_auto_close_period"
t.index ["creator_id"], name: "index_collections_on_creator_id"
t.index ["workflow_id"], name: "index_collections_on_workflow_id"
end
@@ -188,6 +186,18 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_01_161653) do
t.index ["filter_id"], name: "index_creators_filters_on_filter_id"
end
+ create_table "entropy_configurations", force: :cascade do |t|
+ t.bigint "auto_close_period", default: 2592000, null: false
+ t.bigint "auto_reconsider_period", default: 2592000, null: false
+ t.integer "container_id", null: false
+ t.string "container_type", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["container_type", "container_id", "auto_close_period"], name: "idx_on_container_type_container_id_auto_close_perio_74dc880875"
+ t.index ["container_type", "container_id", "auto_reconsider_period"], name: "idx_on_container_type_container_id_auto_reconsider__583aaddbea"
+ t.index ["container_type", "container_id"], name: "index_entropy_configurations_on_container", unique: true
+ end
+
create_table "events", force: :cascade do |t|
t.string "action", null: false
t.integer "collection_id", null: false
diff --git a/db/schema_cache.yml b/db/schema_cache.yml
index e4a61d5f1..e29b9c902 100644
--- a/db/schema_cache.yml
+++ b/db/schema_cache.yml
@@ -495,16 +495,6 @@ columns:
default_function:
collation:
comment:
- - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
- auto_increment:
- name: auto_close_period
- cast_type: *11
- sql_type_metadata: *12
- 'null': true
- default:
- default_function:
- collation:
- comment:
- *5
- *24
- *6
@@ -585,6 +575,50 @@ columns:
creators_filters:
- *24
- *19
+ entropy_configurations:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: auto_close_period
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': false
+ default: 2592000
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: auto_reconsider_period
+ cast_type: *11
+ sql_type_metadata: *12
+ 'null': false
+ default: 2592000
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: container_id
+ cast_type: *1
+ sql_type_metadata: *2
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
+ auto_increment:
+ name: container_type
+ cast_type: *7
+ sql_type_metadata: *8
+ 'null': false
+ default:
+ default_function:
+ collation:
+ comment:
+ - *5
+ - *6
+ - *9
events:
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
auto_increment:
@@ -997,6 +1031,7 @@ primary_keys:
commands: id
comments: id
creators_filters:
+ entropy_configurations: id
events: id
filters: id
filters_stages:
@@ -1035,6 +1070,7 @@ data_sources:
commands: true
comments: true
creators_filters: true
+ entropy_configurations: true
events: true
filters: true
filters_stages: true
@@ -1447,22 +1483,6 @@ indexes:
comment:
valid: true
collections:
- - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
- table: collections
- name: index_collections_on_auto_close_period
- unique: false
- columns:
- - auto_close_period
- lengths: {}
- orders: {}
- opclasses: {}
- where:
- type:
- using:
- include:
- nulls_not_distinct:
- comment:
- valid: true
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: collections
name: index_collections_on_creator_id
@@ -1646,6 +1666,60 @@ indexes:
nulls_not_distinct:
comment:
valid: true
+ entropy_configurations:
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: entropy_configurations
+ name: idx_on_container_type_container_id_auto_close_perio_74dc880875
+ unique: false
+ columns:
+ - container_type
+ - container_id
+ - auto_close_period
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: entropy_configurations
+ name: idx_on_container_type_container_id_auto_reconsider__583aaddbea
+ unique: false
+ columns:
+ - container_type
+ - container_id
+ - auto_reconsider_period
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
+ - !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
+ table: entropy_configurations
+ name: index_entropy_configurations_on_container
+ unique: true
+ columns:
+ - container_type
+ - container_id
+ lengths: {}
+ orders: {}
+ opclasses: {}
+ where:
+ type:
+ using:
+ include:
+ nulls_not_distinct:
+ comment:
+ valid: true
events:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: events
@@ -2137,4 +2211,4 @@ indexes:
comment:
valid: true
workflows: []
-version: 20250601161653
+version: 20250604080022
diff --git a/test/fixtures/collections.yml b/test/fixtures/collections.yml
index 2f00b7646..f08054e6c 100644
--- a/test/fixtures/collections.yml
+++ b/test/fixtures/collections.yml
@@ -2,11 +2,9 @@ writebook:
name: Writebook
creator: david
all_access: true
- auto_close_period: <%= 30.days.to_i %>
workflow: qa
private:
name: Private collection
creator: kevin
all_access: false
- auto_close_period: <%= 30.days.to_i %>
diff --git a/test/fixtures/entropy/configurations.yml b/test/fixtures/entropy/configurations.yml
new file mode 100644
index 000000000..3046b360f
--- /dev/null
+++ b/test/fixtures/entropy/configurations.yml
@@ -0,0 +1,9 @@
+writebook_collection:
+ container: writebook (Collection)
+ auto_reconsider_period: <%= 30.days.to_i %>
+ auto_close_period: <%= 30.days.to_i %>
+
+private_collection:
+ container: private (Collection)
+ auto_reconsider_period: <%= 30.days.to_i %>
+ auto_close_period: <%= 30.days.to_i %>
diff --git a/test/models/card/entropy_test.rb b/test/models/card/entropy_test.rb
index 2c20f58a8..682c6dc0a 100644
--- a/test/models/card/entropy_test.rb
+++ b/test/models/card/entropy_test.rb
@@ -8,7 +8,7 @@ class Card::EntropyTest < ActiveSupport::TestCase
test "auto_close_at infers the period from the collection" do
freeze_time
- collections(:writebook).update! auto_close_period: 123.days
+ entropy_configurations(:writebook_collection).update! auto_close_period: 123.days
cards(:layout).update! last_active_at: 2.day.ago
assert_equal (123-2).days.from_now, cards(:layout).auto_close_at
end
@@ -16,8 +16,8 @@ class Card::EntropyTest < ActiveSupport::TestCase
test "auto close all due" do
cards(:logo, :shipping).each(&:reconsider)
- cards(:logo).update!(last_active_at: 1.day.ago - collections(:writebook).auto_close_period)
- cards(:shipping).update!(last_active_at: 1.day.from_now - collections(:writebook).auto_close_period)
+ cards(:logo).update!(last_active_at: 1.day.ago - entropy_configurations(:writebook_collection).auto_close_period)
+ cards(:shipping).update!(last_active_at: 1.day.from_now - entropy_configurations(:writebook_collection).auto_close_period)
assert_difference -> { Card.closed.count }, +1 do
Card.auto_close_all_due
@@ -30,7 +30,7 @@ class Card::EntropyTest < ActiveSupport::TestCase
test "don't auto close those cards where the collection has no auto close period" do
cards(:logo, :shipping).each(&:reconsider)
- collections(:writebook).update auto_close_period: nil
+ collections(:writebook).entropy_configuration.destroy
assert_no_difference -> { Card.closed.count } do
Card.auto_close_all_due