brick 1.0.75 → 1.0.76

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: acd5fdf68935981a46106c879f5ac1fb44ac790c269c3eb2c5429cd67880f916
4
- data.tar.gz: 260ce60eeaf2e4b17ca5b5e76e54960c4d2685c9cb9d8549a35a16ce49b5a36c
3
+ metadata.gz: 615640b22db113a3959644ee9c2d08ff3a2b37aa10f12dc92d68effef091c228
4
+ data.tar.gz: b0d3e616b0f44584cf6960f8b8b191fc449b9afc426a6486da11c2deadece7a6
5
5
  SHA512:
6
- metadata.gz: 00f45ff020fcd729c2115f6d9f6ab0ab588247b2d727de7e1bd6acf53fa685ef76eeecce181a7a0c4659d8156971f88466915749cfad44e993993179435acdbd
7
- data.tar.gz: 74f0b40c44bae3729624ca0ba6a378f9067e4b0aa98460a03aa193e292432576327991d8c2a107317d8ce51ce223eee780a0d632d5805ca14b91a1b55388259f
6
+ metadata.gz: 02a9f1c74af24e1e23df8b64972d7af8d74bbb870f1510b69d0814f362959e2527f2a7ee9ff7ca65187eb97be4fad1136d64bea7e01a8327a57608924a8f12d8
7
+ data.tar.gz: '0265977b746d0955c76bef269067d14363cda4373fc5f726f19fb284baf31d0df3bdaae6ffad7301b71cd630a2745db4d30f1da79ec43ef4ae1a92bc986058e9'
data/lib/brick/config.rb CHANGED
@@ -90,6 +90,15 @@ module Brick
90
90
  @mutex.synchronize { @additional_references = references }
91
91
  end
92
92
 
93
+ # Custom columns to add to a table, minimally defined with a name and DSL string
94
+ def custom_columns
95
+ @mutex.synchronize { @custom_columns }
96
+ end
97
+
98
+ def custom_columns=(cust_cols)
99
+ @mutex.synchronize { @custom_columns = cust_cols }
100
+ end
101
+
93
102
  # Skip creating a has_many association for these
94
103
  def exclude_hms
95
104
  @mutex.synchronize { @exclude_hms }
@@ -100,73 +100,86 @@ module ActiveRecord
100
100
  dsl
101
101
  end
102
102
 
103
- def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, emit_dsl = false, is_polymorphic = false)
104
- build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
105
- build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
103
+ def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false, dsl = nil, emit_dsl = false)
104
+ unless build_array.is_a?(::Brick::JoinArray)
105
+ build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
106
+ build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
107
+ end
108
+ prefix = [prefix] unless prefix.is_a?(Array)
106
109
  members = []
110
+ unless dsl || (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
111
+ # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
112
+ x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
113
+ x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
114
+ return members
115
+ end
116
+
117
+ # Do the actual dirty work of recursing through nested DSL
107
118
  bracket_name = nil
108
- prefix = [prefix] unless prefix.is_a?(Array)
109
- if (dsl = ::Brick.config.model_descrips[name] || brick_get_dsl)
110
- dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
111
- dsl3 = +'' # To return expanded DSL that is nested from another model
112
- klass = nil
113
- dsl.each_char do |ch|
114
- if bracket_name
115
- if ch == ']' # Time to process a bracketed thing?
116
- parts = bracket_name.split('.')
117
- first_parts = parts[0..-2].map do |part|
118
- klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
119
- puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
120
- part_sym
121
- end
122
- parts = prefix + first_parts + [parts[-1]]
123
- if parts.length > 1
124
- unless is_polymorphic
125
- s = build_array
126
- parts[0..-3].each { |v| s = s[v.to_sym] }
127
- s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
128
- end
129
- translations[parts[0..-2].join('.')] = klass
119
+ dsl2 = +'' # To replace our own DSL definition in case it needs to be expanded
120
+ dsl3 = +'' # To return expanded DSL that is nested from another model
121
+ klass = nil
122
+ dsl.each_char do |ch|
123
+ if bracket_name
124
+ if ch == ']' # Time to process a bracketed thing?
125
+ parts = bracket_name.split('.')
126
+ first_parts = parts[0..-2].map do |part|
127
+ klass = (orig_class = klass).reflect_on_association(part_sym = part.to_sym)&.klass
128
+ puts "Couldn't reference #{orig_class.name}##{part} that's part of the DSL \"#{dsl}\"." if klass.nil?
129
+ part_sym
130
+ end
131
+ parts = prefix + first_parts + [parts[-1]]
132
+ if parts.length > 1
133
+ unless is_polymorphic
134
+ s = build_array
135
+ parts[0..-3].each { |v| s = s[v.to_sym] }
136
+ s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
130
137
  end
131
- if klass.column_names.exclude?(parts.last) &&
132
- (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
- # Expand this entry which refers to an association name
134
- members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, true)
135
- members += members2
136
- dsl2 << dsl2a
137
- dsl3 << dsl2a
138
- else
139
- dsl2 << "[#{bracket_name}]"
140
- if emit_dsl
141
- dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
142
- end
143
- members << parts
138
+ translations[parts[0..-2].join('.')] = klass
139
+ end
140
+ if klass.column_names.exclude?(parts.last) &&
141
+ (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
142
+ if prefix.empty? # Custom columns start with an empty prefix
143
+ prefix << parts.shift until parts.empty?
144
144
  end
145
- bracket_name = nil
145
+ # Expand this entry which refers to an association name
146
+ members2, dsl2a = klass.brick_parse_dsl(build_array, prefix + [possible_dsl], translations, is_polymorphic, nil, true)
147
+ members += members2
148
+ dsl2 << dsl2a
149
+ dsl3 << dsl2a
146
150
  else
147
- bracket_name << ch
151
+ dsl2 << "[#{bracket_name}]"
152
+ if emit_dsl
153
+ dsl3 << "[#{prefix[1..-1].map { |p| "#{p.to_s}." }.join if prefix.length > 1}#{bracket_name}]"
154
+ end
155
+ members << parts
148
156
  end
149
- elsif ch == '['
150
- bracket_name = +''
151
- klass = self
157
+ bracket_name = nil
152
158
  else
153
- dsl2 << ch
154
- dsl3 << ch
159
+ bracket_name << ch
155
160
  end
161
+ elsif ch == '['
162
+ bracket_name = +''
163
+ klass = self
164
+ else
165
+ dsl2 << ch
166
+ dsl3 << ch
156
167
  end
157
- # Rewrite the DSL in case it's now different from having to expand it
158
- # if ::Brick.config.model_descrips[name] != dsl2
159
- # puts ::Brick.config.model_descrips[name]
160
- # puts dsl2.inspect
161
- # puts dsl3.inspect
162
- # binding.pry
163
- # end
164
- ::Brick.config.model_descrips[name] = dsl2 unless emit_dsl
165
- else # With no DSL available, still put this prefix into the JoinArray so we can get primary key (ID) info from this table
166
- x = prefix.each_with_object(build_array) { |v, s| s[v.to_sym] }
167
- x[prefix.last] = nil unless prefix.empty? # Using []= will "hydrate" any missing part(s) in our whole series
168
168
  end
169
- emit_dsl ? [members, dsl3] : members
169
+ # Rewrite the DSL in case it's now different from having to expand it
170
+ # if ::Brick.config.model_descrips[name] != dsl2
171
+ # puts ::Brick.config.model_descrips[name]
172
+ # puts dsl2.inspect
173
+ # puts dsl3.inspect
174
+ # binding.pry
175
+ # end
176
+ if emit_dsl
177
+ # Had been: [members, dsl2, dsl3]
178
+ [members, dsl3]
179
+ else
180
+ ::Brick.config.model_descrips[name] = dsl2
181
+ members
182
+ end
170
183
  end
171
184
 
172
185
  # If available, parse simple DSL attached to a model in order to provide a friendlier name.
@@ -177,7 +190,8 @@ module ActiveRecord
177
190
  end
178
191
 
179
192
  def self.brick_descrip(obj, data = nil, pk_alias = nil)
180
- if (dsl = ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
193
+ dsl = obj if obj.is_a?(String)
194
+ if (dsl ||= ::Brick.config.model_descrips[(klass = self).name] || klass.brick_get_dsl)
181
195
  idx = -1
182
196
  caches = {}
183
197
  output = +''
@@ -272,18 +286,28 @@ module ActiveRecord
272
286
  def _br_associatives
273
287
  @_br_associatives ||= {}
274
288
  end
289
+ # Custom columns
290
+ def _br_cust_cols
291
+ @_br_cust_cols ||= {}
292
+ end
275
293
  end
276
294
 
277
- # Search for BT, HM, and HMT DSL stuff
295
+ # Search for custom column, BT, HM, and HMT DSL stuff
278
296
  def self._brick_calculate_bts_hms(translations, join_array)
297
+ # Add any custom columns
298
+ ::Brick.config.custom_columns&.fetch(table_name, nil)&.each do |k, cc|
299
+ # false = not polymorphic, and true = yes -- please emit_dsl
300
+ pieces, my_dsl = brick_parse_dsl(join_array, [], translations, false, cc, true)
301
+ _br_cust_cols[k] = [pieces, my_dsl]
302
+ end
279
303
  bts, hms, associatives = ::Brick.get_bts_and_hms(self)
280
304
  bts.each do |_k, bt|
281
305
  next if bt[2] # Polymorphic?
282
306
 
283
307
  # join_array will receive this relation name when calling #brick_parse_dsl
284
308
  _br_bt_descrip[bt.first] = if bt[1].is_a?(Array)
285
- # Last two params here: "false" is for don't emit DSL, and "true" is for yes, we are polymorphic
286
- bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, false, true) }
309
+ # Last params here: "true" is for yes, we are polymorphic
310
+ bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
287
311
  else
288
312
  { bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
289
313
  end
@@ -314,7 +338,7 @@ module ActiveRecord
314
338
  else # Expecting only Symbol
315
339
  if _br_hm_counts.key?(ord_part)
316
340
  ord_part = "\"b_r_#{ord_part}_ct\""
317
- elsif !_br_bt_descrip.key?(ord_part) && !column_names.include?(ord_part.to_s)
341
+ elsif !_br_bt_descrip.key?(ord_part) && !_br_cust_cols.key?(ord_part) && !column_names.include?(ord_part.to_s)
318
342
  # Disallow ordering by a bogus column
319
343
  # %%% Note this bogus entry so that Javascript can remove any bogus _brick_order
320
344
  # parameter from the querystring, pushing it into the browser history.
@@ -331,6 +355,11 @@ module ActiveRecord
331
355
  [order_by, order_by_txt]
332
356
  end
333
357
 
358
+ def self.brick_select(params = {}, selects = [], *args)
359
+ (relation = all).brick_select(params, selects, *args)
360
+ relation.select(selects)
361
+ end
362
+
334
363
  private
335
364
 
336
365
  def self._brick_get_fks
@@ -421,7 +450,13 @@ module ActiveRecord
421
450
  end
422
451
  end
423
452
 
424
- def brick_select(params, selects = nil, order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
453
+ def brick_select(params, selects = [], order_by = nil, translations = {}, join_array = ::Brick::JoinArray.new)
454
+ is_add_bts = is_add_hms = true
455
+
456
+ # Build out cust_cols, bt_descrip and hm_counts now so that they are available on the
457
+ # model early in case the user wants to do an ORDER BY based on any of that.
458
+ model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
459
+
425
460
  is_postgres = ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
426
461
  is_mysql = ActiveRecord::Base.connection.adapter_name == 'Mysql2'
427
462
  is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer'
@@ -446,7 +481,7 @@ module ActiveRecord
446
481
  end
447
482
 
448
483
  # %%% Skip the metadata columns
449
- if selects&.empty? # Default to all columns
484
+ if selects.empty? # Default to all columns
450
485
  tbl_no_schema = table.name.split('.').last
451
486
  # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
452
487
  # ActiveRecord::StatementInvalid (TinyTds::Error: DBPROCESS is dead or not enabled)
@@ -481,7 +516,36 @@ module ActiveRecord
481
516
  id_for_tables = Hash.new { |h, k| h[k] = [] }
482
517
  field_tbl_names = Hash.new { |h, k| h[k] = {} }
483
518
  used_col_aliases = {} # Used to make sure there is not a name clash
484
- bt_columns = klass._br_bt_descrip.each_with_object([]) do |v, s|
519
+
520
+ # CUSTOM COLUMNS
521
+ # ==============
522
+ klass._br_cust_cols.each do |k, cc|
523
+ if respond_to?(k) # Name already taken?
524
+ # %%% Use ensure_unique here in this kind of fashion:
525
+ # cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
526
+ # binding.pry
527
+ next
528
+ end
529
+
530
+ cc.first.each do |cc_part|
531
+ dest_klass = cc_part[0..-2].inject(klass) { |kl, cc_part_term| kl.reflect_on_association(cc_part_term).klass }
532
+ tbl_name = (field_tbl_names[k][cc_part.last] ||= shift_or_first(chains[dest_klass])).split('.').last
533
+ # Deal with the conflict if there are two parts in the custom column named the same,
534
+ # "category.name" and "product.name" for instance will end up with aliases of "name"
535
+ # and "product__name".
536
+ cc_part_idx = cc_part.length - 1
537
+ while cc_part_idx > 0 &&
538
+ (col_alias = "br_cc_#{k}__#{cc_part[cc_part_idx..-1].map(&:to_s).join('__')}") &&
539
+ used_col_aliases.key?(col_alias)
540
+ cc_part_idx -= 1
541
+ end
542
+ selects << "#{tbl_name}.#{cc_part.last} AS #{col_alias}"
543
+ cc_part << col_alias
544
+ used_col_aliases[col_alias] = nil
545
+ end
546
+ end
547
+
548
+ klass._br_bt_descrip.each do |v|
485
549
  v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
486
550
  next if chains[k1].nil?
487
551
 
@@ -544,7 +608,13 @@ module ActiveRecord
544
608
  klass._br_hm_counts.each do |k, hm|
545
609
  associative = nil
546
610
  count_column = if hm.options[:through]
547
- hm.foreign_key if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
611
+ if (fk_col = (associative = klass._br_associatives&.[](hm.name))&.foreign_key)
612
+ if hm.source_reflection.macro == :belongs_to # Traditional HMT using an associative table
613
+ hm.foreign_key
614
+ else # A HMT that goes HM -> HM, something like Categories -> Products -> LineItems
615
+ hm.source_reflection.active_record.primary_key
616
+ end
617
+ end
548
618
  else
549
619
  fk_col = hm.foreign_key
550
620
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
@@ -596,7 +666,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
596
666
  end
597
667
  where!(wheres) unless wheres.empty?
598
668
  # Must parse the order_by and see if there are any symbols which refer to BT associations
599
- # as they must be expanded to find the corresponding b_r_model__column naming for each.
669
+ # or custom columns as they must be expanded to find the corresponding b_r_model__column
670
+ # or br_cc_column naming for each.
600
671
  if order_by.present?
601
672
  final_order_by = *order_by.each_with_object([]) do |v, s|
602
673
  if v.is_a?(Symbol)
@@ -605,6 +676,8 @@ JOIN (SELECT #{selects.join(', ')}, COUNT(#{'DISTINCT ' if hm.options[:through]}
605
676
  bt_cols.values.each do |v1|
606
677
  v1.each { |v2| s << "\"#{v2.last}\"" if v2.length > 1 }
607
678
  end
679
+ elsif (cc_cols = klass._br_cust_cols[v])
680
+ cc_cols.first.each { |v1| s << "\"#{v1.last}\"" if v1.length > 1 }
608
681
  else
609
682
  s << v
610
683
  end
@@ -1204,22 +1277,6 @@ class Object
1204
1277
  return
1205
1278
  end
1206
1279
 
1207
- # Normal (non-swagger) request
1208
-
1209
- # We do all of this now so that bt_descrip and hm_counts are available on the model early in case the user
1210
- # wants to do an ORDER BY based on any of that
1211
- translations = {}
1212
- join_array = ::Brick::JoinArray.new
1213
- is_add_bts = is_add_hms = true
1214
- # This builds out bt_descrip and hm_counts on the model
1215
- model._brick_calculate_bts_hms(translations, join_array) if is_add_bts || is_add_hms
1216
-
1217
- # %%% Allow params to define which columns to use for order_by
1218
- # Overriding the default by providing a querystring param?
1219
- ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1220
- order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1221
-
1222
- ::Brick.set_db_schema(params)
1223
1280
  if request.format == :csv # Asking for a template?
1224
1281
  require 'csv'
1225
1282
  exported_csv = CSV.generate(force_quotes: false) do |csv_out|
@@ -1233,7 +1290,16 @@ class Object
1233
1290
  return
1234
1291
  end
1235
1292
 
1236
- @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), order_by, translations, join_array)
1293
+ # Normal (not swagger or CSV) request
1294
+
1295
+ # %%% Allow params to define which columns to use for order_by
1296
+ # Overriding the default by providing a querystring param?
1297
+ ordering = params['_brick_order']&.split(',')&.map(&:to_sym) || Object.send(:default_ordering, table_name, pk)
1298
+ order_by, _ = model._brick_calculate_ordering(ordering, true) # Don't do the txt part
1299
+
1300
+ @_brick_params = (ar_relation = model.all).brick_select(params, (selects = []), nil,
1301
+ translations = {},
1302
+ join_array = ::Brick::JoinArray.new)
1237
1303
  # %%% Add custom HM count columns
1238
1304
  # %%% What happens when the PK is composite?
1239
1305
  counts = model._br_hm_counts.each_with_object([]) do |v, s|
@@ -1902,9 +1968,7 @@ module Brick
1902
1968
  # For any appended references (those that come from config), arrive upon a definitely unique constraint name
1903
1969
  pri_tbl = is_class ? fk[4][:class].underscore : pri_tbl
1904
1970
  pri_tbl = "#{bt_assoc_name}_#{pri_tbl}" if pri_tbl&.singularize != bt_assoc_name
1905
- cnstr_base = cnstr_name = "(brick) #{for_tbl}_#{pri_tbl}"
1906
- cnstr_added_num = 1
1907
- cnstr_name = "#{cnstr_base}_#{cnstr_added_num += 1}" while bts&.key?(cnstr_name) || hms&.key?(cnstr_name)
1971
+ cnstr_name = ensure_unique(+"(brick) #{for_tbl}_#{pri_tbl}", bts, hms)
1908
1972
  missing = []
1909
1973
  missing << fk[1] unless relations.key?(fk[1])
1910
1974
  missing << primary_table unless is_class || relations.key?(primary_table)
@@ -2029,6 +2093,28 @@ module Brick
2029
2093
  ::Brick.relations.keys.map { |v| [(r = v.pluralize), (model = models[r])&.last&.table_name || v, migrations&.fetch(r, nil), model&.first] }
2030
2094
  end
2031
2095
 
2096
+ def ensure_unique(name, *sources)
2097
+ base = name
2098
+ if (added_num = name.slice!(/_(\d+)$/))
2099
+ added_num = added_num[1..-1].to_i
2100
+ else
2101
+ added_num = 1
2102
+ end
2103
+ while (
2104
+ name = "#{base}_#{added_num += 1}"
2105
+ sources.each_with_object(nil) do |v, s|
2106
+ s || case v
2107
+ when Hash
2108
+ v.key?(name)
2109
+ when Array
2110
+ v.include?(name)
2111
+ end
2112
+ end
2113
+ )
2114
+ end
2115
+ name
2116
+ end
2117
+
2032
2118
  # Locate orphaned records
2033
2119
  def find_orphans(multi_schema)
2034
2120
  is_default_schema = multi_schema&.==(Apartment.default_schema)
@@ -33,6 +33,9 @@ module Brick
33
33
  # Additional references (virtual foreign keys)
34
34
  ::Brick.additional_references = app.config.brick.fetch(:additional_references, nil)
35
35
 
36
+ # Custom columns to add to a table, minimally defined with a name and DSL string
37
+ ::Brick.custom_columns = app.config.brick.fetch(:custom_columns, nil)
38
+
36
39
  # When table names have specific prefixes, automatically place them in their own module with a table_name_prefix.
37
40
  ::Brick.order = app.config.brick.fetch(:order, {})
38
41
 
@@ -249,8 +252,8 @@ tr th {
249
252
  right: 0;
250
253
  cursor: pointer;
251
254
  }
252
- #headerTop tr th:hover {
253
- background-color: #18B090;
255
+ #headerTop tr th:hover, #headerTop tr th.highlight {
256
+ background-color: #28B898;
254
257
  }
255
258
  #exclusions {
256
259
  font-size: 0.7em;
@@ -273,6 +276,10 @@ tr th, tr td {
273
276
  padding: 0.2em 0.5em;
274
277
  }
275
278
 
279
+ tr td.highlight {
280
+ background-color: #B0B0FF;
281
+ }
282
+
276
283
  .show-field {
277
284
  background-color: #004998;
278
285
  }
@@ -500,6 +507,34 @@ function changeout(href, param, value, trimAfter) {
500
507
  var grid = document.getElementById(\"#{table_name}\");
501
508
  #{table_name}HtColumns = grid && [grid.getElementsByTagName(\"TR\")[0]];
502
509
  var headerTop = document.getElementById(\"headerTop\");
510
+ var headerCols;
511
+ if (grid) {
512
+ // COLUMN HEADER AND TABLE CELL HIGHLIGHTING
513
+ var gridHighHeader = null,
514
+ gridHighCell = null;
515
+ grid.addEventListener(\"mouseenter\", gridMove);
516
+ grid.addEventListener(\"mousemove\", gridMove);
517
+ grid.addEventListener(\"mouseleave\", function (evt) {
518
+ if (gridHighCell) gridHighCell.classList.remove(\"highlight\");
519
+ gridHighCell = null;
520
+ if (gridHighHeader) gridHighHeader.classList.remove(\"highlight\");
521
+ gridHighHeader = null;
522
+ });
523
+ function gridMove(evt) {
524
+ var lastHighCell = gridHighCell;
525
+ gridHighCell = document.elementFromPoint(evt.x, evt.y);
526
+ if (lastHighCell !== gridHighCell) {
527
+ gridHighCell.classList.add(\"highlight\");
528
+ if (lastHighCell) lastHighCell.classList.remove(\"highlight\");
529
+ }
530
+ var lastHighHeader = gridHighHeader;
531
+ gridHighHeader = headerCols[gridHighCell.cellIndex];
532
+ if (lastHighHeader !== gridHighHeader) {
533
+ if (gridHighHeader) gridHighHeader.classList.add(\"highlight\");
534
+ if (lastHighHeader) lastHighHeader.classList.remove(\"highlight\");
535
+ }
536
+ }
537
+ }
503
538
  function setHeaderSizes() {
504
539
  // console.log(\"start\");
505
540
  // See if the headerTop is already populated
@@ -531,6 +566,7 @@ function setHeaderSizes() {
531
566
  }
532
567
  }
533
568
  }
569
+ headerCols = tr.childNodes;
534
570
  if (isEmpty) headerTop.appendChild(tr);
535
571
  }
536
572
  grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
@@ -779,7 +815,8 @@ erDiagram
779
815
  cols[col_name] = col
780
816
  end
781
817
  unless @_brick_sequence # If no sequence is defined, start with all inclusions
782
- @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
818
+ cust_cols = #{model_name}._br_cust_cols
819
+ @_brick_sequence = col_keys + cust_cols.keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
783
820
  end
784
821
  @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
785
822
  @_brick_sequence.each_with_object(+'') do |col_name, s|
@@ -796,6 +833,8 @@ erDiagram
796
833
  elsif col # HM column
797
834
  s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
798
835
  s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1]._brick_index}_path\"))}\")
836
+ elsif (cc = cust_cols.key?(col_name)) # Custom column
837
+ s << \"<th x-order=\\\"#\{col_name}\\\">#\{col_name}\"
799
838
  else # Bad column name!
800
839
  s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
801
840
  end
@@ -813,7 +852,7 @@ erDiagram
813
852
  <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
814
853
  <% @_brick_sequence.each do |col_name|
815
854
  val = #{obj_name}.attributes[col_name] %>
816
- <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
855
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name) || (cust_col = cust_cols[col_name])%>><%
817
856
  if (bt = bts[col_name])
818
857
  if bt[2] # Polymorphic?
819
858
  bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
@@ -847,6 +886,9 @@ erDiagram
847
886
  elsif (col = cols[col_name])
848
887
  col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
849
888
  %><%= display_value(col_type || col&.sql_type, val) %><%
889
+ elsif cust_col
890
+ data = cust_col.first.map { |cc_part| #{obj_name}.send(cc_part.last) }
891
+ %><%= #{model_name}.brick_descrip(cust_col.last, data) %><%
850
892
  else # Bad column name!
851
893
  %>?<%
852
894
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 75
8
+ TINY = 76
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
data/lib/brick.rb CHANGED
@@ -329,6 +329,15 @@ module Brick
329
329
  end
330
330
  end
331
331
 
332
+ # Custom columns to add to a table, minimally defined with a name and DSL string.
333
+ # @api public
334
+ def custom_columns=(cust_cols)
335
+ if cust_cols
336
+ cust_cols = cust_cols.call if cust_cols.is_a?(Proc)
337
+ Brick.config.custom_columns = cust_cols
338
+ end
339
+ end
340
+
332
341
  # @api public
333
342
  def order=(value)
334
343
  Brick.config.order = value
@@ -207,6 +207,12 @@ module Brick
207
207
  # # to be the primary key.)
208
208
  #{bar}
209
209
 
210
+ # # Custom columns to add to a table, minimally defined with a name and DSL string.
211
+ # Brick.custom_columns = { 'users' => { messages: ['[COUNT(messages)] messages', 'messages'] },
212
+ # 'orders' => { salesperson: '[salesperson.first] [salesperson.last]',
213
+ # products: ['[COUNT(order_items.product)] products', 'order_items.product' ] }
214
+ # }
215
+
210
216
  # # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
211
217
  # # (Uses the same exact three-part format as would define an additional_reference)
212
218
  # # Say for instance that we didn't care to display the favourite colours that users have:
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.75
4
+ version: 1.0.76
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-30 00:00:00.000000000 Z
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord