brick 1.0.51 → 1.0.54

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: 9d0c2c69187f4d9b8eaa4a22777d3e517546bf355316012bf3e333064f8939b7
4
- data.tar.gz: 33fcb2f6ca4373eba2db3a2891de392d9ef71ea693687328f72d23837cabdc46
3
+ metadata.gz: '09f9f2f9f7e21a96d29cdad815c069e52280ea068051f27a542541bcefd33e87'
4
+ data.tar.gz: 54e032633f930a43d1bc929d05d241367d9ebbdb8010f588b922d1111077524e
5
5
  SHA512:
6
- metadata.gz: 7136cb61d19325c1916e9d2e8e6cd7ed6c1d6838c77a216c5275bf4cc0d3540eeabf5caae152b49b9f5cf0d5ea7307bebd98f2fdbc03e651310c1cfc6fbec436
7
- data.tar.gz: 81007bc391e9871ab0bbe80fc5aaa440da2685e1abe16d65d09419e31e93529232757db173cf97ba78ad3e719af760cac2d83b57cb7d97fa488399d93d3d2f76
6
+ metadata.gz: 56cad221c2b20ac62839c1b7904c8acb9bc392b0779946bbe325c48c8ee5cf00b33413e070674343809ee851804e5f9bbec2d6469a148688c4aecbbcaee3e09d
7
+ data.tar.gz: 9757effb2a1d058ae4ccd259b59e0218e002928d90dadfd6ff3d0acd05fb12e27cfcfef30afa3964637e0e4f1e4246b410deba45fb01e452e20056df94820377
@@ -1116,6 +1116,11 @@ class Object
1116
1116
  if namespace && (idx = lookup_context.prefixes.index(table_name))
1117
1117
  lookup_context.prefixes[idx] = "#{namespace.name.underscore}/#{lookup_context.prefixes[idx]}"
1118
1118
  end
1119
+ @_brick_excl = session[:_brick_exclude]&.split(',')&.each_with_object([]) do |excl, s|
1120
+ if (excl_parts = excl.split('.')).first == table_name
1121
+ s << excl_parts.last
1122
+ end
1123
+ end
1119
1124
  @_brick_bt_descrip = model._br_bt_descrip
1120
1125
  @_brick_hm_counts = model._br_hm_counts
1121
1126
  @_brick_join_array = join_array
@@ -1124,9 +1129,7 @@ class Object
1124
1129
  is_pk_string = nil
1125
1130
  if (pk_col = model&.primary_key)
1126
1131
  code << " def show\n"
1127
- code << (find_by_id = " id = params[:id]&.split(/[\\/,_]/)
1128
- id = id.first if id.is_a?(Array) && id.length == 1
1129
- @#{singular_table_name} = #{model.name}.find(id)\n")
1132
+ code << " #{find_by_name = "find_#{singular_table_name}"}\n"
1130
1133
  code << " end\n"
1131
1134
  self.define_method :show do
1132
1135
  ::Brick.set_db_schema(params)
@@ -1137,35 +1140,59 @@ class Object
1137
1140
  params[:id]&.split(/[\/,_]/)
1138
1141
  end
1139
1142
  id = id.first if id.is_a?(Array) && id.length == 1
1140
- instance_variable_set("@#{singular_table_name}".to_sym, model.find(id))
1143
+ instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1141
1144
  end
1142
1145
  end
1143
1146
 
1144
1147
  # By default, views get marked as read-only
1145
- unless is_swagger # model.readonly # (relation = relations[model.table_name]).key?(:isView)
1146
- code << " # (Define :new, :create)\n"
1148
+ # unless model.readonly # (relation = relations[model.table_name]).key?(:isView)
1149
+ code << " def new\n"
1150
+ code << " @#{singular_table_name} = #{model.name}.new\n"
1151
+ code << " end\n"
1152
+ self.define_method :new do
1153
+ ::Brick.set_db_schema(params)
1154
+ instance_variable_set("@#{singular_table_name}".to_sym, model.new)
1155
+ end
1156
+
1157
+ params_name_sym = (params_name = "#{singular_table_name}_params").to_sym
1158
+
1159
+ code << " def create\n"
1160
+ code << " @#{singular_table_name} = #{model.name}.create(#{params_name})\n"
1161
+ code << " end\n"
1162
+ self.define_method :create do
1163
+ ::Brick.set_db_schema(params)
1164
+ if (is_json = request.content_type == 'application/json') && (col = params['_brick_exclude'])
1165
+ session[:_brick_exclude] = ((session[:_brick_exclude]&.split(',') || []) + ["#{table_name}.#{col}"]).join(',')
1166
+ render json: { result: ::Brick.exclude_column(table_name, col) }
1167
+ elsif is_json && (col = params['_brick_unexclude'])
1168
+ if (excls = ((session[:_brick_exclude]&.split(',') || []) - ["#{table_name}.#{col}"]).join(',')).empty?
1169
+ session.delete(:_brick_exclude)
1170
+ else
1171
+ session[:_brick_exclude] = excls
1172
+ end
1173
+ render json: { result: ::Brick.unexclude_column(table_name, col) }
1174
+ else
1175
+ instance_variable_set("@#{singular_table_name}".to_sym,
1176
+ model.send(:create, send(params_name_sym)))
1177
+ end
1178
+ end
1147
1179
 
1148
- if model.primary_key
1180
+ if pk_col
1149
1181
  # if (schema = ::Brick.config.schema_behavior[:multitenant]&.fetch(:schema_to_analyse, nil)) && ::Brick.db_schemas&.include?(schema)
1150
1182
  # ActiveRecord::Base.execute_sql("SET SEARCH_PATH = ?;", schema)
1151
1183
  # end
1152
1184
 
1153
1185
  is_need_params = true
1154
- # code << " # (Define :destroy)\n"
1155
1186
  code << " def edit\n"
1156
- code << find_by_id
1187
+ code << " #{find_by_name}\n"
1157
1188
  code << " end\n"
1158
1189
  self.define_method :edit do
1159
1190
  ::Brick.set_db_schema(params)
1160
- id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1161
- id = id.first if id.is_a?(Array) && id.length == 1
1162
- instance_variable_set("@#{singular_table_name}".to_sym, model.find(id))
1191
+ instance_variable_set("@#{singular_table_name}".to_sym, find_obj)
1163
1192
  end
1164
1193
 
1165
1194
  code << " def update\n"
1166
- code << find_by_id
1167
- params_name = "#{singular_table_name}_params"
1168
- code << " @#{singular_table_name}.update(#{params_name})\n"
1195
+ code << " #{find_by_name}.update(#{params_name})\n"
1169
1196
  code << " end\n"
1170
1197
  self.define_method :update do
1171
1198
  ::Brick.set_db_schema(params)
@@ -1184,31 +1211,50 @@ class Object
1184
1211
  # return
1185
1212
  end
1186
1213
 
1214
+ instance_variable_set("@#{singular_table_name}".to_sym, (obj = find_obj))
1215
+ obj.send(:update, send(params_name_sym))
1216
+ end
1217
+
1218
+ code << " def destroy\n"
1219
+ code << " #{find_by_name}.destroy\n"
1220
+ code << " end\n"
1221
+ self.define_method :destroy do
1222
+ ::Brick.set_db_schema(params)
1223
+ instance_variable_set("@#{singular_table_name}".to_sym, find_obj.send(:destroy))
1224
+ end
1225
+ end
1226
+
1227
+ code << "private\n" if pk_col || is_need_params
1228
+
1229
+ if pk_col
1230
+ code << " def find_#{singular_table_name}
1231
+ id = params[:id]&.split(/[\\/,_]/)
1232
+ @#{singular_table_name} = #{model.name}.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1233
+ end\n"
1234
+ self.define_method :find_obj do
1187
1235
  id = is_pk_string ? params[:id] : params[:id]&.split(/[\/,_]/)
1188
- id = id.first if id.is_a?(Array) && id.length == 1
1189
- instance_variable_set("@#{singular_table_name}".to_sym, (obj = model.find(id)))
1190
- obj = obj.first if obj.is_a?(Array)
1191
- obj.send(:update, send(params_name = params_name.to_sym))
1236
+ model.find(id.is_a?(Array) && id.length == 1 ? id.first : id)
1192
1237
  end
1193
1238
  end
1194
1239
 
1195
1240
  if is_need_params
1196
- code << "private\n"
1197
1241
  code << " def #{params_name}\n"
1198
- permits = model.columns_hash.keys.map { |c| c.to_sym.inspect } +
1199
- model.reflect_on_all_associations.each_with_object([]) do |assoc, s|
1200
- s << "#{assoc.name.to_s.singularize}_ids: []" if assoc.macro == :has_many && assoc.options[:through]
1201
- end
1242
+ permits = model.columns_hash.keys.map(&:to_sym)
1243
+ permits_txt = permits.map(&:inspect) +
1244
+ model.reflect_on_all_associations.select { |assoc| assoc.macro == :has_many && assoc.options[:through] }.map do |assoc|
1245
+ permits << { "#{assoc.name.to_s.singularize}_ids".to_sym => [] }
1246
+ "#{assoc.name.to_s.singularize}_ids: []"
1247
+ end
1202
1248
  code << " params.require(:#{require_name = model.name.underscore.tr('/', '_')
1203
- }).permit(#{permits.join(', ')})\n"
1249
+ }).permit(#{permits_txt.join(', ')})\n"
1204
1250
  code << " end\n"
1205
1251
  self.define_method(params_name) do
1206
- params.require(require_name.to_sym).permit(model.columns_hash.keys)
1252
+ params.require(require_name.to_sym).permit(permits)
1207
1253
  end
1208
1254
  private params_name
1209
1255
  # Get column names for params from relations[model.table_name][:cols].keys
1210
1256
  end
1211
- end
1257
+ # end
1212
1258
  code << "end # #{namespace_name}#{class_name}\n\n"
1213
1259
  end # class definition
1214
1260
  [built_controller, code]
@@ -1243,7 +1289,10 @@ module ActiveRecord::ConnectionHandling
1243
1289
  alias _brick_establish_connection establish_connection
1244
1290
  def establish_connection(*args)
1245
1291
  conn = _brick_establish_connection(*args)
1246
- _brick_reflect_tables
1292
+ begin
1293
+ _brick_reflect_tables
1294
+ rescue ActiveRecord::NoDatabaseError
1295
+ end
1247
1296
  conn
1248
1297
  end
1249
1298
 
@@ -132,27 +132,24 @@ module Brick
132
132
  end
133
133
  case args.first
134
134
  when 'index'
135
- hms_columns << if hm_assoc.macro == :has_many
135
+ hm_entry = +"'#{hm_assoc.name}' => [#{assoc_name.inspect}"
136
+ hm_entry << if hm_assoc.macro == :has_many
137
+ if hm_fk_name # %%% Can remove this check when multiple foreign keys to same destination becomes bulletproof
136
138
  set_ct = if skip_klass_hms.key?(assoc_name.to_sym)
137
- 'nil'
138
- else
139
- # Postgres column names are limited to 63 characters
140
- attrib_name = "_br_#{assoc_name}_ct"[0..62]
141
- "#{obj_name}.#{attrib_name} || 0"
142
- end
143
- if hm_fk_name
144
- "<%= ct = #{set_ct}
145
- link_to \"#\{ct || 'View'\} #{assoc_name}\", #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)} }) unless ct&.zero? %>\n"
146
- else # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
147
- "#{assoc_name}\n"
148
- end
149
- else # has_one
150
- # 0..62 because Postgres column names are limited to 63 characters
151
- "<%= descrips = @_brick_bt_descrip[#{hm.first.inspect}][ho_class = #{hm[1].klass.name}]
152
- ho_txt = ho_class.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
153
- ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
154
- ho_id&.first ? link_to(ho_txt, send(\"#\{ho_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt %>\n"
139
+ 'nil'
140
+ else
141
+ # Postgres column names are limited to 63 characters
142
+ "#{obj_name}.#{"_br_#{assoc_name}_ct"[0..62]} || 0"
143
+ end
144
+ ", #{set_ct}, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}"
155
145
  end
146
+ else # has_one
147
+ # 0..62 because Postgres column names are limited to 63 characters
148
+ ", nil, #{path_keys(hm_assoc, hm_fk_name, obj_name, pk)}"
149
+ end
150
+ hm_entry << ']'
151
+ puts hm_entry
152
+ hms_columns << hm_entry
156
153
  when 'show', 'update'
157
154
  hm_stuff << if hm_fk_name
158
155
  "<%= link_to '#{assoc_name}', #{hm_assoc.klass.name.underscore.tr('/', '_').pluralize}_path({ #{path_keys(hm_assoc, hm_fk_name, "@#{obj_name}", pk)} }) %>\n"
@@ -178,6 +175,9 @@ module Brick
178
175
  end.html_safe
179
176
  table_options << '<option value="brick_orphans">(Orphans)</option>'.html_safe if is_orphans
180
177
  css = +"<style>
178
+ h1, h3 {
179
+ margin-bottom: 0;
180
+ }
181
181
  #dropper {
182
182
  background-color: #eee;
183
183
  }
@@ -199,19 +199,41 @@ table {
199
199
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
200
200
  }
201
201
 
202
- table thead tr th, table tr th {
202
+ tr th {
203
203
  background-color: #009879;
204
204
  color: #fff;
205
205
  text-align: left;
206
206
  }
207
- #headerTop th:hover, #headerTop th:hover {
207
+ #headerTop tr th {
208
+ position: relative;
209
+ }
210
+ #headerTop tr th .exclude {
211
+ position: absolute;
212
+ display: none;
213
+ top: 0;
214
+ right: 0;
215
+ }
216
+ #headerTop tr th:hover {
208
217
  background-color: #18B090;
209
218
  }
210
- table thead tr th a, table tr th a {
219
+ #exclusions {
220
+ font-size: 0.7em;
221
+ }
222
+ #exclusions div {
223
+ border: 1px solid blue;
224
+ display: inline-block;
225
+ cursor: copy;
226
+ }
227
+ #headerTop tr th:hover .exclude {
228
+ display: inline;
229
+ cursor: pointer;
230
+ color: red;
231
+ }
232
+ tr th a {
211
233
  color: #80FFB8;
212
234
  }
213
235
 
214
- table th, table td {
236
+ tr th, tr td {
215
237
  padding: 0.2em 0.5em;
216
238
  }
217
239
 
@@ -257,6 +279,7 @@ a.big-arrow {
257
279
  }
258
280
  .dimmed {
259
281
  background-color: #C0C0C0;
282
+ text-align: center;
260
283
  }
261
284
  .orphan {
262
285
  color: red;
@@ -285,8 +308,8 @@ input+svg.revert {
285
308
  color: #FFF;
286
309
  }
287
310
  </style>
288
- <% is_includes_dates = nil
289
311
 
312
+ <% is_includes_dates = nil
290
313
  def is_bcrypt?(val)
291
314
  val.is_a?(String) && val.length == 60 && val.start_with?('$2a$')
292
315
  end
@@ -418,8 +441,19 @@ function setHeaderSizes() {
418
441
  for (var i = 0; i < row.childNodes.length; ++i) {
419
442
  node = row.childNodes[i];
420
443
  if (node.nodeType === 1) {
421
- var style = tr.childNodes[i].style;
422
- style.minWidth = style.maxWidth = getComputedStyle(node).width;
444
+ var th = tr.childNodes[i];
445
+ th.style.minWidth = th.style.maxWidth = getComputedStyle(node).width;
446
+ if (#{pk&.present? ? 'i > 0' : 'true'}) {
447
+ // Add <span> at the end
448
+ var span = document.createElement(\"SPAN\");
449
+ span.className = \"exclude\";
450
+ span.innerHTML = \"X\";
451
+ span.addEventListener(\"click\", function (e) {
452
+ e.stopPropagation();
453
+ doFetch(\"POST\", {_brick_exclude: this.parentElement.getAttribute(\"x-order\")});
454
+ });
455
+ th.appendChild(span);
456
+ }
423
457
  }
424
458
  }
425
459
  if (isEmpty) headerTop.appendChild(tr);
@@ -427,6 +461,18 @@ function setHeaderSizes() {
427
461
  grid.style.marginTop = \"-\" + getComputedStyle(headerTop).height;
428
462
  // console.log(\"end\");
429
463
  }
464
+ function doFetch(method, payload, success) {
465
+ payload.authenticity_token = <%= session[:_csrf_token].inspect.html_safe %>;
466
+ if (!success) {
467
+ success = function (p) {p.text().then(function (response) {
468
+ var result = JSON.parse(response).result;
469
+ if (result) location.href = location.href;
470
+ });};
471
+ }
472
+ var options = {method: method, headers: {\"Content-Type\": \"application/json\"}};
473
+ if (payload) options.body = JSON.stringify(payload);
474
+ return fetch(location.href, options).then(success);
475
+ }
430
476
  if (headerTop) {
431
477
  setHeaderSizes();
432
478
  window.addEventListener('resize', function(event) {
@@ -461,8 +507,8 @@ if (headerTop) {
461
507
  });
462
508
  btnImport.addEventListener(\"click\", function () {
463
509
  fetch(changeout(<%= #{path_obj_name}_path(-1, format: :csv).inspect.html_safe %>, \"_brick_schema\", brickSchema), {
464
- method: 'PATCH',
465
- headers: { 'Content-Type': 'text/tab-separated-values' },
510
+ method: \"PATCH\",
511
+ headers: { \"Content-Type\": \"text/tab-separated-values\" },
466
512
  body: droppedTSV
467
513
  }).then(function (tsvResponse) {
468
514
  btnImport.style.display = \"none\";
@@ -540,6 +586,7 @@ if (headerTop) {
540
586
  if (description = (relation = Brick.relations[#{model_name}.table_name])&.fetch(:description, nil)) %><%=
541
587
  description %><br><%
542
588
  end
589
+ # FILTER PARAMETERS
543
590
  if @_brick_params&.present? %>
544
591
  <% if @_brick_params.length == 1 # %%% Does not yet work with composite keys
545
592
  k, id = @_brick_params.first
@@ -551,55 +598,78 @@ if (headerTop) {
551
598
  end
552
599
  end %>
553
600
  (<%= link_to 'See all #{model_plural.split('::').last}', #{path_obj_name.pluralize}_path %>)
601
+ <% end
602
+ # COLUMN EXCLUSIONS
603
+ if @_brick_excl&.present? %>
604
+ <div id=\"exclusions\">Excluded columns:
605
+ <% @_brick_excl.each do |excl| %>
606
+ <div class=\"colExclusion\"><%= excl %></div>
607
+ <% end %>
608
+ </div>
609
+ <script>
610
+ [... document.getElementsByClassName(\"colExclusion\")].forEach(function (excl) {
611
+ excl.addEventListener(\"click\", function () {
612
+ doFetch(\"POST\", {_brick_unexclude: this.innerHTML});
613
+ });
614
+ });
615
+ </script>
554
616
  <% end %>
555
- <br>
556
617
  <table id=\"headerTop\">
557
618
  <table id=\"#{table_name}\">
558
- <thead><tr>#{"<th x-order=\"#{pk.join(',')}\"></th>" if pk.present?}<%
559
- col_order = []
560
- @#{table_name}.columns.each do |col|
561
- next if (#{(pk || []).inspect}.include?(col_name = col.name) && col.type == :integer && !bts.key?(col_name)) ||
619
+ <thead><tr>#{"<th x-order=\"#{pk.join(',')}\"></th>" if pk.present?}<%=
620
+ # Consider getting the name from the association -- hm.first.name -- if a more \"friendly\" alias should be used for a screwy table name
621
+ cols = {#{hms_keys = []
622
+ hms_headers.map do |hm|
623
+ hms_keys << (assoc_name = (assoc = hm.first).name.to_s)
624
+ "#{assoc_name.inspect} => [#{(assoc.options[:through] && !assoc.through_reflection).inspect}, #{assoc.klass.name}, #{hm[1].inspect}, #{hm[2].inspect}]"
625
+ end.join(', ')}}
626
+ col_keys = @#{table_name}.columns.each_with_object([]) do |col, s|
627
+ col_name = col.name
628
+ next if @_brick_incl&.exclude?(col_name) ||
629
+ (#{(pk || []).inspect}.include?(col_name) && col.type == :integer && !bts.key?(col_name)) ||
562
630
  ::Brick.config.metadata_columns.include?(col_name) || poly_cols.include?(col_name)
563
631
 
564
- col_order << col_name
565
- %><th<%= \" title=\\\"#\{col.comment}\\\"\".html_safe if col.respond_to?(:comment) && !col.comment.blank? %><%
566
- if (bt = bts[col_name]) %>
567
- <%= \" x-order=\\\"#\{bt.first}\\\"\".html_safe unless bt[2] # Allow sorting any BT except polymorphics
568
- %>>BT <%
569
- bt[1].each do |bt_pair| %><%=
570
- bt_pair.first.bt_link(bt.first) %> <%
571
- end %><%
572
- else %><%= \" x-order=\\\"#\{col_name}\\\"\".html_safe if true # Currently we always allow click to sort
573
- %>><%= col_name %><%
574
- end
575
- %></th><%
632
+ s << col_name
633
+ cols[col_name] = col
576
634
  end
577
- # Consider getting the name from the association -- h.first.name -- if a more \"friendly\" alias should be used for a screwy table name
578
- %>#{hms_headers.map do |h|
579
- # Currently we always allow click to sort
580
- "<th#{" x-order=\"#{h.first.name}\"" if true}>" +
581
- if h.first.options[:through] && !h.first.through_reflection
582
- "#{h[1]} #{h[2]} %></th>" # %%% Would be able to remove this when multiple foreign keys to same destination becomes bulletproof
583
- else
584
- "#{h[1]} <%= link_to('#{h[2]}', #{h.first.klass.name.underscore.tr('/', '_').pluralize}_path) %></th>"
585
- end
586
- end.join
587
- }</tr></thead>
588
-
635
+ unless @_brick_sequence # If no sequence is defined, start with all inclusions
636
+ @_brick_sequence = col_keys + #{(hms_keys).inspect}.reject { |assoc_name| @_brick_incl&.exclude?(assoc_name) }
637
+ end
638
+ @_brick_sequence.reject! { |nm| @_brick_excl.include?(nm) } if @_brick_excl # Reject exclusions
639
+ @_brick_sequence.each_with_object(+'') do |col_name, s|
640
+ if (col = cols[col_name]).is_a?(ActiveRecord::ConnectionAdapters::Column)
641
+ s << '<th'
642
+ s << \" title=\\\"#\{col.comment}\\\"\" if col.respond_to?(:comment) && !col.comment.blank?
643
+ s << if (bt = bts[col_name])
644
+ # Allow sorting for any BT except polymorphics
645
+ \"#\{' x-order=\"' + bt.first.to_s + '\"' unless bt[2]}>BT \" +
646
+ bt[1].map { |bt_pair| bt_pair.first.bt_link(bt.first) }.join(' ')
647
+ else # Normal column
648
+ \"#\{' x-order=\"' + col_name + '\"' if true}>#\{col_name}\"
649
+ end
650
+ elsif col # HM column
651
+ s << \"<th#\{' x-order=\"' + col_name + '\"' if true}>#\{col[2]} \"
652
+ s << (col.first ? \"#\{col[3]}\" : \"#\{link_to(col[3], send(\"#\{col[1].name.underscore.tr('/', '_').pluralize}_path\"))}\")
653
+ else # Bad column name!
654
+ s << \"<th title=\\\"<< Unknown column >>\\\">#\{col_name}\"
655
+ end
656
+ s << '</th>'
657
+ end.html_safe
658
+ %></tr></thead>
589
659
  <tbody>
590
- <% @#{table_name}.each do |#{obj_name}| %>
660
+ <% @#{table_name}.each do |#{obj_name}|
661
+ hms_cols = {#{hms_columns.join(', ')}} %>
591
662
  <tr>#{"
592
663
  <td><%= link_to '⇛', #{path_obj_name}_path(#{obj_pk}), { class: 'big-arrow' } %></td>" if obj_pk}
593
- <% col_order.each do |col_name|
664
+ <% @_brick_sequence.each do |col_name|
594
665
  val = #{obj_name}.attributes[col_name] %>
595
- <td>
596
- <% if (bt = bts[col_name]) %>
597
- <% if bt[2] # Polymorphic?
666
+ <td<%= ' class=\"dimmed\"'.html_safe unless cols.key?(col_name)%>><%
667
+ if (bt = bts[col_name])
668
+ if bt[2] # Polymorphic?
598
669
  bt_class = #{obj_name}.send(\"#\{bt.first\}_type\")
599
670
  base_class = (::Brick.existing_stis[bt_class] || bt_class).constantize.base_class.name.underscore
600
671
  poly_id = #{obj_name}.send(\"#\{bt.first\}_id\")
601
- %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\",
602
- send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
672
+ %><%= link_to(\"#\{bt_class\} ##\{poly_id\}\", send(\"#\{base_class\}_path\".to_sym, poly_id)) if poly_id %><%
603
673
  else
604
674
  bt_txt = (bt_class = bt[1].first.first).brick_descrip(
605
675
  # 0..62 because Postgres column names are limited to 63 characters
@@ -608,14 +678,29 @@ if (headerTop) {
608
678
  bt_txt ||= \"<span class=\\\"orphan\\\">&lt;&lt; Orphaned ID: #\{val} >></span>\".html_safe if val
609
679
  bt_id = bt_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) } %>
610
680
  <%= bt_id&.first ? link_to(bt_txt, send(\"#\{bt_class.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, bt_id)) : bt_txt %>
611
- <%#= Previously was: bt_obj = bt[1].first.first.find_by(bt[2] => val); link_to(bt_obj.brick_descrip, send(\"#\{bt[1].first.first.name.underscore\}_path\".to_sym, bt_obj.send(bt[1].first.first.primary_key.to_sym))) if bt_obj %>
612
- <% end %>
613
- <% else %>
614
- <%= hide_bcrypt(val) %>
615
- <% end %>
616
- </td>
681
+ <% end
682
+ elsif (hms_col = hms_cols[col_name])
683
+ if hms_col.length == 1 %>
684
+ <%= hms_col.first %>
685
+ <% else
686
+ klass = (col = cols[col_name])[1]
687
+ txt = if col[2] == 'HO'
688
+ descrips = @_brick_bt_descrip[col_name.to_sym][klass]
689
+ ho_txt = klass.brick_descrip(#{obj_name}, descrips[0..-2].map { |id| #{obj_name}.send(id.last[0..62]) }, (ho_id_col = descrips.last))
690
+ ho_id = ho_id_col.map { |id_col| #{obj_name}.send(id_col.to_sym) }
691
+ ho_id&.first ? link_to(ho_txt, send(\"#\{klass.base_class.name.underscore.tr('/', '_')\}_path\".to_sym, ho_id)) : ho_txt
692
+ else
693
+ \"#\{hms_col[1] || 'View'\} #\{hms_col.first}\"
694
+ end %>
695
+ <%= link_to txt, send(\"#\{klass.name.underscore.tr('/', '_').pluralize}_path\".to_sym, hms_col[2]) unless hms_col[1]&.zero? %>
696
+ <% end
697
+ elsif cols.key?(col_name)
698
+ %><%= hide_bcrypt(val) %><%
699
+ else # Bad column name!
700
+ %>?<%
701
+ end
702
+ %></td>
617
703
  <% end %>
618
- #{hms_columns.each_with_object(+'') { |hm_col, s| s << "<td>#{hm_col}</td>" }}
619
704
  </tr>
620
705
  <% end %>
621
706
  </tbody>
@@ -5,7 +5,7 @@ module Brick
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 51
8
+ TINY = 54
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
@@ -190,6 +190,15 @@ module Brick
190
190
  [bts, hms]
191
191
  end
192
192
 
193
+ def exclude_column(table, col)
194
+ puts "Excluding #{table}.#{col}"
195
+ true
196
+ end
197
+ def unexclude_column(table, col)
198
+ puts "Unexcluding #{table}.#{col}"
199
+ true
200
+ end
201
+
193
202
  # Switches Brick auto-models on or off, for all threads
194
203
  # @api public
195
204
  def enable_models=(value)
@@ -7,13 +7,13 @@ module Brick
7
7
  class InstallGenerator < ::Rails::Generators::Base
8
8
  # include ::Rails::Generators::Migration
9
9
 
10
- source_root File.expand_path('templates', __dir__)
11
- class_option(
12
- :with_changes,
13
- type: :boolean,
14
- default: false,
15
- desc: 'Store changeset (diff) with each version'
16
- )
10
+ # source_root File.expand_path('templates', __dir__)
11
+ # class_option(
12
+ # :with_changes,
13
+ # type: :boolean,
14
+ # default: false,
15
+ # desc: 'Store changeset (diff) with each version'
16
+ # )
17
17
 
18
18
  desc 'Generates an initializer file for configuring Brick'
19
19
 
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require 'fancy_gets'
6
+
7
+ module Brick
8
+ # Auto-generates migrations
9
+ class MigrationsGenerator < ::Rails::Generators::Base
10
+ include FancyGets
11
+ # include ::Rails::Generators::Migration
12
+
13
+ # SQL types that are the same as their migration data type name: text, integer, bigint, smallint, date, boolean, decimal, float
14
+ SQL_TYPES = { 'character varying' =>'string',
15
+ 'timestamp without time zone' =>'timestamp',
16
+ 'timestamp with time zone' =>'timestamp',
17
+ 'double precision' =>'float' } # might work with 'double'
18
+ # (Still need to find what "inet" and "json" data types map to.)
19
+
20
+ # # source_root File.expand_path('templates', __dir__)
21
+ # class_option(
22
+ # :with_changes,
23
+ # type: :boolean,
24
+ # default: false,
25
+ # desc: 'Add IMPORT_TEMPLATE to model'
26
+ # )
27
+
28
+ desc 'Auto-generates migrations for an existing database.'
29
+
30
+ def brick_migrations
31
+ # If Apartment is active, see if a default schema to analyse is indicated
32
+
33
+ # # Load all models
34
+ # Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
35
+
36
+ if (tables = ::Brick.relations.reject { |k, v| v.key?(:isView) && v[:isView] == true }.map(&:first).sort).empty?
37
+ puts "No tables found in database #{ActiveRecord::Base.connection.current_database}."
38
+ return
39
+ end
40
+
41
+ ar_version = unless ActiveRecord.version < ::Gem::Version.new('5.0')
42
+ arv = ActiveRecord.version.segments[0..1]
43
+ arv.pop if arv&.last == 0
44
+ "[#{arv.join('.')}]"
45
+ end
46
+ default_mig_path = (mig_path = ActiveRecord::Migrator.migrations_paths.first || "#{File.expand_path(__dir__)}/db/migrate")
47
+ if Dir.exist?(mig_path)
48
+ if Dir["#{mig_path}/**/*.rb"].present?
49
+ puts "WARNING: migrations folder #{mig_path} appears to already have ruby files present."
50
+ mig_path2 = "#{File.expand_path(__dir__)}/tmp/brick_migrations"
51
+ if Dir.exist?(mig_path2)
52
+ if Dir["#{mig_path2}/**/*.rb"].present?
53
+ puts "As well, temporary folder #{mig_path2} also has ruby files present."
54
+ puts "Choose a destination -- all existing .rb files will be removed:"
55
+ mig_path2 = gets_list(list: ['Cancel operation!', "Add all migration files to #{mig_path} anyway", mig_path, mig_path2])
56
+ return if mig_path2.start_with?('Cancel')
57
+
58
+ if mig_path2.start_with?('Add all migration files to ')
59
+ mig_path2 = mig_path
60
+ else
61
+ Dir["#{mig_path2}/**/*.rb"].each { |rb| File.delete(rb) }
62
+ end
63
+ else
64
+ puts "Using temporary folder #{mig_path2} for created migration files.\n\n"
65
+ end
66
+ else
67
+ puts "Creating the temporary folder #{mig_path2} for created migration files.\n\n"
68
+ Dir.mkdir(mig_path2)
69
+ end
70
+ mig_path = mig_path2
71
+ else
72
+ puts "Using standard migration folder #{mig_path} for created migration files.\n\n"
73
+ end
74
+ else
75
+ puts "Creating standard ActiveRecord migration folder #{mig_path} to hold new migration files.\n\n"
76
+ Dir.mkdir(mig_path)
77
+ end
78
+
79
+ # Generate a list of tables that can be chosen
80
+ chosen = gets_list(list: tables, chosen: tables.dup)
81
+ # Work from now back the same number of minutes as expected migrations to create
82
+ current_mig_time = Time.now - chosen.length.minutes
83
+ done = []
84
+ fks = {}
85
+ stuck = {}
86
+ # Start by making migrations for fringe tables (those with no foreign keys).
87
+ # Continue layer by layer, creating migrations for tables that reference ones already done, until
88
+ # no more migrations can be created. (At that point hopefully all tables are accounted for.)
89
+ while (fringe = chosen.reject do |tbl|
90
+ snags = ::Brick.relations[tbl][:fks].select do |_k, v|
91
+ v[:is_bt] && !v[:polymorphic] &&
92
+ tbl != v[:inverse_table] && # Ignore self-referencing associations (stuff like "parent_id")
93
+ !done.include?(v[:inverse_table])
94
+ end
95
+ stuck[tbl] = snags if snags.present?
96
+ end).present?
97
+ fringe.each do |tbl|
98
+ tbl = tbl[7..-1] if tbl.start_with?('public.') # %%% Provide better multitenancy support later
99
+ pkey_cols = ::Brick.relations[chosen.first][:pkey].values.flatten
100
+ # %%% For the moment we're skipping polymorphics
101
+ fkey_cols = ::Brick.relations[tbl][:fks].values.select { |assoc| assoc[:is_bt] && !assoc[:polymorphic] }
102
+ mig = +"class Create#{table_name = tbl.camelize} < ActiveRecord::Migration#{ar_version}\n"
103
+ # %%% Support missing foreign key (by adding: ,id: false)
104
+ # also bigint / uuid / other non-standard data type for primary key
105
+ mig << " def change\n create_table :#{tbl} do |t|\n"
106
+ possible_ts = [] # Track possible generic timestamps
107
+ (relation = ::Brick.relations[tbl])[:cols].each do |col, col_type|
108
+ next if pkey_cols.include?(col)
109
+
110
+ # See if there are generic timestamps
111
+ if (sql_type = SQL_TYPES[col_type.first]) == 'timestamp' && ['created_at','updated_at'].include?(col)
112
+ possible_ts << col
113
+ next
114
+ end
115
+
116
+ sql_type ||= col_type.first
117
+ # Determine if this column is used as part of a foreign key
118
+ if fk = fkey_cols.find { |assoc| col == assoc[:fk] }
119
+ mig << " t.references :#{fk[:assoc_name]}#{", type: #{sql_type}" unless sql_type == 'integer'}\n"
120
+ else
121
+ mig << emit_column(sql_type, col)
122
+ end
123
+ end
124
+ if possible_ts.length == 2 # Both created_at and updated_at
125
+ mig << "\n t.timestamps\n"
126
+ else # Just one or the other
127
+ possible_ts.each { |ts| emit_column('timestamp', ts) }
128
+ end
129
+ mig << " end\n end\nend\n"
130
+ current_mig_time += 1.minute
131
+ File.open("#{mig_path}/#{current_mig_time.strftime('%Y%m%d%H%M00')}_create_#{tbl}.rb", "w") { |f| f.write mig }
132
+ end
133
+ done.concat(fringe)
134
+ chosen -= done
135
+ end
136
+ stuck_counts = Hash.new { |h, k| h[k] = 0 }
137
+ chosen.each do |leftover|
138
+ puts "Can't do #{leftover} because:\n #{stuck[leftover].map do |snag|
139
+ stuck_counts[snag.last[:inverse_table]] += 1
140
+ snag.last[:assoc_name]
141
+ end.join(', ')}"
142
+ end
143
+ pretty_mig_path = mig_path[cur_path.length] if mig_path.start_with?(cur_path = File.expand_path(__dir__))
144
+ puts "*** Created #{done.length} migration files under #{pretty_mig_path || mig_path} ***"
145
+ if (stuck_sorted = stuck_counts.to_a.sort { |a, b| b.last <=> a.last }).length.positive?
146
+ puts "-----------------------------------------"
147
+ puts "Unable to create migrations for #{stuck_sorted.length} tables#{
148
+ ". Here's the top 5 blockers" if stuck_sorted.length > 5
149
+ }:"
150
+ pp stuck_sorted[0..4]
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def emit_column(type, name)
157
+ " t.#{type.start_with?('numeric') ? 'decimal' : type} :#{name}\n"
158
+ end
159
+ end
160
+ end
@@ -59,7 +59,6 @@ module Brick
59
59
  end
60
60
  end
61
61
  models.each do |m| # Find longest name in the list for future use to show lists on the right side of the screen
62
- # Strangely this can't be inlined since it assigns to "len"
63
62
  if longest_length < (len = m.name.length)
64
63
  longest_length = len
65
64
  end
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.51
4
+ version: 1.0.54
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-08-02 00:00:00.000000000 Z
11
+ date: 2022-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -238,6 +238,7 @@ files:
238
238
  - lib/brick/version_number.rb
239
239
  - lib/generators/brick/USAGE
240
240
  - lib/generators/brick/install_generator.rb
241
+ - lib/generators/brick/migrations_generator.rb
241
242
  - lib/generators/brick/model_generator.rb
242
243
  - lib/generators/brick/templates/add_object_changes_to_versions.rb.erb
243
244
  - lib/generators/brick/templates/create_versions.rb.erb