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