Skip to content
Draft
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
29 changes: 5 additions & 24 deletions app/components/admin/error_summary_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ def humanized_class_name

def error_items
sorted_errors.map do |error|
message = if dotted_array_attribute?(error)
format_dotted_attribute_message(error)
elsif object.respond_to?(:error_labels) && object.error_labels.key?(error.attribute.to_s)
"#{object.error_labels[error.attribute.to_s]} #{error.message}"
else
error.full_message
end
message = error.full_message

if object.respond_to?(:error_labels) && object.error_labels.key?(error.attribute.to_s)
message = "#{object.error_labels[error.attribute.to_s]} #{error.message}"
end

error_item = {
text: message,
Expand Down Expand Up @@ -77,21 +75,4 @@ def ga4_title

"#{object.try(:new_record?) ? 'New' : 'Editing'} #{object.model_name.human.downcase.titleize}"
end

def dotted_array_attribute?(error)
error.attribute.to_s.split(".").any? { |part| part.match?(/\A\d+\z/) }
end

def format_dotted_attribute_message(error)
model_key = object.class.try(:model_name)&.i18n_key
attribute_label = error.attribute.to_s.split(".").map { |part|
if part.match?(/\A\d+\z/)
(part.to_i + 1).to_s
else
i18n_key = "activerecord.attributes.#{model_key}.#{part}"
I18n.t(i18n_key, default: nil) || part.humanize.downcase
end
}.join(" ").capitalize
"#{attribute_label} #{error.message}"
end
end
20 changes: 0 additions & 20 deletions app/models/configurable_document_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,6 @@ def dynamic_tabs
end
end

def title_for_attribute(attribute)
segments = attribute.split(".").reject { |s| s.match?(/\A\d+\z/) }
find_field_title(form, segments)
end

def schema_for_fields(field_keys)
field_keys = field_keys.map(&:to_s)

Expand All @@ -137,21 +132,6 @@ class NotFoundError < StandardError

private

def find_field_title(config, segments)
return config["title"] if segments.empty?
return nil unless config["fields"]

config["fields"].each_value do |field|
path = Array(field["attribute_path"]) - %w[block_content]
next if path.empty? || segments.first(path.length) != path

result = find_field_title(field, segments.drop(path.length))
return result if result
end

nil
end

def validations_for_fields(field_keys)
(@schema["validations"] || {}).each_with_object({}) do |(validation_name, options), result|
matching_attributes = Array(options["attributes"]) & field_keys
Expand Down
4 changes: 0 additions & 4 deletions app/models/standard_edition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,6 @@ def error_field_order
.map { |block| block.path.validation_error_attribute }
end

def self.human_attribute_name(attribute, options = {})
options[:base]&.type_instance&.title_for_attribute(attribute.to_s) || super
end

def invalid_tab_messages
type_instance.form_keys.filter_map do |tab_key|
tab_form = StandardEdition::TabForm.new(self, tab_key)
Expand Down
10 changes: 2 additions & 8 deletions app/models/standard_edition/block_content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,15 @@ def run_schema_validations
end

def method_missing(symbol, *args)
# Errors on array item fields use dotted attribute names like
# :"social_media_links.0.url" so they can be targeted inline per
# field. Rails calls these as methods during error processing;
# returning nil here prevents a NoMethodError.
if symbol.to_s.include?(".")
nil
elsif attributes.class.instance_methods.include?(symbol)
if attributes.class.instance_methods.include?(symbol)
attributes.public_send(symbol, *args)
else
super
end
end

def respond_to_missing?(method_name, _include_all)
method_name.to_s.include?(".") || attributes.class.instance_methods.include?(method_name) || super
attributes.class.instance_methods.include?(method_name) || super
end

def attributes_class_for(attribute_config)
Expand Down
43 changes: 19 additions & 24 deletions app/validators/social_media_links_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ def initialize(opts = {})
def validate(record)
@attributes.each do |attribute_name|
arr = record.send(attribute_name.to_sym) || []
@channels_seen = []
@urls_seen = []
@titles_seen = []

arr.each_with_index do |social_media_account, index|
channel_name = social_media_account[@channel_field]
Expand All @@ -22,7 +19,7 @@ def validate(record)
channel = { channel_name: channel_name, title: title }

validate_social_media_channel(channel, index, record, attribute_name)
validate_social_media_title(title, index, record, attribute_name)
validate_social_media_title(title, channel_name, index, record, attribute_name)
validate_social_media_url(url, index, record, attribute_name)
end
end
Expand All @@ -31,55 +28,53 @@ def validate(record)
private

def validate_social_media_channel(channel, index, record, attribute_name)
@channels_seen ||= []
if channel[:channel_name].blank?
record.errors.add(
:"#{attribute_name}.#{index}.#{@channel_field}",
:blank,
message: "cannot be blank",
attribute_name.to_sym,
:invalid_social_media_link,
message: "Social media channel #{index + 1} service name cannot be blank",
)
elsif @channels_seen.select { |c| c[:channel_name] == channel[:channel_name] && c[:title] == channel[:title] && c[:title].blank? }.any?
record.errors.add(
:"#{attribute_name}.#{index}.#{@channel_field}",
:taken,
message: "must be unique",
)
else
@channels_seen << channel
return false
end
@channels_seen << channel
end

def validate_social_media_title(title, index, record, attribute_name)
def validate_social_media_title(title, channel_name, index, record, attribute_name)
@titles_seen ||= []
title ||= channel_name
return if title.blank?

if @titles_seen.include?(title)
record.errors.add(
:"#{attribute_name}.#{index}.#{@title_field}",
attribute_name.to_sym,
:taken,
message: "must be unique",
message: "Social media channel #{index + 1} title must be unique",
)
else
@titles_seen << title
end
end

def validate_social_media_url(url, index, record, attribute_name)
@urls_seen ||= []
if url.blank?
record.errors.add(
:"#{attribute_name}.#{index}.#{@url_field}",
attribute_name.to_sym,
:blank,
message: "cannot be blank",
message: "Social media channel #{index + 1} URL cannot be blank",
)
elsif !valid_url?(url)
record.errors.add(
:"#{attribute_name}.#{index}.#{@url_field}",
attribute_name.to_sym,
:invalid,
message: "is invalid - use the full URL, including https://",
message: "Social media channel #{index + 1} URL is invalid - use the full URL, including https://",
)
elsif @urls_seen.include?(url)
record.errors.add(
:"#{attribute_name}.#{index}.#{@url_field}",
attribute_name.to_sym,
:taken,
message: "must be unique",
message: "Social media channel #{index + 1} URL must be unique",
)
else
@urls_seen << url
Expand Down
29 changes: 0 additions & 29 deletions test/components/admin/error_summary_component_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -153,31 +153,6 @@ class Admin::ErrorSummaryComponentTest < ViewComponent::TestCase
assert_equal third_link.text, "Date label is invalid"
assert_equal third_link[:href], "#labelled_error_summary_test_object_date"
end

test "renders full message for dotted attributes" do
object = DottedAttributeErrorSummaryTestObject.new("title", Time.zone.today)
object.errors.add("social_media_links.0.url".to_sym, :blank, message: "cannot be blank")
render_inline(Admin::ErrorSummaryComponent.new(object:))

link = page.find(".gem-c-error-summary__list-item a")
assert_equal "Social media links 1 url cannot be blank", link.text
assert_equal "#dotted_attribute_error_summary_test_object_social_media_links_0_url", link[:href]
end

test "renders full message with locale labels for dotted attributes" do
object = DottedAttributeErrorSummaryTestObject.new("title", Time.zone.today)
original_i18n_key = DottedAttributeErrorSummaryTestObject.model_name.i18n_key
object.class.model_name.define_singleton_method(:i18n_key) { :standard_edition }
object.errors.add("social_media_links.0.url".to_sym, :blank, message: "cannot be blank")
object.errors.add("social_media_links.0.social_media_service_name".to_sym, :blank, message: "cannot be blank")
render_inline(Admin::ErrorSummaryComponent.new(object:))

links = page.find_all(".gem-c-error-summary__list-item a")
assert_equal "Social media account 1 url cannot be blank", links[0].text
assert_equal "Social media account 1 channel cannot be blank", links[1].text
ensure
object.class.model_name.define_singleton_method(:i18n_key) { original_i18n_key }
end
end

class ErrorSummaryTestObject
Expand Down Expand Up @@ -205,7 +180,3 @@ def error_labels
}
end
end

