brick 1.0.124 → 1.0.126

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: 44fce70a53b4467d019ca42a9826f44a4849d472dcbfc2fe324f961708b70a18
4
- data.tar.gz: 2674709f203786560b41c1bf1e2062336cf88282d168848303e9920b398662e3
3
+ metadata.gz: b59690045d31108dece3222f655503a771ab860697993459a6beb92aa0ecdb07
4
+ data.tar.gz: 4c83d54ddc01e03330ac82f5949cece3f21631624adb704b923bd338ac84481c
5
5
  SHA512:
6
- metadata.gz: 337760a0382fd37a64026a07d80e708f9f475f73e8ca420d05341b731c977ab9153556803f10314a4851730e28a22ce4847705e2d8ad81121a8f33736f505f12
7
- data.tar.gz: 04a1e5ec1cc503173da16be708d611f9fb1dd6f3965e6e848dfd1e310f3e1c94b396c8e693982d8f3c3589b5d8ffbfee70fd52593918e12ecd8309c8e110b4c5
6
+ metadata.gz: c12e4073de8230683825617fccabeeda71746dcf3c892a60557990777543c15a3e9a18a8c7bdcf3b1c236bf422ec74d10b98bf1341b5d472e611799c8e74de31
7
+ data.tar.gz: 6e9fdb9e41ff12a1b5092226133cb7c0d8444bf330e2557da156bc7f21d4377678061bd65c5b36ad3b9d1f9bf88203594c34cf8bcaabad7b93c2609b2798e474
@@ -19,19 +19,17 @@ unless ActiveSupport.respond_to?(:version)
19
19
  end
20
20
  end
21
21
  end
22
- if Object.const_defined?('ActionPack')
23
- unless ActionPack.respond_to?(:version)
24
- module ActionPack
25
- def self.version
26
- ::Gem::Version.new(ActionPack::VERSION::STRING)
27
- end
22
+ if Object.const_defined?('ActionPack') && !ActionPack.respond_to?(:version)
23
+ module ActionPack
24
+ def self.version
25
+ ::Gem::Version.new(ActionPack::VERSION::STRING)
28
26
  end
29
27
  end
30
- if Object.const_defined?('ActionView') && !ActionView.respond_to?(:version)
31
- module ActionView
32
- def self.version
33
- ActionPack.version
34
- end
28
+ end
29
+ if Object.const_defined?('ActionView') && !ActionView.respond_to?(:version)
30
+ module ActionView
31
+ def self.version
32
+ ActionPack.version
35
33
  end
36
34
  end
37
35
  end
@@ -68,6 +68,12 @@ module ActiveRecord
68
68
  self
69
69
  end
70
70
  end
71
+
72
+ def json_column?(col)
73
+ col.type == :json || ::Brick.config.json_columns[table_name]&.include?(col.name) ||
74
+ ((attr_types = attribute_types[col.name]).respond_to?(:coder) &&
75
+ (attr_types.coder.is_a?(Class) ? attr_types.coder : attr_types.coder&.class)&.name&.end_with?('JSON'))
76
+ end
71
77
  end
72
78
 
73
79
  def self._brick_primary_key(relation = nil)
@@ -225,6 +231,9 @@ module ActiveRecord
225
231
  if this_obj.is_a?(ActiveRecord::Base) && (obj_descrip = this_obj.class.brick_descrip(this_obj))
226
232
  this_obj = obj_descrip
227
233
  end
234
+ if this_obj.is_a?(ActiveStorage::Filename) && this_obj.instance_variable_get(:@filename).nil?
235
+ this_obj.instance_variable_set(:@filename, '')
236
+ end
228
237
  this_obj&.to_s || ''
229
238
  end
230
239
  is_brackets_have_content = true unless datum.blank?
@@ -671,7 +680,7 @@ module ActiveRecord
671
680
  link_back << nm
672
681
  num_bt_things += 1
673
682
  # puts "BT #{a.table_name}"
674
- "ON br_t#{idx}.id = br_t#{idx - 1}.#{a.foreign_key}"
683
+ "ON br_t#{idx}.#{a.active_record.primary_key} = br_t#{idx - 1}.#{a.foreign_key}"
675
684
  elsif src_ref.options[:as]
676
685
  "ON br_t#{idx}.#{src_ref.type} = '#{src_ref.active_record.name}'" + # "polymorphable_type"
677
686
  " AND br_t#{idx}.#{src_ref.foreign_key} = br_t#{idx - 1}.id"
@@ -682,11 +691,11 @@ module ActiveRecord
682
691
  bail_out = true
683
692
  break
684
693
  # "ON br_t#{idx}.#{a.foreign_type} = '#{src_ref.options[:source_type]}' AND " \
685
- # "br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id"
694
+ # "br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.#{a.active_record.primary_key}"
686
695
  else # Works for HMT through a polymorphic HO
687
696
  link_back << hmt_assoc.source_reflection.inverse_of&.name # Some polymorphic "_able" thing
688
697
  "ON br_t#{idx - 1}.#{a.foreign_type} = '#{src_ref.options[:source_type]}' AND " \
689
- "br_t#{idx - 1}.#{a.foreign_key} = br_t#{idx}.id"
698
+ "br_t#{idx - 1}.#{a.foreign_key} = br_t#{idx}.#{a.active_record.primary_key}"
690
699
  end
691
700
  else # Standard has_many or has_one
692
701
  # puts "HM #{a.table_name}"
@@ -694,7 +703,7 @@ module ActiveRecord
694
703
  nm = hmt_assoc.source_reflection.inverse_of&.name
695
704
  # )
696
705
  link_back << nm # if nm
697
- "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.id"
706
+ "ON br_t#{idx}.#{a.foreign_key} = br_t#{idx - 1}.#{a.active_record.primary_key}"
698
707
  end
699
708
  link_back.unshift(a.source_reflection.name)
700
709
  [a.table_name, a.foreign_key, a.source_reflection.macro]
@@ -856,8 +865,7 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
856
865
  alias _brick_find_sti_class find_sti_class
857
866
  def find_sti_class(type_name)
858
867
  if ::Brick.sti_models.key?(type_name ||= name)
859
- # Used to be: ::Brick.sti_models[type_name].fetch(:base, nil) || _brick_find_sti_class(type_name)
860
- _brick_find_sti_class(type_name)
868
+ ::Brick.sti_models[type_name].fetch(:base, nil) || _brick_find_sti_class(type_name)
861
869
  else
862
870
  # This auto-STI is more of a brute-force approach, building modules where needed
863
871
  # The more graceful alternative is the overload of ActiveSupport::Dependencies#autoload_module! found below
@@ -865,9 +873,13 @@ JOIN (SELECT #{hm_selects.map { |s| "#{'br_t0.' if from_clause}#{s}" }.join(', '
865
873
  module_prefixes = type_name.split('::')
866
874
  module_prefixes.unshift('') unless module_prefixes.first.blank?
867
875
  module_name = module_prefixes[0..-2].join('::')
868
- if (snp = ::Brick.config.sti_namespace_prefixes)&.key?("::#{module_name}::") || snp&.key?("#{module_name}::") ||
876
+ if (base_name = ::Brick.config.sti_namespace_prefixes&.fetch("#{module_name}::", nil)) ||
869
877
  File.exist?(candidate_file = ::Rails.root.join('app/models' + module_prefixes.map(&:underscore).join('/') + '.rb'))
870
- _brick_find_sti_class(type_name) # Find this STI class normally
878
+ if base_name
879
+ base_name == "::#{name}" ? self : base_name.constantize
880
+ else
881
+ _brick_find_sti_class(type_name) # Find this STI class normally
882
+ end
871
883
  else
872
884
  # Build missing prefix modules if they don't yet exist
873
885
  this_module = Object
@@ -905,8 +917,23 @@ end
905
917
 
906
918
  if Object.const_defined?('ActionView')
907
919
  require 'brick/frameworks/rails/form_tags'
908
- module ActionView::Helpers::FormTagHelper
909
- include ::Brick::Rails::FormTags
920
+ require 'brick/frameworks/rails/form_builder'
921
+ module ActionView::Helpers
922
+ module FormTagHelper
923
+ include ::Brick::Rails::FormTags
924
+ end
925
+ FormBuilder.class_exec do
926
+ include ::Brick::Rails::FormBuilder
927
+ end
928
+ end
929
+
930
+ # FormBuilder#field_id isn't available in Rails < 7.0. This is a rudimentary version with no `index`.
931
+ unless ActionView::Helpers::FormBuilder.methods.include?(:field_id)
932
+ ActionView::Helpers::FormBuilder.class_exec do
933
+ def field_id(method)
934
+ [object_name, method.to_s].join('_')
935
+ end
936
+ end
910
937
  end
911
938
  end
912
939
 
@@ -988,6 +1015,8 @@ Module.class_exec do
988
1015
  end
989
1016
  Object
990
1017
  else
1018
+ sti_base = (::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::#{requested}", nil) ||
1019
+ ::Brick.config.sti_namespace_prefixes&.fetch("::#{name}::", nil))&.constantize
991
1020
  self
992
1021
  end
993
1022
  # puts "#{self.name} - #{args.first}"
@@ -995,7 +1024,7 @@ Module.class_exec do
995
1024
  if ((is_defined = self.const_defined?(args.first)) && (possible = self.const_get(args.first)) &&
996
1025
  # Reset `possible` if it's a controller request that's not a perfect match
997
1026
  # Was: (possible = nil) but changed to #local_variable_set in order to suppress the "= should be ==" warning
998
- (possible.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||
1027
+ (possible&.name == desired_classname || (is_controller && binding.local_variable_set(:possible, nil)))) ||
999
1028
  # Try to require the respective Ruby file
1000
1029
  ((filename = ActiveSupport::Dependencies.search_for_file(desired_classname.underscore) ||
1001
1030
  (self != Object && ActiveSupport::Dependencies.search_for_file((desired_classname = requested).underscore))
@@ -1005,9 +1034,11 @@ Module.class_exec do
1005
1034
  # If any class has turned up so far (and we're not in the middle of eager loading)
1006
1035
  # then return what we've found.
1007
1036
  (is_defined && !::Brick.is_eager_loading) # Used to also have: && possible != self
1008
- if (!brick_root && (filename || possible.instance_of?(Class))) ||
1009
- (possible.instance_of?(Module) && possible&.module_parent == self) ||
1010
- (possible.instance_of?(Class) && possible == self) # Are we simply searching for ourselves?
1037
+ if ((!brick_root && (filename || possible.instance_of?(Class))) ||
1038
+ (possible.instance_of?(Module) && possible&.module_parent == self) ||
1039
+ (possible.instance_of?(Class) && possible == self)) && # Are we simply searching for ourselves?
1040
+ # Skip when what we found as `possible` is not related to the base class of an STI model
1041
+ (!sti_base || possible.is_a?(sti_base))
1011
1042
  return possible
1012
1043
  end
1013
1044
  end
@@ -1139,7 +1170,8 @@ class Object
1139
1170
 
1140
1171
  def build_model(relations, base_module, base_name, class_name, inheritable_name = nil)
1141
1172
  tnp = ::Brick.config.table_name_prefixes&.find { |p| p.last == base_module.name }&.first
1142
- if (base_model = ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil)&.constantize) || # Are we part of an auto-STI namespace? ...
1173
+ if (base_model = (::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::#{class_name}", nil) || # Are we part of an auto-STI namespace? ...
1174
+ ::Brick.config.sti_namespace_prefixes&.fetch("::#{base_module.name}::", nil))&.constantize) ||
1143
1175
  base_module != Object # ... or otherwise already in some namespace?
1144
1176
  schema_name = [(singular_schema_name = base_name.underscore),
1145
1177
  (schema_name = singular_schema_name.pluralize),
@@ -1151,7 +1183,6 @@ class Object
1151
1183
  singular_table_name = ActiveSupport::Inflector.underscore(model_name).gsub('/', '.')
1152
1184
 
1153
1185
  if base_model
1154
- schema_name = base_name.underscore # For the auto-STI namespace models
1155
1186
  table_name = base_model.table_name
1156
1187
  build_model_worker(base_module, inheritable_name, model_name, singular_table_name, table_name, relations, table_name)
1157
1188
  else
@@ -1181,7 +1212,7 @@ class Object
1181
1212
  full_name = if relation || schema_name.blank?
1182
1213
  inheritable_name || model_name
1183
1214
  else # Prefix the schema to the table name + prefix the schema namespace to the class name
1184
- schema_module = if schema_name.instance_of?(Module) # from an auto-STI namespace?
1215
+ schema_module = if schema_name.is_a?(Module) # from an auto-STI namespace?
1185
1216
  schema_name
1186
1217
  else
1187
1218
  matching = "#{schema_name}.#{matching}"
@@ -1804,7 +1835,13 @@ class Object
1804
1835
  code << " end\n"
1805
1836
  self.define_method :new do
1806
1837
  _schema, @_is_show_schema_list = ::Brick.set_db_schema(params)
1807
- instance_variable_set("@#{singular_table_name}".to_sym, model.new)
1838
+ if (new_obj = model.new).respond_to?(:serializable_hash)
1839
+ # Convert any Filename objects with nil into an empty string so that #encode can be called on them
1840
+ new_obj.serializable_hash.each do |k, v|
1841
+ new_obj.send("#{k}=", ActiveStorage::Filename.new('')) if v.is_a?(ActiveStorage::Filename) && !v.instance_variable_get(:@filename)
1842
+ end
1843
+ end
1844
+ instance_variable_set("@#{singular_table_name}".to_sym, new_obj)
1808
1845
  end
1809
1846
 
1810
1847
  params_name_sym = (params_name = "#{singular_table_name}_params").to_sym
@@ -1880,7 +1917,7 @@ class Object
1880
1917
  upd_hash['invitation_accepted_at'] = nil if upd_hash['invitation_accepted_at'].blank?
1881
1918
  end
1882
1919
  end
1883
- if (json_cols = model.columns.select { |c| c.type == :json || json_overrides&.include?(c.name) }.map(&:name)).present?
1920
+ if (json_cols = model.columns.select { |c| model.json_column?(c) }.map(&:name)).present?
1884
1921
  upd_hash ||= upd_params.to_h
1885
1922
  json_cols.each do |c|
1886
1923
  begin
@@ -2100,7 +2137,6 @@ end.class_exec do
2100
2137
  load apartment_initializer
2101
2138
  @_apartment_loaded = true
2102
2139
  end
2103
- apartment_excluded = Apartment.excluded_models
2104
2140
  end
2105
2141
  # Only for Postgres (Doesn't work in sqlite3 or MySQL)
2106
2142
  # puts ActiveRecord::Base.execute_sql("SELECT current_setting('SEARCH_PATH')").to_a.inspect
@@ -2190,7 +2226,7 @@ end.class_exec do
2190
2226
  # If Apartment gem lists the table as being associated with a non-tenanted model then use whatever it thinks
2191
2227
  # is the default schema, usually 'public'.
2192
2228
  schema_name = if ::Brick.config.schema_behavior[:multitenant]
2193
- ::Brick.apartment_default_tenant if apartment_excluded&.include?(r['relation_name'].singularize.camelize)
2229
+ ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(r['relation_name'])
2194
2230
  elsif ![schema, 'public'].include?(r['schema'])
2195
2231
  r['schema']
2196
2232
  end
@@ -2340,7 +2376,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2340
2376
  fk_references&.each do |fk|
2341
2377
  fk = fk.values unless fk.is_a?(Array)
2342
2378
  # Multitenancy makes things a little more general overall, except for non-tenanted tables
2343
- if apartment_excluded&.include?(::Brick.namify(fk[1]).singularize.camelize)
2379
+ if ::Brick.is_apartment_excluded_table(::Brick.namify(fk[1]))
2344
2380
  fk[0] = ::Brick.apartment_default_tenant
2345
2381
  elsif (is_postgres && (fk[0] == 'public' || (multitenancy && fk[0] == schema))) ||
2346
2382
  (::Brick.is_oracle && fk[0] == schema) ||
@@ -2348,7 +2384,7 @@ ORDER BY 1, 2, c.internal_column_id, acc.position"
2348
2384
  (!is_postgres && !::Brick.is_oracle && !is_mssql && ['mysql', 'performance_schema', 'sys'].exclude?(fk[0]))
2349
2385
  fk[0] = nil
2350
2386
  end
2351
- if apartment_excluded&.include?(fk[4].singularize.camelize)
2387
+ if ::Brick.is_apartment_excluded_table(fk[4])
2352
2388
  fk[3] = ::Brick.apartment_default_tenant
2353
2389
  elsif (is_postgres && (fk[3] == 'public' || (multitenancy && fk[3] == schema))) ||
2354
2390
  (::Brick.is_oracle && fk[3] == schema) ||
@@ -2524,8 +2560,7 @@ module Brick
2524
2560
  # %%% Temporary schema patch
2525
2561
  for_tbl = fk[1]
2526
2562
  fk_namified = ::Brick.namify(fk[1])
2527
- apartment = Object.const_defined?('Apartment') && Apartment
2528
- fk[0] = ::Brick.apartment_default_tenant if apartment && apartment.excluded_models.include?(fk_namified.singularize.camelize)
2563
+ fk[0] = ::Brick.apartment_default_tenant if ::Brick.is_apartment_excluded_table(fk_namified)
2529
2564
  fk[1] = "#{fk[0]}.#{fk[1]}" if fk[0] # && fk[0] != ::Brick.default_schema
2530
2565
  bts = (relation = relations.fetch(fk[1], nil))&.fetch(:fks) { relation[:fks] = {} }
2531
2566
 
@@ -2540,7 +2575,7 @@ module Brick
2540
2575
  is_schema = if ::Brick.config.schema_behavior[:multitenant]
2541
2576
  # If Apartment gem lists the primary table as being associated with a non-tenanted model
2542
2577
  # then use 'public' schema for the primary table
2543
- if apartment && apartment&.excluded_models.include?(fk[4].singularize.camelize)
2578
+ if ::Brick.is_apartment_excluded_table(fk[4])
2544
2579
  fk[3] = ::Brick.apartment_default_tenant
2545
2580
  true
2546
2581
  end
@@ -2617,7 +2652,7 @@ module Brick
2617
2652
  end
2618
2653
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
2619
2654
  else
2620
- inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && apartment && fk[0] == ::Brick.apartment_default_tenant
2655
+ inv_tbl = if ::Brick.config.schema_behavior[:multitenant] && Object.const_defined?('Apartment') && fk[0] == ::Brick.apartment_default_tenant
2621
2656
  for_tbl
2622
2657
  else
2623
2658
  fk[1]
@@ -2682,11 +2717,13 @@ module Brick
2682
2717
  end
2683
2718
  end
2684
2719
  end
2685
- ::Brick.relations.keys.map do |v|
2686
- tbl_parts = v.split('.')
2720
+ ::Brick.relations.map do |k, v|
2721
+ tbl_parts = k.split('.')
2687
2722
  tbl_parts.shift if ::Brick.apartment_multitenant && tbl_parts.length > 1 && tbl_parts.first == ::Brick.apartment_default_tenant
2688
2723
  res = tbl_parts.join('.')
2689
- [v, (model = models[res])&.last&.table_name, migrations&.fetch(res, nil), model&.first]
2724
+ [k, (model = models[res])&.last&.table_name || v[:class_name].constantize.table_name,
2725
+ migrations&.fetch(res, nil),
2726
+ model&.first]
2690
2727
  end
2691
2728
  end
2692
2729
 
@@ -2785,5 +2822,13 @@ module Brick
2785
2822
  def _class_pk(dotted_name, multitenant)
2786
2823
  Object.const_get((multitenant ? [dotted_name.split('.').last] : dotted_name.split('.')).map { |nm| "::#{nm.singularize.camelize}" }.join).primary_key
2787
2824
  end
2825
+
2826
+ def is_apartment_excluded_table(tbl)
2827
+ if Object.const_defined?('Apartment')
2828
+ tbl_klass = (tnp = ::Brick.config.table_name_prefixes&.find { |k, _v| tbl.start_with?(k) }) ? +"#{tnp.last}::" : +''
2829
+ tbl_klass << tbl[tnp&.first&.length || 0..-1].singularize.camelize
2830
+ Apartment.excluded_models&.include?(tbl_klass)
2831
+ end
2832
+ end
2788
2833
  end
2789
2834
  end
@@ -2,36 +2,103 @@
2
2
 
3
3
  module Brick
4
4
  module Rails
5
- def self.display_binary(val)
6
- @image_signatures ||= { (+"\xFF\xD8\xFF\xEE").force_encoding('ASCII-8BIT') => 'jpeg',
7
- (+"\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01").force_encoding('ASCII-8BIT') => 'jpeg',
8
- (+"\x89PNG\r\n\x1A\n").force_encoding('ASCII-8BIT') => 'png',
9
- '<svg' => 'svg+xml', # %%% Not yet very good detection for SVG
10
- (+'BM').force_encoding('ASCII-8BIT') => 'bmp',
11
- (+'GIF87a').force_encoding('ASCII-8BIT') => 'gif',
12
- (+'GIF89a').force_encoding('ASCII-8BIT') => 'gif' }
13
-
14
- if val[0..1] == "\x15\x1C" # One of those goofy Microsoft OLE containers?
15
- package_header_length = val[2..3].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
16
- # This will often be just FF FF FF FF
17
- # object_size = val[16..19].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
18
- friendly_and_class_names = val[20...package_header_length].split("\0")
19
- object_type_name_length = val[package_header_length + 8..package_header_length+11].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
20
- friendly_and_class_names << val[package_header_length + 12...package_header_length + 12 + object_type_name_length].strip
21
- # friendly_and_class_names will now be something like: ['Bitmap Image', 'Paint.Picture', 'PBrush']
22
- real_object_size = val[package_header_length + 20 + object_type_name_length..package_header_length + 23 + object_type_name_length].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
23
- object_start = package_header_length + 24 + object_type_name_length
24
- val = val[object_start...object_start + real_object_size]
5
+ class << self
6
+ def display_value(col_type, val)
7
+ is_mssql_geography = nil
8
+ # 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)
9
+ if col_type == :binary && val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
10
+ col_type = 'geography'
11
+ is_mssql_geography = true
12
+ end
13
+ case col_type
14
+ when 'geometry', 'geography'
15
+ if Object.const_defined?('RGeo')
16
+ @is_mysql = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) if @is_mysql.nil?
17
+ @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
18
+ val_err = nil
19
+
20
+ if @is_mysql || (is_mssql_geography ||=
21
+ (@is_mssql ||
22
+ (val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12])
23
+ )
24
+ )
25
+ # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
26
+ if (srid = val&.[](0..3)&.unpack('I'))
27
+ val = val.dup.force_encoding('BINARY')[4..-1].bytes
28
+
29
+ # MSSQL spatial bitwise flags, often 0C for a point:
30
+ # xxxx xxx1 = HasZValues
31
+ # xxxx xx1x = HasMValues
32
+ # xxxx x1xx = IsValid
33
+ # xxxx 1xxx = IsSinglePoint
34
+ # xxx1 xxxx = IsSingleLineSegment
35
+ # xx1x xxxx = IsWholeGlobe
36
+ # Convert Microsoft's unique geography binary to standard WKB
37
+ # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
38
+ if is_mssql_geography
39
+ if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
40
+ (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
41
+ val = [0, 0, 0, 0, 1] + val[2..-1].reverse
42
+ else
43
+ val_err = '(Microsoft internal SQL geography type)'
44
+ end
45
+ end
46
+ end
47
+ end
48
+ unless val_err || val.nil?
49
+ if (geometry = RGeo::WKRep::WKBParser.new.parse(val.pack('c*'))).is_a?(RGeo::Cartesian::PointImpl) &&
50
+ !(geometry.y == 0.0 && geometry.x == 0.0)
51
+ # 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
52
+ geometry = "<a href=\"https://www.google.com/maps/place/#{geometry.y}+#{geometry.x}/@#{geometry.y},#{geometry.x},12z\" target=\"blank\">#{geometry.to_s}</a>"
53
+ end
54
+ val = geometry
55
+ end
56
+ val_err || val
57
+ else
58
+ '(Add RGeo gem to parse geometry detail)'
59
+ end
60
+ when :binary
61
+ ::Brick::Rails.display_binary(val) if val
62
+ else
63
+ if col_type
64
+ ::Brick::Rails::FormBuilder.hide_bcrypt(val, col_type == :xml)
65
+ else
66
+ '?'
67
+ end
68
+ end
25
69
  end
26
70
 
27
- if (signature = @image_signatures.find { |k, _v| val[0...k.length] == k })
28
- if val.length < 500_000
29
- "<img src=\"data:image/#{signature.last};base64,#{Base64.encode64(val)}\">"
71
+ def display_binary(val)
72
+ @image_signatures ||= { (+"\xFF\xD8\xFF\xEE").force_encoding('ASCII-8BIT') => 'jpeg',
73
+ (+"\xFF\xD8\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00\x01").force_encoding('ASCII-8BIT') => 'jpeg',
74
+ (+"\x89PNG\r\n\x1A\n").force_encoding('ASCII-8BIT') => 'png',
75
+ '<svg' => 'svg+xml', # %%% Not yet very good detection for SVG
76
+ (+'BM').force_encoding('ASCII-8BIT') => 'bmp',
77
+ (+'GIF87a').force_encoding('ASCII-8BIT') => 'gif',
78
+ (+'GIF89a').force_encoding('ASCII-8BIT') => 'gif' }
79
+
80
+ if val[0..1] == "\x15\x1C" # One of those goofy Microsoft OLE containers?
81
+ package_header_length = val[2..3].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
82
+ # This will often be just FF FF FF FF
83
+ # object_size = val[16..19].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
84
+ friendly_and_class_names = val[20...package_header_length].split("\0")
85
+ object_type_name_length = val[package_header_length + 8..package_header_length+11].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
86
+ friendly_and_class_names << val[package_header_length + 12...package_header_length + 12 + object_type_name_length].strip
87
+ # friendly_and_class_names will now be something like: ['Bitmap Image', 'Paint.Picture', 'PBrush']
88
+ real_object_size = val[package_header_length + 20 + object_type_name_length..package_header_length + 23 + object_type_name_length].bytes.reverse.inject(0) {|m, b| (m << 8) + b }
89
+ object_start = package_header_length + 24 + object_type_name_length
90
+ val = val[object_start...object_start + real_object_size]
91
+ end
92
+
93
+ if (signature = @image_signatures.find { |k, _v| val[0...k.length] == k })
94
+ if val.length < 500_000
95
+ "<img src=\"data:image/#{signature.last};base64,#{Base64.encode64(val)}\">"
96
+ else
97
+ "&lt;&nbsp;#{signature.last} image, #{val.length} bytes&nbsp;>"
98
+ end
30
99
  else
31
- "&lt;&nbsp;#{signature.last} image, #{val.length} bytes&nbsp;>"
100
+ "&lt;&nbsp;Binary, #{val.length} bytes&nbsp;>"
32
101
  end
33
- else
34
- "&lt;&nbsp;Binary, #{val.length} bytes&nbsp;>"
35
102
  end
36
103
  end
37
104
 
@@ -878,138 +945,7 @@ input+svg.revert {
878
945
  window.addEventListener(\"popstate\", function () { location.reload(true); });
879
946
  </script>
880
947
 
881
- <% is_includes_dates = nil
882
- is_includes_json = nil
883
- is_includes_text = nil
884
- def is_bcrypt?(val)
885
- val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
886
- end
887
- def hide_bcrypt(val, is_xml = nil, max_len = 200)
888
- if is_bcrypt?(val)
889
- '(hidden)'
890
- else
891
- if val.is_a?(String)
892
- val = val.dup.force_encoding('UTF-8').strip
893
- return CGI.escapeHTML(val) if is_xml
894
-
895
- if val.length > max_len
896
- if val[0] == '<' # Seems to be HTML?
897
- cur_len = 0
898
- cur_idx = 0
899
- # Find which HTML tags we might be inside so we can apply ending tags to balance
900
- element_name = nil
901
- in_closing = nil
902
- elements = []
903
- val.each_char do |ch|
904
- case ch
905
- when '<'
906
- element_name = +''
907
- when '/' # First character of tag is '/'?
908
- in_closing = true if element_name == ''
909
- when '>'
910
- if element_name
911
- if in_closing
912
- if (idx = elements.index { |tag| tag.downcase == element_name.downcase })
913
- elements.delete_at(idx)
914
- end
915
- elsif (tag_name = element_name.split.first).present?
916
- elements.unshift(tag_name)
917
- end
918
- element_name = nil
919
- in_closing = nil
920
- end
921
- else
922
- element_name << ch if element_name
923
- end
924
- cur_idx += 1
925
- # Unless it's inside wickets then this is real text content, and see if we're at the limit
926
- break if element_name.nil? && ((cur_len += 1) > max_len)
927
- end
928
- val = val[0..cur_idx]
929
- # Somehow still in the middle of an opening tag right at the end? (Should never happen)
930
- if !in_closing && (tag_name = element_name&.split&.first)&.present?
931
- elements.unshift(tag_name)
932
- val << '>'
933
- end
934
- elements.each do |closing_tag|
935
- val << \"</#\{closing_tag}>\"
936
- end
937
- else # Not HTML, just cut it at the length
938
- val = val[0...max_len]
939
- end
940
- val = \"#\{val}...\"
941
- end
942
- val
943
- else
944
- val.to_s
945
- end
946
- end
947
- end
948
- def display_value(col_type, val)
949
- is_mssql_geography = nil
950
- # 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)
951
- if col_type == :binary && val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
952
- col_type = 'geography'
953
- is_mssql_geography = true
954
- end
955
- case col_type
956
- when 'geometry', 'geography'
957
- if Object.const_defined?('RGeo')
958
- @is_mysql = ['Mysql2', 'Trilogy'].include?(ActiveRecord::Base.connection.adapter_name) if @is_mysql.nil?
959
- @is_mssql = ActiveRecord::Base.connection.adapter_name == 'SQLServer' if @is_mssql.nil?
960
- val_err = nil
961
-
962
- if @is_mysql || (is_mssql_geography ||=
963
- (@is_mssql ||
964
- (val && val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12])
965
- )
966
- )
967
- # MySQL's \"Internal Geometry Format\" and MSSQL's Geography are like WKB, but with an initial 4 bytes that indicates the SRID.
968
- if (srid = val&.[](0..3)&.unpack('I'))
969
- val = val.dup.force_encoding('BINARY')[4..-1].bytes
970
-
971
- # MSSQL spatial bitwise flags, often 0C for a point:
972
- # xxxx xxx1 = HasZValues
973
- # xxxx xx1x = HasMValues
974
- # xxxx x1xx = IsValid
975
- # xxxx 1xxx = IsSinglePoint
976
- # xxx1 xxxx = IsSingleLineSegment
977
- # xx1x xxxx = IsWholeGlobe
978
- # Convert Microsoft's unique geography binary to standard WKB
979
- # (MSSQL point usually has two doubles, lng / lat, and can also have Z)
980
- if is_mssql_geography
981
- if val[0] == 1 && (val[1] & 8 > 0) && # Single point?
982
- (val.length - 2) % 8 == 0 && val.length < 27 # And containing up to three 8-byte values?
983
- val = [0, 0, 0, 0, 1] + val[2..-1].reverse
984
- else
985
- val_err = '(Microsoft internal SQL geography type)'
986
- end
987
- end
988
- end
989
- end
990
- unless val_err || val.nil?
991
- if (geometry = RGeo::WKRep::WKBParser.new.parse(val.pack('c*'))).is_a?(RGeo::Cartesian::PointImpl) &&
992
- !(geometry.y == 0.0 && geometry.x == 0.0)
993
- # 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
994
- geometry = \"<a href=\\\"https://www.google.com/maps/place/#\{geometry.y}+#\{geometry.x}/@#\{geometry.y},#\{geometry.x},12z\\\" target=\\\"blank\\\">#\{geometry.to_s}</a>\"
995
- end
996
- val = geometry
997
- end
998
- val_err || val
999
- else
1000
- '(Add RGeo gem to parse geometry detail)'
1001
- end
1002
- when :binary
1003
- ::Brick::Rails.display_binary(val) if val
1004
- else
1005
- if col_type
1006
- hide_bcrypt(val, col_type == :xml)
1007
- else
1008
- '?'
1009
- end
1010
- end
1011
- end
1012
-
948
+ <%
1013
949
  # Accommodate composite primary keys that include strings with forward-slash characters
1014
950
  def slashify(*vals)
1015
951
  vals.map { |val_part| val_part.is_a?(String) ? val_part.gsub('/', '^^sl^^') : val_part }
@@ -1245,6 +1181,7 @@ erDiagram
1245
1181
  }
1246
1182
  <% end
1247
1183
  # callback < %= cb_k % > erdClick
1184
+ @_brick_monetized_attributes = model.respond_to?(:monetized_attributes) ? model.monetized_attributes.values : {}
1248
1185
  %>
1249
1186
  </div>
1250
1187
  "
@@ -1568,10 +1505,10 @@ end
1568
1505
  <title><%=
1569
1506
  base_model = (model = (obj = @#{obj_name})&.class).base_class
1570
1507
  see_all_path = send(\"#\{base_model._brick_index}_path\")
1571
- if obj.respond_to?(:#{inh_col = @_brick_model.inheritance_column}) &&
1572
- (model_name = @#{obj_name}.#{inh_col}) != base_model.name
1508
+ #{(inh_col = @_brick_model.inheritance_column).present? &&
1509
+ " if obj.respond_to?(:#{inh_col}) && (model_name = @#{obj_name}.#{inh_col}) != base_model.name
1573
1510
  see_all_path << \"?#{inh_col}=#\{model_name}\"
1574
- end
1511
+ end"}
1575
1512
  page_title = (\"#\{model_name ||= model.name}: #\{obj&.brick_descrip || controller_name}\")
1576
1513
  %></title>
1577
1514
  </head>
@@ -1612,7 +1549,7 @@ end
1612
1549
  end %>
1613
1550
  </table>
1614
1551
  <%
1615
- if (description = (relation = Brick.relations[tbl_name = #{model_name}.table_name])&.fetch(:description, nil)) %><%=
1552
+ if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
1616
1553
  description %><br><%
1617
1554
  end
1618
1555
  %><%= link_to \"(See all #\{model_name.pluralize})\", see_all_path %>
@@ -1673,100 +1610,7 @@ end
1673
1610
  <% end %>
1674
1611
  </th>
1675
1612
  <td>
1676
- <table><tr><td>
1677
- <% dt_pickers = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
1678
- html_options = {}
1679
- html_options[:class] = 'dimmed' unless val
1680
- is_revert = true
1681
- if bt
1682
- html_options[:prompt] = \"Select #\{bt_name\}\" %>
1683
- <%= f.select k.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options %>
1684
- <%= if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
1685
- link_to('⇛', send(\"#\{bt_class.base_class._brick_index(:singular)\}_path\".to_sym, bt_obj.send(bt_class.primary_key.to_sym)), { class: 'show-arrow' })
1686
- elsif val
1687
- \"<span class=\\\"orphan\\\">Orphaned ID: #\{val}</span>\".html_safe
1688
- end %>
1689
- <% else
1690
- col_type = if ::Brick.config.json_columns[tbl_name]&.include?(k)
1691
- :json
1692
- elsif col&.sql_type == 'geography'
1693
- col.sql_type
1694
- else
1695
- col&.type
1696
- end
1697
- case (col_type ||= col&.sql_type)
1698
- when :string, :text
1699
- if is_bcrypt?(val) # || .readonly?
1700
- is_revert = false %>
1701
- <%= hide_bcrypt(val, nil, 1000) %>
1702
- <% elsif col_type == :string
1703
- if model.respond_to?(:enumerized_attributes) && (opts = model.enumerized_attributes[k]&.options).present? %>
1704
- <%= f.select(k.to_sym, [[\"(No #\{k} chosen)\", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, html_options) %><%
1705
- else %>
1706
- <%= f.text_field(k.to_sym, html_options) %><%
1707
- end
1708
- else
1709
- is_includes_text = true %>
1710
- <%= f.hidden_field(k.to_sym, html_options) %>
1711
- <trix-editor input=\"<%= f.field_id(k) %>\"></trix-editor>
1712
- <% end %>
1713
- <% when :boolean %>
1714
- <%= f.check_box k.to_sym %>
1715
- <% when :integer, :decimal, :float %>
1716
- <%= digit_pattern = col_type == :integer ? '\\d*' : '\\d*(?:\\.\\d*|)'
1717
- # Used to do this for float / decimal: f.number_field k.to_sym
1718
- f.text_field k.to_sym, { pattern: digit_pattern, class: 'check-validity' } %>
1719
- <% when *dt_pickers.keys
1720
- is_includes_dates = true %>
1721
- <%= f.text_field k.to_sym, { class: dt_pickers[col_type] } %>
1722
- <% when :uuid
1723
- is_revert = false %>
1724
- <%=
1725
- # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
1726
- # If it's not yet enabled then: create extension \"uuid-ossp\";
1727
- # ActiveUUID gem created a new :uuid type
1728
- val %>
1729
- <% when :ltree %>
1730
- <%=
1731
- # In Postgres labels of data stored in a hierarchical tree-like structure
1732
- # If it's not yet enabled then: create extension ltree;
1733
- val %>
1734
- <% when :binary %>
1735
- <%= is_revert = false
1736
- if val
1737
- # %%% This same kind of geography check is done two other times above ... would be great to DRY it up.
1738
- if val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
1739
- display_value('geography', val)
1740
- else
1741
- ::Brick::Rails.display_binary(val)
1742
- end.html_safe
1743
- end %>
1744
- <% when :primary_key
1745
- is_revert = false %>
1746
- <% when :json
1747
- is_includes_json = true
1748
- if val.is_a?(String)
1749
- val_str = val
1750
- else
1751
- eheij = ActiveSupport::JSON::Encoding.escape_html_entities_in_json
1752
- ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false if eheij
1753
- val_str = val.to_json
1754
- ActiveSupport::JSON::Encoding.escape_html_entities_in_json = eheij
1755
- end %>
1756
- <%= # Because there are so danged many quotes in JSON, escape them specially by converting to backticks.
1757
- # (and previous to this, escape backticks with our own goofy code of ^^br_btick__ )
1758
- json_field = f.hidden_field k.to_sym, { class: 'jsonpicker', value: val_str.gsub('`', '^^br_btick__').tr('\"', '`').html_safe } %>
1759
- <div id=\"_br_json_<%= f.field_id(k) %>\"></div>
1760
- <% else %>
1761
- <%= is_revert = false
1762
- display_value(col_type, val).html_safe %>
1763
- <% end
1764
- end
1765
- if is_revert
1766
- %></td>
1767
- <td><svg class=\"revert\" width=\"1.5em\" viewBox=\"0 0 512 512\"><use xlink:href=\"#revertPath\" /></svg>
1768
- <% end %>
1769
- </td></tr></table>
1613
+ <%= f.brick_field(k, html_options = {}, val, col, bt, bt_class, bt_name, bt_pair) %>
1770
1614
  </td>
1771
1615
  </tr>
1772
1616
  <% end
@@ -1795,11 +1639,12 @@ end
1795
1639
  # with a .where(). And the polymorphic class name it points to is the base class name of
1796
1640
  # the STI model instead of its subclass.
1797
1641
  poly_type = #{poly_type.inspect}
1798
- if poly_type && @#{obj_name}.respond_to?(:#{@_brick_model.inheritance_column}) &&
1642
+ #{ (inh_col = @_brick_model.inheritance_column).present? &&
1643
+ " if poly_type && @#{obj_name}.respond_to?(:#{inh_col}) &&
1799
1644
  (base_type = collection.where_values_hash[poly_type])
1800
- collection = collection.rewhere(poly_type => [base_type, @#{obj_name}.#{@_brick_model.inheritance_column}])
1801
- end"
1802
- end
1645
+ collection = collection.rewhere(poly_type => [base_type, @#{obj_name}.#{inh_col}])
1646
+ end"}"
1647
+ end
1803
1648
  s << "<table id=\"#{hm_name}\" class=\"shadow\">
1804
1649
  <tr><th>#{hm[1]}#{' poly' if hm[0].options[:as]} #{hm[3]}</th></tr>
1805
1650
  <% collection = @#{obj_name}.#{hm_name}
@@ -1840,7 +1685,7 @@ end}
1840
1685
  end
1841
1686
  unless is_crosstab
1842
1687
  inline << "
1843
- <% if is_includes_dates %>
1688
+ <% if @_date_fields_present %>
1844
1689
  <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css\">
1845
1690
  <style>
1846
1691
  .flatpickr-calendar {
@@ -1855,18 +1700,18 @@ flatpickr(\".timepicker\", {enableTime: true, noCalendar: true});
1855
1700
  </script>
1856
1701
  <% end %>
1857
1702
 
1858
- <% if false # is_includes_dropdowns %>
1703
+ <% if false # @_dropdown_fields_present %>
1859
1704
  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/slim-select/1.27.1/slimselect.min.js\"></script>
1860
1705
  <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdnjs.cloudflare.com/ajax/libs/slim-select/1.27.1/slimselect.min.css\">
1861
1706
  <% end %>
1862
1707
 
1863
- <% if is_includes_text %>
1708
+ <% if @_text_fields_present %>
1864
1709
  <script src=\"https://cdn.jsdelivr.net/npm/trix@2.0/dist/trix.umd.min.js\"></script>
1865
1710
  <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.jsdelivr.net/npm/trix@2.0/dist/trix.min.css\">
1866
1711
  <% end %>
1867
1712
 
1868
1713
  <% # Started with v0.14.4 of vanilla-jsoneditor
1869
- if is_includes_json %>
1714
+ if @_json_fields_present %>
1870
1715
  <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.jsdelivr.net/npm/vanilla-jsoneditor/themes/jse-theme-default.min.css\">
1871
1716
  <script type=\"module\">
1872
1717
  import { JSONEditor } from \"https://cdn.jsdelivr.net/npm/vanilla-jsoneditor/index.min.js\";
@@ -0,0 +1,185 @@
1
+ module Brick::Rails::FormBuilder
2
+ DT_PICKERS = { datetime: 'datetimepicker', timestamp: 'datetimepicker', time: 'timepicker', date: 'datepicker' }
3
+
4
+ # When this field is one of the appropriate types, will set one of these instance variables accordingly:
5
+ # @_text_fields_present - To include trix editor
6
+ # @_date_fields_present - To include flatpickr date / time editor
7
+ # @_json_fields_present - To include JSONEditor
8
+ def brick_field(method, html_options = {}, val = nil, col = nil, bt = nil, bt_class = nil, bt_name = nil, bt_pair = nil)
9
+ model = self.object.class
10
+ col ||= model.columns_hash[method]
11
+ out = +'<table><tr><td>'
12
+ html_options[:class] = 'dimmed' unless val
13
+ is_revert = true
14
+ template = instance_variable_get(:@template)
15
+ if bt
16
+ bt_class ||= bt[1].first.first
17
+ bt_name ||= bt[1].map { |x| x.first.name }.join('/')
18
+ bt_pair ||= bt[1].first
19
+
20
+ html_options[:prompt] = "Select #{bt_name}"
21
+ out << self.select(method.to_sym, bt[3], { value: val || '^^^brick_NULL^^^' }, html_options)
22
+ out << if (bt_obj = bt_class&.find_by(bt_pair[1] => val))
23
+ bt_path = template.send(
24
+ "#{bt_class.base_class._brick_index(:singular)}_path".to_sym,
25
+ bt_obj.send(bt_class.primary_key.to_sym)
26
+ )
27
+ template.link_to('⇛', bt_path, { class: 'show-arrow' })
28
+ elsif val
29
+ "<span class=\"orphan\">Orphaned ID: #{val}</span>".html_safe
30
+ end
31
+ elsif @_brick_monetized_attributes&.include?(method)
32
+ out << self.text_field(method.to_sym, html_options.merge({ value: Money.new(val.to_i).format }))
33
+ else
34
+ col_type = if model.json_column?(col) || val.is_a?(Array)
35
+ :json
36
+ elsif col&.sql_type == 'geography'
37
+ col.sql_type
38
+ else
39
+ col&.type
40
+ end
41
+ case (col_type ||= col&.sql_type)
42
+ when :string, :text
43
+ if ::Brick::Rails::FormBuilder.is_bcrypt?(val) # || .readonly?
44
+ is_revert = false
45
+ out << ::Brick::Rails::FormBuilder.hide_bcrypt(val, nil, 1000)
46
+ elsif col_type == :string
47
+ if model.respond_to?(:enumerized_attributes) && (opts = (attr = model.enumerized_attributes[method])&.options).present?
48
+ enum_html_options = attr.kind_of?(Enumerize::Multiple) ? html_options.merge({ multiple: true, size: opts.length + 1 }) : html_options
49
+ out << self.select(method.to_sym, [["(No #{method} chosen)", '^^^brick_NULL^^^']] + opts, { value: val || '^^^brick_NULL^^^' }, enum_html_options)
50
+ else
51
+ out << self.text_field(method.to_sym, html_options)
52
+ end
53
+ else
54
+ template.instance_variable_set(:@_text_fields_present, true)
55
+ out << self.hidden_field(method.to_sym, html_options)
56
+ out << "<trix-editor input=\"#{self.field_id(method)}\"></trix-editor>"
57
+ end
58
+ when :boolean
59
+ out << self.check_box(method.to_sym)
60
+ when :integer, :decimal, :float
61
+ digit_pattern = col_type == :integer ? '\d*' : '\d*(?:\.\d*|)'
62
+ # Used to do this for float / decimal: self.number_field method.to_sym
63
+ out << self.text_field(method.to_sym, { pattern: digit_pattern, class: 'check-validity' })
64
+ when *DT_PICKERS.keys
65
+ template.instance_variable_set(:@_date_fields_present, true)
66
+ out << self.text_field(method.to_sym, { class: DT_PICKERS[col_type] })
67
+ when :uuid
68
+ is_revert = false
69
+ # Postgres naturally uses the +uuid_generate_v4()+ function from the uuid-ossp extension
70
+ # If it's not yet enabled then: create extension \"uuid-ossp\";
71
+ # ActiveUUID gem created a new :uuid type
72
+ out << val
73
+ when :ltree
74
+ # In Postgres labels of data stored in a hierarchical tree-like structure
75
+ # If it's not yet enabled then: create extension ltree;
76
+ out << val
77
+ when :binary
78
+ is_revert = false
79
+ if val
80
+ # %%% This same kind of geography check is done two other times in engine.rb ... would be great to DRY it up.
81
+ out << if val.length < 31 && (val.length - 6) % 8 == 0 && val[0..5].bytes == [230, 16, 0, 0, 1, 12]
82
+ ::Brick::Rails.display_value('geography', val)
83
+ else
84
+ ::Brick::Rails.display_binary(val)
85
+ end
86
+ end
87
+ when :primary_key
88
+ is_revert = false
89
+ when :json
90
+ template.instance_variable_set(:@_json_fields_present, true)
91
+ if val.is_a?(String)
92
+ val_str = val
93
+ else
94
+ eheij = ActiveSupport::JSON::Encoding.escape_html_entities_in_json
95
+ ActiveSupport::JSON::Encoding.escape_html_entities_in_json = false if eheij
96
+ val_str = val.to_json
97
+ ActiveSupport::JSON::Encoding.escape_html_entities_in_json = eheij
98
+ end
99
+ # Because there are so danged many quotes in JSON, escape them specially by converting to backticks.
100
+ # (and previous to this, escape backticks with our own goofy code of ^^br_btick__ )
101
+ out << (json_field = self.hidden_field(method.to_sym, { class: 'jsonpicker', value: val_str.gsub('`', '^^br_btick__').tr('\"', '`').html_safe }))
102
+ out << "<div id=\"_br_json_#{self.field_id(method)}\"></div>"
103
+ else
104
+ is_revert = false
105
+ out << ::Brick::Rails.display_value(col_type, val).html_safe
106
+ end
107
+ end
108
+ if is_revert
109
+ out << "</td>
110
+ "
111
+ out << '<td><svg class="revert" width="1.5em" viewBox="0 0 512 512"><use xlink:href="#revertPath" /></svg>'
112
+ end
113
+ out << "</td></tr></table>
114
+ "
115
+ out.html_safe
116
+ end # brick_field
117
+
118
+ # --- CLASS METHODS ---
119
+
120
+ def self.is_bcrypt?(val)
121
+ val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
122
+ end
123
+
124
+ def self.hide_bcrypt(val, is_xml = nil, max_len = 200)
125
+ if ::Brick::Rails::FormBuilder.is_bcrypt?(val)
126
+ '(hidden)'
127
+ else
128
+ if val.is_a?(String)
129
+ val = val.dup.force_encoding('UTF-8').strip
130
+ return CGI.escapeHTML(val) if is_xml
131
+
132
+ if val.length > max_len
133
+ if val[0] == '<' # Seems to be HTML?
134
+ cur_len = 0
135
+ cur_idx = 0
136
+ # Find which HTML tags we might be inside so we can apply ending tags to balance
137
+ element_name = nil
138
+ in_closing = nil
139
+ elements = []
140
+ val.each_char do |ch|
141
+ case ch
142
+ when '<'
143
+ element_name = +''
144
+ when '/' # First character of tag is '/'?
145
+ in_closing = true if element_name == ''
146
+ when '>'
147
+ if element_name
148
+ if in_closing
149
+ if (idx = elements.index { |tag| tag.downcase == element_name.downcase })
150
+ elements.delete_at(idx)
151
+ end
152
+ elsif (tag_name = element_name.split.first).present?
153
+ elements.unshift(tag_name)
154
+ end
155
+ element_name = nil
156
+ in_closing = nil
157
+ end
158
+ else
159
+ element_name << ch if element_name
160
+ end
161
+ cur_idx += 1
162
+ # Unless it's inside wickets then this is real text content, and see if we're at the limit
163
+ break if element_name.nil? && ((cur_len += 1) > max_len)
164
+ end
165
+ val = val[0..cur_idx]
166
+ # Somehow still in the middle of an opening tag right at the end? (Should never happen)
167
+ if !in_closing && (tag_name = element_name&.split&.first)&.present?
168
+ elements.unshift(tag_name)
169
+ val << '>'
170
+ end
171
+ elements.each do |closing_tag|
172
+ val << "</#{closing_tag}>"
173
+ end
174
+ else # Not HTML, just cut it at the length
175
+ val = val[0...max_len]
176
+ end
177
+ val = "#{val}..."
178
+ end
179
+ val
180
+ else
181
+ val.to_s
182
+ end
183
+ end
184
+ end
185
+ end
@@ -20,40 +20,50 @@ module Brick::Rails::FormTags
20
20
  s << col_name
21
21
  cols[col_name] = col
22
22
  end
23
+ composite_bts = bts.select { |k, _v| k.is_a?(Array) }
24
+ composite_bt_names = {}
25
+ composite_bt_cols = composite_bts.each_with_object([]) do |bt, s|
26
+ composite_bt_names[bt.first.join('__')] = bt.last
27
+ bt.first.each { |bt_col| s << bt_col unless s.include?(bt_col.first) }
28
+ end
23
29
  unless sequence # If no sequence is defined, start with all inclusions
24
30
  cust_cols = klass._br_cust_cols
25
31
  # HOT columns, kept as symbols
26
32
  hots = klass._br_bt_descrip.keys.select { |k| bts.key?(k) }
27
- sequence = col_keys + cust_cols.keys + hots + hms_keys.reject { |assoc_name| inclusions&.exclude?(assoc_name) }
33
+ sequence = (col_keys - composite_bt_cols) +
34
+ composite_bt_names.keys + cust_cols.keys + hots +
35
+ hms_keys.reject { |assoc_name| inclusions&.exclude?(assoc_name) }
28
36
  end
29
37
  sequence.reject! { |nm| exclusions.include?(nm) } if exclusions
30
38
  out << sequence.each_with_object(+'') do |col_name, s|
31
- if (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
32
- s << '<th'
33
- s << " title=\"#{col.comment}\"" if col.respond_to?(:comment) && !col.comment.blank?
34
- s << if (bt = bts[col_name])
35
- # Allow sorting for any BT except polymorphics
36
- "#{' x-order="' + bt.first.to_s + '"' unless bt[2]}>BT " +
39
+ if (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
40
+ s << '<th '
41
+ s << "title=\"#{col.comment}\"" if col.respond_to?(:comment) && !col.comment.blank?
42
+ s << if (bt = bts[col_name])
43
+ # Allow sorting for any BT except polymorphics
44
+ "x-order=\"#{bt.first.to_s + '"' unless bt[2]}>BT " +
45
+ bt[1].map { |bt_pair| bt_pair.first.bt_link(bt.first) }.join(' ')
46
+ else # Normal column
47
+ "x-order=\"#{col_name + '"' if true}>#{col_name}"
48
+ end
49
+ elsif col # HM column
50
+ options = {}
51
+ options[col[1].inheritance_column] = col[1].name unless col[1] == col[1].base_class
52
+ s << "<th x-order=\"#{col_name + '"' if true}>#{col[2]} "
53
+ s << (col.first ? "#{col[3]}" : "#{link_to(col[3], send("#{col[1]._brick_index}_path", options))}")
54
+ elsif cust_cols.key?(col_name) # Custom column
55
+ s << "<th x-order=\"#{col_name}\">#{col_name}"
56
+ elsif col_name.is_a?(Symbol) && (hot = bts[col_name]) # has_one :through
57
+ s << "<th x-order=\"#{hot.first.to_s}\">HOT " +
58
+ hot[1].map { |hot_pair| hot_pair.first.bt_link(col_name) }.join(' ')
59
+ elsif (bt = composite_bt_names[col_name])
60
+ s << "<th x-order=\"#{bt.first.to_s + '"' unless bt[2]}>BT comp " +
37
61
  bt[1].map { |bt_pair| bt_pair.first.bt_link(bt.first) }.join(' ')
38
- else # Normal column
39
- "#{' x-order="' + col_name + '"' if true}>#{col_name}"
40
- end
41
- elsif col # HM column
42
- options = {}
43
- options[col[1].inheritance_column] = col[1].name unless col[1] == col[1].base_class
44
- s << "<th#{' x-order="' + col_name + '"' if true}>#{col[2]} "
45
- s << (col.first ? "#{col[3]}" : "#{link_to(col[3], send("#{col[1]._brick_index}_path", options))}")
46
- elsif cust_cols.key?(col_name) # Custom column
47
- s << "<th x-order=\"#{col_name}\">#{col_name}"
48
- elsif col_name.is_a?(Symbol) && (hot = bts[col_name]) # has_one :through
49
- s << "<th x-order=\"#{hot.first.to_s}\">HOT " +
50
- hot[1].map { |hot_pair| hot_pair.first.bt_link(col_name) }.join(' ')
51
- hot[1].first
52
- else # Bad column name!
53
- s << "<th title=\"<< Unknown column >>\">#{col_name}"
62
+ else # Bad column name!
63
+ s << "<th title=\"<< Unknown column >>\">#{col_name}"
64
+ end
65
+ s << '</th>'
54
66
  end
55
- s << '</th>'
56
- end
57
67
  out << "</tr></thead>
58
68
  <tbody>"
59
69
  # %%% Have once gotten this error with MSSQL referring to http://localhost:3000/warehouse/cold_room_temperatures__archive
@@ -70,7 +80,7 @@ module Brick::Rails::FormTags
70
80
  out << ' class=\"dimmed\"' unless cols.key?(col_name) || (cust_col = cust_cols[col_name]) ||
71
81
  (col_name.is_a?(Symbol) && bts.key?(col_name)) # HOT
72
82
  out << '>'
73
- if (bt = bts[col_name])
83
+ if (bt = bts[col_name] || composite_bt_names[col_name])
74
84
  if bt[2] # Polymorphic?
75
85
  if (poly_id = obj.send("#{bt.first}_id"))
76
86
  # Was: obj.send("#{bt.first}_type")
@@ -119,8 +129,12 @@ module Brick::Rails::FormTags
119
129
  end
120
130
  elsif (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
121
131
  # binding.pry if col.is_a?(Array)
122
- col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
123
- out << display_value(col_type || col&.sql_type, val).to_s
132
+ out << if @_brick_monetized_attributes&.include?(col_name)
133
+ val ? Money.new(val.to_i).format : ''
134
+ else
135
+ col_type = col&.sql_type == 'geography' ? col.sql_type : col&.type
136
+ ::Brick::Rails.display_value(col_type || col&.sql_type, val).to_s
137
+ end
124
138
  elsif cust_col
125
139
  data = cust_col.first.map { |cc_part| obj.send(cc_part.last) }
126
140
  cust_txt = klass.brick_descrip(cust_col[-2], data)
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 124
8
+ TINY = 126
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
@@ -218,7 +218,8 @@ module Brick
218
218
  puts " belongs_to :#{a.name}, polymorphic: true"
219
219
  end
220
220
  else
221
- s.first[a.foreign_key.to_s] = [a.name, a.klass]
221
+ bt_key = a.foreign_key.is_a?(Array) ? a.foreign_key : a.foreign_key.to_s
222
+ s.first[bt_key] = [a.name, a.klass]
222
223
  end
223
224
  else # This gets all forms of has_many and has_one
224
225
  if through # has_many :through or has_one :through
@@ -525,7 +526,7 @@ module Brick
525
526
  v.each do |type|
526
527
  # Allow polymorphic BT to relate to an STI subclass
527
528
  base_type = ::Brick.config.sti_namespace_prefixes["::#{type}"] ||
528
- ::Brick.config.sti_namespace_prefixes.find { |k, _v| type.start_with?(k[2..-1]) }&.last&.[](2..-1)
529
+ ::Brick.config.sti_namespace_prefixes.find { |k, _v| k.end_with?('::') && type.start_with?(k[2..-1]) }&.last&.[](2..-1)
529
530
  if relations.key?(primary_table = (base_type || type).underscore.pluralize)
530
531
  ::Brick._add_bt_and_hm([nil, table_name, poly, nil, primary_table, "(brick) #{table_name}_#{poly}"], relations,
531
532
  type, # Polymorphic class
@@ -605,7 +606,8 @@ In config/initializers/brick.rb appropriate entries would look something like:
605
606
  ::Rails.configuration.eager_load_namespaces.select { |ns| ns < ::Rails::Application }.each(&:eager_load!)
606
607
  end
607
608
  else
608
- Zeitwerk::Loader.eager_load_all
609
+ # Same as: Zeitwerk::Loader.eager_load_all -- plus retry when something skips a beat
610
+ Zeitwerk::Registry.loaders.each { |loader| load_with_retry(loader) }
609
611
  end
610
612
  abstract_ar_bases = if do_ar_abstract_bases
611
613
  ActiveRecord::Base.descendants.select { |ar| ar.abstract_class? }.map(&:name)
@@ -614,6 +616,20 @@ In config/initializers/brick.rb appropriate entries would look something like:
614
616
  abstract_ar_bases
615
617
  end
616
618
 
619
+ # Some classes (like Phlex::Testing::Rails) will successfully auto-load after a retry
620
+ def load_with_retry(loader, autoloaded = nil)
621
+ autoloaded ||= loader.send(:autoloaded_dirs).dup
622
+ begin
623
+ loader.eager_load
624
+ rescue Zeitwerk::SetupRequired
625
+ # This is fine -- we eager load what can be eager loaded
626
+ rescue Zeitwerk::NameError
627
+ if autoloaded != (new_auto = loader.send(:autoloaded_dirs))
628
+ load_with_retry(loader, new_auto.dup) # Try one more time and it could come together
629
+ end
630
+ end
631
+ end
632
+
617
633
  def display_classes(prefix, rels, max_length)
618
634
  rels.sort.each do |rel|
619
635
  ::Brick.auto_models << rel.first
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.124
4
+ version: 1.0.126
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-03-26 00:00:00.000000000 Z
11
+ date: 2023-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -244,6 +244,7 @@ files:
244
244
  - lib/brick/frameworks/rails/controller.rb
245
245
  - lib/brick/frameworks/rails/crosstab.brk
246
246
  - lib/brick/frameworks/rails/engine.rb
247
+ - lib/brick/frameworks/rails/form_builder.rb
247
248
  - lib/brick/frameworks/rails/form_tags.rb
248
249
  - lib/brick/frameworks/rspec.rb
249
250
  - lib/brick/join_array.rb
@@ -270,7 +271,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
270
271
  requirements:
271
272
  - - ">="
272
273
  - !ruby/object:Gem::Version
273
- version: 2.3.5
274
+ version: 2.3.8
274
275
  required_rubygems_version: !ruby/object:Gem::Requirement
275
276
  requirements:
276
277
  - - ">="