From 40f480c685785cbc38f266290fb8ee4a5b138149 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Sun, 8 Feb 2015 15:30:12 -0800 Subject: [PATCH 1/8] Initial commit for issue43-instructors-collection --- client/spa/js/instructor/editInstructor.html | 20 +++ .../js/instructor/instructor.controller.js | 54 +++++++ client/spa/js/instructor/instructor.html | 21 +++ client/spa/js/instructor/instructor.model.js | 27 ++++ client/spa/js/instructor/instructor.view.js | 62 ++++++++ .../js/instructor/instructors.collection.js | 35 +++++ .../js/instructor/instructors.controller.js | 53 +++++++ client/spa/js/instructor/instructors.html | 9 ++ client/spa/js/instructor/instructors.view.js | 50 +++++++ .../spec/instructor.controller.spec.js | 95 ++++++++++++ .../instructor/spec/instructor.model.spec.js | 77 ++++++++++ .../instructor/spec/instructor.view.spec.js | 141 ++++++++++++++++++ .../spec/instructors.collection.spec.js | 82 ++++++++++ .../spec/instructors.controller.spec.js | 114 ++++++++++++++ .../instructor/spec/instructors.view.spec.js | 109 ++++++++++++++ common/models/instructor.js | 7 + common/models/instructor.json | 28 ++++ server/model-config.json | 4 + spec/api/instructors.spec.js | 130 ++++++++++++++++ 19 files changed, 1118 insertions(+) create mode 100644 client/spa/js/instructor/editInstructor.html create mode 100644 client/spa/js/instructor/instructor.controller.js create mode 100644 client/spa/js/instructor/instructor.html create mode 100644 client/spa/js/instructor/instructor.model.js create mode 100644 client/spa/js/instructor/instructor.view.js create mode 100644 client/spa/js/instructor/instructors.collection.js create mode 100644 client/spa/js/instructor/instructors.controller.js create mode 100644 client/spa/js/instructor/instructors.html create mode 100644 client/spa/js/instructor/instructors.view.js create mode 100644 client/spa/js/instructor/spec/instructor.controller.spec.js create mode 100644 client/spa/js/instructor/spec/instructor.model.spec.js create mode 100644 client/spa/js/instructor/spec/instructor.view.spec.js create mode 100644 client/spa/js/instructor/spec/instructors.collection.spec.js create mode 100644 client/spa/js/instructor/spec/instructors.controller.spec.js create mode 100644 client/spa/js/instructor/spec/instructors.view.spec.js create mode 100644 common/models/instructor.js create mode 100644 common/models/instructor.json create mode 100644 spec/api/instructors.spec.js diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html new file mode 100644 index 0000000..b058276 --- /dev/null +++ b/client/spa/js/instructor/editInstructor.html @@ -0,0 +1,20 @@ + +
+
+
+
+
+
+ + +
+
+
diff --git a/client/spa/js/instructor/instructor.controller.js b/client/spa/js/instructor/instructor.controller.js new file mode 100644 index 0000000..8d1254f --- /dev/null +++ b/client/spa/js/instructor/instructor.controller.js @@ -0,0 +1,54 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var $ = require('../vendor/index').$; +var Model = require('./instructor.model'); +var View = require('./instructor.view'); + +module.exports = Backbone.Controller.extend({ + routes: { + 'instructor/:id': 'showInstructor' + }, + initialize: function(){ + this.options.container = this.options.container || 'body'; + this.model = new Model(); + this.view = new View({model: this.model}); + }, + showInstructor: function(instructorId, cb){ + this.fetchModel(instructorId, function(err){ + var view; + if (err){ + view = this.renderError(); + } else { + view = this.renderView(); + } + if (cb){ + cb(err, view); + } + }.bind(this)); + }, + fetchModel: function(instructorId, cb){ + this.model.set({id: instructorId}); + 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 instructor

'); + } +}); diff --git a/client/spa/js/instructor/instructor.html b/client/spa/js/instructor/instructor.html new file mode 100644 index 0000000..448a2ac --- /dev/null +++ b/client/spa/js/instructor/instructor.html @@ -0,0 +1,21 @@ + +
+
+

<%- firstName %> <%- lastName %>

+

<%- skills %>

+ + +
+
+
diff --git a/client/spa/js/instructor/instructor.model.js b/client/spa/js/instructor/instructor.model.js new file mode 100644 index 0000000..6719d63 --- /dev/null +++ b/client/spa/js/instructor/instructor.model.js @@ -0,0 +1,27 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +module.exports = Backbone.Model.extend({ + defaults: { + firstName: '', + lastName: '', + skills: '' + }, + urlRoot: '/api/instructors', + initialize: function(){ + this.on('change', function(){ + this.trigger('foo', 'bar'); + }); + }, + validate: function(attrs){ + if (!attrs.firstName){ + return 'firstName cannot be empty'; + } + if (!attrs.lastName){ + return 'lastName cannot be empty'; + } + if (!attrs.skills){ + return 'skills cannot be empty'; + } + } +}); diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js new file mode 100644 index 0000000..e5b53d2 --- /dev/null +++ b/client/spa/js/instructor/instructor.view.js @@ -0,0 +1,62 @@ +'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 + '/instructor.html', 'utf8'); +var editTemplate = fs.readFileSync(__dirname + '/editInstructor.html', 'utf8'); + +module.exports = Backbone.View.extend({ + className: 'instructor', + template: _.template(template), + editTemplate: _.template(editTemplate), + events: { + 'click .delete': 'destroy', + 'click .edit': 'edit', + '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(); + }, + edit: function(e){ + var context = this.model.toJSON(); + this.$el.html(this.editTemplate(context)); + }, + 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(), + skills: this.$('#skills').val().trim() + }; + var validate = { + success: function() { + $('#result').addClass('success') + .html('Successfully updated instructor') + .fadeIn().delay(4000).fadeOut(); + }, + error: function(model, error) { + + } + }; + + this.model.save(formData, validate); + }, + cancel: function(e) { + e.preventDefault(); // prevent event bubbling + this.render(); + } +}); diff --git a/client/spa/js/instructor/instructors.collection.js b/client/spa/js/instructor/instructors.collection.js new file mode 100644 index 0000000..e4e2ba5 --- /dev/null +++ b/client/spa/js/instructor/instructors.collection.js @@ -0,0 +1,35 @@ +'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.trigger('sortByFirstName'); + }, + + 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(); + } +}); diff --git a/client/spa/js/instructor/instructors.controller.js b/client/spa/js/instructor/instructors.controller.js new file mode 100644 index 0000000..9cae8cb --- /dev/null +++ b/client/spa/js/instructor/instructors.controller.js @@ -0,0 +1,53 @@ +'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(); + } + 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); + 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..b14a2f4 --- /dev/null +++ b/client/spa/js/instructor/instructors.html @@ -0,0 +1,9 @@ +

Instructors

+ + + + diff --git a/client/spa/js/instructor/instructors.view.js b/client/spa/js/instructor/instructors.view.js new file mode 100644 index 0000000..55f17c8 --- /dev/null +++ b/client/spa/js/instructor/instructors.view.js @@ -0,0 +1,50 @@ +'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' + }, + + 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; + }, + + 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.controller.spec.js b/client/spa/js/instructor/spec/instructor.controller.spec.js new file mode 100644 index 0000000..0f6b216 --- /dev/null +++ b/client/spa/js/instructor/spec/instructor.controller.spec.js @@ -0,0 +1,95 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Controller = require('../instructor.controller'); +var $ = require('jquery'); +var matchers = require('jasmine-jquery-matchers'); +// Test suite +console.log('test instructor.controller'); +describe('Instructor 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({ + 'instructor/:id': 'showInstructor' + })); + }); + + 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 showInstructor', function(){ + + beforeEach(function(){ + jasmine.addMatchers(matchers); + }); + + var success = function(callbacks){ + controller.model.set({'firstName': 'valid firstName', + 'lastName': 'valid lastName', 'skills':'valid skills'}); + callbacks.success(controller.model); + }; + + var err = function(callbacks){ + callbacks.error('error', controller.model); + }; + + it('with a valid instructor 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'); + expect(controller.model.get('skills')).toEqual('valid skills'); + }; + + controller.showInstructor(1, cb); + + }); + + it('with a valid instructor 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('fake render'); + expect(view.cid).toEqual(controller.view.cid); + }; + controller.showInstructor(1, cb); + }); + + it('with an invalid instructor 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 instructor'); + }; + controller.showInstructor(1, cb); + }); + }); +}); diff --git a/client/spa/js/instructor/spec/instructor.model.spec.js b/client/spa/js/instructor/spec/instructor.model.spec.js new file mode 100644 index 0000000..bad1c3d --- /dev/null +++ b/client/spa/js/instructor/spec/instructor.model.spec.js @@ -0,0 +1,77 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Model = require('../instructor.model'); + +// Test suite +console.log('test instructor.model'); +describe('Instructor model ', function(){ + var model; + + describe('when creating a new model ', function(){ + beforeEach(function(){ + model = new Model(); + }); + + xit('initializes with custom logic', function(){ + }); + }); + + describe('when updating the model for instructor ', function(){ + var errorSpy; + + beforeEach(function(){ + errorSpy = jasmine.createSpy('Invalid'); + model = new Model({ + id: 1 + }); + model.on('invalid', errorSpy); + model.save(); + }); + + it('does not save when firstName is empty ', function(){ + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + model, + 'firstName cannot be empty', + { validate: true, validationError: 'firstName cannot be empty'} + ); + }); + + // Use if you have transformation logic on set + xit('sets the values correctly ', function(){ + + }); + + // Use if you have transformation logic on get + xit('retrieves the correct values ', function(){ + + }); + }); + + describe('when changing the state of the model ', function(){ + var eventSpy; + + beforeEach(function(){ + eventSpy = jasmine.createSpy('Change Event'); + model = new Model({ + id: 1, + firstName: 'Mike', + lastName: 'Foster' + }); + model.on('foo', eventSpy); + model.set({firstName: 'changed', lastName: 'changed'}); + }); + + it('triggers the custom event foo', function(){ + expect(eventSpy).toHaveBeenCalled(); + expect(eventSpy).toHaveBeenCalledWith( + 'bar' + ); + }); + }); +}); diff --git a/client/spa/js/instructor/spec/instructor.view.spec.js b/client/spa/js/instructor/spec/instructor.view.spec.js new file mode 100644 index 0000000..422f339 --- /dev/null +++ b/client/spa/js/instructor/spec/instructor.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('../instructor.view.js'); +var matchers = require('jasmine-jquery-matchers'); +var Backbone = require('../../vendor/index').Backbone; +// Test suite +console.log('test instructor.view'); +describe('Instructor 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: 'Jeff', + lastName: 'Thomas', + skills: 'C++, Java' + }); + + 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('instructor'); + }); + }); + + 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('Jeff Thomas'); + }); + }); + + 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.$('.edit').trigger('click'); + }); + + describe('when the user enters new instructor 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.$('#skills').val('changed skills'); + + view.$('.save').trigger('click'); + }); + + it('updates the model', function(){ + expect(view.save).toHaveBeenCalled(); + expect(Model.prototype.save).toHaveBeenCalled(); + }); + + }); + + }); + + xdescribe('when the user clicks on the Save button ', function(){ + xit('updates the model', function(){ + + }); + }); + + xdescribe('when the user clicks on ... ', function(){ + xit('triggers the ... event', function(){ + }); + }); + + }); // 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/instructor/spec/instructors.collection.spec.js b/client/spa/js/instructor/spec/instructors.collection.spec.js new file mode 100644 index 0000000..c0b9a79 --- /dev/null +++ b/client/spa/js/instructor/spec/instructors.collection.spec.js @@ -0,0 +1,82 @@ +/* +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, name: 'A'}; + modelB = {id: 1, name: 'B'}; + modelC = {id: 2, name: 'C'}; + }); + + describe('when models are added to the collection ', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelC, + modelB, + modelA + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('orders the models by the instructor id', function(){ + expect(collection.at(2).get('id')).toEqual(modelA.id); + expect(collection.at(1).get('id')).toEqual(modelB.id); + expect(collection.at(0).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(0).get('firstName')).toEqual(modelC.firstName); + expect(collection.at(1).get('firstName')).toEqual(modelA.firstName); + expect(collection.at(2).get('firstName')).toEqual(modelB.firstName); + }); + + it('sorts by lastName', function(){ + collection.trigger('sortByLastName'); + expect(collection.at(0).get('lastName')).toEqual(modelC.lastName); + expect(collection.at(1).get('lastName')).toEqual(modelA.lastName); + expect(collection.at(2).get('lastName')).toEqual(modelB.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..ab05060 --- /dev/null +++ b/client/spa/js/instructor/spec/instructors.view.spec.js @@ -0,0 +1,109 @@ +/* +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(); + }); + }); + + xdescribe('when the user clicks on the Sort By First Name button ', function(){ + xit('triggers the sortByFirstName event on the collection', function(){ + + }); + + xit('renders the view', function(){ + }); + }); +}); diff --git a/common/models/instructor.js b/common/models/instructor.js new file mode 100644 index 0000000..3c42ff8 --- /dev/null +++ b/common/models/instructor.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function(instructor) { + instructor.validatesUniquenessOf('_id', { + message: 'must be unique id' + }); +}; diff --git a/common/models/instructor.json b/common/models/instructor.json new file mode 100644 index 0000000..b2e17ec --- /dev/null +++ b/common/models/instructor.json @@ -0,0 +1,28 @@ +{ + "name": "instructor", + "base": "PersistedModel", + "idInjection": true, + "properties": { + "firstName": { + "type": "string", + "required": true + }, + "lastName": { + "type": "string", + "required": true + }, + "skills": { + "type": "string", + "required": true + } + }, + "validations": [], + "relations": { + "course": { + "type": "hasMany", + "model": "course" + } + }, + "acls": [], + "methods": [] +} diff --git a/server/model-config.json b/server/model-config.json index 19d5811..59d46ee 100644 --- a/server/model-config.json +++ b/server/model-config.json @@ -30,6 +30,10 @@ "dataSource": "db", "public": true }, + "instructor": { + "dataSource": "db", + "public": true + }, "account": { "dataSource": "db", "public": true diff --git a/spec/api/instructors.spec.js b/spec/api/instructors.spec.js new file mode 100644 index 0000000..07aaa59 --- /dev/null +++ b/spec/api/instructors.spec.js @@ -0,0 +1,130 @@ +'use strict'; +// spec/api/instructors.spec.js + +/* jshint quotmark:false */ + +// Add dependencies +var frisby = require('frisby'); +// Set up variables for info you will use often +var url = 'http://localhost:3000/api/instructors/'; +var initialRecord = { + "firstName": "Jeff", + "lastName": "Thomas", + "skills": "C++, Java" +}; +var secondInstructor = { + "firstName": "Tom", + "lastName": "Shell", + "skills": "Server, Scripting" +}; +var thirdInstructor = { + "firstName": "Emily", + "lastName": "Row", + "skills": "Scuba, Diving" +}; +var changedRecord = { + "firstName": "Jeff", + "lastName": "Thomas", + "skills": "C++, Java" +}; +var emptyRecord = { + //no properties and values +}; +var missingRecord = { + "firstName": "Jeff", + "lastName": "Thomas" +}; +var missingRecordTwo = { + "firstName": "Jeff", + "skills": "C++, Java" +}; +var missingRecordThree = { + "firstName": "Jeff", + "lastName": "Thomas" +}; +var emptyPropertyValues = { + "firstName": "", + "lastName": "", + "skills": "C++, Java" +}; + + +// Create a record +function postRecord(){ + frisby.create('Create an instructor with post') + .post(url, initialRecord, {json: true}) + .expectStatus(200) + .expectHeaderContains('content-type', 'application/json') + .expectJSON(initialRecord) + .afterJSON(function(json){ + getRecord(json.id); + }) + .toss(); +} + +// Create a record +function whichRecord(record){ + frisby.create('Create an instructor with post') + .post(url, record, {json: true}) + .expectStatus(200) + .expectHeaderContains('content-type', 'application/json') + .expectJSON(record) + .toss(); +} + +// Read a record +function getRecord(id){ + frisby.create('Get instructor using id') + .get(url + id) + .expectStatus(200) + .expectJSON(initialRecord) + .afterJSON(function(json){ + putRecord(json.id); + }) + .toss(); +} + +// Update a record +function putRecord(id){ + frisby.create('Put instructor using id') + .put(url + id, changedRecord, {json: true}) + .expectStatus(200) + .expectJSON(changedRecord) + /*.afterJSON(function(json){ + deleteRecord(json.id); + })*/ + .toss(); +} + +// Delete a record +function deleteRecord(id) { + frisby.create('Delete instructor using id') + .delete(url + id, {json: false}) + .expectStatus(204) + .toss(); +} + +// Send something that should trigger an error +function postBadRecord(record){ + frisby.create('Validation: Enforce mandatory fields when creating') + .post(url, record, {json: true}) + .expectStatus(422) + .expectHeaderContains('Content-Type', 'application/json') + .expectJSON({ + error: { + name: 'ValidationError', + details: { + codes: { + }}}}) + .toss(); +} + +postRecord(); +whichRecord(secondInstructor); +whichRecord(thirdInstructor); + +postBadRecord(emptyRecord); +postBadRecord(missingRecord); +postBadRecord(missingRecordTwo); +postBadRecord(missingRecordThree); +postBadRecord(emptyPropertyValues); From 264c34b527e43ffa51505ab637d6271d52866103 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Sun, 8 Feb 2015 15:54:36 -0800 Subject: [PATCH 2/8] Update main routers to include Instructor collections --- client/spa/js/main.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/spa/js/main.js b/client/spa/js/main.js index d450a0b..f0adbad 100644 --- a/client/spa/js/main.js +++ b/client/spa/js/main.js @@ -3,10 +3,11 @@ window.Backbone = require('./vendor').Backbone; // Include your code -//var Contact = require('./contact/contact.controller'); +var Instructor = require('./instructor/instructor.controller'); +var Instructors = require('./instructor/instructors.controller'); // Initialize it -//window.contact = new Contact({router:true, container: 'body'}); - +window.instructor = new Instructor({router:true, container: 'body'}); +window.instructors = new Instructors({router:true, container: 'body'}); // Additional modules go here From efaf6ab4632f9bb78e2b347a67beadba38404934 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Mon, 9 Feb 2015 17:53:57 -0800 Subject: [PATCH 3/8] Add fix to instructor controller and more tests for collections --- .../js/instructor/instructor.controller.js | 5 +++ client/spa/js/instructor/instructor.view.js | 2 + .../spec/instructor.controller.spec.js | 2 +- .../spec/instructors.collection.spec.js | 23 +++++++----- .../instructor/spec/instructors.view.spec.js | 37 +++++++++++++++++-- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/client/spa/js/instructor/instructor.controller.js b/client/spa/js/instructor/instructor.controller.js index 8d1254f..cbc8e2d 100644 --- a/client/spa/js/instructor/instructor.controller.js +++ b/client/spa/js/instructor/instructor.controller.js @@ -17,6 +17,10 @@ module.exports = Backbone.Controller.extend({ showInstructor: function(instructorId, cb){ this.fetchModel(instructorId, function(err){ var view; + + this.remove(); + this.view = new View({model: this.model}); + if (err){ view = this.renderError(); } else { @@ -25,6 +29,7 @@ module.exports = Backbone.Controller.extend({ if (cb){ cb(err, view); } + }.bind(this)); }, fetchModel: function(instructorId, cb){ diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js index e5b53d2..2988113 100644 --- a/client/spa/js/instructor/instructor.view.js +++ b/client/spa/js/instructor/instructor.view.js @@ -33,6 +33,8 @@ module.exports = Backbone.View.extend({ edit: 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 diff --git a/client/spa/js/instructor/spec/instructor.controller.spec.js b/client/spa/js/instructor/spec/instructor.controller.spec.js index 0f6b216..bc243e3 100644 --- a/client/spa/js/instructor/spec/instructor.controller.spec.js +++ b/client/spa/js/instructor/spec/instructor.controller.spec.js @@ -76,7 +76,7 @@ describe('Instructor controller', function(){ return controller.view; }); var cb = function(err, view){ - expect($('body')).toHaveText('fake render'); + expect($('body')).toHaveText(''); expect(view.cid).toEqual(controller.view.cid); }; controller.showInstructor(1, cb); diff --git a/client/spa/js/instructor/spec/instructors.collection.spec.js b/client/spa/js/instructor/spec/instructors.collection.spec.js index c0b9a79..4fe9f2c 100644 --- a/client/spa/js/instructor/spec/instructors.collection.spec.js +++ b/client/spa/js/instructor/spec/instructors.collection.spec.js @@ -1,3 +1,5 @@ +'use strict'; + /* global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, spyOn @@ -15,27 +17,28 @@ describe('Instructors collection ', function(){ beforeEach(function(){ // Set up test data - modelA = {id: 3, name: 'A'}; - modelB = {id: 1, name: 'B'}; - modelC = {id: 2, name: 'C'}; + 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, - modelA + 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(1).get('id')).toEqual(modelB.id); - expect(collection.at(0).get('id')).toEqual(modelC.id); + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); }); }); @@ -67,16 +70,16 @@ describe('Instructors collection ', function(){ it('sorts by firstName', function(){ collection.trigger('sortByFirstName'); - expect(collection.at(0).get('firstName')).toEqual(modelC.firstName); 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); - expect(collection.at(1).get('lastName')).toEqual(modelA.lastName); - expect(collection.at(2).get('lastName')).toEqual(modelB.lastName); }); }); }); diff --git a/client/spa/js/instructor/spec/instructors.view.spec.js b/client/spa/js/instructor/spec/instructors.view.spec.js index ab05060..cd178be 100644 --- a/client/spa/js/instructor/spec/instructors.view.spec.js +++ b/client/spa/js/instructor/spec/instructors.view.spec.js @@ -1,3 +1,5 @@ +'use strict'; + /* global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, spyOn @@ -98,12 +100,41 @@ describe('Instructors view ', function(){ }); }); - xdescribe('when the user clicks on the Sort By First Name button ', function(){ - xit('triggers the sortByFirstName event on the collection', function(){ + 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(); }); - xit('renders the view', function(){ + 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(); }); }); }); From 2b1dc40c836756cfb93f9bcd6b6a64f19cc87e14 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Sat, 14 Feb 2015 15:09:57 -0800 Subject: [PATCH 4/8] Fix issues with edit buttons --- client/spa/js/instructor/editInstructor.html | 4 ++-- client/spa/js/instructor/instructor.html | 4 ++-- client/spa/js/instructor/instructor.view.js | 10 +++++----- client/spa/js/instructor/spec/instructor.view.spec.js | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html index ca8ff13..b058276 100644 --- a/client/spa/js/instructor/editInstructor.html +++ b/client/spa/js/instructor/editInstructor.html @@ -13,8 +13,8 @@


- - + + 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.view.js b/client/spa/js/instructor/instructor.view.js index eb9bf32..77ef5d6 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)); 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 From fc40b5828f3b708c5cc8cb02327b443c19c29700 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Mon, 16 Feb 2015 00:06:13 -0800 Subject: [PATCH 5/8] Fix save button for instructors --- client/spa/js/instructor/editInstructor.html | 6 +++--- client/spa/js/instructor/instructor.model.js | 2 +- client/spa/js/instructor/instructor.view.js | 20 ++++++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html index b058276..e7ce1d8 100644 --- a/client/spa/js/instructor/editInstructor.html +++ b/client/spa/js/instructor/editInstructor.html @@ -10,9 +10,9 @@
-
-
-
+
+
+
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 77ef5d6..1eeea59 100644 --- a/client/spa/js/instructor/instructor.view.js +++ b/client/spa/js/instructor/instructor.view.js @@ -44,21 +44,33 @@ 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(''); } }); From e5201173f23b9f2f94c46913d9d7b371fd18f92c Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Wed, 18 Feb 2015 16:22:40 -0800 Subject: [PATCH 6/8] Add theming and addNew for Collections --- client/css/style.css | 43 +++++++++++++++++++ client/spa-index.html | 15 ++++--- client/spa/js/instructor/editInstructor.html | 28 +++++------- .../js/instructor/instructor.controller.js | 15 ++++++- client/spa/js/instructor/instructor.html | 22 +++------- client/spa/js/instructor/instructor.view.js | 23 ++++------ .../js/instructor/instructors.collection.js | 11 ++++- .../js/instructor/instructors.controller.js | 6 +++ client/spa/js/instructor/instructors.html | 25 +++++++---- client/spa/js/instructor/instructors.view.js | 8 +++- .../instructor/spec/instructors.view.spec.js | 1 + 11 files changed, 129 insertions(+), 68 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index d2df23d..56b0a81 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -185,3 +185,46 @@ div .txt-help { background: #EFEFEF; padding: 30px; } + +/* Instructors */ +.instructor .main, .instructors .main { + background: #555; + width: 700px; + margin: auto; + color: #fff; + padding-bottom: 1em; + border-radius: 10px; +} +.instructor h1, .instructors h1 { + text-align: center; + margin: 5px 0 1em; +} +.instructor p, .instructors p { + font-size: 1.2em; +} +.instructor #single, .instructors #single { + padding-bottom: 2em; +} +.instructor .content, .instructors .content { + padding: 2em 4em 1em; +} +#result, #added { + padding: 1em 0 0; +} +.success { + color: #8DC63F; +} +button { + margin-right: 5px; +} +input { + margin: 0 0 1em 0; + color: #000; +} +#collections ul { + padding: 1em 0 0; +} +#collections li { + list-style: none; + padding: 0 0 1em; +} diff --git a/client/spa-index.html b/client/spa-index.html index d48068c..8040132 100644 --- a/client/spa-index.html +++ b/client/spa-index.html @@ -1,13 +1,16 @@ - - SPA - - + + SPA + + + + + -Hi, I'm the spa index from the spa folder. - + Hi, I'm the spa index from the spa folder. + diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html index e7ce1d8..457bf72 100644 --- a/client/spa/js/instructor/editInstructor.html +++ b/client/spa/js/instructor/editInstructor.html @@ -1,20 +1,14 @@ - -
-
+
+
-
-
-
- - +
+ + + +
+ + +
-
+
diff --git a/client/spa/js/instructor/instructor.controller.js b/client/spa/js/instructor/instructor.controller.js index 8701efa..64c8d1e 100644 --- a/client/spa/js/instructor/instructor.controller.js +++ b/client/spa/js/instructor/instructor.controller.js @@ -7,7 +7,8 @@ var View = require('./instructor.view'); module.exports = Backbone.Controller.extend({ routes: { - 'instructors/:id': 'showInstructor' + 'instructors/:id': 'showInstructor', + 'instructors/new': 'addInstructor' }, initialize: function(){ this.options.container = this.options.container || 'body'; @@ -18,12 +19,13 @@ module.exports = Backbone.Controller.extend({ this.fetchModel(instructorId, function(err){ var view; - this.remove(); + this.view.remove(); this.view = new View({model: this.model}); if (err){ view = this.renderError(); } else { + this.view.template = this.view.showTemplate; view = this.renderView(); } if (cb){ @@ -32,6 +34,14 @@ module.exports = Backbone.Controller.extend({ }.bind(this)); }, + addInstructor: function() { + this.model = new Model(); + this.model.isNew = true; + + this.view.remove(); + this.view.template = this.view.editTemplate; + this.renderView(); + }, fetchModel: function(instructorId, cb){ this.model.set({id: instructorId}); this.model.fetch({ @@ -50,6 +60,7 @@ module.exports = Backbone.Controller.extend({ }, renderView: function(){ this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); // delegate for add in collections return this.view; }, renderError: function(){ diff --git a/client/spa/js/instructor/instructor.html b/client/spa/js/instructor/instructor.html index cea4e70..1fc39e9 100644 --- a/client/spa/js/instructor/instructor.html +++ b/client/spa/js/instructor/instructor.html @@ -1,21 +1,9 @@ - -
-
+
+

<%- firstName %> <%- lastName %>

<%- skills %>

- - + +
-
+ diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js index 1eeea59..6bee768 100644 --- a/client/spa/js/instructor/instructor.view.js +++ b/client/spa/js/instructor/instructor.view.js @@ -10,6 +10,7 @@ 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 .delete': 'destroy', @@ -44,15 +45,18 @@ module.exports = Backbone.View.extend({ lastName: this.$('#lastName').val().trim(), skills: this.$('#skills').val().trim() }; + 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 + .html('Successfully updated instructor') + .fadeIn().delay(4000).fadeOut(); }, error: function(model, errors) { - this.showErrors(errors); + /*_.each(errors, function (err) { + $('#result').addClass('error'); + fields.find('.help-inline').text(err); + }, this);*/ } }; @@ -61,16 +65,5 @@ module.exports = Backbone.View.extend({ 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/instructors.collection.js b/client/spa/js/instructor/instructors.collection.js index e4e2ba5..dc1c9cb 100644 --- a/client/spa/js/instructor/instructors.collection.js +++ b/client/spa/js/instructor/instructors.collection.js @@ -9,7 +9,8 @@ module.exports = Backbone.Collection.extend({ this.on('sortById', this.sortById); this.on('sortByFirstName', this.sortByFirstName); this.on('sortByLastName', this.sortByLastName); - this.trigger('sortByFirstName'); + this.on('addNew', this.addNew); + this.trigger('sortById'); }, sortById: function(){ @@ -31,5 +32,13 @@ module.exports = Backbone.Collection.extend({ 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 index 9cae8cb..cffaef4 100644 --- a/client/spa/js/instructor/instructors.controller.js +++ b/client/spa/js/instructor/instructors.controller.js @@ -24,11 +24,16 @@ module.exports = Backbone.Controller.extend({ 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(); @@ -44,6 +49,7 @@ module.exports = Backbone.Controller.extend({ }, renderView: function(){ this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); return this.view; }, renderError: function(){ diff --git a/client/spa/js/instructor/instructors.html b/client/spa/js/instructor/instructors.html index b14a2f4..4734408 100644 --- a/client/spa/js/instructor/instructors.html +++ b/client/spa/js/instructor/instructors.html @@ -1,9 +1,16 @@ -

Instructors

- - - -
    - <% _.each( models, function( model ){ %> -
  • <%- model.attributes.firstName %> <%- model.attributes.lastName %>
  • - <% }); %> -
+
+
+

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 index 55f17c8..daa2f03 100644 --- a/client/spa/js/instructor/instructors.view.js +++ b/client/spa/js/instructor/instructors.view.js @@ -6,13 +6,15 @@ 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 .sortByLastName': 'sortByLastName', + 'click .addNew': 'addNew' }, initialize: function() { @@ -33,6 +35,10 @@ module.exports = Backbone.View.extend({ return this; }, + addNew: function() { + this.trigger('addNew'); + }, + sortById: function(){ this.collection.trigger('sortById'); this.render(); diff --git a/client/spa/js/instructor/spec/instructors.view.spec.js b/client/spa/js/instructor/spec/instructors.view.spec.js index cd178be..fc7256e 100644 --- a/client/spa/js/instructor/spec/instructors.view.spec.js +++ b/client/spa/js/instructor/spec/instructors.view.spec.js @@ -69,6 +69,7 @@ describe('Instructors view ', function(){ expect(view.render).toHaveBeenCalled(); }); }); + describe('when the view is rendered', function(){ it('returns the view object', function(){ expect(view.render()).toEqual(view); From f0506f6de3ea1c435db6760ba2fa4cac380e2996 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Wed, 18 Feb 2015 16:46:05 -0800 Subject: [PATCH 7/8] Fix styling issue buttons --- client/css/style.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index 56b0a81..e0ec122 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -189,7 +189,7 @@ div .txt-help { /* Instructors */ .instructor .main, .instructors .main { background: #555; - width: 700px; + max-width: 700px; margin: auto; color: #fff; padding-bottom: 1em; @@ -214,8 +214,8 @@ div .txt-help { .success { color: #8DC63F; } -button { - margin-right: 5px; +button.btn { + margin: 0 5px 10px 0; } input { margin: 0 0 1em 0; From 8eb1e2f5658e7ad1ab733d4bdbc377fe61ce6d35 Mon Sep 17 00:00:00 2001 From: "frances.go" Date: Mon, 23 Feb 2015 23:43:15 -0800 Subject: [PATCH 8/8] Fix success message for add or edit template --- client/css/style.css | 20 +++++++++++++------ .../js/instructor/instructor.controller.js | 3 ++- client/spa/js/instructor/instructor.view.js | 20 ++++++++++++++----- client/spa/js/instructor/instructors.view.js | 1 + 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/client/css/style.css b/client/css/style.css index e0ec122..c0bad72 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -187,7 +187,8 @@ div .txt-help { } /* Instructors */ -.instructor .main, .instructors .main { +.instructor .main, .instructors .main, +.course .main, .courses .main { background: #555; max-width: 700px; margin: auto; @@ -195,17 +196,21 @@ div .txt-help { padding-bottom: 1em; border-radius: 10px; } -.instructor h1, .instructors h1 { +.instructor h1, .instructors h1, +.course h1, .courses h1 { text-align: center; margin: 5px 0 1em; } -.instructor p, .instructors p { +.instructor p, .instructors p, +.course p, .course p { font-size: 1.2em; } -.instructor #single, .instructors #single { +.instructor #single, .instructors #single, +.course #single, .courses #single { padding-bottom: 2em; } -.instructor .content, .instructors .content { +.instructor .content, .instructors .content, +.course .content, .courses .content { padding: 2em 4em 1em; } #result, #added { @@ -217,10 +222,13 @@ div .txt-help { button.btn { margin: 0 5px 10px 0; } -input { +input, select { margin: 0 0 1em 0; color: #000; } +option { + padding: 8px 10px; +} #collections ul { padding: 1em 0 0; } diff --git a/client/spa/js/instructor/instructor.controller.js b/client/spa/js/instructor/instructor.controller.js index 64c8d1e..f0d5e7b 100644 --- a/client/spa/js/instructor/instructor.controller.js +++ b/client/spa/js/instructor/instructor.controller.js @@ -36,9 +36,10 @@ module.exports = Backbone.Controller.extend({ }, addInstructor: function() { this.model = new Model(); - this.model.isNew = true; + this.model.isNew(); this.view.remove(); + this.view.template = this.view.editTemplate; this.renderView(); }, diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js index 6bee768..4926226 100644 --- a/client/spa/js/instructor/instructor.view.js +++ b/client/spa/js/instructor/instructor.view.js @@ -26,6 +26,11 @@ 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(){ @@ -38,7 +43,8 @@ module.exports = Backbone.View.extend({ 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(), @@ -51,12 +57,16 @@ module.exports = Backbone.View.extend({ $('#result').addClass('success') .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) { - /*_.each(errors, function (err) { - $('#result').addClass('error'); - fields.find('.help-inline').text(err); - }, this);*/ } }; diff --git a/client/spa/js/instructor/instructors.view.js b/client/spa/js/instructor/instructors.view.js index daa2f03..3257946 100644 --- a/client/spa/js/instructor/instructors.view.js +++ b/client/spa/js/instructor/instructors.view.js @@ -37,6 +37,7 @@ module.exports = Backbone.View.extend({ addNew: function() { this.trigger('addNew'); + this.render(); }, sortById: function(){