diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html
index ca8ff13..e7ce1d8 100644
--- a/client/spa/js/instructor/editInstructor.html
+++ b/client/spa/js/instructor/editInstructor.html
@@ -10,11 +10,11 @@
diff --git a/client/spa/js/instructor/instructor.html b/client/spa/js/instructor/instructor.html
index 7db63c3..cea4e70 100644
--- a/client/spa/js/instructor/instructor.html
+++ b/client/spa/js/instructor/instructor.html
@@ -14,8 +14,8 @@
<%- 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..e93f400 100644
--- a/client/spa/js/instructor/instructor.view.js
+++ b/client/spa/js/instructor/instructor.view.js
@@ -12,10 +12,10 @@ module.exports = Backbone.View.extend({
template: _.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);
@@ -30,7 +30,7 @@ module.exports = Backbone.View.extend({
destroy: function(){
this.model.destroy();
},
- edit: function(e){
+ modify: function(e){
var context = this.model.toJSON();
this.$el.html(this.editTemplate(context));
@@ -44,21 +44,34 @@ module.exports = Backbone.View.extend({
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();
+ this.hideErrors(); // hide if successful for validation array to work
},
- error: function(model, error) {
-
+ error: function(model, errors) {
+ this.showErrors(errors);
}
};
- this.model.save(formData, validate);
+ this.model.save(formData, check);
},
cancel: function(e) {
e.preventDefault(); // prevent event bubbling
this.render();
+ },
+ showErrors: function(errors) {
+ _.each(errors, function (error) {
+ var fields = this.$('.' + error.name);
+ fields.addClass('error');
+ fields.find('.help-inline').text(error.message);
+ }, this);
+ },
+
+ hideErrors: function () {
+ this.$('.textfield').removeClass('error');
+ this.$('.help-inline').text('');
}
});
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/main.js b/client/spa/js/main.js
index b1b493f..3b49c64 100644
--- a/client/spa/js/main.js
+++ b/client/spa/js/main.js
@@ -5,10 +5,11 @@ window.Backbone = require('./vendor').Backbone;
// Include your code
var Instructor = require('./instructor/instructor.controller');
var Resource = require('./learning-resource/learning-resource.controller');
+var Student = require('./student/student.controller');
// Initialize it
window.instructor = new Instructor({router:true, container: 'body'});
window.resource = new Resource({router:true, container: 'body'});
-
+window.student = new Student({router:true, container: 'body'});
// Additional modules go here
diff --git a/client/spa/js/student/editStudent.html b/client/spa/js/student/editStudent.html
new file mode 100644
index 0000000..5bb7e59
--- /dev/null
+++ b/client/spa/js/student/editStudent.html
@@ -0,0 +1,10 @@
+
diff --git a/client/spa/js/student/spec/student.controller.spec.js b/client/spa/js/student/spec/student.controller.spec.js
new file mode 100644
index 0000000..0294529
--- /dev/null
+++ b/client/spa/js/student/spec/student.controller.spec.js
@@ -0,0 +1,94 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var Controller = require('../student.controller');
+var $ = require('jquery');
+var matchers = require('jasmine-jquery-matchers');
+// Test suite
+console.log('test student.controller');
+describe('student 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({
+ 'students/:id': 'showStudent'
+ }));
+ });
+
+ 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 calling showstudent', function(){
+
+ beforeEach(function(){
+ jasmine.addMatchers(matchers);
+ });
+
+ var success = function(callbacks){
+ controller.model.set({'firstName': 'valid firstName',
+ 'lastName': 'valid lastName'});
+ callbacks.success(controller.model);
+ };
+
+ var err = function(callbacks){
+ callbacks.error('error', controller.model);
+ };
+
+ it('with a valid student id, fetches the model', function(){
+ spyOn(controller.model, 'fetch').and.callFake(success);
+ var cb = function(err, view){
+ expect(err).toBeNull();
+ expect(controller.model.get('firstName')).toEqual('valid firstName');
+ expect(controller.model.get('lastName')).toEqual('valid lastName');
+ };
+
+ controller.showStudent(1, cb);
+
+ });
+
+ it('with a valid student id, renders the view', function(){
+ spyOn(controller.model, 'fetch').and.callFake(success);
+ spyOn(controller.view, 'render').and.callFake(function(){
+ controller.view.$el = 'fake render';
+ return controller.view;
+ });
+ var cb = function(err, view){
+ expect($('body')).toHaveText('');
+ expect(view.cid).toEqual(controller.view.cid);
+ };
+ controller.showStudent(1, cb);
+ });
+
+ it('with an invalid student id, renders an error message', function(){
+ spyOn(controller.model, 'fetch').and.callFake(err);
+ var cb = function(err, view){
+ expect(err).toBeTruthy();
+ expect($('body')).toHaveText(
+ 'There was a problem rendering this student');
+ };
+ controller.showStudent('whatid', cb);
+ });
+ });
+});
diff --git a/client/spa/js/student/spec/student.model.spec.js b/client/spa/js/student/spec/student.model.spec.js
new file mode 100644
index 0000000..7104bb8
--- /dev/null
+++ b/client/spa/js/student/spec/student.model.spec.js
@@ -0,0 +1,89 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var Model = require('../student.model');
+
+// Test suite
+console.log('test student.model');
+describe('student model ', function(){
+ var model;
+
+ describe('when creating a new model ', function(){
+ beforeEach(function(){
+ model = new Model();
+ });
+
+ it('has the expected routes', function(){
+ expect(model.urlRoot).toEqual('/api/students');
+ });
+ });
+
+ describe('when updating the model for student with errorSpy ', function(){
+ var errorSpy;
+
+ beforeEach(function(){
+ errorSpy = jasmine.createSpy('Invalid');
+ model = new Model({
+ id: 1,
+ firstName: 'Frances',
+ lastName: 'Go'
+ });
+ model.on('invalid', errorSpy);
+ });
+
+ it('does not save when firstName is empty ', function(){
+ model.set('firstName', null);
+ model.save();
+ expect(errorSpy).toHaveBeenCalled();
+ expect(errorSpy.calls.mostRecent().args[0]).toBe(model);
+ expect(errorSpy.calls.mostRecent().args[1][0]).toEqual(
+ 'firstName cannot be empty');
+ });
+
+ it('does not save when lastName is empty ', function(){
+ model.set('lastName', null);
+ model.save();
+ expect(errorSpy).toHaveBeenCalled();
+ expect(errorSpy.calls.mostRecent().args[0]).toBe(model);
+ expect(errorSpy.calls.mostRecent().args[1][0]).toEqual(
+ 'lastName cannot be empty');
+ });
+ });
+
+ describe('when changing the state of the model without errorSpy', function(){
+
+ beforeEach(function(){
+
+ model = new Model({
+ id: 1,
+ firstName: 'Mike',
+ lastName: 'Foster'
+ });
+
+ });
+
+ it('does not save when firstName is empty ', function(){
+ model.set('firstName', null);
+ model.save();
+ expect(model.validationError).toEqual(['firstName cannot be empty']);
+ });
+
+ it('does not save when lastName is empty ', function(){
+ model.set('lastName', null);
+ model.save();
+ expect(model.validationError).toEqual(['lastName cannot be empty']);
+ });
+
+ it('does not save when firstName and lastName are empty ', function(){
+ model.set({firstName:null, lastName:null});
+ model.save();
+ expect(model.validationError).toEqual(['firstName cannot be empty',
+ 'lastName cannot be empty']);
+ });
+
+ });
+});
diff --git a/client/spa/js/student/spec/student.view.spec.js b/client/spa/js/student/spec/student.view.spec.js
new file mode 100644
index 0000000..8af96d9
--- /dev/null
+++ b/client/spa/js/student/spec/student.view.spec.js
@@ -0,0 +1,128 @@
+'use strict';
+
+/*
+global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit,
+spyOn
+*/
+// Get the code you want to test
+var View = require('../student.view.js');
+var matchers = require('jasmine-jquery-matchers');
+var Backbone = require('../../vendor/index').Backbone;
+// Test suite
+console.log('test student.view');
+describe('student view ', function(){
+
+ var model;
+ var view;
+ var Model;
+
+ beforeEach(function(){
+
+ // Add some convenience tests for working with the DOM
+ jasmine.addMatchers(matchers);
+ Model = Backbone.Model.extend({});
+
+ spyOn(Model.prototype, 'save');
+
+ // Needs to have the fields required by the template
+ model = new Model({
+ firstName: 'Jojo',
+ lastName: 'Jones'
+ });
+
+ view = new View({
+ model: model
+ });
+ });
+
+ 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(){
+ expect(view.$el).toHaveClass('student');
+ });
+ });
+
+ 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('Jojo Jones');
+ });
+ });
+
+ describe('when the user clicks on the Edit button ', function(){
+ beforeEach(function(){
+ // do all spyOn before rendering
+ spyOn(view, 'save').and.callThrough();
+ spyOn(view, 'cancel').and.callThrough();
+ // call delegate after spyOn
+ view.delegateEvents();
+ view.render();
+ view.$('.modify').trigger('click');
+ });
+
+ describe('when the user enters new student information ', function(){
+
+ describe('when user clicks on the cancel button', function(){
+
+ beforeEach(function(){
+ view.$('.cancel').trigger('click');
+ });
+
+ it('cancels the user input', function(){
+ expect(view.cancel).toHaveBeenCalled();
+ });
+ });
+
+ describe('when user clicks on the save button', function(){
+ beforeEach(function(){
+ view.$('#firstName').val('changed firstName');
+ view.$('#lastName').val('changed lastName');
+
+ view.$('.save').trigger('click');
+ });
+
+ it('updates the model', function(){
+ expect(view.save).toHaveBeenCalled();
+ expect(Model.prototype.save).toHaveBeenCalled();
+ });
+
+ });
+
+ });
+
+ }); // end edit/update test
+
+ describe('when the user clicks on the Delete button ', function(){
+
+ beforeEach(function(){
+
+ // Must call through otherwise the actual view function won't be called
+ spyOn(view, 'destroy').and.callThrough();
+
+ // Must delegateEvents for the spy on a DOM event to work
+ view.delegateEvents();
+ spyOn(model, 'destroy');
+ });
+
+ it('deletes the model', function(){
+ // Must render for the event to be fired
+ view.render();
+ view.$('.delete').trigger('click');
+ expect(view.destroy).toHaveBeenCalled();
+ expect(model.destroy).toHaveBeenCalled();
+ }); // end delete model test
+
+ }); // end delete
+
+}); // end entire suite
diff --git a/client/spa/js/student/student.controller.js b/client/spa/js/student/student.controller.js
new file mode 100644
index 0000000..9e198d8
--- /dev/null
+++ b/client/spa/js/student/student.controller.js
@@ -0,0 +1,69 @@
+'use strict';
+
+var Backbone = require('../vendor/index').Backbone;
+var $ = require('../vendor/index').$;
+var Model = require('./student.model');
+var View = require('./student.view');
+
+module.exports = Backbone.Controller.extend({
+
+ routes: {
+ 'students/:id': 'showStudent'
+ },
+
+ initialize: function(){
+ this.options.container = this.options.container || 'body';
+ this.model = new Model();
+ this.view = new View({model: this.model});
+ },
+
+ showStudent: function(studentId, cb){
+ this.fetchModel(studentId, function(err){
+ var view;
+
+ if (err){
+ view = this.renderError();
+ } else {
+ view = this.renderView();
+ }
+
+ if (cb){
+ cb(err, view);
+ }
+
+ }.bind(this));
+ },
+
+ fetchModel: function(studentId, cb){
+ this.model.set({id: studentId});
+
+ this.model.fetch({
+ success: function(model, response, options){
+ //console.log(model);
+ cb(null, model);
+ },
+ error: function(model, response, options){
+ //console.error(response);
+ cb(response, model);
+ }
+ });
+ },
+
+ renderToContainer: function(view){
+ return $(this.options.container).html(view);
+ },
+
+ renderView: function(){
+ this.renderToContainer(this.view.render().$el);
+ return this.view;
+ },
+
+ renderError: function(){
+ return this.renderToContainer(
+ 'There was a problem rendering this student
');
+ }
+
+});
+
+
+
diff --git a/client/spa/js/student/student.html b/client/spa/js/student/student.html
new file mode 100644
index 0000000..cc046ab
--- /dev/null
+++ b/client/spa/js/student/student.html
@@ -0,0 +1,8 @@
+
+
+
<%- firstName %> <%- lastName %>
+
+
+
+
+
diff --git a/client/spa/js/student/student.model.js b/client/spa/js/student/student.model.js
new file mode 100644
index 0000000..c6a23a7
--- /dev/null
+++ b/client/spa/js/student/student.model.js
@@ -0,0 +1,29 @@
+'use strict';
+
+var Backbone = require('../vendor/index').Backbone;
+
+module.exports = Backbone.Model.extend({
+ defaults: {
+ firstName: '',
+ lastName: ''
+ },
+
+ urlRoot: '/api/students',
+
+ initialize: function(){
+ this.on('change', function(){
+ this.trigger('foo', 'bar');
+ });
+ },
+
+ validate: function(attrs){
+ var errors = [];
+ if (!attrs.firstName){
+ errors.push('firstName cannot be empty');
+ }
+ if (!attrs.lastName){
+ errors.push('lastName cannot be empty');
+ }
+ return errors.length > 0 ? errors: false;
+ }
+});
diff --git a/client/spa/js/student/student.view.js b/client/spa/js/student/student.view.js
new file mode 100644
index 0000000..e8956ce
--- /dev/null
+++ b/client/spa/js/student/student.view.js
@@ -0,0 +1,87 @@
+'use strict';
+var Backbone = require('../vendor/index').Backbone;
+var _ = require('../vendor/index')._;
+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 + '/student.html', 'utf8');
+var editTemplate = fs.readFileSync(__dirname + '/editStudent.html', 'utf8');
+
+module.exports = Backbone.View.extend({
+
+ className: 'student',
+
+ template: _.template(template),
+
+ editTemplate: _.template(editTemplate),
+
+ events: {
+ 'click .delete': 'destroy',
+ 'click .modify': 'modify',
+ 'click .save': 'save',
+ 'click .cancel': 'cancel'
+ },
+
+ initialize: function(){
+ this.listenTo(this.model, 'destroy', this.remove);
+ this.listenTo(this.model, 'change', this.render);
+ },
+
+ render: function(){
+ var context = this.model.toJSON();
+ this.$el.html(this.template(context));
+ return this;
+ },
+
+ destroy: function(){
+ this.model.destroy();
+ },
+
+ 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
+
+ var formData = {
+ firstName: this.$('#firstName').val().trim(),
+ lastName: this.$('#lastName').val().trim()
+ };
+ var check = {
+ success: function() {
+ $('#result').addClass('success')
+ .html('Successfully updated student')
+ .fadeIn().delay(4000).fadeOut();
+ this.hideErrors(); // hide if successful for validation array to work
+ },
+ error: function(model, errors) {
+ this.showErrors(errors);
+ }
+ };
+
+ this.model.save(formData, check);
+ },
+
+ cancel: function(e) {
+ e.preventDefault(); // prevent event bubbling
+ this.render();
+ },
+
+ showErrors: function(errors) {
+ _.each(errors, function (error) {
+ var fields = this.$('.' + error.name);
+ fields.addClass('error');
+ fields.find('.help-inline').text(error.message);
+ }, this);
+ },
+
+ hideErrors: function () {
+ this.$('.textfield').removeClass('error');
+ this.$('.help-inline').text('');
+ }
+
+});