brick 1.0.20 → 1.0.23

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: 41b64e2d571540382ce5ecd497f1e1413fde20119d46f1b24e1fae5af428fb45
4
- data.tar.gz: 1c26ccf7b8df54fd121546e8daeeb9508561328186af46aea8ba1bd819524532
3
+ metadata.gz: 24731ac9bd56261179224610406247cc4438c61e411aa3fe96205b2e97fc8cb8
4
+ data.tar.gz: 785d12e36fcf788c05cff3a02ca2ec17fc418b0ea15b54cb6923c83c6f362d3b
5
5
  SHA512:
6
- metadata.gz: e103d061b74402f3fe0b944d06b30c20f053166c706a54f15753afc8cb0c703ae71f83bd88191f985a37180cb9bdafdba918178174df08f58f683a723fcfb73a
7
- data.tar.gz: 13a7dce7f4f5789a0d7a0c6cae4adcaf2f44d22d0f59c355d75102a7f08292d80c781f8d33be121801d375fd33fed8bcdf4ef6b56108ef8f4ab4110c1f49e296
6
+ metadata.gz: '090a3c4d1b3cb4761799e05adf5caa0c83f60cf985f97ed6856beb05620014e9e7401689719209488092c6c4a7605e53781cdb71ad9e7ec204ccaddca0d92cca'
7
+ data.tar.gz: 8d1f9512c948359e7d73cd7cc32302f226df7e3e744fab562e01e16170830be28d0e2e15bcd7b437f66ed79e366b0ff33b7f578e4c76ffde1dd62cdcec059069
data/lib/brick/config.rb CHANGED
@@ -74,6 +74,20 @@ module Brick
74
74
  @mutex.synchronize { @exclude_hms = skips }
75
75
  end
76
76
 
77
+ # Skip showing counts for these specific has_many associations when building auto-generated #index views
78
+ def skip_index_hms
79
+ @mutex.synchronize { @skip_index_hms || {} }
80
+ end
81
+
82
+ def skip_index_hms=(skips)
83
+ @mutex.synchronize do
84
+ @skip_index_hms ||= skips.each_with_object({}) do |v, s|
85
+ class_name, assoc_name = v.split('.')
86
+ (s[class_name] ||= {})[assoc_name.to_sym] = nil
87
+ end
88
+ end
89
+ end
90
+
77
91
  # Associations to treat as a has_one
78
92
  def has_ones
79
93
  @mutex.synchronize { @has_ones }
@@ -26,18 +26,13 @@
26
26
 
27
27
  # colour coded origins
28
28
 
29
- # Drag something like TmfModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
29
+ # Drag something like HierModel#name onto the rows and have it automatically add five columns -- where type=zone / where type = section / etc
30
30
 
31
31
  # Support for Postgres / MySQL enums (add enum to model, use model enums to make a drop-down in the UI)
32
32
 
33
33
  # Currently quadrupling up routes
34
34
 
35
35
 
36
- # From the North app:
37
- # undefined method `built_in_role_path' when referencing show on a subclassed STI:
38
- # http://localhost:3000/roles/3?_brick_schema=cust1
39
-
40
-
41
36
  # ==========================================================
42
37
  # Dynamically create model or controller classes when needed
43
38
  # ==========================================================
@@ -166,16 +161,36 @@ module ActiveRecord
166
161
  if is_brackets_have_content
167
162
  output
168
163
  elsif pk_alias
169
- if (id = obj.send(pk_alias))
170
- "#{klass.name} ##{id}"
164
+ id = []
165
+ pk_alias.each do |pk_alias_part|
166
+ if (pk_part = obj.send(pk_alias_part))
167
+ id << pk_part
168
+ end
169
+ end
170
+ if id.present?
171
+ "#{klass.name} ##{id.join(', ')}"
171
172
  end
172
- # elsif klass.primary_key
173
- # "#{klass.name} ##{obj.send(klass.primary_key)}"
174
173
  else
175
174
  obj.to_s
176
175
  end
177
176
  end
178
177
 
178
+ def self.bt_link(assoc_name)
179
+ model_underscore = name.underscore
180
+ assoc_name = CGI.escapeHTML(assoc_name.to_s)
181
+ model_path = Rails.application.routes.url_helpers.send("#{model_underscore.pluralize}_path".to_sym)
182
+ link = Class.new.extend(ActionView::Helpers::UrlHelper).link_to(name, model_path)
183
+ model_underscore == assoc_name ? link : "#{assoc_name}-#{link}".html_safe
184
+ end
185
+
186
+ def self.brick_import_template
187
+ template = constants.include?(:IMPORT_TEMPLATE) ? self::IMPORT_TEMPLATE : suggest_template(false, false, 0)
188
+ # Add the primary key to the template as being unique (unless it's already there)
189
+ template[:uniques] = [pk = primary_key.to_sym]
190
+ template[:all].unshift(pk) unless template[:all].include?(pk)
191
+ template
192
+ end
193
+
179
194
  private
180
195
 
181
196
  def self._brick_get_fks
@@ -267,27 +282,6 @@ module ActiveRecord
267
282
  # , is_add_bts, is_add_hms
268
283
  )
269
284
  is_add_bts = is_add_hms = true
270
- wheres = {}
271
- has_hm = false
272
- params.each do |k, v|
273
- case (ks = k.split('.')).length
274
- when 1
275
- next unless klass._brick_get_fks.include?(k)
276
- when 2
277
- assoc_name = ks.first.to_sym
278
- # Make sure it's a good association name and that the model has that column name
279
- next unless (assoc = klass.reflect_on_association(assoc_name))&.klass&.columns&.map(&:name)&.include?(ks.last)
280
-
281
- # So that we can map an association name to any special alias name used in an AREL query
282
- ans = (assoc.klass._assoc_names[assoc_name] ||= [])
283
- ans << assoc.klass unless ans.include?(assoc.klass)
284
- # There is some potential for duplicates when there is an HM-based where in play. De-duplicate if so.
285
- has_hm ||= assoc.macro == :has_many
286
- join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
287
- end
288
- wheres[k] = v.split(',')
289
- end
290
- # distinct! if has_hm
291
285
 
