flapjack 0.8.4 → 0.8.5

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.
@@ -131,11 +131,6 @@ module Flapjack
131
131
  '{"entities":[' + entities_json + ']}'
132
132
  end
133
133
 
134
- app.get '/checks/:entity' do
135
- content_type :json
136
- entity = find_entity(params[:entity])
137
- entity.check_list.to_json
138
- end
139
134
 
140
135
  app.get %r{/status#{ENTITY_CHECK_FRAGMENT}} do
141
136
  content_type :json
@@ -316,29 +311,6 @@ module Flapjack
316
311
  errors.empty? ? 204 : err(403, *errors)
317
312
  end
318
313
 
319
- app.post '/entities/:entity/tags' do
320
- content_type :json
321
-
322
- tags = find_tags(params[:tag])
323
- entity = find_entity(params[:entity])
324
- entity.add_tags(*tags)
325
- entity.tags.to_json
326
- end
327
-
328
- app.delete '/entities/:entity/tags' do
329
- tags = find_tags(params[:tag])
330
- entity = find_entity(params[:entity])
331
- entity.delete_tags(*tags)
332
- status 204
333
- end
334
-
335
- app.get '/entities/:entity/tags' do
336
- content_type :json
337
-
338
- entity = find_entity(params[:entity])
339
- entity.tags.to_json
340
- end
341
-
342
314
  end
343
315
 
344
316
  end
@@ -8,11 +8,15 @@ module Flapjack
8
8
  module Rack
9
9
  class JsonParamsParser < Struct.new(:app)
10
10
  def call(env)
11
- if env['rack.input'] and not input_parsed?(env) and type_match?(env)
11
+ t = type(env)
12
+ if env['rack.input'] and not input_parsed?(env) and type_match?(t)
12
13
  env['rack.request.form_input'] = env['rack.input']
13
- data = env['rack.input'].read
14
+ json_data = env['rack.input'].read
14
15
  env['rack.input'].rewind
15
- env['rack.request.form_hash'] = data.empty? ? {} : Oj.load(data)
16
+
17
+ data = Oj.load(json_data)
18
+ env['rack.request.form_hash'] = data.empty? ? {} :
19
+ (('application/json-patch+json'.eql?(t)) ? {'ops' => data} : data)
16
20
  end
17
21
  app.call(env)
18
22
  end
@@ -21,9 +25,13 @@ module Flapjack
21
25
  env['rack.request.form_input'].eql? env['rack.input']
22
26
  end
23
27
 
24
- def type_match? env
25
- type = env['CONTENT_TYPE'] and
26
- Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(type.split(/\s*[;,]\s*/, 2).first.downcase)
28
+ def type(env)
29
+ return unless env['CONTENT_TYPE']
30
+ env['CONTENT_TYPE'].split(/\s*[;,]\s*/, 2).first.downcase
31
+ end
32
+
33
+ def type_match?(t)
34
+ Flapjack::Gateways::JSONAPI::JSON_REQUEST_MIME_TYPES.include?(t)
27
35
  end
28
36
  end
29
37
  end
@@ -441,6 +441,7 @@ module Flapjack
441
441
  def require_js(*js)
442
442
  @required_js ||= []
443
443
  @required_js += js
444
+ @required_js.uniq!
444
445
  end
445
446
 
446
447
  def include_required_js
@@ -73,3 +73,61 @@ Backbone.Model.prototype.parse = function (response) {
73
73
  return obj;
74
74
  };
75
75
 
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
+ };
84
+
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
+
92
+ var context = this;
93
+
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;
98
+ }
99
+
100
+ memo.push({
101
+ op: 'replace',
102
+ path: '/' + context.urlType + '/0/' + key,
103
+ value: val
104
+ });
105
+
106
+ return memo;
107
+ }, new Array());
108
+
109
+ toolbox.savePatch(this, attrs, patch);
110
+ };
111
+
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
+ }];
119
+
120
+ toolbox.savePatch(this, {}, patch);
121
+ this.get('links')[type].add(obj);
122
+ };
123
+
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
+ }];
130
+
131
+ toolbox.savePatch(this, {}, patch);
132
+ this.get('links')[type].remove(obj);
133
+ };
@@ -1,5 +1,8 @@
1
1
  $(document).ready(function() {
2
2
 
3
+ // fix select2 with modal
4
+ $.fn.modal.Constructor.prototype.enforceFocus = function() {};
5
+
3
6
  var app = {
4
7
  api_url: $('div#data-api-url').data('api-url')
5
8
  };
@@ -19,13 +22,38 @@ $(document).ready(function() {
19
22
  toJSON: function() {
20
23
  return { entities: [ _.clone( this.attributes ) ] }
21
24
  },
22
- urlRoot: app.api_url + "/entities"
25
+ urlType: 'entities',
26
+ urlRoot: function() { return app.api_url + "/" + this.urlType; }
23
27
  });
24
28
 
25
29
  app.EntityCollection = Backbone.Collection.extend({
26
30
  model: app.Entity,
27
31
  comparator: 'name',
28
- url: app.api_url + "/entities"
32
+ urlType: 'entities',
33
+ url: function() { return app.api_url + "/" + this.urlType; }
34
+ });
35
+
36
+ app.Medium = Backbone.Model.extend({
37
+ name: 'media',
38
+ defaults: {
39
+ address: '',
40
+ interval: 60,
41
+ rollup_threshold: 3,
42
+ id: null,
43
+ contact_id: null,
44
+ },
45
+ toJSON: function() {
46
+ return { media: [ _.clone( this.attributes ) ] }
47
+ },
48
+ urlType: 'media',
49
+ urlRoot: function() { return app.api_url + "/" + this.urlType; }
50
+ });
51
+
52
+ app.MediumCollection = Backbone.Collection.extend({
53
+ model: app.Medium,
54
+ comparator: 'type',
55
+ urlType: 'media',
56
+ url: function() { return app.api_url + "/" + this.urlType; }
29
57
  });
30
58
 
31
59
  app.Contact = Backbone.Model.extend({
@@ -43,18 +71,24 @@ $(document).ready(function() {
43
71
  // TODO how will we handle circular references? make it a string and eval it?
44
72
  linkages: {
45
73
  entity: app.Entity,
46
- entities: app.EntityCollection
74
+ entities: app.EntityCollection,
75
+ medium: app.Medium,
76
+ media: app.MediumCollection
47
77
  },
48
- urlRoot : app.api_url + "/contacts"
78
+ urlType: 'contacts',
79
+ urlRoot: function() { return app.api_url + "/" + this.urlType; }
49
80
  });
50
81
 
51
82
  app.ContactCollection = Backbone.Collection.extend({
52
83
  model: app.Contact,
53
- url: app.api_url + "/contacts",
54
84
  linkages: {
55
85
  entity: app.Entity,
56
- entities: app.EntityCollection
86
+ entities: app.EntityCollection,
87
+ medium: app.Medium,
88
+ media: app.MediumCollection
57
89
  },
90
+ urlType: 'contacts',
91
+ url: function() { return app.api_url + "/" + this.urlType; }
58
92
  });
59
93
 
60
94
  app.ActionsView = Backbone.View.extend({
@@ -69,17 +103,53 @@ $(document).ready(function() {
69
103
  if ( $('#contactModal').hasClass('in') ) { return; }
70
104
 
71
105
  $('#contactModal h4#contactModalLabel').text('New Contact');
72
- $('#contactModal button.btn.btn-success').text('Create Contact');
106
+ $('#contactModal button#contactAccept').text('Create Contact');
107
+
108
+ var context = this;
109
+
110
+ // TODO if validating or leaving modal open, re-establish the event
111
+ $('#contactModal button#contactAccept').one('click', function() { context.save(); });
112
+
113
+ this.model = new app.Contact();
114
+ this.model.set('links', {entities: new app.EntityCollection(), media: new app.MediumCollection()});
115
+
116
+ var contactView = new app.ContactView({model: this.model});
117
+
118
+ $('#contactModal div.modal-footer').siblings().remove();
119
+ $('#contactModal div.modal-footer').before(contactView.render().$el);
120
+
121
+ var currentEntities = this.model.get('links')['entities'];
122
+
123
+ var contactEntityList = new app.ContactEntityList({collection: currentEntities, contact: this.model});
124
+ $('#contactModal tbody#contactEntityList').replaceWith( contactEntityList.render().$el );
125
+
126
+ var entityChooser = new app.EntityChooser({model: this.model, currentEntities: currentEntities});
127
+ entityChooser.render();
128
+
129
+ var context = this;
130
+
131
+ // Setup contact media
132
+ var contactMediaList = new app.ContactMediaList({
133
+ collection: this.model.get('links')['media'],
134
+ contact: this.model
135
+ });
136
+
137
+ $('#contactModal tbody#contactMediaList')
138
+ .replaceWith( contactMediaList.render().$el )
73
139
 
74
- $('#contactModal input[name=contact_id]').val('');
75
- $('#contactModal input[name=contact_first_name]').val('');
76
- $('#contactModal input[name=contact_last_name]').val('');
77
- $('#contactModal input[name=contact_email]').val('');
78
140
  $('#contactModal').modal('show');
79
141
  },
80
142
  render: function() {
81
143
  this.$el.html(this.template({}));
82
144
  return this;
145
+ },
146
+ save: function() {
147
+ data = {'first_name': $('#contactModal input[name=contact_first_name]').val(),
148
+ 'last_name': $('#contactModal input[name=contact_last_name]').val(),
149
+ 'email': $('#contactModal input[name=contact_email]').val()};
150
+ this.model.save(data, {type: 'POST', contentType: 'application/vnd.api+json'});
151
+ contacts.add(this.model);
152
+ $('#contactModal').modal('hide');
83
153
  }
84
154
  });
85
155
 
@@ -87,73 +157,111 @@ $(document).ready(function() {
87
157
  // this.collection == duplicate of entities with
88
158
  // entities enabled for this contact removed
89
159
  app.EntityChooser = Backbone.View.extend({
90
- tagName: 'input',
91
- attributes: {type: 'hidden'},
92
- className: 'entityChooser',
93
- initialize: function() {
94
- var contact_entity_ids = this.model.get('links')['entities'].pluck('id');
160
+ template: _.template($('#contact-entity-chooser').html()),
161
+ el: $("#entityAdd"),
162
+ events: {
163
+ 'click button#add-contact-entity' : 'addEntities',
164
+ },
165
+ initialize: function(options) {
166
+ this.options = options || {};
167
+ this.listenTo(options.currentEntities, 'add', this.refresh);
168
+ this.listenTo(options.currentEntities, 'remove', this.refresh);
169
+ },
170
+ render: function() {
95
171
 
96
- var someEntities = allEntities.reject(function(item, context) {
97
- return _.contains(contact_entity_ids, item.get('id'));
98
- });
172
+ this.calculate();
99
173
 
100
- this.collection = new app.EntityCollection(someEntities);
101
- },
102
- render: function() {
103
- var jqel = $(this.el);
174
+ // clear array
175
+ this.entityIdsToAdd = new Array();
104
176
 
105
- var results = this.collection.map( function(item) {
106
- return item.attributes;
177
+ this.$el.html(this.template({}));
178
+
179
+ var jqel = $(this.el).find('input#entityChooser');
180
+
181
+ var context = this;
182
+ jqel.on('change', function(e) {
183
+ if ( !_.isArray(e.removed) && _.isObject(e.removed) ) {
184
+ context.entityIdsToAdd = _.without(context.entityIdsToAdd, e.removed.id);
185
+ }
186
+
187
+ if ( !_.isArray(e.added) && _.isObject(e.added) && (context.entityIdsToAdd.indexOf(e.added.id) == -1) ) {
188
+ context.entityIdsToAdd.push(e.added.id);
189
+ }
107
190
  });
108
191
 
109
192
  var format = function(item) { return item.name; }
193
+ var context = this;
110
194
 
111
195
  jqel.select2({
112
- placeholder: "Select an Entity",
113
- data: { results: results, text: 'name'},
196
+ placeholder: "Select Entities",
197
+ data: {results: context.results, text: 'name'},
114
198
  formatSelection: format,
115
- formatResult: format
199
+ formatResult: format,
200
+ multiple: true,
201
+ width: 'off',
116
202
  });
117
203
 
118
204
  return this;
119
205
  },
206
+ calculate: function() {
207
+ var contact_entity_ids = this.options.currentEntities.pluck('id');
120
208
 
121
- });
209
+ var someEntities = allEntities.reject(function(item, context) {
210
+ return _.contains(contact_entity_ids, item.get('id'));
211
+ });
212
+
213
+ this.collection = new app.EntityCollection(someEntities);
122
214
 
123
- app.EntitiesEnabled = Backbone.View.extend({
124
- tagName: 'select',
125
- className: 'entityList',
126
- attributes: {
127
- size: "12",
128
- multiple: "multiple"
215
+ this.results = this.collection.map( function(item) {
216
+ return item.attributes;
217
+ });
129
218
  },
130
- initialize: function() {
131
- this.collection.on('add', this.render, this);
219
+ refresh: function(model, collection, options) {
220
+ this.calculate();
221
+ var jqel = $(this.el).find('input#entityChooser');
222
+ var context = this;
223
+ var format = function(item) { return item.name; }
224
+ jqel.select2({
225
+ placeholder: "Select Entities",
226
+ data: {results: context.results, text: 'name'},
227
+ formatSelection: format,
228
+ formatResult: format,
229
+ multiple: true,
230
+ width: 'off',
231
+ });
132
232
  },
133
- render: function() {
134
- var jqel = $(this.el);
135
-
136
- jqel.find('option').remove();
137
-
138
- this.collection.each(function(entity) {
139
- var item = new app.EntityListItem({ model: entity });
140
- jqel.append(item.render().el);
233
+ addEntities: function() {
234
+ var jqel = $(this.el).find('input#entityChooser');
235
+ jqel.select2("val", null);
236
+ var context = this;
237
+ _.each(this.entityIdsToAdd, function(entity_id) {
238
+ var newEntity = allEntities.find(function(entity) { return entity.id == entity_id; });
239
+ context.model.addLinked('entities', newEntity);
141
240
  });
241
+ this.entityIdsToAdd.length = 0;
242
+ },
243
+ });
142
244
 
245
+ app.ContactView = Backbone.View.extend({
246
+ template: _.template($('#contact-template').html()),
247
+ id: 'contactView',
248
+ render: function() {
249
+ var template_values = _.clone(this.model.attributes);
250
+ this.$el.html(this.template(template_values));
143
251
  return this;
144
- },
252
+ }
145
253
  });
146
254
 
147
255
  app.ContactList = Backbone.View.extend({
148
- tagName: 'table',
149
- className: 'table contactList',
256
+ tagName: 'tbody',
257
+ el: $('#contactList'),
150
258
  initialize: function() {
151
259
  this.collection.on('add', this.render, this);
152
260
  },
153
261
  render: function() {
154
262
  var jqel = $(this.el);
155
263
  jqel.empty();
156
-
264
+ var context = this;
157
265
  this.collection.each(function(contact) {
158
266
  var item = new app.ContactListItem({ model: contact });
159
267
  jqel.append($(item.render().el));
@@ -163,15 +271,45 @@ $(document).ready(function() {
163
271
  },
164
272
  });
165
273
 
166
- app.EntityListItem = Backbone.View.extend({
167
- tagName: 'option',
274
+ app.ContactEntityList = Backbone.View.extend({
275
+ tagName: 'tbody',
276
+ id: 'contactEntityList',
277
+ initialize: function(options) {
278
+ this.options = options || {};
279
+ this.collection.on('add', this.render, this);
280
+ this.collection.on('remove', this.render, this);
281
+ },
168
282
  render: function() {
283
+ var jqel = $(this.el);
284
+ jqel.empty();
285
+ var contact = this.options.contact;
286
+ this.collection.each(function(entity) {
287
+ var item = new app.ContactEntityListItem({ model: entity, contact: contact });
288
+ jqel.append(item.render().el);
289
+ });
169
290
 
170
- // TODO piggyback data from model record
171
- this.$el.html('<option>' + this.model.escape('name') + '</option>');
291
+ return this;
292
+ },
293
+ });
172
294
 
295
+ app.ContactEntityListItem = Backbone.View.extend({
296
+ tagName: 'tr',
297
+ template: _.template($('#contact-entities-list-item-template').html()),
298
+ events: {
299
+ 'click button.delete-entity' : 'removeEntity',
300
+ },
301
+ initialize: function(options) {
302
+ this.options = options || {};
303
+ },
304
+ render: function() {
305
+ var template_values = _.clone(this.model.attributes);
306
+ this.$el.html(this.template(template_values));
173
307
  return this;
174
- }
308
+ },
309
+ removeEntity: function() {
310
+ this.options.contact.removeLinked('entities', this.model);
311
+ this.$el.remove();
312
+ },
175
313
  });
176
314
 
177
315
  app.ContactListItem = Backbone.View.extend({
@@ -179,7 +317,12 @@ $(document).ready(function() {
179
317
  className: 'contact_list_item',
180
318
  template: _.template($('#contact-list-item-template').html()),
181
319
  events: {
182
- "click" : "editContact",
320
+ 'click .button.delete-contact': 'removeContact',
321
+ 'click': 'editContact',
322
+ },
323
+ initialize: function() {
324
+ // causes an unnecessary render on create, but required for update TODO cleanup
325
+ this.listenTo(this.model, "sync", this.render);
183
326
  },
184
327
 
185
328
  render: function() {
@@ -193,16 +336,151 @@ $(document).ready(function() {
193
336
  if ( $('#contactModal').hasClass('in') ) { return; }
194
337
 
195
338
  $('#contactModal h4#contactModalLabel').text('Edit Contact');
196
- $('#contactModal button.btn.btn-success').text('Update Contact');
339
+ $('#contactModal button#contactAccept').text('Update Contact');
340
+
341
+ var context = this;
342
+
343
+ // TODO if validating or leaving modal open, re-establish the event
344
+ $('#contactModal button#contactAccept').one('click', function() { context.save(); });
345
+
346
+ var contactView = new app.ContactView({model: this.model});
347
+
348
+ $('#contactModal div.modal-footer').siblings().remove();
349
+ $('#contactModal div.modal-footer').before(contactView.render().$el);
350
+
351
+ var currentEntities = this.model.get('links')['entities'];
352
+
353
+ var contactEntityList = new app.ContactEntityList({collection: currentEntities, contact: this.model});
354
+ $('#contactModal tbody#contactEntityList').replaceWith( contactEntityList.render().$el );
355
+
356
+ var entityChooser = new app.EntityChooser({model: this.model, currentEntities: currentEntities});
357
+ entityChooser.render();
358
+
359
+ // Setup contact media
360
+ var contactMediaList = new app.ContactMediaList({
361
+ collection: this.model.get('links')['media'],
362
+ contact: this.model
363
+ });
197
364
 
198
- $('#contactModal input[name=contact_id]').val(this.model.get('id'));
199
- $('#contactModal input[name=contact_first_name]').val(this.model.get('first_name'));
200
- $('#contactModal input[name=contact_last_name]').val(this.model.get('last_name'));
201
- $('#contactModal input[name=contact_email]').val(this.model.get('email'));
365
+ $('#contactModal tbody#contactMediaList')
366
+ .replaceWith( contactMediaList.render().$el )
202
367
 
203
368
  $('#contactModal').modal('show');
204
369
  },
205
370
 
371
+ save: function() {
372
+ data = {'first_name': $('#contactModal input[name=contact_first_name]').val(),
373
+ 'last_name': $('#contactModal input[name=contact_last_name]').val(),
374
+ 'email': $('#contactModal input[name=contact_email]').val()};
375
+ this.model.save(data, {type: 'PUT', contentType: 'application/vnd.api+json'});
376
+ $('#contactModal').modal('hide');
377
+ },
378
+
379
+ removeContact: function(e) {
380
+ e.stopImmediatePropagation();
381
+
382
+ var context = this;
383
+
384
+ context.model.destroy({
385
+ success: function() {
386
+ context.remove()
387
+ }
388
+ });
389
+ },
390
+ });
391
+
392
+ app.ContactMediaList = Backbone.View.extend({
393
+ tagName: 'tbody',
394
+ id: 'contactMediaList',
395
+ initialize: function(options) {
396
+ var context = this;
397
+ _.each(['email', 'sms', 'jabber'], function(type) {
398
+ var medium = context.collection.find(function(cm) {
399
+ return cm.get('type') == type;
400
+ });
401
+
402
+ if ( _.isUndefined(medium) ) {
403
+ medium = new app.Medium({
404
+ type: type,
405
+ address: '',
406
+ interval: 15,
407
+ rollup_threshold: 3,
408
+ contact_id: options.contact.get('id')
409
+ });
410
+ context.collection.add(medium);
411
+ }
412
+ });
413
+ },
414
+ render: function() {
415
+ var jqel = $(this.el);
416
+ jqel.empty();
417
+
418
+ this.collection.each(function(medium) {
419
+ var item = new app.ContactMediaListItem({ model: medium });
420
+ jqel.append(item.render().el);
421
+ });
422
+
423
+ return this;
424
+ },
425
+ });
426
+
427
+ app.ContactMediaListItem = Backbone.View.extend({
428
+ tagName: 'tr',
429
+ template: _.template($('#contact-media-list-item-template').html()),
430
+ events: {
431
+ // scoped to this view's el
432
+ 'change input' : 'updateMedium'
433
+ },
434
+ render: function() {
435
+ var template_values = _.clone(this.model.attributes);
436
+ template_values['labels'] = {
437
+ 'email' : 'Email',
438
+ 'sms' : 'SMS',
439
+ 'jabber' : 'Jabber'
440
+ };
441
+ this.$el.html(this.template(template_values));
442
+ return this;
443
+ },
444
+ updateMedium: function(event) {
445
+ var address = $(event.target).parent('td')
446
+ .siblings().addBack().find('input[data-attr=address]');
447
+ var interval = $(event.target).parent('td')
448
+ .siblings().addBack().find('input[data-attr=interval]');
449
+ var rollupThreshold =
450
+ $(event.target).parent('td')
451
+ .siblings().addBack().find('input[data-attr=rollup_threshold]');
452
+
453
+ var addressVal = address.val();
454
+ var intervalVal = interval.val();
455
+ var rollupThresholdVal = rollupThreshold.val();
456
+
457
+ var numRE = /^[0-9]+$/;
458
+
459
+ if ( !numRE.test(intervalVal) || !numRE.test(rollupThresholdVal) ) {
460
+ // only save if numeric fields have acceptable values
461
+ return;
462
+ }
463
+
464
+ if ( _.isUndefined(addressVal) || (addressVal.length == 0) ) {
465
+ // only save if address not blank
466
+ return;
467
+ }
468
+
469
+ // TODO visually highlight error
470
+
471
+ var attrName = event.target.getAttribute('data-attr');
472
+ var value = event.target.value;
473
+
474
+ var attrs = {};
475
+ attrs[attrName] = value;
476
+
477
+ if ( this.model.isNew() ) {
478
+ this.model.save(attrs);
479
+ this.model.set('id', this.model.get('contact_id') + '_' + this.model.get('type'));
480
+ } else {
481
+ this.model.patch(attrs);
482
+ }
483
+ }
206
484
  });
207
485
 
208
486
  var allEntities = new app.EntityCollection();
@@ -215,8 +493,7 @@ $(document).ready(function() {
215
493
  var actionsView = new app.ActionsView({collection: collection});
216
494
  var contactList = new app.ContactList({collection: collection});
217
495
  $('#container').append(actionsView.render().el);
218
-
219
- $('#contactList').append($(contactList.render().el).find('tr'));
496
+ contactList.render();
220
497
  }
221
498
  });
222
499
  }