brick 1.0.102 → 1.0.104

Sign up to get free protection for your applications and to get access to all the features.
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