diff --git a/tim_miller/.gitignore b/tim_miller/.gitignore
index 6b84670..57a37f9 100644
--- a/tim_miller/.gitignore
+++ b/tim_miller/.gitignore
@@ -3,3 +3,6 @@
**/db
**/tim_miller/db
build/**
+**/*bundle.js
+
+
diff --git a/tim_miller/app/index.html b/tim_miller/app/index.html
index c6337f3..c18eb47 100644
--- a/tim_miller/app/index.html
+++ b/tim_miller/app/index.html
@@ -2,15 +2,99 @@
- Wow such website!
+ Cops!
+
+
-
-
- Bad Boys Bad Boys
- {{greeting}}
-
-
+
+ Bad Boys Bad Boys
+
+
+ {{outcome}}
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tim_miller/app/js/busted/busted.js b/tim_miller/app/js/busted/busted.js
new file mode 100644
index 0000000..dacc4e0
--- /dev/null
+++ b/tim_miller/app/js/busted/busted.js
@@ -0,0 +1,4 @@
+module.exports = function(app) {
+ require('./controllers/busted_controller.js')(app);
+
+};
diff --git a/tim_miller/app/js/busted/controllers/busted_controller.js b/tim_miller/app/js/busted/controllers/busted_controller.js
new file mode 100644
index 0000000..a7b02b9
--- /dev/null
+++ b/tim_miller/app/js/busted/controllers/busted_controller.js
@@ -0,0 +1,15 @@
+module.exports = function(app) {
+ app.controller('BustedController', ['$scope', '$http', function($scope, $http) {
+
+ $scope.busted = function() {
+ $http.get('/api/busted')
+ .then(function(res) {
+ $scope.outcome = res.data;
+ $scope.$broadcast('busted', res);
+
+ }, function(err) {
+ console.log(err.data);
+ });
+ };
+ }]);
+};
diff --git a/tim_miller/app/js/directives/directives.js b/tim_miller/app/js/directives/directives.js
new file mode 100644
index 0000000..ad536ea
--- /dev/null
+++ b/tim_miller/app/js/directives/directives.js
@@ -0,0 +1,5 @@
+module.exports = function(app) {
+ require('./form_directive/form_directive.js')(app);
+ require('./list_directive/list_directive.js')(app);
+ require('./view_directive/view_directive.js')(app);
+};
diff --git a/tim_miller/app/js/directives/form_directive/form_directive.js b/tim_miller/app/js/directives/form_directive/form_directive.js
new file mode 100644
index 0000000..afd37b2
--- /dev/null
+++ b/tim_miller/app/js/directives/form_directive/form_directive.js
@@ -0,0 +1,17 @@
+module.exports = function(app) {
+ app.directive('formDirective', function() {
+ return {
+ restrict: 'AC',
+ replace: true,
+ templateUrl: 'templates/form_template.html',
+ transclude: true,
+ scope: {
+ buttonText: '@',
+ headingText: '@',
+ formName: '@',
+ resource: '=',
+ save: '&'
+ }
+ };
+ });
+};
diff --git a/tim_miller/app/js/directives/list_directive/list_directive.js b/tim_miller/app/js/directives/list_directive/list_directive.js
new file mode 100644
index 0000000..07d896f
--- /dev/null
+++ b/tim_miller/app/js/directives/list_directive/list_directive.js
@@ -0,0 +1,14 @@
+module.exports = function(app) {
+ app.directive('listDirective', function() {
+ return {
+ restrict: 'AC',
+ replace: true,
+ templateUrl: 'templates/list_template.html',
+ scope: {
+ name: '=',
+ attribute: '=',
+ attributeTitle: '@'
+ }
+ };
+ });
+};
diff --git a/tim_miller/app/js/entry.js b/tim_miller/app/js/entry.js
index 45343e2..b549332 100644
--- a/tim_miller/app/js/entry.js
+++ b/tim_miller/app/js/entry.js
@@ -1,11 +1,10 @@
require('angular/angular');
+require('angular-route');
var angular = window.angular;
-var officerApp = angular.module('officersAndFelons', []);
-officerApp.controller('OfficerController', ['$scope', function($scope) {
- $scope.greeting = 'What cha gonna do when the come for you?';
-
- $scope.alertGreeting = function() {
- alert($scope.greeting);
- };
-}]);
+var officerAndFelonApp = angular.module('OfficerAndFelonApp', ['ngRoute']);
+require('./directives/directives.js')(officerAndFelonApp);
+require('./services/services.js')(officerAndFelonApp);
+require('./officers/officers.js')(officerAndFelonApp);
+require('./felons/felons.js')(officerAndFelonApp);
+require('./busted/busted.js')(officerAndFelonApp);
diff --git a/tim_miller/app/js/felons/controllers/felon_controller.js b/tim_miller/app/js/felons/controllers/felon_controller.js
new file mode 100644
index 0000000..e5a40fb
--- /dev/null
+++ b/tim_miller/app/js/felons/controllers/felon_controller.js
@@ -0,0 +1,61 @@
+module.exports = function(app) {
+ app.controller('FelonsController', ['$scope', '$http', 'restFunctions', function($scope, $http, restFunctions) {
+ $scope.felons = [];
+ $scope.errors = [];
+ $scope.newFelon = null;
+ var felonResource = restFunctions('felons');
+
+ $scope.getAllFelons = function() {
+ felonResource.getAll(function(err, data) {
+ if (err) return err;
+
+ $scope.felons = data;
+ });
+ };
+
+ $scope.$on('busted', function(e, data) {
+ felonResource.getAll(function(err, data) {
+ if (err) return err;
+
+ $scope.felons = data;
+ });
+ });
+
+
+ $scope.create = function(felon) {
+
+ felonResource.create(felon, function(err, data) {
+ if (err) return err;
+ $scope.felons.push(data);
+ $scope.newFelon = null;
+ });
+ };
+
+ $scope.remove = function(felon) {
+ felonResource.remove($scope.felons, felon, function(err, data) {
+ if (err) {
+ $scope.erros.push('Could not delete Felon ' + felon.name);
+ $scope.getAllFelons();
+ }
+ });
+ };
+
+ $scope.update = function(felon) {
+ felonResource.update(felon, function(err, data) {
+ if (err) {
+ $scope.errors.push('could not get felon: ' + felon.name);
+ }
+ console.log('this felon has a new name');
+ });
+ };
+
+ $scope.temp = function(felon) {
+ felonResource.temp(felon);
+ };
+
+ $scope.cancel = function(felon) {
+ felonResource.cancel(felon);
+ };
+
+ }]);
+};
diff --git a/tim_miller/app/js/felons/felons.js b/tim_miller/app/js/felons/felons.js
new file mode 100644
index 0000000..057442c
--- /dev/null
+++ b/tim_miller/app/js/felons/felons.js
@@ -0,0 +1,4 @@
+module.exports = function(app) {
+ require('./controllers/felon_controller')(app);
+
+};
diff --git a/tim_miller/app/js/officers/controllers/officer_controller.js b/tim_miller/app/js/officers/controllers/officer_controller.js
new file mode 100644
index 0000000..0a92bf7
--- /dev/null
+++ b/tim_miller/app/js/officers/controllers/officer_controller.js
@@ -0,0 +1,60 @@
+module.exports = function(app) {
+ app.controller('OfficersController', ['$scope', '$http', 'restFunctions', function($scope, $http, restFunctions) {
+ $scope.officers = [];
+ $scope.errors = [];
+ $scope.newOfficer = null;
+ var officerResource = restFunctions('officers');
+
+ $scope.getAllOfficers = function() {
+ officerResource.getAll(function(err, data) {
+ if (err) return err;
+
+ $scope.officers = data;
+ });
+ };
+
+ $scope.$on('busted', function(e, data) {
+ officerResource.getAll(function(err, data) {
+ if (err) return err;
+
+ $scope.officers = data;
+ });
+ });
+
+ $scope.create = function(officer) {
+
+ officerResource.create(officer, function(err, data) {
+ if (err) return err;
+ $scope.officers.push(data);
+ $scope.newOfficer = null;
+ });
+ };
+
+ $scope.remove = function(officer) {
+ officerResource.remove($scope.officers, officer, function(err, data) {
+ if (err) {
+ $scope.errors.push('Could not delete Officer ' + officer.name);
+ $scope.getAllOfficers();
+ }
+ });
+ };
+
+ $scope.update = function(officer) {
+ officerResource.update(officer, function(err, data) {
+ if (err) {
+ $scope.errors.push('could not get officer: ' + officer.name);
+ }
+ console.log('this officer has a new name');
+ });
+ };
+
+ $scope.temp = function(officer) {
+ officerResource.temp(officer);
+ };
+
+ $scope.cancel = function(officer) {
+ officerResource.cancel(officer);
+ };
+
+ }]);
+};
diff --git a/tim_miller/app/js/officers/officers.js b/tim_miller/app/js/officers/officers.js
new file mode 100644
index 0000000..bdfb8a3
--- /dev/null
+++ b/tim_miller/app/js/officers/officers.js
@@ -0,0 +1,4 @@
+module.exports = function(app) {
+ require('./controllers/officer_controller')(app);
+
+};
diff --git a/tim_miller/app/js/services/rest_resource/rest_functions.js b/tim_miller/app/js/services/rest_resource/rest_functions.js
new file mode 100644
index 0000000..14c20fc
--- /dev/null
+++ b/tim_miller/app/js/services/rest_resource/rest_functions.js
@@ -0,0 +1,56 @@
+var handleSuccess = function(callback) {
+ return function(res) {
+ callback(null, res.data);
+ };
+};
+
+var handleFail = function(callback) {
+ return function(err) {
+ console.log(err);
+ callback(err.data);
+ };
+};
+
+module.exports = function(app) {
+
+ app.factory('restFunctions', ['$http', function($http) {
+
+ return function(resourceName) {
+ var resource = {};
+ resource.getAll = function(callback) {
+ $http.get('/api/' + resourceName)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+
+ resource.create = function(data, callback) {
+ $http.post('/api/' + resourceName, data)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+
+ resource.remove = function(collection, data, callback) {
+ collection.splice(collection.indexOf(data), 1);
+ $http.delete('/api/' + resourceName + '/' + data._id)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+
+ resource.update = function(data, callback) {
+ data.tempName = '';
+ data.editing = false;
+ $http.put('/api/' + resourceName + '/' + data._id, data)
+ .then(handleSuccess(callback), handleFail(callback));
+ };
+
+ resource.temp = function(data) {
+ data.editing = true;
+ data.tempName = data.name;
+ };
+
+ resource.cancel = function(data) {
+ data.editing = false;
+ data.name = data.tempName;
+ };
+
+ return resource;
+ };
+ }]);
+};
diff --git a/tim_miller/app/js/services/services.js b/tim_miller/app/js/services/services.js
new file mode 100644
index 0000000..c54ec59
--- /dev/null
+++ b/tim_miller/app/js/services/services.js
@@ -0,0 +1,3 @@
+module.exports = function(app) {
+ require('./rest_resource/rest_functions.js')(app);
+};
diff --git a/tim_miller/app/sass/application.scss b/tim_miller/app/sass/application.scss
new file mode 100644
index 0000000..294f70d
--- /dev/null
+++ b/tim_miller/app/sass/application.scss
@@ -0,0 +1,3 @@
+@import "base";
+@import "layout";
+@import "module";
diff --git a/tim_miller/app/sass/base.scss b/tim_miller/app/sass/base.scss
new file mode 100644
index 0000000..5e5e3c8
--- /dev/null
+++ b/tim_miller/app/sass/base.scss
@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device orientation change,
+ * without disabling user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability of focused elements when they are also in an
+ * active/hover state.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
diff --git a/tim_miller/app/sass/layout.scss b/tim_miller/app/sass/layout.scss
new file mode 100644
index 0000000..c740bf4
--- /dev/null
+++ b/tim_miller/app/sass/layout.scss
@@ -0,0 +1,17 @@
+@import "variables";
+
+body {
+ font-family: $primary-font;
+ background-color: $primary-color;
+}
+
+.heading {
+ background-color: $secondary-color;
+ font-family: $secondary-font;
+}
+
+.resource-heading {
+ background-color: $tertiary-color;
+ font-family: $secondary-font;
+ color: $primary-color;
+}
diff --git a/tim_miller/app/sass/module.scss b/tim_miller/app/sass/module.scss
new file mode 100644
index 0000000..1b8975e
--- /dev/null
+++ b/tim_miller/app/sass/module.scss
@@ -0,0 +1,44 @@
+.heading {
+ margin: 15ems;
+}
+
+.resource {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid;
+ margin: 10px;
+ button {
+ font-size: 10px;
+ }
+}
+
+.outer-flex {
+ display: flex;
+ flex-direction: row;
+}
+
+li{
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.list-content{
+ justify-content: center;
+ width: 100%;
+}
+
+.input-form{
+ dispaly: flex;
+ flex-wrap: wrap;
+ h2 {
+ width: 100%;
+ }
+ label{
+ width: 30%;
+ }
+ input {
+ width: 70%;
+ }
+}
+
diff --git a/tim_miller/app/sass/variables.scss b/tim_miller/app/sass/variables.scss
new file mode 100644
index 0000000..91e2a80
--- /dev/null
+++ b/tim_miller/app/sass/variables.scss
@@ -0,0 +1,5 @@
+$primary-color: #ECF0F1;
+$secondary-color: #E74C3C;
+$tertiary-color: #2C3E50;
+$primary-font: 'Roboto', sans-serif;
+$secondary-font: 'Rock Salt', cursive;
diff --git a/tim_miller/app/templates/form_template.html b/tim_miller/app/templates/form_template.html
new file mode 100644
index 0000000..a94f36c
--- /dev/null
+++ b/tim_miller/app/templates/form_template.html
@@ -0,0 +1,12 @@
+
diff --git a/tim_miller/app/templates/list_template.html b/tim_miller/app/templates/list_template.html
new file mode 100644
index 0000000..6daddac
--- /dev/null
+++ b/tim_miller/app/templates/list_template.html
@@ -0,0 +1,3 @@
+
+ Name: {{name}} {{attributeTitle}}: {{attribute}}
+
diff --git a/tim_miller/gulpfile.js b/tim_miller/gulpfile.js
index 8c9e629..4b88b78 100644
--- a/tim_miller/gulpfile.js
+++ b/tim_miller/gulpfile.js
@@ -2,8 +2,11 @@ var gulp = require('gulp');
var mocha = require('gulp-mocha');
var jshint = require('gulp-jshint');
var webpack = require('webpack-stream');
-
-gulp.task('default', ['jshint', 'test']);
+var minifyCss = require('gulp-minify-css');
+var gulpWatch = require('gulp-watch');
+var sass = require('gulp-sass');
+var concatCSS = require('gulp-concat-css');
+var maps = require('gulp-sourcemaps');
gulp.task('jshint', function(){
gulp.src(['gulpfile.js', 'lib/*.js', 'test/*.js', 'server.js', 'models/*.js', 'routes/*.js'])
@@ -21,6 +24,28 @@ gulp.task('static:dev', function() {
.pipe(gulp.dest('build/'));
});
+gulp.task('sass:dev', function() {
+ gulp.src('./app/sass/application.scss')
+ .pipe(maps.init())
+ .pipe(sass().on('error', sass.logError))
+ .pipe(minifyCss())
+ .pipe(maps.write('./'))
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('css:dev', function() {
+ return gulp.src([
+ 'app/css/base.css',
+ 'app/css/skeleton.css'])
+ .pipe(concatCSS('styles.min.css'))
+ .pipe(minifyCss())
+ .pipe(gulp.dest('build/'));
+});
+
+gulp.task('watch', function() {
+ gulp.watch('./app/**/*', ['styles']);
+});
+
gulp.task('webpack:dev', function() {
gulp.src('app/js/entry.js')
.pipe(webpack({
@@ -31,5 +56,16 @@ gulp.task('webpack:dev', function() {
.pipe(gulp.dest('build/'));
});
-gulp.task('build:dev', ['webpack:dev', 'static:dev']);
-gulp.task('default', ['build:dev']);
+gulp.task('webpack:test', function() {
+ return gulp.src('test/client/test_entry.js')
+ .pipe(webpack({
+ output: {
+ filename: 'test_bundle.js'
+ }
+ }))
+ .pipe(gulp.dest('test/client/'));
+});
+
+gulp.task('styles', ['sass:dev', 'css:dev']);
+gulp.task('build:dev', ['webpack:dev', 'static:dev', 'css:dev']);
+gulp.task('default', ['build:dev', 'jshint', 'test']);
diff --git a/tim_miller/karma.conf.js b/tim_miller/karma.conf.js
new file mode 100644
index 0000000..00919be
--- /dev/null
+++ b/tim_miller/karma.conf.js
@@ -0,0 +1,69 @@
+// Karma configuration
+// Generated on Wed Dec 02 2015 15:23:37 GMT-0800 (PST)
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'test/client/test_bundle.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultanous
+ concurrency: Infinity
+ })
+}
diff --git a/tim_miller/package.json b/tim_miller/package.json
index 279ed61..009ffbf 100644
--- a/tim_miller/package.json
+++ b/tim_miller/package.json
@@ -25,13 +25,26 @@
},
"devDependencies": {
"angular": "^1.4.8",
+ "angular-mocks": "^1.4.8",
+ "angular-route": "^1.4.8",
"chai": "^3.4.1",
"chai-http": "^1.0.0",
"gulp": "^3.9.0",
- "gulp-jshint": "^1.12.0",
- "gulp-mocha": "^2.1.3",
+ "gulp-concat-css": "^2.2.0",
+ "gulp-jshint": "^2.0.0",
+ "gulp-minify-css": "^1.2.2",
+ "gulp-mocha": "^2.2.0",
+ "gulp-sass": "^2.1.0",
+ "gulp-sourcemaps": "^1.6.0",
+ "gulp-watch": "^4.3.5",
+ "jasmine-core": "^2.3.4",
+ "jshint": "^2.8.0",
"jshint-stylish": "^2.0.1",
+ "karma": "^0.13.15",
+ "karma-jasmine": "^0.3.6",
+ "karma-phantomjs-launcher": "^0.2.1",
"mocha": "^2.3.3",
- "webpack-stream": "^2.1.1"
+ "phantomjs": "^1.9.19",
+ "webpack-stream": "^2.3.0"
}
}
diff --git a/tim_miller/routes/felonroutes.js b/tim_miller/routes/felonroutes.js
index 9924e19..358871a 100644
--- a/tim_miller/routes/felonroutes.js
+++ b/tim_miller/routes/felonroutes.js
@@ -15,7 +15,7 @@ felonRouter.get('/felons', function(req, res) {
});
});
-felonRouter.post('/felons', bodyParser.json(), eatAuth, function(req, res) {
+felonRouter.post('/felons', bodyParser.json(), /*eatAuth,*/ function(req, res) {
var newFelon = new Felon(req.body);
newFelon.save(function(err, data) {
@@ -25,20 +25,20 @@ felonRouter.post('/felons', bodyParser.json(), eatAuth, function(req, res) {
});
});
-felonRouter.put('/felons', bodyParser.json(), eatAuth, function(req, res) {
+felonRouter.put('/felons/:id', bodyParser.json(), /*eatAuth,*/ function(req, res) {
var felonData = req.body;
- delete req.body._id;
- Felon.update({_id: felonData._id}, felonData, function(err) {
+ delete felonData._id;
+ Felon.update({_id: req.params.id}, felonData, function(err) {
if(err) return error.default(err, res);
res.send('updated!');
});
});
-felonRouter.delete('/felons/:id', bodyParser.json(), eatAuth, function(req, res) {
+felonRouter.delete('/felons/:id', bodyParser.json(), /*eatAuth,*/ function(req, res) {
- Felon.remove({_id: req.params._id}, function(err) {
+ Felon.remove({_id: req.params.id}, function(err) {
if(err) return error.default(err, res);
res.send('deleted!');
diff --git a/tim_miller/routes/officerroutes.js b/tim_miller/routes/officerroutes.js
index bc86d3c..c27c4a4 100644
--- a/tim_miller/routes/officerroutes.js
+++ b/tim_miller/routes/officerroutes.js
@@ -11,11 +11,11 @@ officerRouter.get('/officers', function(req, res) {
Officer.find({}, function(err, data) {
if(err) return error.default(err, res);
- res.json(data);
+ res.send(data);
});
});
-officerRouter.post('/officers', bodyParser.json(), eatAuth, function(req, res) {
+officerRouter.post('/officers', bodyParser.json(), /*eatAuth,*/ function(req, res) {
var newOfficer = new Officer(req.body);
newOfficer.save(function(err, data) {
@@ -25,20 +25,22 @@ officerRouter.post('/officers', bodyParser.json(), eatAuth, function(req, res) {
});
});
-officerRouter.put('/officers', bodyParser.json(), eatAuth, function(req, res) {
+officerRouter.put('/officers/:id', bodyParser.json(), /*eatAuth,*/ function(req, res) {
var officerData = req.body;
delete officerData._id;
- Officer.update({_id: officerData._id}, officerData, function(err) {
+ console.log(officerData);
+ console.log(req.params);
+ Officer.update({_id: req.params.id}, officerData, function(err) {
if(err) return error.default(err, res);
res.send('updated!');
});
});
-officerRouter.delete('/officers/:id',bodyParser.json(), eatAuth, function(req, res) {
+officerRouter.delete('/officers/:id', bodyParser.json(), /*eatAuth,*/ function(req, res) {
- Officer.remove({_id: req.params._id}, function(err) {
+ Officer.remove({_id: req.params.id}, function(err) {
if(err) return error.default(err, res);
res.send('deleted!');
diff --git a/tim_miller/server.js b/tim_miller/server.js
index f09d369..efce96d 100644
--- a/tim_miller/server.js
+++ b/tim_miller/server.js
@@ -1,6 +1,5 @@
var mongoose = require('mongoose');
var express = require('express');
-var fs = require('fs');
var officerRouter = require(__dirname + '/routes/officerroutes.js');
var felonRouter = require(__dirname + '/routes/felonroutes.js');
var bustedRouter = require(__dirname + '/routes/bustedroutes.js');
@@ -11,20 +10,12 @@ var app = express();
process.env.APP_SECRET = process.env.APP_SECRET || '123451234512345';
mongoose.connect(process.env.MONGOLAB_URI || 'mongodb://localhost/officer_dev');
-app.use(officerRouter, felonRouter, bustedRouter, chuckRouter, authRouter);
-
-app.get('/:filename', function(req, res, next) {
- fs.stat(__dirname + '/build/' + req.params.filename, function(err, stats) {
- if (err) {console.log(err);
- return next();
- }
-
- if (!stats.isFile()) return next();
-
- var file = fs.createReadStream(__dirname + '/build/' + req.params.filename);
- file.pipe(res);
- });
-});
+app.use('/api', officerRouter);
+app.use('/api', felonRouter);
+app.use('/api', bustedRouter);
+app.use('/api', chuckRouter);
+app.use('/api', authRouter);
+app.use(express.static(__dirname + '/build'));
app.use(function(req, res) {
res.status(404).send('could not find file');
diff --git a/tim_miller/test/client/busted_controller_test.js b/tim_miller/test/client/busted_controller_test.js
new file mode 100644
index 0000000..991d1e0
--- /dev/null
+++ b/tim_miller/test/client/busted_controller_test.js
@@ -0,0 +1,46 @@
+require(__dirname + '/../../app/js/entry');
+require('angular-mocks');
+
+describe('busted controller', function() {
+ var $httpBackend;
+ var $ControllerConstructor;
+ var $scope;
+
+ beforeEach(angular.mock.module('OfficerAndFelonApp'));
+
+ beforeEach(angular.mock.inject(function($rootScope, $controller) {
+ $scope = $rootScope.$new();
+ $ControllerConstructor = $controller;
+ }));
+
+ it('should be able to create a controller', function() {
+ var controller = $ControllerConstructor('BustedController', {$scope: $scope});
+ expect(typeof $scope).toBe('object');
+ expect(typeof controller).toBe('object');
+ });
+
+ describe('busted controller function', function() {
+
+ beforeEach(angular.mock.inject(function(_$httpBackend_, $rootScope) {
+ $httpBackend = _$httpBackend_;
+ $ControllerConstructor('BustedController', {$scope: $scope});
+ }));
+
+ afterEach(function() {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should respond to a $scope.busted()', function() {
+ $httpBackend.expectGET('/api/busted').respond(200, {name: 'test name'});
+ $scope.busted();
+ $scope.$on('busted', function(e, data) {
+ $scope.busted = true;
+ });
+ $httpBackend.flush();
+ expect($scope.outcome.name).toBe('test name');
+ expect($scope.busted).toBe(true);
+ });
+
+ });
+});
diff --git a/tim_miller/test/client/felon_controller_test.js b/tim_miller/test/client/felon_controller_test.js
new file mode 100644
index 0000000..7d6f777
--- /dev/null
+++ b/tim_miller/test/client/felon_controller_test.js
@@ -0,0 +1,94 @@
+require(__dirname + '/../../app/js/entry.js');
+require('angular-mocks');
+
+describe('felon controller', function() {
+
+ var $httpBackend;
+ var $ControllerConstructor;
+ var $scope;
+
+ beforeEach(angular.mock.module('OfficerAndFelonApp'));
+
+ beforeEach(angular.mock.inject(function($rootScope, $controller) {
+ $scope = $rootScope.$new();
+ $ControllerConstructor = $controller;
+ }));
+
+ it('should be able to create a controller', function() {
+ var controller = $ControllerConstructor('FelonsController', {$scope: $scope});
+ expect(typeof $scope).toBe('object');
+ expect(typeof controller).toBe('object');
+ expect(Array.isArray($scope.felons)).toBe(true);
+ });
+
+ describe('felon controller functions', function() {
+
+ beforeEach(angular.mock.inject(function(_$httpBackend_, $rootScope) {
+ $httpBackend = _$httpBackend_;
+ $ControllerConstructor('FelonsController', {$scope: $scope});
+ $scope.felons = [];
+ }));
+
+ afterEach(function(){
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should make a get response when getAllFelons() is called', function() {
+ $httpBackend.expectGET('/api/felons').respond(200, [{_id: 1, name: 'test name'}]);
+ $scope.getAllFelons();
+ $httpBackend.flush();
+ expect($scope.felons[0]._id).toBe(1);
+ expect($scope.felons[0].name).toBe('test name');
+
+ });
+
+ it('should be able to create a new Felon when create() is called', function() {
+ $httpBackend.expectPOST('/api/felons', {name: 'test name'}).respond(200, {name: 'a different felon'});
+
+ expect($scope.felons.length).toBe(0);
+ $scope.newFelon = {name: 'test name'};
+ $scope.create($scope.newFelon);
+ $httpBackend.flush();
+ expect($scope.felons[0].name).toBe('a different felon');
+ expect($scope.newFelon).toBe(null);
+
+ });
+
+ it('should be able to respond to a DELETE request when remove() is called', function() {
+ felon = {_id: 1234, name: 'test name'};
+ $scope.felons = [felon];
+ $httpBackend.expectDELETE('/api/felons/' + felon._id).respond(200);
+
+ $scope.remove(felon);
+ $httpBackend.flush();
+ expect($scope.felons.length).toBe(0);
+ });
+
+ it('should be able to respond to a PUT request when update() is called', function() {
+ $scope.felons = [{_id: 1234, name: 'a testy name'}];
+ $httpBackend.expectPUT('/api/felons/' + $scope.felons[0]._id, {_id: 1234, name: 'a testy name', tempName: '', editing: false}).respond(200);
+
+ $scope.update($scope.felons[0]);
+ $httpBackend.flush();
+ expect($scope.felons[0].tempName).toBe('');
+ expect($scope.felons[0].editing).toBe(false);
+ });
+
+ it('should be able to change editing to true by calling temp()', function() {
+ felon = {name: 'a very testy name', tempName: '', editing: false};
+
+ $scope.temp(felon);
+ expect(felon.tempName).toBe('a very testy name');
+ expect(felon.editing).toBe(true);
+ });
+
+ it('should be able to change editing back to false by calling cancel()', function() {
+ felon = {name: '', tempName: 'an even more testy name', editing: true};
+
+ $scope.cancel(felon);
+ expect(felon.name).toBe('an even more testy name');
+ expect(felon.editing).toBe(false);
+ });
+ });
+});
diff --git a/tim_miller/test/client/officers_controller_test.js b/tim_miller/test/client/officers_controller_test.js
new file mode 100644
index 0000000..176c49e
--- /dev/null
+++ b/tim_miller/test/client/officers_controller_test.js
@@ -0,0 +1,93 @@
+require(__dirname + '/../../app/js/entry.js');
+require('angular-mocks');
+
+describe('officer controller', function() {
+ var $httpBackend;
+ var $ControllerConstructor;
+ var $scope;
+
+ beforeEach(angular.mock.module('OfficerAndFelonApp'));
+
+ beforeEach(angular.mock.inject(function($rootScope, $controller) {
+ $scope = $rootScope.$new();
+ $ControllerConstructor = $controller;
+ }));
+
+ it('should be able to create a controller', function() {
+ var controller = $ControllerConstructor('OfficersController', {$scope: $scope});
+ expect(typeof $scope).toBe('object');
+ expect(typeof controller).toBe('object');
+ expect(Array.isArray($scope.officers)).toBe(true);
+ });
+
+ describe('officer controller functions', function() {
+
+ beforeEach(angular.mock.inject(function(_$httpBackend_, $rootScope) {
+ $httpBackend = _$httpBackend_;
+ $ControllerConstructor('OfficersController', {$scope: $scope});
+ $scope.officers = [];
+ }));
+
+ afterEach(function(){
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should make a get response when getAllOfficers() is called', function() {
+ $httpBackend.expectGET('/api/officers').respond(200, [{_id: 1, name: 'test name'}]);
+ $scope.getAllOfficers();
+ $httpBackend.flush();
+ expect($scope.officers[0]._id).toBe(1);
+ expect($scope.officers[0].name).toBe('test name');
+
+ });
+
+ it('should be able to create a new Officer when create() is called', function() {
+ $httpBackend.expectPOST('/api/officers', {name: 'test name'}).respond(200, {name: 'a different officer'});
+
+ expect($scope.officers.length).toBe(0);
+ $scope.newOfficer = {name: 'test name'};
+ $scope.create($scope.newOfficer);
+ $httpBackend.flush();
+ expect($scope.officers[0].name).toBe('a different officer');
+ expect($scope.newOfficer).toBe(null);
+
+ });
+
+ it('should be able to respond to a DELETE request when remove() is called', function() {
+ officer = {_id: 1234, name: 'test name'};
+ $scope.officers = [officer];
+ $httpBackend.expectDELETE('/api/officers/' + officer._id).respond(200);
+
+ $scope.remove(officer);
+ $httpBackend.flush();
+ expect($scope.officers.length).toBe(0);
+ });
+
+ it('should be able to respond to a PUT request when update() is called', function() {
+ $scope.officers = [{_id: 1234, name: 'a testy name'}];
+ $httpBackend.expectPUT('/api/officers/' + $scope.officers[0]._id, {_id: 1234, name: 'a testy name', tempName: '', editing: false}).respond(200);
+
+ $scope.update($scope.officers[0]);
+ $httpBackend.flush();
+ expect($scope.officers[0].tempName).toBe('');
+ expect($scope.officers[0].editing).toBe(false);
+ });
+
+ it('should be able to change editing to true by calling temp()', function() {
+ officer = {name: 'a very testy name', tempName: '', editing: false};
+
+ $scope.temp(officer);
+ expect(officer.tempName).toBe('a very testy name');
+ expect(officer.editing).toBe(true);
+ });
+
+ it('should be able to change editing back to false by calling cancel()', function() {
+ officer = {name: '', tempName: 'an even more testy name', editing: true};
+
+ $scope.cancel(officer);
+ expect(officer.name).toBe('an even more testy name');
+ expect(officer.editing).toBe(false);
+ });
+ });
+});
diff --git a/tim_miller/test/client/test_entry.js b/tim_miller/test/client/test_entry.js
new file mode 100644
index 0000000..d84e6ac
--- /dev/null
+++ b/tim_miller/test/client/test_entry.js
@@ -0,0 +1,3 @@
+require('./officers_controller_test.js');
+require('./felon_controller_test.js');
+require('./busted_controller_test.js');