Validate and normalize auto-postpone period to days (#2672)

* Validate and normalize auto-postpone period to days

- Add Entropy::AUTO_POSTPONE_PERIODS and validate auto_postpone_period against the allowed set
- Introduce auto_postpone_period_in_days for forms and entropy update endpoints
- Fall back to index 0 in knob partial when current value isn't in options
- Remove entropy_auto_close_options helper in favor of model constant
- Update tests to use allowed period values

* Use auto_postpone_period_in_days for JSON entropy updates

The JSON API was accepting auto_postpone_period (seconds) which
bypassed the days normalization and validation. Switch to
auto_postpone_period_in_days consistently and add explicit
wrap_parameters so flat JSON params are wrapped correctly for
the virtual attribute.

* Default to account or 30-day fallback when knob value is invalid

* Address PR review feedback for entropy validation

- Fall back to first knob option (index 0) when persisted value isn't in
  the allowed set, preventing nil index errors for legacy values
- Use integer division (1.day.to_i) for consistent day calculations

* Default to account entropy period instead of first option for knob fallback

* Test that default auto-postpone period is in the allowed periods

* Return 422 instead of 500 for invalid entropy auto-postpone values

Rescue ActiveRecord::RecordInvalid in entropy controllers so invalid
auto_postpone_period_in_days values return 422 Unprocessable Entity
instead of raising a 500.

* Fix NoMethodError when board entropy falls back to account default

Use container.account.entropy.auto_postpone_period_in_days instead of
container.account.auto_postpone_period_in_days since Account doesn't
delegate that method.

* Test board entropy fallback to account default for invalid periods
This commit is contained in:
Rob Zolkos
2026-03-09 17:37:56 -04:00
committed by GitHub
parent 0435db30ba
commit 23ac76555b
15 changed files with 113 additions and 41 deletions
@@ -1,4 +1,6 @@
class Account::EntropiesController < ApplicationController
wrap_parameters :entropy, include: [ :auto_postpone_period_in_days ]
before_action :ensure_admin
def update
@@ -8,10 +10,12 @@ class Account::EntropiesController < ApplicationController
format.html { redirect_to account_settings_path, notice: "Account updated" }
format.json { head :no_content }
end
rescue ActiveRecord::RecordInvalid
head :unprocessable_entity
end
private
def entropy_params
params.expect(entropy: [ :auto_postpone_period ])
params.expect(entropy: [ :auto_postpone_period_in_days ])
end
end
@@ -1,5 +1,5 @@
class Boards::EntropiesController < ApplicationController
wrap_parameters :board
wrap_parameters :board, include: [ :auto_postpone_period_in_days ]
include BoardScoped
@@ -12,10 +12,12 @@ class Boards::EntropiesController < ApplicationController
format.turbo_stream
format.json { head :no_content }
end
rescue ActiveRecord::RecordInvalid
head :unprocessable_entity
end
private
def entropy_params
params.expect(board: [ :auto_postpone_period ])
params.expect(board: [ :auto_postpone_period_in_days ])
end
end
+1 -1
View File
@@ -88,7 +88,7 @@ class BoardsController < ApplicationController
end
def board_params
params.expect(board: [ :name, :all_access, :auto_postpone_period, :public_description ])
params.expect(board: [ :name, :all_access, :auto_postpone_period_in_days, :public_description ])
end
def grantees
-4
View File
@@ -1,8 +1,4 @@
module EntropyHelper
def entropy_auto_close_options
[ 3, 7, 30, 90, 365, 11 ]
end
def entropy_bubble_options_for(card)
{
daysBeforeReminder: card.entropy.days_before_reminder,
+1 -3
View File
@@ -6,9 +6,7 @@ module Board::AutoPostponing
end
private
DEFAULT_AUTO_POSTPONE_PERIOD = 30.days
def set_default_auto_postpone_period
self.auto_postpone_period ||= DEFAULT_AUTO_POSTPONE_PERIOD unless attribute_present?(:auto_postpone_period)
self.auto_postpone_period ||= Entropy::DEFAULT_AUTO_POSTPONE_PERIOD_IN_DAYS.days unless attribute_present?(:auto_postpone_period)
end
end
+6 -2
View File
@@ -2,7 +2,7 @@ module Board::Entropic
extend ActiveSupport::Concern
included do
delegate :auto_postpone_period, to: :entropy
delegate :auto_postpone_period, :auto_postpone_period_in_days, to: :entropy
has_one :entropy, as: :container, dependent: :destroy
end
@@ -12,6 +12,10 @@ module Board::Entropic
def auto_postpone_period=(new_value)
entropy ||= association(:entropy).reader || self.build_entropy
entropy.update auto_postpone_period: new_value
entropy.update! auto_postpone_period: new_value
end
def auto_postpone_period_in_days=(value)
self.auto_postpone_period = value.to_i.days.to_i
end
end
+29
View File
@@ -1,6 +1,35 @@
class Entropy < ApplicationRecord
DEFAULT_AUTO_POSTPONE_PERIOD_IN_DAYS = 30
AUTO_POSTPONE_PERIODS_IN_DAYS = [ 3, 7, 30, 90, 365, 11 ].freeze
AUTO_POSTPONE_PERIODS_IN_SECONDS = AUTO_POSTPONE_PERIODS_IN_DAYS.map { |n| n.day.in_seconds }.freeze
belongs_to :account, default: -> { container.account }
belongs_to :container, polymorphic: true
validates :auto_postpone_period, inclusion: { in: AUTO_POSTPONE_PERIODS_IN_SECONDS }
after_commit -> { container.cards.touch_all if container }
def auto_postpone_period_in_days
days = auto_postpone_period / 1.day.to_i
if days.in?(AUTO_POSTPONE_PERIODS_IN_DAYS)
days
else
default_auto_postpone_period_in_days
end
end
def auto_postpone_period_in_days=(new_value)
self.auto_postpone_period = new_value.to_i.days.to_i
end
private
def default_auto_postpone_period_in_days
if container.is_a?(Board)
container.account.entropy.auto_postpone_period_in_days
else
DEFAULT_AUTO_POSTPONE_PERIOD_IN_DAYS
end
end
end
+3 -2
View File
@@ -6,8 +6,9 @@
<%= form_with model: model, url: url, data: { controller: "form" } do |form| %>
<%= render "entropy/knob",
form: form,
name: :auto_postpone_period,
knob_options: entropy_auto_close_options,
name: :auto_postpone_period_in_days,
current_value: model.auto_postpone_period_in_days,
knob_options: Entropy::AUTO_POSTPONE_PERIODS_IN_DAYS,
label: "Days until auto-close",
disabled: disabled %>
<% end %>
+2 -3
View File
@@ -1,6 +1,5 @@
<%
period = form.object.send(name)
current_index = knob_options.index(period.to_i / 1.day)
current_index = knob_options.index(current_value) || knob_options.index(Entropy::DEFAULT_AUTO_POSTPONE_PERIOD_IN_DAYS)
%>
<fieldset class="knob" data-controller="knob" data-knob-target="field" style="--knob-options: <%= knob_options.length %>; --knob-index: <%= current_index %>">
@@ -8,7 +7,7 @@
<% knob_options.each_with_index do |value, index| %>
<label class="knob__option" style="--i: <%= index %>">
<%= form.radio_button name,
value.days,
value,
data: {
action: "change->knob#optionChanged change->form#submit",
index: index,