diff --git a/spec/views/rankingSpec.js b/spec/views/rankingSpec.js index f93b6c12..59642ad1 100644 --- a/spec/views/rankingSpec.js +++ b/spec/views/rankingSpec.js @@ -10,7 +10,7 @@ describe('ranking', function() { number: '123', name: 'foo' }; - var fsMock, stagesMock, scoresMock, handshakeMock, messageMock; + var fsMock, stagesMock, scoresMock, handshakeMock, messageMock, settingsMock; beforeEach(function() { angular.mock.module(module.name); @@ -18,24 +18,30 @@ describe('ranking', function() { $scope = $rootScope.$new(); scoresMock = createScoresMock($q); handshakeMock = createHandshakeMock($q); - stagesMock = createStagesMock(); + stagesMock = createStagesMock($q); messageMock = createMessageMock(); + var settings = {}; + settings.lineStartString = "\""; + settings.lineEndString = "\""; + settings.separatorString = "\",\""; + settingsMock = createSettingsMock($q, settings); controller = $controller('rankingCtrl', { '$scope': $scope, '$scores': scoresMock, '$stages': stagesMock, '$handshake': handshakeMock, - '$message': messageMock + '$message': messageMock, + '$settings': settingsMock, }); }); + $scope.$digest();//resolves all init promises, etc. }); describe('initialization', function() { it('should initialize', function() { expect($scope.sort).toEqual('rank'); expect($scope.rev).toEqual(false); - expect($scope.csvdata).toEqual({}); - expect($scope.csvname).toEqual({}); + expect($scope.exportFiles).toEqual({}); }); }); @@ -79,7 +85,7 @@ describe('ranking', function() { sort: 'foo' }; expect($scope.sortIcon(stage)).toEqual(''); - expect($scope.sortIcon(stage,'foo')).toEqual('icon-sort-up'); + expect($scope.sortIcon(stage,'foo')).toEqual('arrow_drop_up'); }); it('should give the up icon when col is sorted in reverse',function() { var stage = { @@ -87,15 +93,15 @@ describe('ranking', function() { rev: true }; expect($scope.sortIcon(stage)).toEqual(''); - expect($scope.sortIcon(stage,'foo')).toEqual('icon-sort-down'); + expect($scope.sortIcon(stage,'foo')).toEqual('arrow_drop_down'); }); //default sort order stuff, needs a bit of refactoring it('should report a default sorting for any stage',function() { var stage = {}; - expect($scope.sortIcon(stage,'rank')).toEqual('icon-sort-up'); + expect($scope.sortIcon(stage,'rank')).toEqual('arrow_drop_up'); $scope.rev = true; - expect($scope.sortIcon(stage,'rank')).toEqual('icon-sort-down'); + expect($scope.sortIcon(stage,'rank')).toEqual('arrow_drop_down'); }); }); @@ -134,47 +140,55 @@ describe('ranking', function() { }); }); - describe('rebuildCSV',function() { - it('should generate CSV data and filenames',function() { - expect($scope.csvname).toEqual({}); - expect($scope.csvdata).toEqual({}); - $scope.rebuildCSV({ + describe('buildExportFiles',function() { + it('should generate export files',function() { + expect($scope.exportFiles).toEqual({}); + + $scope.scoreboard = { 'qualifying': [ { rank: 1, team: { name: "foo", number: 123 }, highest: 10, scores: [0, 10, 5] }, { rank: 1, team: { name: "\"bar\"", number: 456 }, highest: 10, scores: [10, 0, 5] } ] - }); - expect($scope.csvname["qualifying"]).toEqual("ranking_qualifying.csv"); - expect($scope.csvdata["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ - '"Rank","Team Number","Team Name","Highest","Round 1","Round 2","Round 3"', + }; + $scope.buildExportFiles(); + expect($scope.exportFiles["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ '"1","123","foo","10","0","10","5"', - '"1","456","""bar""","10","10","0","5"', - ].join("\r\n"))); + '"1","456",""bar"","10","10","0","5"', //new format doesn't replace every quotation mark with two + ].join("\r\n").concat("\r\n")));//new format ends in a newline }); it('should not skip empty values, but include as empty string',function() { - $scope.rebuildCSV({ + $scope.scoreboard = { 'qualifying': [ { team: { name: "foo", number: 123 }, highest: 10, scores: [0, 10, 5] }, { team: { name: "\"bar\"", number: 456 }, highest: 10, scores: [10, 0, 5] } ] - }); - expect($scope.csvdata["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ - '"Rank","Team Number","Team Name","Highest","Round 1","Round 2","Round 3"', + }; + $scope.$digest(); + expect($scope.exportFiles["qualifying"]).toEqual("data:text/csv;charset=utf-8," + encodeURIComponent([ '"","123","foo","10","0","10","5"', - '"","456","""bar""","10","10","0","5"', - ].join("\r\n"))); + '"","456",""bar"","10","10","0","5"', //new format doesn't replace every quotation mark with two + ].join("\r\n").concat("\r\n")));//new format ends in a newline }); }); describe('scoreboard watcher',function() { - it('should rebuild the csv when the scoreboard changes',function() { - $scope.rebuildCSV = jasmine.createSpy('rebuildCSV'); + it('should rebuild the export files when the scoreboard changes',function() { + $scope.buildExportFiles = jasmine.createSpy('buildExportFiles'); $scope.scoreboard = 'foo'; $scope.$digest(); - expect($scope.rebuildCSV).toHaveBeenCalledWith(scoresMock.scoreboard); + expect($scope.buildExportFiles).toHaveBeenCalled(); }); }); + describe('settings watcher', function () { + it('should rebuild the export files when the settings change', function () { + $scope.buildExportFiles = jasmine.createSpy('buildExportFiles'); + $scope.settings.bla = "fo"; + $scope.$digest(); + expect($scope.buildExportFiles).toHaveBeenCalled(); + }) + }); + describe('getRoundLabel',function() { it('should create a label for rounds',function() { expect($scope.getRoundLabel(4)).toEqual('Round 4'); diff --git a/src/css/elements.css b/src/css/elements.css index e4f8cbe6..4e02793c 100644 --- a/src/css/elements.css +++ b/src/css/elements.css @@ -327,3 +327,11 @@ button .material-icons { display: none !important; } } + +.sortable span{ + float: left; +} + +.sortable i{ + float: right; +} diff --git a/src/js/views/ranking.js b/src/js/views/ranking.js index 479a6055..2610cd6c 100644 --- a/src/js/views/ranking.js +++ b/src/js/views/ranking.js @@ -5,13 +5,13 @@ define('views/ranking',[ 'services/ng-scores', 'services/ng-handshake', 'services/ng-message', - 'controllers/ExportRankingDialogController', + 'services/ng-settings', 'angular' ],function(log) { var moduleName = 'ranking'; - return angular.module(moduleName,['ExportRankingDialog']).controller(moduleName+'Ctrl', [ - '$scope', '$scores', '$stages','$handshake','$message', - function($scope, $scores, $stages, $handshake, $message) { + return angular.module(moduleName,[]).controller(moduleName+'Ctrl', [ + '$scope', '$scores', '$stages','$handshake','$message', '$settings', + function($scope, $scores, $stages, $handshake, $message, $settings) { log('init ranking ctrl'); // temporary default sort values @@ -70,15 +70,15 @@ define('views/ranking',[ var icon = ''; if (stage.sort == col) { if (stage.rev){ - icon = 'icon-sort-down'; + icon = 'arrow_drop_down'; } else { - icon = 'icon-sort-up'; + icon = 'arrow_drop_up'; } } else if (stage.sort === undefined && col == $scope.sort) { if (stage.rev === undefined && $scope.rev) { - icon = 'icon-sort-down'; + icon = 'arrow_drop_down'; } else { - icon = 'icon-sort-up'; + icon = 'arrow_drop_up'; } } else { icon = ''; // no icon if column is not sorted @@ -102,64 +102,60 @@ define('views/ranking',[ return new Array($scope.maxRounds() - stage.$rounds.length); }; - // Data for CSV export links, indexed by stage ID - $scope.csvdata = {}; // CSV data itself - $scope.csvname = {}; // Filenames suggested to user - - // Convert a 2D matrix to a CSV string. - // All cells are converted to strings and fully quoted, - // except null or undefined cells, which are passed as empty - // values (without quotes). - function toCSV(rows) { - return rows.map(function(row) { - return row.map(function(col) { - // Escape quotes, and wrap in quotes - if (col === undefined || col === null) { - col = ""; - } - return '"' + String(col).replace(/"/gi, '""') + '"'; - }).join(","); - }).join("\r\n"); // Use Windows line-endings, to make it Notepad-friendly - } + /** + * encodes a two dimensional array as a string according to the settings + * specified by the user as reported by the ng-settings.$settings service + * + * @param array the array to be encoded + * @returns {string} the encoded string form of the array + */ + $scope.encodeArray = function (array) { + var string = ""; + var settings = $settings.settings; + array.forEach(function (row) { + row = row.map((elem) => elem || elem === 0 ? String(elem) : ""); + string = string.concat(settings.lineStartString ? String(settings.lineStartString) : ""); + string = string.concat(row.join(settings.separatorString ? String(settings.separatorString) : "")); + string = string.concat((settings.lineEndString ? String(settings.lineEndString) : "") + "\r\n"); + }); + return string; + }; + + $scope.exportFiles = {}; /** - * Rebuild CSV data (contents and filenames) of given scoreboard. - * @param scoreboard Per-stage ranking as present in e.g. $scores.scoreboard. + * Builds the .csv file for exporting score for each stage and assigns it + * to exportFiles in the field corresponding to that stage's id */ - $scope.rebuildCSV = function(scoreboard) { - $scope.csvdata = {}; - $scope.csvname = {}; - Object.keys(scoreboard).forEach(function(stageId) { - var ranking = scoreboard[stageId]; - var rows = ranking.map(function(entry) { - return [ - entry.rank, - entry.team.number, - entry.team.name, - entry.highest, - ].concat(entry.scores); + $scope.buildExportFiles= function () { + Object.keys($scope.scoreboard).forEach(function (stageID) { + var teams = $scope.scoreboard[stageID]; + teams = teams.map(function (teamEntry) { + return [teamEntry.rank, teamEntry.team.number, + teamEntry.team.name, teamEntry.highest].concat(teamEntry.scores); }); - var header = ["Rank", "Team Number", "Team Name", "Highest"]; - var stage = $stages.get(stageId); - header = header.concat(stage.$rounds.map(function(round) { return "Round " + round; })); - rows.unshift(header); - $scope.csvname[stageId] = encodeURIComponent("ranking_" + stageId + ".csv"); - $scope.csvdata[stageId] = "data:text/csv;charset=utf-8," + encodeURIComponent(toCSV(rows)); + $scope.exportFiles[stageID] = "data:text/csv;charset=utf-8,"+encodeURIComponent($scope.encodeArray(teams)); }); }; - // Rebuild CSV data and filenames when scoreboard is updated - $scope.$watch("scoreboard", function() { - $scope.rebuildCSV($scores.scoreboard); + $scope.$watch("scoreboard", function () { + $scope.buildExportFiles(); }, true); + $scope.$watchCollection("settings", function () {//we need to rebuild the files if the user changes his export format + $scope.buildExportFiles(); + }); + + $settings.init().then(function () {//we have to wait for settings to initialize otherwise $scope.settings gets set to undefined + $scope.settings = $settings.settings; + }); $scope.stages = $stages.stages; $scope.scoreboard = $scores.scoreboard; $scope.getRoundLabel = function(round){ return "Round " + round; }; - + } ]); diff --git a/src/js/views/scores.js b/src/js/views/scores.js index 574ab79c..d393cdfc 100644 --- a/src/js/views/scores.js +++ b/src/js/views/scores.js @@ -20,6 +20,18 @@ define('views/scores',[ $scope.rev = (String($scope.sort) === String(col)) ? !$scope.rev : !!defaultSort; $scope.sort = col; }; + + $scope.sortIcon = function(col){ + if(String($scope.sort)!== String(col)){//col and $scope.sort can be arrays, and so this is a quick and dirty way to check for equality + return ''; + } + if ($scope.rev) { + return 'arrow_drop_down'; + } else { + return 'arrow_drop_up'; + } + }; + $scope.removeScore = function(index) { $scores.remove(index); return $scores.save(); diff --git a/src/views/dialogs.html b/src/views/dialogs.html index b3e34e47..205e48c2 100644 --- a/src/views/dialogs.html +++ b/src/views/dialogs.html @@ -156,170 +156,3 @@

-
-
-

-
arrow_back
- Export ranking to standalone file - -

-
- -
-
-
diff --git a/src/views/pages/ranking.html b/src/views/pages/ranking.html index e20bb320..5ec7fbcb 100644 --- a/src/views/pages/ranking.html +++ b/src/views/pages/ranking.html @@ -6,14 +6,11 @@

{{currentPage.title}}

- - -

- + file_download Export @@ -38,30 +35,30 @@

ng-if="stage.rounds > 0" > - + - Rank - + Rank + {{sortIcon(stage, 'rank')}} - Team - + Team + {{sortIcon(stage, 'team.number')}} - Name - + Name + {{sortIcon(stage, 'team.name')}} - Highest - + Highest + {{sortIcon(stage, 'highest')}} - Score - + Score + {{sortIcon(stage, 'highest')}} - Round {{round}} - + Round {{round}} + {{sortIcon(stage, 'scores['+$index+']')}} @@ -69,7 +66,7 @@

- + {{item.rank}} {{item.team.number}} {{item.team.name}} diff --git a/src/views/pages/scores.html b/src/views/pages/scores.html index 0e21587c..9e87f0be 100644 --- a/src/views/pages/scores.html +++ b/src/views/pages/scores.html @@ -14,19 +14,40 @@

Showing {{scores.length}} scores.

- - - - - - - - + + + + + + + + - - + +
#StageRoundTableTeamNameScore
+ # + {{sortIcon('index')}} + + Stage + {{sortIcon(['stage.index','-index'])}} + + Round + {{sortIcon(['stage.index','round','-index'])}} + + Table + {{sortIcon('table')}} + + Team + {{sortIcon('team.number')}} + + Name + {{sortIcon('team.name')}} + + Score + {{sortIcon('score')}} + Action
{{score.index + 1}}
{{$index + 1}} error @@ -76,39 +97,39 @@

diff --git a/src/views/pages/settings.html b/src/views/pages/settings.html index 5c20fb1b..5401d7d4 100644 --- a/src/views/pages/settings.html +++ b/src/views/pages/settings.html @@ -97,6 +97,23 @@

Scoresheet

+
+

Score Export Format

+
+

+ + +

+

+ + +

+

+ + +

+
+
@@ -119,7 +136,7 @@

-

+

@@ -130,7 +147,7 @@

- + shared host to get data from and save data to. E.g. 'http://localhost:1390/' @@ -138,7 +155,7 @@

- + currently the filename containing missions! E.g. '2016_en_US' (no file extension) @@ -147,28 +164,28 @@

- + Enable publishing of scores and rankings, live updates to the score keeping lists, etc.

- + MHub server to send messages to. E.g. 'ws://localhost:13900/'

- + MHub server node to subscribe and publish to. E.g. 'scoring'

-

Log

+

Log

             {{line}}