brick 1.0.124 → 1.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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
  - - ">="