brick 1.0.39 → 1.0.42

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: 965d31143248d85fd47cf2445a266fb9d26cc5eb814330cbdbfb37d50570b5a0
4
- data.tar.gz: d4f070e1c01b3922a610a4385fde6f6b4f345892d68d6b872e1294a33f4b1e61
3
+ metadata.gz: a385c606282392cbefea1eaa045dc6a023bd05f56044013bc3835ef06a630b87
4
+ data.tar.gz: 261e21a2eb02ac1b199c831e1bba5eeec3add7b5faf0b2567fbb94a3252d817f
5
5
  SHA512:
6
- metadata.gz: 70ea705efad428abb335e05569284caabe908c65efd679fffcd044894083626ca432366a65305bcd50506a7ca13169bac9cc2f7354728c3b8f23e4ed0aba7094
7
- data.tar.gz: b61096e6deadf323cc8784bcf93ae5faf72eae02fef3059b002c57bac035b30858da73bb59bd2f0572168a90b39062645580cd120e1d78665f5c909258727ad8
6
+ metadata.gz: 686a6ad73671511c36c6871e86fd6d02f7b8b48b2e1bacb033ec5348a85cedfc1c6848c92293e4144c0ca95ab79888c5993dcdbb7f440d93265a6c65b5c74a6b
7
+ data.tar.gz: 04e0cb25404beb9fead5d07ff3c76066bc23d097fb2a01c53b689737e1a5d70cfef94b432b89f45f61497074171bb171fae0064aef970c2c025f3ce05de23557
data/lib/brick/config.rb CHANGED
@@ -205,5 +205,10 @@ module Brick
205
205
  def not_nullables=(columns)
206
206
  @mutex.synchronize { @not_nullables = columns }
207
207
  end
208
+
209
+ # Add a special page to show references to non-existent records ("orphans")
210
+ def add_orphans
211
+ true
212
+ end
208
213
  end
209
214
  end
@@ -70,7 +70,11 @@ module ActiveRecord
70
70
  def self._brick_primary_key(relation = nil)
71
71
  return instance_variable_get(:@_brick_primary_key) if instance_variable_defined?(:@_brick_primary_key)
72
72
 
73
- pk = primary_key.is_a?(String) ? [primary_key] : primary_key || []
73
+ pk = begin
74
+ primary_key.is_a?(String) ? [primary_key] : primary_key || []
75
+ rescue
76
+ []
77
+ end
74
78
  # Just return [] if we're missing any part of the primary key. (PK is usually just "id")
75
79
  if relation && pk.present?
76
80
  @_brick_primary_key ||= pk.any? { |pk_part| !relation[:cols].key?(pk_part) } ? [] : pk
@@ -411,14 +415,16 @@ module ActiveRecord
411
415
  hm_counts.each do |k, hm|
412
416
  associative = nil
413
417
  count_column = if hm.options[:through]
414
- fk_col = (associative = associatives[hm.name]).foreign_key
415
- hm.foreign_key
418
+ fk_col = (associative = associatives[hm.name])&.foreign_key
419
+ hm.foreign_key if fk_col
416
420
  else
417
421
  fk_col = hm.foreign_key
418
422
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
419
423
  pk = hm.klass.primary_key
420
424
  (pk.is_a?(Array) ? pk.first : pk) || '*'
421
425
  end
426
+ next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
427
+
422
428
  tbl_alias = "_br_#{hm.name}"
423
429
  pri_tbl = hm.active_record
424
430
  on_clause = []
@@ -561,7 +567,9 @@ Module.class_exec do
561
567
  full_class_name = +''
562
568
  full_class_name << "::#{self.name}" unless self == Object
563
569
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
564
- if (plural_class_name == 'BrickSwagger' || model = self.const_get(full_class_name))
570
+ if (plural_class_name == 'BrickSwagger' ||
571
+ (::Brick.config.add_orphans && plural_class_name == 'BrickGem') ||
572
+ model = self.const_get(full_class_name))
565
573
  # if it's a controller and no match or a model doesn't really use the same table name, eager load all models and try to find a model class of the right name.
566
574
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
567
575
  end
@@ -893,6 +901,14 @@ class Object
893
901
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
894
902
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
895
903
 
904
+ # Brick-specific pages
905
+ if plural_class_name == 'BrickGem'
906
+ self.define_method :orphans do
907
+ instance_variable_set(:@orphans, ::Brick.find_orphans(::Brick.set_db_schema(params)))
908
+ end
909
+ return [new_controller_class, code + ' # BrickGem controller']
910
+ end
911
+
896
912
  unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
897
913
  code << " def index\n"
898
914
  code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
@@ -1043,10 +1059,11 @@ class Object
1043
1059
  if is_need_params
1044
1060
  code << "private\n"
1045
1061
  code << " def #{params_name}\n"
1046
- code << " params.require(:#{singular_table_name}).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
1062
+ code << " params.require(:#{require_name = model.name.underscore.tr('/', '_')
1063
+ }).permit(#{model.columns_hash.keys.map { |c| c.to_sym.inspect }.join(', ')})\n"
1047
1064
  code << " end\n"
1048
1065
  self.define_method(params_name) do
1049
- params.require(singular_table_name.to_sym).permit(model.columns_hash.keys)
1066
+ params.require(require_name.to_sym).permit(model.columns_hash.keys)
1050
1067
  end
1051
1068
  private params_name
1052
1069
  # Get column names for params from relations[model.table_name][:cols].keys
@@ -1471,5 +1488,72 @@ module Brick
1471
1488
  end
1472
1489
  assoc_bt[:inverse] = assoc_hm
1473
1490
  end
1491
+
1492
+ # Locate orphaned records
1493
+ def find_orphans(multi_schema)
1494
+ is_default_schema = multi_schema&.==(Apartment.default_schema)
1495
+ relations.each_with_object([]) do |v, s|
1496
+ frn_tbl = v.first
1497
+ next if (relation = v.last).key?(:isView) || config.exclude_tables.include?(frn_tbl) ||
1498
+ !(for_pk = (relation[:pkey].values.first&.first))
1499
+
1500
+ is_default_frn_schema = !is_default_schema && multi_schema &&
1501
+ ((frn_parts = frn_tbl.split('.')).length > 1 && frn_parts.first)&.==(Apartment.default_schema)
1502
+ relation[:fks].select { |_k, assoc| assoc[:is_bt] }.each do |_k, bt|
1503
+ begin
1504
+ if bt.key?(:polymorphic)
1505
+ pri_pk = for_pk
1506
+ pri_tables = Brick.config.polymorphics["#{frn_tbl}.#{bt[:fk]}"]
1507
+ .each_with_object(Hash.new { |h, k| h[k] = [] }) do |pri_class, s|
1508
+ s[Object.const_get(pri_class).table_name] << pri_class
1509
+ end
1510
+ fk_id_col = "#{bt[:fk]}_id"
1511
+ fk_type_col = "#{bt[:fk]}_type"
1512
+ selects = []
1513
+ pri_tables.each do |pri_tbl, pri_types|
1514
+ # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
1515
+ # are both in the "public" schema
1516
+ next if is_default_frn_schema &&
1517
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
1518
+
1519
+ selects << "SELECT '#{pri_tbl}' AS pri_tbl, frn.#{fk_type_col} AS pri_type, frn.#{fk_id_col} AS pri_id, frn.#{for_pk} AS frn_id
1520
+ FROM #{frn_tbl} AS frn
1521
+ LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{fk_id_col}
1522
+ WHERE frn.#{fk_type_col} IN (#{
1523
+ pri_types.map { |pri_type| "'#{pri_type}'" }.join(', ')
1524
+ }) AND frn.#{bt[:fk]}_id IS NOT NULL AND pri.#{pri_pk} IS NULL\n"
1525
+ end
1526
+ ActiveRecord::Base.execute_sql(selects.join("UNION ALL\n")).each do |o|
1527
+ entry = [frn_tbl, o['frn_id'], o['pri_type'], o['pri_id'], fk_id_col]
1528
+ entry << o['pri_tbl'] if (pri_class = Object.const_get(o['pri_type'])) != pri_class.base_class
1529
+ s << entry
1530
+ end
1531
+ else
1532
+ # Skip if database is multitenant, we're not focused on "public", and the foreign and primary tables
1533
+ # are both in the "public" schema
1534
+ pri_tbl = bt.key?(:inverse_table) && bt[:inverse_table]
1535
+ next if is_default_frn_schema &&
1536
+ ((pri_parts = pri_tbl&.split('.'))&.length > 1 && pri_parts.first)&.==(Apartment.default_schema)
1537
+
1538
+ pri_pk = relations[pri_tbl].fetch(:pkey, nil)&.values&.first&.first ||
1539
+ _class_pk(pri_tbl, multi_schema)
1540
+ ActiveRecord::Base.execute_sql(
1541
+ "SELECT frn.#{bt[:fk]} AS pri_id, frn.#{for_pk} AS frn_id
1542
+ FROM #{frn_tbl} AS frn
1543
+ LEFT OUTER JOIN #{pri_tbl} AS pri ON pri.#{pri_pk} = frn.#{bt[:fk]}
1544
+ WHERE frn.#{bt[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
1545
+ ORDER BY 1, 2"
1546
+ ).each { |o| s << [frn_tbl, o['frn_id'], pri_tbl, o['pri_id'], bt[:fk]] }
1547
+ end
1548
+ rescue StandardError => err
1549
+ puts "Strange -- #{err.inspect}"
1550
+ end
1551
+ end
1552
+ end
1553
+ end
1554
+
1555
+ def _class_pk(dotted_name, multitenant)
1556
+ Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
1557
+ end
1474
1558
  end
1475
1559
  end
@@ -53,7 +53,9 @@ module Brick
53
53
  # Used by Rails 5.0 and above
54
54
  alias :_brick_template_exists? :template_exists?
55
55
  def template_exists?(*args, **options)
56
- _brick_template_exists?(*args, **options) || set_brick_model(args)
56
+ (::Brick.config.add_orphans && args.first == 'orphans') ||
57
+ _brick_template_exists?(*args, **options) ||
58
+ set_brick_model(args)
57
59
  end
58
60
 
59
61
  def set_brick_model(find_args)
@@ -88,44 +90,56 @@ module Brick
88
90
  unless (model_name = (
89
91
  @_brick_model ||
90
92
  (ActionView.version < ::Gem::Version.new('5.0') && args[1].is_a?(Array) ? set_brick_model(args) : nil)
91
- )&.name)
93
+ )&.name) ||
94
+ (is_orphans = ::Brick.config.add_orphans && args[0..1] == ['orphans', ['brick_gem']])
92
95
  return _brick_find_template(*args, **options)
93
96
  end
94
97
 
95
- pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
96
- obj_name = model_name.split('::').last.underscore
97
- path_obj_name = model_name.underscore.tr('/', '_')
98
- table_name = obj_name.pluralize
99
- template_link = nil
100
- bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
101
- hms_columns = [] # Used for 'index'
102
- skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
103
- hms_headers = hms.each_with_object([]) do |hm, s|
104
- hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
105
- hm_fk_name = if hm_assoc.options[:through]
106
- associative = associatives[hm_assoc.name]
107
- "'#{associative.name}.#{associative.foreign_key}'"
108
- else
109
- hm_assoc.foreign_key
110
- end
111
- if args.first == 'index'
112
- hms_columns << if hm_assoc.macro == :has_many
113
- set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
114
- 'nil'
115
- else
116
- # Postgres column names are limited to 63 characters
117
- attrib_name = "_br_#{assoc_name}_ct"[0..62]
118
- "#{obj_name}.#{attrib_name} || 0"
119
- end
98
+ unless is_orphans
99
+ pk = @_brick_model._brick_primary_key(::Brick.relations.fetch(model_name, nil))
100
+ obj_name = model_name.split('::').last.underscore
101
+ path_obj_name = model_name.underscore.tr('/', '_')
102
+ table_name = obj_name.pluralize
103
+ template_link = nil
104
+ bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
105
+ hms_columns = [] # Used for 'index'
106
+ skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
107
+ hms_headers = hms.each_with_object([]) do |hm, s|
108
+ hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
109
+ hm_fk_name = if hm_assoc.options[:through]
110
+ associative = associatives[hm_assoc.name]
111
+ associative && "'#{associative.name}.#{associative.foreign_key}'"
112
+ else
113
+ hm_assoc.foreign_key
114
+ end
115
+ case args.first
116
+ when 'index'
117
+ hms_columns << if hm_assoc.macro == :has_many
118
+ set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
119
+ 'nil'
120
+ else
121
+ # Postgres column names are limited to 63 characters
122
+ attrib_name = "_br_#{assoc_name}_ct"[0..62]
123
+ "#{obj_name}.#{attrib_name} || 0"
124
+ end
125
+ if hm_fk_name
120
126
  "<%= ct = #{set_ct}
121
127
  link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
122
- else # has_one
128
+ else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
129
+ "#{assoc_name}\n"
130
+ end
131
+ else # has_one
123
132
  "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
124
- end
125
- elsif args.first == 'show'
126
- hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
133
+ end
134
+ when 'show', 'update'
135
+ hm_stuff << if hm_fk_name
136
+ "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
137
+ else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
138
+ assoc_name
139
+ end
140
+ end
141
+ s << hm_stuff
127
142
  end
128
- s << hm_stuff
129
143
  end
130
144
 
131
145
  schema_options = ::Brick.db_schemas.keys.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
@@ -140,6 +154,7 @@ module Brick
140
154
  end.sort.each_with_object(+'') do |v, s|
141
155
  s << "<option value=\"#{v.underscore.gsub('.', '/').pluralize}\">#{v}</option>"
142
156
  end.html_safe
157
+ table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
143
158
  css = +"<style>
144
159
  #dropper {
145
160
  background-color: #eee;
@@ -199,6 +214,9 @@ table tbody tr.active-row {
199
214
  color: #009879;
200
215
  }
201
216
 
217
+ td.val {
218
+ display: block;
219
+ }
202
220
  a.show-arrow {
203
221
  font-size: 1.5em;
204
222
  text-decoration: none;
@@ -212,11 +230,27 @@ a.big-arrow {
212
230
  overflow: hidden;
213
231
  }
214
232
  .wide-input input[type=text] {
215
- width: 100%;
233
+ display: inline-block;
234
+ width: 90%;
216
235
  }
217
236
  .dimmed {
218
237
  background-color: #C0C0C0;
219
238
  }
239
+
240
+ #revertTemplate {
241
+ display: none;
242
+ }
243
+ svg.revert {
244
+ display: none;
245
+ margin-left: 0.25em;
246
+ }
247
+ .wide-input > svg.revert {
248
+ float: right;
249
+ }
250
+ input+svg.revert {
251
+ top: 0.5em;
252
+ }
253
+
220
254
  input[type=submit] {
221
255
  background-color: #004998;
222
256
  color: #FFF;
@@ -225,7 +259,10 @@ input[type=submit] {
225
259
  text-align: right;
226
260
  }
227
261
  </style>
228
- <% def is_bcrypt?(val)
262
+ <%
263
+ is_includes_dates = nil
264
+
265
+ def is_bcrypt?(val)
229
266
  val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
230
267
  end
231
268
  def hide_bcrypt(val, max_len = 200)
@@ -314,7 +351,7 @@ function changeout(href, param, value, trimAfter) {
314
351
  var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
315
352
  if (value === undefined)
316
353
  // A couple possibilities if it's namespaced, starting with two parts in the path -- and then try just one
317
- return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)];
354
+ return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)[0]];
318
355
  else
319
356
  return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
320
357
  }
@@ -464,7 +501,7 @@ if (headerTop) {
464
501
  end
465
502
  # %%% Instead of our current "for Janet Leverling (Employee)" kind of link we previously had this code that did a "where x = 123" thing:
466
503
  # (where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %>)
467
- "#{css}
504
+ +"#{css}
468
505
  <p style=\"color: green\"><%= notice %></p>#{"
469
506
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
470
507
  <select id=\"tbl\">#{table_options}</select>
@@ -490,8 +527,7 @@ if (headerTop) {
490
527
  <thead><tr>#{'<th></th>' if pk.present?}<%
491
528
  col_order = []
492
529
  @#{table_name}.columns.each do |col|
493
- col_name = col.name
494
- next if (#{(pk || []).inspect}.include?(col_name) && col.type == :integer && !bts.key?(col_name)) ||
530
+ next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
495
531
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
496
532
 
497
533
  col_order << col_name
@@ -504,10 +540,16 @@ if (headerTop) {
504
540
  else %><%=
505
541
  col_name %><%
506
542
  end
507
- %></th><%
543
+ %></th><%
508
544
  end
509
545
  # Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
510
- %>#{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>" }.join
546
+ %>#{hms_headers.map do |h|
547
+ if h.first.options[:through] && !h.first.through_reflection
548
+ "<th>#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
549
+ else
550
+ "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
551
+ end
552
+ end.join
511
553
  }</tr></thead>
512
554
 
513
555
  <tbody>
@@ -529,6 +571,7 @@ if (headerTop) {
529
571
  # 0..62 because Postgres column names are limited to 63 characters
530
572
  #{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)
531
573
  )
574
+ bt_txt ||= \"<< Orphaned ID: #\{val} >>\" if val
532
575
  bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
533
576
  <%= bt_id ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
534
577
  <%#= 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 %>
@@ -546,8 +589,31 @@ if (headerTop) {
546
589
 
547
590
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
548
591
  #{script}"
592
+ when 'orphans'
593
+ if is_orphans
594
+ +"#{css}
595
+ <p style=\"color: green\"><%= notice %></p>#{"
596
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
597
+ <select id=\"tbl\">#{table_options}</select>
598
+ <h1>Orphans<%= \" for #\{}\" if false %></h1>
599
+ <% @orphans.each do |o|
600
+ via = \" (via #\{o[4]})\" unless \"#\{o[2].split('.').last.underscore.singularize}_id\" == o[4] %>
601
+ <a href=\"/<%= o[0].split('.').last %>/<%= o[1] %>\">
602
+ <%= \"#\{o[0]} #\{o[1]} refers#\{via} to non-existent #\{o[2]} #\{o[3]}#\{\" (in table \\\"#\{o[5]}\\\")\" if o[5]}\" %>
603
+ </a><br>
604
+ <% end %>
605
+ #{script}"
606
+ end
607
+
549
608
  when 'show', 'update'
550
- "#{css}
609
+ +"#{css}
610
+
611
+ <svg id=\"revertTemplate\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"
612
+ width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">
613
+ <path id=\"revertPath\" fill=\"#2020A0\" d=\"M271.844,119.641c-78.531,0-148.031,37.875-191.813,96.188l-80.172-80.188v256h256l-87.094-87.094
614
+ c23.141-70.188,89.141-120.906,167.063-120.906c97.25,0,176,78.813,176,176C511.828,227.078,404.391,119.641,271.844,119.641z\" />
615
+ </svg>
616
+
551
617
  <p style=\"color: green\"><%= notice %></p>#{"
552
618
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
553
619
  <select id=\"tbl\">#{table_options}</select>
@@ -605,27 +671,39 @@ end
605
671
  <%= k %>
606
672
  <% end %>
607
673
  </th>
608
- <td>
609
- <% if bt
674
+ <td class=\"val\">
675
+ <% dt_pickers = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
676
+ if bt
610
677
  html_options = { prompt: \"Select #\{bt_name\}\" }
611
678
  html_options[:class] = 'dimmed' unless val %>
612
679
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
613
- <%= bt_obj = bt_class&.find_by(bt_pair[1] => val); link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' }) if bt_obj %>
614
- <% else case #{model_name}.column_for_attribute(k).type
680
+ <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
681
+ link_to('⇛', send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
682
+ elsif val
683
+ \"<span>Orphaned ID: #\{val}</span>\".html_safe
684
+ end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
685
+ <% else case (col_type = #{model_name}.column_for_attribute(k).type)
615
686
  when :string, :text %>
616
687
  <% if is_bcrypt?(val) # || .readonly? %>
617
688
  <%= hide_bcrypt(val, 1000) %>
618
689
  <% else %>
619
- <div class=\"wide-input\"><%= f.text_field k.to_sym %></div>
690
+ <div class=\"wide-input\"><%= f.text_field k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg></div>
620
691
  <% end %>
621
692
  <% when :boolean %>
622
- <%= f.check_box k.to_sym %>
623
- <% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
693
+ <%= f.check_box k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
694
+ <% when :integer, :decimal, :float
624
695
  # What happens when keys are UUID?
625
696
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
626
697
  # If it's not yet enabled then: enable_extension 'uuid-ossp'
627
698
  # ActiveUUID gem created a new :uuid type %>
628
- <%= val %>
699
+ <%= if col_type == :integer
700
+ f.text_field k.to_sym, { pattern: '\\d*', class: 'check-validity' }
701
+ else
702
+ f.number_field k.to_sym
703
+ end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
704
+ <% when *dt_pickers.keys
705
+ is_includes_dates = true %>
706
+ <%= f.text_field k.to_sym, { class: dt_pickers[col_type] } %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
629
707
  <% when :binary, :primary_key %>
630
708
  <% end %>
631
709
  <% end %>
@@ -641,6 +719,9 @@ end
641
719
  <% end %>
642
720
 
643
721
  #{hms_headers.each_with_object(+'') do |hm, s|
722
+ # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
723
+ next if hm.first.options[:through] && !hm.first.through_reflection
724
+
644
725
  if (pk = hm.first.klass.primary_key)
645
726
  hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
646
727
  obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
@@ -664,6 +745,61 @@ end
664
745
  #{script}"
665
746
 
666
747
  end
748
+ inline << "
749
+ <% if is_includes_dates %>
750
+ <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">
751
+ <style>
752
+ .flatpickr-calendar {
753
+ background: #A0FFA0;
754
+ }
755
+ </style>
756
+ <script src=\"https://cdn.jsdelivr.net/npm/flatpickr\"></script>
757
+ <script>
758
+ flatpickr(\".datepicker\");
759
+ flatpickr(\".datetimepicker\", {enableTime: true});
760
+ </script>
761
+ <% end %>
762
+ <script>
763
+ document.querySelectorAll(\"input, select\").forEach(function (inp) {
764
+ var origVal = getInpVal(),
765
+ prevVal = origVal;
766
+ var revert;
767
+ if ((revert = ((inp.tagName === \"SELECT\" && inp.nextElementSibling.nextElementSibling) ||
768
+ inp.nextElementSibling ||
769
+ inp.parentElement.nextElementSibling)) && revert.tagName.toLowerCase() === \"svg\")
770
+ revert.addEventListener(\"click\", function (e) {
771
+ if (inp.type === \"checkbox\")
772
+ inp.checked = origVal;
773
+ else
774
+ inp.value = origVal;
775
+ revert.style.display = \"none\";
776
+ if (!inp._flatpickr) inp.focus();
777
+ });
778
+ inp.addEventListener(inp.type === \"checkbox\" ? \"change\" : \"input\", function (e) {
779
+ if(inp.className.split(\" \").indexOf(\"check-validity\") > 0) {
780
+ if (inp.checkValidity()) {
781
+ prevVal = getInpVal();
782
+ } else {
783
+ inp.value = prevVal;
784
+ }
785
+ } else {
786
+ // If this is the result of changing an hour or minute, keep the calendar open.
787
+ // And if it was the result of selecting a date, the calendar can now close.
788
+ if (inp._flatpickr &&
789
+ // Test only for changes in the date portion of a date or datetime
790
+ ((giv = getInpVal()) && giv.split(' ')[0]) !== (prevVal && prevVal.split(' ')[0])
791
+ )
792
+ inp._flatpickr.close();
793
+ prevVal = getInpVal();
794
+ }
795
+ // Show or hide the revert button
796
+ if (revert) revert.style.display = getInpVal() === origVal ? \"none\" : \"inline-block\";
797
+ });
798
+ function getInpVal() {
799
+ return inp.type === \"checkbox\" ? inp.checked : inp.value;
800
+ }
801
+ });
802
+ </script>"
667
803
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
668
804
  keys = options.has_key?(:locals) ? options[:locals].keys : []
669
805
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ if Object.const_defined?('::Rake::TaskManager')
4
+ namespace :brick do
5
+ desc 'Find any seemingly-orphaned records'
6
+ task orphans: :environment do
7
+ schema_list = ((multi = ::Brick.config.schema_behavior[:multitenant]) && ::Brick.db_schemas.keys.sort) || []
8
+ schema = if schema_list.length == 1
9
+ schema_list.first
10
+ elsif schema_list.length.positive?
11
+ require 'fancy_gets'
12
+ include FancyGets
13
+ gets_list(list: schema_list, chosen: multi[:schema_to_analyse])
14
+ end
15
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema) if schema
16
+ orphans = ::Brick.find_orphans(schema)
17
+ puts "Orphans in #{schema}:\n#{'=' * (schema.length + 12)}" if schema
18
+ if orphans.empty?
19
+ puts "No orphans!"
20
+ else
21
+ orphans.each do |o|
22
+ via = " (via #{o[4]})" unless "#{o[2].split('.').last.underscore.singularize}_id" == o[4]
23
+ puts "#{o[0]} #{o[1]} refers#{via} to non-existent #{o[2]} #{o[3]}#{" (in table \"#{o[5]}\")" if o[5]}"
24
+ end
25
+ puts
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 39
8
+ TINY = 42
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
@@ -128,7 +128,10 @@ module Brick
128
128
 
129
129
  def set_db_schema(params)
130
130
  schema = params['_brick_schema'] || 'public'
131
- ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema) if schema && ::Brick.db_schemas&.include?(schema)
131
+ if schema && ::Brick.db_schemas&.include?(schema)
132
+ ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
133
+ schema
134
+ end
132
135
  end
133
136
 
134
137
  # All tables and views (what Postgres calls "relations" including column and foreign key info)
@@ -457,6 +460,9 @@ In config/initializers/brick.rb appropriate entries would look something like:
457
460
  send(:resources, controller_name.to_sym, **options)
458
461
  end
459
462
  end
463
+ if ::Brick.config.add_orphans && instance_variable_get(:@set).named_routes.names.exclude?(:brick_orphans)
464
+ get('/brick_orphans', to: 'brick_gem#orphans', as: 'brick_orphans')
465
+ end
460
466
  end
461
467
  send(:get, '/api-docs/v1/swagger.json', { to: 'brick_swagger#index' }) if Object.const_defined?('Rswag::Ui')
462
468
  end
@@ -481,6 +487,11 @@ require 'active_record'
481
487
  require 'active_record/relation'
482
488
  require 'active_record/relation/query_methods' if is_add_left_outer_join
483
489
 
490
+ # Rake tasks
491
+ class Railtie < Rails::Railtie
492
+ Dir.glob("#{File.expand_path(__dir__)}/brick/tasks/**/*.rake").each { |task| load task }
493
+ end
494
+
484
495
  # Major compatibility fixes for ActiveRecord < 4.2
485
496
  # ================================================
486
497
  ActiveSupport.on_load(:active_record) do
@@ -209,7 +209,7 @@ module Brick
209
209
  # # Specify STI subclasses either directly by name or as a general module prefix that should always relate to a specific
210
210
  # # parent STI class. The prefixed :: here for these examples is mandatory. Also having a suffixed :: means instead of
211
211
  # # a class reference, this is for a general namespace reference. So in this case requests for, say, either of the
212
- # # non-existant classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
212
+ # # non-existent classes Animals::Cat or Animals::Goat (or anything else with the module prefix of \"Animals::\" would
213
213
  # # build a model that inherits from Animal. And a request specifically for the class Snake would build a new model
214
214
  # # that inherits from Reptile, and no other request would do this -- only specifically for Snake. The ending ::
215
215
  # # indicates that it's a module prefix instead of a specific class name.
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.39
4
+ version: 1.0.42
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-06-30 00:00:00.000000000 Z
11
+ date: 2022-07-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -30,6 +30,20 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '7.2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: fancy_gets
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: appraisal
35
49
  requirement: !ruby/object:Gem::Requirement
@@ -225,6 +239,7 @@ files:
225
239
  - lib/brick/join_array.rb
226
240
  - lib/brick/serializers/json.rb
227
241
  - lib/brick/serializers/yaml.rb
242
+ - lib/brick/tasks/orphans.rake
228
243
  - lib/brick/util.rb
229
244
  - lib/brick/version_number.rb
230
245
  - lib/generators/brick/USAGE