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.
- 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
|