+
+
<%- firstName %> <%- lastName %>
<%- skills %>
-
-
+
+
-
+
diff --git a/client/spa/js/instructor/instructor.model.js b/client/spa/js/instructor/instructor.model.js
index 0cae394..61c9a68 100644
--- a/client/spa/js/instructor/instructor.model.js
+++ b/client/spa/js/instructor/instructor.model.js
@@ -24,6 +24,6 @@ module.exports = Backbone.Model.extend({
if (!attrs.skills){
errors.push('skills cannot be empty');
}
- return errors;
+ return errors.length > 0 ? errors: false;
}
});
diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js
index eb9bf32..4926226 100644
--- a/client/spa/js/instructor/instructor.view.js
+++ b/client/spa/js/instructor/instructor.view.js
@@ -10,12 +10,13 @@ var editTemplate = fs.readFileSync(__dirname + '/editInstructor.html', 'utf8');
module.exports = Backbone.View.extend({
className: 'instructor',
template: _.template(template),
+ showTemplate: _.template(template),
editTemplate: _.template(editTemplate),
events: {
- 'click .i-delete': 'destroy',
- 'click .i-edit': 'edit',
- 'click .i-save': 'save',
- 'click .i-cancel': 'cancel'
+ 'click .delete': 'destroy',
+ 'click .modify': 'modify',
+ 'click .save': 'save',
+ 'click .cancel': 'cancel'
},
initialize: function(){
this.listenTo(this.model, 'destroy', this.remove);
@@ -25,37 +26,51 @@ module.exports = Backbone.View.extend({
var context = this.model.toJSON();
this.$el.html(this.template(context));
+ // if it's adding new model, change button to Add
+ if (this.model.get('id') === undefined) {
+ this.$('.save').html('Add');
+ }
+
return this;
},
destroy: function(){
this.model.destroy();
},
- edit: function(e){
+ modify: function(e){
var context = this.model.toJSON();
this.$el.html(this.editTemplate(context));
return this;
},
save: function(e) {
- e.preventDefault(); // if there's no changes, do not do anything
+ // if there's no changes, do not do anything
+ e.preventDefault();
var formData = {
firstName: this.$('#firstName').val().trim(),
lastName: this.$('#lastName').val().trim(),
skills: this.$('#skills').val().trim()
};
- var validate = {
+
+ var check = {
success: function() {
$('#result').addClass('success')
- .html('Successfully updated instructor')
- .fadeIn().delay(4000).fadeOut();
- },
- error: function(model, error) {
+ .html('Successfully updated instructor')
+ .fadeIn().delay(4000).fadeOut();
+ var addNew = $('.save').html();
+
+ if (addNew === 'Add') {
+ $('#added').addClass('success')
+ .html('Successfully added new instructor')
+ .fadeIn().delay(4000).fadeOut();
+ }
+ },
+ error: function(model, errors) {
}
};
- this.model.save(formData, validate);
+ this.model.save(formData, check);
},
cancel: function(e) {
e.preventDefault(); // prevent event bubbling
diff --git a/client/spa/js/instructor/instructors.collection.js b/client/spa/js/instructor/instructors.collection.js
new file mode 100644
index 0000000..dc1c9cb
--- /dev/null
+++ b/client/spa/js/instructor/instructors.collection.js
@@ -0,0 +1,44 @@
+'use strict';
+
+var Backbone = require('../vendor/index').Backbone;
+
+module.exports = Backbone.Collection.extend({
+ url: '/api/instructors/',
+
+ initialize: function(){
+ this.on('sortById', this.sortById);
+ this.on('sortByFirstName', this.sortByFirstName);
+ this.on('sortByLastName', this.sortByLastName);
+ this.on('addNew', this.addNew);
+ this.trigger('sortById');
+ },
+
+ sortById: function(){
+ this.comparator = function(model){
+ return model.get('id');
+ };
+ this.sort();
+ },
+
+ sortByFirstName: function(){
+ this.comparator = function(model){
+ return model.get('firstName');
+ };
+ this.sort();
+ },
+
+ sortByLastName: function(){
+ this.comparator = function(model){
+ return model.get('lastName');
+ };
+ this.sort();
+ },
+
+ addNew: function() {
+ this.create = function(model) {
+ model.get('firstName');
+ model.get('lastName');
+ model.get('skills');
+ };
+ }
+});
diff --git a/client/spa/js/instructor/instructors.controller.js b/client/spa/js/instructor/instructors.controller.js
new file mode 100644
index 0000000..cffaef4
--- /dev/null
+++ b/client/spa/js/instructor/instructors.controller.js
@@ -0,0 +1,59 @@
+'use strict';
+
+var Backbone = require('../vendor/index').Backbone;
+var $ = require('../vendor/index').$;
+var Model = require('./instructor.model');
+var Collection = require('./instructors.collection');
+var View = require('./instructors.view');
+
+module.exports = Backbone.Controller.extend({
+ routes: {
+ 'instructors': 'showInstructors'
+ },
+ initialize: function(){
+ this.options.container = this.options.container || 'body';
+ },
+ getCollection: function(){
+ if (!this.collection){
+ Collection = Collection.extend({model: Model});
+ this.collection = new Collection();
+ }
+ return this.collection;
+ },
+ getView: function(){
+ if (!this.view){
+ var V = View.extend({collection: this.collection});
+ this.view = new V();
+ this.view.on('addNew', function() {
+ // trigger the router for addNew
+ this.navigate('instructors/new', { trigger: true });
+ }.bind(this));
+ }
+ return this.view;
+ },
+ showInstructors: function(){
+ var self = this;
+
+ this.getCollection().fetch({
+ success: function(collection, response, options){
+ self.getView();
+ self.renderView();
+ },
+ error: function(collection, response, options){
+ self.renderError();
+ }
+ });
+ },
+ renderToContainer: function(html){
+ return $(this.options.container).html(html);
+ },
+ renderView: function(){
+ this.renderToContainer(this.view.render().$el);
+ this.view.delegateEvents();
+ return this.view;
+ },
+ renderError: function(){
+ return this.renderToContainer(
+ '
There was a problem rendering instructors
');
+ }
+});
diff --git a/client/spa/js/instructor/instructors.html b/client/spa/js/instructor/instructors.html
new file mode 100644
index 0000000..4734408
--- /dev/null
+++ b/client/spa/js/instructor/instructors.html
@@ -0,0 +1,16 @@
+
+
+
Instructors
+
+
+
+
+
+ <% _.each( models, function( model ){ %>
+ -
+ <%- model.attributes.firstName %> <%- model.attributes.lastName %>
+
+ <% }); %>
+
+
+
diff --git a/client/spa/js/instructor/instructors.view.js b/client/spa/js/instructor/instructors.view.js
new file mode 100644
index 0000000..3257946
--- /dev/null
+++ b/client/spa/js/instructor/instructors.view.js
@@ -0,0 +1,57 @@
+'use strict';
+
+var Backbone = require('../vendor/index').Backbone;
+var _ = require('../vendor/index')._;
+var fs = require('fs'); //will be replaced by brfs in the browser
+// readFileSync will be evaluated statically so errors can't be caught
+var template = fs.readFileSync(__dirname + '/instructors.html', 'utf8');
+
+
+module.exports = Backbone.View.extend({
+ className: 'instructors',
+ template: _.template(template),
+ events:{
+ 'click .sortById': 'sortById',
+ 'click .sortByFirstName': 'sortByFirstName',
+ 'click .sortByLastName': 'sortByLastName',
+ 'click .addNew': 'addNew'
+ },
+
+ initialize: function() {
+ this.listenTo(this.collection, 'add', function(){
+ this.render();
+ });
+ this.listenTo(this.collection, 'reset', function(){
+ this.render();
+ });
+ this.listenTo(this.collection, 'sort', function(){
+ this.render();
+ });
+ },
+
+ render: function() {
+ var context = this.collection;
+ this.$el.html(this.template(context));
+ return this;
+ },
+
+ addNew: function() {
+ this.trigger('addNew');
+ this.render();
+ },
+
+ sortById: function(){
+ this.collection.trigger('sortById');
+ this.render();
+ },
+
+ sortByFirstName: function(){
+ this.collection.trigger('sortByFirstName');
+ this.render();
+ },
+
+ sortByLastName: function(){
+ this.collection.trigger('sortByLastName');
+ this.render();
+ }
+});
diff --git a/client/spa/js/instructor/spec/instructor.view.spec.js b/client/spa/js/instructor/spec/instructor.view.spec.js
index 7b51e47..c383339 100644
--- a/client/spa/js/instructor/spec/instructor.view.spec.js
+++ b/client/spa/js/instructor/spec/instructor.view.spec.js
@@ -69,7 +69,7 @@ describe('Instructor view ', function(){
// call delegate after spyOn
view.delegateEvents();
view.render();
- view.$('.i-edit').trigger('click');
+ view.$('.modify').trigger('click');
});
describe('when the user enters new instructor information ', function(){
@@ -77,7 +77,7 @@ describe('Instructor view ', function(){
describe('when user clicks on the cancel button', function(){
beforeEach(function(){
- view.$('.i-cancel').trigger('click');
+ view.$('.cancel').trigger('click');
});
it('cancels the user input', function(){
@@ -91,7 +91,7 @@ describe('Instructor view ', function(){
view.$('#lastName').val('changed lastName');
view.$('#skills').val('changed skills');
- view.$('.i-save').trigger('click');
+ view.$('.save').trigger('click');
});
it('updates the model', function(){
@@ -120,7 +120,7 @@ describe('Instructor view ', function(){
it('deletes the model', function(){
// Must render for the event to be fired
view.render();
- view.$('.i-delete').trigger('click');
+ view.$('.delete').trigger('click');
expect(view.destroy).toHaveBeenCalled();
expect(model.destroy).toHaveBeenCalled();
}); // end delete model test
diff --git a/client/spa/js/instructor/spec/instructors.collection.spec.js b/client/spa/js/instructor/spec/instructors.collection.spec.js
new file mode 100644
index 0000000..4fe9f2c
--- /dev/null
+++ b/client/spa/js/instructor/spec/instructors.collection.spec.js
@@ -0,0 +1,85 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var Collection = require('../instructors.collection');
+// Test suite
+console.log('test instructors.collection');
+describe('Instructors collection ', function(){
+ var collection;
+ var modelA;
+ var modelB;
+ var modelC;
+
+ beforeEach(function(){
+
+ // Set up test data
+ modelA = {id: 3, firstName: 'Jeff', lastName: 'Thomas'};
+ modelB = {id: 1, firstName: 'Tom', lastName: 'Shell'};
+ modelC = {id: 2, firstName: 'Emily', lastName: 'Row'};
+ });
+
+ describe('when models are added to the collection ', function(){
+ beforeEach(function(){
+ collection = new Collection();
+ collection.add([
+ modelA,
+ modelC,
+ modelB
+ ],
+ {silent: false} // Set to true to suppress add event
+ );
+ });
+
+ it('orders the models by the instructor id', function(){
+ collection.trigger('sortById');
+ expect(collection.at(2).get('id')).toEqual(modelA.id);
+ expect(collection.at(0).get('id')).toEqual(modelB.id);
+ expect(collection.at(1).get('id')).toEqual(modelC.id);
+ });
+ });
+
+ describe('when the collection interacts with the server', function(){
+ it('fetches from the correct url', function(){
+ collection = new Collection();
+ expect(collection.url).toEqual('/api/instructors/');
+ });
+ });
+
+ describe('when a sort event is triggered', function(){
+ beforeEach(function(){
+ collection = new Collection();
+ collection.add([
+ modelC,
+ modelB,
+ modelA
+ ],
+ {silent: false} // Set to true to suppress add event
+ );
+ });
+
+ it('sorts by id', function(){
+ collection.trigger('sortById');
+ expect(collection.at(2).get('id')).toEqual(modelA.id);
+ expect(collection.at(0).get('id')).toEqual(modelB.id);
+ expect(collection.at(1).get('id')).toEqual(modelC.id);
+ });
+
+ it('sorts by firstName', function(){
+ collection.trigger('sortByFirstName');
+ expect(collection.at(1).get('firstName')).toEqual(modelA.firstName);
+ expect(collection.at(2).get('firstName')).toEqual(modelB.firstName);
+ expect(collection.at(0).get('firstName')).toEqual(modelC.firstName);
+ });
+
+ it('sorts by lastName', function(){
+ collection.trigger('sortByLastName');
+ expect(collection.at(2).get('lastName')).toEqual(modelA.lastName);
+ expect(collection.at(1).get('lastName')).toEqual(modelB.lastName);
+ expect(collection.at(0).get('lastName')).toEqual(modelC.lastName);
+ });
+ });
+});
diff --git a/client/spa/js/instructor/spec/instructors.controller.spec.js b/client/spa/js/instructor/spec/instructors.controller.spec.js
new file mode 100644
index 0000000..b0ebe71
--- /dev/null
+++ b/client/spa/js/instructor/spec/instructors.controller.spec.js
@@ -0,0 +1,114 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var Backbone = require('../../vendor/index').Backbone;
+var Controller = require('../instructors.controller');
+var $ = require('jquery');
+var matchers = require('jasmine-jquery-matchers');
+
+// Test suite
+console.log('test instructors.controller');
+
+describe('Instructors controller', function(){
+ var controller;
+
+ beforeEach(function(){
+ controller = new Controller();
+ });
+
+ it('can be created', function(){
+ expect(controller).toBeDefined();
+ });
+
+ describe('when it is created', function(){
+ it('has the expected routes', function(){
+ expect(controller.routes).toEqual(jasmine.objectContaining({
+ 'instructors': 'showInstructors'
+ }));
+ });
+
+ it('without a container option, uses body as the container', function(){
+ expect(controller.options.container).toEqual('body');
+ });
+
+ it('with a container option, uses specified container', function(){
+ var ctrl = new Controller({container: '.newcontainer'});
+ expect(ctrl.options.container).toEqual('.newcontainer');
+ });
+ });
+
+ describe('when asked to showInstructors', function(){
+ beforeEach(function(){
+ jasmine.addMatchers(matchers);
+ });
+
+ describe('and fetch is successful', function(){
+ beforeEach(function(){
+ spyOn(Backbone.Collection.prototype, 'fetch').and.callFake(
+ function(options){
+ options.success();
+ }
+ );
+ });
+
+ it('sets up the collection if it is not already', function(){
+ expect(controller.collection).not.toBeDefined();
+ controller.showInstructors();
+ expect(controller.collection).toBeDefined();
+ });
+
+ it('uses the existing collection if it is already setup', function(){
+ controller.showInstructors();
+ controller.collection.add({id: 'xyz'});
+ controller.showInstructors();
+ expect(controller.collection.at(0).get('id')).toEqual('xyz');
+ });
+
+ it('fetches data for the collection', function(){
+ controller.showInstructors();
+ expect(controller.collection.fetch).toHaveBeenCalled();
+ });
+
+ it('sets up the view if it is not already', function(){
+ expect(controller.view).not.toBeDefined();
+ controller.showInstructors();
+ expect(controller.view).toBeDefined();
+ });
+
+ it('uses the existing view if it is already setup', function(){
+ controller.showInstructors();
+ controller.view.test = true;
+ controller.showInstructors();
+ expect(controller.view.test).toBeTruthy();
+ });
+
+ it('renders the view to the correct container', function() {
+ spyOn(controller, 'renderView').and.callThrough();
+ controller.showInstructors();
+ var returnedView = controller.renderView.calls.mostRecent().object.view;
+ expect(returnedView).toEqual(controller.view);
+ expect($('body h1')).toHaveText('Instructors');
+ });
+ });
+
+ describe('and fetch errors', function(){
+ beforeEach(function(){
+
+ spyOn(Backbone.Collection.prototype, 'fetch').and.callFake(
+ function(options){
+ options.error();
+ }
+ );
+ });
+
+ it('renders error', function(){
+ controller.showInstructors();
+ expect($('body')).toHaveText('There was a problem rendering instructors');
+ });
+ });
+});
+});
diff --git a/client/spa/js/instructor/spec/instructors.view.spec.js b/client/spa/js/instructor/spec/instructors.view.spec.js
new file mode 100644
index 0000000..fc7256e
--- /dev/null
+++ b/client/spa/js/instructor/spec/instructors.view.spec.js
@@ -0,0 +1,141 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var View = require('../instructors.view.js');
+var matchers = require('jasmine-jquery-matchers');
+var _ = require('../../vendor/index')._;
+var Backbone = require('../../vendor/index').Backbone;
+
+// Test suite
+console.log('test instructors.view');
+
+describe('Instructors view ', function(){
+ var model;
+ var collection;
+ var view;
+
+ beforeEach(function(){
+ // Add some convenience tests for working with the DOM
+ jasmine.addMatchers(matchers);
+ var Model = Backbone.Model.extend({});
+ var Collection = Backbone.Collection.extend({model: Model});
+
+ // Needs to have the fields required by the template
+ model = new Model({
+ firstName: 'Instructor <3',
+ lastName: 'new',
+ skills: 'Teaching, Cooking'
+ });
+
+ collection = new Collection(model);
+ view = new View({
+ collection: collection
+ });
+ });
+
+ describe('when the view is instantiated ', function() {
+ it('creates the correct element', function () {
+ // Element has to be uppercase
+ expect(view.el.nodeName).toEqual('DIV');
+ });
+
+ it('sets the correct class', function () {
+ view.render();
+ expect(view.$el).toHaveClass('instructors');
+ });
+ });
+
+ describe('when collection events happen', function(){
+ beforeEach(function () {
+ spyOn(view, 'render').and.callThrough();
+ });
+
+ it('renders when something is added to the collection', function(){
+ collection.trigger('add');
+ expect(view.render).toHaveBeenCalled();
+ });
+
+ it('renders when the collection is reset', function(){
+ collection.trigger('reset');
+ expect(view.render).toHaveBeenCalled();
+ });
+
+ it('renders when the collection is sorted', function(){
+ collection.trigger('sort');
+ expect(view.render).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the view is rendered', function(){
+ it('returns the view object', function(){
+ expect(view.render()).toEqual(view);
+ });
+
+ it('produces the correct HTML', function(){
+ view.render();
+ expect(view.$('h1').html()).toEqual('Instructors');
+ expect(view.$('.instructor')[0]).toHaveText('Instructor <3');
+ });
+ });
+
+ describe('when the user clicks on the Sort By Id button ', function(){
+ beforeEach(function(){
+ view.render();
+ });
+
+ it('triggers the sortById event on the collection', function(){
+ var spy = jasmine.createSpy('sortById');
+ collection.on('sortById', spy);
+ view.$('.sortById').trigger('click');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('renders the view', function(){
+ spyOn(view, 'render');
+ view.$('.sortById').trigger('click');
+ expect(view.render).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the user clicks on the Sort By First Name button ', function(){
+ beforeEach(function(){
+ view.render();
+ });
+
+ it('triggers the sortByFirstName event on the collection', function(){
+ var spy = jasmine.createSpy('sortByFirstName');
+ collection.on('sortByFirstName', spy);
+ view.$('.sortByFirstName').trigger('click');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('renders the view', function(){
+ spyOn(view, 'render');
+ view.$('.sortByFirstName').trigger('click');
+ expect(view.render).toHaveBeenCalled();
+ });
+ });
+
+ describe('when the user clicks on the Sort By Last Name button ', function(){
+ beforeEach(function(){
+ view.render();
+ });
+
+ it('triggers the sortByLastName event on the collection', function(){
+ var spy = jasmine.createSpy('sortByLastName');
+ collection.on('sortByLastName', spy);
+ view.$('.sortByLastName').trigger('click');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('renders the view', function(){
+ spyOn(view, 'render');
+ view.$('.sortByLastName').trigger('click');
+ expect(view.render).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/client/spa/js/main.js b/client/spa/js/main.js
index b1b493f..67671d0 100644
--- a/client/spa/js/main.js
+++ b/client/spa/js/main.js
@@ -4,11 +4,12 @@ window.Backbone = require('./vendor').Backbone;
// Include your code
var Instructor = require('./instructor/instructor.controller');
+var Instructors = require('./instructor/instructors.controller');
var Resource = require('./learning-resource/learning-resource.controller');
// Initialize it
window.instructor = new Instructor({router:true, container: 'body'});
+window.instructors = new Instructors({router:true, container: 'body'});
window.resource = new Resource({router:true, container: 'body'});
-
// Additional modules go here