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
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/contact'
6
+ require 'flapjack/data/notification_rule'
7
+
8
+ module Flapjack
9
+
10
+ module Gateways
11
+
12
+ class JSONAPI < Sinatra::Base
13
+
14
+ module NotificationRuleMethods
15
+
16
+ # module Helpers
17
+ # end
18
+
19
+ def self.registered(app)
20
+ app.helpers Flapjack::Gateways::JSONAPI::Helpers
21
+ # app.helpers Flapjack::Gateways::JSONAPI::NotificationRuleMethods::Helpers
22
+
23
+ # Creates a notification rule or rules for a contact
24
+ app.post '/contacts/:contact_id/notification_rules' do
25
+ rules_data = params[:notification_rules]
26
+
27
+ if rules_data.nil? || !rules_data.is_a?(Enumerable)
28
+ halt err(422, "No valid notification rules were submitted")
29
+ end
30
+
31
+ contact = find_contact(params[:contact_id])
32
+
33
+ errors = []
34
+ rules_data.each do |rule_data|
35
+ errors << Flapjack::Data::NotificationRule.prevalidate_data(symbolize(rule_data), {:logger => logger})
36
+ end
37
+ errors.compact!
38
+
39
+ unless errors.nil? || errors.empty?
40
+ halt err(422, *errors)
41
+ end
42
+
43
+ rules = []
44
+ errors = []
45
+ rules_data.each do |rule_data|
46
+ rule_data = symbolize(rule_data)
47
+ rule_or_errors = contact.add_notification_rule(rule_data, :logger => logger)
48
+ if rule_or_errors.respond_to?(:critical_media)
49
+ rules << rule_or_errors
50
+ else
51
+ errors << rule_or_errors
52
+ end
53
+ end
54
+
55
+ if rules.empty?
56
+ halt err(422, *errors)
57
+ else
58
+ if errors.empty?
59
+ status 201
60
+ else
61
+ logger.warn("Errors during bulk notification rules creation: " + errors.join(', '))
62
+ status 200
63
+ end
64
+ end
65
+
66
+ ids = rules.map {|r| r.id}
67
+ location(ids)
68
+
69
+ rules_json = rules.map {|r| r.to_json}.join(',')
70
+ '{"notification_rules":[' + rules_json + ']}'
71
+ end
72
+
73
+ # get one or more notification rules
74
+ app.get '/notification_rules/:id' do
75
+ rules_json = params[:id].split(',').collect {|rule_id|
76
+ find_rule(rule_id).to_jsonapi
77
+ }.join(', ')
78
+
79
+ '{"notification_rules":[' + rules_json + ']}'
80
+ end
81
+
82
+ app.patch '/notification_rules/:id' do
83
+ params[:id].split(',').collect {|rule_id|
84
+ find_rule(rule_id)
85
+ }.each do |rule|
86
+ apply_json_patch('notification_rules') do |op, property, linked, value|
87
+ case op
88
+ when 'replace'
89
+ case property
90
+ when 'entities', 'regex_entities', 'tags', 'regex_tags',
91
+ 'time_restrictions', 'unknown_media', 'warning_media',
92
+ 'critical_media', 'unknown_blackhole', 'warning_blackhole',
93
+ 'critical_blackhole'
94
+
95
+ rule.update({property.to_sym => value}, :logger => logger)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ status 204
102
+ end
103
+
104
+ # Deletes one or more notification rules
105
+ app.delete '/notification_rules/:id' do
106
+ params[:id].split(',').each do |rule_id|
107
+ rule = find_rule(rule_id)
108
+ logger.debug("rule to delete: #{rule.inspect}, contact_id: #{rule.contact_id}")
109
+ contact = find_contact(rule.contact_id)
110
+ contact.delete_notification_rule(rule)
111
+ end
112
+
113
+ status 204
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/contact'
6
+
7
+ module Flapjack
8
+
9
+ module Gateways
10
+
11
+ class JSONAPI < Sinatra::Base
12
+
13
+ module PagerdutyCredentialMethods
14
+
15
+ SEMAPHORE_CONTACT_MASS_UPDATE = 'contact_mass_update'
16
+
17
+ # module Helpers
18
+ # end
19
+
20
+ def self.registered(app)
21
+ app.helpers Flapjack::Gateways::JSONAPI::Helpers
22
+ # app.helpers Flapjack::Gateways::JSONAPI::PagerdutyCredentialMethods::Helpers
23
+
24
+ # Creates/overwrites pagerduty credentials for a contact
25
+ app.post '/contacts/:contact_id/pagerduty_credentials' do
26
+
27
+ pagerduty_credentials_data = params[:pagerduty_credentials]
28
+
29
+ if pagerduty_credentials_data.nil? || !pagerduty_credentials_data.is_a?(Enumerable)
30
+ halt err(422, "No valid pagerduty credentials were submitted")
31
+ end
32
+
33
+ fields = ['service_key', 'subdomain', 'username', 'password']
34
+
35
+ pagerduty_credential = pagerduty_credentials_data.last
36
+
37
+ if pagerduty_credential.nil? || !pagerduty_credential.is_a?(Hash)
38
+ halt err(422, "No valid pagerduty credentials were submitted")
39
+ end
40
+
41
+ if (fields | pagerduty_credential.keys).size != fields.size
42
+ halt err(422, "Pagerduty credential data has incorrect fields")
43
+ end
44
+
45
+ semaphore = obtain_semaphore(SEMAPHORE_CONTACT_MASS_UPDATE)
46
+ contact = Flapjack::Data::Contact.find_by_id(params[:contact_id], :redis => redis)
47
+ if contact.nil?
48
+ semaphore.release
49
+ halt err(422, "Contact id '#{params[:contact_id]}' could not be loaded")
50
+ end
51
+
52
+ pagerduty_credential_data = fields.inject({}) do |memo, field|
53
+ memo[field] = pagerduty_credential[field]
54
+ memo
55
+ end
56
+
57
+ contact.set_pagerduty_credentials(pagerduty_credential_data)
58
+ semaphore.release
59
+
60
+ pagerduty_credential_data['links'] = {'contacts' => [contact.id]}
61
+
62
+ status 201
63
+ '{"pagerduty_credentials":[' + pagerduty_credential_data.to_json + ']}'
64
+ end
65
+
66
+ app.get %r{^/pagerduty_credentials(?:/)?([^/]+)?$} do
67
+ contacts = if params[:captures] && params[:captures][0]
68
+ params[:captures][0].split(',').uniq.collect {|c_id| find_contact(c_id)}
69
+ else
70
+ Flapjack::Data::Contact.all(:redis => redis)
71
+ end
72
+
73
+ pagerduty_credentials_data = contacts.inject([]) do |memo, contact|
74
+ pdc = contact.pagerduty_credentials.clone
75
+
76
+ pdc['links'] = {'contacts' => [contact.id]}
77
+ memo << pdc
78
+ memo
79
+ end
80
+
81
+ '{"pagerduty_credentials":' + pagerduty_credentials_data.to_json + '}'
82
+ end
83
+
84
+ # update one or more sets of pagerduty credentials
85
+ app.patch '/pagerduty_credentials/:contact_id' do
86
+ params[:contact_id].split(',').uniq.collect {|c_id| find_contact(c_id)}.each do |contact|
87
+ apply_json_patch('pagerduty_credentials') do |op, property, linked, value|
88
+ if 'replace'.eql?(op)
89
+
90
+ pdc = contact.pagerduty_credentials.clone
91
+
92
+ case property
93
+ when 'service_key'
94
+ pdc['service_key'] = value
95
+ contact.set_pagerduty_credentials(pdc)
96
+ when 'subdomain'
97
+ pdc['subdomain'] = value
98
+ contact.set_pagerduty_credentials(pdc)
99
+ when 'username'
100
+ pdc['username'] = value
101
+ contact.set_pagerduty_credentials(pdc)
102
+ when 'password'
103
+ pdc['password'] = value
104
+ contact.set_pagerduty_credentials(pdc)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ status 204
111
+ end
112
+
113
+ app.delete '/pagerduty_credentials/:contact_id' do
114
+ params[:contact_id].split(',').uniq.collect {|c_id| find_contact(c_id) }.each do |contact|
115
+ contact.delete_pagerduty_credentials
116
+ end
117
+ status 204
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -9,12 +9,11 @@ module Flapjack
9
9
  class JsonParamsParser < Struct.new(:app)
10
10
  def call(env)
11
11
  t = type(env)
12
- if env['rack.input'] and not input_parsed?(env) and type_match?(t)
12
+ if env['rack.input'] && !input_parsed?(env) && type_match?(t)
13
13
  env['rack.request.form_input'] = env['rack.input']
14
14
  json_data = env['rack.input'].read
15
15
  env['rack.input'].rewind
16
-
17
- data = Oj.load(json_data)
16
+ data = json_data.empty? ? {} : Oj.load(json_data)
18
17
  env['rack.request.form_hash'] = data.empty? ? {} :
19
18
  (('application/json-patch+json'.eql?(t)) ? {'ops' => data} : data)
20
19
  end
@@ -26,8 +25,8 @@ module Flapjack
26
25
  end
27
26
 
28
27
  def type(env)
29
- return unless env['CONTENT_TYPE']
30
- env['CONTENT_TYPE'].split(/\s*[;,]\s*/, 2).first.downcase
28
+ return if env['CONTENT_TYPE'].nil?
29
+ env['CONTENT_TYPE'].split(/\s*[;,]\s*/, 2).first
31
30
  end
32
31
 
33
32
  def type_match?(t)
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'sinatra/base'
4
+
5
+ require 'flapjack/data/entity'
6
+ require 'flapjack/data/entity_check'
7
+
8
+ require 'flapjack/gateways/jsonapi/check_presenter'
9
+
10
+ module Flapjack
11
+
12
+ module Gateways
13
+
14
+ class JSONAPI < Sinatra::Base
15
+
16
+ module ReportMethods
17
+
18
+ module Helpers
19
+
20
+ def load_api_data(entity_ids, event_ids, action, &block)
21
+ result_type = case action
22
+ when 'status'
23
+ 'statuses'
24
+ when 'outage'
25
+ 'outages'
26
+ when 'scheduled_maintenance'
27
+ 'scheduled_maintenances'
28
+ when 'unscheduled_maintenance'
29
+ 'unscheduled_maintenances'
30
+ when 'downtime'
31
+ 'downtimes'
32
+ end
33
+
34
+ entities = if entity_ids.nil?
35
+ Flapjack::Data::Entity.all(:redis => redis)
36
+ elsif !entity_ids.empty?
37
+ entity_ids.collect {|entity_id| find_entity_by_id(entity_id) }
38
+ else
39
+ nil
40
+ end
41
+
42
+ checks = if event_ids.nil?
43
+ Flapjack::Data::Entity.all(:redis => redis).collect {|entity|
44
+ entity.check_list.collect {|check_name|
45
+ find_entity_check(entity, check_name)
46
+ }
47
+ }.flatten(2)
48
+ elsif !event_ids.empty?
49
+ event_ids.collect {|event_id| find_entity_check_by_name(*event_id.split(':', 2)) }
50
+ else
51
+ nil
52
+ end
53
+
54
+ entities_by_name = {}
55
+ entity_checks_by_entity_name = {}
56
+
57
+ (entities || []).each do |entity|
58
+ entities_by_name[entity.name] = entity
59
+ check_list_names = entity.check_list
60
+ entity_checks_by_entity_name[entity.name] = check_list_names.collect {|check_name|
61
+ find_entity_check_by_name(entity.name, check_name)
62
+ }
63
+ end
64
+
65
+ (checks || []).each do |check|
66
+ check_entity = check.entity
67
+ check_entity_name = check_entity.name
68
+ entities_by_name[check_entity_name] ||= check_entity
69
+
70
+ entity_checks_by_entity_name[check_entity_name] ||= []
71
+ entity_checks_by_entity_name[check_entity_name] << check
72
+ end
73
+
74
+ entity_data = entities_by_name.inject([]) do |memo, (entity_name, entity)|
75
+ memo << {
76
+ 'id' => entity.id,
77
+ 'name' => entity_name,
78
+ 'links' => {
79
+ 'checks' => entity_checks_by_entity_name[entity_name].collect {|entity_check|
80
+ "#{entity_name}:#{entity_check.check}"
81
+ },
82
+ }
83
+ }
84
+ memo
85
+ end
86
+
87
+ entity_check_data = entity_checks_by_entity_name.inject([]) do |memo, (entity_name, entity_checks)|
88
+ memo += entity_checks.collect do |entity_check|
89
+ entity_check_name = entity_check.check
90
+ {
91
+ 'id' => "#{entity_name}:#{entity_check_name}",
92
+ 'name' => entity_check_name,
93
+ result_type => yield(Flapjack::Gateways::JSONAPI::CheckPresenter.new(entity_check))
94
+ }
95
+ end
96
+ memo
97
+ end
98
+
99
+ [entity_data, entity_check_data]
100
+ end
101
+
102
+ end
103
+
104
+ def self.registered(app)
105
+ app.helpers Flapjack::Gateways::JSONAPI::Helpers
106
+ app.helpers Flapjack::Gateways::JSONAPI::ReportMethods::Helpers
107
+
108
+ app.get %r{^/(status|outage|(?:un)?scheduled_maintenance|downtime)_report/(entities|checks)(?:/([^/]+))?$} do
109
+ entities_or_checks = params[:captures][1]
110
+ action = params[:captures][0]
111
+
112
+ args = []
113
+ unless 'status'.eql?(action)
114
+ args += [validate_and_parsetime(params[:start_time]),
115
+ validate_and_parsetime(params[:end_time])]
116
+ end
117
+
118
+ entity_data, check_data = case entities_or_checks
119
+ when 'entities'
120
+ entity_ids = params[:captures][2].nil? ? nil : params[:captures][2].split(',')
121
+ load_api_data(entity_ids, [], action) {|presenter|
122
+ presenter.send(action, *args)
123
+ }
124
+ when 'checks'
125
+ entity_check_names = params[:captures][2].nil? ? nil : params[:captures][2].split(',')
126
+ load_api_data([], entity_check_names, action) {|presenter|
127
+ presenter.send(action, *args)
128
+ }
129
+ end
130
+
131
+ "{\"#{action}_reports\":" + entity_data.to_json +
132
+ ",\"linked\":{\"checks\":" + check_data.to_json + "}}"
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+
143
+ end
@@ -214,6 +214,7 @@ module Flapjack
214
214
  @check_last_change = last_change
215
215
  @check_summary = entity_check.summary
216
216
  @check_details = entity_check.details
217
+ @check_perfdata = entity_check.perfdata
217
218
 
218
219
  @last_notifications = last_notification_data(entity_check)
219
220
 
@@ -1,133 +1,197 @@
1
1
 
2
2
  var toolbox = {};
3
3
 
4
- toolbox.findById = function (array, id) {
5
- return _.find(array, function (item) {
6
- return item.id === id;
7
- });
4
+ toolbox.getMainCollection = function (response) {
5
+ return _.without(_.keys(response), 'links', 'linked', 'meta')[0];
8
6
  };
9
7
 
10
- toolbox.resolveLinks = function(context, obj, linked) {
11
- if ( _.isUndefined(obj.links) ) { return; }
8
+ toolbox.batchRequest = function(klass, ids, amount, success) {
9
+ // batch requests to avoid GET length limits
10
+ var grouped = _.groupBy(ids, function(element, index){
11
+ return Math.floor(index/75);
12
+ });
12
13
 
13
- _.each(obj.links, function(ids, name) {
14
- var linkage_type = context.linkages[name];
14
+ _.each(grouped, function(ids, index) {
15
+ var linkedClass = klass.extend({
16
+ url: function() { return(klass.prototype.url.call() + "/" + ids.join(',')); }
17
+ });
18
+ var linkedCollection = new linkedClass();
19
+ linkedCollection.fetch({'reset' : true, 'success' : success});
20
+ });
21
+ }
15
22
 
16
- if ( _.isUndefined(linkage_type) ) {
17
- obj.links[name] = null;
18
- return;
23
+ toolbox.savePatch = function(model, attrs, patch) {
24
+ var patch_json = JSON.stringify(patch);
25
+ return model.save(attrs, {
26
+ data: patch_json,
27
+ patch: true,
28
+ contentType: 'application/json-patch+json'
29
+ });
30
+ };
31
+
32
+ Backbone.JSONAPIModel = Backbone.Model.extend({
33
+
34
+ resolveLink: function(name, klass, superset) {
35
+ if ( !_.isUndefined(this.get('links')) && !_.isUndefined(this.get('links')[name]) ) {
36
+ context = this;
37
+ var records = superset.filter(function(obj) {
38
+ return(context.get('links')[name].indexOf(obj.get('id')) > -1);
39
+ });
40
+ if ( _.isUndefined(this.get('linked')) ) {
41
+ this.set('linked', {});
42
+ }
43
+ if ( _.isUndefined(this.get('linked')[name]) ) {
44
+ this.get('linked')[name] = new klass();
45
+ }
46
+ this.get('linked')[name].add(records);
19
47
  }
48
+ },
49
+
50
+ resolveLinks: function(name_klass_h) {
51
+ var context = this;
20
52
 
21
- var linked_for_name = linked[name];
53
+ _.each(name_klass_h, function(klass, name) {
22
54
 
23
- if ( _.isUndefined(linked_for_name) ) {
24
- if ( _.isArray(ids) ) {
25
- obj.links[name] = new linkage_type();
26
- } else {
27
- obj.links[name] = null;
55
+ if ( !_.isUndefined(context.get('links')) &&
56
+ !_.isUndefined(context.get('links')[name]) &&
57
+ !_.isEmpty(context.get('links')[name]) ) {
58
+ var success = function(resultCollection, response, options) {
59
+ context.resolveLink(name, klass, resultCollection);
60
+ }
61
+
62
+ toolbox.batchRequest(klass, context.get('links')[name], 75, success);
28
63
  }
64
+ });
65
+ },
66
+
67
+ parse: function (response) {
68
+ if (response === undefined) {
29
69
  return;
30
70
  }
71
+ if (response._alreadyBBJSONAPIParsed) {
72
+ delete response._alreadyBBJSONAPIParsed;
73
+ return response;
74
+ }
75
+ var mainCollection = toolbox.getMainCollection(response);
76
+ var obj = response[mainCollection][0];
31
77
 
32
- if ( _.isArray(ids) ) {
33
- var collection = new linkage_type();
34
- collection.add(_.map(ids, function(id) {
35
- return new collection.model(toolbox.findById(linked_for_name, id));
36
- }));
37
- obj.links[name] = collection;
38
- } else {
39
- obj.links[name] = new linkage_type(toolbox.findById(linked_for_name, ids));
78
+ return obj;
79
+ },
80
+
81
+ // makes sense to call this with model.patch(model.changedAttributes),
82
+ // if that value isn't false
83
+ patch: function(urlType, attrs) {
84
+ if (attrs == null) {
85
+ attrs = {};
40
86
  }
41
- });
42
87
 
43
- };
88
+ var context = this;
44
89
 
45
- toolbox.getMainCollection = function (response) {
46
- return _.without(_.keys(response), 'links', 'linked', 'meta')[0];
47
- };
90
+ var patch = _.inject(attrs, function(memo, val, key) {
91
+ // skip if not a simple attribute value
92
+ if ( (key == 'links') || _.isObject(val) || _.isArray(val) ) {
93
+ return memo;
94
+ }
48
95
 
49
- Backbone.Collection.prototype.parse = function (response) {
50
- if (response === undefined) {
51
- return;
52
- }
53
- var mainCollection = toolbox.getMainCollection(response);
54
- var context = this;
55
- return _.map(response[mainCollection], function (obj) {
56
- toolbox.resolveLinks(context, obj, (response['linked'] || {}));
57
- obj._alreadyBBJSONAPIParsed = true;
58
- return obj;
59
- });
60
- };
96
+ memo.push({
97
+ op: 'replace',
98
+ path: '/' + urlType + '/0/' + key,
99
+ value: val
100
+ });
61
101
 
62
- Backbone.Model.prototype.parse = function (response) {
63
- if (response === undefined) {
64
- return;
65
- }
66
- if (response._alreadyBBJSONAPIParsed) {
67
- delete response._alreadyBBJSONAPIParsed;
68
- return response;
102
+ return memo;
103
+ }, new Array());
104
+
105
+ toolbox.savePatch(this, attrs, patch);
106
+ },
107
+
108
+ // singular operation only -- TODO batch up and submit en masse
109
+ addLinked: function(urlType, type, obj) {
110
+ var patch = [{
111
+ op: 'add',
112
+ path: '/' + urlType + '/0/links/' + type + '/-',
113
+ value: obj.get('id')
114
+ }];
115
+
116
+ toolbox.savePatch(this, {}, patch);
117
+ this.get('linked')[type].add(obj);
118
+ },
119
+
120
+ // singular operation only -- TODO batch up and submit en masse
121
+ removeLinked: function(urlType, type, obj) {
122
+ var patch = [{
123
+ op: 'remove',
124
+ path: '/' + urlType + '/0/links/' + type + '/' + obj.get('id'),
125
+ }];
126
+
127
+ toolbox.savePatch(this, {}, patch);
128
+ this.get('linked')[type].remove(obj);
129
+ },
130
+
131
+ sync: function(method, model, options) {
132
+ if ( method == 'create' ) {
133
+ options['contentType'] = 'application/vnd.api+json';
134
+ }
135
+ Backbone.sync(method, model, options);
69
136
  }
70
- var mainCollection = toolbox.getMainCollection(response);
71
- var obj = response[mainCollection][0];
72
- toolbox.resolveLinks(this, obj, (response['linked'] || {}));
73
- return obj;
74
- };
75
137
 
76
- toolbox.savePatch = function(model, attrs, patch) {
77
- var patch_json = JSON.stringify(patch);
78
- return model.save(attrs, {
79
- data: patch_json,
80
- patch: true,
81
- contentType: 'application/json-patch+json'
82
- });
83
- };
138
+ });
84
139
 
85
- // makes sense to call this with model.patch(model.changedAttributes),
86
- // if that value isn't false
87
- Backbone.Model.prototype.patch = function(attrs) {
88
- if (attrs == null) {
89
- attrs = {};
90
- }
91
140
 
92
- var context = this;
141
+ Backbone.JSONAPICollection = Backbone.Collection.extend({
93
142
 
94
- var patch = _.inject(attrs, function(memo, val, key) {
95
- // skip if not a simple attribute value
96
- if ( (key == 'links') || _.isObject(val) || _.isArray(val) ) {
97
- return memo;
143
+ resolveLinks: function(name_klass_h) {
144
+ if ( _.isUndefined(this.linked) ) {
145
+ this.linked = {};
98
146
  }
99
147
 
100
- memo.push({
101
- op: 'replace',
102
- path: '/' + context.urlType + '/0/' + key,
103
- value: val
104
- });
148
+ var context = this;
105
149
 
106
- return memo;
107
- }, new Array());
150
+ _.each(name_klass_h, function(klass, name) {
108
151
 
109
- toolbox.savePatch(this, attrs, patch);
110
- };
152
+ if ( !_.isUndefined(context.links[name]) && !_.isEmpty(context.links[name]) ) {
153
+ context.linked[name] = new klass();
111
154
 
112
- // singular operation only -- TODO batch up and submit en masse
113
- Backbone.Model.prototype.addLinked = function(type, obj) {
114
- var patch = [{
115
- op: 'add',
116
- path: '/' + this.urlType + '/0/links/' + type + '/-',
117
- value: obj.get('id')
118
- }];
155
+ var success = function(resultCollection, response, options) {
156
+ context.forEach(function(obj, index) {
157
+ obj.resolveLink(name, klass, resultCollection);
158
+ });
159
+ }
119
160
 
120
- toolbox.savePatch(this, {}, patch);
121
- this.get('links')[type].add(obj);
122
- };
161
+ toolbox.batchRequest(klass, context.links[name], 75, success);
162
+ }
163
+ });
164
+ },
123
165
 
124
- // singular operation only -- TODO batch up and submit en masse
125
- Backbone.Model.prototype.removeLinked = function(type, obj) {
126
- var patch = [{
127
- op: 'remove',
128
- path: '/' + this.urlType + '/0/links/' + type + '/' + obj.get('id'),
129
- }];
166
+ parse: function (response) {
167
+ if (response === undefined) {
168
+ return;
169
+ }
130
170
 
131
- toolbox.savePatch(this, {}, patch);
132
- this.get('links')[type].remove(obj);
133
- };
171
+ this.links = {};
172
+ var context = this;
173
+
174
+ var mainCollection = toolbox.getMainCollection(response);
175
+ var objects = _.map(response[mainCollection], function (obj) {
176
+ _.each(obj.links, function(ids, name) {
177
+ if ( _.isUndefined(context.links[name]) ) {
178
+ context.links[name] = new Array();
179
+ }
180
+ if ( _.isArray(ids) ) {
181
+ context.links[name] = context.links[name].concat(ids);
182
+ }
183
+ });
184
+
185
+ obj._alreadyBBJSONAPIParsed = true;
186
+ return obj;
187
+ });
188
+
189
+ this.links = _.reduce(this.links, function(memo, ids, name) {
190
+ memo[name] = _.uniq(ids);
191
+ return(memo);
192
+ }, {});
193
+
194
+ return objects;
195
+ }
196
+
197
+ });