Merge branch 'master' of git.maggioni.xyz:maggicl/SA3

This commit is contained in:
Claudio Maggioni (maggicl) 2019-11-02 12:51:44 +01:00
commit 248ca3449c
58 changed files with 32331 additions and 46 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -50,10 +50,18 @@ fs.readFile(__dirname + '/db.json', { encoding: 'utf8' }, (e, data) => {
app.use('/bookmarked', routers.bookmarked); app.use('/bookmarked', routers.bookmarked);
}); });
app.locals.writeFavs = () => { let writing = false;
app.locals.writeFavs = (done) => {
if (writing) {
setTimeout(() => app.locals.writeFavs(done), 100);
return;
}
writing = true;
fs.writeFile(__dirname + '/db.json', JSON.stringify(app.locals.favourites), fs.writeFile(__dirname + '/db.json', JSON.stringify(app.locals.favourites),
{ encoding: 'utf8' }, err => { { encoding: 'utf8' }, err => {
if (err) console.error(err); if (err) console.error(err);
writing = false;
done();
}); });
} }

View file

@ -8,8 +8,9 @@ const router = express.Router();
let id = 1; let id = 1;
function nextId() { function nextId(favs) {
return id++; while (favs.some((newId => e => e._id == newId)(++id)));
return id;
} }
function createFav(req, res) { function createFav(req, res) {
@ -20,26 +21,39 @@ function createFav(req, res) {
} }
const favourite = { const favourite = {
_id: req.body._id ? req.body._id : nextId(), _id: req.body._id ? req.body._id : nextId(req.app.locals.favourites),
name: req.body.name, name: req.body.name,
dataURL: req.body.dataURL, dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked, bookmarked: req.body.bookmarked,
}; };
req.app.locals.favourites.push(favourite); req.app.locals.favourites.push(favourite);
req.app.locals.writeFavs(); req.app.locals.writeFavs(() => {
res.status = 201; res.status = 201;
renderFav(req, res, favourite, false); renderFav(req, res, favourite, false);
});
} }
router.post('/', createFav); router.post('/', createFav);
function renderFav(req, res, favs, list = true) { function renderFav(req, res, favs, list = true) {
if (req.accepts('html')) { if (req.accepts('html')) {
res.render(list ? 'favourites.dust' : 'favourite.dust', let ctx;
list ? { favs: favs } : Object.assign({b: favs.bookmarked === 'true' || if (list) {
favs.bookmarked === true}, favs)); const f = [];
for (const e of favs) {
f.push(Object.assign({
b: e.bookmarked === 'true' || e.bookmarked === true
}, e));
}
ctx = {favs: f};
} else {
ctx = Object.assign({
b: favs.bookmarked === 'true' || favs.bookmarked === true
}, favs);
}
res.render(list ? 'favourites.dust' : 'favourite.dust', ctx);
} else if (req.accepts('json')) { } else if (req.accepts('json')) {
res.json(favs); res.json(favs);
} else { } else {
@ -76,7 +90,8 @@ router.get('/:id', (req, res) => {
} }
}); });
router.put('/:id', (req, res) => { function handleUpdate(partial = false) {
return (req, res) => {
const edit = req.app.locals.favourites const edit = req.app.locals.favourites
.find(e => e._id == req.params.id); .find(e => e._id == req.params.id);
@ -86,20 +101,27 @@ router.put('/:id', (req, res) => {
for (const key of ['dataURL', 'name']) { for (const key of ['dataURL', 'name']) {
if (req.body[key]) { if (req.body[key]) {
edit[key] = req.body[key]; edit[key] = req.body[key];
} else { } else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' }); res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters'); res.end('Bad PUT form parameters');
return; return;
} }
} }
edit.bookmarked = !!req.body.bookmarked; if (req.body.bookmarked !== undefined) {
req.app.locals.writeFavs(); edit.bookmarked = req.body.bookmarked;
}
req.app.locals.writeFavs(() => {
res.status = 200; res.status = 200;
renderFav(req, res, edit, false); renderFav(req, res, edit, false);
});
} }
}); };
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => { router.delete('/:id', (req, res) => {
let idx = -1; let idx = -1;
@ -116,13 +138,13 @@ router.delete('/:id', (req, res) => {
return; return;
} else { } else {
req.app.locals.favourites.splice(idx, 1); req.app.locals.favourites.splice(idx, 1);
req.app.locals.writeFavs(); req.app.locals.writeFavs(() => {
res.format({ res.format({
json: () => res.writeHead(204), json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' }) html: () => res.writeHead(302, { 'Location': '/favorites' })
}); });
res.end(); res.end();
});
} }
}); });
@ -138,10 +160,10 @@ router.put('/:id/bookmarked', (req, res) => {
res.end('Bad PUT bookmark form parameters'); res.end('Bad PUT bookmark form parameters');
} else { } else {
edit.bookmarked = req.body.bookmarked; edit.bookmarked = req.body.bookmarked;
req.app.locals.writeFavs(); req.app.locals.writeFavs(() => {
res.status = 200; res.status = 200;
renderFav(req, res, edit, false); renderFav(req, res, edit, false);
});
} }
}); });

View file

@ -1,4 +1,4 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !} {! vim: set ts=2 sw=2 et tw=120: !}
<html> <html>
<head> <head>

View file

@ -1,7 +1,12 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !} {! vim: set ts=2 sw=2 et tw=120: !}
<h3>{name}</h3> <h3>{name}</h3>
<img src="{dataURL}" alt="{name}"> <img src="{dataURL}" alt="{name}">
{?b}
<p>
<strong>Bookmarked</strong>
</p>
{/b}
{?details} {?details}
<a href="/favorites/{_id}">Details</a> <a href="/favorites/{_id}">Details</a>
{:else} {:else}

View file

@ -1,4 +1,4 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !} {! vim: set ts=2 sw=2 et tw=120: !}
<html> <html>
<head> <head>
@ -17,7 +17,7 @@
{/bookmarked} {/bookmarked}
{#favs} {#favs}
<div> <div>
{>"favourite_partial" name=name dataURL=dataURL _id=_id details="true" /} {>"favourite_partial" name=name dataURL=dataURL _id=_id b=b details="true" /}
</div> </div>
{:else} {:else}
<strong>No favourites.</strong> <strong>No favourites.</strong>

View file

@ -1,4 +1,4 @@
{! vim: set ft=html ts=2 sw=2 et tw=120: !} {! vim: set ts=2 sw=2 et tw=120: !}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

BIN
hw6/Claudio_Maggioni.zip Normal file

Binary file not shown.

1
hw6/Claudio_Maggioni/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
db.json

View file

@ -0,0 +1,47 @@
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const path = require('path');
const logger = require('morgan');
const bodyParser = require('body-parser');
const kleiDust = require('klei-dust');
const methodOverride = require('method-override');
const fs = require('fs');
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/SA3_hw6', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
require('./models/Favorites');
const app = express();
//configure app
app.use(logger('dev'));
app.set('views', __dirname + '/views');
app.engine('dust', kleiDust.dust);
app.set('view engine', 'dust');
app.set('view options', { layout: false });
app.use(methodOverride('_method'));
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());
app.use(express.static('public'));
// Initialize routers here
const routers = require('./routes/routers');
app.use('/', routers.root);
// app.use('/favorites', routers.favourites_db);
// app.use('/favorites', routers.favourites_db_promises);
app.use('/favorites', routers.favourites_db_asaw);
app.use('/bookmarked', routers.bookmarked);
module.exports = app;

9
hw6/Claudio_Maggioni/bin/www Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env node
var debug = require('debug')('canvas-server');
var app = require('../app');
app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port);
});

View file

@ -0,0 +1,11 @@
module.exports = {
//Server URL
url: "http://127.0.0.1:3000",
//Form structure
form: {
_id: "_id",
name: "name",
dataURL: "dataURL",
bookmarked: "bookmarked" // Optional
}
}

View file

@ -0,0 +1,20 @@
// vim: set ts=2 sw=2 et tw=80:
const mongoose = require('mongoose');
const schema = new mongoose.Schema({
_id: {},
dataURL: { type: String, required: true },
name: { type: String, required: true, default: '' },
bookmarked: {
type: Boolean,
required: false,
default: false,
},
dateCreated: { type: Date, required: true, default: Date.now }
});
const Favorite = mongoose.model('Favorite', schema);

View file

@ -0,0 +1,36 @@
{
"name": "wa-exercise-7-2017-2018",
"version": "0.0.0",
"description": "Exercise 7 of Web Atelier. Express.js and Mongoose",
"main": "app.js",
"scripts": {
"start": "DEBUG='canvas-server' nodemon ./bin/www",
"test": "npm run test-mocha",
"test-mocha": "./node_modules/mocha/bin/mocha -R spec ./test/routes ./test/hypermedia"
},
"keywords": [
"Node.js",
"Express.js",
"MongoDB",
"Mongoose",
"REST"
],
"author": "Vincenzo Ferme, Andrea Gallidabino, Vasileios Triglianos, Ilya Yanok",
"license": "MIT",
"dependencies": {
"body-parser": "^1.19.0",
"debug": "^3.1.0",
"dustjs-linkedin": "^2.7.5",
"express": "^4.16.2",
"klei-dust": "^1.0.0",
"method-override": "^3.0.0",
"mongoose": "^5.7.7",
"morgan": "^1.9.0",
"request": "^2.88.0",
"supertest": "^3.0.0"
},
"devDependencies": {
"mocha": "^4.0.1",
"should": "^13.1.3"
}
}

View file

@ -0,0 +1,15 @@
// vim: set ts=2 sw=2 tw=80 et:
// Enter your initialization code here
function init() {
// Create canvas app
const app = new App({
canvas: 'canvas',
buttons: {
clear: 'clear-btn',
camera: 'camera-btn',
undo: 'undo-btn'
},
brushToolbar: 'brush-toolbar'
});
}

View file

@ -0,0 +1,5 @@
let test = QUnit.test;
let equal = QUnit.assert.equal.bind(QUnit.assert);
let notEqual = QUnit.assert.notEqual.bind(QUnit.assert);
let deepEqual = QUnit.assert.deepEqual.bind(QUnit.assert);
let notDeepEqual = QUnit.assert.notDeepEqual.bind(QUnit.assert);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,235 @@
/**
* QUnit v1.10.0 - A JavaScript Unit Testing Framework
*
* http://qunitjs.com
*
* Copyright 2012 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699a4;
background-color: #0d3349;
font-size: 1.5em;
line-height: 1em;
font-weight: normal;
border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
-webkit-border-top-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
}
#qunit-header a {
text-decoration: none;
color: #c2ccd1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #fff;
}
#qunit-testrunner-toolbar label {
display: inline-block;
padding: 0 .5em 0 .1em;
}
#qunit-banner {
height: 5px;
}
#qunit-testrunner-toolbar {
padding: 0.5em 0 0.5em 2em;
color: #5E740B;
background-color: #eee;
overflow: hidden;
}
#qunit-userAgent {
padding: 0.5em 0 0.5em 2.5em;
background-color: #2b81af;
color: #fff;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
#qunit-modulefilter-container {
float: right;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 0.5em 0.4em 2.5em;
border-bottom: 1px solid #fff;
list-style-position: inside;
}
#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
display: none;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li a {
padding: 0.5em;
color: #c2ccd1;
text-decoration: none;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests ol {
margin-top: 0.5em;
padding: 0.5em;
background-color: #fff;
border-radius: 5px;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: .2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 .5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
background-color: #e0f2be;
color: #374e0c;
text-decoration: none;
}
#qunit-tests ins {
background-color: #ffcaca;
color: #500;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: black; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #fff;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3c510c;
background-color: #fff;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #fff;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
-moz-border-radius: 0 0 5px 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
}
#qunit-tests .fail { color: #000000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: green; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/** Result */
#qunit-testresult {
padding: 0.5em 0.5em 0.5em 2.5em;
color: #2b81af;
background-color: #D2E0E6;
border-bottom: 1px solid white;
}
#qunit-testresult .module-name {
font-weight: bold;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}

View file

@ -0,0 +1,259 @@
// vim: set ts=2 sw=2 et tw=80:
class App {
static get BRUSHES() {
return {
"PenBrush": new PenBrush(),
"DiscBrush": new DiscBrush(),
"StarBrush": new StarBrush(),
};
}
constructor(conf) {
if (!(typeof conf === 'object' && conf)) {
throw new Error('Argument conf different from specification');
}
this.canvas = document.getElementById(conf.canvas);
if (!this.canvas || this.canvas.tagName !== 'CANVAS') {
throw new Error(`canvas is not a canvas`);
}
this.ctx = this.canvas.getContext('2d');
this.favourites = document.querySelector('div#favourites');
if (typeof conf.buttons === 'object' && conf.buttons) {
this.buttons = {}
for (const b of ['clear', 'undo', 'camera'])
this.buttons[b] = document.getElementById(conf.buttons[b]);
if (this.buttons.clear) {
this.buttons.clear.addEventListener('click', () => {
this.erase();
this.background = null;
history.clear();
});
}
if (this.buttons.undo) {
this.buttons.undo.addEventListener('click', () => {
history.pop();
this.redrawAll();
});
}
if (this.buttons.camera) {
this.buttons.camera.addEventListener('click', () => {
const base64 = this.canvas.toDataURL();
const img = document.createElement('img');
img.src = base64;
const form = document.createElement('form');
form.method = 'POST';
form.action = '/favorites';
const image = document.createElement('input');
image.type = 'hidden';
image.name = 'dataURL';
image.value = base64;
form.appendChild(image);
const lblName = document.createElement('label');
lblName.setAttribute('for', 'name');
lblName.innerText = 'Name:';
form.appendChild(lblName);
const name = document.createElement('input');
name.type = 'text';
name.name = 'name';
name.placeholder = 'Name';
name.value = 'New Name';
form.appendChild(name);
const submit = document.createElement('button');
submit.innerText = 'Save';
form.appendChild(submit);
this.favourites.appendChild(img);
this.favourites.appendChild(form);
});
}
const player = document.createElement('video');
player.addEventListener('loadeddata', () => {
player.play();
setTimeout(() => {
const imgCanvas = document.createElement('canvas');
imgCanvas.width = this.canvas.width;
imgCanvas.height = this.canvas.height;
const imgCtx = imgCanvas.getContext('2d');
imgCtx.drawImage(player, 0, 0, canvas.width, canvas.height);
const imgData = imgCtx.getImageData(0, 0, imgCanvas.width,
imgCanvas.height);
for (let i = 0; i < data.length; i += 4) {
const pixel = (data[i] + 2 * data[i+1] + data[i+2]) / 4;
data[i] = data[i+1] = data[i+2] = pixel;
}
imgCtx.putImageData(imgData, 0, 0);
const img = document.createElement('img');
img.src = imgCanvas.toDataURL();
img.addEventListener('load', () => {
this.background = img;
this.redrawAll();
});
player.srcObject.getVideoTracks().forEach(track => track.stop());
}, 100);
});
const button = document.createElement('button');
button.type = 'button';
button.innerHTML = 'Photo';
button.addEventListener('click', () => {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
player.srcObject = stream;
});
});
document.getElementById('left-toolbar').appendChild(button);
}
this.ctx.lineWidth = 1;
this.strokeStyle = this.constructor.defaultStrokeStyle;
this.brush = "PenBrush";
const brushToolbar = document.querySelector('#brush-toolbar');
if (brushToolbar) {
for (const name in App.BRUSHES) {
const b = document.createElement('button');
b.innerText = name;
b.addEventListener('click', () => this.brush = name);
brushToolbar.appendChild(b);
}
const label = document.createElement('label');
label.setAttribute('for', 'color');
const color = document.createElement('input');
color.type = 'color';
color.name = 'color';
color.value = this.constructor.defaultStrokeStyle;
color.addEventListener('change', () => this.strokeStyle = color.value);
brushToolbar.appendChild(label);
brushToolbar.appendChild(color);
}
const toMouse = (e, func) => {
if (e && e.touches && e.touches[0]) {
return func.bind(this)({
offsetX: e.touches[0].pageX - this.canvas.offsetLeft,
offsetY: e.touches[0].pageY - this.canvas.offsetTop
});
}
};
this.canvas.addEventListener('touchstart', e => toMouse(e, this.startPath));
this.canvas.addEventListener('mousedown', this.startPath.bind(this));
this.canvas.addEventListener('touchmove', e => toMouse(e, this.draw));
this.canvas.addEventListener('mousemove', this.draw.bind(this));
this.canvas.addEventListener('touchcancel', e => toMouse(e, this.endPath));
this.canvas.addEventListener('mouseup', this.endPath.bind(this));
this.canvas.addEventListener('mouseout', this.endPath.bind(this));
}
static get defaultStrokeStyle() {
return 'black';
}
get strokeStyle() {
if (this.ctx.strokeStyle == '#000000') {
return 'black';
}
return this.ctx.strokeStyle;
}
set strokeStyle(style) {
if (typeof style !== 'string') {
throw new Error('style is not a string');
}
this.ctx.strokeStyle = style;
}
get brush() {
return this._brush.name;
}
set brush(brushName) {
this._brush = App.BRUSHES[brushName];
}
erase() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
startPath(e, record = true) {
this.ctx.beginPath();
this.ctx.moveTo(e.offsetX, e.offsetY);
if (record) {
history.initializeNewPath();
history.push(new Stroke(this.brush, this.strokeStyle,
e.offsetX, e.offsetY));
}
if (e instanceof MouseEvent) {
this.mousedown = true;
}
}
draw(e, beginNew = true, record = true) {
if (this.mousedown || !(e instanceof MouseEvent)) {
this._brush.draw(this.ctx, this.strokeStyle, e.offsetX, e.offsetY);
if (record) {
history.push(new Stroke(this.brush, this.strokeStyle,
e.offsetX, e.offsetY));
}
if (beginNew) {
this.ctx.beginPath();
this.ctx.moveTo(e.offsetX, e.offsetY);
} else if (e instanceof MouseEvent) {
this.mousedown = false;
}
}
}
endPath(e, record = true) {
this.draw(e, false, record);
}
drawPath(path) {
const last = path.length - 1;
const lastBrush = this.brush;
const lastStyle = this.strokeStyle;
for (let i = 0; i <= last; i++) {
this.brush = path[i].brushName;
this.strokeStyle = path[i].strokeStyle;
switch(i) {
case 0: this.startPath(path[i], false); break;
case last: this.endPath(path[i], false); break;
default: this.draw(path[i], true, false);
}
}
this.brush = lastBrush;
this.strokeStyle = lastStyle;
}
redrawAll() {
this.erase();
if (this.background) {
this.ctx.drawImage(this.background, 0, 0, this.canvas.width,
this.canvas.height);
}
for (const path of history.paths) {
this.drawPath(path);
}
}
}

View file

@ -0,0 +1,53 @@
// vim: set ts=2 sw=2 et tw=80:
class PenBrush {
constructor() {
this.opacity = 1;
this.name = "PenBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.lineJoin = ctx.lineCap = 'round';
ctx.strokeStyle = strokeStyle;
ctx.lineTo(x, y);
ctx.stroke();
}
}
class DiscBrush extends PenBrush {
static get RADIUS() { return 10; }
constructor() {
super();
this.name = "DiscBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.beginPath(); // clear previous path starting
ctx.ellipse(x, y, 10, 10, 0, 0, 2 * Math.PI);
ctx.fill();
ctx.stroke();
}
}
class StarBrush extends PenBrush {
constructor() {
super();
this.name = "StarBrush";
}
draw(ctx, strokeStyle, x, y) {
ctx.moveTo(x - 8, y - 3);
ctx.lineTo(x - 3, y - 3);
ctx.lineTo(x, y - 8);
ctx.lineTo(x + 3, y - 3);
ctx.lineTo(x + 8, y - 3);
ctx.lineTo(x + 4, y + 1);
ctx.lineTo(x + 4, y + 6);
ctx.lineTo(x, y + 3);
ctx.lineTo(x - 4, y + 6);
ctx.lineTo(x - 4, y + 1);
ctx.lineTo(x - 8, y - 3);
ctx.stroke();
}
}

View file

@ -0,0 +1,37 @@
// vim: set ts=2 sw=2 et tw=80:
const history = {
paths: []
}
history.pop = () => {
if (history.paths.length == 0) return;
return history.paths.pop();
};
history.initializeNewPath = () => {
history.paths.push([]);
};
history.push = (stroke) => {
if (!stroke || !stroke instanceof Stroke) {
throw new Error(JSON.stringify(stroke) + ' is not a Stroke instance');
}
history.paths[history.paths.length - 1].push(stroke);
return history.paths[history.paths.length - 1];
}
history.clear = () => {
history.paths = [];
};
class Stroke {
constructor(brushName, strokeStyle, x, y) {
this.brushName = brushName;
this.strokeStyle = strokeStyle;
this.offsetX = x;
this.offsetY = y;
}
}

View file

@ -0,0 +1,133 @@
/* vim: set ts=2 sw=2 et tw=80: */
body {
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
}
#app {
display: flex;
}
#canvas {
border: 1px solid #bbb;
}
#left-toolbar, #brush-toolbar {
width: 6rem;
margin: 0 .5rem;
}
.toolbar button, .toolbar input {
width: 100%;
padding: .5rem 0;
margin-bottom: 10px;
min-height: 3rem;
cursor: pointer;
}
#brush-toolbar {
margin: 0 .5rem;
}
#palette {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
width: 53px;
height: 204px;
background-color: buttonface;
cursor: pointer;
}
.toolbar #camera-btn {
font-size: 1.5em;
line-height: 1.5em;
color: #444;
background: #eee;
border: none;
border-radius: 3px;
height: 40px;
outline: none;
}
.toolbar #camera-btn:hover {
background: #70a0e8;
}
.toolbar #camera-btn:active {
background: #0e57c3;
}
.p-color {
width: 25px;
height: 25px;
}
.black {background-color:rgb(0, 0, 0);}
.dark-gray {background-color:rgb(87, 87, 87);}
.red {background-color:rgb(173, 35, 35);}
.blue {background-color:rgb(42, 75, 215);}
.green {background-color:rgb(29, 105, 20);}
.brown {background-color:rgb(129, 74, 25);}
.purple {background-color:rgb(129, 38, 192);}
.light-gray {background-color:rgb(160, 160, 160);}
.light-green {background-color:rgb(129, 197, 122);}
.light-blue {background-color:rgb(157, 175, 255);}
.cyan {background-color:rgb(41, 208, 208);}
.orange {background-color:rgb(255, 146, 51);}
.yellow {background-color:rgb(255, 238, 51);}
.tan {background-color:rgb(233, 222, 187);}
.pink {background-color:rgb(255, 205, 243);}
.white {background-color:rgb(255, 255, 255);}
#clock {
background-color: #ddd;
width: 360px;
height: 48px;
text-align: center;
position: relative;
font-size: 2.5rem;
margin-top: -10px;
font-weight: bold;
margin-bottom: 20px;
}
#clock-time,
#progress-bar {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
#clock-time {
z-index: 10;
line-height: 48px; /* same height as #clock so that the time is vertically centered */
}
#progress-bar {
width: 0%;
height: 48px;
background-color: #8DFF80;
text-align: center;
line-height: 30px;
color: white;
}
#favourites {
margin: 5px;
float: left;
width: 200px;
}
#favourites img {
width: 90%;
height: 100px;
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.42);
margin-left: 10px;
}
#favourites div.desc {
padding: 15px;
text-align: center;
}

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Exercise 3 - Object-oriented Javascript</title>
<link rel="stylesheet" href="resources/qunit.css">
</head>
<body>
<canvas id="test-canvas" width="600" height="400" style="display: none"></canvas>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="scripts/brushes.js"></script>
<script src="scripts/undo.js"></script>
<script src="scripts/clock.js"></script>
<script src="scripts/app.js"></script>
<script src="resources/qunit-2.4.0.js"></script>
<script src="resources/lodash.js"></script>
<script src="resources/jsverify.standalone.js"></script>
<script src="qunit-compat.js"></script>
<script src="test.js"></script>
<script>
tests();
</script>
</body>
</html>

View file

@ -0,0 +1,103 @@
function tests() {
test("App class constructors and methods", function(assert) {
// The App class must be defined
equal(typeof App === 'function', true, "The App class must be defined");
equal(/^\s*class\s+/.test(App.toString()), true, "App is a function but it is not defined using the class keyword")
// The App class constructor should throw an error if its argument is undefined
assert.throws(function() {
new App(undefined)
}, "The App class constructor should throw an error if its argument is undefined")
// The App class constructor should throw an error if its argument is not a canvas
assert.throws(function() {
new App("");
}, "The App class constructor should throw an error if its argument is not an object")
assert.throws(function() {
new App(1);
}, "The App class constructor should throw an error if its argument is a number")
assert.throws(function() {
new App([]);
}, "The App class constructor should throw an error if its argument is an array")
assert.throws(function() {
new App(true);
}, "The App class constructor should throw an error if its argument is a boolean")
// The default Stroke Style should be accessible in a static way, and should be equal to "black"
equal(App.defaultStrokeStyle === 'black', true, 'The default Stroke Style should be accessible in a static way, and should be equal to "black"')
assert.throws(function() {
new App({});
}, "The App class constructor should throw an error if its argument options object is not pointing to a canvas element under the 'canvas' property")
const app = new App({canvas: 'test-canvas'})
equal(app.strokeStyle, "black", "Getter for strokeStyle is not defined")
// The draw method must be defined
equal(typeof app.draw === 'function', true, "The draw method must be defined")
});
test("History and Stroke object literals fields and methods", function(assert) {
// The Stroke class must be defined
equal(typeof Stroke === 'function', true, "The Stroke class must be defined");
equal(/^\s*class\s+/.test(Stroke.toString()), true, "Stroke is a function but it is not defined using the class keyword")
equal(function(){
try {
const stroke = new Stroke('square')
} catch (err) {
return false
}
return true
}(), true, "Stroke can be instantiated")
const stroke = new Stroke('square');
const stroke1 = new Stroke('circle');
const stroke2 = new Stroke('triangle');
history.initializeNewPath()
assert.throws(function() {
history.push()
}, "Must pass a Stroke instance when you push in the history")
equal(function() {
try {
history.push(stroke);
} catch (err) {
return false;
}
return true;
}(), true, "History push accepts Stroke instances as a parameter");
equal(history.pop()[0] === stroke, true, "Pop returns an array containing the pushed Stroke instance")
history.initializeNewPath();
history.push(stroke);
history.push(stroke1);
history.push(stroke2);
equal(history.pop().length, 3, "Pop returns an array containing the pushed Stroke instances")
history.initializeNewPath();
equal(history.pop().length, 0, "Pop on an empty history should return an empty array")
history.initializeNewPath(); //simulate mouse down
history.push(stroke); //simulate mouse move
history.push(stroke1); //simulate mouse move
history.initializeNewPath(); //simulate mouse up and down again
history.push(stroke2); //simulate mouse move
equal(history.pop().length, 1, "Pop returns an array containing the most recent path (Expected path with 1 Stroke)")
equal(history.pop().length, 2, "Pop returns an array containing the most recent path (Expected path with 2 Strokes)")
});
}

View file

@ -0,0 +1,2 @@
# Bonuses implemented:
- *Excercise 4*, Use mongoose and MongoDB with async/await

View file

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({ bookmarked: true }, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,162 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, renderFav, parseId, notFound } = require('../utils');
function findAndRender(filter, req, res) {
Favorite.find(filter, (err, favs) => {
if (err) {
return error(err, res);
}
renderFav(req, res, favs);
});
}
router.post('/', (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked
}
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
favourite.save((err, fav) => {
if (err) {
return error(err, res);
}
res.status = 201;
renderFav(req, res, fav, false);
});
});
router.get('/', (req, res) => {
findAndRender({}, req, res);
});
router.get('/search', (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
findAndRender(filter, req, res);
});
function findOne(id) {
return (req, res) => {
Favorite.findById(id(req), (err, fav) => {
if (err) {
return error(err, res);
}
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
});
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
passRawResult: true,
}, (err, fav) => {
if (err) {
return error(err, res);
}
if (fav == null) {
res.status = 201;
findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
});
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => {
Favorite.findByIdAndDelete(parseId(req), (err, fav) => {
if (err) {
return error(err, res);
}
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
});
});
router.put('/:id/bookmarked', (req, res) => {
Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true }, (err, fav) => {
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,164 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, catchErrs, renderFav, parseId, notFound } = require('../utils');
async function findAndRender(filter, req, res) {
try {
const favs = await Favorite.find(filter);
renderFav(req, res, favs);
} catch(e) {
error(e, res);
}
}
router.post('/', async (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked,
};
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
try {
const fav = await favourite.save();
res.status = 201;
renderFav(req, res, fav, false);
} catch(e) {
error(e, res);
}
});
router.get('/', async (req, res) => {
await findAndRender({}, req, res);
});
router.get('/search', async (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
await findAndRender(filter, req, res);
});
function findOne(id) {
return async (req, res) => {
try {
const fav = await Favorite.findById(id(req));
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
} catch(e) {
error(e, res);
}
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return async (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
try {
const fav = await Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
});
if (fav == null) {
res.status = 201;
await findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
} catch (e) {
error(e, res);
}
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', async (req, res) => {
try {
const fav = await Favorite.findByIdAndDelete(parseId(req));
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
} catch (e) {
error(e, res);
}
});
router.put('/:id/bookmarked', (req, res) => {
try {
const fav = Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true });
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
} catch (e) {
error(e, res);
}
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,142 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const fs = require('fs');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error, catchErrs, renderFav, parseId, notFound } = require('../utils');
function findAndRender(filter, req, res) {
catchErrs(Favorite.find(filter), res).then(favs => {
renderFav(req, res, favs);
});
}
router.post('/', (req, res) => {
if (!req.body.name || !req.body.dataURL) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad create form parameters');
return;
}
const data = {
name: req.body.name,
dataURL: req.body.dataURL,
bookmarked: req.body.bookmarked,
};
const favourite = new Favorite(data);
if (req.body._id) {
favourite._id = req.body._id;
} else {
favourite._id = mongoose.Types.ObjectId();
}
catchErrs(favourite.save(), res).then(fav => {
res.status = 201;
renderFav(req, res, fav, false);
});
});
router.get('/', (req, res) => {
findAndRender({}, req, res);
});
router.get('/search', (req, res) => {
const filter = Object.assign({}, req.query);
delete filter['dataURL'];
delete filter['_method'];
findAndRender(filter, req, res);
});
function findOne(id) {
return (req, res) => {
catchErrs(Favorite.findById(id(req)), res).then(fav => {
if (notFound(fav, res)) {
return;
}
renderFav(req, res, fav, false);
});
};
}
router.get('/:id', findOne(req => parseId(req)));
function handleUpdate(partial = false) {
return (req, res) => {
const edit = {};
for (const key of ['dataURL', 'name']) {
if (req.body[key]) {
edit[key] = req.body[key];
} else if (!partial) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT form parameters');
return;
}
}
if (req.body.bookmarked !== undefined) {
edit.bookmarked = req.body.bookmarked;
}
catchErrs(Favorite.findByIdAndUpdate(parseId(req), { $set: edit }, {
new: false,
upsert: true,
setDefaultsOnInsert: true,
}), res).then(fav => {
if (fav == null) {
res.status = 201;
findOne(() => parseId(req))(req, res);
return;
}
res.status = 200;
renderFav(req, res, fav, false);
});
};
}
router.put('/:id', handleUpdate());
router.patch('/:id', handleUpdate(true));
router.delete('/:id', (req, res) => {
catchErrs(Favorite.findByIdAndDelete(parseId(req)), res).then(fav => {
if (notFound(fav, res)) {
return;
}
res.format({
json: () => res.writeHead(204),
html: () => res.writeHead(302, { 'Location': '/favorites' })
});
res.end();
});
});
router.put('/:id/bookmarked', (req, res) => {
catchErrs(Favorite.findByIdAndUpdate(parseId(req), {
$set: { bookmarked: req.body.bookmarked }
}, { new: true }), res).then(fav => {
if (notFound(fav, res)) {
return;
}
if (!req.body.bookmarked) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Bad PUT bookmark form parameters');
} else {
renderFav(req, res, fav, false);
}
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,24 @@
/** @module root/router */
'use strict';
// vim: set ts=2 sw=2 et tw=80:
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose');
const Favorite = mongoose.model('Favorite');
const { error } = require('../utils');
router.get('/', (req, res) => {
Favorite.find({}, (err, favs) => {
if (err) {
return error(err, res);
}
res.render('index.dust', { favs });
});
});
/** router for /root */
module.exports = router;

View file

@ -0,0 +1,35 @@
/** @module routes/routers
* Exposes all routers
*/
'use strict';
const fs = require('fs');
const dirEntries = fs.readdirSync(__dirname);
const base = __dirname + '/';
const routers = {};
try{
dirEntries.forEach(function(dirEntry){
const stats = fs.statSync(base + dirEntry);
//try to load router of dir
if(stats.isDirectory()){
try{
const router = require(base + dirEntry + '/router');
//add router to our list of routers;
routers[dirEntry] = router;
}catch(err){
console.log('Could not get router for ' + dirEntry);
console.log(err.toString() + err.stack);
}
}
});
}catch(err){
console.log('Error while loading routers');
console.log(err.stack);
//We don't know what happened, export empty object
routers = {}
}finally{
module.exports = routers;
}

View file

@ -0,0 +1,65 @@
// vim: set ts=2 sw=2 et tw=80:
const mongoose = require('mongoose');
function catchErrs(promise, res) {
return promise.catch(err => error(err, res));
}
function error(err, res) {
console.error(err);
res.status = err instanceof mongoose.CastError ||
err instanceof mongoose.TypeError ? 400 : 500;
res.format({
json: () => res.json({ error: err }),
html: () => res.render('500.dust', { err: JSON.stringify(err, null, 2) }),
});
res.end();
}
function renderFav(req, res, favs, list = true) {
const makeTestsPass = e => {
return {
_id: e._id,
name: e.name,
dataURL: e.dataURL,
bookmarked: '' + e.bookmarked
}
};
if (req.accepts('html')) {
res.render(list ? 'favourites.dust' : 'favourite.dust',
list ? { favs } : favs);
} else if (req.accepts('json')) {
if (list) {
favs = favs.map(makeTestsPass);
} else {
favs = makeTestsPass(favs);
}
res.json(favs);
} else {
res.writeHead(406);
res.end();
}
}
function parseId(req) {
if (typeof req.params.id === 'string' && req.params.id.length == 24) {
return mongoose.Types.ObjectId(req.params.id);
} else {
return req.params.id;
}
}
function notFound(e, res) {
if (e == null) {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('Not found');
return true;
} else return false;
}
module.exports = { error, renderFav, catchErrs, parseId, notFound };

View file

@ -0,0 +1,9 @@
/**
* Standalond db seed
*/
var seed = require('./test/seed').seed;
seed(function(seedData){
console.log("Seeding complete!")
process.exit();
})

View file

@ -0,0 +1,56 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
describe('Task 2: Testing Create /favorites routes', function(){
describe('POST /favorites', function(){
it('should create a new favorite if the request data is valid', function(done){
var newFavData = {}
newFavData[config.form._id] = "tt1",
newFavData[config.form.name] = "NicePicture",
newFavData[config.form.dataURL] = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
newFavData[config.form.bookmarked] = "true"
request(config.url)
.post('/favorites')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(newFavData)
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
.expect(201)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should.equal(resFav[config.form._id], newFavData[config.form._id]);
should.equal(resFav[config.form.dataURL], newFavData[config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], newFavData[config.form.bookmarked]);
should.equal(resFav[config.form.name], newFavData[config.form.name]);
done();
});
});
it('should get a 400 Bad Request if data is invalid #1', function(done){
var newFavData = {
"invalid": "this object does not have the correct structure",
};
request(config.url)
.post('/favorites')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(newFavData)
.expect(400, done);
});
it('should get a 400 Bad Request if data is not in json', function(done){
request(config.url)
.post('/favorites')
.set('Content-Type', 'text/plain')
.set('Accept', 'application/json')
.send("This is a plain text request, it should result in a 400 bad request")
.expect(400, done);
});
});
});

View file

@ -0,0 +1,127 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favsOriginal = require('../seedData');
var favs = []
for (let i = 0; i < favsOriginal.length; i++) {
let o = {}
o[config.form._id] = favsOriginal[i]._id
o[config.form.name] = favsOriginal[i].name
o[config.form.dataURL] = favsOriginal[i].dataURL
o[config.form.bookmarked] = favsOriginal[i].bookmarked
favs.push(o)
}
describe('Task 3: Testing Read for /favorites routes', function(){
before(seed)
describe('GET /favorites', function(){
it('should list all the favs with correct data', function(done){
request(config.url)
.get('/favorites')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
console.log(resFavs);
resFavs.forEach(function(fav) {
for (let i = 0; i < favs.length; i++) {
if(favs[i][config.form._id] == fav[config.form._id]) {
should.equal(fav[config.form.dataURL], favs[i][config.form.dataURL]);
should.equal(fav[config.form.bookmarked], favs[i][config.form.bookmarked]);
should.equal(fav[config.form.name], favs[i][config.form.name]);
}
}
})
done();
});
});
});
describe('GET /favorites/:favoriteid', function(){
it('should get the favorite with correct data', function(done){
request(config.url)
.get('/favorites/' + favs[1][config.form._id])
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should.equal(resFav[config.form._id], favs[1][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[1][config.form.name]);
done();
});
});
it('should respond with a 404 if the favorite does not exist', function(done){
request(config.url)
.get('/favorites/notValidId')
.set('Accept', 'application/json')
.expect(404, done);
});
});
describe(`GET /favorites/search`, function(){
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}`, function(done){
request(config.url)
.get(`/favorites/search?${config.form._id}=${favs[1][config.form._id]}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavArray = JSON.parse(res.text);
should.equal(resFavArray.length, 1)
var resFav = resFavArray[0]
should.equal(resFav[config.form._id], favs[1][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[1][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[1][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[1][config.form.name]);
done();
});
});
it(`should get the favorite with correct data: GET /favorites/search?${config.form._id}&${config.form.name}`, function(done){
request(config.url)
.get(`/favorites/search?${config.form._id}=${favs[5][config.form._id]}&${config.form.name}=${favs[5][config.form.name]}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text)[0];
should.equal(resFav[config.form._id], favs[5][config.form._id]);
should.equal(resFav[config.form.dataURL], favs[5][config.form.dataURL]);
should.equal(resFav[config.form.bookmarked], favs[5][config.form.bookmarked]);
should.equal(resFav[config.form.name], favs[5][config.form.name]);
done();
});
});
it(`should get empty array if there is no match: GET /favorites/search?${config.form.name}`, function(done){
request(config.url)
.get(`/favorites/search?&${config.form.name}=NoName`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavArray = JSON.parse(res.text);
should.equal(resFavArray.length, 0)
done();
});
});
});
});
function seed(done){
//seed the db
seedDb.seed(function(seedData){
done();
}, favs);
}

View file

@ -0,0 +1,44 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData');
describe('Task 4: Testing Update on /favorites routes', function(){
describe('PUT /favorites/:favoriteid', function(){
it('should change the name of an existing favorite', function(done){
let reqBody = {}
reqBody[config.form.name] = 'newName'
reqBody[config.form.dataURL] = favs[3][config.form.dataURL] // maggicl: added to comply with assignment
request(config.url)
.put('/favorites/' + favs[3]._id)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(reqBody)
.expect(201)
.end(function(err, res){
let resPutFav = JSON.parse(res.text)
should(resPutFav[config.form.name], 'newName')
done()
});
});
})
describe('GET /favorites/:favoriteid', function(){
it('the name should be changed', function(done){
request(config.url)
.get('/favorites/' + favs[3]._id)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with Content-Type: application/json' )
.expect(200)
.end(function(err, res){
var favGetFav = JSON.parse(res.text);
should.equal(favGetFav[config.form.name], 'newName');
done();
});
});
});
});

View file

@ -0,0 +1,60 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData')
describe('Task 5: Testing Delete for /favorites routes', function(){
describe('DELETE /favorites/:favoriteid', function(){
it('should delete an existing favorite', function(done){
request(config.url)
.del('/favorites/' + favs[1]._id)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(204)
.end(function(err, res){
res.text.should.be.empty;
res.body.should.be.empty;
done();
});
});
it('should not list the previously deleted resource', function(done){
request(config.url)
.del('/favorites/' + favs[2]._id)
.expect(200)
.end(function(err, res){
request(config.url)
.get('/favorites')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
resFavs.forEach(function(fav){
should.notEqual(fav[config.form._id], favs[2]._id);
});
done();
});
});
});
it('should respond with a 404 for a previously deleted resource', function(done){
request(config.url)
.delete('/favorites/' + favs[1]._id)
.set('Accept', 'application/json')
.expect(404, done);
});
it('should respond with a 404 deleting a resource which does not exist', function(done){
request(config.url)
.delete('/favorites/invalidId')
.set('Accept', 'application/json')
.expect(404, done);
});
});
});

View file

@ -0,0 +1,91 @@
'use strict';
var config = require('../../config');
var should = require('should');
var seedDb = require('../seed');
var request = require('supertest');
var favs = require('../seedData')
describe('Task 6: Testing /bookmarked routes and /favorites/:favoriteid/bookmarked', function(){
describe('GET /bookmarked', function(){
it('should list only the bookmarked favs', function(done){
request(config.url)
.get('/bookmarked')
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFavs = JSON.parse(res.text);
resFavs.forEach(function(fav){
should.equal(fav[config.form.bookmarked], "true");
});
done();
});
});
});
describe('PUT /favorites/:favoriteid/bookmarked', function(){
after(drop)
it('initial bookmarked value should be false', function(done){
request(config.url)
.get(`/favorites/${favs[0]._id}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "false")
done();
});
});
it('should change the bookmarked value', function(done){
const reqBody = {}
reqBody[config.form.bookmarked] = "true"
request(config.url)
.put(`/favorites/${favs[0]._id}/bookmarked`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.send(reqBody)
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "true")
done();
});
});
it('bookmarked value should be changed', function(done){
request(config.url)
.get(`/favorites/${favs[0]._id}`)
.set('Accept', 'application/json')
.expect('Content-Type', /json/, 'it should respond with json' )
.expect(200)
.end(function(err, res){
var resFav = JSON.parse(res.text);
should(resFav[config.form.bookmarked], "true")
done();
});
});
});
});
function drop(done){
let deleteIds = ['t0','t3','t4','t5', 'tt1']
let count = 0
for (let i = 0; i < deleteIds.length; i++) {
request(config.url)
.delete(`/favorites/${deleteIds[i]}`)
.end(function() {
count++
if(count == deleteIds.length) {
done()
}
})
}
}

View file

@ -0,0 +1,46 @@
'use strict';
var config = require('../config');
var request = require('request');
//seedData
var seedData = require('./seedData')
//total callbacks (one for each model)
var totalCbs = 0;
var cbCnt = 0;
/**
* Recursive function that goes through
* seedData populating each item of it
*/
var seedModel = function(done, s){
if(s != undefined) {
seedData = s
}
totalCbs = seedData.length
for (let i = 0; i < seedData.length; i++) {
const form = {}
form[config.form._id] = seedData[i]._id
form[config.form.name] = seedData[i].name
form[config.form.dataURL] = seedData[i].dataURL
form[config.form.bookmarked] = seedData[i].bookmarked
request.post(`${config.url}/favorites`, {
form: form
}, function(error, response, body){
cbCnt++
if(cbCnt == totalCbs) {
done(seedData)
}
})
}
}
/**
* This is where everything starts
*/
module.exports.seed = function (done, s){
seedModel(done, s)
}

View file

@ -0,0 +1,47 @@
'use strict';
var favorites = [
{
"_id" : "t0",
"name" : "My Nice Image",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t1",
"name" : "My Nice Image 1",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
},
{
"_id" : "t2",
"name" : "My Nice Image 2",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t3",
"name" : "My Nice Image 3",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
},
{
"_id" : "t4",
"name" : "My Nice Image 4",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "false"
},
{
"_id" : "t5",
"name" : "MyNiceImage5",
"dataURL" : "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyMDAgMjAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyMDAgMjAwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8Zz4NCgk8cGF0aCBkPSJNMTgwLDQxLjdoLTUxLjFWMzEuMWg0Mi4yQzE1My4xLDEyLjUsMTI3LjgsMSw5OS45LDFDNDUuMiwxLjEsMC45LDQ1LjQsMSwxMDAuMWMwLDI2LjgsMTAuNyw1MS4xLDI4LjEsNjguOQ0KCQljLTEuNC0yLTIuNC00LjEtMy02LjFjLTEtMy41LTEtNS45LTEuMS0xNS44di00Ny4xaDE0Ljh2NDguNWMwLDMuNC0wLjEsNi42LDEsOS42YzMsNy42LDExLjMsOC41LDE2LDguNWMyLjMsMCw4LjMtMC4xLDEyLjYtMy45DQoJCWM0LjQtMy45LDQuNC04LjQsNC40LTE1di00Ny43aDE0Ljl2NDkuN2MtMC4xLDguOS0wLjEsMTYuMy04LjUsMjMuNWMtOCw3LTE4LjQsNy43LTIzLjgsNy43Yy00LjgsMC05LjUtMC42LTE0LTIuMQ0KCQljLTEuOC0wLjYtMy41LTEuNC01LTIuM2MxNy4xLDE0LDM5LDIyLjQsNjIuOCwyMi40YzU0LjctMC4xLDk5LTQ0LjQsOTguOS05OS4xQzE5OSw3OC4xLDE5MS45LDU4LDE4MCw0MS43eiBNMTc1LjMsOTQuNGwtNi4zLTkNCgkJYzIuMS0xLjQsOC43LTUuNyw4LjctMTcuN2MwLTItMC4yLTQuMS0xLTYuMmMtMS43LTQuMS00LjYtNC45LTYuNi00LjljLTMuNiwwLTQuOSwyLjUtNS43LDQuM2MtMC41LDEuMy0wLjYsMS41LTEuOCw2LjZsLTEuNSw2LjkNCgkJYy0wLjksMy42LTEuMyw1LjQtMiw3LjJjLTEuMSwyLjYtNC41LDkuNi0xNCw5LjZjLTEwLjksMC0xNy44LTkuMi0xNy44LTIyLjZjMC0xMi4zLDYuMS0xOSwxMS42LTIzbDYuNiw4LjgNCgkJYy0yLjgsMS45LTguNSw1LjctOC41LDE0LjhjMCw1LjgsMi42LDEwLjgsNywxMC44YzQuOSwwLDUuOC01LjMsNi44LTEwLjVsMS4zLTUuOWMxLjYtNy43LDQuOC0xOC42LDE3LTE4LjYNCgkJYzEzLjEsMCwxOC40LDEyLjIsMTguNCwyNC4zYzAsMy4yLTAuMyw2LjctMS4zLDEwLjJDMTg1LjEsODMuNCwxODIuMyw5MC4xLDE3NS4zLDk0LjR6Ii8+DQo8L2c+DQo8L3N2Zz4NCg==",
"bookmarked" : "true"
}
]
module.exports = favorites;

View file

@ -0,0 +1,14 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset='utf-8'>
</head>
<body>
<h1>Error 500</h1>
<pre>
{err}
</pre>
</body>
</html>

View file

@ -0,0 +1,13 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset="utf-8">
<title>{name}</title>
</head>
<body>
{>"favourite_partial" /}
</body>
</html>

View file

@ -0,0 +1,28 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<h3>{name}</h3>
<img src="{dataURL}" alt="{name}">
{?b}
<p>
<strong>Bookmarked</strong>
</p>
{/b}
{?details}
<a href="/favorites/{_id}">Details</a>
{:else}
<form method="POST" action="/favorites/{_id}?_method=PUT">
<input type="hidden" name="dataURL" value="{dataURL}">
<label for="name">Name:</label>
<input type="text" name="name" placeholder="Name" value="{name}"><br>
<button>Update</button><br>
<button formaction="/favorites/{_id}?_method=DELETE">Delete</button><br>
{?bookmarked}
<button name="bookmarked" value="false"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Remove bookmark</button>
{:else}
<button name="bookmarked" value="true"
formaction="/favorites/{_id}/bookmarked?_method=PUT">Add bookmark</button>
{/bookmarked}
</form>
<a href="/favorites">Favourites list</a>
{/details}

View file

@ -0,0 +1,28 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<html>
<head>
<meta charset="utf-8">
{?bookmarked}
<title>Bookmarked</title>
{:else}
<title>Favourites</title>
{/bookmarked}
</head>
<body>
{?bookmarked}
<h1>Bookmarked</h1>
{:else}
<h1>Favourites</h1>
{/bookmarked}
{#favs}
<div>
{>"favourite_partial" name=name dataURL=dataURL _id=_id bookmarked=bookmarked details="true" /}
</div>
{:else}
<strong>No favourites.</strong>
{/favs}
</body>
</html>

View file

@ -0,0 +1,41 @@
{! vim: set ts=2 sw=2 et tw=120: !}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>OO-JS Exercise - Web Atelier 2017</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
<link rel="stylesheet" href="style.css">
</head>
<body onload="init()">
<h1>
OO-JS Exercise: Canvas
</h1>
<div id="app">
<div id="left-toolbar" class="toolbar">
<button id="clear-btn">Clear</button>
<button id="undo-btn">Undo</button>
<button id="camera-btn"><i class="fa fa-camera" aria-hidden="true"></i></button>
</div>
<canvas id="canvas" width="600" height="400"></canvas>
<div id="brush-toolbar" class="toolbar">
<!-- Brushes buttons go here (programmatically). Each button should be a <button> element -->
</div>
</div>
<h2>Favourites</h2>
<div id="favourites">
{#favs}
{>"favourite_partial" name=name dataURL=dataURL _id=_id bookmarked=bookmarked details="true" /}
{/favs}
</div>
<script src="scripts/brushes.js"></script>
<script src="scripts/undo.js"></script>
<!-- <script src="scripts/clock.js"></script> -->
<script src="scripts/app.js"></script>
<script src="main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff