brick 1.0.22 → 1.0.25

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: 82ebef3561b9cd7b8abcbbd8a30b7687485543ddf1a0900a9935dfb99299f13f
4
- data.tar.gz: ba071d53409fd8d4833693907364c745585aeb0bb18e885fcf7cbf1bd23b9940
3
+ metadata.gz: b0dfa64d7a8a148b4c2ad990f5af1604d5855b80c6e95f39cd371a43a591b7f1
4
+ data.tar.gz: bc7644a08678136d91696a68e0ad670f4f04d41772f36ba1bd806ebe45f23b04
5
5
  SHA512:
6
- metadata.gz: 69ad3ae158a8e478864db65dc808152d4b1b16bdc8779b1a651f59f199167daf00d2aa10018c5f65d5e619bbca4c09e99bc319ca736eed57023d61e6067ea365
7
- data.tar.gz: 18ca63e176e12ea50a5168e835a2eddfa66c8f522943d42e6d895d2f3e05d0cf6d396ab28d79f98898c91670b618cc27dbad5fa0bc9129a3edcfc3f74f5db029
6
+ metadata.gz: a39a8adc0c72288db5bd08e2483cefb701db9c2122fe1b4012c19bef07c7d048fed7f6e051473a9b4b6c80e321e4a781a16543d40f3e569f23c31f420a5117b6
7
+ data.tar.gz: 3ddcc914e3143a02a4582d6f525417c3012ae4eafc9ae889d5935223f6173afb4fb5ff3a40dc6c9caf95dfbf7a3c5be8e7bc4890efb385867ef5fa6f3ccbfb9e
data/lib/brick/config.rb CHANGED
@@ -97,6 +97,15 @@ module Brick
97
97
  @mutex.synchronize { @has_ones = hos }
98
98
  end
99
99
 
100
+ # Polymorphic associations
101
+ def polymorphics
102
+ @mutex.synchronize { @polymorphics }
103
+ end
104
+
105
+ def polymorphics=(polys)
106
+ @mutex.synchronize { @polymorphics = polys }
107
+ end
108
+
100
109
  def model_descrips
101
110
  @mutex.synchronize { @model_descrips ||= {} }
102
111
  end
@@ -26,18 +26,13 @@
26
26
 
27
27
  # colour coded origins
28
28
 
29
- # Drag something like TmfModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
29
+ # Drag something like HierModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
30
30
 
31
31
  # Support for Postgres / MySQL enums (add enum to model, use model enums to make a drop-down in the UI)
32
32
 
33
33
  # Currently quadrupling up routes
34
34
 
35
35
 
36
- # From the North app:
37
- # undefined method `built_in_role_path' when referencing show on a subclassed STI:
38
- # http://localhost:3000/roles/3?_brick_schema=cust1
39
-
40
-
41
36
  # ==========================================================
42
37
  # Dynamically create model or controller classes when needed
43
38
  # ==========================================================
@@ -57,6 +52,14 @@ module Arel
57
52
  end
58
53
  end
59
54
 
55
+ # module ActiveModel
56
+ # class NotNullValidator < EachValidator
57
+ # def validate_each(record, attribute, value)
58
+ # record.errors[attribute] << "must not be null" if value.nil?
59
+ # end
60
+ # end
61
+ # end
62
+
60
63
  module ActiveRecord
61
64
  class Base
62
65
  def self._assoc_names
@@ -79,8 +82,8 @@ module ActiveRecord
79
82
  dsl
80
83
  end
81
84
 
82
- # Pass in true or a JoinArray
83
- def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
85
+ # Pass in true for build_array, or just pass in a JoinArray
86
+ def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
84
87
  build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
85
88
  build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
86
89
  members = []
@@ -92,12 +95,17 @@ module ActiveRecord
92
95
  if bracket_name
93
96
  if ch == ']' # Time to process a bracketed thing?
94
97
  parts = bracket_name.split('.')
95
- first_parts = parts[0..-2].map { |part| klass = klass.reflect_on_association(part_sym = part.to_sym).klass; part_sym }
98
+ first_parts = parts[0..-2].map do |part|
99
+ klass = klass.reflect_on_association(part_sym = part.to_sym).klass
100
+ part_sym
101
+ end
96
102
  parts = prefix + first_parts + [parts[-1]]
97
103
  if parts.length > 1
98
- s = build_array
99
- parts[0..-3].each { |v| s = s[v.to_sym] }
100
- s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
104
+ unless is_polymorphic
105
+ s = build_array
106
+ parts[0..-3].each { |v| s = s[v.to_sym] }
107
+ s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
108
+ end
101
109
  translations[parts[0..-2].join('.')] = klass
102
110
  end
103
111
  members << parts
@@ -120,8 +128,8 @@ module ActiveRecord
120
128
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
121
129
  # Object property names can be referenced in square brackets like this:
122
130
  # { 'User' => '[profile.firstname] [profile.lastname]' }
123
- def brick_descrip
124
- self.class.brick_descrip(self)
131
+ def brick_descrip(data = nil, pk_alias = nil)
132
+ self.class.brick_descrip(self, data, pk_alias)
125
133
  end
