brick 1.0.102 → 1.0.104

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: 167e29431250efc9170ec3f174cce913584c700ce8405a0d275c63636d04cf81
4
- data.tar.gz: 34826b000c093ab4fb8d63edd4509d8244f3d5e3712066489e00076f67ba9704
3
+ metadata.gz: 6d30161a8558282149a49e7eafbd43bb175ce95574178c65a75323eaacf96422
4
+ data.tar.gz: 6d56c5e77c2a0d120bc8e878bde9d33fdfa7d58bed690a21fea176d5d6b55d50
5
5
  SHA512:
6
- metadata.gz: e0d8d82c98b918c2a6b61c06a636ce60e2c4eaaed32b9556b09984f0b778f624cfb887cf9d82d48042cb75057b1e3688be661c05fa9c79f90052da32d029e1b2
7
- data.tar.gz: 53c296598e9bc85ca2afd5456ff02d3c56fde7f79cd6de9f4684c5fcef0ad1be489e6ccda37c26b81a109393de1a10fa70a08d352825e69437ffdcd8b2bb2bfe
6
+ metadata.gz: 14bfffec6fd768d4b3abff2b3ceaeb2a2dca29b167edd64c2e57a63c72eaa70bf46fc8056af25fca86812bfac63d3b793bdeaf7f13a8c2ba7834ac9e65f1fb16
7
+ data.tar.gz: 97f7c94051bb068c4d8bdb5e847e53576c8ebbe18d5c4c93e12d52566008ac612bb40f7088ba81fea21337a3c13c29f366fb993b77f2f88f9fbddea4ffb95982
@@ -77,14 +77,13 @@ module ActiveRecord
77
77
  def self.brick_get_dsl
78
78
  # If there's no DSL yet specified, just try to find the first usable column on this model
79
79
  unless (dsl = ::Brick.config.model_descrips[name])
80
- descrip_col = (columns.map(&:name) - _brick_get_fks -
81
- (::Brick.config.metadata_columns || []) -
82
- [primary_key]).first
83
- dsl = ::Brick.config.model_descrips[name] = if descrip_col
84
- "[#{descrip_col}]"
85
- elsif (pk_parts = self.primary_key.is_a?(Array) ? self.primary_key : [self.primary_key])
86
- "#{name} ##{pk_parts.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
87
- end
80
+ skip_columns = _brick_get_fks + (::Brick.config.metadata_columns || []) + [primary_key]
81
+ dsl = if (descrip_col = columns.find { |c| [:boolean, :binary, :xml].exclude?(c.type) && skip_columns.exclude?(c.name) })
82
+ "[#{descrip_col.name}]"
83
+ elsif (pk_parts = self.primary_key.is_a?(Array) ? self.primary_key : [self.primary_key])
84
+ "#{name} ##{pk_parts.map { |pk_part| "[#{pk_part}]" }.join(', ')}"
85
+ end
86
+ ::Brick.config.model_descrips[name] = dsl
88
87
  end
89
88
  dsl
90
89
  end
@@ -129,7 +128,7 @@ module ActiveRecord
129
128
  translations[parts[0..-2].join('.')] = klass
130
129
  end
131
130
  if klass&.column_names.exclude?(parts.last) &&
132
- (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
131
+ (klass = (orig_class = klass).reflect_on_association(possible_dsl = parts.pop.to_sym)&.klass)
133
132
  if prefix.empty? # Custom columns start with an empty prefix
134
133
  prefix << parts.shift until parts.empty?
135
134
  end
@@ -258,11 +257,11 @@ module ActiveRecord
258
257
  assoc_html_name ? "#{assoc_name}-#{link}".html_safe : link
259
258
  end
260
259
 
261
- def self._brick_index(mode = nil)
260
+ def self._brick_index(mode = nil, separator = '_')
262
261
  tbl_parts = ((mode == :singular) ? table_name.singularize : table_name).split('.')
263
262
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
264
263
  tbl_parts.unshift(::Brick.config.path_prefix) if ::Brick.config.path_prefix
265
- index = tbl_parts.map(&:underscore).join('_')
264
+ index = tbl_parts.map(&:underscore).join(separator)
266
265
  # Rails applies an _index suffix to that route when the resource name is singular
267
266
  index << '_index' if mode != :singular && index == index.singularize
268
267
  index
@@ -397,6 +396,8 @@ module ActiveRecord
397
396
  end
398
397
 
399
398
  class Relation
399
+ attr_accessor :_brick_page_num
400
+
400
401
  # Links from ActiveRecord association pathing names over to real table correlation names
401
402
  # that get chosen when the AREL AST tree is walked.
402
403
  def brick_links
@@ -786,8 +787,21 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
786
787
  end
787
788
  self.order_values |= final_order_by # Same as: order!(*final_order_by)
788
789
  end
789
- # Don't want to get too carried away just yet
790
- self.limit_value = 1000 # Same as: limit!(1000)
790
+ if (page = params['_brick_page']&.to_i)
791
+ page = 1 if page < 1
792
+ limit = params['_brick_page_size'] || 1000
793
+ offset = (page - 1) * limit.to_i
794
+ else
795
+ offset = params['_brick_offset']
796
+ limit = params['_brick_limit']
797
+ end
798
+ if offset.is_a?(Numeric) || offset&.present?
799
+ offset = offset.to_i
800
+ self.offset_value = offset unless offset == 0
801
+ @_brick_page_num = (offset / limit.to_i) + 1 if limit&.!= 0 && (offset % limit.to_i) == 0
802
+ end
803
+ # By default just 1000 rows (Like doing: limit!(1000) but this way is compatible with AR <= 4.2)
804
+ self.limit_value = limit&.to_i || 1000 unless limit.is_a?(String) && limit.empty?
791
805
  wheres unless wheres.empty? # Return the specific parameters that we did use
792
806
  end
793
807
 
@@ -805,7 +819,8 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
805
819
  alias _brick_find_sti_class find_sti_class
806
820
  def find_sti_class(type_name)
807
821
  if ::Brick.sti_models.key?(type_name ||= name)
808
- ::Brick.sti_models[type_name].fetch(:base, nil) || _brick_find_sti_class(type_name)
822
+ # Used to be: ::Brick.sti_models[type_name].fetch(:base, nil) || _brick_find_sti_class(type_name)
823
+ _brick_find_sti_class(type_name)
809
824
  else
810
825
  # This auto-STI is more of a brute-force approach, building modules where needed
811
826
  # The more graceful alternative is the overload of ActiveSupport::Dependencies#autoload_module! found below
@@ -29,6 +29,54 @@ module Brick
29
29
  var finalParams = Object.keys(params).reduce(function (s, v) { if (params[v]) s.push(v + \"=\" + params[v]); return s; }, []).join(\"&\");
30
30
  return hrefParts[0] + (finalParams.length > 0 ? \"?\" + finalParams : \"\");
31
31
  }
32
+
33
+ // This PageTransitionEvent fires when the page first loads, as well as after any other history
34
+ // transition such as when using the browser's Back and Forward buttons.
35
+ window.addEventListener(\"pageshow\", linkSchemas);
36
+ var brickSchema;
37
+ function linkSchemas() {
38
+ var schemaSelect = document.getElementById(\"schema\");
39
+ var tblSelect = document.getElementById(\"tbl\");
40
+ if (tblSelect) { // Always present
41
+ // Used to be: var i = # {::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
42
+ var changeoutList = changeout(location.href);
43
+ for (var i = 0; i < changeoutList.length; ++i) {
44
+ tblSelect.value = changeoutList[i];
45
+ if (tblSelect.value !== \"\") break;
46
+ }
47
+
48
+ tblSelect.addEventListener(\"change\", function () {
49
+ var lhr = changeout(location.href, null, this.value);
50
+ if (brickSchema) lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
51
+ location.href = lhr;
52
+ });
53
+ }
54
+
55
+ if (schemaSelect) { // First drop-down is only present if multitenant
56
+ if (brickSchema = changeout(location.href, \"_brick_schema\")) {
57
+ [... document.getElementsByTagName(\"A\")].forEach(function (a) { a.href = changeout(a.href, \"_brick_schema\", brickSchema); });
58
+ }
59
+ if (schemaSelect.options.length > 1) {
60
+ schemaSelect.value = brickSchema || \"public\";
61
+ schemaSelect.addEventListener(\"change\", function () {
62
+ // If there's an ID then remove it (trim after selected table)
63
+ location.href = changeout(location.href, \"_brick_schema\", this.value, tblSelect.value);
64
+ });
65
+ }
66
+ }
67
+ tblSelect.focus();
68
+
69
+ [... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
70
+ if (brickSchema)
71
+ form.action = changeout(form.action, \"_brick_schema\", brickSchema);
72
+ form.addEventListener('submit', function (ev) {
73
+ [... ev.target.getElementsByTagName(\"SELECT\")].forEach(function (select) {
74
+ if (select.value === \"^^^brick_NULL^^^\") select.value = null;
75
+ });
76
+ return true;
77
+ });
78
+ });
79
+ };
32
80
  "
33
81
 
34
82
  # paths['app/models'] << 'lib/brick/frameworks/active_record/models'
@@ -168,17 +216,9 @@ module Brick
168
216
  end
169
217
  "<script>
170
218
  #{JS_CHANGEOUT}
171
- window.addEventListener(\"load\", linkSchemas);
172
219
  document.addEventListener(\"turbo:render\", linkSchemas);
173
220
  window.addEventListener(\"popstate\", linkSchemas);
174
221
  // [... document.getElementsByTagName('turbo-frame')].forEach(function (a) { a.addEventListener(\"turbo:frame-render\", linkSchemas); });
175
- function linkSchemas() {
176
- brickSchema = changeout(location.href, \"_brick_schema\");
177
- if (brickSchema) {
178
- [... document.getElementsByTagName(\"A\")].forEach(function (a) { a.href = changeout(a.href, \"_brick_schema\", brickSchema); });
179
- [... document.getElementsByTagName(\"FORM\")].forEach(function (form) { form.action = changeout(form.action, \"_brick_schema\", brickSchema); });
180
- }
181
- }
182
222
  </script>
183
223
  #{_brick_content}".html_safe
184
224
  end
@@ -188,8 +228,8 @@ function linkSchemas() {
188
228
  # When available, add a clickable brick icon to go to the Brick version of the page
189
229
  PanelComponent.class_exec do
190
230
  alias _brick_init initialize
191
- def initialize(*args)
192
- _brick_init(*args)
231
+ def initialize(*args, **kwargs)
232
+ _brick_init(*args, **kwargs)
193
233
  @name = BrickTitle.new(@name, self)
194
234
  end
195
235
  end
@@ -223,6 +263,16 @@ function linkSchemas() {
223
263
  _brick_resource_view_path
224
264
  end
225
265
  end
266
+
267
+ module Concerns::HasFields
268
+ class_methods do
269
+ alias _brick_field field
270
+ def field(name, *args, **kwargs, &block)
271
+ kwargs.merge!(args.pop) if args.last.is_a?(Hash)
272
+ _brick_field(name, **kwargs, &block)
273
+ end
274
+ end
275
+ end
226
276
  end # module Avo
227
277
 
228
278
  # Steer any Avo-related controller/action based URL lookups to the Avo RouteSet
@@ -288,7 +338,14 @@ function linkSchemas() {
288
338
  else
289
339
  [[fk_name, pk.length == 1 ? pk.first : pk.inspect]]
290
340
  end
291
- keys << [hm_assoc.inverse_of.foreign_type, hm_assoc.active_record.name] if hm_assoc.options.key?(:as)
341
+ if hm_assoc.options.key?(:as)
342
+ poly_type = if hm_assoc.active_record.column_names.include?(hm_assoc.active_record.inheritance_column)
343
+ '[sti_type]'
344
+ else
345
+ hm_assoc.active_record.name
346
+ end
347
+ keys << [hm_assoc.inverse_of.foreign_type, poly_type]
348
+ end
292
349
  keys.to_h
293
350
  end
294
351
 
@@ -362,9 +419,14 @@ function linkSchemas() {
362
419
  end
363
420
  when 'show', 'new', 'update'
364
421
  hm_stuff << if hm_fk_name
365
- if hm_assoc.klass.column_names.include?(hm_fk_name)
422
+ if hm_assoc.klass.column_names.include?(hm_fk_name) ||
423
+ (hm_fk_name.is_a?(String) && hm_fk_name.include?('.')) # HMT? (Could do a better check for this)
366
424
  predicates = path_keys(hm_assoc, hm_fk_name, pk).map do |k, v|
367
- v.is_a?(String) ? "#{k}: '#{v}'" : "#{k}: @#{obj_name}.#{v}"
425
+ if v == '[sti_type]'
426
+ "'#{k}': @#{obj_name}.#{hm_assoc.active_record.inheritance_column}"
427
+ else
428
+ v.is_a?(String) ? "'#{k}': '#{v}'" : "'#{k}': @#{obj_name}.#{v}"
429
+ end
368
430
  end.join(', ')
369
431
  "<%= link_to '#{assoc_name}', #{hm_assoc.klass._brick_index}_path({ #{predicates} }) %>\n"
370
432
  else
@@ -402,6 +464,12 @@ function linkSchemas() {
402
464
  table_options << "<option value=\"#{prefix}brick_orphans\">(Orphans)</option>".html_safe if is_orphans
403
465
  table_options << "<option value=\"#{prefix}brick_orphans\">(Crosstab)</option>".html_safe if is_crosstab
404
466
  css = +"<style>
467
+ #titleBox {
468
+ position: sticky;
469
+ display: inline-block;
470
+ left: 0;
471
+ }
472
+
405
473
  h1, h3 {
406
474
  margin-bottom: 0;
407
475
  }
@@ -413,7 +481,6 @@ h1, h3 {
413
481
  cursor: pointer;
414
482
  }
415
483
  #mermaidErd {
416
- position: relative;
417
484
  display: none;
418
485
  }
419
486
  #mermaidErd .exclude {
@@ -622,13 +689,24 @@ def hide_bcrypt(val, max_len = 200)
622
689
  end
623
690
  end
624
691
  def display_value(col_type, val)
692
+ is_mssql_geography = nil
693
+ # Some binary thing that really looks like a Microsoft-encoded WGS84 point? (With the first two bytes, E6 10, indicating an EPSG code of 4326)
694
+ if col_type == :binary && val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
695
+ col_type = 'geography'
696
+ is_mssql_geography = true
697
+ end
625
698
  case col_type
626
699
  when 'geometry', 'geography'
627
700
  if Object.const_defined?('RGeo')
628
701
  @is_mysql = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) if @is_mysql.nil?
629
702
  @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
630
703
  val_err = nil
631
- if @is_mysql || @is_mssql
704
+
705
+ if @is_mysql || (is_mssql_geography ||=
706
+ (@is_mssql ||
707
+ (val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12])
708
+ )
709
+ )
632
710
  # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
633
711
  if (srid = val&.[](0..3)&.unpack('I'))
634
712
  val = val.dup.force_encoding('BINARY')[4..-1].bytes
@@ -642,20 +720,25 @@ def display_value(col_type, val)
642
720
  # xx1x xxxx = IsWholeGlobe
643
721
  # Convert Microsoft's unique geography binary to standard WKB
644
722
  # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
645
- if @is_mssql
723
+ if is_mssql_geography
646
724
  if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
647
- (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
648
- idx = 2
649
- new_val = [0, 0, 0, 0, 1]
650
- new_val.concat(val[idx - 8...idx].reverse) while (idx += 8) <= val.length
651
- val = new_val
725
+ (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
726
+ val = [0, 0, 0, 0, 1] + val[2..-1].reverse
652
727
  else
653
728
  val_err = '(Microsoft internal SQL geography type)'
654
729
  end
655
730
  end
656
731
  end
657
732
  end
658
- val_err || (val ? RGeo::WKRep::WKBParser.new.parse(val.pack('c*')) : nil)
733
+ unless val_err || val.nil?
734
+ if (geometry = RGeo::WKRep::WKBParser.new.parse(val.pack('c*'))).is_a?(RGeo::Cartesian::PointImpl) &&
735
+ !(geometry.y == 0.0 && geometry.x == 0.0)
736
+ # Create a POINT link to this style of Google maps URL: https://www.google.com/maps/place/38.7071296+-121.2810649/@38.7071296,-121.2810649,12z
737
+ geometry = \"<a href=\\\"https://www.google.com/maps/place/#\{geometry.y}+#\{geometry.x}/@#\{geometry.y},#\{geometry.x},12z\\\" target=\\\"blank\\\">#\{geometry.to_s}</a>\"
738
+ end
739
+ val = geometry
740
+ end
741
+ val_err || val
659
742
  else
660
743
  '(Add RGeo gem to parse geometry detail)'
661
744
  end
@@ -671,13 +754,13 @@ def display_value(col_type, val)
671
754
  end
672
755
 
673
756
  def image_signatures
674
- @image_signatures ||= { \"\\xFF\\xD8\\xFF\\xEE\" => 'jpeg',
675
- \"\\xFF\\xD8\\xFF\\xE0\\x00\\x10\\x4A\\x46\\x49\\x46\\x00\\x01\" => 'jpeg',
676
- \"\\x89PNG\\r\\n\\x1A\\n\" => 'png',
757
+ @image_signatures ||= { \"\\xFF\\xD8\\xFF\\xEE\".force_encoding('ASCII-8BIT') => 'jpeg',
758
+ \"\\xFF\\xD8\\xFF\\xE0\\x00\\x10\\x4A\\x46\\x49\\x46\\x00\\x01\".force_encoding('ASCII-8BIT') => 'jpeg',
759
+ \"\\x89PNG\\r\\n\\x1A\\n\".force_encoding('ASCII-8BIT') => 'png',
677
760
  '<svg' => 'svg+xml', # %%% Not yet very good detection for SVG
678
- 'BM' => 'bmp',
679
- 'GIF87a' => 'gif',
680
- 'GIF89a' => 'gif' }
761
+ 'BM'.force_encoding('ASCII-8BIT') => 'bmp',
762
+ 'GIF87a'.force_encoding('ASCII-8BIT') => 'gif',
763
+ 'GIF89a'.force_encoding('ASCII-8BIT') => 'gif' }
681
764
  end
682
765
  def display_binary(val)
683
766
  if val[0..1] == \"\\x15\\x1C\" # One of those goofy Microsoft OLE containers?
@@ -741,56 +824,8 @@ callbacks = {} %>
741
824
 
742
825
  # %%% When doing schema select, if we're on a new page go to index
743
826
  script = "<script>
744
- var schemaSelect = document.getElementById(\"schema\");
745
- var tblSelect = document.getElementById(\"tbl\");
746
- var brickSchema;
747
827
  var #{table_name}HtColumns;
748
828
 
749
- // This PageTransitionEvent fires when the page first loads, as well as after any other history
750
- // transition such as when using the browser's Back and Forward buttons.
751
- window.addEventListener(\"pageshow\", function() {
752
- if (tblSelect) { // Always present
753
- var i = #{::Brick.config.path_prefix ? '0' : 'schemaSelect ? 1 : 0'},
754
- changeoutList = changeout(location.href);
755
- for (; i < changeoutList.length; ++i) {
756
- tblSelect.value = changeoutList[i];
757
- if (tblSelect.value !== \"\") break;
758
- }
759
-
760
- tblSelect.addEventListener(\"change\", function () {
761
- var lhr = changeout(location.href, null, this.value);
762
- if (brickSchema)
763
- lhr = changeout(lhr, \"_brick_schema\", schemaSelect.value);
764
- location.href = lhr;
765
- });
766
- }
767
-
768
- if (schemaSelect && schemaSelect.options.length > 1) { // First drop-down is only present if multitenant
769
- brickSchema = changeout(location.href, \"_brick_schema\");
770
- if (brickSchema) {
771
- [... document.getElementsByTagName(\"A\")].forEach(function (a) { a.href = changeout(a.href, \"_brick_schema\", brickSchema); });
772
- }
773
- schemaSelect.value = brickSchema || \"public\";
774
- schemaSelect.focus();
775
- schemaSelect.addEventListener(\"change\", function () {
776
- // If there's an ID then remove it (trim after selected table)
777
- location.href = changeout(location.href, \"_brick_schema\", this.value, tblSelect.value);
778
- });
779
- }
780
-
781
- [... document.getElementsByTagName(\"FORM\")].forEach(function (form) {
782
- if (brickSchema)
783
- form.action = changeout(form.action, \"_brick_schema\", brickSchema);
784
- form.addEventListener('submit', function (ev) {
785
- [... ev.target.getElementsByTagName(\"SELECT\")].forEach(function (select) {
786
- if (select.value === \"^^^brick_NULL^^^\")
787
- select.value = null;
788
- });
789
- return true;
790
- });
791
- });
792
- });
793
-
794
829
  // Add \"Are you sure?\" behaviour to any data-confirm buttons out there
795
830
  document.querySelectorAll(\"input[type=submit][data-confirm]\").forEach(function (btn) {
796
831
  btn.addEventListener(\"click\", function (evt) {
@@ -1092,13 +1127,16 @@ erDiagram
1092
1127
  %></title>
1093
1128
  </head>
1094
1129
  <body>
1130
+ <div id=\"titleBox\">
1095
1131
  <p style=\"color: green\"><%= notice %></p>#{"
1096
1132
  #{schema_options}" if schema_options}
1097
1133
  <select id=\"tbl\">#{table_options}</select>
1098
1134
  <table id=\"resourceName\"><tr>
1099
- <td><h1><%= model.name %></h1></td>
1135
+ <td><h1><%= td_count = 2
1136
+ model.name %></h1></td>
1100
1137
  <td id=\"imgErd\" title=\"Show ERD\"></td>
1101
- <% if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace) %>
1138
+ <% if Object.const_defined?('Avo') && ::Avo.respond_to?(:railtie_namespace)
1139
+ td_count += 1 %>
1102
1140
  <td><%= link_to_brick(
1103
1141
  avo_svg,
1104
1142
  { index_proc: Proc.new do |avo_model|
@@ -1107,7 +1145,9 @@ erDiagram
1107
1145
  title: \"#\{model.name} in Avo\" }
1108
1146
  ) %></td>
1109
1147
  <% end %>
1110
- </tr></table>#{template_link}<%
1148
+ </tr><%= if (page_num = @#{table_name}._brick_page_num)
1149
+ \"<tr><td colspan=\\\"#\{td_count}\\\">Page #\{page_num}</td></tr>\".html_safe
1150
+ end %></table>#{template_link}<%
1111
1151
  if description.present? %><%=
1112
1152
  description %><br><%
1113
1153
  end
@@ -1118,8 +1158,11 @@ erDiagram
1118
1158
  id = id.first if id.is_a?(Array) && id.length == 1
1119
1159
  origin = (key_parts = k.split('.')).length == 1 ? model : model.reflect_on_association(key_parts.first).klass
1120
1160
  if (destination_fk = Brick.relations[origin.table_name][:fks].values.find { |fk| fk[:fk] == key_parts.last }) &&
1121
- (obj = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id)) %>
1122
- <h3>for <%= link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination._brick_index(:singular)\}_path\".to_sym, id) %></h3><%
1161
+ (objs = (destination = origin.reflect_on_association(destination_fk[:assoc_name])&.klass)&.find(id))
1162
+ objs = [objs] unless objs.is_a?(Array) %>
1163
+ <h3>for <% objs.each do |obj| %><%=
1164
+ link_to \"#{"#\{obj.brick_descrip\} (#\{destination.name\})\""}, send(\"#\{destination._brick_index(:singular)\}_path\".to_sym, id)
1165
+ %><% end %></h3><%
1123
1166
  end
1124
1167
  end %>
1125
1168
  (<%= link_to \"See all #\{model.base_class.name.split('::').last.pluralize}\", #{@_brick_model._brick_index}_path %>)
@@ -1139,6 +1182,7 @@ erDiagram
1139
1182
  });
1140
1183
  </script>
1141
1184
  <% end %>
1185
+ </div>
1142
1186
  #{erd_markup}
1143
1187
 
1144
1188
  <%= # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
@@ -1263,7 +1307,9 @@ erDiagram
1263
1307
  <head>
1264
1308
  #{css}
1265
1309
  <title><%=
1266
- page_title = (\"#{model_name}: #\{(obj = @#{obj_name})&.brick_descrip || controller_name}\")
1310
+ model = (obj = @#{obj_name})&.class
1311
+ model_name = @#{obj_name}.#{inh_col = @_brick_model.inheritance_column} if obj.respond_to?(:#{inh_col})
1312
+ page_title = (\"#\{model_name ||= model.name}: #\{obj&.brick_descrip || controller_name}\")
1267
1313
  %></title>
1268
1314
  </head>
1269
1315
  <body>
@@ -1297,12 +1343,12 @@ end
1297
1343
  <% if obj
1298
1344
  # path_options = [obj.#{pk}]
1299
1345
  # path_options << { '_brick_schema': } if
1300
- # url = send(:#\{model_name._brick_index(:singular)}_path, obj.#{pk})
1346
+ # url = send(:#\{model._brick_index(:singular)}_path, obj.#{pk})
1301
1347
  options = {}
1302
1348
  options[:url] = send(\"#\{#{model_name}._brick_index(:singular)}_path\".to_sym, obj) if ::Brick.config.path_prefix
1303
1349
  %>
1304
1350
  <br><br>
1305
- <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
1351
+ <%= form_for(obj.becomes(#{model_name}), options) do |f| %>
1306
1352
  <table class=\"shadow\">
1307
1353
  <% has_fields = false
1308
1354
  @#{obj_name}.attributes.each do |k, val|
@@ -1393,11 +1439,21 @@ end
1393
1439
  # In Postgres labels of data stored in a hierarchical tree-like structure
1394
1440
  # If it's not yet enabled then: create extension ltree;
1395
1441
  val %>
1396
- <% when :binary, :primary_key
1442
+ <% when :binary %>
1443
+ <%= is_revert = false
1444
+ if val
1445
+ # %%% This same kind of geography check is done two other times above ... would be great to DRY it up.
1446
+ if val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
1447
+ display_value('geography', val)
1448
+ else
1449
+ display_binary(val)
1450
+ end.html_safe
1451
+ end %>
1452
+ <% when :primary_key
1397
1453
  is_revert = false %>
1398
1454
  <% else %>
1399
1455
  <%= is_revert = false
1400
- display_value(col_type, val) %>
1456
+ display_value(col_type, val).html_safe %>
1401
1457
  <% end
1402
1458
  end
1403
1459
  if is_revert
@@ -1414,11 +1470,11 @@ end
1414
1470
  <tr><td colspan=\"2\">(No displayable fields)</td></tr>
1415
1471
  <% end %>
1416
1472
  </table>
1417
- <% end %>
1473
+ <% end %>
1418
1474
 
1419
1475
  #{unless args.first == 'new'
1420
- # Was: confirm_are_you_sure = ActionView.version < ::Gem::Version.new('7.0') ? "data: { confirm: 'Delete #{model_name} -- Are you sure?' }" : "form: { data: { turbo_confirm: 'Delete #{model_name} -- Are you sure?' } }"
1421
- confirm_are_you_sure = "data: { confirm: 'Delete #{model_name} -- Are you sure?' }"
1476
+ # Was: confirm_are_you_sure = ActionView.version < ::Gem::Version.new('7.0') ? "data: { confirm: 'Delete #\{model_name} -- Are you sure?' }" : "form: { data: { turbo_confirm: 'Delete #\{model_name} -- Are you sure?' } }"
1477
+ confirm_are_you_sure = "data: { confirm: 'Delete #\{model_name} -- Are you sure?' }"
1422
1478
  hms_headers.each_with_object(+'') do |hm, s|
1423
1479
  # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
1424
1480
  next if hm.first.options[:through] && !hm.first.through_reflection
@@ -1426,11 +1482,24 @@ end
1426
1482
  if (pk = hm.first.klass.primary_key)
1427
1483
  hm_singular_name = (hm_name = hm.first.name.to_s).singularize.underscore
1428
1484
  obj_pk = (pk.is_a?(Array) ? pk : [pk]).each_with_object([]) { |pk_part, s| s << "#{hm_singular_name}.#{pk_part}" }.join(', ')
1485
+ poly_fix = if (poly_type = (hm.first.options[:as] && hm.first.type))
1486
+ "
1487
+ # Let's fix an unexpected \"feature\" of AR -- when going through a polymorphic has_many
1488
+ # association that points to an STI model then filtering for the __able_type column is done
1489
+ # with a .where(). And the polymorphic class name it points to is the base class name of
1490
+ # the STI model instead of its subclass.
1491
+ if (poly_type = #{poly_type.inspect}) &&
1492
+ @#{obj_name}.respond_to?(:#{@_brick_model.inheritance_column}) &&
1493
+ (base_type = collection.where_values_hash[poly_type])
1494
+ collection = collection.rewhere(poly_type => [base_type, @#{obj_name}.#{@_brick_model.inheritance_column}])
1495
+ end"
1496
+ end
1429
1497
  s << "<table id=\"#{hm_name}\" class=\"shadow\">
1430
- <tr><th>#{hm[3]}</th></tr>
1498
+ <tr><th>#{hm[1]}#{' poly' if hm[0].options[:as]} #{hm[3]}</th></tr>
1431
1499
  <% collection = @#{obj_name}.#{hm_name}
1432
1500
  collection = case collection
1433
- when ActiveRecord::Associations::CollectionProxy
1501
+ when ActiveRecord::Associations::CollectionProxy#{
1502
+ poly_fix}
1434
1503
  collection.order(#{pk.inspect})
1435
1504
  when ActiveRecord::Base # Object from a has_one
1436
1505
  [collection]
@@ -1490,7 +1559,7 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1490
1559
  if (imgErd) imgErd.addEventListener(\"click\", showErd);
1491
1560
  function showErd() {
1492
1561
  imgErd.style.display = \"none\";
1493
- mermaidErd.style.display = \"inline-block\";
1562
+ mermaidErd.style.display = \"block\";
1494
1563
  if (mermaidCode) return; // Cut it short if we've already rendered the diagram
1495
1564
 
1496
1565
  mermaidCode = document.createElement(\"SCRIPT\");
@@ -56,85 +56,85 @@ module Brick::Rails::FormTags
56
56
  end
57
57
  out << "</tr></thead>
58
58
  <tbody>"
59
- # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
60
- # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
61
- # TinyTds::Error: Adaptive Server connection timed out
62
- # (After restarting the server it worked fine again.)
63
- relation.each do |obj|
64
- out << "<tr>\n"
65
- out << "<td>#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym,
66
- pk.map { |pk_part| obj.send(pk_part.to_sym) }), { class: 'big-arrow' })}</td>\n" if pk.present?
67
- sequence.each do |col_name|
68
- val = obj.attributes[col_name]
69
- out << '<td'
70
- out << ' class=\"dimmed\"' unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
71
- (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
72
- out << '>'
73
- if (bt = bts[col_name])
74
- if bt[2] # Polymorphic?
75
- bt_class = obj.send("#{bt.first}_type")
76
- base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
77
- poly_id = obj.send("#{bt.first}_id")
78
- out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id)) if poly_id
79
- else # BT or HOT
80
- bt_class = bt[1].first.first
81
- descrips = bt_descrip[bt.first][bt_class]
82
- bt_id_col = if descrips.nil?
83
- puts "Caught it in the act for obj / #{col_name}!"
84
- elsif descrips.length == 1
85
- [obj.class.reflect_on_association(bt.first)&.foreign_key]
86
- else
87
- descrips.last
88
- end
89
- bt_txt = bt_class.brick_descrip(
90
- # 0..62 because Postgres column names are limited to 63 characters
91
- obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, bt_id_col
92
- )
93
- bt_txt = display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
94
- bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
95
- bt_id = bt_id_col&.map { |id_col| obj.respond_to?(id_sym = id_col.to_sym) ? obj.send(id_sym) : id_col }
96
- out << (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
97
- end
98
- elsif (hms_col = hms_cols[col_name])
99
- if hms_col.length == 1
100
- out << hms_col.first
101
- else
102
- hm_klass = (col = cols[col_name])[1]
103
- if col[2] == 'HO'
104
- descrips = bt_descrip[col_name.to_sym][hm_klass]
105
- if (ho_id = (ho_id_col = descrips.last).map { |id_col| obj.send(id_col.to_sym) })&.first
106
- ho_txt = hm_klass.brick_descrip(obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, ho_id_col)
107
- out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
108
- end
59
+ # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
60
+ # ActiveRecord::StatementTimeout in Warehouse::ColdRoomTemperatures_Archive#index
61
+ # TinyTds::Error: Adaptive Server connection timed out
62
+ # (After restarting the server it worked fine again.)
63
+ relation.each do |obj|
64
+ out << "<tr>\n"
65
+ out << "<td>#{link_to('⇛', send("#{klass._brick_index(:singular)}_path".to_sym,
66
+ pk.map { |pk_part| obj.send(pk_part.to_sym) }), { class: 'big-arrow' })}</td>\n" if pk.present?
67
+ sequence.each do |col_name|
68
+ val = obj.attributes[col_name]
69
+ out << '<td'
70
+ out << ' class=\"dimmed\"' unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
71
+ (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
72
+ out << '>'
73
+ if (bt = bts[col_name])
74
+ if bt[2] # Polymorphic?
75
+ bt_class = obj.send("#{bt.first}_type")
76
+ base_class_underscored = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class._brick_index(:singular)
77
+ poly_id = obj.send("#{bt.first}_id")
78
+ out << link_to("#{bt_class} ##{poly_id}", send("#{base_class_underscored}_path".to_sym, poly_id)) if poly_id
79
+ else # BT or HOT
80
+ bt_class = bt[1].first.first
81
+ descrips = bt_descrip[bt.first][bt_class]
82
+ bt_id_col = if descrips.nil?
83
+ puts "Caught it in the act for obj / #{col_name}!"
84
+ elsif descrips.length == 1
85
+ [obj.class.reflect_on_association(bt.first)&.foreign_key]
86
+ else
87
+ descrips.last
88
+ end
89
+ bt_txt = bt_class.brick_descrip(
90
+ # 0..62 because Postgres column names are limited to 63 characters
91
+ obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, bt_id_col
92
+ )
93
+ bt_txt = display_binary(bt_txt).html_safe if bt_txt&.encoding&.name == 'ASCII-8BIT'
94
+ bt_txt ||= "<span class=\"orphan\">&lt;&lt; Orphaned ID: #{val} >></span>" if val
95
+ bt_id = bt_id_col&.map { |id_col| obj.respond_to?(id_sym = id_col.to_sym) ? obj.send(id_sym) : id_col }
96
+ out << (bt_id&.first ? link_to(bt_txt, send("#{bt_class.base_class._brick_index(:singular)}_path".to_sym, bt_id)) : bt_txt || '')
97
+ end
98
+ elsif (hms_col = hms_cols[col_name])
99
+ if hms_col.length == 1
100
+ out << hms_col.first
109
101
  else
110
- if (ct = obj.send(hms_col[1].to_sym)&.to_i)&.positive?
111
- out << "#{link_to("#{ct || 'View'} #{hms_col.first}",
112
- send("#{hm_klass._brick_index}_path".to_sym,
113
- hms_col[2].each_with_object({}) { |v, s| s[v.first] = v.last.is_a?(String) ? v.last : obj.send(v.last) })
114
- )}\n"
102
+ hm_klass = (col = cols[col_name])[1]
103
+ if col[2] == 'HO'
104
+ descrips = bt_descrip[col_name.to_sym][hm_klass]
105
+ if (ho_id = (ho_id_col = descrips.last).map { |id_col| obj.send(id_col.to_sym) })&.first
106
+ ho_txt = hm_klass.brick_descrip(obj, descrips[0..-2].map { |id| obj.send(id.last[0..62]) }, ho_id_col)
107
+ out << link_to(ho_txt, send("#{hm_klass.base_class._brick_index(:singular)}_path".to_sym, ho_id))
108
+ end
109
+ else
110
+ if (ct = obj.send(hms_col[1].to_sym)&.to_i)&.positive?
111
+ out << "#{link_to("#{ct || 'View'} #{hms_col.first}",
112
+ send("#{hm_klass._brick_index}_path".to_sym,
113
+ hms_col[2].each_with_object({}) { |v, s| s[v.first] = v.last.is_a?(String) ? v.last : obj.send(v.last) })
114
+ )}\n"
115
+ end
115
116
  end
116
117
  end
118
+ elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
119
+ binding.pry if col.is_a?(Array)
120
+ col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
121
+ out << display_value(col_type || col&.sql_type, val).to_s
122
+ elsif cust_col
123
+ data = cust_col.first.map { |cc_part| obj.send(cc_part.last) }
124
+ cust_txt = klass.brick_descrip(cust_col[-2], data)
125
+ if (link_id = obj.send(cust_col.last[1]) if cust_col.last)
126
+ out << link_to(cust_txt, send("#{cust_col.last.first._brick_index(:singular)}_path", link_id))
127
+ else
128
+ out << (cust_txt || '')
129
+ end
130
+ else # Bad column name!
131
+ out << '?'
117
132
  end
118
- elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
119
- binding.pry if col.is_a?(Array)
120
- col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
121
- out << display_value(col_type || col&.sql_type, val).to_s
122
- elsif cust_col
123
- data = cust_col.first.map { |cc_part| obj.send(cc_part.last) }
124
- cust_txt = klass.brick_descrip(cust_col[-2], data)
125
- if (link_id = obj.send(cust_col.last[1]) if cust_col.last)
126
- out << link_to(cust_txt, send("#{cust_col.last.first._brick_index(:singular)}_path", link_id))
127
- else
128
- out << (cust_txt || '')
129
- end
130
- else # Bad column name!
131
- out << '?'
133
+ out << '</td>'
132
134
  end
133
- out << '</td>'
135
+ out << '</tr>'
134
136
  end
135
- out << '</tr>'
136
- end
137
- out << " </tbody>
137
+ out << " </tbody>
138
138
  </table>
139
139
  "
140
140
  out.html_safe
@@ -143,6 +143,16 @@ module Brick::Rails::FormTags
143
143
  def link_to_brick(*args, **kwargs)
144
144
  return unless ::Brick.config.mode == :on
145
145
 
146
+ kwargs.merge!(args.pop) if args.last.is_a?(Hash)
147
+ # Avoid infinite recursion
148
+ if (visited = kwargs.fetch(:visited, nil))
149
+ return if visited.key?(object_id)
150
+
151
+ kwargs[:visited][object_id] = nil
152
+ else
153
+ kwargs[:visited] = {}
154
+ end
155
+
146
156
  text = ((args.first.is_a?(String) || args.first.is_a?(Proc)) && args.shift) || args[1]
147
157
  text = text.call if text.is_a?(Proc)
148
158
  klass_or_obj = ((args.first.is_a?(ActiveRecord::Relation) ||
@@ -192,13 +202,14 @@ module Brick::Rails::FormTags
192
202
  app_routes = Rails.application.routes # In case we're operating in another engine, reference the application since Brick routes are placed there.
193
203
  if (klass_or_obj&.is_a?(Class) && klass_or_obj < ActiveRecord::Base) ||
194
204
  (klass_or_obj&.is_a?(ActiveRecord::Base) && klass_or_obj.new_record? && (klass_or_obj = klass_or_obj.class))
195
- path = (proc = kwargs[:index_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.base_class._brick_index, action: :index)}#{filter}"
205
+ path = (proc = kwargs[:index_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.base_class._brick_index(nil, '/'), action: :index)}#{filter}"
196
206
  lt_args = [text || "Index for #{klass_or_obj.name.pluralize}", path]
197
207
  else
198
208
  # If there are multiple incoming parameters then last one is probably the actual ID, and first few might be some nested tree of stuff leading up to it
199
- path = (proc = kwargs[:show_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.class.base_class._brick_index, action: :show, id: klass_or_obj)}#{filter}"
209
+ path = (proc = kwargs[:show_proc]) ? proc.call(klass_or_obj) : "#{app_routes.path_for(controller: klass_or_obj.class.base_class._brick_index(nil, '/'), action: :show, id: klass_or_obj)}#{filter}"
200
210
  lt_args = [text || "Show this #{klass_or_obj.class.name}", path]
201
211
  end
212
+ kwargs.delete(:visited)
202
213
  link_to(*lt_args, **kwargs)
203
214
  else
204
215
  # puts "Warning: link_to_brick could not find a class for \"#{controller_path}\" -- consider setting @_brick_model within that controller."
@@ -215,7 +226,7 @@ module Brick::Rails::FormTags
215
226
  if links.length == 1 # If there's only one match then use any text that was supplied
216
227
  link_to_brick(text || links.first.last.join('/'), links.first.first, **kwargs)
217
228
  else
218
- links.map { |k, v| link_to_brick(v.join('/'), v, **kwargs) }.join(' &nbsp; ').html_safe
229
+ links.each_with_object([]) { |v, s| s << link if link = link_to_brick(v.join('/'), v, **kwargs) }.join(' &nbsp; ').html_safe
219
230
  end
220
231
  end
221
232
  end # link_to_brick
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 102
8
+ TINY = 104
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -288,16 +288,18 @@ if ActiveRecord::Base.respond_to?(:brick_select)
288
288
 
289
289
  # # POLYMORPHIC ASSOCIATIONS
290
290
 
291
- # # Database schema to use when analysing existing data, such as deriving a list of polymorphic classes in the case that
292
- # # it wasn't originally specified.
291
+ # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
292
+
293
+ # # For multi-tenant databases that use a separate schema for each tenant, a single representative database schema
294
+ # # can be analysed to determine the range of polymorphic classes that can be used for each association. Hopefully
295
+ # # the schema chosen is one loaded with existing data that is representative of all possible polymorphic
296
+ # # associations.
293
297
  # Brick.schema_behavior = :namespaced
294
298
  #{Brick.config.schema_behavior.present? ? " Brick.schema_behavior = { multitenant: { schema_to_analyse: #{
295
299
  Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil).inspect}" :
296
300
  " # Brick.schema_behavior = { multitenant: { schema_to_analyse: 'engineering'"
297
301
  } } }
298
302
 
299
- # # Polymorphic associations are set up by providing a model name and polymorphic association name#{poly}
300
-
301
303
  # # DEFAULT ROOT ROUTE
302
304
 
303
305
  # # If a default route is not supplied, Brick attempts to find the most \"central\" table and wires up the default
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.102
4
+ version: 1.0.104
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-03 00:00:00.000000000 Z
11
+ date: 2023-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -164,20 +164,6 @@ dependencies:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
166
  version: 1.42.0
167
- - !ruby/object:Gem::Dependency
168
- name: mysql2
169
- requirement: !ruby/object:Gem::Requirement
170
- requirements:
171
- - - "~>"
172
- - !ruby/object:Gem::Version
173
- version: '0.5'
174
- type: :development
175
- prerelease: false
176
- version_requirements: !ruby/object:Gem::Requirement
177
- requirements:
178
- - - "~>"
179
- - !ruby/object:Gem::Version
180
- version: '0.5'
181
167
  - !ruby/object:Gem::Dependency
182
168
  name: pg
183
169
  requirement: !ruby/object:Gem::Requirement