diff --git a/internal/models/vote.go b/internal/models/vote.go index 1d504aa909009c4b5cbc090e6d6351b68550c35c..32a0fd6d345d747fd18d9e2fc4d18ee2c2e5cfb5 100644 --- a/internal/models/vote.go +++ b/internal/models/vote.go @@ -35,7 +35,7 @@ func (d *DataHandler) handleVote(w http.ResponseWriter, r *http.Request) { case "PUT": switch auth.GetLoggedUserTechnical(w, r).Role { case "ADMIN", "CAPTURER": - d.putVote(w, r, id) + d.putVote(w, r) case "VISUALIZER": http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed) default: @@ -112,23 +112,32 @@ func (d *DataHandler) postVote(w http.ResponseWriter, r *http.Request) { } -func (d *DataHandler) putVote(w http.ResponseWriter, r *http.Request, id int) { - var o Vote - if err := d.db.First(&o, id).Error; err != nil { - http.Error(w, ErrorIDIsMissing, http.StatusNotFound) - return - } +func (d *DataHandler) putVote(w http.ResponseWriter, r *http.Request) { var vote Vote err := json.NewDecoder(r.Body).Decode(&vote) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } - // check that objects are the same - if o.CandidateListID != vote.CandidateListID || o.DeskRoundID != vote.DeskRoundID { - http.Error(w, "Les objets ne correspondent pas", http.StatusInternalServerError) - return + + var o Vote + if vote.Blank { + if err := d.db.Where("blank = true and desk_round_id = ?", vote.DeskRoundID).Find(&o).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } + } else if vote.NullVote { + if err := d.db.Where("null_vote = true and desk_round_id = ?", vote.DeskRoundID).Find(&o).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } + } else { + if err := d.db.Where("candidate_list_id = ? and desk_round_id = ?", vote.CandidateListID, vote.DeskRoundID).Find(&o).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } } + o.VoiceNumber = vote.VoiceNumber d.db.Save(&o) json.NewEncoder(w).Encode(o) @@ -143,6 +152,16 @@ func (d *DataHandler) deleteVote(w http.ResponseWriter, r *http.Request, id int) return } d.db.Delete(&o) + + // Set completed to false for deskRound + var deskRound DeskRound + if err := d.db.First(&deskRound, o.DeskRoundID).Error; err != nil { + http.Error(w, ErrorParentNotFound, http.StatusNotFound) + return + } + deskRound.Completed = false + d.db.Save(&deskRound) + } else { http.Error(w, ErrorIDIsMissing, http.StatusNotFound) } diff --git a/internal/rootmux/admin_test.go b/internal/rootmux/admin_test.go index 0da5abb840d71b9a14bf1f33566c99f90961f409..5e8fba7ceb6a1b715c879a8e96e2dad54d35a55b 100644 --- a/internal/rootmux/admin_test.go +++ b/internal/rootmux/admin_test.go @@ -124,7 +124,7 @@ func AdminTests(t *testing.T) { // Get Votes do("GET", "/api/Vote/", xsrfHeader, ``, 200, `[{"ID":1,"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":158,"Blank":false,"NullVote":false}]`) // Update a Vote - do("PUT", "/api/Vote/1", xsrfHeader, `{"ID":1,"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":true}`, 200, `{"ID":1,"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":false}`) + do("PUT", "/api/Vote/1", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":false}`, 200, `{"ID":1,"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":false}`) // TODO Update a DeskRound to Validated=true can only be done when votes are captured diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go index 6e0e9df93e2ccf02128e056f7e37cb95456e020f..2d3d47c985699f2b24197f306eb4f7b8275111ff 100644 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -106,23 +106,27 @@ func appTests(t *testing.T) { // Verify that a DeskRound can't be validated witout being completed do("PUT", "/api/DeskRound/1", xsrfHeader, `{"ID":1,"Validated":true}`, 500, `Le bureau doit être complété avant de le valider`) - // Verify that you can't update a Vote if it's not the same - do("PUT", "/api/Vote/1", xsrfHeader, `{"ID":1,"DeskRoundID":2,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":true}`, 500, `Les objets ne correspondent pas`) - do("PUT", "/api/Vote/1", xsrfHeader, `{"ID":1,"DeskRoundID":2,"CandidateListID":2,"VoiceNumber":258,"Blank":false,"NullVote":true}`, 500, `Les objets ne correspondent pas`) - do("PUT", "/api/Vote/1", xsrfHeader, `{"ID":1,"DeskRoundID":1,"CandidateListID":2,"VoiceNumber":258,"Blank":false,"NullVote":true}`, 500, `Les objets ne correspondent pas`) - // Create Votes to complete a Desk do("POST", "/api/Vote", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":3,"Blank":true}`, 200, `{"ID":2,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":3,"Blank":true,"NullVote":false}`) do("POST", "/api/Vote", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":5,"NullVote":true}`, 200, `{"ID":3,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":5,"Blank":false,"NullVote":true}`) do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[],"Completed":true,"DateCompletion":"20`) + // Check to update the good vote + do("PUT", "/api/Vote/1", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":false}`, 200, `{"ID":1,"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":258,"Blank":false,"NullVote":false}`) + do("PUT", "/api/Vote/1", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":158,"Blank":true,"NullVote":false}`, 200, `{"ID":2,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":true,"NullVote":false}`) + do("PUT", "/api/Vote/1", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":158,"Blank":false,"NullVote":true}`, 200, `{"ID":3,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":false,"NullVote":true}`) + // Can't add the same vote several time do("POST", "/api/Vote", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":1,"VoiceNumber":158}`, 500, `Error the vote have already been captured`) do("POST", "/api/Vote", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":3,"Blank":true}`, 500, `Error the vote have already been captured`) do("POST", "/api/Vote", xsrfHeader, `{"DeskRoundID":1,"CandidateListID":null,"VoiceNumber":5,"NullVote":true}`, 500, `Error the vote have already been captured`) + //Check that on Vote deletion, deskRound is updated + do("DELETE", "/api/Vote/1", xsrfHeader, ``, 200, ``) + do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[],"Completed":false,"DateCompletion":"20`) + // 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":true,"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":"20`) do("DELETE", "/api/Desk/1", xsrfHeader, ``, 200, ``) do("GET", "/api/DeskRound/1", xsrfHeader, ``, 404, `id is missing`) diff --git a/web/components/vote/desk-round.js b/web/components/vote/desk-round.js index 34d08b50f32d314ee053c1e5513493b0bd1cba27..e37ac2ef81d19397f5082089d9d088d5280cb556 100644 --- a/web/components/vote/desk-round.js +++ b/web/components/vote/desk-round.js @@ -217,11 +217,10 @@ class DeskRoundSelector { } async openVotes(deskRound) { - this.parent.VoteHandler.displayVotes( + this.parent.voteHandler.displayVotes( this.RoundID, this.AreaID, deskRound.ID ); - console.log("Ouverture des votes"); } } diff --git a/web/components/vote/votes.js b/web/components/vote/votes.js index fb806024523ab5032bab28c5adecce76b99cf1d5..259ab779a059f3dcf6c446441d1cb2e4ab2db609 100644 --- a/web/components/vote/votes.js +++ b/web/components/vote/votes.js @@ -1,5 +1,13 @@ // Imports import * as Auth from "/services/auth/auth.js"; +import * as ElectionModel from "/services/model/election-model.js"; +import * as RoundModel from "/services/model/round-model.js"; +import * as AreaModel from "/services/model/area-model.js"; +import * as SectionModel from "/services/model/section-model.js"; +import * as DeskModel from "/services/model/desk-model.js"; +import * as DeskRoundModel from "/services/model/deskRound-model.js"; +import * as VoteModel from "/services/model/vote-model.js"; +import * as CandidateListModel from "/services/model/candidateList-model.js"; export async function mount(parent) { const voteComponent = new Vote(parent); @@ -10,7 +18,258 @@ class Vote { constructor(parent) { this.method = null; this.parent = parent; + this.ElectionModel = ElectionModel.getElectionModel(); + this.RoundModel = RoundModel.getRoundModel(); + this.AreaModel = AreaModel.getAreaModel(); + this.SectionModel = SectionModel.getSectionModel(); + this.DeskModel = DeskModel.getDeskModel(); + this.DeskRoundModel = DeskRoundModel.getDeskRoundModel(); + this.VoteModel = VoteModel.getVoteModel(); + this.CandidateListModel = CandidateListModel.getCandidateListModel(); } - async displayVotes(RoundID, AreaID, DeskRoundID) {} + async displayVotes(RoundID, AreaID, DeskRoundID) { + this.ElectionModel.current_user = await Auth.GetUser(); + this.RoundModel.current_user = await Auth.GetUser(); + this.AreaModel.current_user = await Auth.GetUser(); + this.SectionModel.current_user = await Auth.GetUser(); + this.DeskModel.current_user = await Auth.GetUser(); + this.DeskRoundModel.current_user = await Auth.GetUser(); + this.VoteModel.current_user = await Auth.GetUser(); + this.CandidateListModel.current_user = await Auth.GetUser(); + + this.RoundID = RoundID; + this.AreaID = AreaID; + this.DeskRoundID = DeskRoundID; + + document.getElementById("vote-section").innerHTML = /* HTML */ ` + <header class="card-header"> + <p > + <nav class="breadcrumb card-header-title" aria-label="breadcrumbs"> + <ul id="vote-breadcrumb"></ul> + </nav> + </p> + </header> + <div id="votes-table" class="card-content"></div> + <nav class="level"> + <div class="level-left"></div> + <div class="level-right"> + <button id="votes-return" class="button level-item"> + Retour + </button> + <button id="votes-cancel" class="button level-item"> + Annuler + </button> + <button id="votes-delete" class="button is-danger level-item"> + Supprimer + </button> + <button id="votes-save" class="button is-success level-item"> + Sauvegarder + </button> + </div> + </nav> + `; + this.handleDom(); + + await this.refreshBreadCrumb(); + await this.loadVotes(); + } + + voteTemplate(candidateList) { + return /* HTML */ ` + <tr id="votes-vote-${candidateList.ID}"> + <td> + ${candidateList.Name} + </td> + <td> + <input + class="input" + type="number" + id="${candidateList.ID}-vote-voice" + /> + </td> + </tr> + `; + } + + handleDom() { + let voteHandler = this; + document + .getElementById(`votes-return`) + .addEventListener("click", function () { + voteHandler.parent.deskRoundHandler.mount("vote-section"); + }); + document + .getElementById(`votes-cancel`) + .addEventListener("click", function () { + voteHandler.loadVotes(); + }); + document + .getElementById(`votes-save`) + .addEventListener("click", function () { + voteHandler.saveVotes(); + }); + document + .getElementById(`votes-delete`) + .addEventListener("click", function () { + voteHandler.deleteVotes(); + }); + } + + async refreshBreadCrumb() { + let round = await this.RoundModel.getRound(this.RoundID); + let election = await this.ElectionModel.getElection(round.ElectionID); + let area = await this.AreaModel.getArea(this.AreaID); + let deskRound = await this.DeskRoundModel.getDeskRound(this.DeskRoundID); + let desk = await this.DeskModel.getDesk(deskRound.DeskID); + let section = await this.SectionModel.getSection(desk.SectionID); + + let breadcrumb = document.getElementById("vote-breadcrumb"); + let el = document.createElement("li"); + el.innerHTML = "<a>" + election.Name + "</a>"; + breadcrumb.appendChild(el); + el = document.createElement("li"); + el.innerHTML = + "<a>tour : " + + round.Round + + ", date : " + + new Date(round.Date).toLocaleDateString() + + "</a>"; + breadcrumb.appendChild(el); + el = document.createElement("li"); + el.innerHTML = "<a>" + area.Name + "</a>"; + breadcrumb.appendChild(el); + el = document.createElement("li"); + el.innerHTML = "<a>" + section.Name + "</a>"; + breadcrumb.appendChild(el); + el = document.createElement("li"); + el.innerHTML = "<a>" + desk.Name + "</a>"; + breadcrumb.appendChild(el); + } + + async loadVotes() { + document.getElementById("votes-table").innerHTML = /* HTML */ `<div + class="table-container" + > + <table class="table is-bordered is-narrow is-hoverable is-fullwidth"> + <thead> + <tr class="is-selected"> + <th>Liste</th> + <th>Nombre de voix</th> + </tr> + </thead> + <tbody id="votes-list"></tbody> + </table> + </div> `; + + let votes = await this.updatesVotes(); + let candidateLists = await this.updateCandidateLists(); + + const markup = candidateLists + .map((vote) => this.voteTemplate(vote)) + .join(""); + document.getElementById("votes-list").innerHTML = markup; + document.getElementById("votes-list").innerHTML += /* HTML */ ` + <tr"> + <td>Votes blanc</td> + <td> + <input + class="input" + type="number" + id="blank-vote-voice" + /> + </td> + </tr> + `; + document.getElementById("votes-list").innerHTML += /* HTML */ ` + <tr> + <td>Votes nul</td> + <td> + <input class="input" type="number" id="null-vote-voice" /> + </td> + </tr> + `; + + votes.forEach((vote) => { + if (vote.Blank) { + document.getElementById("blank-vote-voice").value = vote.VoiceNumber; + } else if (vote.NullVote) { + document.getElementById("null-vote-voice").value = vote.VoiceNumber; + } else { + document.getElementById(vote.CandidateListID + "-vote-voice").value = + vote.VoiceNumber; + } + }); + } + + async saveVotes() { + let voteHandler = this; + let candidateLists = await this.updateCandidateLists(); + let votes = await this.updatesVotes(); + + let method; + if (votes.length == 0) method = "POST"; + else method = "PUT"; + + candidateLists.forEach(async (candidateList) => { + await voteHandler.VoteModel.saveVote( + method, + voteHandler.DeskRoundID, + candidateList.ID, + parseInt( + document.getElementById(candidateList.ID + "-vote-voice").value + ), + false, + false + ); + }); + + await this.VoteModel.saveVote( + method, + this.DeskRoundID, + null, + parseInt(document.getElementById("blank-vote-voice").value), + true, + false + ); + await this.VoteModel.saveVote( + method, + this.DeskRoundID, + null, + parseInt(document.getElementById("null-vote-voice").value), + false, + true + ); + await this.VoteModel.refreshVotes(); + await this.loadVotes(); + } + + async updatesVotes() { + let voteHandler = this; + let votes = await this.VoteModel.getVotes(); + return votes.filter((vote) => { + return vote.DeskRoundID == voteHandler.DeskRoundID; + }); + } + + async updateCandidateLists() { + let voteHandler = this; + let candidateLists = await this.CandidateListModel.getCandidateLists(); + return candidateLists.filter((candidateList) => { + return ( + candidateList.AreaID == voteHandler.AreaID && + candidateList.RoundID == voteHandler.RoundID + ); + }); + } + + async deleteVotes() { + let voteHandler = this; + let votes = await this.updatesVotes(); + votes.forEach(async (vote) => { + await voteHandler.VoteModel.deleteVote(vote.ID); + await voteHandler.VoteModel.refreshVotes(); + await voteHandler.loadVotes(); + }); + } } diff --git a/web/services/model/vote-model.js b/web/services/model/vote-model.js new file mode 100644 index 0000000000000000000000000000000000000000..ac75693e5f23281c54eda48557a2edb34b6ab57e --- /dev/null +++ b/web/services/model/vote-model.js @@ -0,0 +1,105 @@ +import * as Messages from "/services/messages/messages.js"; + +let voteModel; + +export function getVoteModel() { + if (voteModel == null) { + voteModel = new VoteModel(); + } + return voteModel; +} + +class VoteModel { + constructor() {} + + async getVote(id) { + if (this.votes == null) await this.refreshVotes(); + let voteToGet; + this.votes.forEach((vote) => { + if (vote.ID == id) voteToGet = vote; + }); + return voteToGet; + } + + async getVotes() { + if (this.votes == null) { + try { + const response = await fetch("/api/Vote/", { + method: "GET", + headers: new Headers({ + "XSRF-Token": this.current_user.xsrftoken, + }), + }); + if (response.status !== 200) { + throw new Error( + `Votes could not be fetched (status ${response.status})` + ); + } + this.votes = await response.json(); + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + } + } + return this.votes; + } + + async saveVote( + method, + DeskRoundID, + CandidateListID, + VoiceNumber, + Blank, + NullVote + ) { + try { + const response = await fetch("/api/Vote/", { + method: method, + headers: new Headers({ + "XSRF-Token": this.current_user.xsrftoken, + }), + body: JSON.stringify({ + DeskRoundID: DeskRoundID, + CandidateListID: CandidateListID, + VoiceNumber: VoiceNumber, + Blank: Blank, + NullVote: NullVote, + }), + }); + if (response.status !== 200) { + throw new Error( + `Vote could not be updated or created (status ${response.status})` + ); + } + return await response.json(); + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + return; + } + } + + async deleteVote(ID) { + try { + const response = await fetch("/api/Vote/" + ID, { + method: "delete", + headers: new Headers({ + "XSRF-Token": this.current_user.xsrftoken, + }), + }); + if (response.status !== 200) { + throw new Error( + `Vote could not be deleted (status ${response.status})` + ); + } + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + } + } + + async refreshVotes() { + this.votes = null; + await this.getVotes(); + } +} diff --git a/web/style.css b/web/style.css index df4b86c774e0316302ab7cf4d7b26d06d5b017f9..6d759601971ee417ae2223c627037b312ed3a717 100644 --- a/web/style.css +++ b/web/style.css @@ -135,8 +135,13 @@ select { .upper-text { writing-mode: sideways-lr; - background-color: rgba(55,122,195,.95); + background-color: rgba(55, 122, 195, 0.95); text-orientation: sideways-right; text-align: center; cursor: pointer; } + +#vote-breadcrumb a { + color: #000; + cursor: default; +}