brick 1.0.210 → 1.0.212

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: 803d4790408a0b789ff8086b5b81346581071b260cbc528039fd8cfa8894b7ac
4
- data.tar.gz: 4503e1b9feba62d33e83f3fadc25a34b70bf9c661dc750187a3c391e3b24d5e8
3
+ metadata.gz: 3e954e525ea7a17eea2b33a125283f8af47a7bad1e457780815a8f53e06ed8fc
4
+ data.tar.gz: 9ded8bfa75d407294eb3e34895d1fc36ffa17767b9cbdeee0c961d6d9e821653
5
5
  SHA512:
6
- metadata.gz: 1fe28c2a9ca30ef663f28fe79e65e1b2787686c6fce6d5738d9b6addd6070437411d5210e1ba641b28eae5215fb37756f3faa80f246d823d5d553c9db990a427
7
- data.tar.gz: daa9bb88c21467075717062c1ede0ab38af4207bf96d9a2495ce84f99850a4d0cb09f4c1c95017cb3862ef915fab85036397375d43518f7bee2d7bd6f1255aed
6
+ metadata.gz: 6c2d33f2a6247cc001071929731d2b2543d2b91ba0e8c22afeda7d9fa40e37ae9c3957d4046f8157a34b1549dca8e542de97a58f22b25a312532eed775d3989c
7
+ data.tar.gz: 4e63b7986eb7b683421ef92d00fe61a3970ed0d24fb57403a89d05c114760ad00310e6c635d66bc0388e5ca26b6c6fd2061574c83e1f70b60f90baa22babcb92
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
@@ -92,11 +92,49 @@ module ActiveRecord
92
92
  reflect_on_association(assoc).foreign_type || "#{assoc}_type"
93
93
  end
94
94
 
95
- def _brick_all_fields
96
- rtans = if respond_to?(:rich_text_association_names)
97
- rich_text_association_names&.map { |rtan| rtan.to_s.start_with?('rich_text_') ? rtan[10..-1] : rtan }
98
- end
99
- columns_hash.keys.map(&:to_sym) + (rtans || [])
95
+ def _brick_all_fields(skip_id = nil)
96
+ col_names = columns_hash.keys
97
+ # If it's a composite primary key then allow all the values through
98
+ # TODO: Should disallow any autoincrement / SERIAL columns
99
+ if skip_id && (pk_as_array = _pk_as_array).length == 1
100
+ col_names -= _pk_as_array
101
+ end
102
+ hoa, hma, rtans = _activestorage_actiontext_fields
103
+ col_names.map(&:to_sym) + hoa + hma.map { |as| { as => [] } } + rtans.values
104
+ end
105
+
106
+ # Return three lists of fields for this model --
107
+ # has_one_attached, has_many_attached, and has_rich_text
108
+ def _activestorage_actiontext_fields
109
+ fields = [[], [], {}]
110
+ if Object.const_defined?('ActiveStorage') && respond_to?(:generated_association_methods) && !(self <= ::ActiveStorage::Blob) # ActiveStorage
111
+ generated_association_methods.instance_methods.each do |method_sym|
112
+ method_str = method_sym.to_s
113
+ fields[0] << method_str[0..-13].to_sym if method_str.end_with?('_attachment=') # has_one_attached
114
+ fields[1] << method_str[0..-14].to_sym if method_str.end_with?('_attachments=') # has_many_attached
115
+ end
116
+ end
117
+ if respond_to?(:rich_text_association_names) # ActionText
118
+ rich_text_association_names&.each do |rtan| # has_rich_text
119
+ rtan_str = rtan.to_s
120
+ fields[2][rtan] = rtan_str.start_with?('rich_text_') ? rtan_str[10..-1].to_sym : rtan
121
+ end
122
+ end
123
+ fields
124
+ end
125
+
126
+ def _active_storage_name(col_name)
127
+ if Object.const_defined?('ActiveStorage') && (self <= ::ActiveStorage::Attachment || self <= ::ActiveStorage::Blob)
128
+ if (col_str = col_name.to_s).end_with?('_attachments')
129
+ col_str[0..-13]
130
+ elsif col_str.end_with?('_blobs')
131
+ col_str[0..-7]
132
+ end
133
+ end
134
+ end
135
+
136
+ def _pk_as_array
137
+ self.primary_key.is_a?(Array) ? self.primary_key : [self.primary_key]
100
138
  end
101
139
 
102
140
  def _br_quoted_name(name)
@@ -147,8 +185,8 @@ module ActiveRecord
147
185
  skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || []) + [primary_key]
148
186
  dsl = if (descrip_col = columns.find { |c| [:boolean, :binary, :xml].exclude?(c.type) && skip_columns.exclude?(c.name) })
149
187
  "[#{descrip_col.name}]"
150
- elsif (pk_parts = self.primary_key.is_a?(Array) ? self.primary_key : [self.primary_key])
151
- "#{name} ##{pk_parts.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
188
+ else
189
+ "#{name} ##{_pk_as_array.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
152
190
  end
153
191
  ::Brick.config.model_descrips[name] = dsl
154
192
  end
@@ -841,6 +879,8 @@ module ActiveRecord
841
879
  end
842
880
  through_sources.push(src_ref) unless src_ref.belongs_to?
843
881
  from_clause = +"#{_br_quoted_name(through_sources.first.table_name)} br_t0"
882
+ # 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
844
884
  fk_col = through_sources.shift.foreign_key
845
885
 
846
886
  idx = 0
@@ -934,10 +974,14 @@ module ActiveRecord
934
974
  tbl_nm = hm.macro == :has_and_belongs_to_many ? hm.join_table : hm.table_name
935
975
  hm_table_name = _br_quoted_name(tbl_nm)
936
976
  end
977
+ # ActiveStorage has_many_attached needs a bit more filtering
978
+ if (k_str = hm.klass._active_storage_name(k))
979
+ where_ct_clause = "WHERE #{_br_quoted_name("#{tbl_nm}.name")} = '#{k_str}' "
980
+ end
937
981
  group_bys = ::Brick.is_oracle || is_mssql ? hm_selects : (1..hm_selects.length).to_a
938
982
  join_clause = "LEFT OUTER
939
983
  JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#{s}") }.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}#{_br_quoted_name(count_column)
940
- }) AS c_t_ FROM #{from_clause || hm_table_name} GROUP BY #{group_bys.join(', ')}) #{_br_quoted_name(tbl_alias)}"
984
+ }) AS c_t_ FROM #{from_clause || hm_table_name} #{where_ct_clause}GROUP BY #{group_bys.join(', ')}) #{_br_quoted_name(tbl_alias)}"
941
985
  self.joins_values |= ["#{join_clause} ON #{on_clause.join(' AND ')}"] # Same as: joins!(...)
942
986
  end unless cust_col_override
943
987
  while (n = nix.pop)
@@ -1025,8 +1069,8 @@ JOIN (SELECT #{hm_selects.map { |s| _br_quoted_name("#{'br_t0.' if from_clause}#
1025
1069
  end
1026
1070
 
1027
1071
  # ActiveStorage compatibility
1028
- selects << 'service_name' if klass.name == 'ActiveStorage::Blob' && ActiveStorage::Blob.columns_hash.key?('service_name')
1029
- selects << 'blob_id' if klass.name == 'ActiveStorage::Attachment' && ActiveStorage::Attachment.columns_hash.key?('blob_id')
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')
1030
1074
  # Pay gem compatibility
1031
1075
  selects << 'processor' if klass.name == 'Pay::Customer' && Pay::Customer.columns_hash.key?('processor')
1032
1076
  selects << 'customer_id' if klass.name == 'Pay::Subscription' && Pay::Subscription.columns_hash.key?('customer_id')
@@ -2034,6 +2078,23 @@ class Object
2034
2078
  # add_csp_hash
2035
2079
  # end
2036
2080
  # end
2081
+
2082
+ # Associate and unassociate in an N:M relation
2083
+ self.define_method :associate do
2084
+ if (base_class = (model = params['modelName']&.constantize).base_class)
2085
+ args = params['args']
2086
+ record = base_class.create(args[0] => args[1], args[2] => args[3])
2087
+ add_csp_hash
2088
+ render json: { data: record.id }
2089
+ end
2090
+ end
2091
+ self.define_method :unassociate do
2092
+ if (base_class = (model = params['modelName']&.constantize).base_class)
2093
+ base_class.find_by(base_class._pk_as_array&.first => params['id']).delete
2094
+ add_csp_hash
2095
+ end
2096
+ end
2097
+
2037
2098
  self.define_method :orphans do
2038
2099
  instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params).first))
2039
2100
  add_csp_hash
@@ -2369,6 +2430,8 @@ class Object
2369
2430
  end
2370
2431
  end
2371
2432
 
2433
+ params_name_sym = (params_name = "#{singular_table_name}_params").to_sym
2434
+
2372
2435
  # By default, views get marked as read-only
2373
2436
  # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
2374
2437
  code << " def new\n"
@@ -2376,7 +2439,11 @@ class Object
2376
2439
  code << " end\n"
2377
2440
  self.define_method :new do
2378
2441
  _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
2379
- new_params = model.attribute_names.each_with_object({}) do |a, s|
2442
+ new_params = begin
2443
+ send(params_name_sym)
2444
+ rescue
2445
+ end
2446
+ new_params ||= model.attribute_names.each_with_object({}) do |a, s|
2380
2447
  if (val = params["__#{a}"])
2381
2448
  # val = case new_obj.class.column_for_attribute(a).type
2382
2449
  # when :datetime, :date, :time, :timestamp
@@ -2390,15 +2457,13 @@ class Object
2390
2457
  if (new_obj = model.new(new_params)).respond_to?(:serializable_hash)
2391
2458
  # Convert any Filename objects with nil into an empty string so that #encode can be called on them
2392
2459
  new_obj.serializable_hash.each do |k, v|
2393
- new_obj.send("#{k}=", ActiveStorage::Filename.new('')) if v.is_a?(ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
2460
+ new_obj.send("#{k}=", ::ActiveStorage::Filename.new('')) if v.is_a?(::ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
2394
2461
  end if Object.const_defined?('ActiveStorage')
2395
2462
  end
2396
2463
  instance_variable_set("@#{singular_table_name}".to_sym, new_obj)
2397
2464
  add_csp_hash
2398
2465
  end
2399
2466
 
2400
- params_name_sym = (params_name = "#{singular_table_name}_params").to_sym
2401
-
2402
2467
  code << " def create\n"
2403
2468
  code << " @#{singular_table_name} = #{model.name}.create(#{params_name})\n"
2404
2469
  code << " end\n"
@@ -2415,8 +2480,7 @@ class Object
2415
2480
  end
2416
2481
  render json: { result: ::Brick.unexclude_column(table_name, col) }
2417
2482
  else
2418
- @_lookup_context.instance_variable_set("@#{singular_table_name}".to_sym,
2419
- (created_obj = model.send(:create, send(params_name_sym))))
2483
+ created_obj = model.send(:create, send(params_name_sym))
2420
2484
  @_lookup_context.instance_variable_set(:@_brick_model, model)
2421
2485
  if created_obj.errors.empty?
2422
2486
  index
@@ -2493,7 +2557,16 @@ class Object
2493
2557
  if (upd_hash ||= upd_params).fetch(model.inheritance_column, nil)&.strip == ''
2494
2558
  upd_hash[model.inheritance_column] = nil
2495
2559
  end
2496
- obj.send(:update, upd_hash || upd_params)
2560
+ # Do not clear out a has_many_attached field if it already has an entry and nothing is supplied
2561
+ hoa, hma, rtans = model._activestorage_actiontext_fields
2562
+ all_params = params[singular_table_name]
2563
+ hma.each do |hma_field|
2564
+ if upd_hash.fetch(hma_field) == [''] && # No new attachments...
2565
+ all_params&.fetch("_brick_attached_#{hma_field}", nil) # ...and there is something existing
2566
+ upd_hash.delete(hma_field)
2567
+ end
2568
+ end
2569
+ obj.send(:update, upd_hash)
2497
2570
  if obj.errors.any? # Surface errors to the user in a flash message
2498
2571
  flash.now.alert = (obj.errors.errors.map { |err| "<b>#{err.attribute}</b> #{err.message}" }.join(', '))
2499
2572
  end
@@ -2543,7 +2616,7 @@ class Object
2543
2616
 
2544
2617
  if is_need_params
2545
2618
  code << " def #{params_name}\n"
2546
- permits_txt = model._brick_find_permits(model, permits = model._brick_all_fields)
2619
+ permits_txt = model._brick_find_permits(model, permits = model._brick_all_fields(true))
2547
2620
  code << " params.require(:#{require_name = model.name.underscore.tr('/', '_')
2548
2621
  }).permit(#{permits_txt.map(&:inspect).join(', ')})\n"
2549
2622
  code << " end\n"
@@ -3031,7 +3104,7 @@ module Brick
3031
3104
  else
3032
3105
  res_name = (tbl_name_parts = tbl_name.split('.'))[0..-2].first
3033
3106
  res_name << '.' if res_name
3034
- (res_name ||= +'') << relation&.fetch(:resource, nil) || tbl_name_parts.last
3107
+ (res_name ||= +'') << (relation&.fetch(:resource, nil) || tbl_name_parts.last)
3035
3108
  end
3036
3109
 
3037
3110
  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
@@ -641,6 +641,11 @@ window.addEventListener(\"popstate\", linkSchemas);
641
641
  type_col = hm_assoc.inverse_of&.foreign_type || hm_assoc.type
642
642
  keys << [type_col, poly_type]
643
643
  end
644
+ # ActiveStorage has_one_attached and has_many_attached needs additional filtering on the name
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
647
+ keys << ["#{prefix}name", as_name]
648
+ end
644
649
  keys.to_h
645
650
  end
646
651
 
@@ -1310,9 +1315,26 @@ end
1310
1315
  # or
1311
1316
  # Rails.application.reloader.to_prepare do ... end
1312
1317
  self.class.class_exec { include ::Brick::Rails::FormTags } unless respond_to?(:brick_grid)
1313
- # 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
+ "
1314
1333
  brick_grid(@#{res_name}, @_brick_sequence, @_brick_incl, @_brick_excl,
1315
- 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
+ %>
1316
1338
 
1317
1339
  #{"<hr><%= link_to(\"New #{new_path_name = "new_#{path_obj_name}_path"
1318
1340
  obj_name}\", #{new_path_name}, { class: '__brick' }) if respond_to?(:#{new_path_name}) %>" unless @_brick_model.is_view?}
@@ -1506,7 +1528,7 @@ end
1506
1528
 
1507
1529
  if (pk = hm.first.klass.primary_key)
1508
1530
  hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
1509
- obj_br_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "br_#{hm_singular_name}.#{pk_part}" }.join(', ')
1531
+ obj_br_pk = hm.first.klass._pk_as_array.map { |pk_part| "br_#{hm_singular_name}.#{pk_part}" }.join(', ')
1510
1532
  poly_fix = if (poly_type = (hm.first.options[:as] && hm.first.type))
1511
1533
  "
1512
1534
  # Let's fix an unexpected \"feature\" of AR -- when going through a polymorphic has_many
@@ -1722,10 +1744,11 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1722
1744
  }
1723
1745
  <%= \" showErd();\n\" if (@_brick_erd || 0) > 0
1724
1746
  %></script>
1725
-
1726
- <% end
1727
-
1728
- %><script>
1747
+ <% end %>
1748
+ "
1749
+ end
1750
+ if representation == :grid
1751
+ "<script>
1729
1752
  <% # Make column headers sort when clicked
1730
1753
  # %%% Create a smart javascript routine which can do this client-side %>
1731
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
@@ -110,6 +110,41 @@ module Brick::Rails::FormBuilder
110
110
  ::Brick::Rails.display_binary(val)
111
111
  end
112
112
  end
113
+ when :file, :files
114
+ if attached = begin
115
+ self.object.send(method)
116
+ rescue
117
+ end
118
+ # Show any existing image(s)
119
+ existing = []
120
+ got_one = nil
121
+ (attached.respond_to?(:attachments) ? attached.attachments : [attached]).each do |attachment|
122
+ next unless (blob = attachment.blob)
123
+
124
+ existing << blob.key
125
+ out << "#{blob.filename}<br>"
126
+ url = begin
127
+ self.object.send(method)&.url
128
+ rescue StandardError => e
129
+ # Another possible option:
130
+ # Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
131
+ Rails.application.routes.url_helpers.rails_storage_proxy_path(attachment, only_path: true)
132
+ end
133
+ out << if url
134
+ "<img src=\"#{url}\" title=\"#{val}\">"
135
+ else # Convert the raw binary to a Base64 image
136
+ ::Brick::Rails.display_binary(attachment.download, 500_000)
137
+ end
138
+ got_one = true
139
+ out << '<br>'
140
+ end
141
+ out << 'Update: ' if got_one
142
+ end
143
+ out << self.hidden_field("_brick_attached_#{method}", value: existing.join(',')) unless existing.blank?
144
+ # Render a "Choose File(s)" input element
145
+ args = [method.to_sym]
146
+ args << { multiple: true } if col&.type == :files
147
+ out << self.file_field(*args)
113
148
  when :primary_key
114
149
  is_revert = false
115
150
  when :json, :jsonb
@@ -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) ||
@@ -445,19 +427,25 @@ function onImagesLoaded(event) {
445
427
  obj.send("#{model.brick_foreign_type(v.first)}=", v[1].first&.first&.name)
446
428
  end
447
429
  end if obj.new_record?
448
- rtans = model.rich_text_association_names if model.respond_to?(:rich_text_association_names)
449
- (model.column_names + (rtans || [])).each do |k|
430
+ hoa, hma, rtans = model._activestorage_actiontext_fields
431
+ (model.column_names + hoa + hma + rtans.keys).each do |k|
450
432
  pk_pos = (pk.index(k)&.+ 1)
451
433
  next if (pk_pos && pk.length == 1 && !bts.key?(k)) ||
452
434
  ::Brick.config.metadata_columns.include?(k)
453
435
 
454
436
  col = model.columns_hash[k]
455
- if !col && rtans&.include?(k)
456
- k = k[10..-1] if k.start_with?('rich_text_')
457
- col = (rt_col ||= ActiveRecord::ConnectionAdapters::Column.new(
458
- '', nil, ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: 'varchar', type: :text)
459
- )
460
- )
437
+ if !col
438
+ kwargs = if hoa.include?(k) # has_one_attached
439
+ { sql_type: 'binary', type: :file }
440
+ elsif hma.include?(k) # has_many_attached
441
+ { sql_type: 'binary', type: :files }
442
+ elsif rtans&.key?(k) # has_rich_text
443
+ k = rtans[k]
444
+ { sql_type: 'varchar', type: :text }
445
+ end
446
+ col = (ActiveRecord::ConnectionAdapters::Column.new(
447
+ '', nil, ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(**kwargs)
448
+ )) if kwargs
461
449
  end
462
450
  val = obj.attributes[k]
463
451
  out << "
@@ -535,6 +523,159 @@ function onImagesLoaded(event) {
535
523
  end
536
524
  end # brick_form_for
537
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
+
538
679
  # --------------------------------
539
680
  def link_to_brick(*args, **kwargs)
540
681
  return unless ::Brick.config.mode == :on
@@ -621,7 +762,7 @@ function onImagesLoaded(event) {
621
762
  else
622
763
  # puts "Warning: link_to_brick could not find a class for \"#{controller_path}\" -- consider setting @_brick_model within that controller."
623
764
  # if (hits = res_names.keys & instance_variables.map { |v| v.to_s[1..-1] }).present?
624
- 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
625
766
  link_to_brick(text || links.first.last.join('/'), links.first.first, **kwargs)
626
767
  else
627
768
  links.each_with_object([]) do |v, s|
@@ -671,7 +812,7 @@ btnAddCol.addEventListener(\"click\", function () {
671
812
  private
672
813
 
673
814
  # Dig through all instance variables with hopes to find any that appear related to ActiveRecord
674
- def _brick_resource_from_iv(trim_ampersand = false)
815
+ def _brick_relation_from_iv(trim_ampersand = false)
675
816
  instance_variables.each_with_object(Hash.new { |h, k| h[k] = [] }) do |name, s|
676
817
  iv_name = trim_ampersand ? name.to_s[1..-1] : name
677
818
  case (val = instance_variable_get(name))
@@ -682,4 +823,26 @@ private
682
823
  end
683
824
  end
684
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
685
848
  end
@@ -173,7 +173,12 @@ 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
+ puts dt.inspect
177
+ { 'integer' => 'int', 'character varying' => 'varchar', 'double precision' => 'float',
178
+ 'timestamp without time zone' => 'timestamp',
179
+ 'timestamp with time zone' => 'timestamp',
180
+ 'time without time zone' => 'time',
181
+ 'time with time zone' => 'time' }[dt] || dt&.tr(' ', '_') || 'int'
177
182
  end
178
183
  callbacks.merge({#{model_short_name.inspect} => #{model.name}}).each do |cb_k, cb_class|
179
184
  cb_relation = ::Brick.relations[cb_class.table_name]
@@ -193,6 +198,12 @@ erDiagram
193
198
  %>
194
199
  <%= \"#\{dt_lookup(cols[fk]&.first)} #\{fk} \\\"&nbsp;&nbsp;&nbsp;&nbsp;fk\\\"\".html_safe unless pkeys&.include?(fk) %><%
195
200
  end
201
+ end %><%
202
+ if (erd_sc = Brick.config.erd_show_columns) == true || erd_sc&.include?(cb_class.name)
203
+ cols&.each do |col|
204
+ next if pkeys.include?(col.first) || fkeys.include?(col.first) %>
205
+ <%= \"#\{dt_lookup(col[1]&.first&.to_s)} #\{col.first}\".html_safe %><%
206
+ end
196
207
  end %>
197
208
  }
198
209
  <% end
@@ -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 = 210
8
+ TINY = 212
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
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.210
4
+ version: 1.0.212
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-22 00:00:00.000000000 Z
11
+ date: 2024-03-28 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