marty 4.0.0.rc2 → 5.1.0

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.
Files changed (72) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +1 -0
  4. data/.rubocop_todo.yml +3 -16
  5. data/.ssh-docker/.keep +0 -0
  6. data/Dockerfile.dummy +3 -0
  7. data/Gemfile +19 -15
  8. data/app/components/marty/base_rule_view.rb +104 -10
  9. data/app/components/marty/base_rule_view/client/base_rule_view.js +24 -0
  10. data/app/components/marty/data_grid_user_view.rb +39 -0
  11. data/app/components/marty/data_grid_view.rb +68 -18
  12. data/app/components/marty/extras/layout.rb +1 -1
  13. data/app/components/marty/grid.rb +1 -1
  14. data/app/components/marty/grid/client/grid.js +29 -13
  15. data/app/components/marty/import_view.rb +3 -3
  16. data/app/components/marty/main_auth_app.rb +11 -1
  17. data/app/components/marty/report_form.rb +6 -6
  18. data/app/components/marty/script_form.rb +5 -5
  19. data/app/components/marty/script_tester.rb +2 -2
  20. data/app/components/marty/user_view.rb +3 -9
  21. data/app/models/marty/base_rule.rb +92 -32
  22. data/app/models/marty/data_grid.rb +92 -22
  23. data/app/models/marty/event.rb +2 -2
  24. data/app/models/marty/promise.rb +4 -4
  25. data/app/models/marty/role_type.rb +14 -1
  26. data/app/services/marty/data_grid_view/save_grid.rb +2 -2
  27. data/app/services/marty/promises/delorean/create.rb +2 -2
  28. data/app/services/marty/promises/ruby/create.rb +2 -2
  29. data/config/locales/en.yml +11 -2
  30. data/db/migrate/108_add_data_grid_perms.rb +16 -0
  31. data/db/migrate/508_add_not_to_data_grids_tables.rb +18 -0
  32. data/db/migrate/509_update_dg_plpgsql_v1_fns.rb +13 -0
  33. data/db/sql/query_grid_dir_v1.sql +16 -2
  34. data/docker-compose.dummy.yml +1 -0
  35. data/lib/marty/content_handler.rb +2 -2
  36. data/lib/marty/data_change.rb +1 -1
  37. data/lib/marty/data_conversion.rb +3 -4
  38. data/lib/marty/data_importer.rb +4 -4
  39. data/lib/marty/mcfly_model.rb +7 -10
  40. data/lib/marty/migrations.rb +1 -1
  41. data/lib/marty/monkey.rb +2 -2
  42. data/lib/marty/promise_job.rb +5 -5
  43. data/lib/marty/promise_proxy.rb +2 -2
  44. data/lib/marty/promise_ruby_job.rb +4 -4
  45. data/lib/marty/version.rb +1 -1
  46. data/make-app.mk +1 -1
  47. data/marty.gemspec +13 -18
  48. data/other/marty/diagnostic/aws/ec2_instance.rb +17 -2
  49. data/other/marty/diagnostic/aws/error.rb +8 -0
  50. data/other/marty/diagnostic/database.rb +2 -2
  51. data/other/marty/diagnostic/delayed_job_version.rb +0 -1
  52. data/spec/dummy/app/components/gemini/my_rule_view.rb +32 -6
  53. data/spec/dummy/app/models/gemini/fannie_bup.rb +13 -20
  54. data/spec/dummy/app/models/gemini/my_rule.rb +4 -0
  55. data/spec/dummy/app/models/gemini/xyz_rule.rb +3 -1
  56. data/spec/dummy/config/application.rb +1 -0
  57. data/spec/dummy/config/initializers/secret_token.rb +1 -1
  58. data/spec/dummy/db/migrate/20190702115241_add_simple_guards_options_to_rules.rb +37 -0
  59. data/spec/features/data_grid_spec.rb +109 -47
  60. data/spec/features/reporting_spec.rb +4 -4
  61. data/spec/features/rule_spec.rb +62 -31
  62. data/spec/features/scripting_spec.rb +3 -3
  63. data/spec/features/user_view_spec.rb +17 -8
  64. data/spec/fixtures/csv/rule/MyRule.csv +4 -1
  65. data/spec/lib/data_importer_spec.rb +8 -8
  66. data/spec/lib/mcfly_model_spec.rb +6 -6
  67. data/spec/models/data_grid_spec.rb +139 -7
  68. data/spec/models/rule_spec.rb +116 -9
  69. data/spec/spec_helper.rb +2 -2
  70. data/spec/support/netzke.rb +4 -3
  71. metadata +55 -54
  72. data/Gemfile.lock +0 -289
@@ -206,8 +206,8 @@ DELOREAN
206
206
  end
207
207
 
208
208
  wait_for_element do
209
- expect(page).to have_content('XYZ1,XYZ2,XYZ3,XYZ4 1,2,3,4 2,4,6,8 ' +
210
- '3,6,9,12 4,8,12,16')
209
+ expect(page).to have_content("XYZ1,XYZ2,XYZ3,XYZ4\n1,2,3,4\n2,4,6,8\n" +
210
+ "3,6,9,12\n4,8,12,16")
211
211
  end
212
212
  end
213
213
 
@@ -263,8 +263,8 @@ DELOREAN
263
263
  url = generate_rep_url(format: 'txt') + URI.encode('&disposition=inline')
264
264
  visit url
265
265
  wait_for_element do
266
- expect(page).to have_content('XYZ1,XYZ2,XYZ3,XYZ4 1,2,3,4 2,4,6,8 ' +
267
- '3,6,9,12 4,8,12,16')
266
+ expect(page).to have_content("XYZ1,XYZ2,XYZ3,XYZ4\n1,2,3,4\n2,4,6,8\n" +
267
+ "3,6,9,12\n4,8,12,16")
268
268
  end
269
269
  end
270
270
 
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'marty_rspec'
3
2
 
4
3
  feature 'rule view', js: true do
5
4
  before(:all) do
@@ -39,7 +38,6 @@ feature 'rule view', js: true do
39
38
  JS
40
39
  end
41
40
 
42
- # click_checkbox in marty_rspec not working here for some reason
43
41
  def click_checkbox(name)
44
42
  q = %Q(checkbox[fieldLabel="#{name}"])
45
43
  page.execute_script <<-JS
@@ -48,7 +46,6 @@ feature 'rule view', js: true do
48
46
  JS
49
47
  end
50
48
 
51
- # click_col in marty_rspec is not reliable
52
49
  def click_column(rv, name)
53
50
  cid = col_id(rv, name)
54
51
  c = find('#' + cid)
@@ -115,7 +112,7 @@ feature 'rule view', js: true do
115
112
  time_fill_in(1, '08:03:01')
116
113
  press('OK')
117
114
  wait_for_ajax
118
- expect(mrv.row_count()).to eq(9)
115
+ expect(mrv.row_count()).to eq(12)
119
116
  expect(mrv.get_row_vals(1)).to include('name' => 'abc',
120
117
  'rule_type' => 'SimpleRule',
121
118
  'start_dt' => '2013-01-01T11:03:01',
@@ -339,7 +336,7 @@ computed_value = if paramb
339
336
  else (grid2_grid_result||1) / param1
340
337
  EOL
341
338
 
342
- names = mrv.get_col_vals(:name, 9, 0)
339
+ names = mrv.get_col_vals(:name, 12, 0)
343
340
  idx = names.index { |n| n == 'Rule3' } + 1
344
341
  mrv.select_row(idx)
345
342
  press('Edit')
@@ -362,35 +359,68 @@ EOL
362
359
  'ZRule3', 'ZRule4',
363
360
  'ZRule5'])
364
361
  xrv.select_row(1)
362
+
363
+ expect(page).to_not have_content('NOT (G2V1)')
364
+
365
365
  press('Edit')
366
- fill_in('Range Guard 1', with: '[100,200)')
367
- fill_in('Range Guard 2', with: '[30,40)')
366
+
367
+ fill_in('Guard two', with: 'G2V1')
368
+ click_checkbox('Not')
369
+
370
+ fill_in('Range Guard 1', with: '[100,200)', fill_options: { clear: :backspace })
371
+ fill_in('Range Guard 2', with: '[30,40)', fill_options: { clear: :backspace })
368
372
  press('OK')
373
+
369
374
  wait_for_ajax
370
- r = Gemini::XyzRule.get_matches('infinity', {}, 'g_range1' => 150,
371
- 'g_range2' => 35)
375
+
376
+ r = Gemini::XyzRule.get_matches(
377
+ 'infinity',
378
+ {},
379
+ 'g_range1' => 150,
380
+ 'g_range2' => 35,
381
+ 'guard_two' => 'G2V2'
382
+ )
372
383
 
373
384
  expect(r.to_a.count).to eq(1)
374
- exp = { 'user_id' => 1,
375
- 'o_user_id' => nil,
376
- 'name' => 'ZRule1',
377
- 'engine' => 'Gemini::XyzRuleScriptSet',
378
- 'rule_type' => 'ZRule',
379
- 'start_dt' => DateTime.parse('2017-1-1 08:01:00'),
380
- 'simple_guards' => { 'g_bool' => false,
381
- 'g_date' => '2017-1-1',
382
- 'g_range1' => '[100,200)',
383
- 'g_range2' => '[30,40)',
384
- 'g_string' => 'aaa',
385
- 'g_integer' => '5',
386
- 'g_datetime' => '2017-1-1 12:00:01' },
387
- 'computed_guards' => {},
388
- 'grids' => { 'grid1' => 'DataGrid1' },
389
- 'results' =>
390
- { 'bvlen' => 'base_value.length',
391
- 'bv' => 'base_value' } }
385
+ exp = {
386
+ 'user_id' => 1,
387
+ 'o_user_id' => nil,
388
+ 'name' => 'ZRule1',
389
+ 'engine' => 'Gemini::XyzRuleScriptSet',
390
+ 'rule_type' => 'ZRule',
391
+ 'start_dt' => DateTime.parse('2017-1-1 08:01:00'),
392
+ 'simple_guards' => {
393
+ 'g_bool' => false,
394
+ 'g_date' => '2017-1-1',
395
+ 'guard_two' => 'G2V1',
396
+ 'g_range1' => '[100,200)',
397
+ 'g_range2' => '[30,40)',
398
+ 'g_string' => 'aaa',
399
+ 'g_integer' => '5',
400
+ 'g_datetime' => '2017-1-1 12:00:01'
401
+ },
402
+ 'simple_guards_options' => {
403
+ 'g_bool' => { 'not' => false },
404
+ 'g_date' => { 'not' => false },
405
+ 'g_datetime' => { 'not' => false },
406
+ 'g_integer' => { 'not' => false },
407
+ 'g_range1' => { 'not' => false },
408
+ 'g_range2' => { 'not' => false },
409
+ 'g_string' => { 'not' => false },
410
+ 'guard_two' => { 'not' => true }
411
+ },
412
+ 'computed_guards' => {},
413
+ 'grids' => { 'grid1' => 'DataGrid1' },
414
+ 'results' => {
415
+ 'bvlen' => 'base_value.length',
416
+ 'bv' => 'base_value'
417
+ }
418
+ }
392
419
 
393
420
  expect(r.first.as_json).to include(exp)
421
+
422
+ expect(page).to have_content('NOT (G2V1)')
423
+
394
424
  expect(xrv.get_col_vals(:g_string, 8, 0)).to eq(['aaa', 'bbb', 'ccc', 'ddd',
395
425
  'eee', 'eee', 'eee', 'eee'])
396
426
  click_column(xrv, 'String list Guard')
@@ -418,8 +448,9 @@ EOL
418
448
  %Q({"grid1":"DataGrid2"}),
419
449
  %Q({"grid1":"DataGrid1"}),
420
450
  %Q({"grid1":"DataGrid1"})])
451
+
421
452
  press('Applications')
422
- press('Data Grids')
453
+ press('Data Grids Admin')
423
454
  dgv = netzke_find('data_grid_view')
424
455
  cvs = dgv.get_col_vals(:name, 4, 0)
425
456
  ind1 = cvs.index('DataGrid1') + 1
@@ -447,9 +478,9 @@ EOL
447
478
  go_to_my_rules
448
479
  wait_for_ajax
449
480
 
450
- names = mrv.get_col_vals(:name, 9, 0)
451
- gvs = mrv.get_col_vals(:grids, 9, 0)
452
- rvs = mrv.get_col_vals(:results, 9, 0)
481
+ names = mrv.get_col_vals(:name, 12, 0)
482
+ gvs = mrv.get_col_vals(:grids, 12, 0)
483
+ rvs = mrv.get_col_vals(:results, 12, 0)
453
484
  expect(JSON.parse(gvs[names.index('abc')])).to eq(g1h)
454
485
  expect(JSON.parse(gvs[names.index('Rule2b')])).to eq(g1h +
455
486
  { 'grid2' => 'DataGrid2' })
@@ -221,7 +221,7 @@ feature 'under Applications menu, Scripting workflows', js: true do
221
221
 
222
222
  and_by 'form displays correct body' do
223
223
  wait_for_ajax
224
- expect(page).to have_content '1 #5 2 #1 3 #2 4 #3'
224
+ expect(page).to have_content "1\n#5\n2\n#1\n3\n#2\n4\n#3"
225
225
  end
226
226
 
227
227
  and_by 'select different tag' do
@@ -235,7 +235,7 @@ feature 'under Applications menu, Scripting workflows', js: true do
235
235
 
236
236
  and_by 'form displays updated body' do
237
237
  wait_for_ajax
238
- expect(page).to have_content '1 #5 2 #1'
238
+ expect(page).to have_content "1\n#5\n2\n#1"
239
239
  end
240
240
  end
241
241
 
@@ -288,7 +288,7 @@ feature 'under Applications menu, Scripting workflows', js: true do
288
288
  tag_grid.select_row(2)
289
289
  wait_for_ajax
290
290
  script_grid.select_row(5)
291
- expect(page).to have_content '1 #123 2 #456'
291
+ expect(page).to have_content "1\n#123\n2\n#456"
292
292
  expect(tag_grid.get_row_vals(2)).to netzke_include(comment: 'ABCD')
293
293
  end
294
294
  end
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- feature 'under Sytem menu, User Management worflows', js: true do
3
+ feature 'under System menu, User Management worflows', js: true do
4
4
  def go_to_user_view
5
5
  press('System')
6
6
  press('User Management')
@@ -8,7 +8,7 @@ feature 'under Sytem menu, User Management worflows', js: true do
8
8
  end
9
9
 
10
10
  def go_to_user_view_backdoor
11
- sys_btn = first(:btn, 'System')
11
+ sys_btn = first(:btn, 'System') rescue nil
12
12
  if sys_btn
13
13
  sys_btn.click
14
14
  expect(page).not_to have_content 'User Management'
@@ -77,6 +77,7 @@ feature 'under Sytem menu, User Management worflows', js: true do
77
77
 
78
78
  and_by 'check row got edited' do
79
79
  wait_for_ajax
80
+ r2 = user_view.get_row_vals(2)
80
81
  expect(user_view.get_row_vals(2)).to netzke_include(
81
82
  login: 'new_login',
82
83
  firstname: 'new_fname',
@@ -114,6 +115,9 @@ feature 'under Sytem menu, User Management worflows', js: true do
114
115
  by 'check buttons' do
115
116
  find(:btn, 'New User', match: :first)
116
117
  user_view.select_row(1)
118
+ expect { find(:btn, 'New User') }.not_to raise_error
119
+ expect { find(:btn, 'Edit') }.not_to raise_error
120
+ expect { find(:btn, 'Delete') }.not_to raise_error
117
121
  expect(btn_disabled?('New User')).to be_falsy
118
122
  expect(btn_disabled?('Edit')).to be_falsy
119
123
  expect(btn_disabled?('Delete')).to be_falsy
@@ -127,6 +131,9 @@ feature 'under Sytem menu, User Management worflows', js: true do
127
131
  by 'check buttons' do
128
132
  find(:btn, 'New User', match: :first)
129
133
  user_view.select_row(1)
134
+ expect { find(:btn, 'New User') }.not_to raise_error
135
+ expect { find(:btn, 'Edit') }.not_to raise_error
136
+ expect { find(:btn, 'Delete') }.not_to raise_error
130
137
  expect(btn_disabled?('New User')).to be_falsy
131
138
  expect(btn_disabled?('Edit')).to be_falsy
132
139
  expect(btn_disabled?('Delete')).to be_falsy
@@ -140,9 +147,10 @@ feature 'under Sytem menu, User Management worflows', js: true do
140
147
  user_view = netzke_find('user_view')
141
148
  by 'check buttons' do
142
149
  user_view.select_row(1)
143
- expect(page).not_to have_content('New User')
144
- expect(page).not_to have_content('Edit')
145
- expect(page).not_to have_content('Delete')
150
+ err = /Unable to find btn/
151
+ ['New User', 'Edit', 'Delete'].each do |btn_name|
152
+ expect { find(:btn, btn_name) }.to raise_error(err)
153
+ end
146
154
  end
147
155
  end
148
156
 
@@ -153,9 +161,10 @@ feature 'under Sytem menu, User Management worflows', js: true do
153
161
  user_view = netzke_find('user_view')
154
162
  by 'check buttons' do
155
163
  user_view.select_row(1)
156
- expect(page).not_to have_content('New User')
157
- expect(page).not_to have_content('Edit')
158
- expect(page).not_to have_content('Delete')
164
+ err = /Unable to find btn/
165
+ ['New User', 'Edit', 'Delete'].each do |btn_name|
166
+ expect { find(:btn, btn_name) }.to raise_error(err)
167
+ end
159
168
  end
160
169
  end
161
170
  end
@@ -1,4 +1,4 @@
1
- name,rule_type,start_dt,end_dt,other_flag,simple_guards,computed_guards,grids,results
1
+ name,rule_type,start_dt,end_dt,other_flag,simple_guards,computed_guards,grids,results,simple_guards_options
2
2
  Rule1,SimpleRule,2017-1-1 12:00:00,2017-4-1,,"{""g_has_default"":""different"",""g_array"":[""G1V1"",""G1V3""],""g_single"":""G2V2"",""g_string"":""Hi Mom"",""g_bool"":true,""g_bool_def"":false,""g_nbool_def"":true,""g_range"":""[50,)"",""g_integer"":10}",,,"{""simple_result"":""\""a value\""""}"
3
3
  Rule2,SimpleRule,2017-2-1 14:00:00,2017-4-1,true,"{""g_array"":[""G1V2""],""g_single"":""G2V3"",""g_string"":""abc"",""g_bool"":true,""g_range"":""(,50]"",""g_integer"":11}",,,"{
4
4
  ""simple_result"":""\""b value\"" # with comment "",
@@ -13,3 +13,6 @@ Rule2c,SimpleRule,2017-2-1 14:00:00,2017-4-1,true,"{""g_array"":[""G1V2""],""g_s
13
13
  Rule3,ComplexRule,2017-3-1 00:00:00,2017-4-1,false,"{""g_array"":[""G1V2"",""G1V3""],""g_string"":""def"",""g_integer"":11}","{""cguard1"":""1==1"",""cguard2"":""[param2 == 'abc', \""a string\""]""}","{""grid1"":""DataGrid1"",""grid2"":""DataGrid2""}","{""simple_result"":""\""c value\"""",""computed_value"":""if paramb\n then param1 / (grid1_grid_result||1)\n else (grid2_grid_result||1) / param1""}"
14
14
  Rule4,ComplexRule,2017-4-1 15:00:01,2017-5-1,,"{""g_array"":[""G1V2"",""G1V3""],""g_string"":""Hi Mom"",""g_integer"":11}","{""cguard1"":""1==1"",""cguard2"":""param2 == \""abc\""""}","{""grid1"":""DataGrid1"",""grid2"":""DataGrid2""}","{""computed_name_grid"":""\""DataGrid\""+\""X\"""", ""simple_result"":""computed_name_grid"",""grid_sum"":""computed_name_grid_result + (grid1_grid_result||0)+(grid2_grid_result||0)""}"
15
15
  Rule5,ComplexRule,2017-4-2 15:00:01,2017-5-1,,"{""g_string"":""zzz"",""g_integer"":3757,""g_has_default"":""foo""}","{""cguard1"":""[1==1, \""a string\""]""}",,"{""flavor"": ""[\""cherry\"",\""lemon\""][param2]"",""other_grid"": ""\""DataGrid4\"""",""final_value"": ""other_grid_result * 3""}"
16
+ NotRule1,SimpleRule,2017-4-2 15:00:01,2017-5-1,,"{""g_string"":""zzz"",""g_range"":""(,20]"",""g_integer"":3757,""g_has_default"":""foo"",""g_bool"":false,""g_array"":[""G1V1""]}","{""cguard1"":""[1==1, \""a string\""]""}",,"{""flavor"": ""[\""cherry\"",\""lemon\""][param2]"",""other_grid"": ""\""DataGrid4\"""",""final_value"": ""other_grid_result * 3""}","{""g_string"":{""not"":true},""g_integer"":{""not"":false}}"
17
+ NotRule2,SimpleRule,2017-4-2 15:00:01,2017-5-1,,"{""g_string"":""zzz"",""g_range"":""(,20]"",""g_integer"":3757,""g_has_default"":""foo"",""g_bool"":false,""g_array"":[""G1V1""]}","{""cguard1"":""[1==1, \""a string\""]""}",,"{""flavor"": ""[\""cherry\"",\""lemon\""][param2]"",""other_grid"": ""\""DataGrid4\"""",""final_value"": ""other_grid_result * 3""}","{""g_string"":{""not"":true},""g_integer"":{""not"":true}}"
18
+ NotRule3,SimpleRule,2017-4-2 15:00:01,2017-5-1,,"{""g_string"":""zzz"",""g_range"":""(,200]"",""g_integer"":3757,""g_has_default"":""foo"",""g_bool"":false,""g_array"":[""G1V1""]}","{""cguard1"":""[1==1, \""a string\""]""}",,"{""flavor"": ""[\""cherry\"",\""lemon\""][param2]"",""other_grid"": ""\""DataGrid4\"""",""final_value"": ""other_grid_result * 3""}","{""g_string"":{""not"":true},""g_integer"":{""not"":true}, ""g_range"":{""not"":true}}"
@@ -349,8 +349,8 @@ describe DataImporter do
349
349
 
350
350
  begin
351
351
  Marty::DataImporter.do_import_summary(Gemini::FannieBup, fannie_bup4)
352
- rescue Marty::DataImporter::Error => exc
353
- exc.lines.should == [0]
352
+ rescue Marty::DataImporter::Error => e
353
+ e.lines.should == [0]
354
354
  else
355
355
  raise 'should have had an exception'
356
356
  end
@@ -390,9 +390,9 @@ describe DataImporter do
390
390
 
391
391
  begin
392
392
  Marty::DataImporter.do_import_summary(Gemini::FannieBup, fannie_bup5)
393
- rescue Marty::DataImporter::Error => exc
394
- exc.lines.should == [1]
395
- exc.message.should =~ /Conv Fixed XX/
393
+ rescue Marty::DataImporter::Error => e
394
+ e.lines.should == [1]
395
+ e.message.should =~ /Conv Fixed XX/
396
396
  else
397
397
  raise 'should have had an exception'
398
398
  end
@@ -409,9 +409,9 @@ describe DataImporter do
409
409
  begin
410
410
  res = Marty::DataImporter.
411
411
  do_import_summary(Gemini::FannieBup, fannie_bup6)
412
- rescue Marty::DataImporter::Error => exc
413
- exc.lines.should == [1]
414
- exc.message.should =~ /bad float/
412
+ rescue Marty::DataImporter::Error => e
413
+ e.lines.should == [1]
414
+ e.message.should =~ /bad float/
415
415
  else
416
416
  raise 'should have had an exception'
417
417
  end
@@ -113,16 +113,16 @@ describe 'McflyModel' do
113
113
  a1 = @engine.evaluate('A', 'lookup', params)
114
114
  a2 = @engine.evaluate('A', 'clookup', params)
115
115
  expect(a1).to eq(a2) # cache/non return same
116
- expect(a1.class).to eq(Hash) # mode default so return hash
117
- expect(a2.class).to eq(Hash)
116
+ expect(a1.class).to eq(OpenStruct) # mode default so return OS
117
+ expect(a2.class).to eq(OpenStruct)
118
118
 
119
119
  # check that keys are non mcfly non uniqueness
120
- expect(a1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
120
+ expect(a1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
121
121
  end
122
122
 
123
123
  it 'lookup non generated' do
124
124
  # a1 will be AR Relations
125
- # b1 will be hash because the b fns return #first
125
+ # b1 will be OpenStructs because the b fns return #first
126
126
  e_id = Gemini::Entity.where(name: 'PLS').first.id
127
127
  bc_id = Gemini::BudCategory.where(name: 'Conv Fixed 20').first.id
128
128
  p = { 'e_id' => e_id, 'bc_id' => bc_id }
@@ -141,10 +141,10 @@ describe 'McflyModel' do
141
141
  # a1 is AR but still missing the FK entity_id so will raise
142
142
  expect { a1.first.entity }.to raise_error(/missing attribute: entity_id/)
143
143
 
144
- expect(b1.class).to eq(Hash)
144
+ expect(b1.class).to eq(OpenStruct)
145
145
 
146
146
  # make sure b1 has correct keys
147
- expect(b1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
147
+ expect(b1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
148
148
  end
149
149
 
150
150
  it 'lookup mode nil' do
@@ -177,6 +177,16 @@ Investor Services\t-0.625
177
177
  Admin Services\t-1.0
178
178
  Admin Services Plus\t-1.625
179
179
  Investor Services Acadamy\t-0.5
180
+ EOS
181
+
182
+ Gl = <<EOS
183
+ lenient
184
+ fha_203k_option2\tstring\tv\tfha_203k_option2
185
+
186
+ Investor Services\t-0.625
187
+ NOT (Admin Premium Services|Admin Services|Admin Services Plus)\t-1.0
188
+ Admin Services Plus\t-1.625
189
+ Investor Services Acadamy\t-0.5
180
190
  EOS
181
191
 
182
192
  before(:each) do
@@ -295,8 +305,8 @@ EOS
295
305
  let(:pt) { 'infinity' }
296
306
 
297
307
  before(:each) do
298
- ['G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8', 'Ga', 'Gb',
299
- 'Gc', 'Gd', 'Ge', 'Gf', 'Gg', 'Gh', 'Gj'].each do |g|
308
+ %w[G1 G2 G3 G4 G5 G6 G7 G8 Ga Gb
309
+ Gc Gd Ge Gf Gg Gh Gj Gl].each do |g|
300
310
  dg_from_import(g, "Marty::DataGridSpec::#{g}".constantize)
301
311
  end
302
312
  end
@@ -624,18 +634,22 @@ EOS
624
634
  expected_metadata = [{ 'dir' => 'v',
625
635
  'attr' => 'units',
626
636
  'keys' => [[1, 2], [1, 2], [3, 4], [3, 4]],
637
+ 'nots' => [false, false, false, false],
627
638
  'type' => 'integer' },
628
639
  { 'dir' => 'v',
629
640
  'attr' => 'ltv',
630
641
  'keys' => ['[,80]', '(80,105]', '[,80]', '(80,105]'],
642
+ 'nots' => [false, false, false, false],
631
643
  'type' => 'numrange' },
632
644
  { 'dir' => 'h',
633
645
  'attr' => 'cltv',
634
646
  'keys' => ['[100,110)', '[110,120)', '[120,]'],
647
+ 'nots' => [false, false, false],
635
648
  'type' => 'numrange' },
636
649
  { 'dir' => 'h',
637
650
  'attr' => 'fico',
638
651
  'keys' => ['[600,700)', '[700,750)', '[750,]'],
652
+ 'nots' => [false, false, false],
639
653
  'type' => 'numrange' }]
640
654
 
641
655
  dgh = Marty::DataGrid.lookup_h(pt, 'G2')
@@ -651,15 +665,18 @@ EOS
651
665
  expected_metadata = [{ 'dir' => 'v',
652
666
  'attr' => 'state',
653
667
  'keys' => [['CA'], ['HI', 'TX'], ['NM'], ['MA'], nil],
668
+ 'nots' => [false, false, false, false, false],
654
669
  'type' => 'string' },
655
670
  { 'dir' => 'v',
656
671
  'attr' => 'ltv',
657
672
  'keys' => ['[,80]', '(80,105]', '[,80]', '(80,105]',
658
673
  '[,80]'],
674
+ 'nots' => [false, false, false, false, false],
659
675
  'type' => 'numrange' },
660
676
  { 'dir' => 'h',
661
677
  'attr' => 'fico',
662
678
  'keys' => ['[600,700)', '[700,750)', '[750,]'],
679
+ 'nots' => [false, false, false],
663
680
  'type' => 'numrange' }]
664
681
  dgh = Marty::DataGrid.lookup_h(pt, 'G8')
665
682
  res = Marty::DataGrid.lookup_grid_distinct_entry_h(pt,
@@ -676,6 +693,7 @@ EOS
676
693
  expected_metadata = [{ 'dir' => 'v',
677
694
  'attr' => 'ltv',
678
695
  'keys' => ['[,115]', '(115,135]', '(135,140]'],
696
+ 'nots' => [false, false, false],
679
697
  'type' => 'numrange' }]
680
698
  dgh = Marty::DataGrid.lookup_h(pt, 'G8')
681
699
  res = Marty::DataGrid.lookup_grid_distinct_entry_h(pt,
@@ -698,6 +716,7 @@ EOS
698
716
  dgh, nil, false, true)
699
717
  end
700
718
  end
719
+
701
720
  it 'should handle all quote chars in grid inputs' do
702
721
  dgh = Marty::DataGrid.lookup_h(pt, 'G1')
703
722
  # single, double, backslash, grave, acute, unicode quotes: left single,
@@ -710,12 +729,70 @@ EOS
710
729
  pt, { 'ltv' => 10, 'fico' => 690, 'state' => st }, dgh, nil, false, true)
711
730
  end
712
731
  end
732
+
713
733
  it 'should handle quote chars in object name' do
714
734
  dgh = Marty::DataGrid.lookup_h(pt, 'G1')
715
735
  st = Gemini::State.new(name: "'\\")
716
736
  res = Marty::DataGrid.lookup_grid_distinct_entry_h(
717
737
  pt, { 'ltv' => 10, 'fico' => 690, 'state' => st }, dgh, nil, false, true)
718
738
  end
739
+
740
+ it 'Should handle NOT condition in lookups' do
741
+ dgh = Marty::DataGrid.lookup_h(pt, 'Gl')
742
+
743
+ g1_res = lookup_grid_helper(
744
+ 'infinity',
745
+ 'Gl',
746
+ { 'fha_203k_option2' => 'Admin Services Plus' },
747
+ false,
748
+ true
749
+ )
750
+ expect(g1_res).to eq([-1.625, 'Gl'])
751
+
752
+ g1_res = lookup_grid_helper(
753
+ 'infinity',
754
+ 'Gl',
755
+ { 'fha_203k_option2' => 'Not Existing Services' },
756
+ false,
757
+ true
758
+ )
759
+ expect(g1_res).to eq([-1.0, 'Gl'])
760
+
761
+ g1_res = lookup_grid_helper(
762
+ 'infinity',
763
+ 'Gl',
764
+ { 'fha_203k_option2' => 'Admin Services' },
765
+ false,
766
+ true
767
+ )
768
+ expect(g1_res).to eq([nil, 'Gl'])
769
+ end
770
+
771
+ it 'Should handle NOT condition in import' do
772
+ dg = dg_from_import('Gl0', Gl)
773
+ expect(dg.id).to be_present
774
+
775
+ expect(dg.metadata.first['nots']).to eq([false, true, false, false])
776
+ expect(dg.metadata.first['keys']).to eq(
777
+ [
778
+ ['Investor Services'],
779
+ ['Admin Premium Services', 'Admin Services', 'Admin Services Plus'],
780
+ ['Admin Services Plus'],
781
+ ['Investor Services Acadamy']
782
+ ]
783
+ )
784
+
785
+ indexes = Marty::GridIndexString.where(data_grid_id: dg.id)
786
+
787
+ expect(indexes.size).to eq 4
788
+ expect(indexes.where(not: true).size).to eq 1
789
+
790
+ not_index = indexes.find_by(not: true)
791
+ expect(not_index.key).to eq(
792
+ ['Admin Premium Services', 'Admin Services', 'Admin Services Plus']
793
+ )
794
+ expect(not_index.index).to eq(1)
795
+ end
719
796
  end
720
797
 
721
798
  describe 'exports' do
@@ -725,6 +802,11 @@ EOS
725
802
 
726
803
  expect(dg.export).to eq(dg2.export)
727
804
  end
805
+
806
+ it 'Should handle NOT condition in export' do
807
+ dg = dg_from_import('Gl0', Gl)
808
+ expect(dg.export.delete("\r")).to eq(Gl)
809
+ end
728
810
  end
729
811
 
730
812
  describe 'updates' do
@@ -757,15 +839,50 @@ EOS
757
839
  end
758
840
 
759
841
  it 'should be able to export and import back grids' do
760
- [G1, G2, G3, G4, G5, G6, G7, G8, G9, Ga, Gb].each_with_index do |grid, i|
842
+ [G1, G2, G3, G4, G5, G6, G7, G8, G9, Ga, Gb, Gl].each_with_index do |grid, i|
761
843
  dg = dg_from_import("G#{i}", grid)
762
844
  g1 = dg.export
845
+
763
846
  dg = dg_from_import("Gx#{i}", g1)
764
847
  g2 = dg.export
848
+
849
+ dg1 = Marty::DataGrid.lookup_h('infinity', "G#{i}").except(
850
+ 'id',
851
+ 'group_id',
852
+ 'created_dt',
853
+ 'name',
854
+ )
855
+
856
+ dg2 = Marty::DataGrid.lookup_h('infinity', "Gx#{i}").except(
857
+ 'id',
858
+ 'group_id',
859
+ 'created_dt',
860
+ 'name',
861
+ )
862
+
765
863
  expect(g1).to eq g2
864
+ expect(dg1).to eq dg2
766
865
  end
767
866
  end
768
867
 
868
+ it 'Should handle NOT condition in update' do
869
+ dgb = dg_from_import('Gl', Gl, '1/1/2014')
870
+ new_gl = Gl.sub(/-1.0/, '-3.45').sub('Investor Services', 'NOT (Investor Services)')
871
+ dgb.update_from_import('Gl', new_gl, '1/1/2015')
872
+
873
+ grids = Marty::DataGrid.where(name: 'Gl')
874
+ expect(grids.size).to eq 2
875
+
876
+ old_dg = grids.where.not(obsoleted_dt: 'infinity').first
877
+ new_dg = grids.where(obsoleted_dt: 'infinity').first
878
+
879
+ expect(old_dg.metadata.first['nots']).to eq [false, true, false, false]
880
+ expect(old_dg.data).to eq [[-0.625], [-1.0], [-1.625], [-0.5]]
881
+
882
+ expect(new_dg.metadata.first['nots']).to eq [true, true, false, false]
883
+ expect(new_dg.data).to eq [[-0.625], [-3.45], [-1.625], [-0.5]]
884
+ end
885
+
769
886
  it 'should be able to externally export/import grids' do
770
887
  load_scripts(nil, Date.today)
771
888
 
@@ -814,7 +931,8 @@ EOS
814
931
  # rubocop:disable Style/NestedTernaryOperator
815
932
  type_str = type == 'float' ? (explicit_float ? 'float' : nil) : type
816
933
  # rubocop:enable Style/NestedTernaryOperator
817
- top = [lenient_str, type_str].compact.join(' ') + "\t" + constraint + "\n"
934
+ con_part = constraint.present? ? "\t" + constraint : ''
935
+ top = [lenient_str, type_str].compact.join(' ') + con_part + "\n"
818
936
  (top =~ /\A\s*\z/ ? '' : top) +
819
937
  <<~EOS
820
938
  b\tboolean\tv
@@ -836,8 +954,8 @@ EOS
836
954
  tests = JSON.parse(File.read('spec/fixtures/json/data_grid.json'))
837
955
  aggregate_failures do
838
956
  tests.each do |test|
839
- keys = %w[id type constraint values error]
840
- id, type, constraint, values, error = test.values_at(*keys)
957
+ keys = %w[id type constraint values error line1]
958
+ id, type, constraint, values, error, line1 = test.values_at(*keys)
841
959
  err_re = Regexp.new(error) if error
842
960
  # for float, do both ex- and implicit declaration
843
961
  exfls = type == 'float' ? [true, false] : [true]
@@ -848,7 +966,21 @@ EOS
848
966
  got = nil
849
967
  tnam = "Test #{id} lenient=#{lenient} exfl=#{exfl}"
850
968
  begin
851
- dg_from_import(tnam, grid)
969
+ dg = dg_from_import(tnam, grid)
970
+
971
+ # make sure export of line1 works correctly
972
+ # when dg is lenient and/or has constraint and/or
973
+ # not float
974
+ next unless lenient || constraint.present? ||
975
+ type != 'float'
976
+
977
+ # also skip grids where we included float explicitly
978
+ # because export will convert back to implicit
979
+ next if type == 'float' && exfl
980
+
981
+ dga = dg.export_array
982
+ line1 = dga.first.first.join("\t") + "\n"
983
+ expect(line1).to eq(grid.lines.first)
852
984
  rescue StandardError => e
853
985
  got = e.message
854
986
  end