292
286
  # %%% Skip the metadata columns
293
287
  if selects&.empty? # Default to all columns
@@ -301,56 +295,101 @@ module ActiveRecord
301
295
  if is_add_bts || is_add_hms
302
296
  bts, hms, associatives = ::Brick.get_bts_and_hms(klass)
303
297
  bts.each do |_k, bt|
304
- # join_array[bt.first] = nil # Store this relation name in our special collection for .joins()
298
+ # join_array will receive this relation name when calling #brick_parse_dsl
305
299
  bt_descrip[bt.first] = [bt.last, bt.last.brick_parse_dsl(join_array, bt.first, translations)]
306
300
  end
301
+ skip_klass_hms = ::Brick.config.skip_index_hms[klass.name] || {}
307
302
  hms.each do |k, hm|
308
- join_array[k] = nil # Store this relation name in our special collection for .joins()
309
- hm_counts[k] = nil # Placeholder that will be filled in once we know the proper table alias
303
+ next if skip_klass_hms.key?(k)
304
+
305
+ hm_counts[k] = hm
310
306
  end
311
307
  end
312
- where!(wheres) unless wheres.empty?
308
+
309
+ wheres = {}
310
+ params.each do |k, v|
311
+ case (ks = k.split('.')).length
312
+ when 1
313
+ next unless klass._brick_get_fks.include?(k)
314
+ when 2
315
+ assoc_name = ks.first.to_sym
316
+ # Make sure it's a good association name and that the model has that column name
317
+ next unless klass.reflect_on_association(assoc_name)&.klass&.columns&.any? { |col| col.name == ks.last }
318
+
319
+ join_array[assoc_name] = nil # Store this relation name in our special collection for .joins()
320
+ end
321
+ wheres[k] = v.split(',')
322
+ end
323
+
313
324
  if join_array.present?
314
325
  left_outer_joins!(join_array) # joins!(join_array)
315
326
  # Without working from a duplicate, touching the AREL ast tree sets the @arel instance variable, which causes the relation to be immutable.
316
327
  (rel_dupe = dup)._arel_alias_names
317
328
  core_selects = selects.dup
318
- groups = []
319
329
  chains = rel_dupe._brick_chains
320
- id_for_tables = {}
330
+ id_for_tables = Hash.new { |h, k| h[k] = [] }
331
+ field_tbl_names = Hash.new { |h, k| h[k] = {} }
321
332
  bt_columns = bt_descrip.each_with_object([]) do |v, s|
322
- tbl_name = chains[v.last.first].first
323
- if (id_col = v.last.first.primary_key) && !id_for_tables.key?(tbl_name)
324
- groups << (unaliased = "#{tbl_name}.#{id_col}")
325
- selects << "#{unaliased} AS \"#{(id_alias = id_for_tables[tbl_name] = "_brfk_#{v.first}__#{id_col}")}\""
326
- v.last << id_alias
333
+ tbl_name = field_tbl_names[v.first][v.last.first] ||= shift_or_first(chains[v.last.first])
334
+ if (id_col = v.last.first.primary_key) && !id_for_tables.key?(v.first) # was tbl_name
335
+ # Accommodate composite primary key by allowing id_col to come in as an array
336
+ (id_col.is_a?(Array) ? id_col : [id_col]).each do |id_part|
337
+ selects << "#{"#{tbl_name}.#{id_part}"} AS \"#{(id_alias = "_brfk_#{v.first}__#{id_part}")}\""
338
+ id_for_tables[v.first] << id_alias
339
+ end
340
+ v.last << id_for_tables[v.first]
327
341
  end
328
342
  if (col_name = v.last[1].last&.last)
343
+ field_tbl_name = nil
329
344
  v.last[1].map { |x| [translations[x[0..-2].map(&:to_s).join('.')], x.last] }.each_with_index do |sel_col, idx|
330
- groups << (unaliased = "#{tbl_name = chains[sel_col.first].first}.#{sel_col.last}")
345
+ field_tbl_name ||= field_tbl_names[v.first][sel_col.first] ||= shift_or_first(chains[sel_col.first])
331
346
  # col_name is weak when there are multiple, using sel_col.last instead
332
- tbl_name2 = tbl_name.start_with?('public.') ? tbl_name[7..-1] : tbl_name
333
- selects << "#{unaliased} AS \"#{(col_alias = "_brfk_#{tbl_name2}__#{sel_col.last}")}\""
347
+ selects << "#{"#{field_tbl_name}.#{sel_col.last}"} AS \"#{(col_alias = "_brfk_#{v.first}__#{sel_col.last}")}\""
334
348
  v.last[1][idx] << col_alias
335
349
  end
336
350
  end
337
351
  end
338
- group!(core_selects + groups) if hm_counts.any? # + bt_columns
339
352
  join_array.each do |assoc_name|
340
353
  # %%% Need to support {user: :profile}
341
354
  next unless assoc_name.is_a?(Symbol)
342
355
 
343
- klass = reflect_on_association(assoc_name)&.klass
344
- table_alias = chains[klass].length > 1 ? chains[klass].shift : chains[klass].first
356
+ table_alias = shift_or_first(chains[klass = reflect_on_association(assoc_name)&.klass])
345
357
  _assoc_names[assoc_name] = [table_alias, klass]
346
358
  end