126
134
 
127
135
  def self.brick_descrip(obj, data = nil, pk_alias = nil)
@@ -141,11 +149,7 @@ module ActiveRecord
141
149
  this_obj = obj
142
150
  bracket_name.split('.').each do |part|
143
151
  obj_name += ".#{part}"
144
- this_obj = if caches.key?(obj_name)
145
- caches[obj_name]
146
- else
147
- (caches[obj_name] = this_obj&.send(part.to_sym))
148
- end
152
+ this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
149
153
  end
150
154
  this_obj&.to_s || ''
151
155
  end
@@ -165,12 +169,17 @@ module ActiveRecord
165
169
  end
166
170
  if is_brackets_have_content
167
171
  output
168
- elsif pk_alias
169
- if (id = pk_alias.map { |pk_alias_part| obj.send(pk_alias_part) })
172
+ elsif (pk_alias ||= primary_key)
173
+ pk_alias = [pk_alias] unless pk_alias.is_a?(Array)
174
+ id = []
175
+ pk_alias.each do |pk_alias_part|
176
+ if (pk_part = obj.send(pk_alias_part))
177
+ id << pk_part
178
+ end
179
+ end
180
+ if id.present?
170
181
  "#{klass.name} ##{id.join(', ')}"
171
182
  end
172
- # elsif klass.primary_key
173
- # "#{klass.name} ##{obj.send(klass.primary_key)}"
174
183
  else
175
184
  obj.to_s
176
185
  end
@@ -184,10 +193,21 @@ module ActiveRecord
184
193
  model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
185
194
  end
186
195
 
196
+ def self.brick_import_template
197
+ template = constants.include?(:IMPORT_TEMPLATE) ? self::IMPORT_TEMPLATE : suggest_template(false, false, 0)
198
+ # Add the primary key to the template as being unique (unless it's already there)
199
+ template[:uniques] = [pk = primary_key.to_sym]
200
+ template[:all].unshift(pk) unless template[:all].include?(pk)
201
+ template
202
+ end
203
+
187
204
  private
188
205
 
189
206
  def self._brick_get_fks
190
- @_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.map(&:foreign_key)
207
+ @_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
208
+ s << bt.foreign_key
209
+ s << bt.foreign_type if bt.polymorphic?
210
+ end
191
211
  end
192
212
  end
193
213
 
@@ -289,7 +309,11 @@ module ActiveRecord
289
309
  bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
290
310
  bts.each do |_k, bt|
291
311
  # join_array will receive this relation name when calling #brick_parse_dsl
292
- bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
312
+ bt_descrip[bt.first] = if bt[1].is_a?(Array)
313
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
314
+ else
315
+ { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
316
+ end
293
317
  end
294
318
  skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
295
319
  hms.each do |k, hm|
@@ -307,7 +331,7 @@ module ActiveRecord
307
331
  when 2
308
332
  assoc_name = ks.first.to_sym
309
333
  # Make sure it's a good association name and that the model has that column name
310
- next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.any? { |col| col.name == ks.last }
334
+ next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
311
335
 
312
336
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
313
337
  end
@@ -323,23 +347,31 @@ module ActiveRecord
323
347
  id_for_tables = Hash.new { |h, k| h[k] = [] }
324
348
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
325
349
  bt_columns = bt_descrip.each_with_object([]) do |v, s|
326
- tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
327
- if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
328
- # Accommodate composite primary key by allowing id_col to come in as an array
329
- (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
330
- selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
331
- id_for_tables[v.first] << id_alias
332
- end
333
- v.last << id_for_tables[v.first]
334
- end
335
- if (col_name = v.last[1].last&.last)
350
+ v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
351
+ next if chains[k1].nil?
352
+
353
+ tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
354
+ # if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
336
355
  field_tbl_name = nil
337
- v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
356
+ v1.map { |x|
357
+ [translations[x[0..-2].map(&:to_s).join('.')], x.last]
358
+ }.each_with_index do |sel_col, idx|
338
359
  field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
339
- # col_name is weak when there are multiple, using sel_col.last instead
360
+
340
361
  selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
341
- v.last[1][idx] << col_alias
362
+ v1[idx] << col_alias
342
363
  end
364
+ # end
365
+
366
+ if (id_col = k1.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
367
+ # Accommodate composite primary key by allowing id_col to come in as an array
368
+ (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
369
+ selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
370
+ id_for_tables[v.first] << id_alias
371
+ end
372
+ v1 << id_for_tables[v.first]
373
+ end
374
+
343
375
  end
344
376
  end
345
377
  join_array.each do |assoc_name|
@@ -354,25 +386,30 @@ module ActiveRecord
354
386
  hm_counts.each do |k, hm|
355
387
  associative = nil
356
388
  count_column = if hm.options[:through]
357
- fk_col = (associative = associatives[hm.name]).foreign_key
358
- hm.foreign_key
359
- else
360
- fk_col = hm.foreign_key
361
- hm.klass.primary_key || '*'
362
- end
389
+ fk_col = (associative = associatives[hm.name]).foreign_key
390
+ hm.foreign_key
391
+ else
392
+ fk_col = hm.foreign_key
393
+ poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
394
+ hm.klass.primary_key || '*'
395
+ end
363
396
  tbl_alias = "_br_#{hm.name}"
364
397
  pri_tbl = hm.active_record
398
+ on_clause = []
365
399
  if fk_col.is_a?(Array) # Composite key?
366
- on_clause = []
367
400
  fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
368
- joins!("LEFT OUTER
369
- JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
370
- ON #{on_clause.join(' AND ')}")
401
+ selects = fk_col.dup
371
402
  else
372
- joins!("LEFT OUTER
373
- JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
374
- ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
403
+ selects = [fk_col]
404
+ on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
405
+ end
406
+ if poly_type
407
+ selects << poly_type
408
+ on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
375
409
  end
410
+ join_clause = "LEFT OUTER
411
+ JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
412
+ joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
376
413
  end
377
414
  where!(wheres) unless wheres.empty?
378
415
  wheres unless wheres.empty? # Return the specific parameters that we did use
@@ -469,7 +506,7 @@ class Object
469
506
  # path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
470
507
  # return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
471
508
  # If the file really exists, go and snag it:
472
- if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
509
+ if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
473
510
  filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
474
511
  end
475
512
  if is_found
@@ -582,112 +619,132 @@ class Object
582
619
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
583
620
  # The key in each hash entry (fk.first) is the constraint name
584
621
  inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
585
- options = {}
586
- singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
587
- macro = if assoc[:is_bt]
588
- # Try to take care of screwy names if this is a belongs_to going to an STI subclass
589
- assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
590
- sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
591
- a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
592
- end
593
- sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
594
- else
595
- assoc[:assoc_name]
596
- end
597
- need_class_name = singular_table_name.underscore != assoc_name
598
- need_fk = "#{assoc_name}_id" != assoc[:fk]
599
- if (inverse = assoc[:inverse])
600
- inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
601
- if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
602
- inverse_assoc_name = if has_ones[singular_inv_assoc_name]
603
- need_inverse_of = true
604
- has_ones[singular_inv_assoc_name]
605
- else
606
- singular_inv_assoc_name
607
- end
608
- end
609
- end
610
- :belongs_to
611
- else
612
- # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
613
- # Are there multiple foreign keys out to the same table?
614
- assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
615
- need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
616
- # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
617
- if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
618
- assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
619
- need_class_name = custom_assoc_name != singular_assoc_name
620
- custom_assoc_name
621
- else
622
- singular_assoc_name
623
- end
624
- :has_one
625
- else
626
- :has_many
627
- end
628
- end
629
- # Figure out if we need to specially call out the class_name and/or foreign key
630
- # (and if either of those then definitely also a specific inverse_of)
631
- options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
632
- # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
633
- if need_fk # Funky foreign key?
634
- options[:foreign_key] = if assoc[:fk].is_a?(Array)
635
- assoc_fk = assoc[:fk].uniq
636
- assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
637
- else
638
- assoc[:fk].to_sym
639
- end
640
- end
641
- options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
642
-
643
- # Prepare a list of entries for "has_many :through"
644
- if macro == :has_many
645
- relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
646
- next if k == assoc[:fk]
647
-
648
- hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
649
- end
622
+ if (invs = assoc[:inverse_table]).is_a?(Array)
623
+ invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
624
+ else
625
+ build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
650
626
  end
651
-
652
- # And finally create a has_one, has_many, or belongs_to for this association
653
- assoc_name = assoc_name.to_sym
654
- code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
655
- self.send(macro, assoc_name, **options)
656
627
  hmts
657
628
  end
658
629
  hmts.each do |hmt_fk, fks|
659
630
  fks.each do |fk|
660
- source = nil
661
- this_hmt_fk = if fks.length > 1
662
- singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
663
- source = fk.last
664
- through = fk.first[:alternate_name].pluralize
665
- "#{singular_assoc_name}_#{hmt_fk}"
666
- else
667
- source = fk.last unless hmt_fk.singularize == fk.last
668
- through = fk.first[:assoc_name].pluralize
669
- hmt_fk
670
- end
671
- code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
631
+ through = fk.first[:assoc_name]
632
+ hmt_name = if fks.length > 1
633
+ if fks[0].first[:inverse][:assoc_name] == fks[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
634
+ "#{hmt_fk}_through_#{fk.first[:assoc_name]}"
635
+ else # Use BT names to provide uniqueness
636
+ through = fk.first[:alternate_name].pluralize
637
+ singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
638
+ "#{singular_assoc_name}_#{hmt_fk}"
639
+ end
640
+ else
641
+ hmt_fk
642
+ end
643
+ source = fk.last unless hmt_name.singularize == fk.last
644
+ code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
672
645
  options = { through: assoc_name }
673
646
  options[:source] = source.to_sym if source
674
- self.send(:has_many, this_hmt_fk.to_sym, **options)
675
- end
676
- end
677
- # Not NULLables
678
- relation[:cols].each do |col, datatype|
679
- if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
680
- ::Brick.config.not_nullables.include?("#{matching}.#{col}")
681
- code << " validates :#{col}, presence: true\n"
682
- self.send(:validates, col.to_sym, { presence: true })
647
+ self.send(:has_many, hmt_name.to_sym, **options)
683
648
  end
684
649
  end
650
+ # # Not NULLables
651
+ # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
652
+ # relation[:cols].each do |col, datatype|
653
+ # if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
654
+ # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
655
+ # code << " validates :#{col}, not_null: true\n"
656
+ # self.send(:validates, col.to_sym, { not_null: true })
657
+ # end
658
+ # end
685
659
  end
686
660
  code << "end # model #{model_name}\n\n"
687
661
  end # class definition
688
662
  [built_model, code]
689
663
  end
690
664
 
665
+ def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
666
+ singular_table_name = inverse_table&.singularize
667
+ options = {}
668
+ macro = if assoc[:is_bt]
669
+ # Try to take care of screwy names if this is a belongs_to going to an STI subclass
670
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
671
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
672
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
673
+ end
674
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
675
+ else
676
+ assoc[:assoc_name]
677
+ end
678
+ if assoc.key?(:polymorphic)
679
+ options[:polymorphic] = true
680
+ else
681
+ need_class_name = singular_table_name.underscore != assoc_name
682
+ need_fk = "#{assoc_name}_id" != assoc[:fk]
683
+ end
684
+ if (inverse = assoc[:inverse])
685
+ inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
686
+ has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
687
+ if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
688
+ inverse_assoc_name = if has_ones[singular_inv_assoc_name]
689
+ need_inverse_of = true
690
+ has_ones[singular_inv_assoc_name]
691
+ else
692
+ singular_inv_assoc_name
693
+ end
694
+ end
695
+ end
696
+ :belongs_to
697
+ else
698
+ # need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
699
+ # Are there multiple foreign keys out to the same table?
700
+ assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
701
+ # binding.pry if assoc.key?(:polymorphic)
702
+ if assoc.key?(:polymorphic)
703
+ options[:as] = assoc[:fk].to_sym if assoc.key?(:polymorphic)
704
+ else
705
+ need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
706
+ end
707
+ # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
708
+ if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
709
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
710
+ need_class_name = custom_assoc_name != singular_assoc_name
711
+ custom_assoc_name
712
+ else
713
+ singular_assoc_name
714
+ end
715
+ :has_one
716
+ else
717
+ :has_many
718
+ end
719
+ end
720
+ # Figure out if we need to specially call out the class_name and/or foreign key
721
+ # (and if either of those then definitely also a specific inverse_of)
722
+ options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
723
+ # Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
724
+ if need_fk # Funky foreign key?
725
+ options[:foreign_key] = if assoc[:fk].is_a?(Array)
726
+ assoc_fk = assoc[:fk].uniq
727
+ assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
728
+ else
729
+ assoc[:fk].to_sym
730
+ end
731
+ end
732
+ options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
733
+
734
+ # Prepare a list of entries for "has_many :through"
735
+ if macro == :has_many
736
+ relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
737
+ next if k == assoc[:fk]
738
+
739
+ hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
740
+ end
741
+ end
742
+ # And finally create a has_one, has_many, or belongs_to for this association
743
+ assoc_name = assoc_name.to_sym
744
+ code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
745
+ self.send(macro, assoc_name, **options)
746
+ end
747
+
691
748
  def build_controller(class_name, plural_class_name, model, relations)
692
749
  table_name = ActiveSupport::Inflector.underscore(plural_class_name)
693
750
  singular_table_name = ActiveSupport::Inflector.singularize(table_name)
@@ -700,9 +757,22 @@ class Object
700
757
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
701
758
  code << " @#{table_name}.brick_select(params)\n"
702
759
  code << " end\n"
760
+ self.protect_from_forgery unless: -> { self.request.format.js? }
703
761
  self.define_method :index do
704
762
  ::Brick.set_db_schema(params)
705
- ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
763
+ if request.format == :csv # Asking for a template?
764
+ require 'csv'
765
+ exported_csv = CSV.generate(force_quotes: false) do |csv_out|
766
+ model.df_export(model.brick_import_template).each { |row| csv_out << row }
767
+ end
768
+ render inline: exported_csv, content_type: request.format
769
+ return
770
+ elsif request.format == :js # Asking for JSON?
771
+ render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
772
+ return
773
+ end
774
+
775
+ ar_relation = model.primary_key ? model.order("#{model.table_name}.#{model.primary_key}") : model.all
706
776
  @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
707
777
  # %%% Add custom HM count columns
708
778
  # %%% What happens when the PK is composite?
@@ -739,6 +809,22 @@ class Object
739
809
  code << " end\n"
740
810
  self.define_method :update do
741
811
  ::Brick.set_db_schema(params)
812
+
813
+ if request.format == :csv # Importing CSV?
814
+ require 'csv'
815
+ # See if internally it's likely a TSV file (tab-separated)
816
+ tab_counts = []
817
+ 5.times { tab_counts << request.body.readline.count("\t") unless request.body.eof? }
818
+ request.body.rewind
819
+ separator = "\t" if tab_counts.length > 0 && tab_counts.uniq.length == 1 && tab_counts.first > 0
820
+ result = model.df_import(CSV.parse(request.body, { col_sep: separator || :auto }), model.brick_import_template)
821
+ # render inline: exported_csv, content_type: request.format
822
+ return
823
+ # elsif request.format == :js # Asking for JSON?
824
+ # render inline: model.df_export(true).to_json, content_type: request.format
825
+ # return
826
+ end
827
+
742
828
  instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
743
829
  obj = obj.first if obj.is_a?(Array)
744
830
  obj.send(:update, send(params_name = params_name.to_sym))
@@ -971,14 +1057,19 @@ module Brick
971
1057
  # rubocop:enable Style/CommentedKeyword
972
1058
 
973
1059
  class << self
974
- def _add_bt_and_hm(fk, relations = nil)
975
- relations ||= ::Brick.relations
1060
+ def _add_bt_and_hm(fk, relations, is_polymorphic = false)
976
1061
  bt_assoc_name = fk[1].underscore
977
1062
  bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
978
1063
 
979
1064
  bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
980
- primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
981
- hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1065
+ # %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
1066
+ # Maybe it's already gotten this info because we got as far as to say there was a unique class
1067
+ # if is_polymorphic
1068
+ # primary_table = fk[]
1069
+ # else
1070
+ primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
1071
+ hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
1072
+ # end
982
1073
 
983
1074
  unless (cnstr_name = fk[3])
984
1075
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
@@ -993,7 +1084,7 @@ module Brick
993
1084
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
994
1085
  return
995
1086
  end
996
- unless (cols = relations[fk[0]][:cols]).key?(fk[1])
1087
+ unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
997
1088
  columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
998
1089
  puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
999
1090
  return
@@ -1008,10 +1099,17 @@ module Brick
1008
1099
  end
1009
1100
  end
1010
1101
  if (assoc_bt = bts[cnstr_name])
1011
- assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
1012
- assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1102
+ if is_polymorphic
1103
+ # Assuming same fk (don't yet support composite keys for polymorphics)
1104
+ assoc_bt[:inverse_table] << fk[2]
1105
+ else # Expect we've got a composite key going
1106
+ assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
1107
+ assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
1108
+ end
1013
1109
  else
1014
- assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: primary_table }
1110
+ inverse_table = [primary_table] if is_polymorphic
1111
+ assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
1112
+ assoc_bt[:polymorphic] = true if is_polymorphic
1015
1113
  end
1016
1114
  if is_class
1017
1115
  # For use in finding the proper :source for a HMT association that references an STI subclass
@@ -1028,6 +1126,7 @@ module Brick
1028
1126
  assoc_hm[:inverse] = assoc_bt
1029
1127
  else
1030
1128
  assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
1129
+ assoc_hm[:polymorphic] = true if is_polymorphic
1031
1130
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1032
1131
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1033
1132
  end
@@ -38,6 +38,9 @@ module Brick
38
38
 
39
39
  # Has one relationships
40
40
  ::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
41
+
42
+ # Polymorphic associations
43
+ ::Brick.polymorphics = app.config.brick.fetch(:polymorphics, nil)
41
44
  end
42
45
 
43
46
  # After we're initialized and before running the rest of stuff, put our configuration in place
@@ -64,12 +67,14 @@ module Brick
64
67
  is_template_exists
65
68
  end
66
69
 
67
- def path_keys(fk_name, obj_name, pk)
68
- if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
69
- fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
70
- else
71
- [[fk_name, "#{obj_name}.#{pk}"]]
72
- end.map { |x| "#{x.first}: #{x.last}"}.join(', ')
70
+ def path_keys(hm_assoc, fk_name, obj_name, pk)
71
+ keys = if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
72
+ fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
73
+ else
74
+ [[fk_name, "#{obj_name}.#{pk}"]]
75
+ end
76
+ keys << [hm_assoc.inverse_of.foreign_type, "#{hm_assoc.active_record.name}"] if hm_assoc.options.key?(:as)
77
+ keys.map { |x| "#{x.first}: #{x.last}"}.join(', ')
73
78
  end
74
79
 
75
80
  alias :_brick_find_template :find_template
@@ -80,6 +85,7 @@ module Brick
80
85
  pk = @_brick_model.primary_key
81
86
  obj_name = model_name.underscore
82
87
  table_name = model_name.pluralize.underscore
88
+ template_link = nil
83
89
  bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
84
90
  hms_columns = [] # Used for 'index'
85
91
  skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
@@ -96,15 +102,17 @@ module Brick
96
102
  set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
97
103
  'nil'
98
104
  else
99
- "#{obj_name}._br_#{assoc_name}_ct || 0"
105
+ # Postgres column names are limited to 63 characters
106
+ attrib_name = "_br_#{assoc_name}_ct"[0..62]
107
+ "#{obj_name}.#{attrib_name} || 0"
100
108
  end
101
109
  "<%= ct = #{set_ct}
102
- link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
110
+ link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
103
111
  else # has_one
104
112
  "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
105
113
  end
106
114
  elsif args.first == 'show'
107
- hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
115
+ hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
108
116
  end
109
117
  s << hm_stuff
110
118
  end
@@ -115,6 +123,13 @@ module Brick
115
123
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
116
124
  .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
117
125
  css = +"<style>
126
+ #dropper {
127
+ background-color: #eee;
128
+ }
129
+ #btnImport {
130
+ display: none;
131
+ }
132
+
118
133
  table {
119
134
  border-collapse: collapse;
120
135
  margin: 25px 0;
@@ -195,7 +210,16 @@ def hide_bcrypt(val)
195
210
  end %>"
196
211
 
197
212
  if ['index', 'show', 'update'].include?(args.first)
198
- css << "<% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
213
+ css << "<% bts = { #{
214
+ bts.each_with_object([]) do |v, s|
215
+ foreign_models = if v.last[1].is_a?(Array)
216
+ v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
217
+ else
218
+ "[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
219
+ end
220
+ s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}]]"
221
+ end.join(', ')
222
+ } } %>"
199
223
  end
200
224
 
201
225
  # %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
@@ -260,11 +284,102 @@ function changeout(href, param, value) {
260
284
  elsif pk
261
285
  "#{obj_name}.#{pk}"
262
286
  end
287
+ if Object.const_defined?('DutyFree')
288
+ template_link = "
289
+ <%= link_to 'CSV', #{table_name}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
290
+ <div id=\"dropper\" contenteditable=\"true\"></div>
291
+ <input type=\"button\" id=\"btnImport\" value=\"Import\">
292
+
293
+ <script>
294
+ var dropperDiv = document.getElementById(\"dropper\");
295
+ var btnImport = document.getElementById(\"btnImport\");
296
+ var droppedTSV;
297
+ if (dropperDiv) { // Other interesting events: blur keyup input
298
+ dropperDiv.addEventListener(\"paste\", function (evt) {
299
+ droppedTSV = evt.clipboardData.getData('text/plain');
300
+ var html = evt.clipboardData.getData('text/html');
301
+ var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
302
+ console.log(tbl);
303
+ btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
304
+ });
305
+ btnImport.addEventListener(\"click\", function () {
306
+ fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
307
+ method: 'PATCH',
308
+ headers: { 'Content-Type': 'text/tab-separated-values' },
309
+ body: droppedTSV
310
+ }).then(function (tsvResponse) {
311
+ btnImport.style.display = \"none\";
312
+ console.log(\"toaster\", tsvResponse);
313
+ });
314
+ });
315
+ }
316
+ var sheetUrl;
317
+ var spreadsheetId;
318
+ var sheetsLink = document.getElementById(\"sheetsLink\");
319
+ function gapiLoaded() {
320
+ // Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
321
+ sheetsLink.addEventListener(\"click\", async function (evt) {
322
+ evt.preventDefault();
323
+ await gapi.load(\"client\", function () {
324
+ gapi.client.init({ // Load the discovery doc to initialize the API
325
+ clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
326
+ scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
327
+ discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
328
+ }).then(function () {
329
+ gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
330
+ updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
331
+ });
332
+ });
333
+ });
334
+ }
335
+
336
+ async function updateSignInStatus(isSignedIn) {
337
+ if (isSignedIn) {
338
+ console.log(\"turds!\");
339
+ await gapi.client.sheets.spreadsheets.create({
340
+ properties: {
341
+ title: #{table_name.inspect},
342
+ },
343
+ sheets: [
344
+ // sheet1, sheet2, sheet3
345
+ ]
346
+ }).then(function (response) {
347
+ sheetUrl = response.result.spreadsheetUrl;
348
+ spreadsheetId = response.result.spreadsheetId;
349
+ sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
350
+ console.log(\"x1\", sheetUrl);
351
+
352
+ // Get JSON data
353
+ fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
354
+ response.json().then(function (data) {
355
+ gapi.client.sheets.spreadsheets.values.append({
356
+ spreadsheetId: spreadsheetId,
357
+ range: \"Sheet1\",
358
+ valueInputOption: \"RAW\",
359
+ insertDataOption: \"INSERT_ROWS\"
360
+ }, {
361
+ range: \"Sheet1\",
362
+ majorDimension: \"ROWS\",
363
+ values: data,
364
+ }).then(function (response2) {
365
+ // console.log(\"beefcake\", response2);
366
+ });
367
+ });
368
+ });
369
+ });
370
+ window.open(sheetUrl, '_blank');
371
+ }
372
+ }
373
+ </script>
374
+ <script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
375
+ "
376
+ end
263
377
  "#{css}
264
378
  <p style=\"color: green\"><%= notice %></p>#{"
265
379
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
266
380
  <select id=\"tbl\">#{table_options}</select>
267
- <h1>#{model_name.pluralize}</h1>
381
+ <h1>#{model_name.pluralize}</h1>#{template_link}
382
+
268
383
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
269
384
  <table id=\"#{table_name}\">
270
385
  <thead><tr>#{'<th></th>' if pk}
@@ -272,10 +387,13 @@ function changeout(href, param, value) {
272
387
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
273
388
  <th>
274
389
  <% if (bt = bts[col]) %>
275
- BT <%= bt[1].bt_link(bt.first) %>
276
- <% else %>
277
- <%= col %>
278
- <% end %>
390
+ BT <%
391
+ bt[1].each do |bt_pair| %><%=
392
+ bt_pair.first.bt_link(bt.first) %> <%
393
+ end %><%
394
+ else %><%=
395
+ col %><%
396
+ end %>
279
397
  </th>
280
398
  <% end %>
281
399
  <%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
@@ -285,23 +403,32 @@ function changeout(href, param, value) {
285
403
  <tbody>
286
404
  <% @#{table_name}.each do |#{obj_name}| %>
287
405
  <tr>#{"
288
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if pk}
406
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
289
407
  <% #{obj_name}.attributes.each do |k, val| %>
290
- <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
408
+ <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
291
409
  <td>
292
410
  <% if (bt = bts[k]) %>
293
- <%# binding.pry # Postgres column names are limited to 63 characters!!! %>
294
- <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last[0..62]) }, @_brick_bt_descrip[bt.first][2]) %>
295
- <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
296
- <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
297
- <%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
411
+ <%# binding.pry # Postgres column names are limited to 63 characters %>
412
+ <% if (pairs = bt[1].length > 1)
413
+ bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
414
+ # descrips = @_brick_bt_descrip[bt.first][bt_class]
415
+ poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
416
+ %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
417
+ send(\"#\{bt_class.underscore\}_path\".to_sym, poly_id)) if poly_id %><%
418
+ else # We should do something other than [0..-2] for when there is no primary key (or maybe have an empty final array there in that case?)
419
+ bt_txt = (bt_class = bt[1].first.first).brick_descrip(
420
+ #{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
421
+ )
422
+ bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
423
+ <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
424
+ <%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
425
+ <% end %>
298
426
  <% else %>
299
427
  <%= hide_bcrypt(val) %>
300
428
  <% end %>
301
429
  </td>
302
430
  <% end %>
303
- <td>#{hms_columns.join('</td><td>')}</td>
304
- <!-- td>X</td -->
431
+ #{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
305
432
  </tr>
306
433
  </tbody>
307
434
  <% end %>
@@ -318,33 +445,47 @@ function changeout(href, param, value) {
318
445
  <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
319
446
  <% if obj %>
320
447
  <%= # path_options = [obj.#{pk}]
321
- # path_options << { '_brick_schema': } if
322
- # url = send(:#{model_name.underscore}_path, obj.#{pk})
323
- form_for(obj.becomes(#{model_name})) do |f| %>
448
+ # path_options << { '_brick_schema': } if
449
+ # url = send(:#{model_name.underscore}_path, obj.#{pk})
450
+ form_for(obj.becomes(#{model_name})) do |f| %>
324
451
  <table>
325
- <% @#{obj_name}.first.attributes.each do |k, val| %>
452
+ <% has_fields = false
453
+ @#{obj_name}.first.attributes.each do |k, val| %>
326
454
  <tr>
455
+ <%# %%% Accommodate composite keys %>
327
456
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
328
457
  <th class=\"show-field\">
329
- <% if (bt = bts[k])
330
- # Add a final member in this array with descriptive options to be used in <select> drop-downs
331
- bt_name = bt[1].name
332
- # %%% Only do this if the user has permissions to edit this bt field
333
- if bt.length < 4
334
- bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
335
- bt[1].order(:#{pk}).each { |obj| option_detail << [obj.brick_descrip, obj.#{pk}] }
336
- end %>
337
- BT <%= bt[1].bt_link(bt.first) %>
458
+ <% has_fields = true
459
+ if (bt = bts[k])
460
+ # Add a final member in this array with descriptive options to be used in <select> drop-downs
461
+ bt_name = bt[1].map { |x| x.first.name }.join('/')
462
+ # %%% Only do this if the user has permissions to edit this bt field
463
+ if (pairs = bt[1]).length > 1
464
+ poly_class_name = @#{obj_name}.first.send(\"#\{bt.first\}_type\")
465
+ bt_pair = pairs.find { |pair| pair.first.name == poly_class_name }
466
+ # descrips = @_brick_bt_descrip[bt.first][bt_class]
467
+ poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
468
+ # bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
469
+ else # No polymorphism, so just get the first one
470
+ bt_pair = bt[1].first
471
+ end
472
+ bt_class = bt_pair.first
473
+ if bt.length < 3
474
+ bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
475
+ # %%% Accommodate composite keys for obj.pk at the end here
476
+ bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
477
+ end %>
478
+ BT <%= bt_class.bt_link(bt.first) %>
338
479
  <% else %>
339
480
  <%= k %>
340
481
  <% end %>
341
482
  </th>
342
483
  <td>
343
- <% if (bt = bts[k]) # bt_obj.brick_descrip
484
+ <% if bt
344
485
  html_options = { prompt: \"Select #\{bt_name\}\" }
345
486
  html_options[:class] = 'dimmed' unless val %>
346
- <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
347
- <%= bt_obj = bt[1].find_by(bt[2] => val); link_to('⇛', send(\"#\{bt_obj_path_base = bt_name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
487
+ <%= f.select k.to_sym, bt[2], { value: val || '^^^brick_NULL^^^' }, html_options %>
488
+ <%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
348
489
  <% else case #{model_name}.column_for_attribute(k).type
349
490
  when :string, :text %>
350
491
  <% if is_bcrypt?(val) # || .readonly? %>
@@ -365,8 +506,12 @@ function changeout(href, param, value) {
365
506
  <% end %>
366
507
  </td>
367
508
  </tr>
368
- <% end %>
509
+ <% end
510
+ if has_fields %>
369
511
  <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
512
+ <% else %>
513
+ <tr><td colspan=\"2\">(No displayable fields)</td></tr>
514
+ <% end %>
370
515
  </table>
371
516
  <% end %>
372
517
 
@@ -380,6 +525,7 @@ function changeout(href, param, value) {
380
525
  <tr><td>(none)</td></tr>
381
526
  <% else %>
382
527
  <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
528
+ <%# %%% accommodate composite primary key %>
383
529
  <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
384
530
  <% end %>
385
531
  <% end %>
@@ -392,6 +538,7 @@ function changeout(href, param, value) {
392
538
  #{script}"
393
539
 
394
540
  end
541
+ puts inline
395
542
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
396
543
  keys = options.has_key?(:locals) ? options[:locals].keys : []
397
544
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
@@ -408,7 +555,7 @@ function changeout(href, param, value) {
408
555
  end
409
556
 
410
557
  # Just in case it hadn't been done previously when we tried to load the brick initialiser,
411
- # go make sure we've loaded additional references (virtual foreign keys).
558
+ # go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
412
559
  ::Brick.load_additional_references
413
560
  end
414
561
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 22
8
+ TINY = 25
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
@@ -103,11 +103,17 @@ module Brick
103
103
 
104
104
  def get_bts_and_hms(model)
105
105
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
106
- next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
106
+ next if (!const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name))
107
107
 
108
108
  case a.macro
109
109
  when :belongs_to
110
- s.first[a.foreign_key] = [a.name, a.klass]
110
+ s.first[a.foreign_key] = if a.polymorphic?
111
+ primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
112
+ models = primary_tables&.map { |table| table.singularize.camelize.constantize }
113
+ [a.name, models]
114
+ else
115
+ [a.name, a.klass]
116
+ end
111
117
  when :has_many, :has_one # This gets has_many as well as has_many :through
112
118
  # %%% weed out ones that don't have an available model to reference
113
119
  s.last[a.name] = a
@@ -126,9 +132,7 @@ module Brick
126
132
  skip_hms[hmt.last.name] = nil
127
133
  end
128
134
  end
129
- skip_hms.each do |k, _v|
130
- puts hms.delete(k).inspect
131
- end
135
+ skip_hms.each { |k, _v| hms.delete(k) }
132
136
  [bts, hms, associatives]
133
137
  end
134
138
 
@@ -258,6 +262,11 @@ module Brick
258
262
  end
259
263
  end
260
264
 
265
+ # Polymorphic associations
266
+ def polymorphics=(polys)
267
+ Brick.config.polymorphics = polys || {}
268
+ end
269
+
261
270
  # DSL templates for individual models to provide prettier descriptions of objects
262
271
  # @api public
263
272
  def model_descrips=(descrips)
@@ -276,8 +285,18 @@ module Brick
276
285
  def load_additional_references
277
286
  return if @_additional_references_loaded
278
287
 
279
- if (ars = ::Brick.config.additional_references)
280
- ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2]) }
288
+ relations = ::Brick.relations
289
+ if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
290
+ ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
291
+ if (polys = ::Brick.config.polymorphics)
292
+ polys.each do |k, v|
293
+ table_name, poly = k.split('.')
294
+ v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
295
+ v.each do |type|
296
+ ::Brick._add_bt_and_hm([table_name, poly, type.underscore.pluralize, "(brick) #{table_name}_#{poly}"], relations, true)
297
+ end
298
+ end
299
+ end
281
300
  @_additional_references_loaded = true
282
301
  end
283
302
 
@@ -157,6 +157,10 @@ Brick.skip_index_hms = ['User.litany_of_woes']
157
157
  # Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
158
158
  # '::Snake' => 'Reptile' }
159
159
 
160
+ # # Polymorphic associations must be explicitly specified, which is as easy as providing a model name and polymorphic
161
+ # # association name like this:
162
+ # Brick.polymorphics = ['Comment.commentable', 'Image.imageable']
163
+
160
164
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
161
165
  # # route to go to the :index action for what would be a controller for that table. You can specify any controller
162
166
  # # name and action you wish in order to override this and have that be the default route when none other has been
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brick
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.22
4
+ version: 1.0.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-10 00:00:00.000000000 Z
11
+ date: 2022-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord