marty 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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