brick 1.0.21 → 1.0.24

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: 0ad289d9f4db9ef6ce9edcfb8948926d4a61910294291272ccf2b342fa2f5051
4
- data.tar.gz: 9030f9ccf39d7f0a9a0ff9413e4f24ee7a46beaf3aba056e9875fc5c29551419
3
+ metadata.gz: 96ba9e8b4ddc54e5feaaf8652c93e5d9e1ffe15345a8cad719674e051b296718
4
+ data.tar.gz: 3901467e7918ece559f55740759bd555bababe96c407488307fcb01298c14034
5
5
  SHA512:
6
- metadata.gz: ed6f08eabba91cb304141a0c84884cfbeaae0087094b1fe1ff4a80195aee44dbb697c385ab9d7afaac2e337f9008bc7bef059be98f95264fec34a2288beaff6b
7
- data.tar.gz: 5ebbb793d23634232a529b3d39224e1e4deaa010cf8bf2841effe20a9e3b4d7079fb7163e6c2f90feeed8526c4a1314f319bad47d1c8a6079bf54ca92f0e6224
6
+ metadata.gz: aa73995e69947be5ca83597a71751cfffb2433551d04fb00b760bc51c199bcd9890d7a05c0deb2f2c07c2c89205acf52b942d00ae78e1d7c0a26544acf70a4d0
7
+ data.tar.gz: 2328082844a0c993ada29c02eab22cf0d04efbcf3e4479c804e16f7d2a39a42d28debcd5592620fecd3bc9c404458fe3b5ff448f1f2c1b7f0d2e13557f5f9fa8
@@ -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
  # ==========================================================
@@ -120,8 +115,8 @@ module ActiveRecord
120
115
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
121
116
  # Object property names can be referenced in square brackets like this:
122
117
  # { 'User' => '[profile.firstname] [profile.lastname]' }
123
- def brick_descrip
124
- self.class.brick_descrip(self)
118
+ def brick_descrip(data = nil, pk_alias = nil)
119
+ self.class.brick_descrip(self, data, pk_alias)
125
120
  end
126
121
 
127
122
  def self.brick_descrip(obj, data = nil, pk_alias = nil)
@@ -141,11 +136,7 @@ module ActiveRecord
141
136
  this_obj = obj
142
137
  bracket_name.split('.').each do |part|
143
138
  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
139
+ this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
149
140
  end
150
141
  this_obj&.to_s || ''
151
142
  end
@@ -165,17 +156,38 @@ module ActiveRecord
165
156
  end
166
157
  if is_brackets_have_content
167
158
  output
168
- elsif pk_alias
169
- if (id = obj.send(pk_alias))
170
- "#{klass.name} ##{id}"
159
+ elsif (pk_alias ||= primary_key)
160
+ pk_alias = [pk_alias] unless pk_alias.is_a?(Array)
161
+ id = []
162
+ pk_alias.each do |pk_alias_part|
163
+ if (pk_part = obj.send(pk_alias_part))
164
+ id << pk_part
165
+ end
166
+ end
167
+ if id.present?
168
+ "#{klass.name} ##{id.join(', ')}"
171
169
  end
172
- # elsif klass.primary_key
173
- # "#{klass.name} ##{obj.send(klass.primary_key)}"
174
170
  else
175
171
  obj.to_s
176
172
  end
177
173
  end
178
174
 
175
+ def self.bt_link(assoc_name)
176
+ model_underscore = name.underscore
177
+ assoc_name = CGI.escapeHTML(assoc_name.to_s)
178
+ model_path = Rails.application.routes.url_helpers.send("#{model_underscore.pluralize}_path".to_sym)
179
+ link = Class.new.extend(ActionView::Helpers::UrlHelper).link_to(name, model_path)
180
+ model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
181
+ end
182
+
183
+ def self.brick_import_template
184
+ template = constants.include?(:IMPORT_TEMPLATE) ? self::IMPORT_TEMPLATE : suggest_template(false, false, 0)
185
+ # Add the primary key to the template as being unique (unless it's already there)
186
+ template[:uniques] = [pk = primary_key.to_sym]
187
+ template[:all].unshift(pk) unless template[:all].include?(pk)
188
+ template
189
+ end
190
+
179
191
  private
180
192
 
181
193
  def self._brick_get_fks
@@ -278,16 +290,12 @@ module ActiveRecord
278
290
  # Make sure it's a good association name and that the model has that column name
279
291
  next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
280
292
 
281
- # So that we can map an association name to any special alias name used in an AREL query
282
- ans = (assoc.klass._assoc_names[assoc_name] ||= [])
283
- ans << assoc.klass unless ans.include?(assoc.klass)
284
293
  # There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
285
294
  has_hm ||= assoc.macro == :has_many
286
295
  join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
287
296
  end
288
297
  wheres[k] = v.split(',')
289
298
  end
290
- # distinct! if has_hm
291
299
 
292
300
  # %%% Skip the metadata columns
293
301
  if selects&.empty? # Default to all columns
@@ -301,59 +309,101 @@ module ActiveRecord
301
309
  if is_add_bts || is_add_hms
302
310
  bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
303
311
  bts.each do |_k, bt|
304
- # join_array[bt.first] = nil # Store this relation name in our special collection for .joins()
312
+ # join_array will receive this relation name when calling #brick_parse_dsl
305
313
  bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
306
314
  end
307
315
  skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
308
316
  hms.each do |k, hm|
309
317
  next if skip_klass_hms.key?(k)
310
318
 
311
- join_array[k] = nil # Store this relation name in our special collection for .joins()
312
- hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
319
+ hm_counts[k] = hm
313
320
  end
314
321
  end
315
- where!(wheres) unless wheres.empty?
322
+
323
+ wheres = {}
324
+ params.each do |k, v|
325
+ case (ks = k.split('.')).length
326
+ when 1
327
+ next unless klass._brick_get_fks.include?(k)
328
+ when 2
329
+ assoc_name = ks.first.to_sym
330
+ # Make sure it's a good association name and that the model has that column name
331
+ next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.any? { |col| col.name == ks.last }
332
+
333
+ join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
334
+ end
335
+ wheres[k] = v.split(',')
336
+ end
337
+
316
338
  if join_array.present?
317
339
  left_outer_joins!(join_array) # joins!(join_array)
318
340
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
319
341
  (rel_dupe = dup)._arel_alias_names
320
342
  core_selects = selects.dup
321
- groups = []
322
343
  chains = rel_dupe._brick_chains
323
- id_for_tables = {}
344
+ id_for_tables = Hash.new { |h, k| h[k] = [] }
345
+ field_tbl_names = Hash.new { |h, k| h[k] = {} }
324
346
  bt_columns = bt_descrip.each_with_object([]) do |v, s|
325
- tbl_name = chains[v.last.first].first
326
- if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
327
- groups << (unaliased = "#{tbl_name}.#{id_col}")
328
- selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
329
- v.last << id_alias
347
+ tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
348
+ if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
349
+ # Accommodate composite primary key by allowing id_col to come in as an array
350
+ (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
351
+ selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
352
+ id_for_tables[v.first] << id_alias
353
+ end
354
+ v.last << id_for_tables[v.first]
330
355
  end
331
356
  if (col_name = v.last[1].last&.last)
357
+ field_tbl_name = nil
332
358
  v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
333
- groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
359
+ field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
334
360
  # col_name is weak when there are multiple, using sel_col.last instead
335
- tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
336
- selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
361
+ selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
337
362
  v.last[1][idx] << col_alias
338
363
  end
339
364
  end
340
365
  end
341
- group!(core_selects + groups) if hm_counts.any? # + bt_columns
342
366
  join_array.each do |assoc_name|
343
367
  # %%% Need to support {user: :profile}
344
368
  next unless assoc_name.is_a?(Symbol)
345
369
 
346
- klass = reflect_on_association(assoc_name)&.klass
347
- table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
370
+ table_alias = shift_or_first(chains[klass = reflect_on_association(assoc_name)&.klass])
348
371
  _assoc_names[assoc_name] = [table_alias, klass]
349
372
  end
350
- # Copy entries over
351
- hm_counts.keys.each do |k|
352
- hm_counts[k] = _assoc_names[k]
373
+ end
374
+ # Add derived table JOIN for the has_many counts
375
+ hm_counts.each do |k, hm|
376
+ associative = nil
377
+ count_column = if hm.options[:through]
378
+ fk_col = (associative = associatives[hm.name]).foreign_key
379
+ hm.foreign_key
380
+ else
381
+ fk_col = hm.foreign_key
382
+ hm.klass.primary_key || '*'
383
+ end
384
+ tbl_alias = "_br_#{hm.name}"
385
+ pri_tbl = hm.active_record
386
+ if fk_col.is_a?(Array) # Composite key?
387
+ on_clause = []
388
+ 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]}" }
389
+ joins!("LEFT OUTER
390
+ JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
391
+ ON #{on_clause.join(' AND ')}")
392
+ else
393
+ joins!("LEFT OUTER
394
+ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
395
+ ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
353
396
  end
354
397
  end
398
+ where!(wheres) unless wheres.empty?
355
399
  wheres unless wheres.empty? # Return the specific parameters that we did use
356
400
  end
401
+
402
+ private
403
+
404
+ def shift_or_first(ary)
405
+ ary.length > 1 ? ary.shift : ary.first
406
+ end
357
407
  end
358
408
 
359
409
  module Inheritance
@@ -552,27 +602,30 @@ class Object
552
602
  # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
553
603
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
554
604
  # The key in each hash entry (fk.first) is the constraint name
555
- assoc_name = (assoc = fk.last)[:assoc_name]
556
- inverse_assoc_name = assoc[:inverse]&.fetch(:assoc_name, nil)
605
+ inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
557
606
  options = {}
558
607
  singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
559
608
  macro = if assoc[:is_bt]
560
609
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
561
- if (primary_class = assoc.fetch(:primary_class, nil)) &&
562
- (sti_inverse_assoc = primary_class.reflect_on_all_associations.find { |a| a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key })
563
- assoc_name = sti_inverse_assoc.options[:inverse_of].to_s || assoc_name
564
- end
610
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
611
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
612
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
613
+ end
614
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
615
+ else
616
+ assoc[:assoc_name]
617
+ end
565
618
  need_class_name = singular_table_name.underscore != assoc_name
566
619
  need_fk = "#{assoc_name}_id" != assoc[:fk]
567
620
  if (inverse = assoc[:inverse])
568
621
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
569
622
  if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
570
623
  inverse_assoc_name = if has_ones[singular_inv_assoc_name]
571
- need_inverse_of = true
572
- has_ones[singular_inv_assoc_name]
573
- else
574
- singular_inv_assoc_name
575
- end
624
+ need_inverse_of = true
625
+ has_ones[singular_inv_assoc_name]
626
+ else
627
+ singular_inv_assoc_name
628
+ end
576
629
  end
577
630
  end
578
631
  :belongs_to
@@ -583,12 +636,12 @@ class Object
583
636
  need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
584
637
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
585
638
  if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
586
- assoc_name = if has_ones[singular_assoc_name]
587
- need_class_name = true
588
- has_ones[singular_assoc_name]
589
- else
590
- singular_assoc_name
591
- end
639
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
640
+ need_class_name = custom_assoc_name != singular_assoc_name
641
+ custom_assoc_name
642
+ else
643
+ singular_assoc_name
644
+ end
592
645
  :has_one
593
646
  else
594
647
  :has_many
@@ -625,31 +678,34 @@ class Object
625
678
  end
626
679
  hmts.each do |hmt_fk, fks|
627
680
  fks.each do |fk|
628
- source = nil
629
- this_hmt_fk = if fks.length > 1
630
- singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
631
- source = fk.last
632
- through = fk.first[:alternate_name].pluralize
633
- "#{singular_assoc_name}_#{hmt_fk}"
634
- else
635
- source = fk.last unless hmt_fk.singularize == fk.last
636
- through = fk.first[:assoc_name].pluralize
637
- hmt_fk
638
- end
639
- code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
681
+ through = fk.first[:assoc_name]
682
+ hmt_name = if fks.length > 1
683
+ if fks[0].first[:inverse][:assoc_name] == fks[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
684
+ "#{hmt_fk}_through_#{fk.first[:assoc_name]}"
685
+ else # Use BT names to provide uniqueness
686
+ through = fk.first[:alternate_name].pluralize
687
+ singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
688
+ "#{singular_assoc_name}_#{hmt_fk}"
689
+ end
690
+ else
691
+ hmt_fk
692
+ end
693
+ source = fk.last unless hmt_name.singularize == fk.last
694
+ code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
640
695
  options = { through: assoc_name }
641
696
  options[:source] = source.to_sym if source
642
- self.send(:has_many, this_hmt_fk.to_sym, **options)
643
- end
644
- end
645
- # Not NULLables
646
- relation[:cols].each do |col, datatype|
647
- if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
648
- ::Brick.config.not_nullables.include?("#{matching}.#{col}")
649
- code << " validates :#{col}, presence: true\n"
650
- self.send(:validates, col.to_sym, { presence: true })
697
+ self.send(:has_many, hmt_name.to_sym, **options)
651
698
  end
652
699
  end
700
+ # # Not NULLables
701
+ # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
702
+ # relation[:cols].each do |col, datatype|
703
+ # if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
704
+ # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
705
+ # code << " validates :#{col}, not_null: true\n"
706
+ # self.send(:validates, col.to_sym, { not_null: true })
707
+ # end
708
+ # end
653
709
  end
654
710
  code << "end # model #{model_name}\n\n"
655
711
  end # class definition
@@ -668,13 +724,26 @@ class Object
668
724
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
669
725
  code << " @#{table_name}.brick_select(params)\n"
670
726
  code << " end\n"
727
+ self.protect_from_forgery unless: -> { self.request.format.js? }
671
728
  self.define_method :index do
672
729
  ::Brick.set_db_schema(params)
673
- ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
730
+ if request.format == :csv # Asking for a template?
731
+ require 'csv'
732
+ exported_csv = CSV.generate(force_quotes: false) do |csv_out|
733
+ model.df_export(model.brick_import_template).each { |row| csv_out << row }
734
+ end
735
+ render inline: exported_csv, content_type: request.format
736
+ return
737
+ elsif request.format == :js # Asking for JSON?
738
+ render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
739
+ return
740
+ end
741
+
742
+ ar_relation = model.primary_key ? model.order("#{model.table_name}.#{model.primary_key}") : model.all
674
743
  @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
675
744
  # %%% Add custom HM count columns
676
745
  # %%% What happens when the PK is composite?
677
- counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
746
+ counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
678
747
  # *selects,
679
748
  instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
680
749
  # binding.pry
@@ -707,6 +776,22 @@ class Object
707
776
  code << " end\n"
708
777
  self.define_method :update do
709
778
  ::Brick.set_db_schema(params)
779
+
780
+ if request.format == :csv # Importing CSV?
781
+ require 'csv'
782
+ # See if internally it's likely a TSV file (tab-separated)
783
+ tab_counts = []
784
+ 5.times { tab_counts << request.body.readline.count("\t") unless request.body.eof? }
785
+ request.body.rewind
786
+ separator = "\t" if tab_counts.length > 0 && tab_counts.uniq.length == 1 && tab_counts.first > 0
787
+ result = model.df_import(CSV.parse(request.body, { col_sep: separator || :auto }), model.brick_import_template)
788
+ # render inline: exported_csv, content_type: request.format
789
+ return
790
+ # elsif request.format == :js # Asking for JSON?
791
+ # render inline: model.df_export(true).to_json, content_type: request.format
792
+ # return
793
+ end
794
+
710
795
  instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
711
796
  obj = obj.first if obj.is_a?(Array)
712
797
  obj.send(:update, send(params_name = params_name.to_sym))
@@ -732,7 +817,8 @@ class Object
732
817
 
733
818
  def _brick_get_hm_assoc_name(relation, hm_assoc)
734
819
  if relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1
735
- [ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name]), true]
820
+ plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
821
+ [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
736
822
  else
737
823
  [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
738
824
  end
@@ -989,18 +1075,16 @@ module Brick
989
1075
 
990
1076
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
991
1077
 
992
- cnstr_name = "hm_#{cnstr_name}"
993
- if (assoc_hm = hms.fetch(cnstr_name, nil))
1078
+ if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
994
1079
  assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
995
1080
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
996
1081
  assoc_hm[:inverse] = assoc_bt
997
1082
  else
998
- assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
1083
+ 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 }
999
1084
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
1000
1085
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
1001
1086
  end
1002
1087
  assoc_bt[:inverse] = assoc_hm
1003
- # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
1004
1088
  end
1005
1089
  end
1006
1090
  end
@@ -64,6 +64,14 @@ module Brick
64
64
  is_template_exists
65
65
  end
66
66
 
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(', ')
73
+ end
74
+
67
75
  alias :_brick_find_template :find_template
68
76
  def find_template(*args, **options)
69
77
  return _brick_find_template(*args, **options) unless @_brick_model
@@ -72,36 +80,36 @@ module Brick
72
80
  pk = @_brick_model.primary_key
73
81
  obj_name = model_name.underscore
74
82
  table_name = model_name.pluralize.underscore
83
+ template_link = nil
75
84
  bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
76
- hms_columns = +'' # Used for 'index'
85
+ hms_columns = [] # Used for 'index'
77
86
  skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
78
87
  hms_headers = hms.each_with_object([]) do |hm, s|
79
- hm_assoc = hm.last
80
- if args.first == 'index'
81
- hm_fk_name = if hm_assoc.options[:through]
82
- associative = associatives[hm_assoc.name]
83
- "'#{associative.name}.#{associative.foreign_key}'"
84
- else
85
- hm_assoc.foreign_key
86
- end
87
- hms_columns << if hm_assoc.macro == :has_many
88
- set_ct = if skip_klass_hms.key?((assoc_name = hm.first).to_sym)
89
- 'nil'
88
+ hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
89
+ hm_fk_name = if hm_assoc.options[:through]
90
+ associative = associatives[hm_assoc.name]
91
+ "'#{associative.name}.#{associative.foreign_key}'"
90
92
  else
91
- "#{obj_name}._br_#{assoc_name}_ct"
93
+ hm_assoc.foreign_key
92
94
  end
93
-
94
- "<td>
95
- <%= ct = #{set_ct}
96
- link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct&.zero? %>
97
- </td>\n"
95
+ if args.first == 'index'
96
+ hms_columns << if hm_assoc.macro == :has_many
97
+ set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
98
+ 'nil'
99
+ else
100
+ # Postgres column names are limited to 63 characters
101
+ attrib_name = "_br_#{assoc_name}_ct"[0..62]
102
+ "#{obj_name}.#{attrib_name} || 0"
103
+ end
104
+ "<%= ct = #{set_ct}
105
+ 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"
98
106
  else # has_one
99
- "<td>
100
- <%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
101
- </td>\n"
107
+ "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
102
108
  end
109
+ elsif args.first == 'show'
110
+ hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
103
111
  end
104
- s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
112
+ s << hm_stuff
105
113
  end
106
114
 
107
115
  schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
@@ -110,6 +118,13 @@ module Brick
110
118
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
111
119
  .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
112
120
  css = +"<style>
121
+ #dropper {
122
+ background-color: #eee;
123
+ }
124
+ #btnImport {
125
+ display: none;
126
+ }
127
+
113
128
  table {
114
129
  border-collapse: collapse;
115
130
  margin: 25px 0;
@@ -121,9 +136,12 @@ table {
121
136
 
122
137
  table thead tr th, table tr th {
123
138
  background-color: #009879;
124
- color: #ffffff;
139
+ color: #fff;
125
140
  text-align: left;
126
141
  }
142
+ table thead tr th a, table tr th a {
143
+ color: #80FFB8;
144
+ }
127
145
 
128
146
  table th, table td {
129
147
  padding: 0.2em 0.5em;
@@ -132,6 +150,9 @@ table th, table td {
132
150
  .show-field {
133
151
  background-color: #004998;
134
152
  }
153
+ .show-field a {
154
+ color: #80B8D2;
155
+ }
135
156
 
136
157
  table tbody tr {
137
158
  border-bottom: thin solid #dddddd;
@@ -244,11 +265,107 @@ function changeout(href, param, value) {
244
265
  </script>"
245
266
  inline = case args.first
246
267
  when 'index'
268
+ obj_pk = if pk&.is_a?(Array) # Composite primary key?
269
+ "[#{pk.map { |pk_part| "#{obj_name}.#{pk_part}" }.join(', ')}]"
270
+ elsif pk
271
+ "#{obj_name}.#{pk}"
272
+ end
273
+ if Object.const_defined?('DutyFree')
274
+ template_link = "
275
+ <%= link_to 'CSV', #{table_name}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
276
+ <div id=\"dropper\" contenteditable=\"true\"></div>
277
+ <input type=\"button\" id=\"btnImport\" value=\"Import\">
278
+
279
+ <script>
280
+ var dropperDiv = document.getElementById(\"dropper\");
281
+ var btnImport = document.getElementById(\"btnImport\");
282
+ var droppedTSV;
283
+ if (dropperDiv) { // Other interesting events: blur keyup input
284
+ dropperDiv.addEventListener(\"paste\", function (evt) {
285
+ droppedTSV = evt.clipboardData.getData('text/plain');
286
+ var html = evt.clipboardData.getData('text/html');
287
+ var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
288
+ console.log(tbl);
289
+ btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
290
+ });
291
+ btnImport.addEventListener(\"click\", function () {
292
+ fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
293
+ method: 'PATCH',
294
+ headers: { 'Content-Type': 'text/tab-separated-values' },
295
+ body: droppedTSV
296
+ }).then(function (tsvResponse) {
297
+ btnImport.style.display = \"none\";
298
+ console.log(\"toaster\", tsvResponse);
299
+ });
300
+ });
301
+ }
302
+ var sheetUrl;
303
+ var spreadsheetId;
304
+ var sheetsLink = document.getElementById(\"sheetsLink\");
305
+ function gapiLoaded() {
306
+ // Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
307
+ sheetsLink.addEventListener(\"click\", async function (evt) {
308
+ evt.preventDefault();
309
+ await gapi.load(\"client\", function () {
310
+ gapi.client.init({ // Load the discovery doc to initialize the API
311
+ clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
312
+ scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
313
+ discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
314
+ }).then(function () {
315
+ gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
316
+ updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
317
+ });
318
+ });
319
+ });
320
+ }
321
+
322
+ async function updateSignInStatus(isSignedIn) {
323
+ if (isSignedIn) {
324
+ console.log(\"turds!\");
325
+ await gapi.client.sheets.spreadsheets.create({
326
+ properties: {
327
+ title: #{table_name.inspect},
328
+ },
329
+ sheets: [
330
+ // sheet1, sheet2, sheet3
331
+ ]
332
+ }).then(function (response) {
333
+ sheetUrl = response.result.spreadsheetUrl;
334
+ spreadsheetId = response.result.spreadsheetId;
335
+ sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
336
+ console.log(\"x1\", sheetUrl);
337
+
338
+ // Get JSON data
339
+ fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
340
+ response.json().then(function (data) {
341
+ gapi.client.sheets.spreadsheets.values.append({
342
+ spreadsheetId: spreadsheetId,
343
+ range: \"Sheet1\",
344
+ valueInputOption: \"RAW\",
345
+ insertDataOption: \"INSERT_ROWS\"
346
+ }, {
347
+ range: \"Sheet1\",
348
+ majorDimension: \"ROWS\",
349
+ values: data,
350
+ }).then(function (response2) {
351
+ // console.log(\"beefcake\", response2);
352
+ });
353
+ });
354
+ });
355
+ });
356
+ window.open(sheetUrl, '_blank');
357
+ }
358
+ }
359
+ </script>
360
+ <script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
361
+ "
362
+ end
247
363
  "#{css}
248
364
  <p style=\"color: green\"><%= notice %></p>#{"
249
365
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
250
366
  <select id=\"tbl\">#{table_options}</select>
251
- <h1>#{model_name.pluralize}</h1>
367
+ <h1>#{model_name.pluralize}</h1>#{template_link}
368
+
252
369
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
253
370
  <table id=\"#{table_name}\">
254
371
  <thead><tr>#{'<th></th>' if pk}
@@ -256,26 +373,27 @@ function changeout(href, param, value) {
256
373
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
257
374
  <th>
258
375
  <% if (bt = bts[col]) %>
259
- BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
376
+ BT <%= bt[1].bt_link(bt.first) %>
260
377
  <% else %>
261
378
  <%= col %>
262
379
  <% end %>
263
380
  </th>
264
381
  <% end %>
265
- #{hms_headers.map { |h| "<th>#{h.last}</th>\n" }.join}
382
+ <%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
383
+ #{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.pluralize}_path) %></th>\n" }.join}
266
384
  </tr></thead>
267
385
 
268
386
  <tbody>
269
387
  <% @#{table_name}.each do |#{obj_name}| %>
270
388
  <tr>#{"
271
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
389
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
272
390
  <% #{obj_name}.attributes.each do |k, val| %>
273
391
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
274
392
  <td>
275
393
  <% if (bt = bts[k]) %>
276
- <%# binding.pry if bt.first == :user %>
277
- <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
278
- <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
394
+ <%# binding.pry # Postgres column names are limited to 63 characters %>
395
+ <% 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]) %>
396
+ <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
279
397
  <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
280
398
  <%#= 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 %>
281
399
  <% else %>
@@ -283,8 +401,7 @@ function changeout(href, param, value) {
283
401
  <% end %>
284
402
  </td>
285
403
  <% end %>
286
- #{hms_columns}
287
- <!-- td>X</td -->
404
+ #{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
288
405
  </tr>
289
406
  </tbody>
290
407
  <% end %>
@@ -301,23 +418,27 @@ function changeout(href, param, value) {
301
418
  <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
302
419
  <% if obj %>
303
420
  <%= # path_options = [obj.#{pk}]
304
- # path_options << { '_brick_schema': } if
421
+ # path_options << { '_brick_schema': } if
305
422
  # url = send(:#{model_name.underscore}_path, obj.#{pk})
306
- form_for(obj) do |f| %>
423
+ form_for(obj.becomes(#{model_name})) do |f| %>
307
424
  <table>
308
- <% @#{obj_name}.first.attributes.each do |k, val| %>
425
+ <% has_fields = false
426
+ @#{obj_name}.first.attributes.each do |k, val| %>
309
427
  <tr>
428
+ <%# %%% Accommodate composite keys %>
310
429
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
311
430
  <th class=\"show-field\">
312
- <% if (bt = bts[k])
431
+ <% has_fields = true
432
+ if (bt = bts[k])
313
433
  # Add a final member in this array with descriptive options to be used in <select> drop-downs
314
434
  bt_name = bt[1].name
315
435
  # %%% Only do this if the user has permissions to edit this bt field
316
436
  if bt.length < 4
317
437
  bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
318
- bt[1].order(:#{pk}).each { |obj| option_detail << [obj.brick_descrip, obj.#{pk}] }
438
+ # %%% Accommodate composite keys for obj.pk at the end here
439
+ bt[1].order(obj_pk = bt[1].primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
319
440
  end %>
320
- BT <%= \"#\{bt.first\}-\" unless bt_name.underscore == bt.first.to_s %><%= bt_name %>
441
+ BT <%= bt[1].bt_link(bt.first) %>
321
442
  <% else %>
322
443
  <%= k %>
323
444
  <% end %>
@@ -348,26 +469,34 @@ function changeout(href, param, value) {
348
469
  <% end %>
349
470
  </td>
350
471
  </tr>
472
+ <% end
473
+ if has_fields %>
474
+ <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
475
+ <% else %>
476
+ <tr><td colspan=\"2\">(No displayable fields)</td></tr>
351
477
  <% end %>
352
- <tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
353
478
  </table>
354
479
  <% end %>
355
480
 
356
- #{hms_headers.map do |hm|
357
- next unless (pk = hm.first.klass.primary_key)
358
-
359
- "<table id=\"#{hm_name = hm.first.name.to_s}\">
360
- <tr><th>#{hm.last}</th></tr>
361
- <% collection = @#{obj_name}.first.#{hm_name}
362
- collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
363
- if collection.empty? %>
364
- <tr><td>(none)</td></tr>
365
- <% else %>
366
- <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
367
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
368
- <% end %>
369
- <% end %>
370
- </table>" end.join}
481
+ #{hms_headers.each_with_object(+'') do |hm, s|
482
+ if (pk = hm.first.klass.primary_key)
483
+ s << "<table id=\"#{hm_name = hm.first.name.to_s}\">
484
+ <tr><th>#{hm[3]}</th></tr>
485
+ <% collection = @#{obj_name}.first.#{hm_name}
486
+ collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
487
+ if collection.empty? %>
488
+ <tr><td>(none)</td></tr>
489
+ <% else %>
490
+ <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
491
+ <%# %%% accommodate composite primary key %>
492
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
493
+ <% end %>
494
+ <% end %>
495
+ </table>"
496
+ else
497
+ s
498
+ end
499
+ end}
371
500
  <% end %>
372
501
  #{script}"
373
502
 
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 21
8
+ TINY = 24
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
@@ -105,9 +105,6 @@ module Brick
105
105
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
106
106
  next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
107
107
 
108
- # So that we can map an association name to any special alias name used in an AREL query
109
- ans = (model._assoc_names[a.name] ||= [])
110
- ans << a.klass unless ans.include?(a.klass)
111
108
  case a.macro
112
109
  when :belongs_to
113
110
  s.first[a.foreign_key] = [a.name, a.klass]
@@ -129,9 +126,7 @@ module Brick
129
126
  skip_hms[hmt.last.name] = nil
130
127
  end
131
128
  end
132
- skip_hms.each do |k, _v|
133
- puts hms.delete(k).inspect
134
- end
129
+ skip_hms.each { |k, _v| hms.delete(k) }
135
130
  [bts, hms, associatives]
136
131
  end
137
132
 
@@ -427,9 +422,7 @@ ActiveSupport.on_load(:active_record) do
427
422
  end
428
423
 
429
424
  result = result.map do |attributes|
430
- values = klass.initialize_attributes(attributes).values
431
-
432
- columns.zip(values).map do |column, value|
425
+ columns.zip(klass.initialize_attributes(attributes).values).map do |column, value|
433
426
  column.type_cast(value)
434
427
  end
435
428
  end
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.21
4
+ version: 1.0.24
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-05 00:00:00.000000000 Z
11
+ date: 2022-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord