marty 2.9.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitlab-ci.yml +6 -5
- data/Gemfile.lock +1 -1
- data/app/components/marty/data_grid_view.rb +64 -1
- data/app/components/marty/data_grid_view/client/data_grid_edit.js +550 -0
- data/app/components/marty/import_type_view.rb +11 -2
- data/app/components/marty/main_auth_app.rb +23 -23
- data/app/components/marty/promise_view.rb +1 -1
- data/app/components/marty/report_select.rb +1 -0
- data/app/components/marty/script_form.rb +1 -1
- data/app/components/marty/user_view.rb +30 -18
- data/app/models/marty/data_grid.rb +34 -8
- data/app/models/marty/import_type.rb +2 -4
- data/app/models/marty/role_type.rb +10 -0
- data/app/models/marty/user.rb +9 -4
- data/app/models/marty/user_role.rb +2 -3
- data/app/models/marty/vw_promise.rb +2 -2
- data/app/services/marty/data_grid/constraint.rb +73 -0
- data/app/services/marty/data_grid_view/save_grid.rb +63 -0
- data/config/locales/en.yml +1 -0
- data/db/migrate/107_add_data_grid_constraint.rb +8 -0
- data/db/migrate/507_migrate_marty_roles_to_enum.rb +72 -0
- data/db/seeds.rb +1 -6
- data/lib/marty/permissions.rb +6 -16
- data/lib/marty/version.rb +1 -1
- data/spec/dummy/config/locales/en.yml +1 -0
- data/spec/features/data_grid_spec.rb +499 -0
- data/spec/features/data_import_spec.rb +11 -8
- data/spec/features/user_view_spec.rb +1 -1
- data/spec/fixtures/json/data_grid.json +210 -0
- data/spec/fixtures/misc/data_grid_1.txt +15 -0
- data/spec/fixtures/misc/data_grid_2.txt +17 -0
- data/spec/fixtures/misc/data_grid_3.txt +9 -0
- data/spec/fixtures/misc/data_grid_4.txt +5 -0
- data/spec/fixtures/misc/data_grid_5.txt +19 -0
- data/spec/fixtures/misc/grid1_final_data.json +23 -0
- data/spec/fixtures/misc/grid1_final_meta.json +182 -0
- data/spec/fixtures/misc/grid2_final_data.json +11 -0
- data/spec/fixtures/misc/grid2_final_meta.json +181 -0
- data/spec/fixtures/misc/grid5_final_data.json +142 -0
- data/spec/fixtures/misc/grid5_final_meta.json +152 -0
- data/spec/fixtures/misc/grid_log_errs.json +418 -0
- data/spec/models/data_grid_spec.rb +689 -626
- data/spec/models/import_type_spec.rb +5 -5
- data/spec/spec_helper.rb +9 -7
- data/spec/support/users.rb +1 -1
- metadata +22 -3
- data/app/models/marty/role.rb +0 -6
data/app/models/marty/user.rb
CHANGED
@@ -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.
|
99
|
-
mr.any? { |
|
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
|
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: [:
|
3
|
-
validates_presence_of :user_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
|
-
'
|
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: :
|
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
|
data/config/locales/en.yml
CHANGED
@@ -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
|
data/db/seeds.rb
CHANGED
@@ -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::
|
16
|
+
Marty::RoleType.get_all.map { |role|
|
22
17
|
ur = Marty::UserRole.new
|
23
18
|
ur.user = Mcfly.whodunnit
|
24
19
|
ur.role = role
|
data/lib/marty/permissions.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
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
|
-
|
53
|
-
|
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
|
60
|
-
|
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
|
data/lib/marty/version.rb
CHANGED
@@ -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
|