brick 1.0.22 → 1.0.25
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 +4 -4
- data/lib/brick/config.rb +9 -0
- data/lib/brick/extensions.rb +252 -153
- data/lib/brick/frameworks/rails/engine.rb +189 -42
- data/lib/brick/version_number.rb +1 -1
- data/lib/brick.rb +26 -7
- data/lib/generators/brick/install_generator.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0dfa64d7a8a148b4c2ad990f5af1604d5855b80c6e95f39cd371a43a591b7f1
|
4
|
+
data.tar.gz: bc7644a08678136d91696a68e0ad670f4f04d41772f36ba1bd806ebe45f23b04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a39a8adc0c72288db5bd08e2483cefb701db9c2122fe1b4012c19bef07c7d048fed7f6e051473a9b4b6c80e321e4a781a16543d40f3e569f23c31f420a5117b6
|
7
|
+
data.tar.gz: 3ddcc914e3143a02a4582d6f525417c3012ae4eafc9ae889d5935223f6173afb4fb5ff3a40dc6c9caf95dfbf7a3c5be8e7bc4890efb385867ef5fa6f3ccbfb9e
|
data/lib/brick/config.rb
CHANGED
@@ -97,6 +97,15 @@ module Brick
|
|
97
97
|
@mutex.synchronize { @has_ones = hos }
|
98
98
|
end
|
99
99
|
|
100
|
+
# Polymorphic associations
|
101
|
+
def polymorphics
|
102
|
+
@mutex.synchronize { @polymorphics }
|
103
|
+
end
|
104
|
+
|
105
|
+
def polymorphics=(polys)
|
106
|
+
@mutex.synchronize { @polymorphics = polys }
|
107
|
+
end
|
108
|
+
|
100
109
|
def model_descrips
|
101
110
|
@mutex.synchronize { @model_descrips ||= {} }
|
102
111
|
end
|
data/lib/brick/extensions.rb
CHANGED
@@ -26,18 +26,13 @@
|
|
26
26
|
|
27
27
|
# colour coded origins
|
28
28
|
|
29
|
-
# Drag something like
|
29
|
+
# Drag something like HierModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
|
30
30
|
|
31
31
|
# Support for Postgres / MySQL enums (add enum to model, use model enums to make a drop-down in the UI)
|
32
32
|
|
33
33
|
# Currently quadrupling up routes
|
34
34
|
|
35
35
|
|
36
|
-
# From the North app:
|
37
|
-
# undefined method `built_in_role_path' when referencing show on a subclassed STI:
|
38
|
-
# http://localhost:3000/roles/3?_brick_schema=cust1
|
39
|
-
|
40
|
-
|
41
36
|
# ==========================================================
|
42
37
|
# Dynamically create model or controller classes when needed
|
43
38
|
# ==========================================================
|
@@ -57,6 +52,14 @@ module Arel
|
|
57
52
|
end
|
58
53
|
end
|
59
54
|
|
55
|
+
# module ActiveModel
|
56
|
+
# class NotNullValidator < EachValidator
|
57
|
+
# def validate_each(record, attribute, value)
|
58
|
+
# record.errors[attribute] << "must not be null" if value.nil?
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
# end
|
62
|
+
|
60
63
|
module ActiveRecord
|
61
64
|
class Base
|
62
65
|
def self._assoc_names
|
@@ -79,8 +82,8 @@ module ActiveRecord
|
|
79
82
|
dsl
|
80
83
|
end
|
81
84
|
|
82
|
-
# Pass in true or a JoinArray
|
83
|
-
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {})
|
85
|
+
# Pass in true for build_array, or just pass in a JoinArray
|
86
|
+
def self.brick_parse_dsl(build_array = nil, prefix = [], translations = {}, is_polymorphic = false)
|
84
87
|
build_array = ::Brick::JoinArray.new.tap { |ary| ary.replace([build_array]) } if build_array.is_a?(::Brick::JoinHash)
|
85
88
|
build_array = ::Brick::JoinArray.new unless build_array.nil? || build_array.is_a?(Array)
|
86
89
|
members = []
|
@@ -92,12 +95,17 @@ module ActiveRecord
|
|
92
95
|
if bracket_name
|
93
96
|
if ch == ']' # Time to process a bracketed thing?
|
94
97
|
parts = bracket_name.split('.')
|
95
|
-
first_parts = parts[0..-2].map
|
98
|
+
first_parts = parts[0..-2].map do |part|
|
99
|
+
klass = klass.reflect_on_association(part_sym = part.to_sym).klass
|
100
|
+
part_sym
|
101
|
+
end
|
96
102
|
parts = prefix + first_parts + [parts[-1]]
|
97
103
|
if parts.length > 1
|
98
|
-
|
99
|
-
|
100
|
-
|
104
|
+
unless is_polymorphic
|
105
|
+
s = build_array
|
106
|
+
parts[0..-3].each { |v| s = s[v.to_sym] }
|
107
|
+
s[parts[-2]] = nil # unless parts[-2].empty? # Using []= will "hydrate" any missing part(s) in our whole series
|
108
|
+
end
|
101
109
|
translations[parts[0..-2].join('.')] = klass
|
102
110
|
end
|
103
111
|
members << parts
|
@@ -120,8 +128,8 @@ module ActiveRecord
|
|
120
128
|
# If available, parse simple DSL attached to a model in order to provide a friendlier name.
|
121
129
|
# Object property names can be referenced in square brackets like this:
|
122
130
|
# { 'User' => '[profile.firstname] [profile.lastname]' }
|
123
|
-
def brick_descrip
|
124
|
-
self.class.brick_descrip(self)
|
131
|
+
def brick_descrip(data = nil, pk_alias = nil)
|
132
|
+
self.class.brick_descrip(self, data, pk_alias)
|
125
133
|
end
|
126
134
|
|
127
135
|
def self.brick_descrip(obj, data = nil, pk_alias = nil)
|
@@ -141,11 +149,7 @@ module ActiveRecord
|
|
141
149
|
this_obj = obj
|
142
150
|
bracket_name.split('.').each do |part|
|
143
151
|
obj_name += ".#{part}"
|
144
|
-
this_obj =
|
145
|
-
caches[obj_name]
|
146
|
-
else
|
147
|
-
(caches[obj_name] = this_obj&.send(part.to_sym))
|
148
|
-
end
|
152
|
+
this_obj = caches.fetch(obj_name) { caches[obj_name] = this_obj&.send(part.to_sym) }
|
149
153
|
end
|
150
154
|
this_obj&.to_s || ''
|
151
155
|
end
|
@@ -165,12 +169,17 @@ module ActiveRecord
|
|
165
169
|
end
|
166
170
|
if is_brackets_have_content
|
167
171
|
output
|
168
|
-
elsif pk_alias
|
169
|
-
|
172
|
+
elsif (pk_alias ||= primary_key)
|
173
|
+
pk_alias = [pk_alias] unless pk_alias.is_a?(Array)
|
174
|
+
id = []
|
175
|
+
pk_alias.each do |pk_alias_part|
|
176
|
+
if (pk_part = obj.send(pk_alias_part))
|
177
|
+
id << pk_part
|
178
|
+
end
|
179
|
+
end
|
180
|
+
if id.present?
|
170
181
|
"#{klass.name} ##{id.join(', ')}"
|
171
182
|
end
|
172
|
-
# elsif klass.primary_key
|
173
|
-
# "#{klass.name} ##{obj.send(klass.primary_key)}"
|
174
183
|
else
|
175
184
|
obj.to_s
|
176
185
|
end
|
@@ -184,10 +193,21 @@ module ActiveRecord
|
|
184
193
|
model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
|
185
194
|
end
|
186
195
|
|
196
|
+
def self.brick_import_template
|
197
|
+
template = constants.include?(:IMPORT_TEMPLATE) ? self::IMPORT_TEMPLATE : suggest_template(false, false, 0)
|
198
|
+
# Add the primary key to the template as being unique (unless it's already there)
|
199
|
+
template[:uniques] = [pk = primary_key.to_sym]
|
200
|
+
template[:all].unshift(pk) unless template[:all].include?(pk)
|
201
|
+
template
|
202
|
+
end
|
203
|
+
|
187
204
|
private
|
188
205
|
|
189
206
|
def self._brick_get_fks
|
190
|
-
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.
|
207
|
+
@_brick_get_fks ||= reflect_on_all_associations.select { |a2| a2.macro == :belongs_to }.each_with_object([]) do |bt, s|
|
208
|
+
s << bt.foreign_key
|
209
|
+
s << bt.foreign_type if bt.polymorphic?
|
210
|
+
end
|
191
211
|
end
|
192
212
|
end
|
193
213
|
|
@@ -289,7 +309,11 @@ module ActiveRecord
|
|
289
309
|
bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
|
290
310
|
bts.each do |_k, bt|
|
291
311
|
# join_array will receive this relation name when calling #brick_parse_dsl
|
292
|
-
bt_descrip[bt.first] =
|
312
|
+
bt_descrip[bt.first] = if bt[1].is_a?(Array)
|
313
|
+
bt[1].each_with_object({}) { |bt_class, s| s[bt_class] = bt_class.brick_parse_dsl(join_array, bt.first, translations, true) }
|
314
|
+
else
|
315
|
+
{ bt.last => bt[1].brick_parse_dsl(join_array, bt.first, translations) }
|
316
|
+
end
|
293
317
|
end
|
294
318
|
skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
|
295
319
|
hms.each do |k, hm|
|
@@ -307,7 +331,7 @@ module ActiveRecord
|
|
307
331
|
when 2
|
308
332
|
assoc_name = ks.first.to_sym
|
309
333
|
# Make sure it's a good association name and that the model has that column name
|
310
|
-
next unless klass.reflect_on_association(assoc_name)&.klass&.
|
334
|
+
next unless klass.reflect_on_association(assoc_name)&.klass&.column_names&.any?(ks.last)
|
311
335
|
|
312
336
|
join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
|
313
337
|
end
|
@@ -323,23 +347,31 @@ module ActiveRecord
|
|
323
347
|
id_for_tables = Hash.new { |h, k| h[k] = [] }
|
324
348
|
field_tbl_names = Hash.new { |h, k| h[k] = {} }
|
325
349
|
bt_columns = bt_descrip.each_with_object([]) do |v, s|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
id_for_tables[v.first] << id_alias
|
332
|
-
end
|
333
|
-
v.last << id_for_tables[v.first]
|
334
|
-
end
|
335
|
-
if (col_name = v.last[1].last&.last)
|
350
|
+
v.last.each do |k1, v1| # k1 is class, v1 is array of columns to snag
|
351
|
+
next if chains[k1].nil?
|
352
|
+
|
353
|
+
tbl_name = field_tbl_names[v.first][k1] ||= shift_or_first(chains[k1])
|
354
|
+
# if (col_name = v1[1].last&.last) # col_name is weak when there are multiple, using sel_col.last instead
|
336
355
|
field_tbl_name = nil
|
337
|
-
|
356
|
+
v1.map { |x|
|
357
|
+
[translations[x[0..-2].map(&:to_s).join('.')], x.last]
|
358
|
+
}.each_with_index do |sel_col, idx|
|
338
359
|
field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
|
339
|
-
|
360
|
+
|
340
361
|
selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
|
341
|
-
|
362
|
+
v1[idx] << col_alias
|
342
363
|
end
|
364
|
+
# end
|
365
|
+
|
366
|
+
if (id_col = k1.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
|
367
|
+
# Accommodate composite primary key by allowing id_col to come in as an array
|
368
|
+
(id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
|
369
|
+
selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
|
370
|
+
id_for_tables[v.first] << id_alias
|
371
|
+
end
|
372
|
+
v1 << id_for_tables[v.first]
|
373
|
+
end
|
374
|
+
|
343
375
|
end
|
344
376
|
end
|
345
377
|
join_array.each do |assoc_name|
|
@@ -354,25 +386,30 @@ module ActiveRecord
|
|
354
386
|
hm_counts.each do |k, hm|
|
355
387
|
associative = nil
|
356
388
|
count_column = if hm.options[:through]
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
389
|
+
fk_col = (associative = associatives[hm.name]).foreign_key
|
390
|
+
hm.foreign_key
|
391
|
+
else
|
392
|
+
fk_col = hm.foreign_key
|
393
|
+
poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
|
394
|
+
hm.klass.primary_key || '*'
|
395
|
+
end
|
363
396
|
tbl_alias = "_br_#{hm.name}"
|
364
397
|
pri_tbl = hm.active_record
|
398
|
+
on_clause = []
|
365
399
|
if fk_col.is_a?(Array) # Composite key?
|
366
|
-
on_clause = []
|
367
400
|
fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
|
368
|
-
|
369
|
-
JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
|
370
|
-
ON #{on_clause.join(' AND ')}")
|
401
|
+
selects = fk_col.dup
|
371
402
|
else
|
372
|
-
|
373
|
-
|
374
|
-
|
403
|
+
selects = [fk_col]
|
404
|
+
on_clause << "#{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}"
|
405
|
+
end
|
406
|
+
if poly_type
|
407
|
+
selects << poly_type
|
408
|
+
on_clause << "#{tbl_alias}.#{poly_type} = '#{name}'"
|
375
409
|
end
|
410
|
+
join_clause = "LEFT OUTER
|
411
|
+
JOIN (SELECT #{selects.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..selects.length).to_a.join(', ')}) AS #{tbl_alias}"
|
412
|
+
joins!("#{join_clause} ON #{on_clause.join(' AND ')}")
|
376
413
|
end
|
377
414
|
where!(wheres) unless wheres.empty?
|
378
415
|
wheres unless wheres.empty? # Return the specific parameters that we did use
|
@@ -469,7 +506,7 @@ class Object
|
|
469
506
|
# path_suffix = ActiveSupport::Dependencies.qualified_name_for(Object, args.first).underscore
|
470
507
|
# return self._brick_const_missing(*args) if ActiveSupport::Dependencies.search_for_file(path_suffix)
|
471
508
|
# If the file really exists, go and snag it:
|
472
|
-
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = self.name&.split('::'))
|
509
|
+
if !(is_found = ActiveSupport::Dependencies.search_for_file(class_name.underscore)) && (filepath = (self.name || class_name)&.split('::'))
|
473
510
|
filepath = (filepath[0..-2] + [class_name]).join('/').underscore + '.rb'
|
474
511
|
end
|
475
512
|
if is_found
|
@@ -582,112 +619,132 @@ class Object
|
|
582
619
|
hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
|
583
620
|
# The key in each hash entry (fk.first) is the constraint name
|
584
621
|
inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
590
|
-
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
591
|
-
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
592
|
-
end
|
593
|
-
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
594
|
-
else
|
595
|
-
assoc[:assoc_name]
|
596
|
-
end
|
597
|
-
need_class_name = singular_table_name.underscore != assoc_name
|
598
|
-
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
599
|
-
if (inverse = assoc[:inverse])
|
600
|
-
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
|
601
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
602
|
-
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
603
|
-
need_inverse_of = true
|
604
|
-
has_ones[singular_inv_assoc_name]
|
605
|
-
else
|
606
|
-
singular_inv_assoc_name
|
607
|
-
end
|
608
|
-
end
|
609
|
-
end
|
610
|
-
:belongs_to
|
611
|
-
else
|
612
|
-
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
613
|
-
# Are there multiple foreign keys out to the same table?
|
614
|
-
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
615
|
-
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
616
|
-
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
617
|
-
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
618
|
-
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
619
|
-
need_class_name = custom_assoc_name != singular_assoc_name
|
620
|
-
custom_assoc_name
|
621
|
-
else
|
622
|
-
singular_assoc_name
|
623
|
-
end
|
624
|
-
:has_one
|
625
|
-
else
|
626
|
-
:has_many
|
627
|
-
end
|
628
|
-
end
|
629
|
-
# Figure out if we need to specially call out the class_name and/or foreign key
|
630
|
-
# (and if either of those then definitely also a specific inverse_of)
|
631
|
-
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
632
|
-
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
633
|
-
if need_fk # Funky foreign key?
|
634
|
-
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
635
|
-
assoc_fk = assoc[:fk].uniq
|
636
|
-
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
637
|
-
else
|
638
|
-
assoc[:fk].to_sym
|
639
|
-
end
|
640
|
-
end
|
641
|
-
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
642
|
-
|
643
|
-
# Prepare a list of entries for "has_many :through"
|
644
|
-
if macro == :has_many
|
645
|
-
relations[assoc[:inverse_table]][:hmt_fks].each do |k, hmt_fk|
|
646
|
-
next if k == assoc[:fk]
|
647
|
-
|
648
|
-
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
649
|
-
end
|
622
|
+
if (invs = assoc[:inverse_table]).is_a?(Array)
|
623
|
+
invs.each { |inv| build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inv, code) }
|
624
|
+
else
|
625
|
+
build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, invs, code)
|
650
626
|
end
|
651
|
-
|
652
|
-
# And finally create a has_one, has_many, or belongs_to for this association
|
653
|
-
assoc_name = assoc_name.to_sym
|
654
|
-
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
655
|
-
self.send(macro, assoc_name, **options)
|
656
627
|
hmts
|
657
628
|
end
|
658
629
|
hmts.each do |hmt_fk, fks|
|
659
630
|
fks.each do |fk|
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
631
|
+
through = fk.first[:assoc_name]
|
632
|
+
hmt_name = if fks.length > 1
|
633
|
+
if fks[0].first[:inverse][:assoc_name] == fks[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
|
634
|
+
"#{hmt_fk}_through_#{fk.first[:assoc_name]}"
|
635
|
+
else # Use BT names to provide uniqueness
|
636
|
+
through = fk.first[:alternate_name].pluralize
|
637
|
+
singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
|
638
|
+
"#{singular_assoc_name}_#{hmt_fk}"
|
639
|
+
end
|
640
|
+
else
|
641
|
+
hmt_fk
|
642
|
+
end
|
643
|
+
source = fk.last unless hmt_name.singularize == fk.last
|
644
|
+
code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
|
672
645
|
options = { through: assoc_name }
|
673
646
|
options[:source] = source.to_sym if source
|
674
|
-
self.send(:has_many,
|
675
|
-
end
|
676
|
-
end
|
677
|
-
# Not NULLables
|
678
|
-
relation[:cols].each do |col, datatype|
|
679
|
-
if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
|
680
|
-
::Brick.config.not_nullables.include?("#{matching}.#{col}")
|
681
|
-
code << " validates :#{col}, presence: true\n"
|
682
|
-
self.send(:validates, col.to_sym, { presence: true })
|
647
|
+
self.send(:has_many, hmt_name.to_sym, **options)
|
683
648
|
end
|
684
649
|
end
|
650
|
+
# # Not NULLables
|
651
|
+
# # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
|
652
|
+
# relation[:cols].each do |col, datatype|
|
653
|
+
# if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
|
654
|
+
# ::Brick.config.not_nullables.include?("#{matching}.#{col}")
|
655
|
+
# code << " validates :#{col}, not_null: true\n"
|
656
|
+
# self.send(:validates, col.to_sym, { not_null: true })
|
657
|
+
# end
|
658
|
+
# end
|
685
659
|
end
|
686
660
|
code << "end # model #{model_name}\n\n"
|
687
661
|
end # class definition
|
688
662
|
[built_model, code]
|
689
663
|
end
|
690
664
|
|
665
|
+
def build_bt_or_hm(relations, model_name, relation, hmts, assoc, inverse_assoc_name, inverse_table, code)
|
666
|
+
singular_table_name = inverse_table&.singularize
|
667
|
+
options = {}
|
668
|
+
macro = if assoc[:is_bt]
|
669
|
+
# Try to take care of screwy names if this is a belongs_to going to an STI subclass
|
670
|
+
assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
|
671
|
+
sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
|
672
|
+
a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
|
673
|
+
end
|
674
|
+
sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
|
675
|
+
else
|
676
|
+
assoc[:assoc_name]
|
677
|
+
end
|
678
|
+
if assoc.key?(:polymorphic)
|
679
|
+
options[:polymorphic] = true
|
680
|
+
else
|
681
|
+
need_class_name = singular_table_name.underscore != assoc_name
|
682
|
+
need_fk = "#{assoc_name}_id" != assoc[:fk]
|
683
|
+
end
|
684
|
+
if (inverse = assoc[:inverse])
|
685
|
+
inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[inverse_table], inverse)
|
686
|
+
has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil)
|
687
|
+
if has_ones&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
|
688
|
+
inverse_assoc_name = if has_ones[singular_inv_assoc_name]
|
689
|
+
need_inverse_of = true
|
690
|
+
has_ones[singular_inv_assoc_name]
|
691
|
+
else
|
692
|
+
singular_inv_assoc_name
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
:belongs_to
|
697
|
+
else
|
698
|
+
# need_class_name = ActiveSupport::Inflector.singularize(assoc_name) == ActiveSupport::Inflector.singularize(table_name.underscore)
|
699
|
+
# Are there multiple foreign keys out to the same table?
|
700
|
+
assoc_name, need_class_name = _brick_get_hm_assoc_name(relation, assoc)
|
701
|
+
# binding.pry if assoc.key?(:polymorphic)
|
702
|
+
if assoc.key?(:polymorphic)
|
703
|
+
options[:as] = assoc[:fk].to_sym if assoc.key?(:polymorphic)
|
704
|
+
else
|
705
|
+
need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
|
706
|
+
end
|
707
|
+
# fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
|
708
|
+
if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
|
709
|
+
assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
|
710
|
+
need_class_name = custom_assoc_name != singular_assoc_name
|
711
|
+
custom_assoc_name
|
712
|
+
else
|
713
|
+
singular_assoc_name
|
714
|
+
end
|
715
|
+
:has_one
|
716
|
+
else
|
717
|
+
:has_many
|
718
|
+
end
|
719
|
+
end
|
720
|
+
# Figure out if we need to specially call out the class_name and/or foreign key
|
721
|
+
# (and if either of those then definitely also a specific inverse_of)
|
722
|
+
options[:class_name] = assoc[:primary_class]&.name || singular_table_name.camelize if need_class_name
|
723
|
+
# Work around a bug in CPK where self-referencing belongs_to associations double up their foreign keys
|
724
|
+
if need_fk # Funky foreign key?
|
725
|
+
options[:foreign_key] = if assoc[:fk].is_a?(Array)
|
726
|
+
assoc_fk = assoc[:fk].uniq
|
727
|
+
assoc_fk.length < 2 ? assoc_fk.first : assoc_fk
|
728
|
+
else
|
729
|
+
assoc[:fk].to_sym
|
730
|
+
end
|
731
|
+
end
|
732
|
+
options[:inverse_of] = inverse_assoc_name.to_sym if inverse_assoc_name && (need_class_name || need_fk || need_inverse_of)
|
733
|
+
|
734
|
+
# Prepare a list of entries for "has_many :through"
|
735
|
+
if macro == :has_many
|
736
|
+
relations[inverse_table][:hmt_fks].each do |k, hmt_fk|
|
737
|
+
next if k == assoc[:fk]
|
738
|
+
|
739
|
+
hmts[ActiveSupport::Inflector.pluralize(hmt_fk.last)] << [assoc, hmt_fk.first]
|
740
|
+
end
|
741
|
+
end
|
742
|
+
# And finally create a has_one, has_many, or belongs_to for this association
|
743
|
+
assoc_name = assoc_name.to_sym
|
744
|
+
code << " #{macro} #{assoc_name.inspect}#{options.map { |k, v| ", #{k}: #{v.inspect}" }.join}\n"
|
745
|
+
self.send(macro, assoc_name, **options)
|
746
|
+
end
|
747
|
+
|
691
748
|
def build_controller(class_name, plural_class_name, model, relations)
|
692
749
|
table_name = ActiveSupport::Inflector.underscore(plural_class_name)
|
693
750
|
singular_table_name = ActiveSupport::Inflector.singularize(table_name)
|
@@ -700,9 +757,22 @@ class Object
|
|
700
757
|
code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
|
701
758
|
code << " @#{table_name}.brick_select(params)\n"
|
702
759
|
code << " end\n"
|
760
|
+
self.protect_from_forgery unless: -> { self.request.format.js? }
|
703
761
|
self.define_method :index do
|
704
762
|
::Brick.set_db_schema(params)
|
705
|
-
|
763
|
+
if request.format == :csv # Asking for a template?
|
764
|
+
require 'csv'
|
765
|
+
exported_csv = CSV.generate(force_quotes: false) do |csv_out|
|
766
|
+
model.df_export(model.brick_import_template).each { |row| csv_out << row }
|
767
|
+
end
|
768
|
+
render inline: exported_csv, content_type: request.format
|
769
|
+
return
|
770
|
+
elsif request.format == :js # Asking for JSON?
|
771
|
+
render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
|
772
|
+
return
|
773
|
+
end
|
774
|
+
|
775
|
+
ar_relation = model.primary_key ? model.order("#{model.table_name}.#{model.primary_key}") : model.all
|
706
776
|
@_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
|
707
777
|
# %%% Add custom HM count columns
|
708
778
|
# %%% What happens when the PK is composite?
|
@@ -739,6 +809,22 @@ class Object
|
|
739
809
|
code << " end\n"
|
740
810
|
self.define_method :update do
|
741
811
|
::Brick.set_db_schema(params)
|
812
|
+
|
813
|
+
if request.format == :csv # Importing CSV?
|
814
|
+
require 'csv'
|
815
|
+
# See if internally it's likely a TSV file (tab-separated)
|
816
|
+
tab_counts = []
|
817
|
+
5.times { tab_counts << request.body.readline.count("\t") unless request.body.eof? }
|
818
|
+
request.body.rewind
|
819
|
+
separator = "\t" if tab_counts.length > 0 && tab_counts.uniq.length == 1 && tab_counts.first > 0
|
820
|
+
result = model.df_import(CSV.parse(request.body, { col_sep: separator || :auto }), model.brick_import_template)
|
821
|
+
# render inline: exported_csv, content_type: request.format
|
822
|
+
return
|
823
|
+
# elsif request.format == :js # Asking for JSON?
|
824
|
+
# render inline: model.df_export(true).to_json, content_type: request.format
|
825
|
+
# return
|
826
|
+
end
|
827
|
+
|
742
828
|
instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
|
743
829
|
obj = obj.first if obj.is_a?(Array)
|
744
830
|
obj.send(:update, send(params_name = params_name.to_sym))
|
@@ -971,14 +1057,19 @@ module Brick
|
|
971
1057
|
# rubocop:enable Style/CommentedKeyword
|
972
1058
|
|
973
1059
|
class << self
|
974
|
-
def _add_bt_and_hm(fk, relations =
|
975
|
-
relations ||= ::Brick.relations
|
1060
|
+
def _add_bt_and_hm(fk, relations, is_polymorphic = false)
|
976
1061
|
bt_assoc_name = fk[1].underscore
|
977
1062
|
bt_assoc_name = bt_assoc_name[0..-4] if bt_assoc_name.end_with?('_id')
|
978
1063
|
|
979
1064
|
bts = (relation = relations.fetch(fk[0], nil))&.fetch(:fks) { relation[:fks] = {} }
|
980
|
-
|
981
|
-
|
1065
|
+
# %%% Do we miss out on has_many :through or even HM based on constantizing this model early?
|
1066
|
+
# Maybe it's already gotten this info because we got as far as to say there was a unique class
|
1067
|
+
# if is_polymorphic
|
1068
|
+
# primary_table = fk[]
|
1069
|
+
# else
|
1070
|
+
primary_table = (is_class = fk[2].is_a?(Hash) && fk[2].key?(:class)) ? (primary_class = fk[2][:class].constantize).table_name : fk[2]
|
1071
|
+
hms = (relation = relations.fetch(primary_table, nil))&.fetch(:fks) { relation[:fks] = {} } unless is_class
|
1072
|
+
# end
|
982
1073
|
|
983
1074
|
unless (cnstr_name = fk[3])
|
984
1075
|
# For any appended references (those that come from config), arrive upon a definitely unique constraint name
|
@@ -993,7 +1084,7 @@ module Brick
|
|
993
1084
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent #{'table'.pluralize(missing.length)} #{missing.join(' and ')}. (Available tables include #{tables.join(', ')}.)"
|
994
1085
|
return
|
995
1086
|
end
|
996
|
-
unless (cols = relations[fk[0]][:cols]).key?(fk[1])
|
1087
|
+
unless (cols = relations[fk[0]][:cols]).key?(fk[1]) || (is_polymorphic && cols.key?("#{fk[1]}_id") && cols.key?("#{fk[1]}_type"))
|
997
1088
|
columns = cols.map { |k, v| "#{k} (#{v.first.split(' ').first})" }
|
998
1089
|
puts "Brick: Additional reference #{fk.inspect} refers to non-existent column #{fk[1]}. (Columns present in #{fk[0]} are #{columns.join(', ')}.)"
|
999
1090
|
return
|
@@ -1008,10 +1099,17 @@ module Brick
|
|
1008
1099
|
end
|
1009
1100
|
end
|
1010
1101
|
if (assoc_bt = bts[cnstr_name])
|
1011
|
-
|
1012
|
-
|
1102
|
+
if is_polymorphic
|
1103
|
+
# Assuming same fk (don't yet support composite keys for polymorphics)
|
1104
|
+
assoc_bt[:inverse_table] << fk[2]
|
1105
|
+
else # Expect we've got a composite key going
|
1106
|
+
assoc_bt[:fk] = assoc_bt[:fk].is_a?(String) ? [assoc_bt[:fk], fk[1]] : assoc_bt[:fk].concat(fk[1])
|
1107
|
+
assoc_bt[:assoc_name] = "#{assoc_bt[:assoc_name]}_#{fk[1]}"
|
1108
|
+
end
|
1013
1109
|
else
|
1014
|
-
|
1110
|
+
inverse_table = [primary_table] if is_polymorphic
|
1111
|
+
assoc_bt = bts[cnstr_name] = { is_bt: true, fk: fk[1], assoc_name: bt_assoc_name, inverse_table: inverse_table || primary_table }
|
1112
|
+
assoc_bt[:polymorphic] = true if is_polymorphic
|
1015
1113
|
end
|
1016
1114
|
if is_class
|
1017
1115
|
# For use in finding the proper :source for a HMT association that references an STI subclass
|
@@ -1028,6 +1126,7 @@ module Brick
|
|
1028
1126
|
assoc_hm[:inverse] = assoc_bt
|
1029
1127
|
else
|
1030
1128
|
assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
|
1129
|
+
assoc_hm[:polymorphic] = true if is_polymorphic
|
1031
1130
|
hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
|
1032
1131
|
hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
|
1033
1132
|
end
|
@@ -38,6 +38,9 @@ module Brick
|
|
38
38
|
|
39
39
|
# Has one relationships
|
40
40
|
::Brick.has_ones = app.config.brick.fetch(:has_ones, nil)
|
41
|
+
|
42
|
+
# Polymorphic associations
|
43
|
+
::Brick.polymorphics = app.config.brick.fetch(:polymorphics, nil)
|
41
44
|
end
|
42
45
|
|
43
46
|
# After we're initialized and before running the rest of stuff, put our configuration in place
|
@@ -64,12 +67,14 @@ module Brick
|
|
64
67
|
is_template_exists
|
65
68
|
end
|
66
69
|
|
67
|
-
def path_keys(fk_name, obj_name, pk)
|
68
|
-
if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
def path_keys(hm_assoc, fk_name, obj_name, pk)
|
71
|
+
keys = if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
|
72
|
+
fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
|
73
|
+
else
|
74
|
+
[[fk_name, "#{obj_name}.#{pk}"]]
|
75
|
+
end
|
76
|
+
keys << [hm_assoc.inverse_of.foreign_type, "#{hm_assoc.active_record.name}"] if hm_assoc.options.key?(:as)
|
77
|
+
keys.map { |x| "#{x.first}: #{x.last}"}.join(', ')
|
73
78
|
end
|
74
79
|
|
75
80
|
alias :_brick_find_template :find_template
|
@@ -80,6 +85,7 @@ module Brick
|
|
80
85
|
pk = @_brick_model.primary_key
|
81
86
|
obj_name = model_name.underscore
|
82
87
|
table_name = model_name.pluralize.underscore
|
88
|
+
template_link = nil
|
83
89
|
bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
|
84
90
|
hms_columns = [] # Used for 'index'
|
85
91
|
skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
|
@@ -96,15 +102,17 @@ module Brick
|
|
96
102
|
set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
|
97
103
|
'nil'
|
98
104
|
else
|
99
|
-
|
105
|
+
# Postgres column names are limited to 63 characters
|
106
|
+
attrib_name = "_br_#{assoc_name}_ct"[0..62]
|
107
|
+
"#{obj_name}.#{attrib_name} || 0"
|
100
108
|
end
|
101
109
|
"<%= ct = #{set_ct}
|
102
|
-
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
110
|
+
link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
|
103
111
|
else # has_one
|
104
112
|
"<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
|
105
113
|
end
|
106
114
|
elsif args.first == 'show'
|
107
|
-
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
115
|
+
hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
|
108
116
|
end
|
109
117
|
s << hm_stuff
|
110
118
|
end
|
@@ -115,6 +123,13 @@ module Brick
|
|
115
123
|
table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
|
116
124
|
.each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
|
117
125
|
css = +"<style>
|
126
|
+
#dropper {
|
127
|
+
background-color: #eee;
|
128
|
+
}
|
129
|
+
#btnImport {
|
130
|
+
display: none;
|
131
|
+
}
|
132
|
+
|
118
133
|
table {
|
119
134
|
border-collapse: collapse;
|
120
135
|
margin: 25px 0;
|
@@ -195,7 +210,16 @@ def hide_bcrypt(val)
|
|
195
210
|
end %>"
|
196
211
|
|
197
212
|
if ['index', 'show', 'update'].include?(args.first)
|
198
|
-
css << "<% bts = { #{
|
213
|
+
css << "<% bts = { #{
|
214
|
+
bts.each_with_object([]) do |v, s|
|
215
|
+
foreign_models = if v.last[1].is_a?(Array)
|
216
|
+
v.last[1].each_with_object([]) { |x, s| s << "[#{x.name}, #{x.primary_key.inspect}]" }.join(', ')
|
217
|
+
else
|
218
|
+
"[#{v.last[1].name}, #{v.last[1].primary_key.inspect}]"
|
219
|
+
end
|
220
|
+
s << "#{v.first.inspect} => [#{v.last.first.inspect}, [#{foreign_models}]]"
|
221
|
+
end.join(', ')
|
222
|
+
} } %>"
|
199
223
|
end
|
200
224
|
|
201
225
|
# %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
|
@@ -260,11 +284,102 @@ function changeout(href, param, value) {
|
|
260
284
|
elsif pk
|
261
285
|
"#{obj_name}.#{pk}"
|
262
286
|
end
|
287
|
+
if Object.const_defined?('DutyFree')
|
288
|
+
template_link = "
|
289
|
+
<%= link_to 'CSV', #{table_name}_path(format: :csv) %> <a href=\"#\" id=\"sheetsLink\">Sheets</a>
|
290
|
+
<div id=\"dropper\" contenteditable=\"true\"></div>
|
291
|
+
<input type=\"button\" id=\"btnImport\" value=\"Import\">
|
292
|
+
|
293
|
+
<script>
|
294
|
+
var dropperDiv = document.getElementById(\"dropper\");
|
295
|
+
var btnImport = document.getElementById(\"btnImport\");
|
296
|
+
var droppedTSV;
|
297
|
+
if (dropperDiv) { // Other interesting events: blur keyup input
|
298
|
+
dropperDiv.addEventListener(\"paste\", function (evt) {
|
299
|
+
droppedTSV = evt.clipboardData.getData('text/plain');
|
300
|
+
var html = evt.clipboardData.getData('text/html');
|
301
|
+
var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
|
302
|
+
console.log(tbl);
|
303
|
+
btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
|
304
|
+
});
|
305
|
+
btnImport.addEventListener(\"click\", function () {
|
306
|
+
fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
|
307
|
+
method: 'PATCH',
|
308
|
+
headers: { 'Content-Type': 'text/tab-separated-values' },
|
309
|
+
body: droppedTSV
|
310
|
+
}).then(function (tsvResponse) {
|
311
|
+
btnImport.style.display = \"none\";
|
312
|
+
console.log(\"toaster\", tsvResponse);
|
313
|
+
});
|
314
|
+
});
|
315
|
+
}
|
316
|
+
var sheetUrl;
|
317
|
+
var spreadsheetId;
|
318
|
+
var sheetsLink = document.getElementById(\"sheetsLink\");
|
319
|
+
function gapiLoaded() {
|
320
|
+
// Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
|
321
|
+
sheetsLink.addEventListener(\"click\", async function (evt) {
|
322
|
+
evt.preventDefault();
|
323
|
+
await gapi.load(\"client\", function () {
|
324
|
+
gapi.client.init({ // Load the discovery doc to initialize the API
|
325
|
+
clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
|
326
|
+
scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
|
327
|
+
discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
|
328
|
+
}).then(function () {
|
329
|
+
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
|
330
|
+
updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
|
331
|
+
});
|
332
|
+
});
|
333
|
+
});
|
334
|
+
}
|
335
|
+
|
336
|
+
async function updateSignInStatus(isSignedIn) {
|
337
|
+
if (isSignedIn) {
|
338
|
+
console.log(\"turds!\");
|
339
|
+
await gapi.client.sheets.spreadsheets.create({
|
340
|
+
properties: {
|
341
|
+
title: #{table_name.inspect},
|
342
|
+
},
|
343
|
+
sheets: [
|
344
|
+
// sheet1, sheet2, sheet3
|
345
|
+
]
|
346
|
+
}).then(function (response) {
|
347
|
+
sheetUrl = response.result.spreadsheetUrl;
|
348
|
+
spreadsheetId = response.result.spreadsheetId;
|
349
|
+
sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
|
350
|
+
console.log(\"x1\", sheetUrl);
|
351
|
+
|
352
|
+
// Get JSON data
|
353
|
+
fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
|
354
|
+
response.json().then(function (data) {
|
355
|
+
gapi.client.sheets.spreadsheets.values.append({
|
356
|
+
spreadsheetId: spreadsheetId,
|
357
|
+
range: \"Sheet1\",
|
358
|
+
valueInputOption: \"RAW\",
|
359
|
+
insertDataOption: \"INSERT_ROWS\"
|
360
|
+
}, {
|
361
|
+
range: \"Sheet1\",
|
362
|
+
majorDimension: \"ROWS\",
|
363
|
+
values: data,
|
364
|
+
}).then(function (response2) {
|
365
|
+
// console.log(\"beefcake\", response2);
|
366
|
+
});
|
367
|
+
});
|
368
|
+
});
|
369
|
+
});
|
370
|
+
window.open(sheetUrl, '_blank');
|
371
|
+
}
|
372
|
+
}
|
373
|
+
</script>
|
374
|
+
<script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
|
375
|
+
"
|
376
|
+
end
|
263
377
|
"#{css}
|
264
378
|
<p style=\"color: green\"><%= notice %></p>#{"
|
265
379
|
<select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
|
266
380
|
<select id=\"tbl\">#{table_options}</select>
|
267
|
-
<h1>#{model_name.pluralize}</h1
|
381
|
+
<h1>#{model_name.pluralize}</h1>#{template_link}
|
382
|
+
|
268
383
|
<% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
|
269
384
|
<table id=\"#{table_name}\">
|
270
385
|
<thead><tr>#{'<th></th>' if pk}
|
@@ -272,10 +387,13 @@ function changeout(href, param, value) {
|
|
272
387
|
<% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
|
273
388
|
<th>
|
274
389
|
<% if (bt = bts[col]) %>
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
390
|
+
BT <%
|
391
|
+
bt[1].each do |bt_pair| %><%=
|
392
|
+
bt_pair.first.bt_link(bt.first) %> <%
|
393
|
+
end %><%
|
394
|
+
else %><%=
|
395
|
+
col %><%
|
396
|
+
end %>
|
279
397
|
</th>
|
280
398
|
<% end %>
|
281
399
|
<%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
|
@@ -285,23 +403,32 @@ function changeout(href, param, value) {
|
|
285
403
|
<tbody>
|
286
404
|
<% @#{table_name}.each do |#{obj_name}| %>
|
287
405
|
<tr>#{"
|
288
|
-
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if
|
406
|
+
<td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
|
289
407
|
<% #{obj_name}.attributes.each do |k, val| %>
|
290
|
-
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
|
408
|
+
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && (k.length == 63 || k.end_with?('_ct'))) %>
|
291
409
|
<td>
|
292
410
|
<% if (bt = bts[k]) %>
|
293
|
-
<%# binding.pry # Postgres column names are limited to 63 characters
|
294
|
-
<%
|
295
|
-
|
296
|
-
|
297
|
-
|
411
|
+
<%# binding.pry # Postgres column names are limited to 63 characters %>
|
412
|
+
<% if (pairs = bt[1].length > 1)
|
413
|
+
bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
|
414
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
415
|
+
poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
|
416
|
+
%><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
|
417
|
+
send(\"#\{bt_class.underscore\}_path\".to_sym, poly_id)) if poly_id %><%
|
418
|
+
else # We should do something other than [0..-2] for when there is no primary key (or maybe have an empty final array there in that case?)
|
419
|
+
bt_txt = (bt_class = bt[1].first.first).brick_descrip(
|
420
|
+
#{obj_name}, (descrips = @_brick_bt_descrip[bt.first][bt_class])[0..-2].map { |z| #{obj_name}.send(z.last[0..62]) }, (bt_id_col = descrips.last)
|
421
|
+
)
|
422
|
+
bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
|
423
|
+
<%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
|
424
|
+
<%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
|
425
|
+
<% end %>
|
298
426
|
<% else %>
|
299
427
|
<%= hide_bcrypt(val) %>
|
300
428
|
<% end %>
|
301
429
|
</td>
|
302
430
|
<% end %>
|
303
|
-
|
304
|
-
<!-- td>X</td -->
|
431
|
+
#{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
|
305
432
|
</tr>
|
306
433
|
</tbody>
|
307
434
|
<% end %>
|
@@ -318,33 +445,47 @@ function changeout(href, param, value) {
|
|
318
445
|
<%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
|
319
446
|
<% if obj %>
|
320
447
|
<%= # path_options = [obj.#{pk}]
|
321
|
-
|
322
|
-
|
323
|
-
|
448
|
+
# path_options << { '_brick_schema': } if
|
449
|
+
# url = send(:#{model_name.underscore}_path, obj.#{pk})
|
450
|
+
form_for(obj.becomes(#{model_name})) do |f| %>
|
324
451
|
<table>
|
325
|
-
<%
|
452
|
+
<% has_fields = false
|
453
|
+
@#{obj_name}.first.attributes.each do |k, val| %>
|
326
454
|
<tr>
|
455
|
+
<%# %%% Accommodate composite keys %>
|
327
456
|
<% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
|
328
457
|
<th class=\"show-field\">
|
329
|
-
<%
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
458
|
+
<% has_fields = true
|
459
|
+
if (bt = bts[k])
|
460
|
+
# Add a final member in this array with descriptive options to be used in <select> drop-downs
|
461
|
+
bt_name = bt[1].map { |x| x.first.name }.join('/')
|
462
|
+
# %%% Only do this if the user has permissions to edit this bt field
|
463
|
+
if (pairs = bt[1]).length > 1
|
464
|
+
poly_class_name = @#{obj_name}.first.send(\"#\{bt.first\}_type\")
|
465
|
+
bt_pair = pairs.find { |pair| pair.first.name == poly_class_name }
|
466
|
+
# descrips = @_brick_bt_descrip[bt.first][bt_class]
|
467
|
+
poly_id = @#{obj_name}.first.send(\"#\{bt.first\}_id\")
|
468
|
+
# bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
469
|
+
else # No polymorphism, so just get the first one
|
470
|
+
bt_pair = bt[1].first
|
471
|
+
end
|
472
|
+
bt_class = bt_pair.first
|
473
|
+
if bt.length < 3
|
474
|
+
bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
|
475
|
+
# %%% Accommodate composite keys for obj.pk at the end here
|
476
|
+
bt_class.order(obj_pk = bt_class.primary_key).each { |obj| option_detail << [obj.brick_descrip(nil, obj_pk), obj.send(obj_pk)] }
|
477
|
+
end %>
|
478
|
+
BT <%= bt_class.bt_link(bt.first) %>
|
338
479
|
<% else %>
|
339
480
|
<%= k %>
|
340
481
|
<% end %>
|
341
482
|
</th>
|
342
483
|
<td>
|
343
|
-
<% if
|
484
|
+
<% if bt
|
344
485
|
html_options = { prompt: \"Select #\{bt_name\}\" }
|
345
486
|
html_options[:class] = 'dimmed' unless val %>
|
346
|
-
<%= f.select k.to_sym, bt[
|
347
|
-
<%= bt_obj =
|
487
|
+
<%= f.select k.to_sym, bt[2], { value: val || '^^^brick_NULL^^^' }, html_options %>
|
488
|
+
<%= bt_obj = bt_class.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.name.underscore\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
|
348
489
|
<% else case #{model_name}.column_for_attribute(k).type
|
349
490
|
when :string, :text %>
|
350
491
|
<% if is_bcrypt?(val) # || .readonly? %>
|
@@ -365,8 +506,12 @@ function changeout(href, param, value) {
|
|
365
506
|
<% end %>
|
366
507
|
</td>
|
367
508
|
</tr>
|
368
|
-
|
509
|
+
<% end
|
510
|
+
if has_fields %>
|
369
511
|
<tr><td colspan=\"2\" class=\"right\"><%= f.submit %></td></tr>
|
512
|
+
<% else %>
|
513
|
+
<tr><td colspan=\"2\">(No displayable fields)</td></tr>
|
514
|
+
<% end %>
|
370
515
|
</table>
|
371
516
|
<% end %>
|
372
517
|
|
@@ -380,6 +525,7 @@ function changeout(href, param, value) {
|
|
380
525
|
<tr><td>(none)</td></tr>
|
381
526
|
<% else %>
|
382
527
|
<% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
|
528
|
+
<%# %%% accommodate composite primary key %>
|
383
529
|
<tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
|
384
530
|
<% end %>
|
385
531
|
<% end %>
|
@@ -392,6 +538,7 @@ function changeout(href, param, value) {
|
|
392
538
|
#{script}"
|
393
539
|
|
394
540
|
end
|
541
|
+
puts inline
|
395
542
|
# As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
|
396
543
|
keys = options.has_key?(:locals) ? options[:locals].keys : []
|
397
544
|
handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
|
@@ -408,7 +555,7 @@ function changeout(href, param, value) {
|
|
408
555
|
end
|
409
556
|
|
410
557
|
# Just in case it hadn't been done previously when we tried to load the brick initialiser,
|
411
|
-
# go make sure we've loaded additional references (virtual foreign keys).
|
558
|
+
# go make sure we've loaded additional references (virtual foreign keys and polymorphic associations).
|
412
559
|
::Brick.load_additional_references
|
413
560
|
end
|
414
561
|
end
|
data/lib/brick/version_number.rb
CHANGED
data/lib/brick.rb
CHANGED
@@ -103,11 +103,17 @@ module Brick
|
|
103
103
|
|
104
104
|
def get_bts_and_hms(model)
|
105
105
|
bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
|
106
|
-
next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
|
106
|
+
next if (!const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name))
|
107
107
|
|
108
108
|
case a.macro
|
109
109
|
when :belongs_to
|
110
|
-
s.first[a.foreign_key] =
|
110
|
+
s.first[a.foreign_key] = if a.polymorphic?
|
111
|
+
primary_tables = relations[model.table_name][:fks].find { |_k, fk| fk[:assoc_name] == a.name.to_s }&.last&.fetch(:inverse_table, [])
|
112
|
+
models = primary_tables&.map { |table| table.singularize.camelize.constantize }
|
113
|
+
[a.name, models]
|
114
|
+
else
|
115
|
+
[a.name, a.klass]
|
116
|
+
end
|
111
117
|
when :has_many, :has_one # This gets has_many as well as has_many :through
|
112
118
|
# %%% weed out ones that don't have an available model to reference
|
113
119
|
s.last[a.name] = a
|
@@ -126,9 +132,7 @@ module Brick
|
|
126
132
|
skip_hms[hmt.last.name] = nil
|
127
133
|
end
|
128
134
|
end
|
129
|
-
skip_hms.each
|
130
|
-
puts hms.delete(k).inspect
|
131
|
-
end
|
135
|
+
skip_hms.each { |k, _v| hms.delete(k) }
|
132
136
|
[bts, hms, associatives]
|
133
137
|
end
|
134
138
|
|
@@ -258,6 +262,11 @@ module Brick
|
|
258
262
|
end
|
259
263
|
end
|
260
264
|
|
265
|
+
# Polymorphic associations
|
266
|
+
def polymorphics=(polys)
|
267
|
+
Brick.config.polymorphics = polys || {}
|
268
|
+
end
|
269
|
+
|
261
270
|
# DSL templates for individual models to provide prettier descriptions of objects
|
262
271
|
# @api public
|
263
272
|
def model_descrips=(descrips)
|
@@ -276,8 +285,18 @@ module Brick
|
|
276
285
|
def load_additional_references
|
277
286
|
return if @_additional_references_loaded
|
278
287
|
|
279
|
-
|
280
|
-
|
288
|
+
relations = ::Brick.relations
|
289
|
+
if (ars = ::Brick.config.additional_references) || ::Brick.config.polymorphics
|
290
|
+
ars.each { |fk| ::Brick._add_bt_and_hm(fk[0..2], relations) } if ars
|
291
|
+
if (polys = ::Brick.config.polymorphics)
|
292
|
+
polys.each do |k, v|
|
293
|
+
table_name, poly = k.split('.')
|
294
|
+
v ||= ActiveRecord::Base.execute_sql("SELECT DISTINCT #{poly}_type AS typ FROM #{table_name}").map { |result| result['typ'] }
|
295
|
+
v.each do |type|
|
296
|
+
::Brick._add_bt_and_hm([table_name, poly, type.underscore.pluralize, "(brick) #{table_name}_#{poly}"], relations, true)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
281
300
|
@_additional_references_loaded = true
|
282
301
|
end
|
283
302
|
|
@@ -157,6 +157,10 @@ Brick.skip_index_hms = ['User.litany_of_woes']
|
|
157
157
|
# Brick.sti_namespace_prefixes = { '::Animals::' => 'Animal',
|
158
158
|
# '::Snake' => 'Reptile' }
|
159
159
|
|
160
|
+
# # Polymorphic associations must be explicitly specified, which is as easy as providing a model name and polymorphic
|
161
|
+
# # association name like this:
|
162
|
+
# Brick.polymorphics = ['Comment.commentable', 'Image.imageable']
|
163
|
+
|
160
164
|
# # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
|
161
165
|
# # route to go to the :index action for what would be a controller for that table. You can specify any controller
|
162
166
|
# # name and action you wish in order to override this and have that be the default route when none other has been
|
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.
|
4
|
+
version: 1.0.25
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-05-
|
11
|
+
date: 2022-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|