class DottedAttributeErrorSummaryTestObject < ErrorSummaryTestObject
include WithNestedAttributeErrors
end
13 changes: 0 additions & 13 deletions test/support/with_nested_attribute_errors.rb

This file was deleted.

114 changes: 0 additions & 114 deletions test/unit/app/models/configurable_document_type_test.rb
Original file line number Diff line number Diff line change
@@ -1,120 +1,6 @@
require "test_helper"

class ConfigurableDocumentTypeTest < ActiveSupport::TestCase
test "#title_for_attribute strips numeric index segments and returns the field title" do
type_config = build_configurable_document_type("test_type", {
"forms" => {
"documents" => {
"fields" => {
"field_attribute" => {
"title" => "Test Attribute",
"attribute_path" => %w[field_attribute],
},
},
},
},
})
ConfigurableDocumentType.setup_test_types(type_config)
document_type = ConfigurableDocumentType.find("test_type")

assert_equal "Test Attribute", document_type.title_for_attribute("field_attribute.0")
end

test "#find_field_title returns the field title when segments is empty" do
type_config = build_configurable_document_type("test_type", {
"forms" => {
"documents" => {
"fields" => {
"field_attribute" => {
"title" => "Test Attribute",
"attribute_path" => %w[field_attribute],
},
},
},
},
})
ConfigurableDocumentType.setup_test_types(type_config)
document_type = ConfigurableDocumentType.find("test_type")

assert_equal "Test Attribute", document_type.title_for_attribute("field_attribute")
assert_equal document_type.title_for_attribute("field_attribute"), document_type.title_for_attribute("field_attribute.0")
end

test "#title_for_attribute returns the title for a nested field" do
type_config = build_configurable_document_type("test_type", {
"forms" => {
"documents" => {
"fields" => {
"outer_field" => {
"title" => "Outer Field",
"attribute_path" => %w[outer_field],
"fields" => {
"inner_field" => {
"title" => "Inner Field Title",
"attribute_path" => %w[inner_field],
},
},
},
},
},
},
})
ConfigurableDocumentType.setup_test_types(type_config)
document_type = ConfigurableDocumentType.find("test_type")

assert_equal "Inner Field Title", document_type.title_for_attribute("outer_field.0.inner_field")
end

test "#title_for_attribute returns the configured title for a field whose attribute_path includes 'block_content', so error messages use the exact label from the document type config rather than Rails' default humanisation" do
type_config = build_configurable_document_type("test_type", {
"forms" => {
"documents" => {
"fields" => {
"social_media_links" => {
"title" => "Social Media Links",
"attribute_path" => %w[block_content social_media_links],
},
},
},
},
})
ConfigurableDocumentType.setup_test_types(type_config)
document_type = ConfigurableDocumentType.find("test_type")

assert_equal "Social Media Links", document_type.title_for_attribute("social_media_links")
end

test "#title_for_attribute returns nil for an unrecognised attribute" do
ConfigurableDocumentType.setup_test_types(build_configurable_document_type("test_type"))
document_type = ConfigurableDocumentType.find("test_type")

assert_nil document_type.title_for_attribute("nonexistent_attribute")
end

test "#title_for_attribute only returns the title for the field whose attribute_path matches the given attribute, not the first field it encounters" do
type_config = build_configurable_document_type("test_type", {
"forms" => {
"documents" => {
"fields" => {
"social_media_links" => {
"title" => "Social Media Links",
"attribute_path" => %w[social_media_links],
},
"body" => {
"title" => "Body",
"attribute_path" => %w[body],
},
},
},
},
})
ConfigurableDocumentType.setup_test_types(type_config)
document_type = ConfigurableDocumentType.find("test_type")

assert_equal "Body", document_type.title_for_attribute("body")
assert_equal "Social Media Links", document_type.title_for_attribute("social_media_links")
end

test ".find raises an error if the type is not specified" do
error = assert_raises(ConfigurableDocumentType::NotFoundError) { ConfigurableDocumentType.find(nil) }
assert_equal "No document type specified", error.message
Expand Down
Loading