marty 2.9.3 → 3.0.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +6 -5
  3. data/Gemfile.lock +1 -1
  4. data/app/components/marty/data_grid_view.rb +64 -1
  5. data/app/components/marty/data_grid_view/client/data_grid_edit.js +550 -0
  6. data/app/components/marty/import_type_view.rb +11 -2
  7. data/app/components/marty/main_auth_app.rb +23 -23
  8. data/app/components/marty/promise_view.rb +1 -1
  9. data/app/components/marty/report_select.rb +1 -0
  10. data/app/components/marty/script_form.rb +1 -1
  11. data/app/components/marty/user_view.rb +30 -18
  12. data/app/models/marty/data_grid.rb +34 -8
  13. data/app/models/marty/import_type.rb +2 -4
  14. data/app/models/marty/role_type.rb +10 -0
  15. data/app/models/marty/user.rb +9 -4
  16. data/app/models/marty/user_role.rb +2 -3
  17. data/app/models/marty/vw_promise.rb +2 -2
  18. data/app/services/marty/data_grid/constraint.rb +73 -0
  19. data/app/services/marty/data_grid_view/save_grid.rb +63 -0
  20. data/config/locales/en.yml +1 -0
  21. data/db/migrate/107_add_data_grid_constraint.rb +8 -0
  22. data/db/migrate/507_migrate_marty_roles_to_enum.rb +72 -0
  23. data/db/seeds.rb +1 -6
  24. data/lib/marty/permissions.rb +6 -16
  25. data/lib/marty/version.rb +1 -1
  26. data/spec/dummy/config/locales/en.yml +1 -0
  27. data/spec/features/data_grid_spec.rb +499 -0
  28. data/spec/features/data_import_spec.rb +11 -8
  29. data/spec/features/user_view_spec.rb +1 -1
  30. data/spec/fixtures/json/data_grid.json +210 -0
  31. data/spec/fixtures/misc/data_grid_1.txt +15 -0
  32. data/spec/fixtures/misc/data_grid_2.txt +17 -0
  33. data/spec/fixtures/misc/data_grid_3.txt +9 -0
  34. data/spec/fixtures/misc/data_grid_4.txt +5 -0
  35. data/spec/fixtures/misc/data_grid_5.txt +19 -0
  36. data/spec/fixtures/misc/grid1_final_data.json +23 -0
  37. data/spec/fixtures/misc/grid1_final_meta.json +182 -0
  38. data/spec/fixtures/misc/grid2_final_data.json +11 -0
  39. data/spec/fixtures/misc/grid2_final_meta.json +181 -0
  40. data/spec/fixtures/misc/grid5_final_data.json +142 -0
  41. data/spec/fixtures/misc/grid5_final_meta.json +152 -0
  42. data/spec/fixtures/misc/grid_log_errs.json +418 -0
  43. data/spec/models/data_grid_spec.rb +689 -626
  44. data/spec/models/import_type_spec.rb +5 -5
  45. data/spec/spec_helper.rb +9 -7
  46. data/spec/support/users.rb +1 -1
  47. metadata +22 -3
  48. data/app/models/marty/role.rb +0 -6
@@ -8,7 +8,6 @@ class Marty::User < Marty::Base
8
8
  MARTY_IMPORT_UNIQUENESS = [:login]
9
9
 
10
10
  has_many :user_roles, dependent: :destroy
11
- has_many :roles, through: :user_roles
12
11
 
13
12
  scope :active, -> { where(active: true) }
14
13
 
@@ -23,6 +22,10 @@ class Marty::User < Marty::Base
23
22
  name
24
23
  end
25
24
 
25
+ def roles
26
+ user_roles.map(&:role)
27
+ end
28
+
26
29
  # Returns the user who matches the given autologin +key+ or nil
27
30
  def self.try_to_autologin(key)
28
31
  tokens = Marty::Token.find_all_by_action_and_value('autologin', key.to_s)
@@ -95,8 +98,8 @@ class Marty::User < Marty::Base
95
98
  end
96
99
 
97
100
  def self.has_role(role)
98
- mr = Mcfly.whodunnit.roles rescue []
99
- mr.any? { |attr| attr.name == role }
101
+ mr = Mcfly.whodunnit.user_roles rescue []
102
+ mr.any? { |ur| ur.role == role }
100
103
  end
101
104
 
102
105
  private
@@ -110,8 +113,10 @@ class Marty::User < Marty::Base
110
113
  Rails.configuration.marty.system_account.to_s)
111
114
  system_id = system_user.id if system_user
112
115
 
116
+ roles = user_roles.map(&:role)
117
+
113
118
  if id == Mcfly.whodunnit.id
114
- roles.each { |r| roles.delete r unless r.name == 'user_manager' }
119
+ roles.each { |r| roles.delete r unless r == 'user_manager' }
115
120
  errors.add :base, 'User Managers cannot edit '\
116
121
  'or add additional roles to their own accounts'
117
122
  elsif id == system_id
@@ -1,7 +1,6 @@
1
1
  class Marty::UserRole < Marty::Base
2
- validates_uniqueness_of :user_id, scope: [:role_id]
3
- validates_presence_of :user_id, :role_id
2
+ validates_uniqueness_of :user_id, scope: [:role]
3
+ validates_presence_of :user_id, :role
4
4
 
5
5
  belongs_to :user
6
- belongs_to :role
7
6
  end
@@ -60,12 +60,12 @@ class Marty::VwPromise < Marty::Base
60
60
  'marty_users.login ILIKE ?',
61
61
  'marty_users.firstname ILIKE ?',
62
62
  'marty_users.lastname ILIKE ?',
63
- 'marty_roles.name ILIKE ?',
63
+ 'marty_user_roles.role::text ILIKE ?',
64
64
  ].join(' OR ')
65
65
 
66
66
  st = "%#{search_text}%"
67
67
  # Convert "Role Name" or "Role name" to "role_name" (underscore is key)
68
68
  st2 = "%#{search_text.titleize.gsub(/\s/, '').underscore}%"
69
- joins(user: :roles).where(query, st, st, st, st2).distinct
69
+ joins(user: :user_roles).where(query, st, st, st, st2).distinct
70
70
  }
71
71
  end
@@ -0,0 +1,73 @@
1
+ module Marty
2
+ class DataGrid
3
+ class Constraint
4
+ def self.parse(data_type, constraint)
5
+ return [] unless constraint
6
+
7
+ dt = DataGrid.convert_data_type(data_type)
8
+ if constraint =~ /[><]/
9
+ raise "range constraint not allowed for type #{dt}" unless
10
+ ['integer', 'float'].include?(dt)
11
+
12
+ pgr = Marty::Util.human_to_pg_range(constraint)
13
+ r = DataGrid.parse_range(pgr)
14
+ [r[0, 2], r[2..-1].reverse]
15
+ else
16
+ raw_vals = constraint.split('|')
17
+ return unless raw_vals.present?
18
+ raise 'list constraint not allowed for type Float' if dt == 'float'
19
+
20
+ pt = 'infinity'
21
+ vals = raw_vals.map do |v|
22
+ DataGrid.parse_fvalue(pt, v, data_type, dt)
23
+ end
24
+ [[:in?, vals.flatten]]
25
+ end
26
+ end
27
+
28
+ def self.real_type(data_type)
29
+ types = case data_type
30
+ when nil
31
+ [DEFAULT_DATA_TYPE.capitalize.constantize]
32
+ when 'string', 'integer', 'float'
33
+ [data_type.capitalize.constantize]
34
+ when 'boolean'
35
+ [TrueClass, FalseClass]
36
+ else
37
+ [data_type]
38
+ end
39
+ types << Integer if types.include?(Float)
40
+ end
41
+
42
+ # if check_data is called from validation, data has already been converted
43
+ # if called directly from DataGridView, it is still array of array of
44
+ # string.
45
+ def self.check_data(data_type, data, chks, cvt: false)
46
+ dt = Marty::DataGrid.convert_data_type(data_type)
47
+ klass = dt.class == Class ? dt : DataGrid.maybe_get_klass(dt)
48
+ rt = real_type(data_type) # get real type for string, trueclass etc
49
+ res = []
50
+ pt = 'infinity'
51
+ (0...data.first.length).each do |x|
52
+ (0...data.length).each do |y|
53
+ data_v = data[y][x]
54
+ cvt_val = nil
55
+ err = nil
56
+ begin
57
+ cvt_val = cvt && !data_v.class.in?(rt) ?
58
+ DataGrid.parse_fvalue(pt, data_v, dt, klass).first :
59
+ data_v
60
+ rescue StandardError => e
61
+ err = e.message
62
+ end
63
+ next res << [:type, x, y] if err
64
+
65
+ res << [:constraint, x, y] unless
66
+ chks.map { |op, chk_val| cvt_val.send(op, chk_val) }.all? { |v| v }
67
+ end
68
+ end
69
+ res
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,63 @@
1
+ module Marty
2
+ class DataGridView
3
+ class SaveGrid
4
+ class GridError < StandardError
5
+ attr_reader :data, :id
6
+ def initialize(msg, data, id)
7
+ @msg = msg
8
+ @data = data
9
+ @id = id
10
+ end
11
+
12
+ def message
13
+ "save_grid: #{@msg}"
14
+ end
15
+ end
16
+
17
+ def self.call(params)
18
+ user_perm = Marty::DataGridView.get_edit_save_permission
19
+ rec_id = params['record_id']
20
+ data = params['data']
21
+ raise GridError.new('entered with view permissions', data, rec_id) if
22
+ user_perm == 'view'
23
+
24
+ data_as_array = data.map do |row|
25
+ row.keys.map { |key| row[key] }
26
+ end
27
+ dg = Marty::DataGrid.mcfly_pt('infinity').find_by(group_id: rec_id)
28
+ vcnt = dg.metadata.select { |md| md['dir'] == 'v' }.count
29
+ hcnt = dg.metadata.select { |md| md['dir'] == 'h' }.count
30
+ cur_data_dim = [dg.data.length, dg.data[0].length]
31
+ exported = dg.export.lines
32
+ sep = exported.each_with_index.detect { |l, _i| /^\s*$/.match(l) }.last
33
+ new_data = data_as_array.map do |line|
34
+ line.join("\t") + "\r\n"
35
+ end.compact
36
+ new_data_dim = [data_as_array.count - hcnt,
37
+ data_as_array[0].count - vcnt]
38
+ if cur_data_dim != new_data_dim && user_perm != 'edit_all'
39
+ raise GridError.new('grid modification not allowed', data_as_array,
40
+ rec_id)
41
+ end
42
+ data_only = data_as_array[hcnt..-1].map do |a|
43
+ a[vcnt..-1]
44
+ end
45
+ chks = Marty::DataGrid::Constraint.parse(dg.data_type, dg.constraint)
46
+ probs = Marty::DataGrid::Constraint.check_data(dg.data_type, data_only,
47
+ chks, cvt: true)
48
+ return { 'problem_array' => probs } if probs.present?
49
+
50
+ to_import = (exported[0..sep] + new_data).join
51
+ dg.update_from_import(dg.name, to_import)
52
+ false
53
+ rescue GridError => e
54
+ Marty::Logger.error(e.message, rec_id: e.id,
55
+ data: e.data,
56
+ perm: user_perm)
57
+ { 'error_message' => e.message }
58
+ rescue StandardError => e
59
+ { 'error_message' => e.message }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -8,6 +8,7 @@ en:
8
8
  fields: Fields
9
9
  updated_at: Updated At
10
10
  user_view: User Management
11
+ roles_view: Roles Management
11
12
  config_view: Configuration
12
13
  reports: Reports
13
14
  import_synonym: Import Synonyms
@@ -0,0 +1,8 @@
1
+ class AddDataGridConstraint < ActiveRecord::Migration[4.2]
2
+ def self.up
3
+ add_column :marty_data_grids, :constraint, :string, null: true
4
+ end
5
+ def self.down
6
+ remove_column :marty_data_grids, :constraint
7
+ end
8
+ end
@@ -0,0 +1,72 @@
1
+ class MigrateMartyRolesToEnum < ActiveRecord::Migration[5.1]
2
+ include Marty::Migrations
3
+
4
+ def up
5
+ new_enum(Marty::RoleType, 'keep_marty_prefix_here')
6
+
7
+ add_column :marty_user_roles, :role, :marty_role_types
8
+ add_column :marty_import_types, :role, :marty_role_types
9
+
10
+ execute <<-SQL
11
+ UPDATE marty_user_roles AS ur
12
+ SET role = roles.name::marty_role_types
13
+ FROM marty_roles AS roles
14
+ WHERE ur.role_id = roles.id
15
+ SQL
16
+
17
+ execute <<-SQL
18
+ UPDATE marty_import_types AS it
19
+ SET role = roles.name::marty_role_types
20
+ FROM marty_roles AS roles
21
+ WHERE it.role_id = roles.id
22
+ SQL
23
+
24
+ remove_column :marty_user_roles, :role_id
25
+ remove_column :marty_import_types, :role_id
26
+
27
+ drop_table :marty_roles
28
+
29
+ change_column_null :marty_user_roles, :role, false
30
+ change_column_null :marty_import_types, :role, false
31
+ end
32
+
33
+ def down
34
+ create_table :marty_roles do |t|
35
+ t.string :name, null: false, limit: 255
36
+ end
37
+
38
+ add_column :marty_user_roles, :role_id, :integer
39
+ add_column :marty_import_types, :role_id, :integer
40
+
41
+ Marty::RoleType::VALUES.each do |role|
42
+ Marty::Role.create!(name: role)
43
+ end
44
+
45
+ execute <<-SQL
46
+ UPDATE marty_user_roles AS ur
47
+ SET role_id = roles.id
48
+ FROM marty_roles AS roles
49
+ WHERE ur.role::text = roles.name
50
+ SQL
51
+
52
+ execute <<-SQL
53
+ UPDATE marty_import_types AS ur
54
+ SET role_id = roles.id
55
+ FROM marty_roles AS roles
56
+ WHERE ur.role::text = roles.name
57
+ SQL
58
+
59
+ remove_column :marty_user_roles, :role
60
+ remove_column :marty_import_types, :role
61
+
62
+ execute <<-SQL
63
+ DROP TYPE marty_role_types
64
+ SQL
65
+
66
+ change_column_null :marty_user_roles, :role_id, false
67
+ change_column_null :marty_import_types, :role_id, false
68
+
69
+ add_fk :marty_user_roles, :marty_roles, column: :role_id
70
+ add_fk :marty_import_types, :marty_roles, column: :role_id
71
+ end
72
+ end
@@ -12,13 +12,8 @@ end
12
12
  # FIXME: hacky -- globally changes whodunnit
13
13
  Mcfly.whodunnit = Marty::User.find_by_login(system_login)
14
14
 
15
- # Create all Marty roles from configuration
16
- (Rails.configuration.marty.roles || []).each do |role|
17
- Marty::Role.create(name: role.to_s)
18
- end
19
-
20
15
  # Give system account all roles
21
- Marty::Role.all.map { |role|
16
+ Marty::RoleType.get_all.map { |role|
22
17
  ur = Marty::UserRole.new
23
18
  ur.user = Mcfly.whodunnit
24
19
  ur.role = role
@@ -1,9 +1,4 @@
1
1
  module Marty::Permissions
2
- # Make sure there are admin and user_manager roles,
3
- # even if hosting app doesn't define them
4
- REQ_ROLES = [:admin, :user_manager]
5
- ALL_ROLES = Rails.configuration.marty.roles.to_set.merge(REQ_ROLES)
6
-
7
2
  # Call using following format
8
3
  # has_marty_permissions create: [:dev, :admin],
9
4
  # read: :any,
@@ -13,15 +8,13 @@ module Marty::Permissions
13
8
  # :any gives permission to the action if user belongs to at least 1 role
14
9
  def has_marty_permissions(attrs)
15
10
  raise 'bad attrs' unless attrs.is_a?(Hash)
16
- raise 'unknown role' unless
17
- attrs.values.flatten.to_set.subset? (ALL_ROLES << :any)
18
11
 
19
12
  define_singleton_method(:marty_permissions) { attrs }
20
13
  end
21
14
 
22
15
  def current_user_roles
23
- roles = Mcfly.whodunnit.roles rescue []
24
- roles.map { |r| r.name.to_sym }.to_set
16
+ user_roles = Mcfly.whodunnit.user_roles rescue []
17
+ user_roles.map { |r| r.role.to_sym }.to_set
25
18
  end
26
19
 
27
20
  def can_perform_action?(action)
@@ -49,15 +42,12 @@ module Marty::Permissions
49
42
  end.compact
50
43
  end
51
44
 
52
- # generate has_xxx_perm? methods for all permissions.
53
- Rails.configuration.marty.roles.each do |role|
54
- define_method("has_#{role}_perm?") do
55
- current_user_roles.member? role
56
- end
45
+ def has_any_perm?
46
+ current_user_roles.any?
57
47
  end
58
48
 
59
- def has_any_perm?
60
- !(current_user_roles & ALL_ROLES).empty?
49
+ def has_perm?(role)
50
+ current_user_roles.member? role.to_sym
61
51
  end
62
52
 
63
53
  # FIXME: for backwards compatibility returns true
@@ -1,3 +1,3 @@
1
1
  module Marty
2
- VERSION = '2.9.3'
2
+ VERSION = '3.0.0'
3
3
  end
@@ -18,3 +18,4 @@ en:
18
18
  other_flag: "Other"
19
19
  g_bool_def: "Bool def Guard"
20
20
  g_nbool_def: "NBool def Guard"
21
+ project_legal_structure_type: "PLST"
@@ -0,0 +1,499 @@
1
+ require 'spec_helper'
2
+ require 'marty_rspec'
3
+
4
+ feature 'data grid view', js: true do
5
+ before(:each) do
6
+ marty_whodunnit
7
+ Marty::Script.load_scripts
8
+ dt = DateTime.parse('2017-1-1')
9
+ p = File.expand_path('../../fixtures/misc', __FILE__)
10
+ Dir.glob(p + '/data_grid_*.txt').each do |path|
11
+ n = File.basename(path, '.txt').camelize
12
+ Marty::DataGrid.create_from_import(n, File.read(path), dt)
13
+ end
14
+ end
15
+
16
+ def go_to_data_grids
17
+ press('Applications')
18
+ press('Data Grids')
19
+ expect(page).to have_content 'Data Grids'
20
+ end
21
+
22
+ # setup the info for the ext grid
23
+ def grid_setup
24
+ widths, rows = get_grid_info
25
+ @grid = page.all(:xpath,
26
+ ".//div[contains(@class, 'x-grid-item-container')]").
27
+ reject { |e| e.text.include?('2016-12-31') }.first
28
+ @gridx = @grid.native.rect.x
29
+ @gridy = @grid.native.rect.y
30
+ height = @grid.native.size.height
31
+ @perr = (height / rows).to_i
32
+ col_preh = widths.each_with_index.map do |c, i|
33
+ [i, [c[:x] + @gridx, c[:width]]]
34
+ end
35
+ @colh = Hash[col_preh]
36
+ end
37
+
38
+ def context_click(col, row, menuidx, click: false)
39
+ colx = @colh[col][0]
40
+ colw = @colh[col][1]
41
+ xpos = (colx + colw / 2).to_i - @gridx
42
+ ypos = (@perr * row + @perr / 2).to_i
43
+ mxpos = xpos + 20
44
+ mypos = ypos + (@perr * menuidx + @perr / 2).to_i
45
+ page.driver.browser.action.
46
+ move_to(@grid.native, xpos, ypos).context_click.perform
47
+
48
+ if click
49
+ page.driver.browser.action.move_to(@grid.native, mxpos, mypos).click.
50
+ release.perform
51
+ else
52
+ ret = get_menu
53
+ page.driver.browser.action.move_to(@grid.native, 0, 0).click.perform
54
+ ret
55
+ end
56
+ end
57
+
58
+ def get_menu
59
+ m_en = page.all(:xpath,
60
+ ".//div[contains(@class, 'x-menu-item-default')]").to_a
61
+ m_dis = page.all(:xpath,
62
+ ".//div[contains(@class, 'x-menu-item-disabled')]").to_a
63
+ labels = ['Insert Row Above', 'Insert Row Below',
64
+ 'Insert Column Left', 'Insert Column Right',
65
+ 'Delete Row', 'Delete Column']
66
+ en = m_en.select { |m| labels.include?(m.text) }.
67
+ map { |m| [m.text, :enabled] }
68
+ dis = m_dis.select { |m| labels.include?(m.text) }.
69
+ map { |m| [m.text, :disabled] }
70
+ Hash[en + dis]
71
+ end
72
+
73
+ def cell_edit(col, row, text)
74
+ colx = @colh[col][0]
75
+ colw = @colh[col][1]
76
+ xpos = (colx + colw / 2).to_i - @gridx
77
+ ypos = (@perr * row + @perr / 2).to_i
78
+ page.driver.browser.action.move_to(@grid.native, xpos, ypos).double_click.
79
+ send_keys("\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08").perform
80
+ sleep 0.1
81
+ page.driver.browser.action.
82
+ move_to(@grid.native, xpos, ypos).double_click.
83
+ send_keys(text.to_s).perform
84
+ end
85
+
86
+ def get_grid(to_get: :values)
87
+ get_expr = case to_get
88
+ when :values
89
+ 'value'
90
+ when :colors
91
+ 'cell.style.backgroundColor'
92
+ when :qtips
93
+ "('data-qtip' in cell.attributes) && "\
94
+ "cell.attributes['data-qtip'].value"
95
+ end
96
+ db = 'debugger;' if to_get == :qtips
97
+ ret = run_js <<-JS
98
+ var grid = Ext.ComponentQuery.query('grid').find(function(v) {
99
+ return v.name=='data_grid_edit_grid'
100
+ });
101
+ var store = grid.getStore().data.items;
102
+ var view = grid.getView();
103
+ var ret = [];
104
+ for (var i = 0; i < store.length; ++i) {
105
+ var row = [];
106
+ var j = 0;
107
+ for (const [key, value] of Object.entries(store[i].data)) {
108
+ if (key != 'id') {
109
+ var cell = view.getCell(i, j);
110
+ row.push(#{get_expr});
111
+ }
112
+ j++;
113
+ }
114
+ ret.push(row);
115
+ }
116
+ return ret;
117
+ JS
118
+ ret
119
+ end
120
+
121
+ def get_grid_info
122
+ cwraw, rc = run_js <<-JS
123
+ var grid = Ext.ComponentQuery.query('grid').find(function(v) {
124
+ return v.name=='data_grid_edit_grid'
125
+ });
126
+ var cols = grid.getColumnManager().getColumns();
127
+ var ret = [];
128
+ for (col of cols)
129
+ ret.push(col.lastBox);
130
+ return [ret, grid.getStore().data.items.length];
131
+ JS
132
+ cw = cwraw.map do |cwr|
133
+ h = {}
134
+ h[:x] = cwr['x']
135
+ h[:width] = cwr['width']
136
+ h
137
+ end
138
+ [cw, rc]
139
+ end
140
+
141
+ def iterate_area(rect, a_of_a)
142
+ ulx, uly, lrx, lry = rect.values
143
+ sub = []
144
+ a_of_a[uly...lry].each_with_index do |row, row_idx|
145
+ subrow = []
146
+ row[ulx...lrx].each_with_index do |cell, col_idx|
147
+ subrow << cell
148
+ yield(cell, row_idx, col_idx) if block_given?
149
+ end
150
+ sub << subrow
151
+ end
152
+ sub
153
+ end
154
+
155
+ def check_dim(dim, dim_size, dim_area, ui_colors, ui_data, by_col: true)
156
+ return unless dim.count > 0
157
+
158
+ color_sets = Array.new(dim_size) { Set.new }
159
+ iterate_area(dim_area, ui_colors) do |cell, row_idx, col_idx|
160
+ idx = by_col ? col_idx : row_idx
161
+ color_sets[idx] << cell
162
+ end
163
+ color_sets.each do |the_set|
164
+ expect(the_set.length).to eq(1)
165
+ expect(the_set.to_a[0]).to match(/^rgb/)
166
+ end
167
+ all_set = color_sets.map(&:to_a).reduce(&:+).to_set
168
+ expect(all_set.length).to eq(color_sets.length)
169
+
170
+ dim_data = iterate_area(dim_area, ui_data)
171
+ keys = dim.map { |d| Marty::DataGrid.export_keys(d) }
172
+ piv = by_col ?
173
+ keys.each_with_object([]) do |key_array, piv_array|
174
+ key_array.each_with_index do |v, idx|
175
+ (piv_array[idx] ||= []) << v
176
+ end
177
+ end : keys
178
+ expect(piv.map(&:flatten)).to eq(dim_data)
179
+ end
180
+
181
+ def check_grid(grid_name, perm, all_cells)
182
+ grid = Marty::DataGrid.mcfly_pt('infinity').find_by(name: grid_name)
183
+ hdim = grid.metadata.select { |md| md['dir'] == 'h' }
184
+ vdim = grid.metadata.select { |md| md['dir'] == 'v' }
185
+ data = grid.data
186
+ ui_data = get_grid
187
+ row_cnt = ui_data.length
188
+ col_cnt = ui_data[0].length
189
+ ui_colors = get_grid(to_get: :colors)
190
+
191
+ vdim_size = vdim.count
192
+ hdim_size = hdim.count
193
+ vdim_area = { ulx: 0, uly: hdim_size, lrx: vdim_size, lry: row_cnt }
194
+ hdim_area = { ulx: vdim_size, uly: 0, lrx: col_cnt, lry: hdim_size }
195
+ data_area = { ulx: vdim_size, uly: hdim_size, lrx: col_cnt,
196
+ lry: row_cnt }
197
+ # check the dim areas
198
+ check_dim(vdim, vdim_size, vdim_area, ui_colors, ui_data) if vdim_size > 0
199
+ check_dim(hdim, hdim_size, hdim_area, ui_colors, ui_data, by_col: false) if
200
+ hdim_size > 0
201
+
202
+ # data section color check
203
+ colors = Set.new
204
+ iterate_area(data_area, ui_colors) do |cell, _row_idx, _col_idx|
205
+ colors << cell
206
+ end
207
+ expect(colors.length).to eq(1)
208
+ expect(colors.to_a[0]).to eq('')
209
+
210
+ # check that data section matches actual grid data
211
+ data_data = iterate_area(data_area, ui_data)
212
+ expect(struct_compare(data_data, grid.data)).to be_falsey
213
+
214
+ grid_setup
215
+ all_disabled = {
216
+ 'Insert Row Above' => :disabled,
217
+ 'Insert Row Below' => :disabled,
218
+ 'Insert Column Left' => :disabled,
219
+ 'Insert Column Right' => :disabled,
220
+ 'Delete Row' => :disabled,
221
+ 'Delete Column' => :disabled
222
+ }
223
+ col_disabled = all_disabled + {
224
+ 'Insert Row Above' => :enabled,
225
+ 'Insert Row Below' => :enabled,
226
+ 'Delete Row' => :enabled
227
+ }
228
+ row_disabled = all_disabled + {
229
+ 'Insert Column Left' => :enabled,
230
+ 'Insert Column Right' => :enabled,
231
+ 'Delete Column' => :enabled
232
+ }
233
+ all_enabled = Hash[all_disabled.map { |k, _v| [k, :enabled] }]
234
+
235
+ # in the data area, what is allowed depends on whether there
236
+ # are only vdims, hdims, or both
237
+ data_menu = if ['view', 'edit_data'].include?(perm)
238
+ then all_disabled
239
+ elsif hdim.count == 0
240
+ col_disabled
241
+ elsif vdim.count == 0
242
+ row_disabled
243
+ else
244
+ all_enabled
245
+ end
246
+ areas = [[data_area, data_menu],
247
+ [vdim_area, perm == 'edit_all' && vdim.count > 0 ? col_disabled :
248
+ all_disabled],
249
+ [hdim_area, perm == 'edit_all' && hdim.count > 0 ? row_disabled :
250
+ all_disabled]]
251
+ areas.each do |area, menu_exp|
252
+ xrangefull = (area[:ulx]...area[:lrx]).to_a
253
+ yrangefull = (area[:uly]...area[:lry]).to_a
254
+
255
+ # grid has only v or h dims
256
+ next if xrangefull.empty? || yrangefull.empty?
257
+
258
+ xrange = all_cells ? xrangefull : [xrangefull[0], xrangefull[-1]].uniq
259
+ yrange = all_cells ? yrangefull : [yrangefull[0], yrangefull[-1]].uniq
260
+ xrange.each do |x|
261
+ yrange.each do |y|
262
+ m = context_click(x, y, 0)
263
+ expect(m).to eq(menu_exp)
264
+ end
265
+ end
266
+ end
267
+ end
268
+
269
+ def validate_grid(gn)
270
+ grid = Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid' + gn)
271
+ fix = 'spec/fixtures/misc'
272
+ exp_data = JSON.parse(File.read("#{fix}/grid#{gn}_final_data.json"))
273
+ expect(grid.data).to eq(exp_data)
274
+ exp_meta = JSON.parse(File.read("#{fix}/grid#{gn}_final_meta.json"))
275
+ grid_meta = grid.metadata.map do |md|
276
+ [md['dir'], md['attr'], md['keys']]
277
+ end.sort
278
+ expect(grid_meta).to eq(exp_meta)
279
+ end
280
+
281
+ it 'dg editor' do
282
+ log_in_as('marty')
283
+ go_to_data_grids
284
+ dgv = netzke_find('data_grid_view')
285
+ grids = dgv.get_col_vals('name', 5)
286
+ context_test_all = ENV['DG_FEATURE_QUICK'] != 'true'
287
+ [['edit_all', context_test_all],
288
+ ['edit_data', false],
289
+ ['view', false]].each do |perm, all_cells|
290
+ grids.each do |grid|
291
+ Marty::Config['grid_edit_edit_perm'] = perm
292
+ Marty::Config['grid_edit_save_perm'] = perm
293
+ pos = grids.index(grid) + 1
294
+ dgv.select_row(pos)
295
+ press('Edit Grid')
296
+ wait_for_ajax
297
+ check_grid(grid, perm, all_cells)
298
+ expect(page).not_to have_content('Save') if perm == 'view'
299
+ press('Cancel')
300
+ wait_for_ajax
301
+ end
302
+ end
303
+ Marty::Config['grid_edit_edit_perm'] = 'edit_all'
304
+ Marty::Config['grid_edit_save_perm'] = 'edit_all'
305
+
306
+ # now test some editing, saving, and cancel logic
307
+ get_latest = lambda do
308
+ Marty::DataGrid.mcfly_pt('infinity').find_by(name: 'DataGrid5')
309
+ end
310
+ grid = get_latest.call
311
+ grid.constraint = '>=0<200'
312
+ grid.save!
313
+ pos = grids.index('DataGrid5') + 1
314
+ dgv.select_row(pos)
315
+ press('Edit Grid')
316
+ wait_for_ajax
317
+ # test saving and validation etc
318
+ grid_setup
319
+ cell_edit(2, 3, 'abc')
320
+ cell_edit(3, 3, '250')
321
+ press('Save')
322
+ expect(page).to have_content('error: some entries failed constraint or '\
323
+ 'data type check')
324
+ press('OK')
325
+ colors = get_grid(to_get: :colors)
326
+ qtips = get_grid(to_get: :qtips)
327
+ expect(colors[3][2]).to eq('rgb(255, 177, 129)')
328
+ expect(colors[3][3]).to eq('rgb(255, 129, 129)')
329
+ expect(qtips[3][2]).to eq('failed type check')
330
+ expect(qtips[3][3]).to eq('failed constraint check')
331
+ expect(qtips[0][0]).to eq(false)
332
+ expect(qtips[0][2]).to eq('mortgage_type')
333
+ expect(qtips[1][2]).to eq('PLST')
334
+ expect(qtips[3][0]).to eq('property_state')
335
+ expect(qtips[3][1]).to eq('property_county_name')
336
+
337
+ grid_setup
338
+ cell_edit(2, 3, '123.456')
339
+ cell_edit(3, 3, '1.1')
340
+
341
+ cell_edit(2, 2, 'yo')
342
+ press('Save')
343
+ expect(page).to have_content('error: bad range yo')
344
+ press('OK')
345
+ cell_edit(2, 2, '>=1')
346
+
347
+ cell_edit(2, 0, 'xyz')
348
+ press('Save')
349
+ expect(page).to have_content('error: instance xyz of Gemini::MortgageType'\
350
+ ' not found')
351
+ press('OK')
352
+ cell_edit(2, 0, 'Conventional')
353
+
354
+ cell_edit(0, 3, 'xyz')
355
+ press('Save')
356
+ expect(page).to have_content("error: no such Gemini::EnumState: 'xyz'")
357
+ press('OK')
358
+ cell_edit(0, 3, 'AK')
359
+
360
+ press('Save')
361
+ wait_for_ajax
362
+ grid = get_latest.call
363
+ expect(grid.data[0][0]).to eq(123.456)
364
+ press('Edit Grid')
365
+ wait_for_ajax
366
+
367
+ colors = get_grid(to_get: :colors)
368
+ qtips = get_grid(to_get: :qtips)
369
+ expect(colors[3][2]).to eq('')
370
+ expect(colors[3][3]).to eq('')
371
+ expect(qtips[3][2]).to eq(false)
372
+ expect(qtips[3][2]).to eq(false)
373
+
374
+ # each time we change the grid, grid_setup needs to ask
375
+ # JS for the updated grid info
376
+ grid_setup
377
+ cell_edit(2, 3, '0.0')
378
+ press('Cancel')
379
+ exp = 'You are closing a window that has unsaved changes'
380
+ expect(page).to have_content(exp)
381
+ press('No')
382
+ find(:xpath, ".//div[contains(@class,'x-tool-close')]").click
383
+ exp = 'You are closing a window that has unsaved changes'
384
+ expect(page).to have_content(exp)
385
+ press('No')
386
+ press('Cancel')
387
+ exp = 'You are closing a window that has unsaved changes'
388
+ expect(page).to have_content(exp)
389
+ press('Yes')
390
+ grid = get_latest.call
391
+ expect(grid.data[0][0]).to eq(123.456)
392
+
393
+ press('Edit Grid')
394
+ wait_for_ajax
395
+ grid_setup
396
+ context_click(1, 9, 1, click: true)
397
+ grid_setup
398
+ ['AR', 'Abc', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].each_with_index do |v, idx|
399
+ cell_edit(idx, 10, v)
400
+ end
401
+ context_click(5, 12, 0, click: true)
402
+ grid_setup
403
+ ['TN', 'Xyz', 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].each_with_index do |v, idx|
404
+ cell_edit(idx, 12, v)
405
+ end
406
+ context_click(5, 5, 2, click: true)
407
+ grid_setup
408
+ (['FHA', nil, '<1'] + [100] * 11).each_with_index do |v, idx|
409
+ cell_edit(5, idx, v) if v
410
+ end
411
+ context_click(13, 1, 3, click: true)
412
+ grid_setup
413
+ (['VA', nil, '>5'] + [100] * 11).each_with_index do |v, idx|
414
+ cell_edit(14, idx, v)
415
+ end
416
+ grid_setup
417
+ context_click(2, 4, 4, click: true)
418
+ grid_setup
419
+ context_click(2, 1, 5, click: true)
420
+ wait_for_ajax
421
+ press('Save')
422
+ wait_for_ajax
423
+ sleep 1
424
+ validate_grid('5')
425
+
426
+ pos = grids.index('DataGrid2') + 1
427
+ dgv.select_row(pos)
428
+ begin
429
+ press('Edit Grid')
430
+ press('Edit Grid')
431
+ rescue StandardError => e # rubocop:disable Lint/HandleExceptions
432
+ end
433
+ wait_for_ajax
434
+ grid_setup
435
+ context_click(2, 3, 2, click: true)
436
+ grid_setup
437
+ ['99', 'StrNew', 'StrNew2', 'Strnew3', 'StrNew4', 'StrNew5',
438
+ '>10000000<11000000', '12345'].each_with_index do |v, idx|
439
+ cell_edit(2, idx, v)
440
+ end
441
+ press('Save')
442
+ sleep 1
443
+ validate_grid('2')
444
+
445
+ pos = grids.index('DataGrid1') + 1
446
+ dgv.select_row(pos)
447
+ begin
448
+ press('Edit Grid')
449
+ press('Edit Grid')
450
+ rescue StandardError => e # rubocop:disable Lint/HandleExceptions
451
+ end
452
+ wait_for_ajax
453
+ grid_setup
454
+ cell_edit(7, 2, '11111')
455
+ context_click(3, 4, 1, click: true)
456
+ ['5|4', 'Abc', 'Def', 'QQQ', 'ZZZ', 'AAA', '>5000011<6000000', 765].
457
+ each_with_index do |v, idx|
458
+ cell_edit(idx, 5, v)
459
+ end
460
+ press('Save')
461
+ sleep 1
462
+ validate_grid('1')
463
+
464
+ Marty::Config['grid_edit_save_perm'] = 'view'
465
+ pos = grids.index('DataGrid5') + 1
466
+ dgv.select_row(pos)
467
+ count = 1
468
+ begin
469
+ press('Edit Grid')
470
+ wait_for_ajax
471
+ grid_setup
472
+ cell_edit(2, 3, '123.456')
473
+ rescue StandardError => e
474
+ if count > 0
475
+ sleep 1
476
+ retry
477
+ end
478
+ count -= 1
479
+ end
480
+ press('Save')
481
+ errexp = 'error: save_grid: entered with view permissions'
482
+ expect(page).to have_content(errexp)
483
+ press('OK')
484
+ wait_for_ajax
485
+ context_click(2, 4, 4, click: true)
486
+ Marty::Config['grid_edit_save_perm'] = 'edit_data'
487
+ press('Save')
488
+ errexp = 'error: save_grid: grid modification not allowed'
489
+ expect(page).to have_content(errexp)
490
+ exp_log = JSON.parse(File.read('spec/fixtures/misc/grid_log_errs.json'))
491
+ got = Marty::Log.all.select(:message, :details).attributes.map do |l|
492
+ newl = l.except('id')
493
+ newl['details'].delete('rec_id')
494
+ newl
495
+ end
496
+ cmp = struct_compare(got, exp_log)
497
+ expect(cmp).to be_falsey
498
+ end
499
+ end