brick 1.0.40 → 1.0.43

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: cf29d4af64b65c01ae68df45762b816403a1ebc0035e1221552f249ff3ba2ce9
4
- data.tar.gz: 076cdb672d7c2137cc74421467d72491e9ea89751fab32015f427158fc367b45
3
+ metadata.gz: 8387796a98b0bcf24586c6b31f091cfdc4cca1b45b73d02de3bdba17430625f7
4
+ data.tar.gz: 1c7509e21a1a9a6c8c52f3015b0a7e34222ff944433e028be314f8525ee948b8
5
5
  SHA512:
6
- metadata.gz: ce17038567ddd9d6d9d9dce6eb71fd50612d167b56351898ef9b3f9441d1a7ef3ee4ec3a57f3f24e613829e6c8a2f59517fc0d4e9266f11166b92821310b0c06
7
- data.tar.gz: 6a7bb38b009853594a8bf1ab28489bbc92d6300fcd0f8497c7950e81c6a62a4c6da2bd65840f93bd7afea40153727cb93914529120ecf9b0dceca39b21fef880
6
+ metadata.gz: 75222971d2407d8ffb3b8412694da3fe6ad0d3f27f147159f6d75aa712d1dfe8a9d54627cb2c816fdfc8cb7c44de4407f180fbc559b9c811719c22a016c913f5
7
+ data.tar.gz: 44e03a9d2ddec85969d5b1aca67a9426a4b3dd33f196c058d57b380fa77e78424768dd3e97a40227b2372bec4ccd81dfd1b7760b65e2c5b292c8b47c0d27e2c7
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
@@ -415,14 +415,16 @@ module ActiveRecord
415
415
  hm_counts.each do |k, hm|
416
416
  associative = nil
417
417
  count_column = if hm.options[:through]
418
- fk_col = (associative = associatives[hm.name]).foreign_key
419
- hm.foreign_key
418
+ fk_col = (associative = associatives[hm.name])&.foreign_key
419
+ hm.foreign_key if fk_col
420
420
  else
421
421
  fk_col = hm.foreign_key
422
422
  poly_type = hm.inverse_of.foreign_type if hm.options.key?(:as)
423
423
  pk = hm.klass.primary_key
424
424
  (pk.is_a?(Array) ? pk.first : pk) || '*'
425
425
  end
426
+ next unless count_column # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
427
+
426
428
  tbl_alias = "_br_#{hm.name}"
427
429
  pri_tbl = hm.active_record
428
430
  on_clause = []
@@ -565,7 +567,9 @@ Module.class_exec do
565
567
  full_class_name = +''
566
568
  full_class_name << "::#{self.name}" unless self == Object
567
569
  full_class_name << "::#{plural_class_name.underscore.singularize.camelize}"
568
- 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))
569
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.
570
574
  Object.send(:build_controller, self, class_name, plural_class_name, model, relations)
571
575
  end
@@ -897,6 +901,14 @@ class Object
897
901
  built_controller = Class.new(ActionController::Base) do |new_controller_class|
898
902
  (namespace || Object).const_set(class_name.to_sym, new_controller_class)
899
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
+
900
912
  unless (is_swagger = plural_class_name == 'BrickSwagger') # && request.format == :json)
901
913
  code << " def index\n"
902
914
  code << " @#{table_name} = #{model.name}#{pk&.present? ? ".order(#{pk.inspect})" : '.all'}\n"
@@ -1047,10 +1059,11 @@ class Object
1047
1059
  if is_need_params
1048
1060
  code << "private\n"
1049
1061
  code << " def #{params_name}\n"
1050
- 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"
1051
1064
  code << " end\n"
1052
1065
  self.define_method(params_name) do
1053
- params.require(singular_table_name.to_sym).permit(model.columns_hash.keys)
1066
+ params.require(require_name.to_sym).permit(model.columns_hash.keys)
1054
1067
  end
1055
1068
  private params_name
1056
1069
  # Get column names for params from relations[model.table_name][:cols].keys
@@ -1475,5 +1488,72 @@ module Brick
1475
1488
  end
1476
1489
  assoc_bt[:inverse] = assoc_hm
1477
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
1478
1558
  end
1479
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,9 @@ input[type=submit] {
225
259
  text-align: right;
226
260
  }
227
261
  </style>
228
- <% def is_bcrypt?(val)
262
+ <% is_includes_dates = nil
263
+
264
+ def is_bcrypt?(val)
229
265
  val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
230
266
  end
231
267
  def hide_bcrypt(val, max_len = 200)
@@ -297,7 +333,13 @@ window.addEventListener(\"pageshow\", function() {
297
333
  });
298
334
 
299
335
  if (tblSelect) { // Always present
300
- tblSelect.value = changeout(location.href)[schemaSelect ? 1 : 0];
336
+ var i = schemaSelect ? 1 : 0,
337
+ changeoutList = changeout(location.href);
338
+ for (; i < changeoutList.length; ++i) {
339
+ tblSelect.value = changeoutList[i];
340
+ if (tblSelect.value !== \"\") break;
341
+ }
342
+
301
343
  tblSelect.addEventListener(\"change\", function () {
302
344
  var lhr = changeout(location.href, null, this.value);
303
345
  if (brickSchema)
@@ -314,13 +356,13 @@ function changeout(href, param, value, trimAfter) {
314
356
  var pathParts = hrefParts[hrefParts.length - 1].split(\"/\");
315
357
  if (value === undefined)
316
358
  // 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)];
359
+ return [pathParts.slice(1, 3).join('/'), pathParts.slice(1, 2)[0]];
318
360
  else
319
361
  return hrefParts[0] + \"://\" + pathParts[0] + \"/\" + value;
320
362
  }
321
363
  if (trimAfter) {
322
364
  var pathParts = hrefParts[0].split(\"/\");
323
- while (pathParts.lastIndexOf(trimAfter) != pathParts.length - 1) pathParts.pop();
365
+ while (pathParts.lastIndexOf(trimAfter) !== pathParts.length - 1) pathParts.pop();
324
366
  hrefParts[0] = pathParts.join(\"/\");
325
367
  }
326
368
  var params = hrefParts.length > 1 ? hrefParts[1].split(\"&\") : [];
@@ -464,7 +506,7 @@ if (headerTop) {
464
506
  end
465
507
  # %%% Instead of our current "for Janet Leverling (Employee)" kind of link we previously had this code that did a "where x = 123" thing:
466
508
  # (where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %>)
467
- "#{css}
509
+ +"#{css}
468
510
  <p style=\"color: green\"><%= notice %></p>#{"
469
511
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
470
512
  <select id=\"tbl\">#{table_options}</select>
@@ -490,8 +532,7 @@ if (headerTop) {
490
532
  <thead><tr>#{'<th></th>' if pk.present?}<%
491
533
  col_order = []
492
534
  @#{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)) ||
535
+ next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
495
536
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
496
537
 
497
538
  col_order << col_name
@@ -504,10 +545,16 @@ if (headerTop) {
504
545
  else %><%=
505
546
  col_name %><%
506
547
  end
507
- %></th><%
548
+ %></th><%
508
549
  end
509
550
  # 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
551
+ %>#{hms_headers.map do |h|
552
+ if h.first.options[:through] && !h.first.through_reflection
553
+ "<th>#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
554
+ else
555
+ "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
556
+ end
557
+ end.join
511
558
  }</tr></thead>
512
559
 
513
560
  <tbody>
@@ -547,8 +594,31 @@ if (headerTop) {
547
594
 
548
595
  #{"<hr><%= link_to \"New #{obj_name}\", new_#{path_obj_name}_path %>" unless @_brick_model.is_view?}
549
596
  #{script}"
597
+ when 'orphans'
598
+ if is_orphans
599
+ +"#{css}
600
+ <p style=\"color: green\"><%= notice %></p>#{"
601
+ <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
602
+ <select id=\"tbl\">#{table_options}</select>
603
+ <h1>Orphans<%= \" for #\{}\" if false %></h1>
604
+ <% @orphans.each do |o|
605
+ via = \" (via #\{o[4]})\" unless \"#\{o[2].split('.').last.underscore.singularize}_id\" == o[4] %>
606
+ <a href=\"/<%= o[0].split('.').last %>/<%= o[1] %>\">
607
+ <%= \"#\{o[0]} #\{o[1]} refers#\{via} to non-existent #\{o[2]} #\{o[3]}#\{\" (in table \\\"#\{o[5]}\\\")\" if o[5]}\" %>
608
+ </a><br>
609
+ <% end %>
610
+ #{script}"
611
+ end
612
+
550
613
  when 'show', 'update'
551
- "#{css}
614
+ +"#{css}
615
+
616
+ <svg id=\"revertTemplate\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"
617
+ width=\"32px\" height=\"32px\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">
618
+ <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
619
+ 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\" />
620
+ </svg>
621
+
552
622
  <p style=\"color: green\"><%= notice %></p>#{"
553
623
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.config.schema_behavior[:multitenant] && ::Brick.db_schemas.length > 1}
554
624
  <select id=\"tbl\">#{table_options}</select>
@@ -606,31 +676,39 @@ end
606
676
  <%= k %>
607
677
  <% end %>
608
678
  </th>
609
- <td>
610
- <% if bt
679
+ <td class=\"val\">
680
+ <% dt_pickers = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
681
+ if bt
611
682
  html_options = { prompt: \"Select #\{bt_name\}\" }
612
683
  html_options[:class] = 'dimmed' unless val %>
613
684
  <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
614
685
  <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
615
686
  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' })
616
687
  elsif val
617
- \"Orphaned ID: #\{val}\"
618
- end %>
619
- <% else case #{model_name}.column_for_attribute(k).type
688
+ \"<span>Orphaned ID: #\{val}</span>\".html_safe
689
+ end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
690
+ <% else case (col_type = #{model_name}.column_for_attribute(k).type)
620
691
  when :string, :text %>
621
692
  <% if is_bcrypt?(val) # || .readonly? %>
622
693
  <%= hide_bcrypt(val, 1000) %>
623
694
  <% else %>
624
- <div class=\"wide-input\"><%= f.text_field k.to_sym %></div>
695
+ <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>
625
696
  <% end %>
626
697
  <% when :boolean %>
627
- <%= f.check_box k.to_sym %>
628
- <% when :integer, :decimal, :float, :date, :datetime, :time, :timestamp
698
+ <%= f.check_box k.to_sym %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
699
+ <% when :integer, :decimal, :float
629
700
  # What happens when keys are UUID?
630
701
  # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
631
702
  # If it's not yet enabled then: enable_extension 'uuid-ossp'
632
703
  # ActiveUUID gem created a new :uuid type %>
633
- <%= val %>
704
+ <%= if col_type == :integer
705
+ f.text_field k.to_sym, { pattern: '\\d*', class: 'check-validity' }
706
+ else
707
+ f.number_field k.to_sym
708
+ end %><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
709
+ <% when *dt_pickers.keys
710
+ is_includes_dates = true %>
711
+ <%= 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>
634
712
  <% when :binary, :primary_key %>
635
713
  <% end %>
636
714
  <% end %>
@@ -646,6 +724,9 @@ end
646
724
  <% end %>
647
725
 
648
726
  #{hms_headers.each_with_object(+'') do |hm, s|
727
+ # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
728
+ next if hm.first.options[:through] && !hm.first.through_reflection
729
+
649
730
  if (pk = hm.first.klass.primary_key)
650
731
  hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
651
732
  obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
@@ -669,6 +750,66 @@ end
669
750
  #{script}"
670
751
 
671
752
  end
753
+ inline << "
754
+ <% if is_includes_dates %>
755
+ <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">
756
+ <style>
757
+ .flatpickr-calendar {
758
+ background: #A0FFA0;
759
+ }
760
+ </style>
761
+ <script src=\"https://cdn.jsdelivr.net/npm/flatpickr\"></script>
762
+ <script>
763
+ flatpickr(\".datepicker\");
764
+ flatpickr(\".datetimepicker\", {enableTime: true});
765
+ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
766
+ </script>
767
+ <% end %>
768
+ <script>
769
+ document.querySelectorAll(\"input, select\").forEach(function (inp) {
770
+ var origVal = getInpVal(),
771
+ prevVal = origVal;
772
+ var revert;
773
+ if ((revert = ((inp.tagName === \"SELECT\" && inp.nextElementSibling.nextElementSibling) ||
774
+ inp.nextElementSibling ||
775
+ inp.parentElement.nextElementSibling)) && revert.tagName.toLowerCase() === \"svg\")
776
+ revert.addEventListener(\"click\", function (e) {
777
+ if (inp.type === \"checkbox\")
778
+ inp.checked = origVal;
779
+ else
780
+ inp.value = origVal;
781
+ revert.style.display = \"none\";
782
+ if (inp._flatpickr)
783
+ inp._flatpickr.setDate(origVal);
784
+ else
785
+ inp.focus();
786
+ });
787
+ inp.addEventListener(inp.type === \"checkbox\" ? \"change\" : \"input\", function (e) {
788
+ if(inp.className.split(\" \").indexOf(\"check-validity\") > 0) {
789
+ if (inp.checkValidity()) {
790
+ prevVal = getInpVal();
791
+ } else {
792
+ inp.value = prevVal;
793
+ }
794
+ } else {
795
+ // If this is the result of changing an hour or minute, keep the calendar open.
796
+ // And if it was the result of selecting a date, the calendar can now close.
797
+ if (inp._flatpickr &&
798
+ // Test only for changes in the date portion of a date or datetime
799
+ ((giv = getInpVal()) && (giv1 = giv.split(' ')[0])) !== (prevVal && prevVal.split(' ')[0]) &&
800
+ giv1.indexOf(\":\") < 0 // (definitely not any part of a time thing)
801
+ )
802
+ inp._flatpickr.close();
803
+ prevVal = getInpVal();
804
+ }
805
+ // Show or hide the revert button
806
+ if (revert) revert.style.display = getInpVal() === origVal ? \"none\" : \"inline-block\";
807
+ });
808
+ function getInpVal() {
809
+ return inp.type === \"checkbox\" ? inp.checked : inp.value;
810
+ }
811
+ });
812
+ </script>"
672
813
  # As if it were an inline template (see #determine_template in actionview-5.2.6.2/lib/action_view/renderer/template_renderer.rb)
673
814
  keys = options.has_key?(:locals) ? options[:locals].keys : []
674
815
  handler = ActionView::Template.handler_for_extension(options[:type] || 'erb')
@@ -4,53 +4,25 @@ if Object.const_defined?('::Rake::TaskManager')
4
4
  namespace :brick do
5
5
  desc 'Find any seemingly-orphaned records'
6
6
  task orphans: :environment do
7
- def class_pk(dotted_name, multitenant)
8
- Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
9
- end
10
-
11
7
  schema_list = ((multi = ::Brick.config.schema_behavior[:multitenant]) && ::Brick.db_schemas.keys.sort) || []
12
- if schema_list.length > 1
13
- require 'fancy_gets'
14
- include FancyGets
15
- schema = gets_list(
16
- list: schema_list,
17
- chosen: multi[:schema_to_analyse]
18
- )
19
- elsif schema_list.length.positive?
20
- schema = schema_list.first
21
- end
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
22
15
  ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?", schema) if schema
23
- orphans = +''
24
- ::Brick.relations.each do |k, v|
25
- next if v.key?(:isView) || ::Brick.config.exclude_tables.include?(k) ||
26
- !(pri_pk = v[:pkey].values.first&.first) ||
27
- !(pri_pk = class_pk(k, multi))
28
- v[:fks].each do |k1, v1|
29
- next if v1[:is_bt] ||
30
- !(for_rel = ::Brick.relations.fetch(v1[:inverse_table], nil)) ||
31
- v1[:inverse]&.key?(:polymorphic) ||
32
- !(for_pk = for_rel.fetch(:pkey, nil)&.values&.first&.first) ||
33
- !(for_pk = class_pk(v1[:inverse_table], multi))
34
- begin
35
- ActiveRecord::Base.execute_sql(
36
- "SELECT DISTINCT frn.#{v1[:fk]} AS pri_id, frn.#{for_pk} AS fk_id
37
- FROM #{v1[:inverse_table]} AS frn
38
- LEFT OUTER JOIN #{k} AS pri ON pri.#{pri_pk} = frn.#{v1[:fk]}
39
- WHERE frn.#{v1[:fk]} IS NOT NULL AND pri.#{pri_pk} IS NULL
40
- ORDER BY 1, 2"
41
- ).each do |o|
42
- orphans << "#{v1[:inverse_table]} #{o['fk_id']} refers to non-existant #{k} #{o['pri_id']}\n"
43
- end
44
- rescue StandardError => err
45
- puts "Strange -- #{err.inspect}"
46
- end
47
- end
48
- end
49
- puts "For #{schema}:\n#{'=' * (schema.length + 5)}" if schema
50
- if orphans.blank?
16
+ orphans = ::Brick.find_orphans(schema)
17
+ puts "Orphans in #{schema}:\n#{'=' * (schema.length + 12)}" if schema
18
+ if orphans.empty?
51
19
  puts "No orphans!"
52
20
  else
53
- print orphans
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
54
26
  end
55
27
  end
56
28
  end
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 40
8
+ TINY = 43
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
@@ -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.
@@ -225,7 +225,7 @@ module Brick
225
225
  # # it wasn't originally specified.
226
226
  # Brick.schema_behavior = :namespaced
227
227
  #{Brick.config.schema_behavior ? "Brick.schema_behavior = { multitenant: { schema_to_analyse: #{
228
- Brick.config.schema_behavior[:multitenant][:schema_to_analyse].inspect}" :
228
+ Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil).inspect}" :
229
229
  "# Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering'"
230
230
  } } }
231
231
 
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.40
4
+ version: 1.0.43
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-07-02 00:00:00.000000000 Z
11
+ date: 2022-07-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord