Skip to content

Commit 6ef934c

Browse files
committed
Add logic for dependent pick lists
1 parent 188ab5b commit 6ef934c

File tree

6 files changed

+94
-57
lines changed

6 files changed

+94
-57
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,13 @@ options = {
3737
# multiple rows
3838
"data_sources" => {
3939
"food_types" => %w[Polenta Paella Papaya],
40+
"countries" => {"Canada"=>["Sxwōxwiyám", "Toronto"], "Türkiye"=>["Eskişehir", "İzmir", "İstanbul"]}
4041
},
4142
"validations" => {
4243
"favourite_food" => SpreadsheetExporter::ColumnValidation.new(
43-
attribute_name: "favourite_food",
4444
data_source: "food_types"
4545
),
4646
"yuckiest_food" => SpreadsheetExporter::ColumnValidation.new(
47-
attribute_name: "yuckiest_food",
4847
data_source: "food_types"
4948
)
5049
}

lib/spreadsheet_exporter/column_validation.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module SpreadsheetExporter
2-
ColumnValidation = Struct.new(:attribute_name, :ignore_blank, :data_source, :indirect_built_from, :error_type, keyword_init: true) do
2+
ColumnValidation = Struct.new(:ignore_blank, :data_source, :indirect_built_from, :error_type, keyword_init: true) do
33
def initialize(*)
44
super
55
self.ignore_blank = true if ignore_blank.nil?

lib/spreadsheet_exporter/spreadsheet.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ def self.from_objects(objects, options = {})
77

88
# Get all the data and accumulate headers from each row (since rows may not have all the same attributes)
99
Array(objects).each do |object|
10-
data = object.respond_to?(:as_csv) ? get_values(object.as_csv(options)) : get_values(object.as_json(options))
11-
headers = headers | data.keys
10+
data = if object.respond_to?(:as_spreadsheet)
11+
get_values(object.as_spreadsheet(options))
12+
elsif object.respond_to?(:as_csv)
13+
get_values(object.as_csv(options))
14+
else
15+
get_values(object.as_json(options))
16+
end
17+
18+
headers |= data.keys
1219
rows << data
1320
end
1421

lib/spreadsheet_exporter/xlsx.rb

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def self.from_spreadsheet(spreadsheet, options = {})
3737
end
3838

3939
data_sources = add_data_sources(workbook, header_format, options)
40+
pp data_sources
4041
add_worksheet_validation(workbook, worksheet, column_indexes, data_sources, header_format, options)
4142

4243
workbook.worksheets.each do |ws|
@@ -47,6 +48,10 @@ def self.from_spreadsheet(spreadsheet, options = {})
4748
io.string
4849
end
4950

51+
def self.sanitize_defined_name(raw)
52+
raw.gsub(/^[A-Za-z_]/, "_")
53+
end
54+
5055
def self.add_data_sources(workbook, header_format, options = {})
5156
data_sources = options.fetch("data_sources", {}) || {}
5257
return {} if data_sources.empty?
@@ -57,8 +62,19 @@ def self.add_data_sources(workbook, header_format, options = {})
5762

5863
data_source_refs = {}
5964

60-
data_sources.each_with_index do |(data_key, data_values), column_index|
61-
data_source_refs[data_key] = add_data_source(workbook, data_sheet, data_key, data_values, column_index, header_format)
65+
column_index = 0
66+
data_sources.each do |data_key, data_values|
67+
if data_values.is_a?(Hash)
68+
# nested, conditional data structure
69+
data_values.each do |data_value, sub_values|
70+
sub_key = sanitize_defined_name("#{data_key}_#{data_value}")
71+
data_source_refs[sub_key] = add_data_source(workbook, data_sheet, sub_key, sub_values, column_index, header_format)
72+
column_index += 1
73+
end
74+
else
75+
data_source_refs[data_key] = add_data_source(workbook, data_sheet, data_key, data_values, column_index, header_format)
76+
column_index += 1
77+
end
6278
end
6379

6480
data_source_refs
@@ -68,15 +84,18 @@ def self.add_data_sources(workbook, header_format, options = {})
6884
#
6985
# Returnd the named range's name
7086
def self.add_data_source(workbook, data_sheet, data_key, data_values, column_index, header_format)
71-
raise ArgumentError unless data_values.is_a?(Array)
87+
unless data_values.is_a?(Array)
88+
debugger
89+
raise ArgumentError, "data_values should be an array (got #{data_values.inspect}"
90+
end
7291

7392
data_start = xl_rowcol_to_cell(1, column_index, true, true)
7493
data_end = xl_rowcol_to_cell(data_values.length, column_index, true, true)
7594

7695
defined_name_source = "=#{DATA_WORKSHEET_NAME}!#{data_start}:#{data_end}"
7796

7897
data_sheet.write(0, column_index, data_key, header_format)
79-
data_sheet.write_col(1, column_index, data_values)
98+
data_sheet.write_col(1, column_index, data_values.map(&:strip))
8099
defined_name = data_key
81100
workbook.define_name(defined_name, defined_name_source)
82101
defined_name
@@ -85,6 +104,7 @@ def self.add_data_source(workbook, data_sheet, data_key, data_values, column_ind
85104
# TODO: we should DRY this up with the Spreadsheet.from_objects logic
86105
def self.rewrite_validation_column_names(column_validations, options)
87106
return column_validations unless options["humanize_headers_class"]
107+
88108
klass = options["humanize_headers_class"]
89109

90110
column_validations.each_with_object({}) do |(attribute, v), obj|
@@ -109,18 +129,34 @@ def self.add_worksheet_validation(workbook, worksheet, column_indexes, data_sour
109129
next
110130
end
111131

112-
defined_name = data_sources[column_validation.data_source]
113-
raise ArgumentError, "missing data for data_source=#{column_validation.data_source}" unless defined_name
114-
115-
validation_options = add_column_validation(column_validation, defined_name)
116-
117-
pp validation_options
118-
119-
worksheet.data_validation(1, column_index, ROW_MAX, column_index, validation_options)
132+
# Excel's `INDIRECT` function lets us build up the name of a defined range dynamically
133+
if column_validation.indirect_built_from
134+
parent_column_index = column_indexes[column_validation.indirect_built_from]
135+
136+
(1..100).each do |row_index|
137+
indirect_cell = xl_rowcol_to_cell(row_index, parent_column_index, false, false)
138+
defined_name = "INDIRECT(\"#{column_validation.data_source}\" & \"_\" & SUBSTITUTE(#{indirect_cell}, \" \", \"\"))"
139+
140+
validation_options = generate_validation(column_validation, defined_name)
141+
pp validation_options
142+
worksheet.data_validation(row_index, column_index, validation_options)
143+
end
144+
else
145+
defined_name = data_sources[column_validation.data_source]
146+
unless defined_name
147+
raise ArgumentError, "missing data for data_source=#{column_validation.data_source}"
148+
end
149+
150+
validation_options = generate_validation(column_validation, defined_name)
151+
pp validation_options
152+
worksheet.data_validation(1, column_index, ROW_MAX, column_index, validation_options)
153+
end
154+
rescue StandardError => e
155+
debugger
120156
end
121157
end
122158

123-
def self.add_column_validation(column_validation, defined_name)
159+
def self.generate_validation(column_validation, defined_name)
124160
{
125161
"validate" => "list",
126162
"input_title" => "Select a value",

test.rb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,37 +20,33 @@
2020
"data_sources" => {
2121
"all_meals" => MEALS,
2222
"roles" => %w[admin user spammer boss],
23-
"countries" => CONDITIONAL_CITIES.keys
23+
"countries" => COUNTRIES,
24+
"cities" => CONDITIONAL_CITIES
2425
},
2526

2627
"validations" => {
2728
"role" => SpreadsheetExporter::ColumnValidation.new(
28-
attribute_name: "role",
2929
ignore_blank: false,
3030
data_source: "roles"
3131
),
3232

3333
"country" => SpreadsheetExporter::ColumnValidation.new(
34-
attribute_name: "country",
3534
ignore_blank: true,
3635
error_type: "information",
3736
data_source: "countries"
3837
),
39-
# "city" => SpreadsheetExporter::ColumnValidation.new(
40-
# :attribute_name => "city",
41-
# :ignore_blank => true,
42-
# :error_type => "information",
43-
# :indirect_built_from => "country",
44-
# :data_source => CONDITIONAL_CITIES
45-
# ),
38+
"city" => SpreadsheetExporter::ColumnValidation.new(
39+
ignore_blank: true,
40+
error_type: "information",
41+
indirect_built_from: "country",
42+
data_source: "cities"
43+
),
4644
"favourite_meal" => SpreadsheetExporter::ColumnValidation.new(
47-
attribute_name: "favourite_meal",
4845
ignore_blank: true,
4946
error_type: "warning",
5047
data_source: "all_meals"
5148
),
5249
"most_recent_meal" => SpreadsheetExporter::ColumnValidation.new(
53-
attribute_name: "most_recent_meal",
5450
ignore_blank: true,
5551
error_type: "warning",
5652
data_source: "all_meals"

test_data.rb

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
1-
CITIES = [
2-
"Sxwōxwiyám: Sqáyéx/Xwyélés",
3-
"Ceuta",
4-
"Juanhaven",
5-
"佳市",
6-
"East Sarah",
7-
"山武郡横芝光町",
8-
"川崎市宮前区",
9-
"Blondel-sur-Pottier",
10-
"West Christine",
11-
"Lake Amandahaven",
12-
"Weekshaven",
13-
"Breuerscheid",
14-
"Matisscheid",
15-
"Groß Naemidorf",
16-
"Groß Maxim",
17-
"Scharfgrün",
18-
"Neu Liahhagen",
19-
"Eliasstadt",
20-
"Mögenburgscheid",
21-
"Edirne",
22-
"Eskişehir",
23-
"İzmir",
24-
"İstanbul",
25-
"Van",
26-
"Şırnak"
27-
].map { |s| s.encode("UTF-8") }
1+
def country_and_city
2+
country = sample_country
3+
city = CONDITIONAL_CITIES[country].sample
4+
{country: country, city: city}
5+
end
6+
7+
def sample_country
8+
CONDITIONAL_CITIES.keys.sample
9+
end
10+
11+
MEALS = %w[Omnivore Veg Vegan]
12+
13+
# COUNTRIES = %w[Canada Türkiye]
14+
COUNTRIES = %w[Canada Turkey]
15+
16+
CONDITIONAL_CITIES = {
17+
COUNTRIES[0] => [
18+
"Sxwōxwiyám",
19+
"Toronto"
20+
].map { |s| s.encode("UTF-8") },
21+
COUNTRIES[1] => [
22+
"Eskişehir",
23+
"İzmir",
24+
"İstanbul"
25+
].map { |s| s.encode("UTF-8") }
26+
}

0 commit comments

Comments
 (0)