From 9d810354afbdcece8089f0a6eca8b42ead532dc4 Mon Sep 17 00:00:00 2001 From: Alexis POYEN <apoyen@grandlyon.com> Date: Tue, 30 Jun 2020 10:42:04 +0200 Subject: [PATCH] Resolve "Display news flow" --- internal/models/vote.go | 1 + internal/rootmux/rootmux_test.go | 4 +- .../visualization/results-section.js | 118 ++++++++++++++++-- web/services/common/scroller.js | 48 +++++++ .../election/calculate-election-generic.js | 32 +++-- web/style.css | 30 ++++- 6 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 web/services/common/scroller.js diff --git a/internal/models/vote.go b/internal/models/vote.go index 458be68..5f1d800 100644 --- a/internal/models/vote.go +++ b/internal/models/vote.go @@ -239,6 +239,7 @@ func (d *DataHandler) deleteVote(w http.ResponseWriter, r *http.Request, id int) return } deskRound.Completed = false + deskRound.DateCompletion = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC) d.db.Save(&deskRound) d.db.Delete(&o) diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go index 81e046d..7d19555 100644 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -131,10 +131,10 @@ func appTests(t *testing.T) { // //Check that on Vote deletion, deskRound is updated do("PUT", "/api/DeskRound/1", xsrfHeader, `{"ID":1,"Validated":false}`, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":true,"DateCompletion":"20`) do("DELETE", "/api/Vote/1", xsrfHeader, ``, 200, ``) - do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":false,"DateCompletion":"20`) + do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":[{"ID":3,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":true,"NullVote":false},{"ID":4,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":false,"NullVote":true}]}`) // Verify that on Desk deletion deskRounds are deleted - do("GET", "/api/Desk/1", xsrfHeader, ``, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"20`) + do("GET", "/api/Desk/1", xsrfHeader, ``, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}]}`) do("DELETE", "/api/Desk/1", xsrfHeader, ``, 200, ``) do("GET", "/api/DeskRound/1", xsrfHeader, ``, 404, `id is missing`) diff --git a/web/components/visualization/results-section.js b/web/components/visualization/results-section.js index b27d99c..cba58a7 100644 --- a/web/components/visualization/results-section.js +++ b/web/components/visualization/results-section.js @@ -1,18 +1,24 @@ // Imports +import * as Auth from "/services/auth/auth.js"; import * as results from "/services/election/calculate-election-generic.js"; +import * as PartyModel from "/services/model/party-model.js"; +import * as Scroller from "/services/common/scroller.js"; export async function mount(where, round) { const resultComponent = new ResultComponent(round); await resultComponent.mount(where); await resultComponent.calculateResults(); + resultComponent.displayResults(); } class ResultComponent { constructor(round) { this.round = round; + this.PartyModel = PartyModel.getPartyModel(); } async mount(where) { + this.PartyModel.current_user = await Auth.GetUser(); const mountpoint = where; document.getElementById(mountpoint).innerHTML = /* HTML */ ` <div class="tabs is-boxed is-toggle is-fullwidth"> @@ -76,7 +82,7 @@ class ResultComponent { </button> </header> <div class="card-content"> - <div id="round-list" class="content"> + <div id="news-flow" class="content"> Flux d'actualité </div> </div> @@ -100,22 +106,55 @@ class ResultComponent { </div> </div> `; + this.calculator = await results.mountCalculator(this.round); this.handleDom(); document.getElementById("areas").click(); - this.calculator = await results.mountCalculator(this.round); + } + + resultFlowTemplate(zone) { + return /* HTML */ `<div class="card-list card-no-hover"> + <div class="card-content"> + <div id="flow-content-${zone.ID}" class="content"> + <h5 class="title is-5">${zone.Name}</h5> + </div> + </div> + </div>`; + } + + async progressBarTemplate(candidateList) { + let party = await this.PartyModel.getParty(candidateList.PartyID); + return /* HTML */ `<div class="progressBar"> + <div + class="progressBarValue" + style="background-color : ${party.Color}; width : ${candidateList.Percentage}%" + > + ${candidateList.Name + + " (" + + candidateList.VoiceNumber + + " voix soit : " + + candidateList.Percentage + + "%)"} + </div> + </div>`; } handleDom() { + let resultHandler = this; document.getElementById("areas").addEventListener("click", function () { + resultHandler.zone = "areas"; + resultHandler.calculateResults(); + resultHandler.displayResults(); document.getElementById("sections").setAttribute("class", ""); document.getElementById("areas").setAttribute("class", "is-active"); }); document.getElementById("sections").addEventListener("click", function () { + resultHandler.zone = "sections"; + resultHandler.calculateResults(); + resultHandler.displayResults(); document.getElementById("areas").setAttribute("class", ""); document.getElementById("sections").setAttribute("class", "is-active"); }); - let resultHandler = this; document.getElementById("zoom-map").addEventListener("click", function () { resultHandler.zoomMap(); }); @@ -132,8 +171,9 @@ class ResultComponent { let radioButtons = document.getElementsByName("filter"); for (var i = 0; i < radioButtons.length; i++) { - radioButtons[i].addEventListener("click", (e) => { - this.calculateResults(); + radioButtons[i].addEventListener("click", async (e) => { + await this.calculateResults(); + this.displayResults(); }); } } @@ -151,7 +191,7 @@ class ResultComponent { let resultHandler = this; document.getElementById("news-flow-section").parentElement.className = "column is-full"; - document.getElementById("news-flow-section").style.height = "auto"; + document.getElementById("news-flow-section").style.height = "70vh"; document.getElementById("map-section").parentElement.className = "column"; document.getElementById("map-section").parentElement.style.display = "none"; document.getElementById("results-section").style.display = "none"; @@ -191,7 +231,69 @@ class ResultComponent { } async calculateResults() { - let filter = document.querySelector('input[name="filter"]:checked').value; - await this.calculator.calculateResults(filter); + this.filter = document.querySelector('input[name="filter"]:checked').value; + this.results = await this.calculator.calculateResults(this.filter); + } + + displayResults() { + console.log(this.results); + document.getElementById("news-flow").innerHTML = ""; + if (this.zone === "areas") { + this.displayFlowAreas(); + } else if (this.zone === "sections") { + this.displayFlowSections(); + } + let scroller = Scroller.scrollInit("news-flow"); + } + + async displayFlowAreas() { + let resultHandler = this; + this.results.areasResults.sort(function (a, b) { + return b.DateCompletion - a.DateCompletion; + }); + for (let j in this.results.areasResults) { + let area = this.results.areasResults[j]; + if (area.status === resultHandler.filter) { + document.getElementById( + "news-flow" + ).innerHTML += this.resultFlowTemplate(area); + + for (let i in area.candidateLists) { + document.getElementById( + "flow-content-" + area.ID + ).innerHTML += await resultHandler.progressBarTemplate( + area.candidateLists[i] + ); + } + } + } + } + + async displayFlowSections() { + let resultHandler = this; + let sections = []; + this.results.areasResults.forEach((area) => { + sections = sections.concat(area.Sections); + }); + sections.sort(function (a, b) { + return b.DateCompletion - a.DateCompletion; + }); + + for (let j in sections) { + let section = sections[j]; + if (section.status === resultHandler.filter) { + document.getElementById( + "news-flow" + ).innerHTML += this.resultFlowTemplate(section); + + for (let i in section.candidateLists) { + document.getElementById( + "flow-content-" + section.ID + ).innerHTML += await resultHandler.progressBarTemplate( + section.candidateLists[i] + ); + } + } + } } } diff --git a/web/services/common/scroller.js b/web/services/common/scroller.js new file mode 100644 index 0000000..1db6044 --- /dev/null +++ b/web/services/common/scroller.js @@ -0,0 +1,48 @@ +export function scrollInit(divName) { + let scroller = new Scroller(divName); +} + +class Scroller { + constructor(divName) { + this.elmnt = document.getElementById(divName); + this.reachedMaxScroll = false; + + this.elmnt.scrollTop = 0; + this.previousScrollTop = 0; + + this.scrollDiv(); + this.elmnt.addEventListener("mouseover", () => { + this.pauseScroll(); + }); + this.elmnt.addEventListener("mouseout", () => { + this.resumeScroll(); + }); + } + + scrollDiv() { + if (!this.reachedMaxScroll) { + this.elmnt.scrollBy({ top: 5, left: 0, behavior: "smooth" }); + this.reachedMaxScroll = + this.elmnt.scrollTop >= + this.elmnt.scrollHeight - this.elmnt.offsetHeight; + this.scrollTimeout = setTimeout(() => { + this.scrollDiv(); + }, 100); + } else { + this.reachedMaxScroll = this.elmnt.scrollTop == 0 ? false : true; + this.elmnt.scrollBy({ top: -5, left: 0, behavior: "smooth" }); + + this.scrollTimeout = setTimeout(() => { + this.scrollDiv(); + }, 100); + } + } + + pauseScroll() { + clearInterval(this.scrollTimeout); + } + + resumeScroll() { + this.scrollDiv(); + } +} diff --git a/web/services/election/calculate-election-generic.js b/web/services/election/calculate-election-generic.js index 5a6ccb6..f35ff0f 100644 --- a/web/services/election/calculate-election-generic.js +++ b/web/services/election/calculate-election-generic.js @@ -98,7 +98,7 @@ class DirectMetropolitanCalculator { } this.areasResults = await this.calculateAreasResults(); - console.log(this); + return this; } async calculateRoundResults() { @@ -176,6 +176,13 @@ class DirectMetropolitanCalculator { } area.Sections = sections; + let lastDate = new Date(area.Sections[0].DateCompletion); + area.Sections.forEach((section) => { + if (lastDate - new Date(section.DateCompletion) < 0) + lastDate = new Date(section.DateCompletion); + }); + area.DateCompletion = lastDate; + area.stats = await this.calculateStats(deskRounds); let flag = true; @@ -183,7 +190,7 @@ class DirectMetropolitanCalculator { case "partial": if (area.stats.VotesExpressed === 0) { area.status = "no_results"; - return; + return area; } area.status = "partial"; break; @@ -196,7 +203,7 @@ class DirectMetropolitanCalculator { area.status = "completed"; } else { area.status = "incompleted"; - return; + return area; } break; case "validated": @@ -208,7 +215,7 @@ class DirectMetropolitanCalculator { area.status = "validated"; } else { area.status = "not validated"; - return; + return area; } } @@ -220,7 +227,7 @@ class DirectMetropolitanCalculator { 0 ); candidateList.Percentage = - (candidateList.VoiceNumber / area.stats.VotesExpressed) * 100; + Number((candidateList.VoiceNumber / area.stats.VotesExpressed) * 100).toFixed(2); }); area.candidateLists = candidateListToKeep; area.candidateLists.sort((a, b) => { @@ -246,13 +253,20 @@ class DirectMetropolitanCalculator { } } + let lastDate = new Date(deskRounds[0].DateCompletion); + deskRounds.forEach((desk) => { + if (lastDate - new Date(desk.DateCompletion) < 0) + lastDate = new Date(desk.DateCompletion); + }); + section.DateCompletion = lastDate; + section.stats = await this.calculateStats(deskRounds); let flag = true; switch (this.filter) { case "partial": if (section.stats.VotesExpressed === 0) { section.status = "no_results"; - return; + return section; } section.status = "partial"; break; @@ -265,7 +279,7 @@ class DirectMetropolitanCalculator { section.status = "completed"; } else { section.status = "incompleted"; - return; + return section; } break; case "validated": @@ -277,7 +291,7 @@ class DirectMetropolitanCalculator { section.status = "validated"; } else { section.status = "not validated"; - return; + return section ; } } @@ -294,7 +308,7 @@ class DirectMetropolitanCalculator { 0 ); candidateList.Percentage = - (candidateList.VoiceNumber / section.stats.VotesExpressed) * 100; + Number((candidateList.VoiceNumber / section.stats.VotesExpressed) * 100).toFixed(2); }); section.candidateLists = candidateLists; section.candidateLists.sort((a, b) => { diff --git a/web/style.css b/web/style.css index a17f16f..d7a5bc0 100644 --- a/web/style.css +++ b/web/style.css @@ -120,13 +120,21 @@ select { } #round-desks .column, -#candidate-lists .column { +#candidate-lists .column, +#news-flow { overflow-y: auto; } +#news-flow, +#news-flow .content { + flex: 1; +} + #round-desks .columns, -#candidate-lists .columns { +#candidate-lists .columns, +#news-flow-section .card-content { max-height: 90%; + display: flex; } .card-header-success { @@ -154,11 +162,25 @@ select { background-color: #fff; } -.filter{ +.filter { text-align: center; } #news-flow-section { height: 45vh; - margin-bottom: 15px;; + margin-bottom: 15px; +} + +.progressBar { + width: 99%; + margin: 3px; + background-color: lightgray; + border-radius: 5px; +} + +.progressBarValue { + height: 30px; + white-space: nowrap; + padding: 4px; + border-radius: 5px; } -- GitLab