brick 1.0.211 → 1.0.213

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7950119f788fe1cac3fde68806f2b4a1145da0cf17ff0d47363b102be9c69933
4
- data.tar.gz: 33bb7d89a0744890d4450afde332dbd0f0635a8d3acd0beb4107cf4e44ff9718
3
+ metadata.gz: 02a0436d0c71a40e1aa3858e4fdf20822f32b02864e621964bce288b8b21ad60
4
+ data.tar.gz: 8257218e3c1e9c26842753590e627802842c67dec93891aec7139fd2f31d67c0
5
5
  SHA512:
6
- metadata.gz: 959c6ada7e03de34f566ad6059a4431ea66d2be9f12764de3edb3760074a70f3e91354e9052bc3c1a74eed4d5d7a274a78cf490195815b736b02f282ffe03d47
7
- data.tar.gz: a87abaf8c878fecda93fc6751bae171e5194b6b63d933f83ba331191aee701859d3022754f68af3d730c25add0cc0a8174246fd18913040bed75ec700ba2b543
6
+ metadata.gz: 222b736da74b699a1f352ffff9f087f39048fceaf995dc28733823b4d38271c0bc04ecba506fb6fd027a7f39883009baede3d9c70f81a46dc023f692d6c831e4
7
+ data.tar.gz: bdee88e65dad07aa66b6ce9458a44ba8d1ad069b937d5bceae65f020162107336b6d7619aee03ed4c56c25a3e38b4c17ce0352677883426802dfe28ac3c8567a
data/lib/brick/config.rb CHANGED
@@ -238,7 +238,24 @@ module Brick
238
238
  end
239
239
 
240
240
  def treat_as_associative=(tables)
241
- @mutex.synchronize { @treat_as_associative = tables }
241
+ @mutex.synchronize do
242
+ @treat_as_associative = if tables.is_a?(Hash)
243
+ tables.each_with_object({}) do |v, s|
244
+ # If it's :constellation, or anything else in a hash, we'll take its value
245
+ # (and hopefully in this case that would be either a string or nil)
246
+ dsl = ((v.last.is_a?(Symbol) && v.last) || v.last&.values&.last)
247
+ unless (dsl ||= '').is_a?(String) || dsl.is_a?(Symbol)
248
+ puts "Was really expecting #{v.first} / #{v.last.first&.first} / #{dsl} to be a string, " +
249
+ "so will disregard #{dsl} and just turn on simple constellation view for #{v.first}."
250
+ end
251
+ s[v.first] = v.last.is_a?(Hash) ? dsl : v.last
252
+ end
253
+ elsif tables.is_a?(String) # comma-separated list?
254
+ tables.split(',').each_with_object({}) { |v, s| s[v.trim] = nil }
255
+ else # Expecting an Array, and have no special presentation
256
+ tables&.each_with_object({}) { |v, s| s[v] = nil }
257
+ end
258
+ end
242
259
  end
243
260
 
244
261
  # Polymorphic associations
@@ -286,6 +303,14 @@ module Brick
286
303
  @mutex.synchronize { @model_descrips = descrips }
287
304
  end
288
305
 
306
+ def erd_show_columns
307
+ @mutex.synchronize { @erd_show_columns ||= [] }
308
+ end
309
+
310
+ def erd_show_columns=(descrips)
311
+ @mutex.synchronize { @erd_show_columns = descrips }
312
+ end
313
+
289
314
  def sti_namespace_prefixes
290
315
  @mutex.synchronize { @sti_namespace_prefixes ||= {} }
291
316
  end
@@ -404,6 +429,19 @@ module Brick
404
429
  end
405
430
  end
406
431
 
432
+ def acts_as_list_cols
433
+ @mutex.synchronize { @acts_as_list || {} }
434
+ end
435
+
436
+ # Get something like:
437
+ # { 'on_call_list' => { _brick_default: [:last_name, :first_name] } }
438
+ # { 'on_call_list' => { _brick_default: :sequence } }
439
+ def acts_as_list_cols=(position_cols)
440
+ @mutex.synchronize do
441
+ @acts_as_list ||= position_cols
442
+ end
443
+ end
444
+
407
445
  def metadata_columns
408
446
  @mutex.synchronize { @metadata_columns ||= ['created_at', 'updated_at', 'deleted_at'] }
409
447
  end
@@ -107,7 +107,7 @@ module ActiveRecord
107
107
  # has_one_attached, has_many_attached, and has_rich_text
108
108
  def _activestorage_actiontext_fields
109
109
  fields = [[], [], {}]
110
- if !(self <= ActiveStorage::Blob) && respond_to?(:generated_association_methods) # ActiveStorage
110
+ if Object.const_defined?('ActiveStorage') && respond_to?(:generated_association_methods) && !(self <= ::ActiveStorage::Blob) # ActiveStorage
111
111
  generated_association_methods.instance_methods.each do |method_sym|
112
112
  method_str = method_sym.to_s
113
113
  fields[0] << method_str[0..-13].to_sym if method_str.end_with?('_attachment=') # has_one_attached
@@ -124,7 +124,7 @@ module ActiveRecord
124
124
  end
125
125
 
126
126
  def _active_storage_name(col_name)
127
- if Object.const_defined?('ActiveStorage') && (self <= ActiveStorage::Attachment || self <= ActiveStorage::Blob)
127
+ if Object.const_defined?('ActiveStorage') && (self <= ::ActiveStorage::Attachment || self <= ::ActiveStorage::Blob)
128
128
  if (col_str = col_name.to_s).end_with?('_attachments')
129
129
  col_str[0..-13]
130
130
  elsif col_str.end_with?('_blobs')
@@ -196,6 +196,33 @@ module ActiveRecord
196
196
  def _brick_monetized_attributes
197
197
  @_brick_monetized_attributes ||= respond_to?(:monetized_attributes) ? monetized_attributes.values : {}
198
198
  end
199
+
200
+ # def acts_as_list(aal_cols = nil)
201
+ # if aal_cols
202
+ # aal_cols = [aal_cols] unless aal_cols.is_a?(Array)
203
+ # @acts_as_list_cols = aal_cols.each_with_object([]) do |aal_col, s|
204
+ # if column_names.include?(aal_col = aal_col.to_s) && !s.include?(aal_col)
205
+ # s << aal_col
206
+ # end
207
+ # end
208
+ # else
209
+ # if [:integer, :bigint].include?(columns_hash['position']&.type)
210
+ # @acts_as_list_cols = ['position']
211
+ # else
212
+ # return
213
+ # end
214
+ # end
215
+ # # Override save in order to update neighbours when necessary
216
+ # alias _brick_save save
217
+ # def save
218
+ # # @acts_as_list_cols
219
+ # # -1
220
+ # @acts_as_list_cols.each do |aal_col|
221
+ # binding.pry if (aal_change = changes[aal_col])
222
+ # end
223
+ # _brick_save
224
+ # end
225
+ # end
199
226
  end
200
227
 
