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
@@ -109,7 +109,19 @@ LOGGER:
109
109
  result = Gemini::Helper.testlog('message', [msgid]) &&
110
110
  Gemini::Helper.testaction('message %d', msgid) && msgid
111
111
  EOS
112
-
112
+ NAME_L = 'PromiseL'
113
+ SCRIPT_L = <<EOS
114
+ Node:
115
+ jobs = [1, 2, 3]
116
+ job_title =? 'base'
117
+ base_attr = [ (Node(p_title = ('%s %s' % [job_title, j]),
118
+ job_title = ('%s %s' % [job_title, j])) | "run0") == true
119
+ for j in jobs ]
120
+ run0 = [ (Node(p_title = ('%s %s' % [job_title, j]),
121
+ job_title = ('%s %s' % [job_title, j])) | "run1") == true
122
+ for j in jobs ]
123
+ run1 = Gemini::Helper.sleep(5, job_title)
124
+ EOS
113
125
  NAME_M = 'PromiseM'
114
126
  SCRIPT_M = <<EOS
115
127
  Node:
@@ -160,6 +172,7 @@ def promise_bodies
160
172
  NAME_I => SCRIPT_I,
161
173
  NAME_J => SCRIPT_J,
162
174
  NAME_K => SCRIPT_K,
175
+ NAME_L => SCRIPT_L,
163
176
  NAME_M => SCRIPT_M,
164
177
  NAME_N => SCRIPT_N,
165
178
  }
@@ -46,6 +46,7 @@ A:
46
46
 
47
47
  a_func = Gemini::FannieBup.a_func('infinity', e_id, bc_id)
48
48
  b_func = Gemini::FannieBup.b_func('infinity', e_id, bc_id, 12)
49
+ c_func = Gemini::FannieBup.c_func('infinity', e_id, bc_id, 12)
49
50
  EOF
50
51
  errscript = <<EOF
51
52
  Err:
@@ -121,22 +122,23 @@ describe 'McflyModel' do
121
122
  it 'lookup mode default' do
122
123
  a1 = @engine.evaluate('A', 'lookup', params)
123
124
  a2 = @engine.evaluate('A', 'clookup', params)
124
- expect(a1).to eq(a2) # cache/non return same
125
- expect(a1.class).to eq(OpenStruct) # mode default so return OS
126
- expect(a2.class).to eq(OpenStruct)
125
+ expect(a1).to eq(a2) # cache/non return same
126
+ expect(a1.class).to eq(Hash) # mode default so return hash
127
+ expect(a2.class).to eq(Hash)
127
128
 
128
129
  # check that keys are non mcfly non uniqueness
129
- expect(a1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
130
+ expect(a1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
130
131
  end
131
132
 
132
133
  it 'lookup non generated' do
133
134
  # a1 will be AR Relations
134
- # b1 will be OpenStructs because the b fns return #first
135
+ # b1 will be hash because the b fns return #first
135
136
  e_id = Gemini::Entity.where(name: 'PLS').first.id
136
137
  bc_id = Gemini::BudCategory.where(name: 'Conv Fixed 20').first.id
137
138
  p = { 'e_id' => e_id, 'bc_id' => bc_id }
138
139
  a1 = @engine.evaluate('A', 'a_func', p)
139
140
  b1 = @engine.evaluate('A', 'b_func', p)
141
+ c1 = @engine.evaluate('A', 'c_func', p)
140
142
 
141
143
  # all return relations
142
144
  expect(ActiveRecord::Relation === a1).to be_truthy
@@ -150,10 +152,15 @@ describe 'McflyModel' do
150
152
  # a1 is AR but still missing the FK entity_id so will raise
151
153
  expect { a1.first.entity }.to raise_error(/missing attribute: entity_id/)
152
154
 
153
- expect(b1.class).to eq(OpenStruct)
155
+ expect(b1.class).to eq(Hash)
154
156
 
155
157
  # make sure b1 has correct keys
156
- expect(b1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
158
+ expect(b1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
159
+
160
+ expect(c1.class).to eq(OpenStruct)
161
+
162
+ # make sure c1 has correct keys
163
+ expect(c1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
157
164
  end
158
165
 
159
166
  it 'lookup mode nil' do
@@ -93,6 +93,127 @@ describe Marty::Promise, slow: true, retry: 3 do
93
93
  expect(log.message).to eq 'was called'
94
94
  end
95
95
 
96
+ it 'can cancel jobs' do
97
+ run_ruby_job = lambda do |title1, title2|
98
+ title = title1 + ' ' + title2
99
+ Marty::Promises::Ruby::Create.call(
100
+ module_name: 'Gemini::Helper',
101
+ method_name: 'promise_test',
102
+ method_args: [title2],
103
+ params: {
104
+ p_title: title,
105
+ _user_id: 1,
106
+ }
107
+ )
108
+ end
109
+ engine = Marty::ScriptSet.new.get_engine(NAME_L)
110
+ run_delorean_job = lambda do |title1, title2|
111
+ engine.background_eval('Node', { 'p_title' => title1 + ' ' + title2,
112
+ 'job_title' => title2 }, ['base_attr'])
113
+ end
114
+
115
+ aggregate_failures do
116
+ [['Ruby', run_ruby_job],
117
+ ['PromiseL', run_delorean_job]].each do |title1, runner|
118
+ # first run with no cancel. make sure the test parts work as expected
119
+ title2 = 'first run'
120
+ x = runner.call(title1, title2)
121
+
122
+ base_p = nil
123
+ timeout = 60
124
+ # wait until base promise completes
125
+ loop do
126
+ base_p = Marty::Promise.find_by("title like '#{title1}%'")
127
+ break if p&.status || timeout == 0
128
+
129
+ timeout -= 1
130
+ sleep 1
131
+ end
132
+ expect(base_p.is_a?(Marty::Promise)).to be_truthy
133
+ expect(base_p.status).to be_truthy
134
+ expect(timeout).to be < 55
135
+
136
+ # count promises that ran
137
+ ps = Marty::Promise.where("title like '#{title2} %' or "\
138
+ "title = '#{title1} #{title2}'").
139
+ pluck(:id, :title, :end_dt, :status, :result)
140
+
141
+ # this check could fail on rare occasion due to the fact that
142
+ # delayed jobs sometimes run twice
143
+ expect(ps.count).to eq(13)
144
+
145
+ # check status
146
+ expect(ps.all? { |p| p[4] }).to be_truthy
147
+
148
+ # check the names
149
+ exp_pnames = [
150
+ 'first run 1', 'first run 1 1', 'first run 1 2', 'first run 1 3',
151
+ 'first run 2', 'first run 2 1', 'first run 2 2', 'first run 2 3',
152
+ 'first run 3', 'first run 3 1', 'first run 3 2', 'first run 3 3'
153
+ ]
154
+
155
+ expect(ps.map { |p| p[1] }.reject { |s| s.starts_with?(title1) }.sort).
156
+ to eq(exp_pnames)
157
+
158
+ # make sure the log was written by the leaf jobs. (uniq because
159
+ # rarely jobs run twice due to race condition -- see promise.rb:126)
160
+ exp_log = ['first run 1 1', 'first run 1 2', 'first run 1 3',
161
+ 'first run 2 1', 'first run 2 2', 'first run 2 3',
162
+ 'first run 3 1', 'first run 3 2', 'first run 3 3']
163
+ logs = Marty::Log.all.pluck(:details).map { |d| d['label'] }.sort.uniq
164
+ expect(logs).to eq(exp_log)
165
+
166
+ Marty::Promise.where("title like '#{title2}%'").destroy_all
167
+ Marty::Log.where("details->>'label' like '#{title2}%'").destroy_all
168
+
169
+ # run with early cancel
170
+ cancel_with_checks(runner, title1, '2nd run', '1', ps.count,
171
+ exp_log.count)
172
+
173
+ # run with later cancel
174
+ cancel_with_checks(runner, title1, '3rd run', '1 1', ps.count,
175
+ exp_log.count)
176
+
177
+ # some workers may die because we deleted the promises,
178
+ # so restart them
179
+ stop_delayed_job
180
+ start_delayed_job
181
+ end
182
+ end
183
+ end
184
+
185
+ def cancel_with_checks(runner, title1, title2, cancel_name, cnt1, cnt2)
186
+ testinfo = "#{title1} #{title2}"
187
+ runner.call(title1, title2)
188
+
189
+ # wait for indicated job and cancel
190
+ timeout = 30
191
+ title_where = "title = '#{title2} #{cancel_name}'"
192
+ until timeout == 0 || (p = Marty::Promise.find_by(title_where))
193
+ sleep 1
194
+ timeout -= 1
195
+ end
196
+ expect(p).to be_a(Marty::Promise), testinfo
197
+ Marty::Promises::Cancel.call(p.id)
198
+
199
+ # count the logs that were generated, should be less
200
+ l = Marty::Log.where("details->>'label' like '#{title2}%'").uniq
201
+ expect(l.count).to be < cnt2, testinfo
202
+
203
+ ps = Marty::Promise.where("title like '#{title2}%' or "\
204
+ "title = '#{title1} #{title2}'").
205
+ pluck(:job_id, :id, :title, :end_dt, :status, :result)
206
+
207
+ # cancel should have stopped creation of promises
208
+ expect(ps.count).to be < cnt1, testinfo
209
+
210
+ # make sure all the promises have an error=Cancelled in result
211
+ errors = ps.map(&:last).map { |h| h['error'] }.to_set
212
+ expect(errors).to eq(['Cancelled'].to_set), testinfo
213
+ Marty::Promise.where("title like '#{title2}%'").destroy_all
214
+ Marty::Log.where("details->>'label' like '#{title2}%'").destroy_all
215
+ end
216
+
96
217
  it 'fails on exception' do
97
218
  expect(Marty::Promise.where(title: 'PromiseJ').exists?).to be false
98
219
 
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ module Marty
4
+ describe Notifications::Create do
5
+ before do
6
+ populate_test_users
7
+ end
8
+
9
+ let(:user1) do
10
+ Marty::User.find_by(login: :admin1)
11
+ end
12
+
13
+ let(:user2) do
14
+ Marty::User.find_by(login: :admin2)
15
+ end
16
+
17
+ let(:event_type) do
18
+ 'API reached the limit'
19
+ end
20
+
21
+ let!(:config) do
22
+ ::Marty::Notifications::Config.create!(
23
+ event_type: event_type,
24
+ recipient: user1,
25
+ delivery_type: :web,
26
+ state: :on,
27
+ text: 'Please contact the customer'
28
+ )
29
+ end
30
+
31
+ let!(:config2) do
32
+ ::Marty::Notifications::Config.create!(
33
+ event_type: event_type,
34
+ recipient: user2,
35
+ delivery_type: :web,
36
+ state: :on,
37
+ )
38
+ end
39
+
40
+ def create_notification
41
+ hash = described_class.call(
42
+ event_type: event_type,
43
+ text: 'Test limit is 1000'
44
+ )
45
+
46
+ Marty::Notifications::Notification.find(hash[:id])
47
+ end
48
+
49
+ it 'creates deliveries' do
50
+ notification = create_notification
51
+
52
+ expect(notification.deliveries.size).to eq 2
53
+
54
+ delivery1 = user1.notification_deliveries.find_by(
55
+ notification: notification
56
+ )
57
+
58
+ expect(delivery1.state).to eq 'sent'
59
+ expect(delivery1.text).to eq 'Please contact the customer'
60
+
61
+ delivery2 = user2.notification_deliveries.find_by(
62
+ notification: notification
63
+ )
64
+ expect(delivery2.state).to eq 'sent'
65
+ expect(delivery2.text).to eq ''
66
+ end
67
+
68
+ it "doesn't create delivery when config is off" do
69
+ config2.update!(state: :off)
70
+
71
+ notification = create_notification
72
+
73
+ expect(notification.deliveries.size).to eq 1
74
+
75
+ delivery2 = user2.notification_deliveries.find_by(
76
+ notification: notification
77
+ )
78
+
79
+ expect(delivery2).to_not be_present
80
+ end
81
+ end
82
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marty
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0
4
+ version: 8.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arman Bostani
@@ -14,8 +14,22 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2019-11-08 00:00:00.000000000 Z
17
+ date: 2019-11-26 00:00:00.000000000 Z
18
18
  dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: actioncable
21
+ requirement: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
19
33
  - !ruby/object:Gem::Dependency
20
34
  name: aws-sigv4
21
35
  requirement: !ruby/object:Gem::Requirement
@@ -212,6 +226,34 @@ dependencies:
212
226
  - - ">="
213
227
  - !ruby/object:Gem::Version
214
228
  version: '0'
229
+ - !ruby/object:Gem::Dependency
230
+ name: state_machines
231
+ requirement: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ type: :runtime
237
+ prerelease: false
238
+ version_requirements: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ - !ruby/object:Gem::Dependency
244
+ name: state_machines-activerecord
245
+ requirement: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ type: :runtime
251
+ prerelease: false
252
+ version_requirements: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - ">="
255
+ - !ruby/object:Gem::Version
256
+ version: '0'
215
257
  - !ruby/object:Gem::Dependency
216
258
  name: zip-zip
217
259
  requirement: !ruby/object:Gem::Requirement
@@ -248,6 +290,7 @@ files:
248
290
  - README.md
249
291
  - Rakefile
250
292
  - app/assets/javascripts/marty/application.js
293
+ - app/assets/javascripts/marty/cable.js
251
294
  - app/assets/javascripts/marty/codemirror/codemirror.js
252
295
  - app/assets/javascripts/marty/codemirror/mode/delorean/delorean.js
253
296
  - app/assets/javascripts/marty/codemirror/mode/javascript/javascript.js
@@ -257,7 +300,11 @@ files:
257
300
  - app/assets/stylesheets/marty/application.css
258
301
  - app/assets/stylesheets/marty/codemirror/codemirror.css
259
302
  - app/assets/stylesheets/marty/codemirror/delorean.css
303
+ - app/assets/stylesheets/marty/codemirror/notifications.css
260
304
  - app/assets/stylesheets/marty/dark_mode.css
305
+ - app/channels/application_cable/channel.rb
306
+ - app/channels/application_cable/connection.rb
307
+ - app/channels/marty/notification_channel.rb
261
308
  - app/components/marty/api_auth_view.rb
262
309
  - app/components/marty/api_config_view.rb
263
310
  - app/components/marty/api_log_view.rb
@@ -290,6 +337,10 @@ files:
290
337
  - app/components/marty/new_posting_form.rb
291
338
  - app/components/marty/new_posting_form/client/new_posting_form.js
292
339
  - app/components/marty/new_posting_window.rb
340
+ - app/components/marty/notifications/config_view.rb
341
+ - app/components/marty/notifications/deliveries_view.rb
342
+ - app/components/marty/notifications/grid_view.rb
343
+ - app/components/marty/notifications/window.rb
293
344
  - app/components/marty/panel.rb
294
345
  - app/components/marty/panel/client/panel.js
295
346
  - app/components/marty/posting_grid.rb
@@ -306,8 +357,8 @@ files:
306
357
  - app/components/marty/reporting.rb
307
358
  - app/components/marty/reporting/client/reporting.js
308
359
  - app/components/marty/schedule_jobs_dashboard.rb
309
- - app/components/marty/schedule_jobs_dashboard/client/schedule_jobs_dashboard.js
310
360
  - app/components/marty/schedule_jobs_grid.rb
361
+ - app/components/marty/schedule_jobs_grid/client/schedule_jobs_grid.js
311
362
  - app/components/marty/schedule_jobs_logs.rb
312
363
  - app/components/marty/schedule_jobs_logs/client/schedule_jobs_logs.js
313
364
  - app/components/marty/script_form.rb
@@ -322,6 +373,7 @@ files:
322
373
  - app/components/marty/simple_app/client/statusbar_ext.js
323
374
  - app/components/marty/tag_grid.rb
324
375
  - app/components/marty/user_view.rb
376
+ - app/components/marty/users/user_view.rb
325
377
  - app/controllers/marty/application_controller.rb
326
378
  - app/controllers/marty/components_controller.rb
327
379
  - app/controllers/marty/delayed_job_controller.rb
@@ -356,6 +408,11 @@ files:
356
408
  - app/models/marty/import_type.rb
357
409
  - app/models/marty/log.rb
358
410
  - app/models/marty/name_validator.rb
411
+ - app/models/marty/notifications.rb
412
+ - app/models/marty/notifications/config.rb
413
+ - app/models/marty/notifications/delivery.rb
414
+ - app/models/marty/notifications/event_type.rb
415
+ - app/models/marty/notifications/notification.rb
359
416
  - app/models/marty/pg_enum.rb
360
417
  - app/models/marty/posting.rb
361
418
  - app/models/marty/posting_type.rb
@@ -373,6 +430,13 @@ files:
373
430
  - app/services/marty/data_grid_view/save_grid.rb
374
431
  - app/services/marty/enums/report.rb
375
432
  - app/services/marty/jobs/schedule.rb
433
+ - app/services/marty/notifications/create.rb
434
+ - app/services/marty/notifications/create_deliveries.rb
435
+ - app/services/marty/notifications/process_delivery.rb
436
+ - app/services/marty/notifications/processors/email.rb
437
+ - app/services/marty/notifications/processors/sms.rb
438
+ - app/services/marty/notifications/processors/web.rb
439
+ - app/services/marty/promises/cancel.rb
376
440
  - app/services/marty/promises/delorean.rb
377
441
  - app/services/marty/promises/delorean/create.rb
378
442
  - app/services/marty/promises/ruby.rb
@@ -437,6 +501,10 @@ files:
437
501
  - db/migrate/512_add_promise_priority.rb
438
502
  - db/migrate/513_add_priority_to_promise_view.rb
439
503
  - db/migrate/514_remove_marty_events.rb
504
+ - db/migrate/519_create_marty_notifications_event_types.rb
505
+ - db/migrate/520_create_marty_notifications.rb
506
+ - db/migrate/521_create_marty_notifications_deliveries.rb
507
+ - db/migrate/522_create_marty_notifications_config.rb
440
508
  - db/seeds.rb
441
509
  - db/sql/lookup_grid_distinct_v1.sql
442
510
  - db/sql/query_grid_dir_v1.sql
@@ -1636,6 +1704,7 @@ files:
1636
1704
  - spec/features/javascripts/login.js.coffee
1637
1705
  - spec/features/jobs_dashboard_spec.rb
1638
1706
  - spec/features/log_view_spec.rb
1707
+ - spec/features/notifications_spec.rb
1639
1708
  - spec/features/reporting_spec.rb
1640
1709
  - spec/features/rule_spec.rb
1641
1710
  - spec/features/schedule_jobs_dashboard_spec.rb
@@ -1700,6 +1769,7 @@ files:
1700
1769
  - spec/requests/routes_spec.rb
1701
1770
  - spec/services/background_job/fetch_missing_in_schedule_cron_jobs_spec.rb
1702
1771
  - spec/services/jobs/schedule_spec.rb
1772
+ - spec/services/notifications/create_spec.rb
1703
1773
  - spec/spec_helper.rb
1704
1774
  - spec/support/chromedriver.rb
1705
1775
  - spec/support/components/netzke_combobox.rb