flapjack 0.8.10 → 0.8.11

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Gemfile +1 -1
  4. data/bin/flapjack +10 -1
  5. data/bin/flapjack-nagios-receiver +1 -2
  6. data/bin/simulate-failed-check +12 -4
  7. data/etc/flapjack_config.yaml.example +2 -1
  8. data/flapjack.gemspec +1 -0
  9. data/lib/flapjack/data/contact.rb +46 -26
  10. data/lib/flapjack/data/entity.rb +28 -0
  11. data/lib/flapjack/data/entity_check.rb +52 -11
  12. data/lib/flapjack/data/event.rb +9 -3
  13. data/lib/flapjack/data/notification_rule.rb +8 -0
  14. data/lib/flapjack/gateways/api.rb +0 -1
  15. data/lib/flapjack/gateways/api/entity_check_presenter.rb +2 -1
  16. data/lib/flapjack/gateways/email.rb +1 -2
  17. data/lib/flapjack/gateways/jabber.rb +3 -3
  18. data/lib/flapjack/gateways/jsonapi.rb +186 -38
  19. data/lib/flapjack/gateways/jsonapi/check_methods.rb +120 -0
  20. data/lib/flapjack/gateways/jsonapi/{entity_check_presenter.rb → check_presenter.rb} +7 -6
  21. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +61 -352
  22. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +117 -248
  23. data/lib/flapjack/gateways/jsonapi/medium_methods.rb +179 -0
  24. data/lib/flapjack/gateways/jsonapi/notification_rule_methods.rb +124 -0
  25. data/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods.rb +128 -0
  26. data/lib/flapjack/gateways/jsonapi/rack/json_params_parser.rb +4 -5
  27. data/lib/flapjack/gateways/jsonapi/report_methods.rb +143 -0
  28. data/lib/flapjack/gateways/web.rb +1 -0
  29. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +165 -101
  30. data/lib/flapjack/gateways/web/public/js/contacts.js +34 -46
  31. data/lib/flapjack/gateways/web/public/js/select2.js +232 -90
  32. data/lib/flapjack/gateways/web/public/js/select2.min.js +4 -4
  33. data/lib/flapjack/gateways/web/views/check.html.erb +11 -2
  34. data/lib/flapjack/processor.rb +6 -6
  35. data/lib/flapjack/version.rb +1 -1
  36. data/spec/lib/flapjack/data/entity_check_spec.rb +1 -1
  37. data/spec/lib/flapjack/data/event_spec.rb +10 -9
  38. data/spec/lib/flapjack/gateways/api/entity_methods_spec.rb +25 -25
  39. data/spec/lib/flapjack/gateways/api_spec.rb +23 -1
  40. data/spec/lib/flapjack/gateways/email_spec.rb +40 -2
  41. data/spec/lib/flapjack/gateways/jabber_spec.rb +1 -1
  42. data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +134 -0
  43. data/spec/lib/flapjack/gateways/jsonapi/{entity_check_presenter_spec.rb → check_presenter_spec.rb} +17 -17
  44. data/spec/lib/flapjack/gateways/jsonapi/contact_methods_spec.rb +27 -232
  45. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +217 -687
  46. data/spec/lib/flapjack/gateways/jsonapi/medium_methods_spec.rb +232 -0
  47. data/spec/lib/flapjack/gateways/jsonapi/notification_rule_methods_spec.rb +131 -0
  48. data/spec/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods_spec.rb +113 -0
  49. data/spec/lib/flapjack/gateways/jsonapi/report_methods_spec.rb +546 -0
  50. data/spec/lib/flapjack/gateways/jsonapi_spec.rb +10 -1
  51. data/spec/lib/flapjack/gateways/web_spec.rb +1 -0
  52. data/spec/support/jsonapi_helper.rb +62 -0
  53. metadata +36 -8
  54. data/lib/flapjack/gateways/jsonapi/entity_presenter.rb +0 -75
  55. data/spec/lib/flapjack/gateways/jsonapi/entity_presenter_spec.rb +0 -108
@@ -12,7 +12,7 @@ module Flapjack
12
12
 
13
13
  class JSONAPI < Sinatra::Base
14
14
 