201
228
  def self.brick_parse_dsl(join_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
@@ -230,11 +257,7 @@ module ActiveRecord
230
257
  end
231
258
  if first_parts
232
259
  if (parts = prefix + first_parts + [parts[-1]]).length > 1 && klass
233
- unless is_polymorphic
234
- s = join_array
235
- parts[0..-3].each { |v| s = s[v.to_sym] }
236
- s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
237
- end
260
+ join_array.add_parts(parts) unless is_polymorphic
238
261
  translations[parts[0..-2].join('.')] = klass
239
262
  end
240
263
  if klass&.column_names.exclude?(parts.last) &&
@@ -538,7 +561,11 @@ module ActiveRecord
538
561
  end
539
562
 
540
563
  def self.brick_where(*args)
541
- (relation = all).brick_where(*args)
564
+ all.brick_where(*args)
565
+ end
566
+
567
+ def self.brick_group(*args, withhold_ids: true, **kwargs)
568
+ all.brick_group(*args, withhold_ids: withhold_ids, **kwargs)
542
569
  end
543
570
 
544
571
  private
@@ -612,27 +639,36 @@ module ActiveRecord
612
639
  pluck(selects)
613
640
  end
614
641
 
615
- def _brick_querying(*args, withhold_ids: nil, params: {}, order_by: nil, translations: {},
642
+ def brick_group(*args, **kwargs)
643
+ grouping = args[0].is_a?(Array) ? args[0] : args
644
+ _brick_querying(select_values.frozen? ? select_values.dup : select_values,
645
+ grouping: grouping, **kwargs)
646
+ self
647
+ end
648
+
649
+ def _brick_querying(*args, grouping: nil, withhold_ids: nil, params: {}, order_by: nil, translations: {},
616
650
  join_array: ::Brick::JoinArray.new,
617
651
  cust_col_override: nil,
618
652
  brick_col_names: nil)
619
653
  selects = args[0].is_a?(Array) ? args[0] : args
620
- if selects.present? && cust_col_override.nil? # See if there's any fancy ones in the select list
621
- idx = 0
622
- while idx < selects.length
623
- v = selects[idx]
624
- if v.is_a?(String) && v.index('.')
625
- # No prefixes and not polymorphic
626
- pieces = self.brick_parse_dsl(join_array, [], translations, false, dsl = "[#{v}]")
627
- (cust_col_override ||= {})[v.tr('.', '_').to_sym] = [pieces, dsl, true]
628
- selects.delete_at(idx)
629
- else
630
- idx += 1
654
+ unless cust_col_override
655
+ if selects.present? # See if there's any fancy ones in the select list
656
+ idx = 0
657
+ while idx < selects.length
658
+ v = selects[idx]
659
+ if v.is_a?(String) && v.index('.')
660
+ # No prefixes and not polymorphic
661
+ pieces = self.brick_parse_dsl(join_array, [], translations, false, dsl = "[#{v}]")
662
+ (cust_col_override ||= {})[v.tr('.', '_').to_sym] = [pieces, dsl, true]
663
+ selects.delete_at(idx)
664
+ else
665
+ idx += 1
666
+ end
631
667
  end
668
+ elsif selects.is_a?(Hash) && params.empty? # Make sense of things if they've passed in only params
669
+ params = selects
670
+ selects = []
632
671
  end
633
- elsif selects.is_a?(Hash) && params.empty? && cust_col_override.nil? # Make sense of things if they've passed in only params
634
- params = selects
635
- selects = []
636
672
  end
637
673
  is_add_bts = is_add_hms = !cust_col_override
638
674
 
@@ -710,6 +746,14 @@ module ActiveRecord
710
746
  end
711
747
  end
712
748
 
749
+ # Establish necessary JOINs for any custom GROUP BY columns
750
+ grouping&.each do |group_item|
751
+ # JOIN in all the same ways as the pathing describes
752
+ if group_item.is_a?(String) && (ref_parts = group_item.split('.')).length > 1
753
+ join_array.add_parts(ref_parts)
754
+ end
755
+ end
756
+
713
757
  if join_array.present?
714
758
  if ActiveRecord.version < Gem::Version.new('4.2')
715
759
  self.joins_values += join_array # Same as: joins!(join_array)
@@ -725,7 +769,9 @@ module ActiveRecord
725
769
 
726
770
  # CUSTOM COLUMNS
727
771
  # ==============
728
- (cust_col_override || (!withhold_ids && klass._br_cust_cols))&.each do |k, cc|
772
+ cust_cols = cust_col_override
773
+ cust_cols ||= klass._br_cust_cols unless withhold_ids
774
+ cust_cols&.each do |k, cc|
729
775
  brick_links # Intentionally create a relation duplicate
730
776
  if @_brick_rel_dup.respond_to?(k) # Name already taken?
731
777
  # %%% Use ensure_unique here in this kind of fashion:
@@ -753,7 +799,7 @@ module ActiveRecord
753
799
  col_alias = "#{col_prefix}#{k}__#{table_name.tr('.', '_')}_#{cc_part.first}"
754
800
  elsif brick_col_names ||
755
801
  used_col_aliases.key?(col_alias = k.to_s) # This sets a simpler custom column name if possible
756
- while cc_part_idx > 0 &&
802
+ while cc_part_idx >= 0 &&
757
803
  (col_alias = "#{col_prefix}#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__').tr('.', '_')}") &&
758
804
  used_col_aliases.key?(col_alias)
759
805
  cc_part_idx -= 1
@@ -880,7 +926,7 @@ module ActiveRecord
880
926
  through_sources.push(src_ref) unless src_ref.belongs_to?
881
927
  from_clause = +"#{_br_quoted_name(through_sources.first.table_name)} br_t0"
882
928
  # ActiveStorage will not get the correct count unless we do some extra filtering later
883
- tbl_nm = 'br_t0' if Object.const_defined?('ActiveStorage') && through_sources.first.klass <= ActiveStorage::Attachment
929
+ tbl_nm = 'br_t0' if Object.const_defined?('ActiveStorage') && through_sources.first.klass <= ::ActiveStorage::Attachment
884
930
  fk_col = through_sources.shift.foreign_key
885
931
 
886
932
  idx = 0
@@ -988,6 +1034,20 @@ JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#
988
1034
  klass._br_hm_counts.delete(n)
989
1035
  end
990
1036
 
1037
+ # Rewrite the group values to reference table and correlation names built out by AREL
1038
+ if grouping
1039
+ group2 = (gvgu = (group_values + grouping).uniq).each_with_object([]) do |v, s|
1040
+ if v.is_a?(Symbol) || (v_parts = v.split('.')).length == 1
1041
+ s << v
1042
+ elsif (tbl_name = brick_links[v_parts[0..-2].join('.')]&.split('.')&.last)
1043
+ s << "#{tbl_name}.#{v_parts.last}"
1044
+ else
1045
+ s << v
1046
+ end
1047
+ end
1048
+ group!(*group2)
1049
+ end
1050
+
991
1051
  unless wheres.empty?
992
1052
  # Rewrite the wheres to reference table and correlation names built out by AREL
993
1053
  where_nots = {}
@@ -998,7 +1058,7 @@ JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#
998
1058
  if (v_parts = v.first.split('.')).length == 1
999
1059
  (is_not ? where_nots : s)[v.first] = v.last
1000
1060
  else
1001
- tbl_name = brick_links[v_parts.first].split('.').last
1061
+ tbl_name = brick_links[v_parts[0..-2].join('.')].split('.').last
1002
1062
  (is_not ? where_nots : s)["#{tbl_name}.#{v_parts.last}"] = v.last
1003
1063
  end
1004
1064
  end
@@ -1069,8 +1129,8 @@ JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#
1069
1129
  end
1070
1130
 
1071
1131
  # ActiveStorage compatibility
1072
- selects << 'service_name' if klass.name == 'ActiveStorage::Blob' && ActiveStorage::Blob.columns_hash.key?('service_name')
1073
- selects << 'blob_id' if klass.name == 'ActiveStorage::Attachment' && ActiveStorage::Attachment.columns_hash.key?('blob_id')
1132
+ selects << 'service_name' if klass.name == 'ActiveStorage::Blob' && ::ActiveStorage::Blob.columns_hash.key?('service_name')
1133
+ selects << 'blob_id' if klass.name == 'ActiveStorage::Attachment' && ::ActiveStorage::Attachment.columns_hash.key?('blob_id')
1074
1134
  # Pay gem compatibility
1075
1135
  selects << 'processor' if klass.name == 'Pay::Customer' && Pay::Customer.columns_hash.key?('processor')
1076
1136
  selects << 'customer_id' if klass.name == 'Pay::Subscription' && Pay::Subscription.columns_hash.key?('customer_id')
@@ -1095,13 +1155,9 @@ JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#
1095
1155
  # && joins_values.empty? # Make sure we don't step on any toes if they've already specified JOIN things
1096
1156
  ja = nil
1097
1157
  opts.each do |k, v|
1158
+ # JOIN in all the same ways as the pathing describes
1098
1159
  if k.is_a?(String) && (ref_parts = k.split('.')).length > 1
1099
- # JOIN in all the same ways as the pathing describes
1100
- linkage = (ja ||= ::Brick::JoinArray.new)
1101
- ref_parts[0..-3].each do |prefix_part|
1102
- linkage = linkage[prefix_part.to_sym]
1103
- end
1104
- linkage[ref_parts[-2].to_sym] = nil
1160
+ (ja ||= ::Brick::JoinArray.new).add_parts(ref_parts)
1105
1161
  end
1106
1162
  end
1107
1163
  if ja&.present?
@@ -1846,6 +1902,12 @@ class Object
1846
1902
  end
1847
1903
  end
1848
1904
 
1905
+ # Apply any acts_as_list things
1906
+ if (aal_col = ::Brick.config.acts_as_list_cols.fetch(matching, nil))
1907
+ new_model_class.send(:acts_as_list, aal_col.to_sym)
1908
+ code << " acts_as_list :#{aal_col}\n"
1909
+ end
1910
+
1849
1911
  # Auto-support Ransack if it's present
1850
1912
  if self.respond_to?(:ransackable_attributes)
1851
1913
  def self.ransackable_attributes(auth_object = nil)
@@ -2078,6 +2140,23 @@ class Object
2078
2140
  # add_csp_hash
2079
2141
  # end
2080
2142
  # end
2143
+
2144
+ # Associate and unassociate in an N:M relation
2145
+ self.define_method :associate do
2146
+ if (base_class = (model = params['modelName']&.constantize).base_class)
2147
+ args = params['args']
2148
+ record = base_class.create(args[0] => args[1], args[2] => args[3])
2149
+ add_csp_hash
2150
+ render json: { data: record.id }
2151
+ end
2152
+ end
2153
+ self.define_method :unassociate do
2154
+ if (base_class = (model = params['modelName']&.constantize).base_class)
2155
+ base_class.find_by(base_class._pk_as_array&.first => params['id']).delete
2156
+ add_csp_hash
2157
+ end
2158
+ end
2159
+
2081
2160
  self.define_method :orphans do
2082
2161
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
2083
2162
  add_csp_hash
@@ -2440,7 +2519,7 @@ class Object
2440
2519
  if (new_obj = model.new(new_params)).respond_to?(:serializable_hash)
2441
2520
  # Convert any Filename objects with nil into an empty string so that #encode can be called on them
2442
2521
  new_obj.serializable_hash.each do |k, v|
2443
- new_obj.send("#{k}=", ActiveStorage::Filename.new('')) if v.is_a?(ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
2522
+ new_obj.send("#{k}=", ::ActiveStorage::Filename.new('')) if v.is_a?(::ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
2444
2523
  end if Object.const_defined?('ActiveStorage')
2445
2524
  end
2446
2525
  instance_variable_set("@#{singular_table_name}".to_sym, new_obj)
@@ -3087,7 +3166,7 @@ module Brick
3087
3166
  else
3088
3167
  res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
3089
3168
  res_name << '.' if res_name
3090
- (res_name ||= +'') << relation&.fetch(:resource, nil) || tbl_name_parts.last
3169
+ (res_name ||= +'') << (relation&.fetch(:resource, nil) || tbl_name_parts.last)
3091
3170
  end
3092
3171
 
3093
3172
  res_parts = ((mode == :singular) ? res_name.singularize : res_name).split('.')
@@ -198,7 +198,7 @@ function linkSchemas() {
198
198
 
199
199
  # Treat ActiveStorage::Blob metadata as JSON
200
200
  if ::Brick.config.table_name_prefixes.fetch('active_storage_', nil) == 'ActiveStorage' &&
201
- ActiveStorage.const_defined?('Blob')
201
+ ::ActiveStorage.const_defined?('Blob')
202
202
  unless (md = (::Brick.config.model_descrips ||= {})).key?('ActiveStorage::Blob')
203
203
  md['ActiveStorage::Blob'] = '[filename]'
204
204
  end
@@ -643,7 +643,7 @@ window.addEventListener(\"popstate\", linkSchemas);
643
643
  end
644
644
  # ActiveStorage has_one_attached and has_many_attached needs additional filtering on the name
645
645
  if (as_name = hm_assoc.klass&._active_storage_name(hm_assoc.name)) # ActiveStorage HMT
646
- prefix = 'attachments.' if hm_assoc.through_reflection&.klass&.<= ActiveStorage::Attachment
646
+ prefix = 'attachments.' if hm_assoc.through_reflection&.klass&.<= ::ActiveStorage::Attachment
647
647
  keys << ["#{prefix}name", as_name]
648
648
  end
649
649
  keys.to_h
@@ -1315,9 +1315,26 @@ end
1315
1315
  # or
1316
1316
  # Rails.application.reloader.to_prepare do ... end
1317
1317
  self.class.class_exec { include ::Brick::Rails::FormTags } unless respond_to?(:brick_grid)
1318
- # Write out the mega-grid
1318
+
1319
+ #{# Determine if we should render an N:M representation or the standard "mega_grid"
1320
+ taa = ::Brick.config.treat_as_associative&.fetch(res_name, nil)
1321
+ options = {}
1322
+ options[:prefix] = prefix unless prefix.blank?
1323
+ if taa.is_a?(String) # Write out a constellation
1324
+ representation = :constellation
1325
+ "
1326
+ brick_constellation(@#{res_name}, #{options.inspect}, bt_descrip: @_brick_bt_descrip, bts: bts)"
1327
+ elsif taa.is_a?(Symbol) # Write out a bezier representation
1328
+ "
1329
+ brick_bezier(@#{res_name}, #{options.inspect}, bt_descrip: @_brick_bt_descrip, bts: bts)"
1330
+ else # Write out the mega-grid
1331
+ representation = :grid
1332
+ "
1319
1333
  brick_grid(@#{res_name}, @_brick_sequence, @_brick_incl, @_brick_excl,
1320
- cols, bt_descrip: @_brick_bt_descrip, poly_cols: poly_cols, bts: bts, hms_keys: #{hms_keys.inspect}, hms_cols: {#{hms_columns.join(', ')}}) %>
1334
+ cols, bt_descrip: @_brick_bt_descrip,
1335
+ poly_cols: poly_cols, bts: bts, hms_keys: #{hms_keys.inspect}, hms_cols: {#{hms_columns.join(', ')}})"
1336
+ end}
1337
+ %>
1321
1338
 
1322
1339
  #{"<hr><%= link_to(\"New #{new_path_name = "new_#{path_obj_name}_path"
1323
1340
  obj_name}\", #{new_path_name}, { class: '__brick' }) if respond_to?(:#{new_path_name}) %>" unless @_brick_model.is_view?}
@@ -1727,10 +1744,11 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1727
1744
  }
1728
1745
  <%= \" showErd();\n\" if (@_brick_erd || 0) > 0
1729
1746
  %></script>
1730
-
1731
- <% end
1732
-
1733
- %><script>
1747
+ <% end %>
1748
+ "
1749
+ end
1750
+ if representation == :grid
1751
+ "<script>
1734
1752
  <% # Make column headers sort when clicked
1735
1753
  # %%% Create a smart javascript routine which can do this client-side %>
1736
1754
  [... document.getElementsByTagName(\"TH\")].forEach(function (th) {
@@ -84,7 +84,7 @@ module Brick::Rails::FormBuilder
84
84
  opts = enum_type.send(:mapping)&.each_with_object([]) { |v, s| s << [v.first, v.first] } || []
85
85
  out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, options)
86
86
  else
87
- digit_pattern = col_type == :integer ? '\d*' : '\d*(?:\.\d*|)'
87
+ digit_pattern = col_type == :integer ? '(?:-|)\d*' : '(?:-|)\d*(?:\.\d*|)'
88
88
  # Used to do this for float / decimal: self.number_field method.to_sym
89
89
  out << self.text_field(method.to_sym, { pattern: digit_pattern, class: 'check-validity' })
90
90
  end
@@ -3,27 +3,9 @@ module Brick::Rails::FormTags
3
3
  def brick_grid(relation = nil, sequence = nil, inclusions = nil, exclusions = nil,
4
4
  cols = {}, bt_descrip: nil, poly_cols: nil, bts: {}, hms_keys: [], hms_cols: {},
5
5
  show_header: nil, show_row_count: nil, show_erd_button: nil, show_in_app_button: nil, show_new_button: nil, show_avo_button: nil, show_aa_button: nil)
6
- # When a relation is not provided, first see if one exists which matches the controller name
7
- unless (relation ||= instance_variable_get("@#{controller_name}".to_sym))
8
- # Failing that, dig through the instance variables with hopes to find something that is an ActiveRecord::Relation
9
- case (collections = _brick_resource_from_iv).length
10
- when 0
11
- puts '#brick_grid: Not having been provided with a collection to work from, searched through all instance variables to find an ActiveRecord::Relation. None could be found.'
12
- return
13
- when 1 # If there's only one type match then simply get the first one, hoping that this is what they intended
14
- relation = instance_variable_get(iv = (chosen = collections.first).last.first)
15
- puts "#brick_grid: Not having been provided with a collection to work from, first tried @#{controller_name}.
16
- Failing that, have searched through instance variables and found #{iv} of type #{chosen.first.name}.
17
- Running with it!"
18
- else
19
- myriad = collections.each_with_object([]) { |c, s| c.last.each { |iv| s << "#{iv} (#{c.first.name})" } }
20
- puts "#brick_grid: Not having been provided with a collection to work from, first tried @#{controller_name}, and then searched through all instance variables.
21
- Found ActiveRecord::Relation objects of multiple types:
22
- #{myriad.inspect}
23
- Not knowing which of these to render, have erred on the side of caution and simply provided this warning message."
24
- return
25
- end
26
- end
6
+ # When a relation is not provided, first see if one exists which matches the controller name or
7
+ # something has turned up in the instance variables.
8
+ relation ||= (instance_variable_get("@#{controller_name}".to_sym) || _brick_resource_from_iv)
27
9
 
28
10
  nfc = Brick.config.sidescroll.fetch(relation.table_name, nil)&.fetch(:num_frozen_columns, nil) ||
29
11
  Brick.config.sidescroll.fetch(:num_frozen_columns, nil) ||
@@ -458,8 +440,8 @@ function onImagesLoaded(event) {
458
440
  elsif hma.include?(k) # has_many_attached
459
441
  { sql_type: 'binary', type: :files }
460
442
  elsif rtans&.key?(k) # has_rich_text
461
- k = rtans[k]
462
- { sql_type: 'varchar', type: :text }
443
+ k = rtans[k]
444
+ { sql_type: 'varchar', type: :text }
463
445
  end
464
446
  col = (ActiveRecord::ConnectionAdapters::Column.new(
465
447
  '', nil, ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(**kwargs)
@@ -541,6 +523,159 @@ function onImagesLoaded(event) {
541
523
  end
542
524
  end # brick_form_for
543
525
 
526
+ # ------------------------------------------
527
+ # Our cool N:M checkbox constellation editor
528
+ def brick_constellation(relation = nil, options = {}, x_axis: nil, y_axis: nil, bt_descrip: nil, bts: {})
529
+ x_axis, x_list, y_axis, y_list, existing = _n_m_prep(relation, x_axis, y_axis)
530
+
531
+ # HTML for constellation
532
+ prefix = options[:prefix]
533
+ out = +"<form action=\"#{"#{prefix}/" if prefix}brick_constellation\">
534
+ <table id=\"#{table_name = relation.table_name.split('.').last}\" class=\"shadow\">
535
+ <thead><tr><td></td>
536
+ "
537
+ # Header row with X axis values
538
+ x_list.each do |x_item|
539
+ out << " <th>#{x_item.first}</th>
540
+ "
541
+ end
542
+ out << " </tr></thead>
543
+ <tbody>
544
+ "
545
+ obj_path = "#{relation.klass._brick_index(:singular)}_path".to_sym
546
+ link_arrow = link_to('⇛', send(obj_path, '____'), { class: 'big-arrow' })
547
+ pk_as_array = relation.klass._pk_as_array
548
+ y_list.each do |y_item|
549
+ out << " <tr><th>#{y_item.first}</th>
550
+ "
551
+ x_list.each do |x_item|
552
+ checked = existing.find { |e| e[1] == x_item.last && e[2] == y_item.last }
553
+ item_id = pk_as_array.map { |pk_part| checked.first }.join('%2F') if checked
554
+ out << " <td><input type=\"checkbox\" name=\"#{table_name}\" #{"x-id=\"#{item_id}\" " if checked
555
+ }\" value=\"#{x_item.last}_#{y_item.last}\"#{' checked' if checked}>
556
+ #{link_arrow.gsub('____', item_id) if checked}</td>
557
+ "
558
+ end
559
+ out << " </tr>
560
+ "
561
+ end
562
+ out << " </tbody>
563
+ </table>
564
+ <script>
565
+ var constellation = document.getElementById(\"#{table_name}\");
566
+ var nextSib,
567
+ _this;
568
+ [... constellation.getElementsByTagName(\"INPUT\")].forEach(function (x) {
569
+ x.addEventListener(\"change\", function (y) {
570
+ _this = this;
571
+ if (this.checked) {
572
+ var ids = this.value.split(\"_\");
573
+ doFetch(\"POST\", {modelName: \"#{relation.klass.name}\",
574
+ args: [#{x_axis[1].inspect}, ids[0], #{y_axis[1].inspect}, ids[1]],
575
+ _brick_action: \"/#{prefix}brick_associate\"},
576
+ function (p) { // If it returns successfully, create an <a> element
577
+ p.text().then(function (response) {
578
+ var recordId = JSON.parse(response).data;
579
+ if (recordId) {
580
+ console.log(_this.getAttribute(\"x-id\"));
581
+ var tmp = document.createElement(\"DIV\");
582
+ tmp.innerHTML = \"#{link_arrow.gsub('"', '\"')}\".replace(\"____\", recordId);
583
+ _this.parentElement.append(tmp.firstChild);
584
+ }
585
+ });
586
+ }
587
+ );
588
+ } else if (nextSib = this.nextElementSibling) {
589
+ doFetch(\"DELETE\", {modelName: \"#{relation.klass.name}\",
590
+ id: this.getAttribute(\"x-id\"),
591
+ _brick_action: \"/#{prefix}brick_associate\"},
592
+ function (p) { // If it returns successfully, remove the an <a> element
593
+ _this.parentElement.removeChild(nextSib);
594
+ }
595
+ );
596
+ }
597
+ });
598
+ });
599
+ </script>
600
+ </form>
601
+ "
602
+ out.html_safe
603
+ end # brick_constellation
604
+
605
+ # ---------------------------------
606
+ # Our cool N:M bezier visualisation
607
+ # (...... work in progress .......)
608
+ def brick_bezier(relation = nil, options = {}, x_axis: nil, y_axis: nil, bt_descrip: nil, bts: {})
609
+ x_axis, x_list, y_axis, y_list, existing = _n_m_prep(relation, x_axis, y_axis)
610
+ # HTML for constellation
611
+ # X axis (List on left side)
612
+ out = +"<table id=\"#{x_axis.first}\" class=\"shadow\">
613
+ <tbody>
614
+ "
615
+ x_list.each_with_index { |x_item, idx| out << " <tr>#{"<th rowspan=\"#{x_list.length}\">#{x_axis.first}</th>" if idx.zero?}<td>#{x_item.first}</td></tr>" }
616
+ out << " </tbody>
617
+ </table>
618
+ "
619
+
620
+ # Y axis (List on right side)
621
+ out << "<table id=\"#{y_axis.first}\" class=\"shadow\">
622
+ <tbody>
623
+ "
624
+ y_list.each_with_index { |y_item, idx| out << " <tr><td>#{y_item.first}</td>#{"<th rowspan=\"#{y_list.length}\">#{y_axis.first}</th>" if idx.zero?}</tr>" }
625
+ out << " </tbody>
626
+ </table>
627
+ "
628
+
629
+ out.html_safe
630
+ end # brick_bezier
631
+
632
+ def _n_m_prep(relation, x_axis, y_axis)
633
+ relation ||= (instance_variable_get("@#{controller_name}".to_sym) || _brick_resource_from_iv)
634
+ # Just find the first two BT things at this point
635
+
636
+ klass = relation.klass
637
+ rel = ::Brick.relations&.fetch(relation.table_name, nil)
638
+ # fk_assocs = rel[:fks].map { |k, fk| [fk[:assoc_name], fk[:fk]] }
639
+ fk_assocs = klass.reflect_on_all_associations.each_with_object([]) do |assoc, s|
640
+ s << [assoc.name.to_s, assoc.foreign_key, assoc.klass] if assoc.belongs_to?
641
+ end
642
+
643
+ if (x_axis = fk_assocs.find { |assoc| assoc.include?(x_axis) })
644
+ fk_assocs -= x_axis
645
+ end
646
+ if (y_axis = fk_assocs.find { |assoc| assoc.include?(y_axis) })
647
+ fk_assocs -= y_axis
648
+ end
649
+ y_axis = fk_assocs.shift unless y_axis
650
+ x_axis = fk_assocs.shift unless x_axis
651
+ puts "FK Leftovers: #{fk_assocs.join(', ')}" unless fk_assocs.empty?
652
+
653
+ existing = relation.each_with_object([]) do |row, s|
654
+ row_id = row.send(klass.primary_key)
655
+ if (x_id = row.send(x_axis[1])) && (y_id = row.send(y_axis[1]))
656
+ s << [row_id, x_id, y_id]
657
+ end
658
+ end
659
+ x_list = _expand_collection(x_axis.last.all)
660
+ y_list = _expand_collection(y_axis.last.all)
661
+ [x_axis, x_list, y_axis, y_list, existing]
662
+ end
663
+
664
+ def _expand_collection(relation)
665
+ collection, descrip_cols = relation.brick_list
666
+ details = []
667
+ obj_pk = relation.klass.primary_key
668
+ collection&.brick_(:each) do |obj|
669
+ details << [
670
+ obj.brick_descrip(
671
+ descrip_cols&.first&.map { |col2| obj.send(col2.last) },
672
+ obj_pk
673
+ ), obj.send(obj_pk)
674
+ ]
675
+ end
676
+ details
677
+ end
678
+
544
679
  # --------------------------------
545
680
  def link_to_brick(*args, **kwargs)
546
681
  return unless ::Brick.config.mode == :on
@@ -627,7 +762,7 @@ function onImagesLoaded(event) {
627
762
  else
628
763
  # puts "Warning: link_to_brick could not find a class for \"#{controller_path}\" -- consider setting @_brick_model within that controller."
629
764
  # if (hits = res_names.keys & instance_variables.map { |v| v.to_s[1..-1] }).present?
630
- if (links = _brick_resource_from_iv(true)).length == 1 # If there's only one match then use any text that was supplied
765
+ if (links = _brick_relation_from_iv(true)).length == 1 # If there's only one match then use any text that was supplied
631
766
  link_to_brick(text || links.first.last.join('/'), links.first.first, **kwargs)
632
767
  else
633
768
  links.each_with_object([]) do |v, s|
@@ -677,7 +812,7 @@ btnAddCol.addEventListener(\"click\", function () {
677
812
  private
678
813
 
679
814
  # Dig through all instance variables with hopes to find any that appear related to ActiveRecord
680
- def _brick_resource_from_iv(trim_ampersand = false)
815
+ def _brick_relation_from_iv(trim_ampersand = false)
681
816
  instance_variables.each_with_object(Hash.new { |h, k| h[k] = [] }) do |name, s|
682
817
  iv_name = trim_ampersand ? name.to_s[1..-1] : name
683
818
  case (val = instance_variable_get(name))
@@ -688,4 +823,26 @@ private
688
823
  end
689
824
  end
690
825
  end
826
+
827
+ def _brick_resource_from_iv
828
+ # Failing that, dig through the instance variables with hopes to find something that is an ActiveRecord::Relation
829
+ case (collections = _brick_relation_from_iv).length
830
+ when 0
831
+ puts '#brick_grid: Not having been provided with a collection to work from, searched through all instance variables to find an ActiveRecord::Relation. None could be found.'
832
+ return
833
+ when 1 # If there's only one type match then simply get the first one, hoping that this is what they intended
834
+ relation = instance_variable_get(iv = (chosen = collections.first).last.first)
835
+ puts "#brick_grid: Not having been provided with a collection to work from, first tried @#{controller_name}.
836
+ Failing that, have searched through instance variables and found #{iv} of type #{chosen.first.name}.
837
+ Running with it!"
838
+ relation
839
+ else
840
+ myriad = collections.each_with_object([]) { |c, s| c.last.each { |iv| s << "#{iv} (#{c.first.name})" } }
841
+ puts "#brick_grid: Not having been provided with a collection to work from, first tried @#{controller_name}, and then searched through all instance variables.
842
+ Found ActiveRecord::Relation objects of multiple types:
843
+ #{myriad.inspect}
844
+ Not knowing which of these to render, have erred on the side of caution and simply provided this warning message."
845
+ return
846
+ end
847
+ end
691
848
  end
@@ -173,7 +173,11 @@ erDiagram
173
173
  <%= erd_sidelinks(shown_classes, hm_class).html_safe %>
174
174
  <% end
175
175
  def dt_lookup(dt)
176
- { 'integer' => 'int', }[dt] || dt&.tr(' ', '_') || 'int'
176
+ { 'integer' => 'int', 'character varying' => 'varchar', 'double precision' => 'float',
177
+ 'timestamp without time zone' => 'timestamp',
178
+ 'timestamp with time zone' => 'timestamp',
179
+ 'time without time zone' => 'time',
180
+ 'time with time zone' => 'time' }[dt] || dt&.tr(' ', '_') || 'int'
177
181
  end
178
182
  callbacks.merge({#{model_short_name.inspect} => #{model.name}}).each do |cb_k, cb_class|
179
183
  cb_relation = ::Brick.relations[cb_class.table_name]
@@ -193,6 +197,12 @@ erDiagram
193
197
  %>
194
198
  <%= \"#\{dt_lookup(cols[fk]&.first)} #\{fk} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk) %><%
195
199
  end
200
+ end %><%
201
+ if (erd_sc = Brick.config.erd_show_columns) == true || erd_sc&.include?(cb_class.name)
202
+ cols&.each do |col|
203
+ next if pkeys.include?(col.first) || fkeys.include?(col.first) %>
204
+ <%= \"#\{dt_lookup(col[1]&.first&.to_s)} #\{col.first}\".html_safe %><%
205
+ end
196
206
  end %>
197
207
  }
198
208
  <% end
@@ -165,6 +165,12 @@ module Brick
165
165
  end.tap { |member| push(member) }
166
166
  end
167
167
  end
168
+
169
+ def add_parts(parts)
170
+ s = self
171
+ parts[0..-3].each { |part| s = s[part.to_sym] }
172
+ s[parts[-2].to_sym] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
173
+ end
168
174
  end
169
175
 
170
176
  class JoinHash < Hash
@@ -287,6 +287,15 @@ module Brick
287
287
  # post("/#{controller_prefix}brick_schema", to: 'brick_gem#schema_create', as: schema_as.to_s)
288
288
  # end
289
289
 
290
+ if (associate_as = "#{controller_prefix.tr('/', '_')}brick_associate".to_sym)
291
+ (
292
+ !(associate_route = instance_variable_get(:@set).named_routes.find { |route| route.first == associate_as }&.last) ||
293
+ !associate_route.ast.to_s.include?("/#{controller_prefix}brick_associate/")
294
+ )
295
+ post("/#{controller_prefix}brick_associate", to: 'brick_gem#associate', as: associate_as.to_s)
296
+ delete("/#{controller_prefix}brick_associate", to: 'brick_gem#unassociate')
297
+ end
298
+
290
299
  if ::Brick.config.add_orphans && (orphans_as = "#{controller_prefix.tr('/', '_')}brick_orphans".to_sym)
291
300
  (
292
301
  !(orphans_route = instance_variable_get(:@set).named_routes.find { |route| route.first == orphans_as }&.last) ||
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 211
8
+ TINY = 213
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -310,6 +310,14 @@ if ActiveRecord::Base.respond_to?(:brick_select) && !::Brick.initializer_loaded
310
310
  # # have a table to still be treated as associative, causing HMTs to be auto-generated.)
311
311
  # Brick.treat_as_associative = ['flights']
312
312
 
313
+ # # Further, if you want to present a given associative table in various ways then you can choose a 2D
314
+ # # constellation map of checkboxes, or bezier curves showing the association between a list at the left and at
315
+ # # the right. Indicating just :bezier is the same as :bezier_full, which shows the full list of all possible
316
+ # # things that can be associated. :bezier_union shows just the ones that are currently wired up, and
317
+ # # :bezier_excluded, :bezier_excluded_left, or :bezier_excluded_right shows the ones not yet wired up.
318
+ # Brick.treat_as_associative = { 'flights' => [:bezier, 'departure.code', 'arrival.code'],
319
+ # 'crew' => [:constellation, 'flight', 'personnel', '[used ? [used it!] : []]'] }
320
+
313
321
  # # We normally don't show the timestamp columns \"created_at\", \"updated_at\", and \"deleted_at\", and also do
314
322
  # # not consider them when finding associative tables to support an N:M association. (That is, ones that can be a
315
323
  # # part of a has_many :through association.) If you want to use different exclusion columns than our defaults
@@ -334,6 +342,14 @@ if ActiveRecord::Base.respond_to?(:brick_select) && !::Brick.initializer_loaded
334
342
  # # user, then you can use model_descrips like this, putting expressions with property references in square brackets:
335
343
  # Brick.model_descrips = { 'User' => '[profile.firstname] [profile.lastname]' }
336
344
 
345
+ # # ERD SETTINGS
346
+
347
+ # # By default the Entity Relationship Diagram fragment which is available to be shown on the Grid page includes
348
+ # # primary and foreign keys. In order for it to show all columns in all cases, set this value to +true+:
349
+ # Brick.config.erd_show_columns = true
350
+ # # or to show all columns for specific tables, supply an array of model names:
351
+ # Brick.config.erd_show_columns = ['User', 'OrderDetail']
352
+
337
353
  # # SINGLE TABLE INHERITANCE
338
354
 
339
355
  # # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
@@ -53,7 +53,12 @@ module Brick
53
53
  s[v_parts.first] = nil unless [::Brick.default_schema, 'public'].include?(v_parts.first)
54
54
  end
55
55
  end
56
- seeds = +"# Seeds file for #{ActiveRecord::Base.connection.current_database}:\n"
56
+ seeds = +'# Seeds file for '
57
+ if (arbc = ActiveRecord::Base.connection).respond_to?(:current_database) # SQLite3 can't do this!
58
+ seeds << "#{arbc.current_database}:\n"
59
+ elsif (filename = arbc.instance_variable_get(:@connection_parameters)&.fetch(:database, nil))
60
+ seeds << "#{filename}:\n"
61
+ end
57
62
  done = []
58
63
  fks = {}
59
64
  stuck = {}
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.211
4
+ version: 1.0.213
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-24 00:00:00.000000000 Z
11
+ date: 2024-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -285,7 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
285
285
  - !ruby/object:Gem::Version
286
286
  version: 1.3.6
287
287
  requirements: []
288
- rubygems_version: 3.1.6
288
+ rubygems_version: 3.2.33
289
289
  signing_key:
290
290
  specification_version: 4
291
291
  summary: Create a Rails app from data alone