Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for procs and method calls in inclusion validation dropdown resolution #1040

Merged
merged 4 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,23 @@ to run. Since arguments are specified in the user interface via text area
inputs, it’s important to check that they conform to the format your Task
expects, and to sanitize any inputs if necessary.

#### Validating Task Parameters

Task attributes can be validated using Active Model Validations. Attributes are
validated before a Task is enqueued.

If an attribute uses an inclusion validator with a supported `in:` option, the
set of values will be used to populate a dropdown in the user interface. The
following types are supported:

* Arrays
* Procs and lambdas that optionally accept the Task instance, and return an Array.
* Callable objects that receive one argument, the Task instance, and return an Array.
* Methods that return an Array, called on the Task instance.

For enumerables that don't match the supported types, a text field will be
rendered instead.

### Custom cursor columns to improve performance

The [job-iteration gem][job-iteration], on which this gem depends, adds an
Expand Down
34 changes: 29 additions & 5 deletions app/helpers/maintenance_tasks/tasks_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,53 @@ def csv_file_download_path(run)
end

# Resolves values covered by the inclusion validator for a task attribute.
# Only Arrays are supported, option types such as:
# Procs, lambdas, symbols, and Range are not supported and return nil.
# Supported option types:
# - Arrays
# - Procs and lambdas that optionally accept the Task instance, and return an Array.
# - Callable objects that receive one argument, the Task instance, and return an Array.
# - Methods that return an Array, called on the Task instance.
#
# Other types are not supported and will return nil.
#
# Returned values are used to populate a dropdown list of options.
#
# @param task_class [Class<Task>] The task class for which the value needs to be resolved.
# @param parameter_name [String] The parameter name.
#
# @return [Array] value of the resolved inclusion option.
def resolve_inclusion_value(task_class, parameter_name)
def resolve_inclusion_value(task, parameter_name)
task_class = task.class
inclusion_validator = task_class.validators_on(parameter_name).find do |validator|
validator.kind == :inclusion
end
return unless inclusion_validator

in_option = inclusion_validator.options[:in] || inclusion_validator.options[:within]
in_option if in_option.is_a?(Array)
resolved_in_option = case in_option
when Proc
if in_option.arity == 0
in_option.call
else
in_option.call(task)
end
when Symbol
method = task.method(in_option)
method.call if method.arity.zero?
else
if in_option.respond_to?(:call)
in_option.call(task)
else
in_option
end
end

resolved_in_option if resolved_in_option.is_a?(Array)
end

# Return the appropriate field tag for the parameter, based on its type.
# If the parameter has a `validates_inclusion_of` validator, return a dropdown list of options instead.
def parameter_field(form_builder, parameter_name)
inclusion_values = resolve_inclusion_value(form_builder.object.class, parameter_name)
inclusion_values = resolve_inclusion_value(form_builder.object, parameter_name)
return form_builder.select(parameter_name, inclusion_values, prompt: "Select a value") if inclusion_values

case form_builder.object.class.attribute_types[parameter_name]
Expand Down
29 changes: 22 additions & 7 deletions test/dummy/app/tasks/maintenance/params_task.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# frozen_string_literal: true

module Maintenance
class DropdownOptions
class << self
def call(_task)
[100, 200, 300]
end
end
end

class ParamsTask < MaintenanceTasks::Task
attribute :post_ids, :string

Expand All @@ -20,24 +28,31 @@ class ParamsTask < MaintenanceTasks::Task

# Dropdown options with supported scenarios
attribute :integer_dropdown_attr, :integer
attribute :integer_dropdown_attr_proc_no_arg, :integer
attribute :integer_dropdown_attr_proc_arg, :integer
attribute :integer_dropdown_attr_from_method, :integer
attribute :integer_dropdown_attr_callable, :integer
attribute :boolean_dropdown_attr, :boolean

validates_inclusion_of :integer_dropdown_attr, in: [100, 200, 300], allow_nil: true
validates_inclusion_of :integer_dropdown_attr_proc_no_arg, in: proc { [100, 200, 300] }, allow_nil: true
validates_inclusion_of :integer_dropdown_attr_proc_arg, in: proc { |_task| [100, 200, 300] }, allow_nil: true
validates_inclusion_of :integer_dropdown_attr_from_method, in: :dropdown_attr_options, allow_nil: true
validates_inclusion_of :integer_dropdown_attr_callable, in: DropdownOptions, allow_nil: true
validates_inclusion_of :boolean_dropdown_attr, within: [true, false], allow_nil: true

# Dropdown options with unsupported scenarios
attribute :text_integer_attr, :integer
attribute :text_integer_attr2, :integer
attribute :text_integer_attr3, :integer

validates_inclusion_of :text_integer_attr, in: proc { [100, 200, 300] }, allow_nil: true
validates_inclusion_of :text_integer_attr2, in: :undefined_symbol, allow_nil: true
validates_inclusion_of :text_integer_attr3, in: (100..), allow_nil: true
attribute :text_integer_attr_unbounded_range, :integer
validates_inclusion_of :text_integer_attr_unbounded_range, in: (100..), allow_nil: true

class << self
attr_accessor :fast_task
end

def dropdown_attr_options
[100, 200, 300]
end

def collection
Post.where(id: post_ids_array)
end
Expand Down
17 changes: 11 additions & 6 deletions test/helpers/maintenance_tasks/tasks_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,18 @@ class TasksHelperTest < ActionView::TestCase
end

test "#resolve_inclusion_value resolves inclusion validator for task attributes" do
assert_match "Select a value", markup("integer_dropdown_attr").squish

assert_match "Select a value", markup("boolean_dropdown_attr").squish

["text_integer_attr", "text_integer_attr2", "text_integer_attr3"].each do |text_integer_attr|
refute_match "Select a value", markup(text_integer_attr).squish
[
"integer_dropdown_attr",
"boolean_dropdown_attr",
"integer_dropdown_attr_proc_no_arg",
"integer_dropdown_attr_proc_arg",
"integer_dropdown_attr_from_method",
"integer_dropdown_attr_callable",
].each do |attribute|
assert_match "Select a value", markup(attribute).squish
end

refute_match "Select a value", markup("text_integer_attr_unbounded_range").squish
end

test "#parameter_field adds information about datetime fields when Time.zone_default is not set" do
Expand Down
8 changes: 5 additions & 3 deletions test/models/maintenance_tasks/task_data_show_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@ class TaskDataShowTest < ActiveSupport::TestCase
"time_attr",
"boolean_attr",
"integer_dropdown_attr",
"integer_dropdown_attr_proc_no_arg",
"integer_dropdown_attr_proc_arg",
"integer_dropdown_attr_from_method",
"integer_dropdown_attr_callable",
"boolean_dropdown_attr",
"text_integer_attr",
"text_integer_attr2",
"text_integer_attr3",
"text_integer_attr_unbounded_range",
],
TaskDataShow.new("Maintenance::ParamsTask").parameter_names,
)
Expand Down
34 changes: 17 additions & 17 deletions test/system/maintenance_tasks/tasks_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,24 +117,24 @@ class TasksTest < ApplicationSystemTestCase
assert_equal("input", boolean_field.tag_name)
assert_equal("checkbox", boolean_field[:type])

integer_dropdown_field = page.find_field("task[integer_dropdown_attr]")
assert_equal("select", integer_dropdown_field.tag_name)
assert_equal("select-one", integer_dropdown_field[:type])
integer_dropdown_field_options = integer_dropdown_field.find_all("option").map { |option| option[:value] }
assert_equal(["", "100", "200", "300"], integer_dropdown_field_options)

boolean_dropdown_field = page.find_field("task[boolean_dropdown_attr]")
assert_equal("select", boolean_dropdown_field.tag_name)
assert_equal("select-one", boolean_dropdown_field[:type])
boolean_dropdown_field_options = boolean_dropdown_field.find_all("option").map { |option| option[:value] }
assert_equal(["", "true", "false"], boolean_dropdown_field_options)

["text_integer_attr", "text_integer_attr2", "text_integer_attr3"].each do |text_integer_attr|
text_integer_dropdown_field = page.find_field("task[#{text_integer_attr}]")
assert_equal("input", text_integer_dropdown_field.tag_name)
assert_equal("number", text_integer_dropdown_field[:type])
assert_empty(text_integer_dropdown_field[:step])
[
"integer_dropdown_attr",
"integer_dropdown_attr_proc_no_arg",
"integer_dropdown_attr_proc_arg",
"integer_dropdown_attr_from_method",
"integer_dropdown_attr_callable",
].each do |dropdown_integer_attr|
integer_dropdown_field = page.find_field("task[#{dropdown_integer_attr}]")
assert_equal("select", integer_dropdown_field.tag_name)
assert_equal("select-one", integer_dropdown_field[:type])
integer_dropdown_field_options = integer_dropdown_field.find_all("option").map { |option| option[:value] }
assert_equal(["", "100", "200", "300"], integer_dropdown_field_options)
end

text_integer_field = page.find_field("task[text_integer_attr_unbounded_range]")
assert_equal("input", text_integer_field.tag_name)
assert_equal("number", text_integer_field[:type])
assert_empty(text_integer_field[:step])
end

test "view a Task with multiple pages of Runs" do
Expand Down
Loading