Configure the autoclose period at the collection level

This commit is contained in:
Jorge Manrubia
2025-04-25 12:37:17 +02:00
parent c61298dfc8
commit e90b062440
17 changed files with 189 additions and 117 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ class CollectionsController < ApplicationController
end
def create
@collection = Collection.create! collection_params
@collection = Collection.create! collection_params.with_defaults(all_access: true)
redirect_to cards_path(collection_ids: [ @collection ])
end
@@ -19,7 +19,7 @@ class CollectionsController < ApplicationController
@collection.update! collection_params
@collection.accesses.revise granted: grantees, revoked: revokees
redirect_to cards_path(collection_ids: [ @collection ])
redirect_to edit_collection_path(@collection), notice: "Collection updated"
end
def destroy
@@ -33,7 +33,7 @@ class CollectionsController < ApplicationController
end
def collection_params
params.expect(collection: [ :name, :all_access ]).with_defaults(all_access: true)
params.expect(collection: [ :name, :all_access, :auto_close_period ])
end
def grantees
+12
View File
@@ -0,0 +1,12 @@
module CollectionsHelper
def collection_auto_close_options
[
[ "30 days", 30.days ],
[ "60 days", 60.days ],
[ "90 days", 90.days ],
[ "6 months", 180.days ],
[ "1 year", 365.days ],
[ "Never", nil ]
]
end
end
+5 -13
View File
@@ -1,17 +1,6 @@
module Card::Closeable
extend ActiveSupport::Concern
AUTO_CLOSE_OPTIONS = [
[ "30 days", 30.days ],
[ "60 days", 60.days ],
[ "90 days", 90.days ],
[ "6 months", 180.days ],
[ "1 year", 365.days ],
[ "Never", nil ]
].freeze
AUTO_CLOSE_AFTER = 30.days
included do
has_one :closure, dependent: :destroy
@@ -19,7 +8,10 @@ module Card::Closeable
scope :open, -> { where.missing(:closure) }
scope :recently_closed_first, -> { closed.order("closures.created_at": :desc) }
scope :due_to_be_closed, -> { considering.where(last_active_at: ..AUTO_CLOSE_AFTER.ago) }
scope :in_auto_closing_collection, -> { joins(:collection).merge(Collection.auto_closing) }
scope :due_to_be_closed, -> { considering.in_auto_closing_collection.where("last_active_at <= DATETIME('now', '-' || auto_close_period || ' seconds')") }
delegate :auto_closing?, :auto_close_period, to: :collection
end
class_methods do
@@ -31,7 +23,7 @@ module Card::Closeable
end
def auto_close_at
last_active_at + AUTO_CLOSE_AFTER if last_active_at
last_active_at + auto_close_period if auto_closing? && last_active_at
end
def closed?
+1 -1
View File
@@ -1,5 +1,5 @@
class Collection < ApplicationRecord
include Accessible, Broadcastable, Filterable, Workflowing
include AutoClosing, Accessible, Broadcastable, Filterable, Workflowing
belongs_to :creator, class_name: "User", default: -> { Current.user }
+19
View File
@@ -0,0 +1,19 @@
module Collection::AutoClosing
extend ActiveSupport::Concern
included do
scope :auto_closing, -> { where.not(auto_close_period: nil) }
before_create :set_default_auto_close_period
end
def auto_closing?
auto_close_period.present?
end
private
DEFAULT_AUTO_CLOSE_PERIOD = 30.days
def set_default_auto_close_period
self.auto_close_period ||= DEFAULT_AUTO_CLOSE_PERIOD unless attribute_present?(:auto_close_period)
end
end
+1 -1
View File
@@ -24,7 +24,7 @@
<span class="card-perma__closure-message">
<% if card.doing? %>
Returns to <em>Considering</em> if no activity <%= local_datetime_tag(card.auto_reconsider_at, style: :indays) -%>.
<% else %>
<% elsif card.auto_closing? %>
Auto-closes if no activity <%= local_datetime_tag(card.auto_close_at, style: :indays) -%>.
<% end %>
</span>
+4 -74
View File
@@ -17,56 +17,9 @@
<strong class="txt-large">Name and Access</strong>
<p class="margin-none-block-start">Choose who can access this Collection.</p>
<%= form_with model: @collection, class: "flex flex-column gap txt-large", data: { controller: "form" } do |form| %>
<div class="flex align-center gap">
<label class="flex-item-grow">
<strong><%= form.text_field :name, name: "collection[name]", class: "input full-width",
required: true, autofocus: true, placeholder: "Give it a name…",
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" } %></strong>
</label>
</div>
<section class="margin-block fill-shade border-radius">
<%= access_menu_tag @collection do %>
<li class="flex align-center gap pad unpad-block-end txt-normal">
<figure class="avatar flex-item-no-shrink" style="--avatar-border-radius: 0; --avatar-size: 3.5ch; margin-inline: 0.3em;">
<%= icon_tag "everyone" %>
<span class="for-screen-reader">Everyone</span>
</figure>
<div class="min-width">
<div class="overflow-ellipsis fill-shade"><strong>Everyone</strong></div>
</div>
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true" />
<label for="collection_all_access" class="switch">
<%= form.check_box :all_access, class: "switch__input", checked: @collection.all_access?, data: { action: "change->toggle-class#toggle" } %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give everyone access to this project</span>
</label>
</li>
<hr class="separator--horizontal margin-inline" style="--border-color: var(--color-subtle-dark);" aria-hidden="true" />
<% if User.active.count > 20 %>
<div class="pad-inline">
<input placeholder="Filter…" class="input input--transparent full-width" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-action="input->filter#filter">
</div>
<% end %>
<div data-filter-target="list" class="users-list pad-inline">
<% 20.times do %>
<%= access_toggles_for @selected_users, selected: true %>
<% end %>
<% if @selected_users.any? && @unselected_users.any? %>
<hr class="separator full-width" style="--border-style: solid">
<% end %>
<%= access_toggles_for @unselected_users, selected: false %>
</div>
<% end %>
</section>
<%= render "collections/edit/name", form: form %>
<%= render "collections/edit/users", collection: @collection, selected_users: @selected_users, unselected_users: @unselected_users, form: form %>
<button type="submit" id="log_in" class="btn btn--reversed center txt-normal">
<%= icon_tag "check" %>
@@ -78,33 +31,10 @@
</div>
<div class="panel shadow center flex flex-column">
<strong class="txt-large">Auto-Close Cards</strong>
<label for="closure" class="margin-block-end">Cards automatically close when there is no activity for…</label>
<%= select_tag :closure, options_for_select(Card::Closeable::AUTO_CLOSE_OPTIONS, Card::Closeable::AUTO_CLOSE_AFTER), { class: "input input--select full-width margin-block-end", data: { action: "change->form#submit" } } %>
<%= render "collections/edit/auto_close", collection: @collection %>
<hr class="separator--horizontal full-width margin-block" style="--border-color: var(--color-subtle);" aria-hidden="true" />
<strong class="txt-large">Workflows</strong>
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
<div class="flex flex-wrap gap">
<% Workflow.all.each do |workflow| %>
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == @collection.workflow %>">
<%= button_to collection_workflow_path(@collection), method: :patch do %>
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
<strong class="txt-small overflow-ellipsis"><%= workflow.name %></strong>
<ul class="list-style-none txt-xx-small ">
<% workflow.stages.each do |stage| %>
<li class="overflow-ellipsis flex align-center gap-half min-width">
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
<span class="overflow-ellipsis"><%= stage.name %></span>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
</div>
<p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
<%= render "collections/edit/workflows", collection: @collection %>
<hr class="separator--horizontal full-width" style="--border-color: var(--color-subtle); margin-block-start: auto;" aria-hidden="true" />
@@ -0,0 +1,6 @@
<strong class="txt-large">Auto-Close Cards</strong>
<label for="closure" class="margin-block-end">Cards automatically close when there is no activity for…</label>
<%= 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-end", data: { action: "change->form#submit" } } %>
<% end %>
@@ -0,0 +1,7 @@
<div class="flex align-center gap">
<label class="flex-item-grow">
<strong><%= form.text_field :name, name: "collection[name]", class: "input full-width",
required: true, autofocus: true, placeholder: "Give it a name…",
data: { action: "keydown.enter->form#submit:prevent, keydown.esc->form#cancel" } %></strong>
</label>
</div>
@@ -0,0 +1,42 @@
<section class="margin-block fill-shade border-radius">
<%= access_menu_tag collection do %>
<li class="flex align-center gap pad unpad-block-end txt-normal">
<figure class="avatar flex-item-no-shrink" style="--avatar-border-radius: 0; --avatar-size: 3.5ch; margin-inline: 0.3em;">
<%= icon_tag "everyone" %>
<span class="for-screen-reader">Everyone</span>
</figure>
<div class="min-width">
<div class="overflow-ellipsis fill-shade"><strong>Everyone</strong></div>
</div>
<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true" />
<label for="collection_all_access" class="switch">
<%= form.check_box :all_access, class: "switch__input", checked: collection.all_access?, data: { action: "change->toggle-class#toggle" } %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Give everyone access to this project</span>
</label>
</li>
<hr class="separator--horizontal margin-inline" style="--border-color: var(--color-subtle-dark);" aria-hidden="true" />
<% if User.active.count > 20 %>
<div class="pad-inline">
<input placeholder="Filter…" class="input input--transparent full-width" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-action="input->filter#filter">
</div>
<% end %>
<div data-filter-target="list" class="users-list pad-inline">
<% 20.times do %>
<%= access_toggles_for selected_users, selected: true %>
<% end %>
<% if selected_users.any? && unselected_users.any? %>
<hr class="separator full-width" style="--border-style: solid">
<% end %>
<%= access_toggles_for unselected_users, selected: false %>
</div>
<% end %>
</section>
@@ -0,0 +1,21 @@
<strong class="txt-large">Workflows</strong>
<div class="flex justify-center margin-block-end">Use a Workflow to track progress in this Collection.</div>
<div class="flex flex-wrap gap">
<% Workflow.all.each do |workflow| %>
<div class="workflow-preview txt-align-start border border-radius <%= 'workflow-preview--selected' if workflow == collection.workflow %>">
<%= button_to collection_workflow_path(collection), method: :patch do %>
<%= hidden_field_tag "collection[workflow_id]", workflow.id %>
<strong class="txt-small overflow-ellipsis"><%= workflow.name %></strong>
<ul class="list-style-none txt-xx-small ">
<% workflow.stages.each do |stage| %>
<li class="overflow-ellipsis flex align-center gap-half min-width">
<span class="workflow-preview__swatch btn btn--circle" style="--btn-background: <%= stage.color.present? ? stage.color : Card::Colored::DEFAULT_COLOR %>;"></span>
<span class="overflow-ellipsis"><%= stage.name %></span>
</li>
<% end %>
</ul>
<% end %>
</div>
<% end %>
</div>
<p><%= link_to "Manage workflows", workflows_path, class: "btn btn--plain txt-link" %></p>
@@ -0,0 +1,11 @@
class AddAutoClosePeriodToCollections < ActiveRecord::Migration[8.1]
def change
add_column :collections, :auto_close_period, :bigint
add_index :collections, :auto_close_period
execute <<~SQL
UPDATE collections SET auto_close_period = #{30.days.to_i} WHERE auto_close_period IS NULL;
SQL
end
end
Generated
+3 -2
View File
@@ -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_04_25_070025) do
ActiveRecord::Schema[8.1].define(version: 2025_04_25_092727) do
create_table "accesses", force: :cascade do |t|
t.integer "collection_id", null: false
t.datetime "created_at", null: false
@@ -141,11 +141,13 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_25_070025) 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
@@ -292,7 +294,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_04_25_070025) do
t.datetime "created_at", null: false
t.string "title"
t.datetime "updated_at", null: false
t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
end
create_table "users", force: :cascade do |t|
+28 -18
View File
@@ -504,6 +504,16 @@ 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
- *23
- *6
@@ -1401,6 +1411,22 @@ 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
@@ -1941,23 +1967,7 @@ indexes:
nulls_not_distinct:
comment:
valid: true
tags:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: tags
name: index_tags_on_account_id_and_title
unique: true
columns:
- title
lengths: {}
orders: {}
opclasses: {}
where:
type:
using:
include:
nulls_not_distinct:
comment:
valid: true
tags: []
users:
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
table: users
@@ -2042,4 +2052,4 @@ indexes:
comment:
valid: true
workflows: []
version: 20250425070025
version: 20250425092727
@@ -35,7 +35,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
user_ids: users(:david, :jz).pluck(:id)
}
assert_redirected_to cards_path(collection_ids: [ collections(:writebook) ])
assert_redirected_to edit_collection_path(collections(:writebook))
assert_equal "Writebook bugs", collections(:writebook).reload.name
assert_equal users(:david, :jz).sort, collections(:writebook).users.sort
assert_not collections(:writebook).all_access?
@@ -49,7 +49,7 @@ class CollectionsControllerTest < ActionDispatch::IntegrationTest
patch collection_path(collection), params: { collection: { name: "Bugs", all_access: true } }
assert_redirected_to cards_path(collection_ids: [ collection ])
assert_redirected_to edit_collection_path(collection)
assert collection.reload.all_access?
assert_equal User.all, collection.users
end
+1
View File
@@ -2,4 +2,5 @@ writebook:
name: Writebook
creator: david
all_access: true
auto_close_period: 30
workflow: qa
+23 -3
View File
@@ -15,11 +15,19 @@ class Card::CloseableTest < ActiveSupport::TestCase
assert_equal users(:kevin), cards(:logo).closed_by
end
test "auto_close_all_due" do
test "autoclose_at infers the period from the collection" do
freeze_time
collections(:writebook).update! auto_close_period: 123.days
cards(:logo).update! last_active_at: 2.day.ago
assert_equal (123-2).days.from_now, cards(:logo).auto_close_at
end
test "auto close all due" do
cards(:logo, :shipping).each(&:reconsider)
cards(:logo).update!(last_active_at: 1.day.ago - Card::Closeable::AUTO_CLOSE_AFTER)
cards(:shipping).update!(last_active_at: 1.day.from_now - Card::Closeable::AUTO_CLOSE_AFTER)
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)
assert_difference -> { Card.closed.count }, +1 do
Card.auto_close_all_due
@@ -28,4 +36,16 @@ class Card::CloseableTest < ActiveSupport::TestCase
assert cards(:logo).reload.closed?
assert_not cards(:shipping).reload.closed?
end
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
assert_no_difference -> { Card.closed.count } do
Card.auto_close_all_due
end
assert_not cards(:logo).reload.closed?
end
end