15
- class EntityCheckPresenter
15
+ class CheckPresenter
16
16
 
17
17
  def initialize(entity_check)
18
18
  @entity_check = entity_check
@@ -24,6 +24,7 @@ module Flapjack
24
24
  'enabled' => @entity_check.enabled?,
25
25
  'summary' => @entity_check.summary,
26
26
  'details' => @entity_check.details,
27
+ 'perfdata' => @entity_check.perfdata,
27
28
  'in_unscheduled_maintenance' => @entity_check.in_unscheduled_maintenance?,
28
29
  'in_scheduled_maintenance' => @entity_check.in_scheduled_maintenance?,
29
30
  'last_update' => @entity_check.last_update,
@@ -32,7 +33,7 @@ module Flapjack
32
33
  'last_acknowledgement_notification' => @entity_check.last_notification_for_state(:acknowledgement)[:timestamp]}
33
34
  end
34
35
 
35
- def outages(start_time, end_time, options = {})
36
+ def outage(start_time, end_time, options = {})
36
37
  # hist_states is an array of hashes, with [state, timestamp, summary] keys
37
38
  hist_states = @entity_check.historical_states(start_time, end_time)
38
39
  return hist_states if hist_states.empty?
@@ -83,7 +84,7 @@ module Flapjack
83
84
  result
84
85
  end
85
86
 
86
- def unscheduled_maintenances(start_time, end_time)
87
+ def unscheduled_maintenance(start_time, end_time)
87
88
  # unsched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
88
89
  unsched_maintenance = @entity_check.maintenances(start_time, end_time,
89
90
  :scheduled => false)
@@ -98,7 +99,7 @@ module Flapjack
98
99
  start_in_unsched + unsched_maintenance
99
100
  end
100
101
 
101
- def scheduled_maintenances(start_time, end_time)
102
+ def scheduled_maintenance(start_time, end_time)
102
103
  # sched_maintenance is an array of hashes, with [duration, timestamp, summary] keys
103
104
  sched_maintenance = @entity_check.maintenances(start_time, end_time,
104
105
  :scheduled => true)
@@ -119,9 +120,9 @@ module Flapjack
119
120
  #
120
121
  # TODO test performance with larger data sets
121
122
  def downtime(start_time, end_time)
122
- sched_maintenances = scheduled_maintenances(start_time, end_time)
123
+ sched_maintenances = scheduled_maintenance(start_time, end_time)
123
124
 
124
- outs = outages(start_time, end_time)
125
+ outs = outage(start_time, end_time)
125
126
 
126
127
  total_secs = {}
127
128
  percentages = {}
@@ -18,28 +18,11 @@ module Flapjack
18
18
 
19
19
  module Helpers
20
20
 
21
- def find_contact(contact_id)
22
- contact = Flapjack::Data::Contact.find_by_id(contact_id, :logger => logger, :redis => redis)
23
- raise Flapjack::Gateways::JSONAPI::ContactNotFound.new(contact_id) if contact.nil?
24
- contact
25
- end
26
-
27
- def find_rule(rule_id)
28
- rule = Flapjack::Data::NotificationRule.find_by_id(rule_id, :logger => logger, :redis => redis)
29
- raise Flapjack::Gateways::JSONAPI::NotificationRuleNotFound.new(rule_id) if rule.nil?
30
- rule
31
- end
32
-
33
- def find_tags(tags)
34
- halt err(400, "no tags given") if tags.nil? || tags.empty?
35
- tags
36
- end
37
-
38
21
  def obtain_semaphore(resource)
39
22
  semaphore = nil
40
23
  strikes = 0
41
24
  begin
42
- semaphore = Flapjack::Data::Semaphore.new(resource, {:redis => redis, :expiry => 30})
25
+ semaphore = Flapjack::Data::Semaphore.new(resource, :redis => redis, :expiry => 30)
43
26
  rescue Flapjack::Data::Semaphore::ResourceLocked
44
27
  strikes += 1
45
28
  raise Flapjack::Gateways::JSONAPI::ResourceLocked.new(resource) unless strikes < 3
@@ -50,50 +33,32 @@ module Flapjack
50
33
  semaphore
51
34
  end
52
35
 
53
- def apply_json_patch(object_path, &block)
54
- ops = params[:ops]
36
+ def bulk_contact_operation(contact_ids, &block)
37
+ semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
55
38
 
56
- if ops.nil? || !ops.is_a?(Array)
57
- halt err(400, "Invalid JSON-Patch request")
39
+ contacts_by_id = contact_ids.inject({}) do |memo, contact_id|
40
+ # can't use find_contact here as that would halt immediately
41
+ memo[contact_id] = Flapjack::Data::Contact.find_by_id(contact_id, :redis => redis, :logger => logger)
42
+ memo
58
43
  end
59
44
 
60
- ops.each do |operation|
61
- linked = nil
62
- property = nil
63
-
64
- op = operation['op']
65
- operation['path'] =~ /\A\/#{object_path}\/0\/([^\/]+)(?:\/([^\/]+)(?:\/([^\/]+))?)?\z/
66
- if 'links'.eql?($1)
67
- linked = $2
68
-
69
- value = case op
70
- when 'add'
71
- operation['value']
72
- when 'remove'
73
- $3
74
- end
75
- elsif 'replace'.eql?(op)
76
- property = $1
77
- value = $3
78
- else
79
- next
80
- end
81
-
82
- yield(op, property, linked, value)
45
+ missing_ids = contacts_by_id.select {|k, v| v.nil? }.keys
46
+ unless missing_ids.empty?
47
+ semaphore.release
48
+ halt(404, "Contacts with ids #{missing_ids.join(', ')} were not found")
83
49
  end
50
+
51
+ block.call(contacts_by_id.select {|k, v| !v.nil? }.values)
52
+ semaphore.release
84
53
  end
85
54
 
86
55
  end
87
56
 
88
57
  def self.registered(app)
89
-
58
+ app.helpers Flapjack::Gateways::JSONAPI::Helpers
90
59
  app.helpers Flapjack::Gateways::JSONAPI::ContactMethods::Helpers
91
60
 
92
61
  app.post '/contacts' do
93
- pass unless is_json_request?
94
- content_type :json
95
- cors_headers
96
-
97
62
  contacts_data = params[:contacts]
98
63
 
99
64
  if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
@@ -126,23 +91,20 @@ module Flapjack
126
91
 
127
92
  ids = contacts_data.map {|c| c['id']}
128
93
  location(ids)
94
+ status 201
129
95
 
130
96
  contacts_data.map {|cd| cd['id']}.to_json
131
97
  end
132
98
 
133
- # Returns all (/contacts) or some (/contacts/1,2,3) or one (/contact/2) contact(s)
99
+ # Returns all (/contacts) or some (/contacts/1,2,3) or one (/contacts/2) contact(s)
134
100
  # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts
135
- app.get %r{/contacts(?:/)?([^/]+)?$} do
136
- content_type 'application/vnd.api+json'
137
- cors_headers
138
-
101
+ app.get %r{^/contacts(?:/)?([^/]+)?$} do
139
102
  requested_contacts = if params[:captures] && params[:captures][0]
140
103
  params[:captures][0].split(',').uniq
141
104
  else
142
105
  nil
143
106
  end
144
107
 
145
- #FIXME: do we need to url decode the ids? has rack or some middleware already done this?
146
108
  contacts = if requested_contacts
147
109
  Flapjack::Data::Contact.find_by_ids(requested_contacts, :logger => logger, :redis => redis)
148
110
  else
@@ -154,315 +116,62 @@ module Flapjack
154
116
  raise Flapjack::Gateways::JSONAPI::ContactsNotFound.new(requested_contacts)
155
117
  end
156
118
 
157
- linked_entity_data, linked_entity_ids = if contacts.empty?
158
- [[], []]
159
- else
160
- Flapjack::Data::Contact.entities_jsonapi(contacts.map(&:id), :redis => redis)
161
- end
162
-
163
- linked_media_data = []
164
- linked_media_ids = {}
165
- contacts.each do |contact|
166
- contact.media.keys.each do |medium|
167
- id = "#{contact.id}_#{medium}"
168
- interval = contact.media_intervals[medium].nil? ? nil : contact.media_intervals[medium].to_i
169
- rollup_threshold = contact.media_rollup_thresholds[medium].nil? ? nil : contact.media_rollup_thresholds[medium].to_i
170
- linked_media_ids[contact.id] = id
171
- linked_media_data <<
172
- { "id" => id,
173
- "type" => medium,
174
- "address" => contact.media[medium],
175
- "interval" => interval,
176
- "rollup_threshold" => rollup_threshold,
177
- "contact_id" => contact.id }
178
- end
179
- end
119
+ entity_ids = Flapjack::Data::Contact.entity_ids_for(contacts.map(&:id), :redis => redis)
180
120
 
181
121
  contacts_json = contacts.collect {|contact|
182
- contact.linked_entity_ids = linked_entity_ids[contact.id]
183
- contact.linked_media_ids = linked_media_ids[contact.id]
184
- contact.to_jsonapi
122
+ contact.to_jsonapi(:entity_ids => entity_ids[contact.id])
185
123
  }.join(", ")
186
124
 
187
- '{"contacts":[' + contacts_json + ']' +
188
- ',"linked":{"entities":' + linked_entity_data.to_json +
189
- ',"media":' + linked_media_data.to_json + '}}'
190
- end
191
-
192
- # Returns the core information about the specified contact
193
- # https://github.com/flpjck/flapjack/wiki/API#wiki-get_contacts_id
194
- app.get '/contacts/:contact_id' do
195
- content_type 'application/vnd.api+json'
196
- cors_headers
197
- contact = find_contact(params[:contact_id])
198
-
199
- entities = contact.entities.map {|e| e[:entity] }
200
-
201
- '{"contacts":[' + contact.to_jsonapi + ']' +
202
- ( entities.empty? ? '}' :
203
- ', "linked": {"entities":' + entities.values.to_json + '}}')
204
- end
205
-
206
- # Updates a contact
207
- app.put '/contacts/:contact_id' do
208
- cors_headers
209
- content_type :json
210
-
211
- contacts_data = params[:contacts]
212
-
213
- if contacts_data.nil? || !contacts_data.is_a?(Enumerable)
214
- halt err(422, "No valid contacts were submitted")
215
- end
216
-
217
- unless contacts_data.length == 1
218
- halt err(422, "Exactly one contact hash must be supplied.")
219
- end
220
-
221
- contact_data = contacts_data.first
222
-
223
- if contact_data['id'] && contact_data['id'].to_s != params[:contact_id]
224
- halt err(422, "ID, if supplied, must match URL")
225
- end
226
-
227
- contact = find_contact(params[:contact_id])
228
- #contact_data = hashify('first_name', 'last_name', 'email', 'media', 'tags') {|k| [k, params[k]]}
229
- logger.debug("contact_data: #{contact_data}")
230
- contact.update(contact_data)
231
-
232
- contact.to_jsonapi
233
- end
234
-
235
- # TODO this should build up all data, verify entities exist, etc.
236
- # before applying any changes
237
- # TODO generalise JSON-Patch data parsing code
238
- app.patch '/contacts/:contact_id' do
239
- pass unless is_jsonpatch_request?
240
- content_type :json
241
- cors_headers
242
-
243
- contact = find_contact(params[:contact_id])
244
-
245
- apply_json_patch('contacts') do |op, property, linked, value|
246
- case op
247
- when 'replace'
248
- if ['first_name', 'last_name', 'email'].include?(property)
249
- contact.update(property => value)
250
- end
251
- when 'add'
252
- logger.debug "patch add operation. linked: #{linked}"
253
- if 'entities'.eql?(linked)
254
- entity = Flapjack::Data::Entity.find_by_id(value, :redis => redis)
255
- logger.debug "adding this entity: #{entity}"
256
- contact.add_entity(entity) unless entity.nil?
257
- end
258
- when 'remove'
259
- if 'entities'.eql?(linked)
260
- entity = Flapjack::Data::Entity.find_by_id(value, :redis => redis)
261
- contact.remove_entity(entity) unless entity.nil?
125
+ '{"contacts":[' + contacts_json + ']}'
126
+ end
127
+
128
+ app.patch '/contacts/:id' do
129
+ bulk_contact_operation(params[:id].split(',')) do |contacts|
130
+ contacts.each do |contact|
131
+ apply_json_patch('contacts') do |op, property, linked, value|
132
+ case op
133
+ when 'replace'
134
+ if ['first_name', 'last_name', 'email'].include?(property)
135
+ contact.update(property => value)
136
+ end
137
+ when 'add'
138
+ case linked
139
+ when 'entities'
140
+ entity = Flapjack::Data::Entity.find_by_id(value, :redis => redis)
141
+ contact.add_entity(entity) unless entity.nil?
142
+ when 'notification_rules'
143
+ notification_rule = Flapjack::Data::NotificationRule.find_by_id(value, :redis => redis)
144
+ unless notification_rule.nil?
145
+ contact.grab_notification_rule(notification_rule)
146
+ end
147
+ # when 'media' # not supported yet due to id brokenness
148
+ end
149
+ when 'remove'
150
+ case linked
151
+ when 'entities'
152
+ entity = Flapjack::Data::Entity.find_by_id(value, :redis => redis)
153
+ contact.remove_entity(entity) unless entity.nil?
154
+ when 'notification_rules'
155
+ notification_rule = Flapjack::Data::NotificationRule.find_by_id(value, :redis => redis)
156
+ unless notification_rule.nil?
157
+ contact.delete_notification_rule(notification_rule)
158
+ end
159
+ # when 'media' # not supported yet due to id brokenness
160
+ end
161
+ end
262
162
  end
263
163
  end
264
164
  end
265
165
 
266
- # will need to be 200 and return contact.to_jsonapi
267
- # if updated_at changes, or Etag, when those are introduced
268
- status 204
269
- end
270
-
271
- # Deletes a contact
272
- app.delete '/contacts/:contact_id' do
273
- cors_headers
274
- semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
275
- contact = find_contact(params[:contact_id])
276
- contact.delete!
277
- semaphore.release
278
166
  status 204
279
167
  end
280
168
 
281
- app.post '/media' do
282
- pass unless is_json_request?
283
- content_type :json
284
- cors_headers
285
-
286
- media_data = params[:media]
287
-
288
- if media_data.nil? || !media_data.is_a?(Enumerable)
289
- halt err(422, "No valid media were submitted")
290
- end
291
-
292
- unless media_data.all? {|m| m['id'].nil? }
293
- halt err(422, "Media creation cannot include IDs")
169
+ # Delete one or more contacts
170
+ app.delete '/contacts/:id' do
171
+ bulk_contact_operation(params[:id].split(',')) do |contacts|
172
+ contacts.each {|contact| contact.delete!}
294
173
  end
295
174
 
296
- semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
297
-
298
- contacts = media_data.inject({}) {|memo, medium_data|
299
- contact_id = medium_data['contact_id']
300
- if contact_id.nil?
301
- semaphore.release
302
- halt err(422, "Media data must include 'contact_id'")
303
- end
304
- next memo if memo.has_key?(contact_id)
305
- contact = Flapjack::Data::Contact.find_by_id(contact_id, :redis => redis)
306
- if contact.nil?
307
- semaphore.release
308
- halt err(422, "Contact id:'#{contact_id}' could not be loaded")
309
- end
310
- memo[contact_id] = contact
311
- memo
312
- }
313
-
314
- media_data.each do |medium_data|
315
- contact = contacts[medium_data['contact_id']]
316
- type = medium_data['type']
317
- contact.set_address_for_media(type, medium_data['address'])
318
- contact.set_interval_for_media(type, medium_data['interval'])
319
- contact.set_rollup_threshold_for_media(type, medium_data['rollup_threshold'])
320
- medium_data['id'] = "#{contact.id}_#{type}"
321
- end
322
-
323
- semaphore.release
324
-
325
- '{"media":' + media_data.to_json + '}'
326
- end
327
-
328
- app.patch '/media/:media_id' do
329
- pass unless is_jsonpatch_request?
330
- content_type :json
331
- cors_headers
332
-
333
- media_id = params[:media_id]
334
- media_id =~ /\A(.+)_(email|sms|jabber)\z/
335
-
336
- contact_id = $1
337
- type = $2
338
-
339
- halt err(422, "Could not get contact_id from media_id") if contact_id.nil?
340
- halt err(422, "Could not get type from media_id") if type.nil?
341
-
342
- contact = find_contact(contact_id)
343
-
344
- apply_json_patch('media') do |op, property, linked, value|
345
- if 'replace'.eql?(op)
346
- case property
347
- when 'address'
348
- contact.set_address_for_media(type, value)
349
- when 'interval'
350
- contact.set_interval_for_media(type, value)
351
- when 'rollup_threshold'
352
- contact.set_rollup_threshold_for_media(type, value)
353
- end
354
- end
355
- end
356
-
357
- status 204
358
- end
359
-
360
- app.get '/notification_rules/:id' do
361
- content_type :json
362
- cors_headers
363
-
364
- '{"notification_rules":[' +
365
- find_rule(params[:id]).to_json +
366
- ']}'
367
- end
368
-
369
- # Creates a notification rule or rules for a contact
370
- app.post '/notification_rules' do
371
- content_type :json
372
- cors_headers
373
-
374
- rules_data = params[:notification_rules]
375
-
376
- if rules_data.nil? || !rules_data.is_a?(Enumerable)
377
- halt err(422, "No valid notification rules were submitted")
378
- end
379
-
380
- errors = []
381
- rules_data.each do |rule_data|
382
- errors << Flapjack::Data::NotificationRule.prevalidate_data(symbolize(rule_data), {:logger => logger})
383
- end
384
- errors.compact!
385
-
386
- unless errors.nil? || errors.empty?
387
- halt err(422, *errors)
388
- end
389
-
390
- rules = []
391
- errors = []
392
- rules_data.each do |rule_data|
393
- rule_data = symbolize(rule_data)
394
- contact = find_contact(rule_data.delete(:contact_id))
395
- rule_or_errors = contact.add_notification_rule(rule_data, :logger => logger)
396
- if rule_or_errors.respond_to?(:critical_media)
397
- rules << rule_or_errors
398
- else
399
- errors << rule_or_errors
400
- end
401
- end
402
-
403
- if rules.empty?
404
- halt err(422, *errors)
405
- else
406
- if errors.empty?
407
- status 201
408
- else
409
- logger.warn("Errors during bulk notification rules creation: " + errors.join(', '))
410
- status 200
411
- end
412
- end
413
- ids = rules.map {|r| r.id}
414
- location(ids)
415
- '{"notification_rules":[' +
416
- rules.map {|r| r.to_json}.join(',') +
417
- ']}'
418
- end
419
-
420
- # Updates a notification rule
421
- app.put('/notification_rules/:id') do
422
- content_type :json
423
- cors_headers
424
-
425
- rules_data = params[:notification_rules]
426
-
427
- if rules_data.nil? || !rules_data.is_a?(Enumerable)
428
- halt err(422, "No valid notification rules were submitted")
429
- end
430
-
431
- unless rules_data.length == 1
432
- halt err(422, "Exactly one notification rules hash must be supplied.")
433
- end
434
-
435
- rule_data = rules_data.first
436
-
437
- if rule_data['id'] && rule_data['id'].to_s != params[:id]
438
- halt err(422, "ID, if supplied, must match URL")
439
- end
440
-
441
- rule = find_rule(params[:id])
442
- contact = find_contact(rule.contact_id)
443
-
444
- supplied_contact = rule_data.delete('contact_id')
445
- if supplied_contact && supplied_contact != contact.id
446
- halt err(422, "contact_id cannot be modified")
447
- end
448
-
449
- errors = rule.update(symbolize(rule_data), :logger => logger)
450
-
451
- unless errors.nil? || errors.empty?
452
- halt err(422, *errors)
453
- end
454
- '{"notification_rules":[' +
455
- rule.to_json +
456
- ']}'
457
- end
458
-
459
- # Deletes a notification rule
460
- app.delete('/notification_rules/:id') do
461
- cors_headers
462
- rule = find_rule(params[:id])
463
- logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
464
- contact = find_contact(rule.contact_id)
465
- contact.delete_notification_rule(rule)
466
175
  status 204
467
176
  end
468
177