diff --git a/internal/models/capturer.go b/internal/models/capturer.go index 115b42d4121017ae51c41697d794d88638496576..f404ef30bea434f447d3a573b2dd312d0c3eb3a3 100644 --- a/internal/models/capturer.go +++ b/internal/models/capturer.go @@ -82,7 +82,6 @@ func (d *DataHandler) getCapturerCapturer(w http.ResponseWriter, r *http.Request http.Error(w, ErrorIDIsMissing, http.StatusNotFound) return } - // fmt.Println(o) if o.UserID != user.UserID { http.Error(w, ErrorCannotAccessRessource, http.StatusForbidden) return diff --git a/internal/models/capturerDeskRound.go b/internal/models/capturerDeskRound.go new file mode 100644 index 0000000000000000000000000000000000000000..eeb51ee03dd20547857cb9c30e030fa14383cc9b --- /dev/null +++ b/internal/models/capturerDeskRound.go @@ -0,0 +1,89 @@ +package models + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "forge.grandlyon.com/apoyen/elections/internal/auth" +) + +type capturerDeskRound struct { + CapturerID uint + DeskRoundID uint +} + +// handleCapturer handle API calls on Capturer +func (d *DataHandler) handleCapturerDeskRound(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/CapturerDeskRound/")) + switch method := r.Method; method { + case "POST": + switch auth.GetLoggedUserTechnical(w, r).Role { + case "ADMIN": + d.postCapturerDeskRound(w, r) + case "CAPTURER", "VISUALIZER": + http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed) + default: + http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError) + } + case "DELETE": + switch auth.GetLoggedUserTechnical(w, r).Role { + case "ADMIN": + d.deleteCapturerDeskRound(w, r, id) + case "CAPTURER", "VISUALIZER": + http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed) + default: + http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError) + } + default: + http.Error(w, "method not allowed", 400) + } +} + +func (d *DataHandler) postCapturerDeskRound(w http.ResponseWriter, r *http.Request) { + var o capturerDeskRound + err := json.NewDecoder(r.Body).Decode(&o) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var capturer Capturer + if err := d.db.First(&capturer, o.CapturerID).Error; err != nil { + http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound) + return + } + + var deskRound DeskRound + if err := d.db.First(&deskRound, o.DeskRoundID).Error; err != nil { + http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound) + return + } + + d.db.Model(&capturer).Association("DeskRounds").Append(&deskRound) + d.db.Preload("DeskRounds").First(&capturer) + json.NewEncoder(w).Encode(capturer) +} + +func (d *DataHandler) deleteCapturerDeskRound(w http.ResponseWriter, r *http.Request, id int) { + var o capturerDeskRound + err := json.NewDecoder(r.Body).Decode(&o) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var capturer Capturer + if err := d.db.First(&capturer, o.CapturerID).Error; err != nil { + http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound) + return + } + + var deskRound DeskRound + if err := d.db.First(&deskRound, o.DeskRoundID).Error; err != nil { + http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound) + return + } + + d.db.Model(&capturer).Association("DeskRounds").Delete(&deskRound) + json.NewEncoder(w).Encode(capturer) +} diff --git a/internal/models/models.go b/internal/models/models.go index 1939950a95c756014eaa947d996d139be98f04ed..2788b429c8d1be7fd07cb7ec603bc6cb53b86fc0 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -232,6 +232,8 @@ func (d *DataHandler) ProcessAPI(w http.ResponseWriter, r *http.Request) { d.handleRound(w, r) case "DeskRound": d.handleDeskRound(w, r) + case "CapturerDeskRound": + d.handleCapturerDeskRound(w, r) } } diff --git a/internal/rootmux/admin_test.go b/internal/rootmux/admin_test.go index b80792167f94c7f0e8ec61a9e3abc04e5752f7e4..64b1c739a43020dc9d79e2eec04265c46ab9da91 100644 --- a/internal/rootmux/admin_test.go +++ b/internal/rootmux/admin_test.go @@ -87,6 +87,11 @@ func AdminTests(t *testing.T) { // Delete a DeskRound should fail with 400 do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`) + // Add deskround to capturer + do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 200, `{"ID":1,"UserID":2,"Name":"capturer","DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}]}`) + // Remove DeskRound to capturer + do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 200, ``) + // Update a DeskRound should be done after desk completion by capturing votes // do("PUT", "/api/DeskRound/1", xsrfHeader, `{"ID":1,"Validated":true}`, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[],"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":true,"Votes":[]}`) diff --git a/internal/rootmux/capturer_test.go b/internal/rootmux/capturer_test.go index 5ccd09ee4a273b082cc7819498467133f1b86d7c..2de600a1517f14a07cb3a7cf8bc6a07ae7fe1a89 100644 --- a/internal/rootmux/capturer_test.go +++ b/internal/rootmux/capturer_test.go @@ -99,6 +99,11 @@ func CapturerTests(t *testing.T) { // Delete a DeskRound should fail with 400 do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`) + // Add deskround to capturer should fail with 405 + do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`) + // Remove DeskRound to capturer + do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`) + } // Do a in memory login with an known admin do("POST", "/Login", noH, `{"login": "capturer","password": "password"}`, 200, "") diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go index 4295acfdc663451b32807634239330eabdcd70ab..c8dfb25d5fde42419d09056d66d9c4af2e8561f7 100644 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -61,10 +61,10 @@ func TestAll(t *testing.T) { removeRoundRemoveDeskRoundsTest(t) resetData(t) AdminTests(t) - // resetDataWithData(t) - // CapturerTests(t) - // resetDataWithData(t) - // VisualizerTests(t) + resetDataWithData(t) + CapturerTests(t) + resetDataWithData(t) + VisualizerTests(t) os.RemoveAll("./data") } diff --git a/internal/rootmux/visualizer_test.go b/internal/rootmux/visualizer_test.go index f872760c7395c63e36ee2b78c908a581ffbca119..a36147f1b636ada67e491bb5610c11945e4cb6c0 100644 --- a/internal/rootmux/visualizer_test.go +++ b/internal/rootmux/visualizer_test.go @@ -86,6 +86,11 @@ func VisualizerTests(t *testing.T) { // Delete a DeskRound should fail with 400 do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`) + // Add deskround to capturer should fail with 405 + do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`) + // Remove DeskRound to capturer + do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`) + } // Do a in memory login with an known admin do("POST", "/Login", noH, `{"login": "visualizer","password": "password"}`, 200, "") diff --git a/web/components/management/round-desks.js b/web/components/management/round-desks.js index 28ce765bd5beaaabb9011eb5c691816a6909682b..6d82d7000060fb3940af6479b082f3dbb1597cc5 100644 --- a/web/components/management/round-desks.js +++ b/web/components/management/round-desks.js @@ -1,5 +1,6 @@ // Imports import * as Auth from "/services/auth/auth.js"; +import * as Common from "/services/common/common.js"; import * as Getters from "/services/common/getters.js"; import * as Messages from "/services/messages/messages.js"; @@ -43,6 +44,61 @@ class RoundDesk { </div> `; current_user = await Auth.GetUser(); + this.mountModal("capturers-modal"); + this.handleDom(); + } + + handleDom() { + let roundHandler = this; + document.getElementById(`round-new`).addEventListener("click", function () { + roundHandler.newRound(); + }); + + document + .getElementById(`capturers-modal-close`) + .addEventListener("click", function () { + Common.toggleModal("capturers-modal", "capturers-modal-card"); + }); + } + + mountModal(where) { + const mountpoint = where; + document.getElementById(mountpoint).innerHTML = /* HTML */ ` + <div class="modal-background"></div> + <div class="modal-card" id="capturers-modal-card"> + <header class="modal-card-head"> + <p class="modal-card-title">Ajout/supression de saisisseurs</p> + <button + class="delete" + aria-label="close" + id="capturers-modal-close" + ></button> + </header> + <section class="modal-card-body"> + <div class="columns"> + <div class="column"> + <header class="card-header card-header-success"> + <p class="card-header-title"> + Saisisseurs actifs + </p> + </header> + <div id="active-capturers"> + Liste des saisisseurs actif + </div> + </div> + <div class="column"> + <header class="card-header card-header-danger"> + <p class="card-header-title"> + Saisisseurs disponibles + </p> + </header> + + <div id="available-capturers""> Liste des saisseurs disponibles + </div> + </div> + </section> + </div> + `; } deskRoundTemplate(deskRound) { @@ -54,7 +110,17 @@ class RoundDesk { id="deskrounds-deskround-desc-${deskRound.ID}" class="level-left" ></div> - <div class="level-right"></div> + <div class="level-right"> + <a + id="deskrounds-deskround-capturers-${deskRound.ID}" + class="button is-link is-small" + title="Saisisseurs" + > + <span class="icon is-small"> + <i class="fas fa-user"></i> + </span> + </a> + </div> </nav> </div> </div> @@ -85,7 +151,10 @@ class RoundDesk { </div> <div class="field"> <div class="control"> - <label class="checkbox" ${!this.deskRound.Completed ? 'disabled="true"' : ""}> + <label + class="checkbox" + ${!this.deskRound.Completed ? 'disabled="true"' : ""} + > <input class="input" type="checkbox" @@ -109,6 +178,19 @@ class RoundDesk { </div>`; } + capturersTemplate(capturer) { + return /* HTML */ `<div class="card card-list"> + <div id="capturers-capturer-${capturer.ID}" class="card-content"> + <div class="content"> + <nav class="level"> + <div class="level-left">${capturer.Name}</div> + <div class="level-right"></div> + </nav> + </div> + </div> + </div>`; + } + async displayDesks() { let deskRounds = await this.updateDeskRounds(); const markup = deskRounds @@ -128,7 +210,12 @@ class RoundDesk { deskRoundHandler.activateDeskRound(deskRound); deskRoundHandler.deskRound = deskRound; deskRoundHandler.displayDeskRoundsDetails(); - // TODO open capturers affected + }); + document + .getElementById(`deskrounds-deskround-capturers-${deskRound.ID}`) + .addEventListener("click", function () { + Common.toggleModal("capturers-modal", "capturers-modal-card"); + deskRoundHandler.refreshCapturers(); }); }); } @@ -182,7 +269,6 @@ class RoundDesk { } async saveDeskRound() { - let round; try { const response = await fetch("/api/DeskRound/" + this.deskRound.ID, { method: "PUT", @@ -208,4 +294,117 @@ class RoundDesk { Messages.Show("is-success", "Bureau de vote mis à jour"); this.displayDesks(); } + + async refreshCapturers() { + let deskRoundHandler = this; + let capturersAvailable; + let capturersActive; + try { + const response = await fetch("/api/Capturer/", { + method: "GET", + headers: new Headers({ + "XSRF-Token": current_user.xsrftoken, + }), + }); + if (response.status !== 200) { + throw new Error( + `Capturers could not be fetched (status ${response.status})` + ); + } + let capturers = await response.json(); + capturersAvailable = capturers.filter(function (capturer) { + let flag = true; + capturer.DeskRounds.forEach((deskRound) => { + if (deskRound.ID === deskRoundHandler.deskRound.ID) flag = false; + }); + return flag; + }); + capturersActive = capturers.filter(function (capturer) { + let flag = false; + capturer.DeskRounds.forEach((deskRound) => { + if (deskRound.ID === deskRoundHandler.deskRound.ID) flag = true; + }); + return flag; + }); + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + } + + let markup = capturersAvailable + .map((capturer) => this.capturersTemplate(capturer)) + .join(""); + document.getElementById("available-capturers").innerHTML = markup; + + markup = capturersActive + .map((capturer) => this.capturersTemplate(capturer)) + .join(""); + document.getElementById("active-capturers").innerHTML = markup; + + capturersAvailable.map(async (capturer) => { + document + .getElementById(`capturers-capturer-${capturer.ID}`) + .addEventListener("click", async function () { + await deskRoundHandler.addCapturerToDesk(capturer); + await deskRoundHandler.refreshCapturers(); + }); + }); + + capturersActive.map(async (capturer) => { + document + .getElementById(`capturers-capturer-${capturer.ID}`) + .addEventListener("click", async function () { + await deskRoundHandler.removeCapturerFromDesk(capturer); + await deskRoundHandler.refreshCapturers(); + }); + }); + } + + async addCapturerToDesk(capturer) { + try { + const response = await fetch("/api/CapturerDeskRound/", { + method: "POST", + headers: new Headers({ + "XSRF-Token": current_user.xsrftoken, + }), + body: JSON.stringify({ + CapturerID: capturer.ID, + DeskRoundID: this.deskRound.ID, + }), + }); + if (response.status !== 200) { + throw new Error( + `DeskRound could not be added to capturer (status ${response.status})` + ); + } + await response.json(); + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + return; + } + } + + async removeCapturerFromDesk(capturer) { + try { + const response = await fetch("/api/CapturerDeskRound/", { + method: "delete", + headers: new Headers({ + "XSRF-Token": current_user.xsrftoken, + }), + body: JSON.stringify({ + CapturerID: capturer.ID, + DeskRoundID: this.deskRound.ID, + }), + }); + if (response.status !== 200) { + throw new Error( + `DeskRound could not be removed from capturer (status ${response.status})` + ); + } + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + } + } } diff --git a/web/components/management/round.js b/web/components/management/round.js index a977ceaa39cdc2e3e620bc1dc3b11f6aff80b61f..43b9b9e62f9ce0d1ae54f241cdb4d6c588548109 100644 --- a/web/components/management/round.js +++ b/web/components/management/round.js @@ -37,6 +37,7 @@ class Round { </div> <div class="modal" id="round-modal"></div> + <div class="modal" id="capturers-modal"></div> `; current_user = await Auth.GetUser(); this.roundsHandler = await RoundsCard.mount("rounds-list", this) diff --git a/web/style.css b/web/style.css index 8039cb53528ec56e52a9bfae911e6ea69dc50a9a..b75e09429e22f0ada035d7f7e1f5746a6f310e59 100644 --- a/web/style.css +++ b/web/style.css @@ -121,3 +121,11 @@ select { #round-desks .columns { max-height: 90%; } + +.card-header-success{ + background-color: rgba(127,186,0,.95); +} + +.card-header-danger{ + background-color: rgba(242,80,34,.95); +} \ No newline at end of file