brick 1.0.246 → 1.0.247

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3410ae5fca9b12aa31007df9d3fac95a39f09eee58f4ffc2089bada97bde7793
4
- data.tar.gz: 0da56c9311d8dd0ce2fd3a05039a6f85ca9910c64b303c25fc26f8d65fab512b
3
+ metadata.gz: baf36636026c4ecad5ff76ccdeea80604c6c9128f7065d7b546ab41cc4fccf1c
4
+ data.tar.gz: ec8c081354229eb5b4cdcd3112b24313468aaf731f856168cace28d3882e1f51
5
5
  SHA512:
6
- metadata.gz: 7a2e821dfee25a5df265caf5700e25003cbc7cab88e439102fc949f5dfbba2f9f7b73803c98068d373ffbf12cab27dac3859c76a7772ea19bbaf4ffdbfbddbe6
7
- data.tar.gz: 701d6d9e7d374cbe850806ce732f5b765c1ea3d051c111f316bcd29fdf45622d8999c04fa89ca058f13052670f56c77350fde24e00139ef40dc892b0c97ca896
6
+ metadata.gz: 3742d4d628c4f0ff18008f5dd965b3e49c545d24141cea917a0ba141cff7f3243b6f159e732f2e786b87f274e80237bcea2af74f17b9494345cf898e54e72928
7
+ data.tar.gz: 3aa5e54bc3b4af270e5b2342430e294920b151139af66ec5771b15a575377f7275f9c10e593d7c1d314e67d145fd9ffb0ae834b4f68a5d4c1afff8b22ecc404c
@@ -105,13 +105,41 @@ module ActiveRecord
105
105
  col_names = columns_hash.keys
106
106
  # If it's a composite primary key then allow all the values through
107
107
  # TODO: Should disallow any autoincrement / SERIAL columns
108
- if skip_id && (pk_as_array = _pk_as_array).length == 1
109
- col_names -= _pk_as_array
108
+ if skip_id && (pk_as_array = _pk_as_array).length == 1 &&
109
+ [:string, :text].exclude?(columns_hash[pk_as_array.first]&.type)
110
+ col_names -= pk_as_array
110
111
  end
111
112
  hoa, hma, rtans = _activestorage_actiontext_fields
112
113
  col_names.map(&:to_sym) + hoa + hma.map { |as| { as => [] } } + rtans.values
113
114
  end
114
115
 
116
+ def _brick_set_wheres(params, join_array = nil)
117
+ is_distinct = nil
118
+ wheres = {}
119
+ params.each do |k, v|
120
+ k = k.to_s # Rails < 4.2 comes in as a symbol
121
+ next unless k.start_with?('__')
122
+
123
+ k = k[2..-1] # Take off leading "__"
124
+ if (where_col = (ks = k.split('.')).last)[-1] == '!'
125
+ where_col = where_col[0..-2]
126
+ end
127
+ case ks.length
128
+ when 1
129
+ next unless self.column_names.any?(where_col) || self._brick_get_fks.include?(where_col)
130
+ when 2
131
+ assoc_name = ks.first.to_sym
132
+ # Make sure it's a good association name and that the model has that column name
133
+ next unless self.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
134
+
135
+ join_array&.[]=(assoc_name, nil) # Store this relation name in our special collection for .joins()
136
+ is_distinct = true
137
+ end
138
+ wheres[k] = v.is_a?(String) ? v.split(',') : v
139
+ end
140
+ [wheres, is_distinct]
141
+ end
142
+
115
143
  # Return three lists of fields for this model --
116
144
  # has_one_attached, has_many_attached, and has_rich_text
117
145
  def _activestorage_actiontext_fields
@@ -219,10 +247,17 @@ module ActiveRecord
219
247
  else
220
248
  # If there's no DSL yet specified, just try to find the first usable column on this model
221
249
  dsl = if table_exists?
222
- skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || []) + [primary_key]
223
- if (descrip_col = columns.find { |c| c.type == :string && skip_columns.exclude?(c.name) })
250
+ skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || [])
251
+ column_list = columns.dup
252
+ pk_idx = column_list.index { |c| c.name == primary_key }
253
+ if pk_idx
254
+ pk_column = column_list.delete_at(pk_idx)
255
+ # if the PK column is a string, put it at the end so it will get chosen as a last resort
256
+ column_list << pk_column if [:string, :text].include?(columns_hash[primary_key]&.type)
257
+ end
258
+ if (descrip_col = column_list.find { |c| c.type == :string && skip_columns.exclude?(c.name) })
224
259
  "[#{descrip_col.name}]"
225
- elsif (descrip_col = columns.find { |c| [:boolean, :binary, :xml].exclude?(c.type) && skip_columns.exclude?(c.name) })
260
+ elsif (descrip_col = column_list.find { |c| [:boolean, :binary, :xml].exclude?(c.type) && skip_columns.exclude?(c.name) })
226
261
  "[#{descrip_col.name}]"
227
262
  else
228
263
  "#{name} ##{_pk_as_array.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
@@ -689,32 +724,10 @@ module ActiveRecord
689
724
  # model early in case the user wants to do an ORDER BY based on any of that.
690
725
  model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
691
726
 
692
- is_distinct = nil
693
- wheres = {}
694
727
  params = params.to_unsafe_h unless params.is_a?(Hash)
695
728
  params.merge!(args[1]) if args[1]
696
- params.each do |k, v|
697
- k = k.to_s # Rails < 4.2 comes in as a symbol
698
- next unless k.start_with?('__')
699
-
700
- k = k[2..-1] # Take off leading "__"
701
- if (where_col = (ks = k.split('.')).last)[-1] == '!'
702
- where_col = where_col[0..-2]
703
- end
704
- case ks.length
705
- when 1
706
- next unless klass.column_names.any?(where_col) || klass._brick_get_fks.include?(where_col)
707
- when 2
708
- assoc_name = ks.first.to_sym
709
- # Make sure it's a good association name and that the model has that column name
710
- next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(where_col)
711
-
712
- join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
713
- is_distinct = true
714
- distinct!
715
- end
716
- wheres[k] = v.is_a?(String) ? v.split(',') : v
717
- end
729
+ wheres, is_distinct = klass._brick_set_wheres(params, join_array)
730
+ distinct! if is_distinct
718
731
 
719
732
  # %%% Skip the metadata columns
720
733
  if selects.empty? # Default to all columns
@@ -1756,7 +1769,7 @@ class Object
1756
1769
  end
1757
1770
  full_name = if relation || schema_name.blank?
1758
1771
  if singular_table_name != table_name.singularize && # %%% Try this with http://localhost:3000/brick/spree/property_translations
1759
- (schema_module = ::Brick.config.table_name_prefixes.find { |k, v| table_name.start_with?(k) }&.last&.constantize)
1772
+ (schema_module = ::Brick.config.table_name_prefixes.find { |k, v| table_name.length > k&.length && table_name.start_with?(k) }&.last&.constantize)
1760
1773
  "#{schema_module&.name}::#{inheritable_name || model_name}"
1761
1774
  else
1762
1775
  inheritable_name || model_name
@@ -1775,7 +1788,7 @@ class Object
1775
1788
  ::Brick.config.exclude_tables.include?(matching)
1776
1789
 
1777
1790
  # Are they trying to use a pluralised class name such as "Employees" instead of "Employee"?
1778
- if table_name == singular_table_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
1791
+ if table_name == singular_table_name && model_name.pluralize == model_name && !ActiveSupport::Inflector.inflections.uncountable.include?(table_name)
1779
1792
  # unless ::Brick.config.sti_namespace_prefixes&.key?("::#{singular_table_name.camelize}::")
1780
1793
  # puts "Warning: Class name for a model that references table \"#{matching
1781
1794
  # }\" should be \"#{ActiveSupport::Inflector.singularize(inheritable_name || model_name)}\"."
@@ -2475,10 +2488,19 @@ class Object
2475
2488
 
2476
2489
  real_model = model.find_real_model(params)
2477
2490
 
2491
+ # %%% Allow params to define which columns to use for order_by
2492
+ # Overriding the default by providing a querystring param?
2493
+ order_by = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
2494
+
2478
2495
  if request.format == :csv # Asking for a template?
2479
2496
  require 'csv'
2480
2497
  exported_csv = CSV.generate(force_quotes: false) do |csv_out|
2481
- real_model.df_export(true, real_model.brick_import_template, false, wheres, order_by).each do |row|
2498
+ export_args = [true, real_model.brick_import_template, false]
2499
+ if real_model.method(:df_export).parameters.length > 3
2500
+ wheres, _is_distinct = real_model._brick_set_wheres(params)
2501
+ export_args += [wheres, order_by]
2502
+ end
2503
+ real_model.df_export(*export_args).each do |row|
2482
2504
  row.each do |d|
2483
2505
  row.each_with_index do |d, idx|
2484
2506
  # "false" disallows HTML encoding the descriptions of binary content
@@ -2499,11 +2521,6 @@ class Object
2499
2521
  end
2500
2522
 
2501
2523
  # Normal (not swagger or CSV) request
2502
-
2503
- # %%% Allow params to define which columns to use for order_by
2504
- # Overriding the default by providing a querystring param?
2505
- order_by = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
2506
-
2507
2524
  ar_relation = ActiveRecord.version < Gem::Version.new('4') ? real_model.preload : real_model.all
2508
2525
  params['_brick_is_api'] = true if (is_api = request.format == :js || current_api_root)
2509
2526
  @_brick_params = ar_relation._brick_querying((selects ||= []), params: params, order_by: order_by,
@@ -2696,7 +2713,14 @@ class Object
2696
2713
  render json: { result: es_result }
2697
2714
  else
2698
2715
  real_model, real_singular_table_name = model.real_singular(params)
2699
- created_obj = model.send(:new, send(params_name_sym))
2716
+ # If there are any foreign keys that are strings and we've received a blank value, set that to nil
2717
+ internal_params = (ac_params = send(params_name_sym))&.send(:parameters)
2718
+ real_model.reflect_on_all_associations.each do |a|
2719
+ if a.belongs_to? && internal_params.key?(fk_name = a.foreign_key.to_s) && [:string, :text].include?((fk_col = real_model.columns_hash[fk_name])&.type)
2720
+ internal_params[fk_name] = nil if internal_params[fk_name].blank?
2721
+ end
2722
+ end
2723
+ created_obj = model.send(:new, ac_params)
2700
2724
  if created_obj.respond_to?(inh_col = model.inheritance_column) && created_obj.send(inh_col) == ''
2701
2725
  created_obj.send("#{inh_col}=", model.name)
2702
2726
  end
@@ -2846,14 +2870,14 @@ class Object
2846
2870
  code << " def #{params_name}\n"
2847
2871
  require_txt = model.base_class.name.underscore.tr('/', '_')
2848
2872
  is_for_expects = ::ActiveSupport.version >= ::Gem::Version.new('8.0a')
2849
- permits_txt = model._brick_find_permits(model, permits = model._brick_all_fields(true), is_for_expects)
2873
+ permits = model._brick_find_permits(model, model._brick_all_fields(true), is_for_expects)
2850
2874
  if is_for_expects
2851
- code << " params.expect(#{require_txt}: #{permits_txt})\n"
2875
+ code << " params.expect(#{require_txt}: #{permits})\n"
2852
2876
  self.define_method(params_name) do
2853
2877
  params.expect({ model.base_class.name.underscore.tr('/', '_').to_sym => permits })
2854
2878
  end
2855
2879
  else
2856
- code << " params.require(:#{require_txt}).permit(#{permits_txt.map(&:inspect).join(', ')})\n"
2880
+ code << " params.require(:#{require_txt}).permit(#{permits.map(&:inspect).join(', ')})\n"
2857
2881
  self.define_method(params_name) do
2858
2882
  params.require(model.base_class.name.underscore.tr('/', '_').to_sym).permit(permits)
2859
2883
  end
@@ -950,7 +950,10 @@ if (window.brickFontFamily) {
950
950
  when 'index'
951
951
  if Object.const_defined?('DutyFree')
952
952
  template_link = "
953
- <%= link_to 'CSV', #{@_brick_model._brick_index}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
953
+ <%= csv_path = #{@_brick_model._brick_index}_path(format: :csv)
954
+ where_params = params.to_unsafe_h.each_with_object([]) { |v, s| s << \"#\{v.first}=#\{v[1]}\" if v.first.start_with?('__') }.join('&')
955
+ csv_path << \"?#\{where_params}\" unless where_params.blank?
956
+ link_to 'CSV', csv_path %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
954
957
  <div id=\"dropper\" contenteditable=\"true\"></div>
955
958
  <input type=\"button\" id=\"btnImport\" value=\"Import\">
956
959
 
@@ -1466,21 +1469,11 @@ if (description = rel&.fetch(:description, nil)) %>
1466
1469
  end
1467
1470
  %><%= link_to \"(See all #\{model_name.pluralize})\", see_all_path, { class: '__brick' } %>
1468
1471
  #{::Brick::Rails.erd_markup(@_brick_model, prefix) if @_brick_model}
1469
- <% if obj
1470
- # path_options = [obj.#{pk}]
1471
- # path_options << { '_brick_schema': } if
1472
- options = {}
1473
- options[:url] = if obj.new_record?
1474
- link_to_brick(obj.class, path_only: true) # Properly supports STI, but only works for :new
1475
- else
1476
- path_helper = obj.new_record? ? #{model_name}._brick_index : #{model_name}._brick_index(:singular)
1477
- options[:url] = send(\"#\{path_helper}_path\".to_sym, obj) if ::Brick.config.path_prefix || (path_helper != obj.class.table_name)
1478
- end
1479
- %>
1472
+ <% if obj %>
1480
1473
  <br><br>
1481
1474
 
1482
1475
  <%= # Write out the mega-form
1483
- brick_form_for(obj, options, #{model_name}, bts, #{pk.inspect}) %>
1476
+ brick_form_with(model: obj, bts: bts, pk: #{pk.inspect}) %>
1484
1477
 
1485
1478
  #{unless args.first == 'new'
1486
1479
  # Was: confirm_are_you_sure = ActionView.version < ::Gem::Version.new('7.0') ? "data: { confirm: \"Delete #\{model_name} -- Are you sure?\" }" : "form: { data: { turbo_confirm: \"Delete #\{model_name} -- Are you sure?\" } }"
@@ -9,18 +9,19 @@ module Brick::Rails::FormBuilder
9
9
  def brick_field(method, html_options = {}, val = nil, col = nil,
10
10
  bt = nil, bt_class = nil, bt_name = nil, bt_pair = nil)
11
11
  model = self.object.class
12
- col ||= model.columns_hash[method]
12
+ col ||= model.columns_hash[method.to_s]
13
13
  out = +'<table><tr><td>'
14
14
  html_options[:class] = 'dimmed' unless val
15
15
  is_revert = true
16
16
  template = instance_variable_get(:@template)
17
+ method_sym = method.to_sym
17
18
  if bt
18
19
  bt_class ||= bt[1].first.first
19
20
  bt_name ||= bt[1].map { |x| x.first.name }.join('/')
20
21
  bt_pair ||= bt[1].first
21
22
 
22
23
  html_options[:prompt] = "Select #{bt_name}"
23
- out << self.select(method.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options)
24
+ out << self.select(method_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options)
24
25
  bt_obj = nil
25
26
  begin
26
27
  bt_obj = bt_class&.find_by(bt_pair[1] => val)
@@ -38,8 +39,8 @@ module Brick::Rails::FormBuilder
38
39
  "<span class=\"orphan\">Orphaned ID: #{val}</span>".html_safe
39
40
  end
40
41
  out << bt_link if bt_link
41
- elsif model._brick_monetized_attributes&.include?(method)
42
- out << self.text_field(method.to_sym, html_options.merge({ value: Money.new(val.to_i).format }))
42
+ elsif model._brick_monetized_attributes&.include?(method.to_s)
43
+ out << self.text_field(method_sym, html_options.merge({ value: Money.new(val.to_i).format }))
43
44
  else
44
45
  col_type = if model.json_column?(col) || val.is_a?(Array)
45
46
  :json
@@ -61,40 +62,40 @@ module Brick::Rails::FormBuilder
61
62
  out << "<img src=\"#{url}\" title=\"#{val}\">"
62
63
  elsif model.respond_to?(:enumerized_attributes) && (opts = (attr = model.enumerized_attributes[method])&.options).present?
63
64
  enum_html_options = attr.kind_of?(Enumerize::Multiple) ? html_options.merge({ multiple: true, size: opts.length + 1 }) : html_options
64
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, enum_html_options)
65
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, enum_html_options)
65
66
  else
66
67
  spit_out_text_field = true
67
68
  end
68
69
  elsif col_type == :enum
69
70
  if col.respond_to?(:limit) && col.limit.present?
70
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + col.limit, { value: val.to_s || '^^^brick_NULL^^^' }, html_options)
71
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + col.limit, { value: val.to_s || '^^^brick_NULL^^^' }, html_options)
71
72
  else
72
73
  spit_out_text_field = true
73
74
  end
74
75
  else
75
76
  template.instance_variable_set(:@_text_fields_present, true)
76
- out << self.hidden_field(method.to_sym, html_options)
77
+ out << self.hidden_field(method_sym, html_options)
77
78
  out << "<trix-editor input=\"#{self.field_id(method)}\"></trix-editor>"
78
79
  end
79
80
  if spit_out_text_field
80
81
  # %%% Need to update the max-width with javascript when page width is adjusted?
81
82
  html_options.merge!(style: 'min-width: 154px;field-sizing: content;') # max-width: auto;
82
- out << self.text_field(method.to_sym, html_options)
83
+ out << self.text_field(method_sym, html_options)
83
84
  end
84
85
  when :boolean
85
- out << self.check_box(method.to_sym)
86
+ out << self.check_box(method_sym)
86
87
  when :integer, :decimal, :float
87
- if model.respond_to?(:attribute_types) && (enum_type = model.attribute_types[method]).is_a?(ActiveRecord::Enum::EnumType)
88
+ if model.respond_to?(:attribute_types) && (enum_type = model.attribute_types[method.to_s]).is_a?(ActiveRecord::Enum::EnumType)
88
89
  opts = enum_type.send(:mapping)&.each_with_object([]) { |v, s| s << [v.first, v.first] } || []
89
- out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, options)
90
+ out << self.select(method_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, options)
90
91
  else
91
92
  digit_pattern = col_type == :integer ? '(?:-|)\d*' : '(?:-|)\d*(?:\.\d*|)'
92
- # Used to do this for float / decimal: self.number_field method.to_sym
93
- out << self.text_field(method.to_sym, { pattern: digit_pattern, class: 'check-validity' })
93
+ # Used to do this for float / decimal: self.number_field method_sym
94
+ out << self.text_field(method_sym, { pattern: digit_pattern, class: 'check-validity' })
94
95
  end
95
96
  when *DT_PICKERS.keys
96
97
  template.instance_variable_set(:@_date_fields_present, true)
97
- out << self.text_field(method.to_sym, { class: DT_PICKERS[col_type] })
98
+ out << self.text_field(method_sym, { class: DT_PICKERS[col_type] })
98
99
  when :uuid
99
100
  is_revert = false
100
101
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
@@ -146,7 +147,7 @@ module Brick::Rails::FormBuilder
146
147
  end
147
148
  out << self.hidden_field("_brick_attached_#{method}", value: existing.join(',')) unless existing.blank?
148
149
  # Render a "Choose File(s)" input element
149
- args = [method.to_sym]
150
+ args = [method_sym]
150
151
  args << { multiple: true } if col&.type == :files
151
152
  out << self.file_field(*args)
152
153
  when :primary_key
@@ -163,7 +164,7 @@ module Brick::Rails::FormBuilder
163
164
  end
164
165
  # Because there are so danged many quotes in JSON, escape them specially by converting to backticks.
165
166
  # (and previous to this, escape backticks with our own goofy code of ^^br_btick__ )
166
- out << (json_field = self.hidden_field(method.to_sym, { class: 'jsonpicker', value: val_str&.gsub('`', '^^br_btick__')&.tr('\"', '`')&.html_safe }))
167
+ out << (json_field = self.hidden_field(method_sym, { class: 'jsonpicker', value: val_str&.gsub('`', '^^br_btick__')&.tr('\"', '`')&.html_safe }))
167
168
  out << "<div id=\"_br_json_#{self.field_id(method)}\"></div>"
168
169
  else
169
170
  is_revert = false
@@ -177,6 +178,7 @@ module Brick::Rails::FormBuilder
177
178
  end
178
179
  out << "</td></tr></table>
179
180
  "
181
+ @_brick&.fetch(:fields_rendered, nil)&.<< method.to_s # Track that we've rendered this one
180
182
  out.html_safe
181
183
  end # brick_field
182
184
  end
@@ -30,7 +30,7 @@ module Brick::Rails::FormTags
30
30
  col_keys = relation.columns.each_with_object([]) do |col, s|
31
31
  col_name = col.name
32
32
  next if inclusions&.exclude?(col_name) ||
33
- (pk.include?(col_name) && [:integer, :uuid].include?(col.type) && !bts&.key?(col_name)) ||
33
+ (pk.include?(col_name) && ([:string, :text].exclude?(col.type) || col_name == 'airtable_id') && !bts&.key?(col_name)) ||
34
34
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols&.include?(col_name)
35
35
 
36
36
  s << col_name
@@ -252,25 +252,52 @@ module Brick::Rails::FormTags
252
252
 
253
253
  # -----------------------------
254
254
  # Our mega show/new/update form
255
- def brick_form_for(obj, options = {}, model = obj.class, bts = {}, pk = (obj.class.primary_key || []))
255
+ def brick_form_with(**options, &block)
256
+ obj = options.delete(:model) || instance_variable_get("@#{obj_name = controller_name.singularize}".to_sym)
257
+ unless obj
258
+ puts "WARNING: Could not find #{obj_name} object"
259
+ return
260
+ end
261
+
262
+ model = obj.class
263
+ bts = options.delete(:bts) || {}
264
+ pk = options.delete(:pk) || model.primary_key || []
256
265
  pk = [pk] unless pk.is_a?(Array)
257
266
  pk.map!(&:to_s)
267
+ brick_stuff = { fields_rendered: (fields_rendered = []), bts: bts, pk: pk }
268
+
269
+ # options[:model] = obj.becomes(model.base_class) unless options.include?(:model)
270
+ unless options.include?(:url)
271
+ options[:url] = if obj.new_record?
272
+ link_to_brick(obj.class, path_only: true) # Properly supports STI, but only works for :new
273
+ else
274
+ path_helper = obj.new_record? ? model._brick_index : model._brick_index(:singular)
275
+ send("#{path_helper}_path".to_sym, obj) if ::Brick.config.path_prefix || (path_helper != obj.class.table_name)
276
+ end
277
+ end
278
+
279
+ # We call into the older #form_for in order to have compatibility with Rails < 5.1
258
280
  form_for(obj.becomes(model.base_class), options) do |f|
259
- out = +'<table class="shadow">'
260
- has_fields = false
281
+ out = +''
282
+ out << '<table class="shadow">'
283
+ f.instance_variable_set(:@_brick, brick_stuff)
284
+ has_fields = has_updatable_fields = nil
261
285
  # If it's a new record, set any default polymorphic types
262
- bts&.each do |_k, v|
286
+ bts.each do |_k, v|
263
287
  if v[2]
264
288
  obj.send("#{model.brick_foreign_type(v.first)}=", v[1].first&.first&.name)
265
289
  end
266
290
  end if obj.new_record?
267
291
  hoa, hma, rtans = model._activestorage_actiontext_fields
268
- (model.column_names + hoa + hma + rtans.keys).each do |k|
292
+
293
+ block.call(f) if block_given?
294
+
295
+ ((model.column_names + hoa + hma + rtans.keys) - fields_rendered).each do |k|
269
296
  pk_pos = (pk.index(k)&.+ 1)
270
- next if (pk_pos && pk.length == 1 && !bts.key?(k)) ||
297
+ col = model.columns_hash[k]
298
+ next if (pk_pos && pk.length == 1 && !bts.key?(k) && [:string, :text].exclude?(col.type)) ||
271
299
  ::Brick.config.metadata_columns.include?(k)
272
300
 
273
- col = model.columns_hash[k]
274
301
  if !col
275
302
  kwargs = if hoa.include?(k) # has_one_attached
276
303
  { sql_type: 'binary', type: :file }
@@ -291,7 +318,7 @@ module Brick::Rails::FormTags
291
318
  val = obj.attributes[k]
292
319
  out << "
293
320
  <tr>
294
- <th class=\"show-field\"#{" title=\"#{col&.comment}\"".html_safe if col&.respond_to?(:comment) && !col&.comment.blank?}>"
321
+ <th class=\"show-field\"#{" title=\"#{col&.comment}\"" if col&.respond_to?(:comment) && !col&.comment.blank?}>"
295
322
  has_fields = true
296
323
  if (bt = bts[k])
297
324
  # Add a final member in this array with descriptive options to be used in <select> drop-downs
@@ -334,35 +361,38 @@ module Brick::Rails::FormTags
334
361
  else
335
362
  out << model.human_attribute_name(k, { default: k })
336
363
  end
337
- out << " (PK #{pk_pos})" if pk_pos
364
+ out << " (PK&nbsp;#{pk_pos})" if pk_pos
338
365
  out << "
339
366
  </th>
340
367
  <td>
341
368
  "
342
- if pk_pos
369
+ if pk_pos &&
370
+ # Allow new records to have a text-based primary key to be entered
371
+ (!obj.new_record? || [:string, :text].exclude?(col.type))
343
372
  out << val.to_s
344
373
  else
374
+ has_updatable_fields = true
345
375
  out << f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair)
346
376
  end
347
377
  out << "
348
378
  </td>
349
379
  </tr>"
350
380
  end
351
- if has_fields
381
+ if has_updatable_fields
352
382
  out << "<tr><td colspan=\"2\">#{f.submit({ class: 'update' })}</td></tr>"
353
- else
383
+ elsif !has_fields
354
384
  out << '<tr><td colspan="2">(No displayable fields)</td></tr>'
355
385
  end
356
386
  out << '</table>'
357
387
  if model.name == 'ActiveStorage::Attachment'
358
388
  begin
359
- out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)&.html_safe
389
+ out << ::Brick::Rails.display_binary(obj&.blob&.download, 500_000)
360
390
  rescue
361
391
  end
362
392
  end
363
- out.html_safe
364
- end
365
- end # brick_form_for
393
+ output_buffer << out.html_safe
394
+ end # form_for
395
+ end # brick_form_with
366
396
 
367
397
  # ------------------------------------------
368
398
  # Our cool N:M checkbox constellation editor
@@ -413,7 +443,7 @@ module Brick::Rails::FormTags
413
443
  end
414
444
  out << " </tbody>
415
445
  </table>
416
- <script<%= @_request.env['_brick_nonce'] %>>
446
+ <script#{@_request.env['_brick_nonce']}>
417
447
  var constellation = document.getElementById(\"#{table_name}\");
418
448
  var nextSib,
419
449
  _this;
@@ -480,7 +510,6 @@ module Brick::Rails::FormTags
480
510
  out << " </tbody>
481
511
  </table>
482
512
  "
483
-
484
513
  out.html_safe
485
514
  end # brick_bezier
486
515
 
@@ -537,7 +566,7 @@ module Brick::Rails::FormTags
537
566
  </div>
538
567
  </div>
539
568
  "
540
- out
569
+ out.html_safe
541
570
  end # brick_header
542
571
 
543
572
  # All the standard CSS with teal colouration for use with Brick
@@ -547,6 +576,8 @@ module Brick::Rails::FormTags
547
576
 
548
577
  # -----------------------------------------------------------------------------------------------
549
578
  def set_grid_javascript(klass, pk, show_new_button = nil, row_count = nil, total_row_count = nil)
579
+ camel_name = klass.name.split('::').last
580
+ camel_name[0].downcase!
550
581
  table_name = klass.table_name.split('.').last
551
582
 
552
583
  # Javascript for brick_grid and brick_constellation
@@ -556,13 +587,13 @@ module Brick::Rails::FormTags
556
587
  // Plunk the row count in now that we know it
557
588
  var rowCount = document.getElementById(\"rowCount\");
558
589
  if (rowCount) rowCount.innerHTML = \"#{pluralize(row_count, "row")}#{total_row_count} &nbsp;\";
559
- var #{table_name}HtColumns;
590
+ var #{camel_name}HtColumns;
560
591
  " unless row_count.nil?
561
592
 
562
593
  grid_scripts << "
563
594
  // Snag first TR for sticky header
564
595
  var grid = document.getElementById(\"#{table_name}\");
565
- #{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
596
+ #{camel_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
566
597
  var headerTop = document.getElementById(\"headerTop\");
567
598
  var headerCols;
568
599
  if (grid) {
@@ -615,8 +646,8 @@ function setHeaderSizes() {
615
646
 
616
647
  // Set up proper sizings of sticky column header
617
648
  var node;
618
- for (var j = 0; j < #{table_name}HtColumns.length; ++j) {
619
- var row = #{table_name}HtColumns[j];
649
+ for (var j = 0; j < #{camel_name}HtColumns.length; ++j) {
650
+ var row = #{camel_name}HtColumns[j];
620
651
  var tr = isEmpty ? document.createElement(\"TR\") : headerTop.childNodes[j];
621
652
  tr.innerHTML = row.innerHTML.trim();
622
653
  var curLeft = 0.0;
data/lib/brick/rails.rb CHANGED
@@ -6,7 +6,7 @@ require 'brick/rails/engine'
6
6
  module ::Brick::Rails
7
7
  class << self
8
8
  # Low-level way to render read-only data for a field based on its data type.
9
- # Used by both brick_grid and brick_form_for (which gets down to this low-level
9
+ # Used by both brick_grid and brick_form_with (which gets down to this low-level
10
10
  # implementation from brick_field).
11
11
  def display_value(col_type, val, lat_lng = nil)
12
12
  is_mssql_geography = nil
@@ -143,7 +143,7 @@ module Brick
143
143
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema)
144
144
  else
145
145
  puts "*** In the brick.rb initializer the line \"::Brick.schema_behavior = ...\" refers to schema(s) called #{possible_schemas.map { |s| "\"#{s}\"" }.join(', ')}. No mentioned schema exists. ***"
146
- if ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
146
+ if Object.const_defined?('Apartment') && ::Brick.db_schemas.key?(::Brick.apartment_default_tenant)
147
147
  ::Brick.default_schema = schema = ::Brick.apartment_default_tenant
148
148
  end
149
149
  end
@@ -214,6 +214,7 @@ module Brick
214
214
  cols[col_name] = [r['data_type'], r['max_length'], measures&.include?(col_name), r['is_nullable'] == 'NO']
215
215
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
216
216
  relation[:col_descrips][col_name] = r['column_description'] if r['column_description']
217
+ relation[:col_order] << col_name
217
218
  end
218
219
  else # MySQL2, OracleEnhanced, and MSSQL act a little differently, bringing back an array for each row
219
220
  schema_and_tables = case ActiveRecord::Base.connection.adapter_name
@@ -269,6 +270,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
269
270
  # 'data_type', 'max_length', measure, 'is_nullable'
270
271
  cols[col_name] = [r[4], r[5], measures&.include?(col_name), r[8] == 'NO']
271
272
  # puts "KEY! #{r['relation_name']}.#{col_name} #{r['key']} #{r['const']}" if r['key']
273
+ relation[:col_order] << col_name
272
274
  end
273
275
  end
274
276
 
@@ -443,14 +445,18 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
443
445
  loop do
444
446
  klass = Object
445
447
  proposed_name_parts.each do |part|
446
- if klass.const_defined?(part) && klass.name != part
447
- begin
448
+ begin
449
+ if klass.const_defined?(part) && klass.name != part
448
450
  klass = klass.const_get(part)
449
- rescue NoMethodError => e
451
+ else
450
452
  klass = nil
451
453
  break
452
454
  end
453
- else
455
+ rescue NoMethodError => e
456
+ klass = nil
457
+ break
458
+ rescue NameError => e
459
+ puts "WARNING: Unable to automatically generate a class for the table named #{part}.\n In the brick.rb initializer file you can add the line \"::Brick.table_name_prefixes = { '#{part}' => '::YourModelName' }\" in order to specify a usable class name for this object."
454
460
  klass = nil
455
461
  break
456
462
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 246
8
+ TINY = 247
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -162,7 +162,7 @@ module Brick
162
162
  (existing = @relations.find { |_k, v| v.fetch(:db_name, nil) == db_name })
163
163
  existing.last
164
164
  else
165
- Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = {} } }.tap do |h|
165
+ Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = (k == :col_order ? [] : {}) } }.tap do |h|
166
166
  h[:db_name] = db_name if db_name
167
167
  end
168
168
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  class << self
6
6
  include FancyGets
7
7
 
8
- def pick_tables(usage = :migrations)
8
+ def pick_tables(is_airtable_id, usage = :migrations)
9
9
  puts "In order to reference Airtable data you will need a Personal Access Token (PAT) which can be generated by referencing this URL:
10
10
  https://airtable.com/create/tokens
11
11
  You need only #{usage == :migrations ? 'this scope:' : "these three scopes:
@@ -19,6 +19,10 @@ Please provide your Airtable PAT:"
19
19
  require 'net/http'
20
20
  # Generate a list of bases that can be chosen
21
21
  bases = https_get('https://api.airtable.com/v0/meta/bases', pat)
22
+ if (error = bases.fetch('error', nil))
23
+ puts error.fetch('message', nil) || 'Error'
24
+ return
25
+ end
22
26
  base = gets_list(bases.fetch('bases', nil)&.map { |z| AirtableTable.new(z['id'], z['name']) })
23
27
  puts
24
28
  # Generate a list of tables that can be chosen
@@ -35,10 +39,11 @@ Please provide your Airtable PAT:"
35
39
  # Build out a '::Brick.relations' hash that represents this Airtable schema
36
40
  fks = []
37
41
  associatives = {}
38
- relations = chosen.each_with_object({}) do |table, s|
42
+ relations = chosen.each_with_object({base_name: base.name}) do |table, s|
39
43
  tbl_name = sane_table_name(table.name)
40
44
  # Build out columns and foreign keys
41
45
  cols = {}
46
+ col_order = []
42
47
  table.fields.each do |col|
43
48
  col_name = sane_name(col['name'])
44
49
  # This is like a has_many or has_many through
@@ -48,7 +53,8 @@ Please provide your Airtable PAT:"
48
53
  chosen.find { |t| t.id == col['options']['linkedTableId'] }&.name
49
54
  ))
50
55
  if col['options']['prefersSingleRecordLink'] # 1:M
51
- fks << [frn_tbl, "#{col_name}_id", tbl_name, col_name]
56
+ fks << [frn_tbl, (fk_col_name = "#{col_name}_id"), tbl_name, col_name]
57
+ col_order << fk_col_name
52
58
  else # N:M
53
59
  # Queue up to build associative table with two foreign keys
54
60
  camelized = (assoc_name = "#{tbl_name}_#{col_name}_#{frn_tbl}").camelize
@@ -64,17 +70,17 @@ Please provide your Airtable PAT:"
64
70
  else
65
71
  # puts col['type']
66
72
  dt = case col['type']
67
- when 'singleLineText', 'url', 'email', 'singleSelect'
73
+ when 'singleLineText', 'url', 'email', 'singleSelect', 'phoneNumber'
68
74
  'string'
69
75
  when 'multilineText'
70
76
  'text'
71
77
  when 'number', 'currency'
72
- 'decimal'
78
+ col['options']['precision'] == 0 ? 'integer' : 'decimal'
73
79
  when 'checkbox'
74
80
  'boolean'
75
81
  when 'date'
76
82
  'date'
77
- when 'multipleSelects'
83
+ when 'multipleSelects', 'singleCollaborator'
78
84
  # Sqlite3 can do json
79
85
  'json'
80
86
  when 'formula', 'count', 'rollup', 'multipleAttachments'
@@ -83,13 +89,21 @@ Please provide your Airtable PAT:"
83
89
  # binding.pry
84
90
  end
85
91
  cols[col_name] = [dt, nil, true, false] # true is the col[:nillable]
92
+ col_order << col_name
86
93
  end
87
94
  end
88
95
  # Put it all into a relation entry, named the same as the table
89
- pkey = table.fields.find { |f| f['id'] == table.primary_key }['name']
96
+ pkey = if is_airtable_id
97
+ cols['airtable_id'] = ['string', nil, true, false]
98
+ 'airtable_id'
99
+ else
100
+ table.fields.find { |f| f['id'] == table.primary_key }['name']
101
+ end
102
+
90
103
  s[tbl_name] = {
91
104
  pkey: { "#{tbl_name}_pkey" => [sane_name(pkey)] },
92
105
  cols: cols,
106
+ col_order: col_order,
93
107
  fks: {},
94
108
  airtable_table: table
95
109
  }
@@ -15,9 +15,10 @@ module Brick
15
15
  def airtable_migrations
16
16
  mig_path, is_insert_versions, is_delete_versions = ::Brick::MigrationsBuilder.check_folder
17
17
  return unless mig_path &&
18
- (relations = ::Brick::AirtableApiCaller.pick_tables)
18
+ (relations = ::Brick::AirtableApiCaller.pick_tables(!ENV['OMIT_AIRTABLE_ID']))
19
19
 
20
- ::Brick::MigrationsBuilder.generate_migrations(relations.keys, mig_path, is_insert_versions, is_delete_versions, relations,
20
+ chosen = relations.keys.reject { |k| k.is_a?(Symbol) }
21
+ ::Brick::MigrationsBuilder.generate_migrations(chosen, mig_path, is_insert_versions, is_delete_versions, relations,
21
22
  do_fks_last: 'Separate', do_schema_migrations: false)
22
23
  end
23
24
  end
@@ -11,7 +11,7 @@ module Brick
11
11
  desc 'Auto-generates a seeds file from existing data in an Airtable "base".'
12
12
 
13
13
  def airtable_seeds
14
- return unless (relations = ::Brick::AirtableApiCaller.pick_tables(:seeds))
14
+ return unless (relations = ::Brick::AirtableApiCaller.pick_tables(!ENV['OMIT_AIRTABLE_ID'], :seeds))
15
15
 
16
16
  ::Brick::SeedsBuilder.generate_seeds(relations)
17
17
  end
@@ -43,7 +43,7 @@ module Brick
43
43
  relation.last[:cols].each do |col, type|
44
44
  col_down = col.downcase
45
45
 
46
- if (is_possible_poly = ['character varying', 'text'].include?(type.first))
46
+ if (is_possible_poly = ['character varying', 'varchar', 'text', 'VARCHAR2'].include?(type.first))
47
47
  if col_down.end_with?('_type')
48
48
  poly_type_cut_length = -6
49
49
  col_down = col_down[0..-6]
@@ -119,7 +119,8 @@ module Brick
119
119
  stuck = {}
120
120
  indexes = {} # Track index names to make sure things are unique
121
121
  built_schemas = {} # Track all built schemas so we can place an appropriate drop_schema command only in the first
122
- # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
122
+ # migration in which that schema is referenced, thereby allowing rollbacks to function properly.
123
+ is_airtable = nil
123
124
  versions_to_create = [] # Resulting versions to be used when updating the schema_migrations table
124
125
  # Start by making migrations for fringe tables (those with no foreign keys).
125
126
  # Continue layer by layer, creating migrations for tables that reference ones already done, until
@@ -148,8 +149,9 @@ module Brick
148
149
  mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
149
150
  key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
150
151
  after_fks.concat(add_fks) if do_fks_last
151
- current_mig_time[0] += 1.minute
152
- versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relations[tbl])}", current_mig_time, ar_version, mig)
152
+ increment_time(mig_path, current_mig_time)
153
+ is_airtable ||= (relation = relations[tbl])&.fetch(:airtable_table, nil)
154
+ versions_to_create << migration_file_write(mig_path, "create_#{::Brick._brick_index(tbl, nil, separator, relation, true)}", current_mig_time, ar_version, mig)
153
155
  end
154
156
  done.concat(fringe)
155
157
  chosen -= done
@@ -161,15 +163,15 @@ module Brick
161
163
  mig = gen_migration_columns(relations, tbl, (tbl_parts = tbl.split('.')), (add_fks = []), built_schemas, mig_path, current_mig_time,
162
164
  key_type, is_4x_rails, ar_version, do_fks_last, versions_to_create)
163
165
  after_fks.concat(add_fks)
164
- current_mig_time[0] += 1.minute
166
+ increment_time(mig_path, current_mig_time)
165
167
  versions_to_create << migration_file_write(mig_path, "create_#{
166
- ::Brick._brick_index(tbl, :migration, separator, relations[tbl])
168
+ ::Brick._brick_index(tbl, :migration, separator, relations[tbl], true)
167
169
  }", current_mig_time, ar_version, mig)
168
170
  end
169
171
  done.concat(chosen)
170
172
  chosen.clear
171
173
 
172
- case do_fks_last
174
+ case after_fks.present? && do_fks_last
173
175
  when 'Separate' # Add a final migration to create all the foreign keys
174
176
  mig = +" def change\n"
175
177
  after_fks.each do |add_fk|
@@ -201,9 +203,10 @@ module Brick
201
203
  'rb'
202
204
  end
203
205
  mig << +" end\n"
204
- current_mig_time[0] += 1.minute
205
- versions_to_create << migration_file_write(mig_path, "create_brick_fks.#{fks_extension}", current_mig_time, ar_version, mig)
206
- puts "Have written out a final migration called 'create_brick_fks.#{fks_extension}' which creates #{after_fks.length} foreign keys."
206
+ increment_time(mig_path, current_mig_time)
207
+ base_name = "_#{relations[:base_name]&.tr(' ', '')&.underscore}" if relations[:base_name]
208
+ versions_to_create << migration_file_write(mig_path, "create_brick_fks#{base_name}.#{fks_extension}", current_mig_time, ar_version, mig)
209
+ puts "Have written out a final migration called 'create_brick_fks#{base_name}.#{fks_extension}' which creates #{after_fks.length} foreign keys."
207
210
  if fks_extension == 'rbx'
208
211
  puts " This file extension (.rbx) will cause it not to run yet when you do a 'rails db:migrate'.
209
212
  The idea here is to do all data loading first, and then rename that migration file back
@@ -247,7 +250,7 @@ module Brick
247
250
  ". Here's the top 5 blockers" if stuck_sorted.length > 5
248
251
  }:"
249
252
  pp stuck_sorted[0..4]
250
- elsif do_schema_migrations # Successful, and now we can update the schema_migrations table accordingly
253
+ elsif do_schema_migrations && !is_airtable # Successful, and now we can update the schema_migrations table accordingly
251
254
  smtn = ::Brick._schema_migrations_table_name
252
255
  if ActiveRecord::Migration.table_exists?(smtn)
253
256
  # Remove to_delete - to_create
@@ -298,7 +301,7 @@ module Brick
298
301
  end
299
302
  unless schema.blank? || built_schemas.key?(schema)
300
303
  mig = +" def change\n create_schema(:#{schema}) unless schema_exists?(:#{schema})\n end\n"
301
- current_mig_time[0] += 1.minute
304
+ increment_time(mig_path, current_mig_time)
302
305
  versions_to_create << migration_file_write(mig_path, "create_db_schema_#{schema.underscore}", current_mig_time, ar_version, mig)
303
306
  built_schemas[schema] = nil
304
307
  end
@@ -352,7 +355,8 @@ module Brick
352
355
  mig = +" def change\n return unless reverting? || !table_exists?(#{tbl_code})\n\n"
353
356
  mig << " create_table #{tbl_code}#{id_option} do |t|\n"
354
357
  possible_ts = [] # Track possible generic timestamps
355
- relation[:cols].each do |col, col_type|
358
+ (relation.key?(:col_order) && relation[:col_order].present? && relation[:col_order].map { |col_name| [col_name, relation[:cols][col_name]] } ||
359
+ relation[:cols]).each do |col, col_type|
356
360
  sql_type = SQL_TYPES[col_type.first] || SQL_TYPES[col_type[0..1]] ||
357
361
  SQL_TYPES.find { |r| r.first.is_a?(Regexp) && col_type.first =~ r.first }&.last ||
358
362
  col_type.first
@@ -449,7 +453,16 @@ module Brick
449
453
  " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}#{suffix}\n"
450
454
  end
451
455
 
456
+ def increment_time(mig_path, current_mig_time)
457
+ loop do
458
+ current_mig_time[0] += 1.minute
459
+ break if Dir.glob("#{mig_path}/#{current_mig_time.first.strftime('%Y%m%d%H%M00')}_*.*").empty?
460
+
461
+ end
462
+ end
463
+
452
464
  def migration_file_write(mig_path, name, current_mig_time, ar_version, mig)
465
+ name.gsub!(/[^a-zA-Z0-9_\.\p{L}\p{N}]/, 'x') # Strip out any weird unicode characters
453
466
  File.open("#{mig_path}/#{version = current_mig_time.first.strftime('%Y%m%d%H%M00')}_#{name}#{'.rb' unless name.index('.')}", "w") do |f|
454
467
  f.write "class #{name.split('.').first.camelize} < ActiveRecord::Migration#{ar_version}\n"
455
468
  f.write mig
@@ -13,9 +13,10 @@ module Brick
13
13
  end
14
14
 
15
15
  def generate_seeds(relations = nil)
16
+ action = 'Overwrite'
16
17
  if File.exist?((seed_file_path = "#{::Rails.root}/db/seed") + 's.rb')
17
- puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\nOverwrite?"
18
- return unless gets_list(list: ['No', 'Yes']) == 'Yes'
18
+ puts "WARNING: seeds file #{seed_file_path}s.rb appears to already be present.\nAppend or Overwrite?"
19
+ return if (action = gets_list(list: ['Cancel', 'Append', 'Overwrite'])) == 'Cancel'
19
20
 
20
21
  puts "\n"
21
22
  end
@@ -24,7 +25,7 @@ module Brick
24
25
  is_airtable = true # So far the only thing that feeds us relations is Airtable
25
26
  require 'generators/brick/airtable_api_caller'
26
27
  # include ::Brick::MigrationsBuilder
27
- chosen = relations.map { |k, v| SeedModel.new(k, nil, false, v[:airtable_table]) }
28
+ chosen = relations.each_with_object([]) { |v, s| s << SeedModel.new(v.first, nil, false, v.last[:airtable_table]) unless v.first.is_a?(Symbol) }
28
29
  else
29
30
  ::Brick.mode = :on
30
31
  ActiveRecord::Base.establish_connection
@@ -57,11 +58,14 @@ module Brick
57
58
  end
58
59
  end
59
60
  batch_num = 1
60
- seeds_file = File.open("#{seed_file_path}s.rb", 'w')
61
+ seeds_file = File.open("#{seed_file_path}s.rb", action == 'Overwrite' ? 'w' : 'a+')
62
+ seeds_file.write("\n") if action == 'Append'
61
63
  seeds_len = lines_len = 0
62
64
  seeds_file.write('# Seeds file for ')
63
65
  seed_rows = nil
64
- if (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
66
+ if is_airtable
67
+ seeds_file.write("Airtable Base \"#{relations[:base_name]}\":\n")
68
+ elsif (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
65
69
  seeds_file.write("#{arbc.current_database}:\n")
66
70
  elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
67
71
  seeds_file.write("#{filename}:\n")
@@ -146,19 +150,38 @@ module Brick
146
150
 
147
151
  has_rows = false
148
152
  is_empty = true
149
- klass_name = is_airtable ? ::Brick::AirtableApiCaller.sane_table_name(relation[:airtable_table]&.name)&.singularize&.camelize : klass.name
150
- # Pull the records
151
- collection = if is_airtable
152
- if (airtable_table = relation[:airtable_table])
153
- ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}").fetch('records', nil)
154
- end
155
- else
156
- # Must convert to a symbol when doing ORDER BY due to this ActiveRecord nuance:
157
- # https://github.com/rails/rails/issues/2601
158
- klass.order(*pkey_cols.map(&:to_sym)) unless relation[:pkey].empty?
159
- end
153
+ collection = nil
154
+ klass_name = if is_airtable
155
+ airtable_table = relation[:airtable_table]
156
+ ::Brick.config.table_name_prefixes&.fetch(tbl, nil) ||
157
+ tbl.singularize.camelize
158
+ else
159
+ # Must convert to a symbol when doing ORDER BY due to this ActiveRecord nuance:
160
+ # https://github.com/rails/rails/issues/2601
161
+ collection = klass.order(*pkey_cols.map(&:to_sym)).enum_for unless relation[:pkey].empty?
162
+ klass.name
163
+ end
160
164
  seed_rows = +''
161
- collection&.each do |obj|
165
+ airtable_idx = offset = nil
166
+ while collection || airtable_table do
167
+ obj = if is_airtable
168
+ if collection.nil? || (airtable_idx += 1) >= collection.length
169
+ break if collection && !offset
170
+
171
+ offset_param = "?offset=#{offset}" if offset
172
+ result = ::Brick::AirtableApiCaller.https_get("https://api.airtable.com/v0/#{airtable_table.base_id}/#{airtable_table.id}#{offset_param}")
173
+ offset = result.fetch('offset', nil)
174
+ collection = result.fetch('records', nil)
175
+ airtable_idx = 0
176
+ end
177
+ collection[airtable_idx]
178
+ else
179
+ begin
180
+ collection.next
181
+ rescue StopIteration
182
+ break
183
+ end
184
+ end
162
185
  if is_airtable
163
186
  fields = obj['fields'].each_with_object({}) do |field, s|
164
187
  if relation[:cols].keys.include?(col_name = ::Brick::AirtableApiCaller.sane_name(field.first))
@@ -178,7 +201,7 @@ module Brick
178
201
  end
179
202
  end
180
203
  end
181
- objects = relation[:airtable_table].objects
204
+ objects = airtable_table.objects
182
205
  obj = objects[airtable_id = obj['id']] = AirtableObject.new(seed_model, obj['fields'], obj['createdTime'])
183
206
  end
184
207
  unless has_rows
@@ -204,7 +227,12 @@ module Brick
204
227
  # Used to be: obj.send(col)
205
228
  # (and with that it was possible to raise ActiveRecord::Encryption::Errors::Configuration...)
206
229
  # %%% should test further and see if that is possible with this code!)
207
- if (val = obj.attributes_before_type_cast[col])
230
+ val = if is_airtable && col == 'airtable_id'
231
+ pk_val
232
+ else
233
+ obj.attributes_before_type_cast[col]
234
+ end
235
+ if val
208
236
  if (val.is_a?(Time) || val.is_a?(Date))
209
237
  val = val.to_s
210
238
  elsif val.is_a?(Numeric)
@@ -283,28 +311,42 @@ end\n"
283
311
  end
284
312
  seed_rows = +''
285
313
  end
286
- unless has_rows || klass_name.nil?
314
+ unless has_rows || (is_airtable && !airtable_table)
287
315
  seeds_file.write(" # (Skipping #{klass_name} as it has no rows)\n")
288
316
  end
289
317
  end
290
318
  done.concat(fringe)
291
319
  chosen -= done
292
320
  end
293
- airtable_assocs_done = {}
294
- airtable_assoc_recids.each do |table_name, assoc_pairs| # N:M links
295
- # Make note of any other tables which have exactly duplicated data
296
- dupes = airtable_assocs_done.select do |dupe_table, dupe_pairs|
297
- dupe_table != table_name && assoc_pairs.length > 0 && assoc_pairs.length == dupe_pairs.length &&
298
- dupe_pairs.all? { |pair_name, _v3| assoc_pairs.keys.include?(pair_name) }
321
+
322
+ if airtable_assoc_recids.present?
323
+ seed_rows << "\n# Seeding #{airtable_assoc_recids.count} sets of Airtable associations:\n"
324
+ airtable_assocs_done = {}
325
+ dupe_list = []
326
+ airtable_assoc_recids.each do |table_name, assoc_pairs| # N:M links
327
+ # Make note of any other tables which have exactly duplicated data
328
+ dupes = airtable_assocs_done.select do |dupe_table, dupe_pairs|
329
+ dupe_table != table_name && assoc_pairs.length > 0 && assoc_pairs.length == dupe_pairs.length &&
330
+ dupe_pairs.all? { |pair_name, _v3| assoc_pairs.keys.include?(pair_name) }
331
+ end
332
+ seed_rows << " if ActiveRecord::Migration.table_exists?('#{table_name}')\n"
333
+ unless dupes.empty?
334
+ seed_rows << " # Duplicate data found in: #{dupes.keys.join(', ')}\n"
335
+ dupes.keys.each { |k| dupe_list << "#{table_name} | #{k}" }
336
+ end
337
+ seed_rows << " puts 'Airtable associations for #{table_name.camelize.singularize}'\n"
338
+ assoc_pairs.each do |_k2, pair_values|
339
+ seed_rows << "#{table_name.singularize.camelize}.create(#{pair_values})\n"
340
+ end
341
+ seed_rows << " end\n"
342
+ airtable_assocs_done[table_name] = assoc_pairs
299
343
  end
300
- seed_rows << " if ActiveRecord::Migration.table_exists?('#{table_name}')\n"
301
- seed_rows << " # Duplicate data found in: #{dupes.keys.join(', ')}\n" unless dupes.empty?
302
- seed_rows << " puts 'Seeding Airtable associations for #{table_name.camelize.singularize}'\n"
303
- assoc_pairs.each do |_k2, pair_values|
304
- seed_rows << "#{table_name.singularize.camelize}.create(#{pair_values})\n"
344
+
345
+ if dupe_list.present?
346
+ puts "\nAssociative tables with duplicate data:"
347
+ dupe_list.each { |entry| puts " #{entry}" }
348
+ puts '(You can safely remove one of the two tables in each of the pairs listed above.)'
305
349
  end
306
- seed_rows << " end\n"
307
- airtable_assocs_done[table_name] = assoc_pairs
308
350
  end
309
351
  seeds_file.write(seed_rows) unless seed_rows.blank?
310
352
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.246
4
+ version: 1.0.247
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits