marty 8.0.0 → 8.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +7 -0
  3. data/app/assets/javascripts/marty/cable.js +12 -0
  4. data/app/assets/stylesheets/marty/codemirror/notifications.css +4 -0
  5. data/app/channels/application_cable/channel.rb +4 -0
  6. data/app/channels/application_cable/connection.rb +17 -0
  7. data/app/channels/marty/notification_channel.rb +8 -0
  8. data/app/components/marty/auth_app.rb +54 -6
  9. data/app/components/marty/auth_app/client/auth_app.js +53 -0
  10. data/app/components/marty/main_auth_app.rb +47 -1
  11. data/app/components/marty/notifications/config_view.rb +56 -0
  12. data/app/components/marty/notifications/deliveries_view.rb +50 -0
  13. data/app/components/marty/notifications/grid_view.rb +56 -0
  14. data/app/components/marty/notifications/window.rb +23 -0
  15. data/app/components/marty/promise_view.rb +13 -1
  16. data/app/components/marty/promise_view/client/promise_view.js +18 -0
  17. data/app/components/marty/{schedule_jobs_dashboard/client/schedule_jobs_dashboard.js → schedule_jobs_grid/client/schedule_jobs_grid.js} +0 -0
  18. data/app/components/marty/simple_app/client/simple_app.js +5 -1
  19. data/app/components/marty/users/user_view.rb +177 -0
  20. data/app/controllers/marty/application_controller.rb +13 -0
  21. data/app/models/marty/data_grid.rb +1 -1
  22. data/app/models/marty/notifications.rb +4 -0
  23. data/app/models/marty/notifications/config.rb +25 -0
  24. data/app/models/marty/notifications/delivery.rb +56 -0
  25. data/app/models/marty/notifications/event_type.rb +11 -0
  26. data/app/models/marty/notifications/notification.rb +25 -0
  27. data/app/models/marty/promise.rb +1 -0
  28. data/app/models/marty/user.rb +14 -0
  29. data/app/services/marty/notifications/create.rb +39 -0
  30. data/app/services/marty/notifications/create_deliveries.rb +25 -0
  31. data/app/services/marty/notifications/process_delivery.rb +11 -0
  32. data/app/services/marty/notifications/processors/email.rb +13 -0
  33. data/app/services/marty/notifications/processors/sms.rb +13 -0
  34. data/app/services/marty/notifications/processors/web.rb +27 -0
  35. data/app/services/marty/promises/cancel.rb +37 -0
  36. data/app/services/marty/promises/delorean/create.rb +4 -0
  37. data/app/services/marty/promises/ruby/create.rb +4 -1
  38. data/app/views/marty/diagnostic/op.html.erb +4 -0
  39. data/db/migrate/514_remove_marty_events.rb +1 -1
  40. data/db/migrate/519_create_marty_notifications_event_types.rb +16 -0
  41. data/db/migrate/520_create_marty_notifications.rb +11 -0
  42. data/db/migrate/521_create_marty_notifications_deliveries.rb +22 -0
  43. data/db/migrate/522_create_marty_notifications_config.rb +21 -0
  44. data/db/sql/lookup_grid_distinct_v1.sql +6 -6
  45. data/lib/marty.rb +2 -0
  46. data/lib/marty/diagnostic/version.rb +36 -26
  47. data/lib/marty/engine.rb +7 -1
  48. data/lib/marty/mcfly_model.rb +27 -6
  49. data/lib/marty/railtie.rb +1 -0
  50. data/lib/marty/version.rb +1 -1
  51. data/marty.gemspec +3 -0
  52. data/spec/controllers/diagnostic/controller_spec.rb +1 -1
  53. data/spec/dummy/app/models/gemini/fannie_bup.rb +27 -13
  54. data/spec/dummy/app/models/gemini/helper.rb +62 -0
  55. data/spec/features/notifications_spec.rb +224 -0
  56. data/spec/job_helper.rb +14 -1
  57. data/spec/lib/mcfly_model_spec.rb +14 -7
  58. data/spec/models/promise_spec.rb +121 -0
  59. data/spec/services/notifications/create_spec.rb +82 -0
  60. metadata +73 -3
@@ -14,7 +14,13 @@ module Marty
14
14
 
15
15
  config.assets.precompile += [
16
16
  'marty/application.js',
17
- 'marty/application.css'
17
+ 'marty/cable.js',
18
+ 'marty/application.css',
19
+ 'marty/dark_mode.css'
18
20
  ]
21
+
22
+ config.action_cable.disable_request_forgery_protection = true
23
+ # Can be overriden by config/cable.yml in Rails app
24
+ ActionCable.server.config.cable ||= { 'adapter' => 'postgresql' }
19
25
  end
20
26
  end
@@ -6,10 +6,16 @@ module Mcfly::Model
6
6
  end
7
7
 
8
8
  module ClassMethods
9
- def hash_if_necessary(q, private)
9
+ def openstruct_if_necessary(q, private)
10
10
  !private && q.is_a?(ActiveRecord::Base) ? make_openstruct(q) : q
11
11
  end
12
12
 
13
+ def hash_if_necessary(q, to_hash)
14
+ return make_hash(q) if to_hash && q.is_a?(ActiveRecord::Base)
15
+
16
+ q
17
+ end
18
+
13
19
  def base_mcfly_lookup(name, options = {}, &block)
14
20
  delorean_options = {
15
21
  # private: options.fetch(:private, false),
@@ -41,7 +47,14 @@ module Mcfly::Model
41
47
 
42
48
  q = q.first if q.respond_to?(:first) && options[:mode] == :first
43
49
 
44
- hash_if_necessary(q, options[:private])
50
+ if options[:to_hash]
51
+ next hash_if_necessary(
52
+ q,
53
+ options.fetch(:to_hash, false)
54
+ )
55
+ end
56
+
57
+ openstruct_if_necessary(q, options[:private])
45
58
  end
46
59
  end
47
60
 
@@ -55,7 +68,7 @@ module Mcfly::Model
55
68
 
56
69
  def gen_mcfly_lookup(name, attrs, options = {})
57
70
  raise "bad options #{options.keys}" unless
58
- (options.keys - [:mode, :cache, :private]).empty?
71
+ (options.keys - [:mode, :cache, :private, :to_hash]).empty?
59
72
 
60
73
  mode = options.fetch(:mode, :first)
61
74
 
@@ -134,7 +147,7 @@ module Mcfly::Model
134
147
 
135
148
  pc_name = "pc_#{name}".to_sym
136
149
 
137
- gen_mcfly_lookup(pc_name, pc_attrs, options + { private: true })
150
+ gen_mcfly_lookup(pc_name, pc_attrs, options + { private: true, to_hash: false })
138
151
 
139
152
  lpi = attrs.keys.index rel_attr
140
153
 
@@ -142,7 +155,7 @@ module Mcfly::Model
142
155
  raise "need #{rel_attr} argument" unless lpi
143
156
 
144
157
  # cache if mode is not nil
145
- priv = options[:private]
158
+ to_hash = options.fetch(:to_hash, false)
146
159
 
147
160
  # cache if mode is not explicitly set to nil or cache is true
148
161
  cache = options.fetch(:cache) { options.fetch(:mode, :first) }
@@ -165,7 +178,15 @@ module Mcfly::Model
165
178
  send(cat_attr_id)
166
179
 
167
180
  q = send(pc_name, ts, *args)
168
- hash_if_necessary(q, priv)
181
+
182
+ if to_hash
183
+ next hash_if_necessary(
184
+ q,
185
+ options.fetch(:to_hash, false)
186
+ )
187
+ end
188
+
189
+ openstruct_if_necessary(q, options[:private])
169
190
  end
170
191
  end
171
192
  end
@@ -5,5 +5,6 @@ module Marty
5
5
  config.marty.extjs_theme = 'classic'
6
6
  config.marty.promise_job_enqueue_hooks = []
7
7
  config.marty.redis_url = nil
8
+ config.marty.enable_action_cable = true
8
9
  end
9
10
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Marty
4
- VERSION = '8.0.0'
4
+ VERSION = '8.2.0'
5
5
  end
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  s.files = `git ls-files`.split($\)
28
28
  s.licenses = ['MIT']
29
29
  # used for signing aws ec2 requests
30
+ s.add_dependency 'actioncable'
30
31
  s.add_dependency 'aws-sigv4'
31
32
  # Only pinning this because there's no other way around it for Axlsx.
32
33
  # DO NOT unpin this.
@@ -43,5 +44,7 @@ Gem::Specification.new do |s|
43
44
  s.add_dependency 'pg'
44
45
  s.add_dependency 'redis'
45
46
  s.add_dependency 'rubyzip'
47
+ s.add_dependency 'state_machines'
48
+ s.add_dependency 'state_machines-activerecord'
46
49
  s.add_dependency 'zip-zip'
47
50
  end
@@ -67,7 +67,7 @@ module Marty::Diagnostic
67
67
  'status' => true,
68
68
  'consistent' => true
69
69
  },
70
- 'Git' => {
70
+ 'Root Git' => {
71
71
  'description' => git,
72
72
  'status' => true,
73
73
  'consistent' => true
@@ -29,63 +29,77 @@ module Gemini
29
29
  gen_mcfly_lookup :lookup, {
30
30
  entity: true,
31
31
  note_rate: false
32
- }
32
+ }, to_hash: true
33
+
33
34
  gen_mcfly_lookup :lookup_p, {
34
35
  entity: true,
35
36
  note_rate: false
36
- }, private: true
37
+ }, to_hash: false
38
+
37
39
  gen_mcfly_lookup :clookup, {
38
40
  entity: true,
39
41
  note_rate: false
40
- }, cache: true
42
+ }, cache: true, to_hash: true
43
+
41
44
  gen_mcfly_lookup :clookup_p, {
42
45
  entity: true,
43
46
  note_rate: false
44
- }, cache: true, private: true
47
+ }, cache: true, to_hash: false
48
+
45
49
  gen_mcfly_lookup :lookupn, {
46
50
  entity: true,
47
51
  note_rate: false
48
- }, mode: nil
52
+ }, mode: nil, to_hash: true
53
+
49
54
  gen_mcfly_lookup :lookupn_p, {
50
55
  entity: true,
51
56
  note_rate: false
52
- }, private: true, mode: nil
57
+ }, to_hash: false, mode: nil
58
+
53
59
  gen_mcfly_lookup :clookupn, {
54
60
  entity: true,
55
61
  note_rate: false
56
- }, cache: true, mode: nil
62
+ }, cache: true, mode: nil, to_hash: true
63
+
57
64
  gen_mcfly_lookup :clookupn_p, {
58
65
  entity: true,
59
66
  note_rate: false
60
- }, cache: true, private: true, mode: nil
67
+ }, cache: true, to_hash: false, mode: nil
61
68
 
62
- mcfly_lookup :a_func, sig: 3 do
69
+ mcfly_lookup :a_func, sig: 3, to_hash: true do
63
70
  |pt, e_id, bc_id|
64
71
  where(entity_id: e_id, bud_category_id: bc_id).
65
72
  order(:settlement_mm)
66
73
  end
67
74
 
68
- mcfly_lookup :b_func, sig: [3, 4] do
75
+ mcfly_lookup :b_func, sig: [3, 4], to_hash: true do
76
+ |pt, e_id, bc_id, mm = nil|
77
+ q = where(entity_id: e_id, bud_category_id: bc_id)
78
+ q = q.where(settlement_mm: mm) if mm
79
+ q.order(:settlement_mm).first
80
+ end
81
+
82
+ mcfly_lookup :c_func, sig: [3, 4], to_hash: false do
69
83
  |pt, e_id, bc_id, mm = nil|
70
84
  q = where(entity_id: e_id, bud_category_id: bc_id)
71
85
  q = q.where(settlement_mm: mm) if mm
72
86
  q.order(:settlement_mm).first
73
87
  end
74
88
 
75
- mcfly_lookup :a_func_p, sig: 3, private: true do
89
+ mcfly_lookup :a_func_p, sig: 3, to_hash: false do
76
90
  |pt, e_id, bc_id|
77
91
  where(entity_id: e_id, bud_category_id: bc_id).
78
92
  order(:settlement_mm)
79
93
  end
80
94
 
81
- mcfly_lookup :b_func_p, sig: [3, 4], private: true do
95
+ mcfly_lookup :b_func_p, sig: [3, 4], to_hash: false do
82
96
  |pt, e_id, bc_id, mm = nil|
83
97
  q = where(entity_id: e_id, bud_category_id: bc_id)
84
98
  q = q.where(settlement_mm: mm) if mm
85
99
  q.order(:settlement_mm)
86
100
  end
87
101
 
88
- cached_mcfly_lookup :ca_func, sig: 3 do
102
+ cached_mcfly_lookup :ca_func, sig: 3, to_hash: true do
89
103
  |pt, e_id, bc_id|
90
104
  where(entity_id: e_id, bud_category_id: bc_id).
91
105
  order(:settlement_mm)
@@ -34,6 +34,68 @@ class Gemini::Helper
34
34
  idh.values.map { |h| h['result'] }
35
35
  end
36
36
 
37
+ def self.pr_wait(ids)
38
+ idh = ids.each_with_object({}) do |id, h|
39
+ h[id] = false
40
+ end
41
+ timeout = 60
42
+ all_done = false
43
+ loop do
44
+ idh.each do |id, v|
45
+ next if v
46
+ p = Marty::Promise.uncached { Marty::Promise.find_by(id: id) }
47
+ idh[id] = p.result if p.status
48
+ end
49
+ all_done = idh.values.all? { |v| v }
50
+ break if all_done || timeout == 0
51
+ timeout -= 1
52
+ sleep 1
53
+ end
54
+ raise "DID NOT FINISH" unless all_done
55
+ idh.values.map { |h| h['result'] }
56
+ end
57
+
58
+ def self.promise_test(job_title)
59
+ sleep 5
60
+ pps = [1, 2, 3].map do |id|
61
+ new_title = job_title + ' ' + id.to_s
62
+ Marty::Promises::Ruby::Create.call(
63
+ module_name: 'Gemini::Helper',
64
+ method_name: 'promise_test_1',
65
+ method_args: [new_title],
66
+ params: {
67
+ p_title: new_title,
68
+ _user_id: 1,
69
+ _parent_id: ENV['__promise_id']&.to_i
70
+ }.compact
71
+ ).as_json.values.first.first
72
+ end
73
+ pr_wait(pps)
74
+ end
75
+
76
+ def self.promise_test_1(job_title)
77
+ pps = [1, 2, 3].map do |id|
78
+ new_title = job_title + ' ' + id.to_s
79
+ sleep 2
80
+ Marty::Promises::Ruby::Create.call(
81
+ module_name: 'Gemini::Helper',
82
+ method_name: 'promise_test_2',
83
+ method_args: [new_title],
84
+ params: {
85
+ p_title: new_title,
86
+ _user_id: 1,
87
+ _parent_id: ENV['__promise_id']&.to_i
88
+ }.compact
89
+ ).as_json.values.first.first
90
+ end
91
+ pr_wait(pps)
92
+ end
93
+
94
+ def self.promise_test_2(job_title)
95
+ sleep(5, job_title)
96
+ job_title
97
+ end
98
+
37
99
  # Just for testing
38
100
  delorean_fn :to_csv, sig: [1, 2] do
39
101
  |*args|
@@ -0,0 +1,224 @@
1
+ require 'spec_helper'
2
+
3
+ feature 'Notifications spec', js: true do
4
+ before do
5
+ populate_test_users
6
+
7
+ ::Marty::Notifications::Config.create!(
8
+ event_type: event_type,
9
+ recipient: user1,
10
+ delivery_type: :web,
11
+ state: :on,
12
+ text: 'Please contact the customer'
13
+ )
14
+
15
+ Marty::Notifications::Create.call(
16
+ event_type: event_type,
17
+ text: 'Test limit is 1000'
18
+ )
19
+ end
20
+
21
+ let(:user1) do
22
+ Marty::User.find_by(login: :marty)
23
+ end
24
+
25
+ let(:user2) do
26
+ Marty::User.find_by(login: :viewer1)
27
+ end
28
+
29
+ let(:event_type) do
30
+ 'API reached the limit'
31
+ end
32
+
33
+ let(:notification_grid) do
34
+ netzke_find 'grid_view'
35
+ end
36
+
37
+ context 'as admin' do
38
+ before do
39
+ log_in_as('marty')
40
+ end
41
+
42
+ describe 'Notifications pop-up' do
43
+ it 'shows notifications' do
44
+ notification_link = find 'a[data-qtip="Show notifications"]'
45
+ expect(notification_link.text).to eq '1'
46
+
47
+ notification_link.click
48
+
49
+ wait_for_ajax
50
+
51
+ expect(notification_grid.row_count).to eq 1
52
+ values = notification_grid.get_row_vals 1
53
+
54
+ expect(values['notification__event_type']).to eq 'API reached the limit'
55
+
56
+ expect(values['text']).to eq(
57
+ 'Test limit is 1000, Please contact the customer'
58
+ )
59
+
60
+ expect(values['error_text']).to eq ''
61
+
62
+ close_btn = find 'div[data-qtip="Close dialog"]'
63
+ close_btn.click
64
+
65
+ # Check that no unread notifications left
66
+ notification_link = find 'a[data-qtip="Show notifications"]'
67
+ expect(notification_link.text).to eq ''
68
+ end
69
+ end
70
+
71
+ describe 'Configuration grid' do
72
+ before do
73
+ press('System')
74
+ press('Notifications')
75
+ press('User Notification Rules')
76
+
77
+ wait_for_ajax
78
+ end
79
+
80
+ let(:grid) do
81
+ netzke_find 'notifications_config_view'
82
+ end
83
+
84
+ let(:state_select) do
85
+ netzke_find('state', 'combobox')
86
+ end
87
+
88
+ let(:event_type_select) do
89
+ netzke_find('event_type', 'combobox')
90
+ end
91
+
92
+ let(:recipient_select) do
93
+ netzke_find('recipient__name', 'combobox')
94
+ end
95
+
96
+ let(:delivery_type_select) do
97
+ netzke_find('delivery_type', 'combobox')
98
+ end
99
+
100
+ it 'allows to view and edit configuration' do
101
+ expect(grid.row_count).to eq 1
102
+
103
+ by 'turns the rule off' do
104
+ values = grid.get_row_vals 1
105
+ expect(values['state']).to eq 'on'
106
+
107
+ grid.select_row 1
108
+ press 'Edit'
109
+
110
+ state_select.select_values 'off'
111
+
112
+ press 'OK'
113
+
114
+ wait_for_ajax
115
+
116
+ values = grid.get_row_vals 1
117
+ expect(values['state']).to eq 'off'
118
+ end
119
+
120
+ and_by 'adds new rule' do
121
+ press 'Add'
122
+
123
+ event_type_select.select_values 'API reached the limit'
124
+
125
+ recipient_select.click
126
+ recipient_select.select_values 'admin1 admin1'
127
+
128
+ delivery_type_select.select_values 'web'
129
+
130
+ fill_in :text, with: 'example text'
131
+
132
+ press 'OK'
133
+
134
+ wait_for_ajax
135
+
136
+ expect(grid.row_count).to eq 2
137
+
138
+ values = grid.get_row_vals 1
139
+ expect(values['state']).to eq 'on'
140
+ expect(values['text']).to eq 'example text'
141
+ expect(values['recipient__name']).to eq 'admin1 admin1'
142
+ end
143
+
144
+ and_by "doesn't allow to duplicate the rule with the same delivery_type" do
145
+ press 'Add'
146
+
147
+ event_type_select.select_values 'API reached the limit'
148
+
149
+ recipient_select.click
150
+ recipient_select.select_values 'admin1 admin1'
151
+
152
+ delivery_type_select.select_values 'web'
153
+
154
+ fill_in :text, with: 'example text'
155
+
156
+ press 'OK'
157
+
158
+ expect(page).to have_content 'Delivery type has already been taken'
159
+
160
+ press 'Cancel'
161
+ end
162
+
163
+ and_by 'deletes rule' do
164
+ row_count = grid.row_count
165
+
166
+ grid.select_row 1
167
+ press 'Delete'
168
+ press 'Yes'
169
+
170
+ wait_for_ajax
171
+
172
+ expect(grid.row_count).to eq(row_count - 1)
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ context 'as viewer' do
179
+ before do
180
+ ::Marty::Notifications::Config.create!(
181
+ event_type: event_type,
182
+ recipient: user2,
183
+ delivery_type: :web,
184
+ state: :on,
185
+ text: 'Please notify your manager'
186
+ )
187
+
188
+ Marty::Notifications::Create.call(
189
+ event_type: event_type,
190
+ text: 'Test limit is 1000'
191
+ )
192
+
193
+ log_in_as('viewer1')
194
+ end
195
+
196
+ it 'shows notifications' do
197
+ notification_link = find 'a[data-qtip="Show notifications"]'
198
+ expect(notification_link.text).to eq '1'
199
+
200
+ notification_link.click
201
+
202
+ wait_for_ajax
203
+
204
+ expect(notification_grid.row_count).to eq 1
205
+ values = notification_grid.get_row_vals 1
206
+
207
+ expect(values['notification__event_type']).to eq 'API reached the limit'
208
+
209
+ expect(values['text']).to eq(
210
+ 'Test limit is 1000, Please notify your manager'
211
+ )
212
+ end
213
+
214
+ it "doesn't have access to notifications config" do
215
+ visit '#notifications_config_view'
216
+ expect(page).to have_content "You don't have permissions to read data"
217
+ end
218
+
219
+ it "doesn't have access to all notification messages" do
220
+ visit '#notifications_deliveries_view'
221
+ expect(page).to have_content "You don't have permissions to read data"
222
+ end
223
+ end
224
+ end