marty 2.9.3 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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