347
- # Copy entries over
348
- hm_counts.keys.each do |k|
349
- hm_counts[k] = _assoc_names[k]
359
+ end
360
+ # Add derived table JOIN for the has_many counts
361
+ hm_counts.each do |k, hm|
362
+ associative = nil
363
+ count_column = if hm.options[:through]
364
+ fk_col = (associative = associatives[hm.name]).foreign_key
365
+ hm.foreign_key
366
+ else
367
+ fk_col = hm.foreign_key
368
+ hm.klass.primary_key || '*'
369
+ end
370
+ tbl_alias = "_br_#{hm.name}"
371
+ pri_tbl = hm.active_record
372
+ if fk_col.is_a?(Array) # Composite key?
373
+ on_clause = []
374
+ fk_col.each_with_index { |fk_col_part, idx| on_clause << "#{tbl_alias}.#{fk_col_part} = #{pri_tbl.table_name}.#{pri_tbl.primary_key[idx]}" }
375
+ joins!("LEFT OUTER
376
+ JOIN (SELECT #{fk_col.join(', ')}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY #{(1..fk_col.length).to_a.join(', ')}) AS #{tbl_alias}
377
+ ON #{on_clause.join(' AND ')}")
378
+ else
379
+ joins!("LEFT OUTER
380
+ JOIN (SELECT #{fk_col}, COUNT(#{count_column}) AS _ct_ FROM #{associative&.table_name || hm.klass.table_name} GROUP BY 1) AS #{tbl_alias}
381
+ ON #{tbl_alias}.#{fk_col} = #{pri_tbl.table_name}.#{pri_tbl.primary_key}")
350
382
  end
351
383
  end
384
+ where!(wheres) unless wheres.empty?
352
385
  wheres unless wheres.empty? # Return the specific parameters that we did use
353
386
  end
387
+
388
+ private
389
+
390
+ def shift_or_first(ary)
391
+ ary.length > 1 ? ary.shift : ary.first
392
+ end
354
393
  end
355
394
 
356
395
  module Inheritance
@@ -549,27 +588,30 @@ class Object
549
588
  # Do the bulk of the has_many / belongs_to processing, and store details about HMT so they can be done at the very last
550
589
  hmts = fks.each_with_object(Hash.new { |h, k| h[k] = [] }) do |fk, hmts|
551
590
  # The key in each hash entry (fk.first) is the constraint name
552
- assoc_name = (assoc = fk.last)[:assoc_name]
553
- inverse_assoc_name = assoc[:inverse]&.fetch(:assoc_name, nil)
591
+ inverse_assoc_name = (assoc = fk.last)[:inverse]&.fetch(:assoc_name, nil)
554
592
  options = {}
555
593
  singular_table_name = ActiveSupport::Inflector.singularize(assoc[:inverse_table])
556
594
  macro = if assoc[:is_bt]
557
595
  # Try to take care of screwy names if this is a belongs_to going to an STI subclass
558
- if (primary_class = assoc.fetch(:primary_class, nil)) &&
559
- (sti_inverse_assoc = primary_class.reflect_on_all_associations.find { |a| a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key })
560
- assoc_name = sti_inverse_assoc.options[:inverse_of].to_s || assoc_name
561
- end
596
+ assoc_name = if (primary_class = assoc.fetch(:primary_class, nil)) &&
597
+ sti_inverse_assoc = primary_class.reflect_on_all_associations.find do |a|
598
+ a.macro == :has_many && a.options[:class_name] == self.name && assoc[:fk] = a.foreign_key
599
+ end
600
+ sti_inverse_assoc.options[:inverse_of]&.to_s || assoc_name
601
+ else
602
+ assoc[:assoc_name]
603
+ end
562
604
  need_class_name = singular_table_name.underscore != assoc_name
563
605
  need_fk = "#{assoc_name}_id" != assoc[:fk]
564
606
  if (inverse = assoc[:inverse])
565
607
  inverse_assoc_name, _x = _brick_get_hm_assoc_name(relations[assoc[:inverse_table]], inverse)
566
608
  if (has_ones = ::Brick.config.has_ones&.fetch(inverse[:alternate_name].camelize, nil))&.key?(singular_inv_assoc_name = ActiveSupport::Inflector.singularize(inverse_assoc_name))
567
609
  inverse_assoc_name = if has_ones[singular_inv_assoc_name]
568
- need_inverse_of = true
569
- has_ones[singular_inv_assoc_name]
570
- else
571
- singular_inv_assoc_name
572
- end
610
+ need_inverse_of = true
611
+ has_ones[singular_inv_assoc_name]
612
+ else
613
+ singular_inv_assoc_name
614
+ end
573
615
  end
574
616
  end
575
617
  :belongs_to
@@ -580,12 +622,12 @@ class Object
580
622
  need_fk = "#{ActiveSupport::Inflector.singularize(assoc[:inverse][:inverse_table])}_id" != assoc[:fk]
581
623
  # fks[table_name].find { |other_assoc| other_assoc.object_id != assoc.object_id && other_assoc[:assoc_name] == assoc[assoc_name] }
582
624
  if (has_ones = ::Brick.config.has_ones&.fetch(model_name, nil))&.key?(singular_assoc_name = ActiveSupport::Inflector.singularize(assoc_name))
583
- assoc_name = if has_ones[singular_assoc_name]
584
- need_class_name = true
585
- has_ones[singular_assoc_name]
586
- else
587
- singular_assoc_name
588
- end
625
+ assoc_name = if (custom_assoc_name = has_ones[singular_assoc_name])
626
+ need_class_name = custom_assoc_name != singular_assoc_name
627
+ custom_assoc_name
628
+ else
629
+ singular_assoc_name
630
+ end
589
631
  :has_one
590
632
  else
591
633
  :has_many
@@ -622,31 +664,34 @@ class Object
622
664
  end
623
665
  hmts.each do |hmt_fk, fks|
624
666
  fks.each do |fk|
625
- source = nil
626
- this_hmt_fk = if fks.length > 1
627
- singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
628
- source = fk.last
629
- through = fk.first[:alternate_name].pluralize
630
- "#{singular_assoc_name}_#{hmt_fk}"
631
- else
632
- source = fk.last unless hmt_fk.singularize == fk.last
633
- through = fk.first[:assoc_name].pluralize
634
- hmt_fk
635
- end
636
- code << " has_many :#{this_hmt_fk}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
667
+ through = fk.first[:assoc_name]
668
+ hmt_name = if fks.length > 1
669
+ if fks[0].first[:inverse][:assoc_name] == fks[1].first[:inverse][:assoc_name] # Same BT names pointing back to us? (Most common scenario)
670
+ "#{hmt_fk}_through_#{fk.first[:assoc_name]}"
671
+ else # Use BT names to provide uniqueness
672
+ through = fk.first[:alternate_name].pluralize
673
+ singular_assoc_name = fk.first[:inverse][:assoc_name].singularize
674
+ "#{singular_assoc_name}_#{hmt_fk}"
675
+ end
676
+ else
677
+ hmt_fk
678
+ end
679
+ source = fk.last unless hmt_name.singularize == fk.last
680
+ code << " has_many :#{hmt_name}, through: #{(assoc_name = through.to_sym).to_sym.inspect}#{", source: :#{source}" if source}\n"
637
681
  options = { through: assoc_name }
638
682
  options[:source] = source.to_sym if source
639
- self.send(:has_many, this_hmt_fk.to_sym, **options)
640
- end
641
- end
642
- # Not NULLables
643
- relation[:cols].each do |col, datatype|
644
- if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
645
- ::Brick.config.not_nullables.include?("#{matching}.#{col}")
646
- code << " validates :#{col}, presence: true\n"
647
- self.send(:validates, col.to_sym, { presence: true })
683
+ self.send(:has_many, hmt_name.to_sym, **options)
648
684
  end
649
685
  end
686
+ # # Not NULLables
687
+ # # %%% For the minute we've had to pull this out because it's been troublesome implementing the NotNull validator
688
+ # relation[:cols].each do |col, datatype|
689
+ # if (datatype[3] && ar_pks.exclude?(col) && ::Brick.config.metadata_columns.exclude?(col)) ||
690
+ # ::Brick.config.not_nullables.include?("#{matching}.#{col}")
691
+ # code << " validates :#{col}, not_null: true\n"
692
+ # self.send(:validates, col.to_sym, { not_null: true })
693
+ # end
694
+ # end
650
695
  end
651
696
  code << "end # model #{model_name}\n\n"
652
697
  end # class definition
@@ -665,14 +710,26 @@ class Object
665
710
  code << " @#{table_name} = #{model.name}#{model.primary_key ? ".order(#{model.primary_key.inspect})" : '.all'}\n"
666
711
  code << " @#{table_name}.brick_select(params)\n"
667
712
  code << " end\n"
713
+ self.protect_from_forgery unless: -> { self.request.format.js? }
668
714
  self.define_method :index do
669
715
  ::Brick.set_db_schema(params)
670
- ar_relation = model.all # model.primary_key ? model.order(model.primary_key) : model.all
716
+ if request.format == :csv # Asking for a template?
717
+ require 'csv'
718
+ exported_csv = CSV.generate(force_quotes: false) do |csv_out|
719
+ model.df_export(model.brick_import_template).each { |row| csv_out << row }
720
+ end
721
+ render inline: exported_csv, content_type: request.format
722
+ return
723
+ elsif request.format == :js # Asking for JSON?
724
+ render inline: model.df_export(model.brick_import_template).to_json, content_type: request.format
725
+ return
726
+ end
727
+
728
+ ar_relation = model.primary_key ? model.order("#{model.table_name}.#{model.primary_key}") : model.all
671
729
  @_brick_params = ar_relation.brick_select(params, (selects = []), (bt_descrip = {}), (hm_counts = {}), (join_array = ::Brick::JoinArray.new))
672
730
  # %%% Add custom HM count columns
673
731
  # %%% What happens when the PK is composite?
674
- counts = hm_counts.each_with_object([]) { |v, s| s << "COUNT(DISTINCT #{v.last.first}.#{v.last.last.primary_key}) AS _br_#{v.first}_ct" }
675
- puts counts.inspect
732
+ counts = hm_counts.each_with_object([]) { |v, s| s << "_br_#{v.first}._ct_ AS _br_#{v.first}_ct" }
676
733
  # *selects,
677
734
  instance_variable_set("@#{table_name}".to_sym, ar_relation.dup._select!(*selects, *counts))
678
735
  # binding.pry
@@ -705,6 +762,22 @@ class Object
705
762
  code << " end\n"
706
763
  self.define_method :update do
707
764
  ::Brick.set_db_schema(params)
765
+
766
+ if request.format == :csv # Importing CSV?
767
+ require 'csv'
768
+ # See if internally it's likely a TSV file (tab-separated)
769
+ tab_counts = []
770
+ 5.times { tab_counts << request.body.readline.count("\t") unless request.body.eof? }
771
+ request.body.rewind
772
+ separator = "\t" if tab_counts.length > 0 && tab_counts.uniq.length == 1 && tab_counts.first > 0
773
+ result = model.df_import(CSV.parse(request.body, { col_sep: separator || :auto }), model.brick_import_template)
774
+ # render inline: exported_csv, content_type: request.format
775
+ return
776
+ # elsif request.format == :js # Asking for JSON?
777
+ # render inline: model.df_export(true).to_json, content_type: request.format
778
+ # return
779
+ end
780
+
708
781
  instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(params[:id].split(','))))
709
782
  obj = obj.first if obj.is_a?(Array)
710
783
  obj.send(:update, send(params_name = params_name.to_sym))
@@ -730,7 +803,8 @@ class Object
730
803
 
731
804
  def _brick_get_hm_assoc_name(relation, hm_assoc)
732
805
  if relation[:hm_counts][hm_assoc[:assoc_name]]&.> 1
733
- [ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name]), true]
806
+ plural = ActiveSupport::Inflector.pluralize(hm_assoc[:alternate_name])
807
+ [hm_assoc[:alternate_name] == name.underscore ? "#{hm_assoc[:assoc_name].singularize}_#{plural}" : plural, true]
734
808
  else
735
809
  [ActiveSupport::Inflector.pluralize(hm_assoc[:inverse_table]), nil]
736
810
  end
@@ -987,18 +1061,16 @@ module Brick
987
1061
 
988
1062
  return if is_class || ::Brick.config.exclude_hms&.any? { |exclusion| fk[0] == exclusion[0] && fk[1] == exclusion[1] && primary_table == exclusion[2] }
989
1063
 
990
- cnstr_name = "hm_#{cnstr_name}"
991
- if (assoc_hm = hms.fetch(cnstr_name, nil))
1064
+ if (assoc_hm = hms.fetch((hm_cnstr_name = "hm_#{cnstr_name}"), nil))
992
1065
  assoc_hm[:fk] = assoc_hm[:fk].is_a?(String) ? [assoc_hm[:fk], fk[1]] : assoc_hm[:fk].concat(fk[1])
993
1066
  assoc_hm[:alternate_name] = "#{assoc_hm[:alternate_name]}_#{bt_assoc_name}" unless assoc_hm[:alternate_name] == bt_assoc_name
994
1067
  assoc_hm[:inverse] = assoc_bt
995
1068
  else
996
- assoc_hm = hms[cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
1069
+ assoc_hm = hms[hm_cnstr_name] = { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0], inverse: assoc_bt }
997
1070
  hm_counts = relation.fetch(:hm_counts) { relation[:hm_counts] = {} }
998
1071
  hm_counts[fk[0]] = hm_counts.fetch(fk[0]) { 0 } + 1
999
1072
  end
1000
1073
  assoc_bt[:inverse] = assoc_hm
1001
- # hms[cnstr_name] << { is_bt: false, fk: fk[1], assoc_name: fk[0], alternate_name: bt_assoc_name, inverse_table: fk[0] }
1002
1074
  end
1003
1075
  end
1004
1076
  end
@@ -64,6 +64,14 @@ module Brick
64
64
  is_template_exists
65
65
  end
66
66
 
67
+ def path_keys(fk_name, obj_name, pk)
68
+ if fk_name.is_a?(Array) && pk.is_a?(Array) # Composite keys?
69
+ fk_name.zip(pk.map { |pk_part| "#{obj_name}.#{pk_part}" })
70
+ else
71
+ [[fk_name, "#{obj_name}.#{pk}"]]
72
+ end.map { |x| "#{x.first}: #{x.last}"}.join(', ')
73
+ end
74
+
67
75
  alias :_brick_find_template :find_template
68
76
  def find_template(*args, **options)
69
77
  return _brick_find_template(*args, **options) unless @_brick_model
@@ -72,29 +80,36 @@ module Brick
72
80
  pk = @_brick_model.primary_key
73
81
  obj_name = model_name.underscore
74
82
  table_name = model_name.pluralize.underscore
83
+ template_link = nil
75
84
  bts, hms, associatives = ::Brick.get_bts_and_hms(@_brick_model) # This gets BT and HM and also has_many :through (HMT)
76
- hms_columns = +'' # Used for 'index'
85
+ hms_columns = [] # Used for 'index'
86
+ skip_klass_hms = ::Brick.config.skip_index_hms[model_name] || {}
77
87
  hms_headers = hms.each_with_object([]) do |hm, s|
78
- hm_assoc = hm.last
88
+ hm_stuff = [(hm_assoc = hm.last), "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]}", (assoc_name = hm.first)]
89
+ hm_fk_name = if hm_assoc.options[:through]
90
+ associative = associatives[hm_assoc.name]
91
+ "'#{associative.name}.#{associative.foreign_key}'"
92
+ else
93
+ hm_assoc.foreign_key
94
+ end
79
95
  if args.first == 'index'
80
- hm_fk_name = if hm_assoc.options[:through]
81
- associative = associatives[hm_assoc.name]
82
- "'#{associative.name}.#{associative.foreign_key}'"
83
- else
84
- hm_assoc.foreign_key
85
- end
86
96
  hms_columns << if hm_assoc.macro == :has_many
87
- "<td>
88
- <%= ct = #{obj_name}._br_#{hm.first}_ct
89
- link_to \"#\{ct\} #{hm.first}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{hm_fk_name}: #{obj_name}.#{pk} }) unless ct.zero? %>
90
- </td>\n"
97
+ set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
98
+ 'nil'
99
+ else
100
+ # Postgres column names are limited to 63 characters
101
+ attrib_name = "_br_#{assoc_name}_ct"[0..62]
102
+ "#{obj_name}.#{attrib_name} || 0"
103
+ end
104
+ "<%= ct = #{set_ct}
105
+ link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
91
106
  else # has_one
92
- "<td>
93
- <%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>
94
- </td>\n"
107
+ "<%= obj = #{obj_name}.#{hm.first}; link_to(obj.brick_descrip, obj) if obj %>\n"
95
108
  end
109
+ elsif args.first == 'show'
110
+ hm_stuff << "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.pluralize}_path({ #{path_keys(hm_fk_name, "@#{obj_name}&.first&", pk)} }) %>\n"
96
111
  end
97
- s << [hm_assoc, "H#{hm_assoc.macro == :has_one ? 'O' : 'M'}#{'T' if hm_assoc.options[:through]} #{hm.first}"]
112
+ s << hm_stuff
98
113
  end
99
114
 
100
115
  schema_options = ::Brick.db_schemas.each_with_object(+'') { |v, s| s << "<option value=\"#{v}\">#{v}</option>" }.html_safe
@@ -103,6 +118,13 @@ module Brick
103
118
  table_options = (::Brick.relations.keys - ::Brick.config.exclude_tables)
104
119
  .each_with_object(+'') { |v, s| s << "<option value=\"#{v.underscore.pluralize}\">#{v}</option>" }.html_safe
105
120
  css = +"<style>
121
+ #dropper {
122
+ background-color: #eee;
123
+ }
124
+ #btnImport {
125
+ display: none;
126
+ }
127
+
106
128
  table {
107
129
  border-collapse: collapse;
108
130
  margin: 25px 0;
@@ -114,9 +136,12 @@ table {
114
136
 
115
137
  table thead tr th, table tr th {
116
138
  background-color: #009879;
117
- color: #ffffff;
139
+ color: #fff;
118
140
  text-align: left;
119
141
  }
142
+ table thead tr th a, table tr th a {
143
+ color: #80FFB8;
144
+ }
120
145
 
121
146
  table th, table td {
122
147
  padding: 0.2em 0.5em;
@@ -125,6 +150,9 @@ table th, table td {
125
150
  .show-field {
126
151
  background-color: #004998;
127
152
  }
153
+ .show-field a {
154
+ color: #80B8D2;
155
+ }
128
156
 
129
157
  table tbody tr {
130
158
  border-bottom: thin solid #dddddd;
@@ -177,10 +205,10 @@ def hide_bcrypt(val)
177
205
  end %>"
178
206
 
179
207
  if ['index', 'show', 'update'].include?(args.first)
180
- # Example: <% bts = { "site_id" => [:site, Site, "id"], "study_id" => [:study, Study, "id"], "study_country_id" => [:study_country, StudyCountry, "id"], "user_id" => [:user, User, "id"], "role_id" => [:role, Role, "id"] } %>
181
208
  css << "<% bts = { #{bts.each_with_object([]) { |v, s| s << "#{v.first.inspect} => [#{v.last.first.inspect}, #{v.last[1].name}, #{v.last[1].primary_key.inspect}]"}.join(', ')} } %>"
182
209
  end
183
210
 
211
+ # %%% When doing schema select, if there's an ID then remove it, or if we're on a new page go to index
184
212
  script = "<script>
185
213
  var schemaSelect = document.getElementById(\"schema\");
186
214
  var brickSchema;
@@ -237,11 +265,106 @@ function changeout(href, param, value) {
237
265
  </script>"
238
266
  inline = case args.first
239
267
  when 'index'
268
+ obj_pk = if pk&.is_a?(Array) # Composite primary key?
269
+ "[#{pk.map { |pk_part| "#{obj_name}.#{pk_part}" }.join(', ')}]"
270
+ elsif pk
271
+ "#{obj_name}.#{pk}"
272
+ end
273
+ if Object.const_defined?('DutyFree')
274
+ template_link = "
275
+ <%= link_to 'CSV', #{table_name}_path(format: :csv) %> &nbsp; <a href=\"#\" id=\"sheetsLink\">Sheets</a>
276
+ <div id=\"dropper\" contenteditable=\"true\"></div>
277
+ <input type=\"button\" id=\"btnImport\" value=\"Import\">"
278
+ end
240
279
  "#{css}
241
280
  <p style=\"color: green\"><%= notice %></p>#{"
242
281
  <select id=\"schema\">#{schema_options}</select>" if ::Brick.db_schemas.length > 1}
243
282
  <select id=\"tbl\">#{table_options}</select>
244
- <h1>#{model_name.pluralize}</h1>
283
+ <h1>#{model_name.pluralize}</h1>#{template_link}
284
+ <script>
285
+ var dropperDiv = document.getElementById(\"dropper\");
286
+ var btnImport = document.getElementById(\"btnImport\");
287
+ var droppedTSV;
288
+ if (dropperDiv) { // Other interesting events: blur keyup input
289
+ dropperDiv.addEventListener(\"paste\", function (evt) {
290
+ droppedTSV = evt.clipboardData.getData('text/plain');
291
+ var html = evt.clipboardData.getData('text/html');
292
+ var tbl = html.substring(html.indexOf(\"<tbody>\") + 7, html.lastIndexOf(\"</tbody>\"));
293
+ console.log(tbl);
294
+ btnImport.style.display = droppedTSV.length > 0 ? \"block\" : \"none\";
295
+ });
296
+ btnImport.addEventListener(\"click\", function () {
297
+ fetch(changeout(<%= #{obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
298
+ method: 'PATCH',
299
+ headers: { 'Content-Type': 'text/tab-separated-values' },
300
+ body: droppedTSV
301
+ }).then(function (tsvResponse) {
302
+ btnImport.style.display = \"none\";
303
+ console.log(\"toaster\", tsvResponse);
304
+ });
305
+ });
306
+ }
307
+ var sheetUrl;
308
+ var spreadsheetId;
309
+ var sheetsLink = document.getElementById(\"sheetsLink\");
310
+ function gapiLoaded() {
311
+ // Have a click on the sheets link to bring up the sign-in window. (Must happen from some kind of user click.)
312
+ sheetsLink.addEventListener(\"click\", async function (evt) {
313
+ evt.preventDefault();
314
+ await gapi.load(\"client\", function () {
315
+ gapi.client.init({ // Load the discovery doc to initialize the API
316
+ clientId: \"487319557829-fgj4u660igrpptdji7ev0r5hb6kh05dh.apps.googleusercontent.com\",
317
+ scope: \"https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file\",
318
+ discoveryDocs: [\"https://sheets.googleapis.com/$discovery/rest?version=v4\"]
319
+ }).then(function () {
320
+ gapi.auth2.getAuthInstance().isSignedIn.listen(updateSignInStatus);
321
+ updateSignInStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
322
+ });
323
+ });
324
+ });
325
+ }
326
+
327
+ async function updateSignInStatus(isSignedIn) {
328
+ if (isSignedIn) {
329
+ console.log(\"turds!\");
330
+ await gapi.client.sheets.spreadsheets.create({
331
+ properties: {
332
+ title: #{table_name.inspect},
333
+ },
334
+ sheets: [
335
+ // sheet1, sheet2, sheet3
336
+ ]
337
+ }).then(function (response) {
338
+ sheetUrl = response.result.spreadsheetUrl;
339
+ spreadsheetId = response.result.spreadsheetId;
340
+ sheetsLink.setAttribute(\"href\", sheetUrl); // response.result.spreadsheetUrl
341
+ console.log(\"x1\", sheetUrl);
342
+
343
+ // Get JSON data
344
+ fetch(changeout(<%= #{table_name}_path(format: :js).inspect.html_safe %>, \"_brick_schema\", brickSchema)).then(function (response) {
345
+ response.json().then(function (data) {
346
+ gapi.client.sheets.spreadsheets.values.append({
347
+ spreadsheetId: spreadsheetId,
348
+ range: \"Sheet1\",
349
+ valueInputOption: \"RAW\",
350
+ insertDataOption: \"INSERT_ROWS\"
351
+ }, {
352
+ range: \"Sheet1\",
353
+ majorDimension: \"ROWS\",
354
+ values: data,
355
+ }).then(function (response2) {
356
+ // console.log(\"beefcake\", response2);
357
+ });
358
+ });
359
+ });
360
+ });
361
+ window.open(sheetUrl, '_blank');
362
+ }
363
+ }
364
+ </script>
365
+ <script async defer src=\"https://apis.google.com/js/api.js\" onload=\"gapiLoaded()\"></script>
366
+
367
+
245
368
  <% if @_brick_params&.present? %><h3>where <%= @_brick_params.each_with_object([]) { |v, s| s << \"#\{v.first\} = #\{v.last.inspect\}\" }.join(', ') %></h3><% end %>
246
369
  <table id=\"#{table_name}\">
247
370
  <thead><tr>#{'<th></th>' if pk}
@@ -249,26 +372,27 @@ function changeout(href, param, value) {
249
372
  <% next if col == '#{pk}' || ::Brick.config.metadata_columns.include?(col) %>
250
373
  <th>
251
374
  <% if (bt = bts[col]) %>
252
- BT <%= \"#\{bt.first\}-\" unless bt[1].name.underscore == bt.first.to_s %><%= bt[1].name %>
375
+ BT <%= bt[1].bt_link(bt.first) %>
253
376
  <% else %>
254
377
  <%= col %>
255
378
  <% end %>
256
379
  </th>
257
380
  <% end %>
258
- #{hms_headers.map { |h| "<th>#{h.last}</th>\n" }.join}
381
+ <%# Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name %>
382
+ #{hms_headers.map { |h| "<th>#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.pluralize}_path) %></th>\n" }.join}
259
383
  </tr></thead>
260
384
 
261
385
  <tbody>
262
386
  <% @#{table_name}.each do |#{obj_name}| %>
263
387
  <tr>#{"
264
- <td><%= link_to '⇛', #{obj_name}_path(#{obj_name}.#{pk}), { class: 'big-arrow' } %></td>" if pk}
388
+ <td><%= link_to '⇛', #{obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
265
389
  <% #{obj_name}.attributes.each do |k, val| %>
266
390
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) || k.start_with?('_brfk_') || (k.start_with?('_br_') && k.end_with?('_ct')) %>
267
391
  <td>
268
392
  <% if (bt = bts[k]) %>
269
- <%# binding.pry if bt.first == :user %>
270
- <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last) }, @_brick_bt_descrip[bt.first][2]) %>
271
- <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(bt_id_col) if bt_id_col %>
393
+ <%# binding.pry # Postgres column names are limited to 63 characters %>
394
+ <% bt_txt = bt[1].brick_descrip(#{obj_name}, @_brick_bt_descrip[bt.first][1].map { |z| #{obj_name}.send(z.last[0..62]) }, @_brick_bt_descrip[bt.first][2]) %>
395
+ <% bt_id_col = @_brick_bt_descrip[bt.first][2]; bt_id = #{obj_name}.send(*bt_id_col) if bt_id_col&.present? %>
272
396
  <%= bt_id ? link_to(bt_txt, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_id)) : bt_txt %>
273
397
  <%#= Previously was: bt_obj = bt[1].find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt_obj_path_base = bt[1].name.underscore\}_path\".to_sym, bt_obj.send(bt[1].primary_key.to_sym))) if bt_obj %>
274
398
  <% else %>
@@ -276,7 +400,7 @@ function changeout(href, param, value) {
276
400
  <% end %>
277
401
  </td>
278
402
  <% end %>
279
- #{hms_columns}
403
+ <td>#{hms_columns.join('</td><td>')}</td>
280
404
  <!-- td>X</td -->
281
405
  </tr>
282
406
  </tbody>
@@ -294,12 +418,13 @@ function changeout(href, param, value) {
294
418
  <%= link_to '(See all #{obj_name.pluralize})', #{table_name}_path %>
295
419
  <% if obj %>
296
420
  <%= # path_options = [obj.#{pk}]
297
- # path_options << { '_brick_schema': } if
421
+ # path_options << { '_brick_schema': } if
298
422
  # url = send(:#{model_name.underscore}_path, obj.#{pk})
299
- form_for(obj) do |f| %>
423
+ form_for(obj.becomes(#{model_name})) do |f| %>
300
424
  <table>
301
425
  <% @#{obj_name}.first.attributes.each do |k, val| %>
302
426
  <tr>
427
+ <%# %%% Accommodate composite keys %>
303
428
  <% next if k == '#{pk}' || ::Brick.config.metadata_columns.include?(k) %>
304
429
  <th class=\"show-field\">
305
430
  <% if (bt = bts[k])
@@ -308,9 +433,10 @@ function changeout(href, param, value) {
308
433
  # %%% Only do this if the user has permissions to edit this bt field
309
434
  if bt.length < 4
310
435
  bt << (option_detail = [[\"(No #\{bt_name\} chosen)\", '^^^brick_NULL^^^']])
311
- bt[1].order(:#{pk}).each { |obj| option_detail << [obj.brick_descrip, obj.#{pk}] }
436
+ # %%% Accommodate composite keys for obj.pk at the end here
437
+ bt[1].order(bt[1].primary_key).each { |obj| option_detail << [obj.brick_descrip, obj.send(bt[1].primary_key)] }
312
438
  end %>
313
- BT <%= \"#\{bt.first\}-\" unless bt_name.underscore == bt.first.to_s %><%= bt_name %>
439
+ BT <%= bt[1].bt_link(bt.first) %>
314
440
  <% else %>
315
441
  <%= k %>
316
442
  <% end %>
@@ -346,21 +472,24 @@ function changeout(href, param, value) {
346
472
  </table>
347
473
  <% end %>
348
474
 
349
- #{hms_headers.map do |hm|
350
- next unless (pk = hm.first.klass.primary_key)
351
-
352
- "<table id=\"#{hm_name = hm.first.name.to_s}\">
353
- <tr><th>#{hm.last}</th></tr>
354
- <% collection = @#{obj_name}.first.#{hm_name}
355
- collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
356
- if collection.empty? %>
357
- <tr><td>(none)</td></tr>
358
- <% else %>
359
- <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
360
- <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
361
- <% end %>
362
- <% end %>
363
- </table>" end.join}
475
+ #{hms_headers.each_with_object(+'') do |hm, s|
476
+ if (pk = hm.first.klass.primary_key)
477
+ s << "<table id=\"#{hm_name = hm.first.name.to_s}\">
478
+ <tr><th>#{hm[3]}</th></tr>
479
+ <% collection = @#{obj_name}.first.#{hm_name}
480
+ collection = collection.is_a?(ActiveRecord::Associations::CollectionProxy) ? collection.order(#{pk.inspect}) : [collection]
481
+ if collection.empty? %>
482
+ <tr><td>(none)</td></tr>
483
+ <% else %>
484
+ <% collection.uniq.each do |#{hm_singular_name = hm_name.singularize.underscore}| %>
485
+ <tr><td><%= link_to(#{hm_singular_name}.brick_descrip, #{hm.first.klass.name.underscore}_path(#{hm_singular_name}.#{pk})) %></td></tr>
486
+ <% end %>
487
+ <% end %>
488
+ </table>"
489
+ else
490
+ s
491
+ end
492
+ end}
364
493
  <% end %>
365
494
  #{script}"
366
495
 
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 20
8
+ TINY = 23
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
@@ -103,11 +103,8 @@ module Brick
103
103
 
104
104
  def get_bts_and_hms(model)
105
105
  bts, hms = model.reflect_on_all_associations.each_with_object([{}, {}]) do |a, s|
106
- # So that we can map an association name to any special alias name used in an AREL query
107
- ans = (model._assoc_names[a.name] ||= [])
108
106
  next if !const_defined?(a.name.to_s.singularize.camelize) && ::Brick.config.exclude_tables.include?(a.plural_name)
109
107
 
110
- ans << a.klass unless ans.include?(a.klass)
111
108
  case a.macro
112
109
  when :belongs_to
113
110
  s.first[a.foreign_key] = [a.name, a.klass]
@@ -129,9 +126,7 @@ module Brick
129
126
  skip_hms[hmt.last.name] = nil
130
127
  end
131
128
  end
132
- skip_hms.each do |k, _v|
133
- puts hms.delete(k).inspect
134
- end
129
+ skip_hms.each { |k, _v| hms.delete(k) }
135
130
  [bts, hms, associatives]
136
131
  end
137
132
 
@@ -240,6 +235,12 @@ module Brick
240
235
  end
241
236
  end
242
237
 
238
+ # Skip showing counts for these specific has_many associations when building auto-generated #index views
239
+ # @api public
240
+ def skip_index_hms=(value)
241
+ Brick.config.skip_index_hms = value
242
+ end
243
+
243
244
  # Associations to treat as a has_one
244
245
  # @api public
245
246
  def has_ones=(hos)
@@ -421,9 +422,7 @@ ActiveSupport.on_load(:active_record) do
421
422
  end
422
423
 
423
424
  result = result.map do |attributes|
424
- values = klass.initialize_attributes(attributes).values
425
-
426
- columns.zip(values).map do |column, value|
425
+ columns.zip(klass.initialize_attributes(attributes).values).map do |column, value|
427
426
  column.type_cast(value)
428
427
  end
429
428
  end
@@ -111,11 +111,16 @@ module Brick
111
111
  # # to be the primary key.)
112
112
  #{bar}
113
113
 
114
- # # Skip creating a has_many association for these
114
+ # # Skip creating a has_many association for these (only retain the belongs_to built from this additional_reference).
115
115
  # # (Uses the same exact three-part format as would define an additional_reference)
116
116
  # # Say for instance that we didn't care to display the favourite colours that users have:
117
117
  # Brick.exclude_hms = [['users', 'favourite_colour_id', 'colours']]
118
118
 
119
+ # # Skip showing counts for these specific has_many associations when building auto-generated #index views.
120
+ # # When there are related tables with a significant number of records, this can lessen the load on the database
121
+ # # considerably, sometimes fixing what might appear to be an index page that just \"hangs\" for no apparent reason.
122
+ Brick.skip_index_hms = ['User.litany_of_woes']
123
+
119
124
  # # By default primary tables involved in a foreign key relationship will indicate a \"has_many\" relationship pointing
120
125
  # # back to the foreign table. In order to represent a \"has_one\" association instead, an override can be provided
121
126
  # # using the primary model name and the association name which you instead want to have treated as a \"has_one\":
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.20
4
+ version: 1.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lorin Thwaits
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-04-29 00:00:00.000000000 Z
11
+ date: 2022-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord