brick 1.0.210 → 1.0.212

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: 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