Skip to content
This repository was archived by the owner on Jun 16, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ src/components/*/*
!src/components/angular-touch/angular*.js
!src/components/angular-touch/angular*.map
!src/components/angular-bootstrap/*.js
!src/components/angular-storage/*.js

!src/components/bootstrap-css/css
!src/components/bootstrap-css/img
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "index.js",
"scripts": {
"start": "node localserver",
"test": "./node_modules/.bin/karma start --single-run --browsers Firefox --reporters dots,coverage"
"test": "./node_modules/.bin/karma start --single-run --reporters dots,coverage"
},
"repository": {
"type": "git",
Expand All @@ -30,6 +30,7 @@
},
"dependencies": {
"basic-auth-connect": "~1.0.0",
"lockfile": "~1.0.3",
"uuid": "^3.1.0",
"express": "^4.10.4",
"express-session": "^1.5.0",
Expand Down
14 changes: 14 additions & 0 deletions server_modules/file_system.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ exports.readJsonFile = function(file) {
return exports.readFile(file).then(parseData);
};

exports.filesInDir = function(path) {
path = exports.resolve(path);

return Q.promise(function(resolve, reject) {
fs.readdir(path, (err, files) => {
if(err) {
reject(err);
} else {
resolve(files);
}
});
});
};

exports.route = function(app) {

//reading the "file system"
Expand Down
33 changes: 33 additions & 0 deletions server_modules/lock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//This module wraps lockfile with promises.
var lockfile = require('lockfile');

module.exports = function(filename, options) {
this.filename = filename;
this.options = options || {};

this.lock = function() {
var self = this;

return new Promise(function(resolve, reject) {
lockfile.lock('scores.json.lock', self.options, function(err) {
if(err && err.code !== 'EEXIST') {
reject(err);
}

resolve();
});
});
};

this.unlock = function() {
return new Promise(function(resolve, reject) {
lockfile.unlock('scores.json.lock', function(err) {
if(err && err.code !== 'EEXIST') {
reject(err);
}

resolve();
});
});
};
}
188 changes: 188 additions & 0 deletions server_modules/scores.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
var Lock = require('./lock');
var utils = require('./utils');
var fileSystem = require('./file_system');
var log = require('./log').log;
var Q = require('q');
var id = require('uuid/v4');

function filterPublished(score) {
return score.published;
Expand All @@ -15,6 +18,45 @@ function reduceToMap(key) {
}
}

/**
* Atomically change scores file.
* Action callback is called with the current contents of the scores file, and is expected
* to return the new contents (or a Promise for it).
* A lock is acquired and held during the entire operation.
* @param action Callback that receives current scores.json object, must return new contents (or Promise for it)
* @return Promise for updated scores object
*/
function changeScores(action) {
var path = fileSystem.getDataFilePath('scores.json');
var lock = new Lock('scores.json.lock', { retries: 5, retryWait: 100 });

console.log(lock.options);

return lock.lock()
.then(() => fileSystem.readJsonFile(path))
.catch((err) => { //Ignoring all file not found errors, and just returning empty scores.json
console.log("catching");
if(err.message === 'file not found') {
console.log("hells yeah!");
return { version:3, scores: [], sheets: [] };
} else {
console.log("ho no! " + err.message);
throw err;
}
})
.then(action)
.then(scores => {
return fileSystem.writeFile(path, JSON.stringify(scores))
.then(() => {
return lock.unlock();
}).catch((err) => {
return lock.unlock();
}).then(function() {
return scores;
});
});
}

exports.route = function(app) {

//get all, grouped by round
Expand Down Expand Up @@ -47,4 +89,150 @@ exports.route = function(app) {
}).catch(utils.sendError(res)).done();
});

//save a new score
app.post('/scores/create',function(req,res) {
var body = JSON.parse(req.body);
var scoresheet = body.scoresheet;
var score = body.score;

fileSystem.writeFile(fileSystem.getDataFilePath("scoresheets/" + score.file), req.body)
.then(changeScores(function(result) {
if(typeof(score.id) === 'undefined') {
score.id = id();
}
result.scores.push(score);
result.sheets.push(score.file)
return result;
}))
.then(function(scores) {
res.json(scores).end();
}).catch(utils.sendError(res)).done();

});

//delete a score at an id
app.post('/scores/delete/:id',function(req,res) {
changeScores(function(result) {
var index = result.scores.findIndex((score) => score.id === req.params.id);
if(index === -1) {
throw new Error(`Could not find score with id ${req.params.id}`);
}
result.scores.splice(index, 1);
return result;
}).then(function(scores) {
res.json(scores).end();
}).catch(utils.sendError(res)).done();
});

//edit a score at an id
app.post('/scores/update/:id',function(req,res) {
var score = JSON.parse(req.body);
changeScores(function(result) {
var index = result.scores.findIndex((score) => score.id === req.params.id);
if(index === -1) {
throw new Error(`Could not find score with id ${req.params.id}`);
}
result.scores[index] = score;
return result;
}).then(function(scores) {
res.json(scores).end();
}).catch(utils.sendError(res)).done();
});


};

// For backward compatibility

changeScores(function(scores) {
if(typeof(scores.version) === 'undefined') {
scores.forEach(score => score.id = id())
return {
version: 3,
scores: scores,
sheets: []
}

} else if(scores.version === 3) {
return scores;

} else if(scores.version === 2) {
scores.scores.forEach(score => score.id = id())
scores.version = 3;
return scores;

} else {
throw new Error('Unkown scores version');
}

});

// Polling for sheets automatically on server load

function sanitizeScore(score) {
// Passthrough for already valid inputs
if (typeof score === "number")
return score;
switch (score) {
case "dnc":
case "dsq":
case null:
return score;
}
// Accept numbers stored as strings
var n = parseInt(score, 10);
if (String(n) === score)
return n;
// Try to convert some spellings of accepted strings
if (typeof score === "string") {
var s = score.toLowerCase();
switch (s) {
case "dnc":
case "dsq":
return s;
case "":
return null;
}
}
// Pass through the rest
log.warn("Invalid score " + score);
return score;
}

function loadScoresheetScore(filename) {
return fileSystem.readJsonFile(fileSystem.getDataFilePath("scoresheets/" + filename)).then(function(entry) {
return {
file: (entry.file !== undefined && entry.file !== null) ? String(entry.file) : "",
teamNumber: parseInt((entry.teamNumber !== undefined) ? entry.teamNumber : entry.team.number, 10),
stageId: String((entry.stageId !== undefined) ? entry.stageId : entry.stage.id),
round: parseInt(entry.round, 10),
score: sanitizeScore(entry.score), // can be Number, null, "dnc", etc.
originalScore: parseInt(entry.originalScore !== undefined ? entry.originalScore : entry.score, 10),
edited: entry.edited !== undefined ? String(entry.edited) : undefined, // timestamp, e.g. "Wed Nov 26 2014 21:11:43 GMT+0100 (CET)"
published: !!entry.published,
table: entry.table
};
});
}

changeScores(function(scores) {
return fileSystem.filesInDir('data/scoresheets').then(function(files) {
var promises = []

for(var i = 0; i < files.length; i++) {
if(!scores.sheets.includes(files[i])) {
var promise = loadScoresheetScore(files[i]).then(function(score) {
scores.scores.push(score);
scores.sheets.push(files[i]);
}).catch(function(err) {
log.error(`Error reading scoresheet ${files[i]}: ${err}`);
});
promises.push(promise);
}
}

return Q.all(promises).spread(function() {
return scores;
});
});
});
2 changes: 1 addition & 1 deletion spec/controllers/ExportRankingDialiogControllerSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('ExportRankingDialogController',function() {
angular.mock.inject(function($controller,$rootScope,$q,_$timeout_) {
$scope = $rootScope.$new();
$timeout = _$timeout_;
scoresMock = createScoresMock($q,fakeScoreboard);
scoresMock = createScoresMock(fakeScoreboard);
handshakeMock = createHandshakeMock($q);
stagesMock = createStagesMock();
controller = $controller('ExportRankingDialogController', {
Expand Down
6 changes: 6 additions & 0 deletions spec/mocks/independenceMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var createIndependenceMock = function() {
return {
act: jasmine.createSpy('independenceActSpy').and.returnValue(Promise.resolve()),
pendingActions: jasmine.createSpy('independencePendingActionsSpy')
};
};
7 changes: 5 additions & 2 deletions spec/mocks/messageMock.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
var createMessageMock = function() {
return {};
}
return {
send: jasmine.createSpy('messageSendSpy'),
on: jasmine.createSpy('messageOnSpy')
};
}
10 changes: 5 additions & 5 deletions spec/mocks/scoresMock.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function createScoresMock($q,scoreboard) {
function createScoresMock(scoreboard) {
scoreboard = scoreboard || {};
return {
scores: [{
Expand All @@ -9,12 +9,12 @@ function createScoresMock($q,scoreboard) {
index: 1
}],
scoreboard: scoreboard,
remove: jasmine.createSpy('scoreRemoveSpy'),
load: jasmine.createSpy('scoreLoadSpy'),
pollSheets: jasmine.createSpy('scorePollSheetsSpy').and.returnValue($q.when()),
update: jasmine.createSpy('scoreUpdateSpy'),
clear: jasmine.createSpy('scoreClearSpy'),
create: jasmine.createSpy('scoreCreateSpy').and.returnValue(Promise.resolve()),
delete: jasmine.createSpy('scoreDeleteSpy'),
update: jasmine.createSpy('scoreUpdateSpy').and.returnValue(Promise.resolve()),
_update: jasmine.createSpy('score_UpdateSpy'),
save: jasmine.createSpy('scoreSaveSpy'),
getRankings: jasmine.createSpy('getRankings').and.returnValue({
scoreboard: scoreboard
})
Expand Down
Loading