From 8fc22fa4c5bcc0fa88658dc2087d93edb532ce8f Mon Sep 17 00:00:00 2001 From: Alexis POYEN <apoyen@grandlyon.com> Date: Tue, 18 Aug 2020 09:16:42 +0200 Subject: [PATCH] Feat : create default parameter on round creation preload Parameter on API call --- data/test.db | Bin 77824 -> 77824 bytes internal/models/models.go | 3 + internal/models/parameters.go | 96 +++++ internal/models/round.go | 16 +- internal/models/vote.go | 10 +- internal/rootmux/admin_test.go | 11 +- internal/rootmux/capturer_test.go | 4 +- internal/rootmux/rootmux_test.go | 5 +- internal/rootmux/visualizer_test.go | 4 +- web/components/help/management-help.js | 373 ++++++++++-------- web/components/management/rounds-card.js | 52 ++- .../visualization/results-general.js | 12 +- web/components/visualization/results-zone.js | 33 +- .../visualization/visualization-section.js | 30 +- web/components/vote/votes.js | 92 ++--- web/services/model/round-model.js | 60 ++- 16 files changed, 516 insertions(+), 285 deletions(-) create mode 100644 internal/models/parameters.go diff --git a/data/test.db b/data/test.db index 9bc1a6ad1c39b57807326f33a2b4e09dd863fb6f..e845823db7d6ac62c9be7b05180e57ab3cf69e6f 100644 GIT binary patch delta 1169 zcmZ`&UuYCZ9G=<xyLY!ccP5vVq}4rZQL#B^cV>2WW9zATsgX8bPK0V{d(~ABY?R9- zDn2xApMpsK3>d*A_Y$FRLH9urlF;_W7X_abUwR0F4}v`qg}$`A$u?@DJN))xe*68t z`M&vfq!AuzgvUEG4FEukR~x*NyxKbPDcRWo`JO+N=qr$x`XSn)Hr2Y+ugs@zC+{U5 z$amuZMxMue<Dt-r@Nf*k!o<XlaP%c?xiNvD+b_+9TfIR*_Fv!^kd3wHa`4v!ld-y} zC9<)hf!cRJoGy#sVxPNGT-x?w3gHlHqJ8Kga?$VT3c_d<710GWfactc_KzAyCqSFn zSlAEmic^KpkdhAN<g?{!`A4VbR0mzR@YS9qx(+1u8C0L4FWl5fLrool2~a9|$H)O$ z<my@RbgXsiuM~`KW!;!{>sH=iIwpo?nLDu2jj`daE^Kxg`VlDsiY3uEmS<L%&Nbab z;BJcZoEwZ|o6$80%eM|3EfwEEm-fKar=!HOY-(BJ@3Ho52wg&1wXSYg7L|jkC#iF( zWO7z$<Da;D$0w8RYiX%L88)fdIirchG+Wmvwqaw=m;1xhX4_gkF?CM4$j{{BAyA2V z@`@re-6<k7E$hD}gj3rab$A_(VRJ@_-_sHRR3e_ko@DXUbXGi?>F5Zo3e2V0=C;1# zlxSY-5Zh;WWnOzvOm68qo4TKXABfAdU-~`;NMaC+S>pS8+hqOV8~X;g4Qz?xY}?dX z1xCchdhZ7RZUUG=3_~~R)XxZZxXpSxomECbRM7ysit_58%4J1UzgDQ5Nhg%#$%yR7 z!-g^xxy6q^TDkpI--3}Qp<Y0Av3qf2Sa|`$;+sX2zb%v5gz2WeqO)}7SfzZrGFWp4 zwc|rtk26rKI(|5uQlA)G$m^>FE0Lg#a}wmQ=gh#MQynhXoT2J6`k0RvqYHHJD(e4Z zag%Z$r10H1CnQxu%MuGvmPPZu5*am3Tc<)@I`ldU0T06j>$W(*bh!0x(UdWaiTikI xb2<vqJV5i_pL!i7``|LA>^<Vuw>vuDL=kFvDb$TX?#%|mu;o@J4WDm<;6GC`Bh>%^ delta 721 zcmY*WT}TvB6rP#=pP#+6x|6X6j#iMQ<DEPITVdrx7-X(eAQWPMq=XTa(TiF=N!0Fo zpbc7`2;YU~K`7Rxr06N=K@#d=i5_|oVhIW&sOuI)7ruw{oqN7>zH?@4;n`Za(3)Cj z7^az`iGri(D0Ug$>x|a-LZXq(4KZj}vYzz0A!%8BD!k#}C0@sWMZQG8#gE2Ap$p-u zASJ(IgCTAy%-`arSWA05dwUf4n4dU%`P$^w(KBP0;AQLp*QLU<SZlz=xjh*HVHVZV zVYGn)^c>ws7|o#^x`8Hxl-h&<_d4ujxY)ZNF)2F6zZV3(0|x~fbEuy|OQ=5(XKTsg zqtnq5j%RW?7|3hzy^LTy-yE^KiOa#}JquRyJK<+uP*`*aDM>%sDOIFF@soI66of@k z3%i3ug?Ry{X9Ur(G~4qG$Fgl3dKOgR%NC4J3nFnfVpzoSOp7!UQ)Row=_Zb<>sj5* znnZJmV;P=pHVQ+bi5N)bHi#}XHw3V!IgYKHx=CCraFVtY1yjSsaEWK@Tb!C;CL%mD zJj^6urmVpGg-of3Wn*bCt6Q3GS=b{cHuhqY#Te=jGtjxXuT)F0@dUAT$04TWmEQ8~ zcDTEEtdtknsGw^)!NjqtB^dLSd4I4}km!F0+tdlxx7c&=*_R@&OANhq6S0|~?#r@5 zw;Yw`81w*X$+hGu>8ms+ne^K25__Up%)!X23F2}bq*Vp_R=0)Dhd59xe>ktAz?BLe zqhc%AR2%EbTGWDJ5*@pi1sjzeu=ZEs164>@2O4TestU|hkEj2yGF;tpaqzL)A#;qp Vl9JgU=t&LXm9)%mwiO2>zX2(V%aQ;9 diff --git a/internal/models/models.go b/internal/models/models.go index f389279..3da7356 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -127,6 +127,7 @@ type Parameter struct { CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` DeletedAt *time.Time `json:"-"` + RoundID uint CountBlankAndNull bool ShowOnlyCompleted bool ShowMap bool @@ -258,6 +259,8 @@ func (d *DataHandler) ProcessAPI(w http.ResponseWriter, r *http.Request) { d.handleVote(w, r) case "Maps": d.handleMaps(w, r) + case "Parameter": + d.handleParameters(w, r) } } diff --git a/internal/models/parameters.go b/internal/models/parameters.go new file mode 100644 index 0000000..1198536 --- /dev/null +++ b/internal/models/parameters.go @@ -0,0 +1,96 @@ +package models + +import ( + "encoding/json" + "net/http" + "strconv" + "strings" + + "forge.grandlyon.com/gestion-des-assemblees/elections/internal/auth" +) + +func (d *DataHandler) handleParameters(w http.ResponseWriter, r *http.Request) { + id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/Parameter/")) + switch method := r.Method; method { + case "GET": + switch auth.GetLoggedUserTechnical(w, r).Role { + case "ADMIN", "CAPTURER", "VISUALIZER": + d.getParameter(w, r, id) + default: + http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError) + } + case "POST": + switch auth.GetLoggedUserTechnical(w, r).Role { + case "ADMIN", "CAPTURER", "VISUALIZER": + http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed) + default: + http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError) + } + + case "PUT": + switch auth.GetLoggedUserTechnical(w, r).Role { + case "ADMIN": + d.putParameter(w, r, id) + 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.deleteParameter(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) getParameter(w http.ResponseWriter, r *http.Request, id int) { + if id != 0 { + var o Parameter + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(o) + } +} + +func (d *DataHandler) putParameter(w http.ResponseWriter, r *http.Request, id int) { + var o Parameter + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } + var parameter Parameter + err := json.NewDecoder(r.Body).Decode(¶meter) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + o.CountBlankAndNull = parameter.CountBlankAndNull + o.ShowMap = parameter.ShowMap + o.ShowOnlyCompleted = parameter.ShowOnlyCompleted + d.db.Save(&o) + json.NewEncoder(w).Encode(o) + +} + +func (d *DataHandler) deleteParameter(w http.ResponseWriter, r *http.Request, id int) { + if id != 0 { + var o Parameter + if err := d.db.First(&o, id).Error; err != nil { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + return + } + + d.db.Delete(&o) + } else { + http.Error(w, ErrorIDIsMissing, http.StatusNotFound) + } +} diff --git a/internal/models/round.go b/internal/models/round.go index d542d03..2e953f4 100644 --- a/internal/models/round.go +++ b/internal/models/round.go @@ -55,14 +55,14 @@ func (d *DataHandler) handleRound(w http.ResponseWriter, r *http.Request) { func (d *DataHandler) getRound(w http.ResponseWriter, r *http.Request, id int) { if id != 0 { var o Round - if err := d.db.Preload("DeskRounds").Preload("CandidateLists").First(&o, id).Error; err != nil { + if err := d.db.Preload("DeskRounds").Preload("CandidateLists").Preload("Parameter").First(&o, id).Error; err != nil { http.Error(w, ErrorIDIsMissing, http.StatusNotFound) return } json.NewEncoder(w).Encode(o) } else { var o []Round - d.db.Preload("DeskRounds").Preload("CandidateLists").Find(&o) + d.db.Preload("DeskRounds").Preload("CandidateLists").Preload("Parameter").Find(&o) json.NewEncoder(w).Encode(o) } } @@ -78,6 +78,16 @@ func (d *DataHandler) postRound(w http.ResponseWriter, r *http.Request) { d.db.Create(&o) d.db.Last(&o) + parameter := Parameter{ + RoundID: o.ID, + ShowMap: false, + ShowOnlyCompleted: false, + CountBlankAndNull: false, + } + d.db.Create(¶meter) + d.db.Last(¶meter) + o.Parameter = parameter + var election Election d.db.Preload("Areas").First(&election, o.ElectionID) for _, area := range election.Areas { @@ -95,7 +105,7 @@ func (d *DataHandler) postRound(w http.ResponseWriter, r *http.Request) { func (d *DataHandler) putRound(w http.ResponseWriter, r *http.Request, id int) { var o Round - if err := d.db.Preload("DeskRounds").Preload("CandidateLists").First(&o, id).Error; err != nil { + if err := d.db.Preload("DeskRounds").Preload("CandidateLists").Preload("Parameter").First(&o, id).Error; err != nil { http.Error(w, ErrorIDIsMissing, http.StatusNotFound) return } diff --git a/internal/models/vote.go b/internal/models/vote.go index 72d07f5..7cb688b 100644 --- a/internal/models/vote.go +++ b/internal/models/vote.go @@ -229,7 +229,7 @@ func (vote *Vote) AfterSave(scope *gorm.Scope) error { return errors.New(ErrorValidateVote) } var round Round - if err := scope.DB().First(&round, deskRound.RoundID).Error; err != nil { + if err := scope.DB().Preload("Parameter").First(&round, deskRound.RoundID).Error; err != nil { return errors.New(ErrorValidateVote) } @@ -245,7 +245,13 @@ func (vote *Vote) AfterSave(scope *gorm.Scope) error { } var votesNumberPerDesk = len(votes) - if votesNumberPerDesk == (listNumberPerArea + 2) { + var expectedNumberOfVote = 0 + if round.Parameter.CountBlankAndNull { + expectedNumberOfVote = listNumberPerArea + 2 + } else { + expectedNumberOfVote = listNumberPerArea + } + if votesNumberPerDesk == expectedNumberOfVote { deskRound.Completed = true deskRound.DateCompletion = time.Now() scope.DB().Save(&deskRound) diff --git a/internal/rootmux/admin_test.go b/internal/rootmux/admin_test.go index 554015f..89082be 100644 --- a/internal/rootmux/admin_test.go +++ b/internal/rootmux/admin_test.go @@ -70,13 +70,16 @@ func AdminTests(t *testing.T) { do("PUT", "/api/Desk/1", xsrfHeader, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":false,"Subscribed":3587}`, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":false,"Subscribed":3587,"DeskRounds":[]}`) // Create a Round - do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":null,"CandidateLists":null}`) + do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":null,"CandidateLists":null}`) // Get a Round - do("GET", "/api/Round/1", xsrfHeader, ``, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}`) + do("GET", "/api/Round/1", xsrfHeader, ``, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}`) // Get Rounds - do("GET", "/api/Round/", xsrfHeader, ``, 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}]`) + do("GET", "/api/Round/", xsrfHeader, ``, 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}]`) // Update a Round - do("PUT", "/api/Round/1", xsrfHeader, `{"ID":1,"ElectionID":1,"Date":"2020-07-28","Round":2}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-07-28","Round":2,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}`) + do("PUT", "/api/Round/1", xsrfHeader, `{"ID":1,"ElectionID":1,"Date":"2020-07-28","Round":2}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-07-28","Round":2,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[]}`) + + // Update Parameters + do("PUT", "/api/Parameter/1", xsrfHeader, `{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true}`, 200, `{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true`) // Create a DeskRound should fail with 400 do("POST", "/api/DeskRound", xsrfHeader, `{"Test":1,"Date":"2020-06-28","Round":1}`, 400, `method not allowed`) diff --git a/internal/rootmux/capturer_test.go b/internal/rootmux/capturer_test.go index 8e0e864..2628938 100644 --- a/internal/rootmux/capturer_test.go +++ b/internal/rootmux/capturer_test.go @@ -82,9 +82,9 @@ func CapturerTests(t *testing.T) { // Create a round should fail with 405 do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 405, `You're not authorize to execute this method on this ressource.`) // Get a desk - do("GET", "/api/Round/1", xsrfHeader, "", 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) + do("GET", "/api/Round/1", xsrfHeader, "", 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) // Get all the desks - do("GET", "/api/Round/", xsrfHeader, "", 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}]`) + do("GET", "/api/Round/", xsrfHeader, "", 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}]`) // Update a desk should fail with 405 do("PUT", "/api/Round/1", xsrfHeader, `{"ID":1,"ElectionID":1,"Date":"2020-07-28","Round":2}`, 405, `You're not authorize to execute this method on this ressource.`) // Delete a desk should fail with 405 diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go index 6a8fb30..574aa7f 100644 --- a/internal/rootmux/rootmux_test.go +++ b/internal/rootmux/rootmux_test.go @@ -100,7 +100,7 @@ func appTests(t *testing.T) { do("POST", "/api/Capturer", xsrfHeader, `{"UserID":2,"Name":"Capturer"}`, 500, `UserID is already bind to a Capturer`) // Verify that RoundDesks have been created on Round Creation - do("GET", "/api/Round/1", xsrfHeader, ``, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) + do("GET", "/api/Round/1", xsrfHeader, ``, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) 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}]}`) // Verify that a DeskRound can't be validated witout being completed @@ -287,7 +287,8 @@ func resetDataWithData(t *testing.T) { do("POST", "/api/Section", xsrfHeader, `{"AreaID":1,"Name":"Section 1","MapID":"1"}`, 200, `{"ID":1,"AreaID":1,"Name":"Section 1","MapID":"1","Desks":null}`) do("POST", "/api/Desk", xsrfHeader, `{"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587}`, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587,"DeskRounds":null}`) do("POST", "/api/Desk", xsrfHeader, `{"SectionID":1,"Name":"Desk 2","WitnessDesk":false,"Subscribed":3784}`, 200, `{"ID":2,"SectionID":1,"Name":"Desk 2","WitnessDesk":false,"Subscribed":3784,"DeskRounds":null}`) - do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":null,"CandidateLists":null}`) + do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":null,"CandidateLists":null}`) + do("PUT", "/api/Parameter/1", xsrfHeader, `{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true}`, 200, `{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true`) do("POST", "/api/Party", xsrfHeader, `{"Name":"MyGreatParty","Color":"#FFFFFF"}`, 200, `{"ID":1,"Name":"MyGreatParty","Color":"#FFFFFF","CandidateLists":null}`) do("POST", "/api/CandidateList", xsrfHeader, `{"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1}`, 200, `{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}`) do("POST", "/api/Candidate", xsrfHeader, `{"CandidateListID":1,"FullName":"Candidate","Rank":1,"CommunityCounseller":true,"Birthdate":"2020-06-28","PotentialIncompatibility":false,"Refused":false,"Removed":false}`, 200, `{"ID":1,"CandidateListID":1,"FullName":"Candidate","Rank":1,"CommunityCounseller":true,"Birthdate":"2020-06-28","PotentialIncompatibility":false,"Refused":false,"Removed":false}`) diff --git a/internal/rootmux/visualizer_test.go b/internal/rootmux/visualizer_test.go index 77c1c57..a956e75 100644 --- a/internal/rootmux/visualizer_test.go +++ b/internal/rootmux/visualizer_test.go @@ -69,9 +69,9 @@ func VisualizerTests(t *testing.T) { // Create a round should fail with 405 do("POST", "/api/Round", xsrfHeader, `{"ElectionID":1,"Date":"2020-06-28","Round":1}`, 405, `You're not authorize to execute this method on this ressource.`) // Get a desk - do("GET", "/api/Round/1", xsrfHeader, "", 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) + do("GET", "/api/Round/1", xsrfHeader, "", 200, `{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}`) // Get all the desks - do("GET", "/api/Round/", xsrfHeader, "", 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":0,"CountBlankAndNull":false,"ShowOnlyCompleted":false,"ShowMap":false},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}]`) + do("GET", "/api/Round/", xsrfHeader, "", 200, `[{"ID":1,"ElectionID":1,"Parameter":{"ID":1,"RoundID":1,"CountBlankAndNull":true,"ShowOnlyCompleted":true,"ShowMap":true},"Date":"2020-06-28","Round":1,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null},{"ID":2,"RoundID":1,"DeskID":2,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}],"CandidateLists":[{"ID":1,"Name":"MyGreatList","PartyID":1,"RoundID":1,"AreaID":1,"Candidates":null,"Votes":null}]}]`) // Update a desk should fail with 405 do("PUT", "/api/Round/1", xsrfHeader, `{"ID":1,"ElectionID":1,"Date":"2020-07-28","Round":2}`, 405, `You're not authorize to execute this method on this ressource.`) // Delete a desk should fail with 405 diff --git a/web/components/help/management-help.js b/web/components/help/management-help.js index c9349fa..c59a4dc 100644 --- a/web/components/help/management-help.js +++ b/web/components/help/management-help.js @@ -10,178 +10,207 @@ class ManagementHelper { const mountpoint = where; document.getElementById(mountpoint).innerHTML = /* HTML */ ` <div class="card-no-hover helper"> - <h2 class="title is-2">Aide pour la gestion d'une élection</h2> - - <h5 class="title is-5">Onglet Partis politiques</h5> - - L'onglet "Partis Politiques" permet de créer des tendances poltiques qui - se retrouvent sur plusieurs circonscriptions.<br /> - Pour ajuter un parti il suffit de cliquer sur - <span class="icon is-small"> - <i class="fas fa-plus"></i> - </span> - Un fenêtre s'ouvre et permet de saisir le nom du parti (ou de la - tendance politique) et de choisir la couleur qui sera associé à ce parti - dans l'affichage des résultats. Pour modifier un parti, il suffit de - cliquer sur - <span class="icon is-small"> - <i class="fas fa-pen"></i> - </span> - et pour en supprimer un sur - <span class="icon is-small"> <i class="fas fa-trash"></i> </span>. La - suppression affichera l'ensemble des listes qui sont rattachées à ce - parti sur toutes les élections et demandera de confirmer l'action de - suppression car toutes ces listes seront alors supprimées.<br /><br /> - - <h5 class="title is-5">Onglet Élection</h5> - L'onglet élection permet de créer le découpage territorial d'une - élection. Il est possible de naviguer dans l'abrorescence de l'élection - en cliquant sur les différentes zones la composant<br /><br /> - - <article class="message is-warning"> - <div class="message-header"> - <p>Attention</p> - </div> - <div class="message-body"> - Il est necessaire pour que l'application fonctionne corrctement que - chaque élection ai au moins une circonscription composée elle même - d'au moins une ville avec au minimum un bureau. - </div> - </article> - - <strong>Élection</strong> - Pour créer une nouvelle élection cliquer sur - <span class="icon is-small"> <i class="fas fa-plus"></i> </span>, une - fenêtre s'ouvre alors, il faut saisir le nom de cette élection, choisir - le système de vote qui sera appliqué (pour calculer les résultats), - selectionner la carte pour les cirocnscriptions et celle pour les - villes. En cliquant sur "Sauvegarder" l'élection sera ajoutée et en - cliquant sur "Sauvegarder (ajouter circonscription)" elle sera - sauvegardée et la fenêtre pour y attacher une circonscription sera - ouverte.<br /><br /> - - <strong>Circonscription</strong> - Une circonscription peut être ajoutée à l'élection selectionnée en - cliquant sur - <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il faut - alors saisir son nom, le nombre de siège à pourvoir et l'identifiant qui - lui correspond sur la carte des circonscriptions. <br />En cliquant sur - "Sauvegarder" la circonscription sera ajoutée et en cliquant sur - "Sauvegarder (ajouter circonscription)" elle sera ajoutée et la fenêtre - pour ajouter une nouvelle circonscription à l'élection sera ouverte. En - cliquant sur "Sauvegarder (ajouter ville)" elle sera ajoutée et la - fenêtre pour ajouter une ville à la circonscription sera ouverte.<br /><br /> - - <strong>Ville</strong> - Une ville peut être ajoutée à une circonscription selectionnée en - cliquant sur - <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il faut - alors saisir son nom et l'identifiant qui lui correspond sur la carte - des villes. <br />En cliquant sur "Sauvegarder" la ville sera ajoutée et - en cliquant sur "Sauvegarder (ajouter ville)" elle sera ajoutée et la - fenêtre pour ajouter une nouvelle ville à la circonscription sera - ouverte. En cliquant sur "Sauvegarder (ajouter bureau)" elle sera - ajoutée et la fenêtre pour ajouter un bureau à la ville sera ouverte.<br /><br /> - - <strong>Bureau</strong> - Un bureau peut être ajoutée à une ville selectionnée en cliquant sur - <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il faut - alors saisir son nom et le nombre d'inscrit dans ce bureau. - <article class="message is-warning"> - <div class="message-header"> - <p>Attention</p> - <button class="delete" aria-label="delete"></button> - </div> - <div class="message-body"> - Le nombre d'inscrit est obligatoire car il permet de calculer le - taux d'abstention et le pourcentage de saisie en cours qui est - pondéré par le nombre d'inscrits par bureau. - </div> - </article> - En cliquant sur "Sauvegarder" le bureau sera ajouté et en cliquant sur - "Sauvegarder (ajouter bureau)" il sera ajouté et la fenêtre pour ajouter - un nouveau bureau à la ville sera ouverte.<br /><br /> - - <strong>Cloner une élection</strong> - Il est possible de cloner une élection et son arborescence en cliquant - sur - <span class="icon is-small"> - <i class="fas fa-clone"></i> - </span> - dans la liste des élections.<br /><br /> - - <h5 class="title is-5">Onglet Tour</h5> - Une élection est composée de un ou plusieurs tours, il faut donc - configuré ces tours pour utiliser l'application.<br /><br /> - - Dans l'onglet "Tour" il est possible de créer un tour pour une élection - en cliquant sur - <span class="icon is-small"><i class="fas fa-plus"></i></span>. Il faut - alors sélectionner l'élection à laquelle on crée un tour, la date du - tour et le numéro du tour. Il est possible de modifier les informations - en cliquant sur - <span class="icon is-small"><i class="fas fa-pen"></i></span> et de le - supprimer en cliquant sur - <span class="icon is-small"><i class="fas fa-trash"></i></span - ><br /><br /> - - En cliquant sur un tour, on rentre dans la configuration du tour. La - bouton sur la gauche permet de revenir à l'écran précédent. Il y a deux - sections qui peuvent être ouvertes l'une après l'autre, il faut cliquer - sur la barre de titre de l'une des deux sections pour en changer.<br /><br /> - - <strong>Section "Bureaux de Votes"</strong><br /> - La liste de tous les bureaux de votes de l'éelection s'affiche ici. En - cliquant sur un bureau les détails de ce bureau s'affiche. Il est - possible de consulter les votes et de les modifier comme dans l'onglet - "Votes". Il est possible de "Valider" un bureau, les votes de ce bureau - ne pourront alors plus être modifié.<br /><br /> - - Pour ajouter un utilisateur à la liste des saisseurs, cliquer sur - l'icône - <span class="icon is-small"> - <i class="fas fa-user"></i> - </span> - pour ouvrir la liste des saisisseurs de ce burea, il suffit ensuite de - cliquer sur l'utilisateur dans la colonne de droite "Saisisseurs - disponibles", il va alors basculer dans la colonne de gauche - "Saisisseurs actifs". Pour le supprimer il suffit de faire de même dans - la colonne de gauche "Saisisseurs actifs", il va alors basculer dans la - colonne de droite "Saisisseurs disponibles".<br /><br /> - - <strong>Section "Liste des candidats par circonscription"</strong> - La liste des circonscription du tour est affichée, en cliquant sur une - circonscription, la liste des listes de candidats de la circonscription - s'affiche. Il est possible d'en créer une en cliquant sur - <span class="icon is-small"> <i class="fas fa-plus"></i>. </span>Une - fenêtre s'ouvre alors pour saisir le nom de la liste et le parti auquel - elle est attachée. En cliquant sur - <span class="icon is-small"> - <i class="fas fa-download"></i> - </span> - Une fenêtre s'ouvre et permet d'importer une liste directement depuis un - tour précédent pour éviter de resaisir toutes les informations de la - listes.<br /><br /> - - En cliquant sur une liste, les canddiats s'affichent, il est possible - d'ajouter et de modifier des candidats en éditant les champs dans le - tableau et en cliquant sur - <span class="icon is-small"> - <i class="fas fa-check"></i> - </span> - pour chaque ligne éditée afin qu'elles soient prises en compte.<br /> - Le rang ordonne les candidats lors du calcul des élus, la date de - naissance permet de départager des listes et des candidats lors du - calcul des élus en cas d'égalité, la case incompatibilité potentielle - permet d'identifier les élus qui pourraient poser problème s'ils sont - élus (autre mandat en cours, fonctionnaire...). La case refusé permet - d'éliminer du calcul des élus des candidats qui se sont porter sur une - liste mais qui finalement ne souhaient pas siéger. La case supprimer - permet d'éliminer du calcul des élus les candidats qui seraient décédé - entre la date de cloture de dépôt des listes et l'élection. (La case - "Conseiller communautaire" n'est pas fonctionelle mais permettrai - d'identifier les élus qui vont siéger au conseil communautaire lors - d'élection municipale) + <div class="content"> + <h2 class="title is-2">Aide pour la gestion d'une élection</h2> + + <h5 class="title is-5">Onglet Partis politiques</h5> + + L'onglet "Partis Politiques" permet de créer des tendances poltiques + qui se retrouvent sur plusieurs circonscriptions.<br /> + Pour ajuter un parti il suffit de cliquer sur + <span class="icon is-small"> + <i class="fas fa-plus"></i> + </span> + Un fenêtre s'ouvre et permet de saisir le nom du parti (ou de la + tendance politique) et de choisir la couleur qui sera associé à ce + parti dans l'affichage des résultats. Pour modifier un parti, il + suffit de cliquer sur + <span class="icon is-small"> + <i class="fas fa-pen"></i> + </span> + et pour en supprimer un sur + <span class="icon is-small"> <i class="fas fa-trash"></i> </span>. La + suppression affichera l'ensemble des listes qui sont rattachées à ce + parti sur toutes les élections et demandera de confirmer l'action de + suppression car toutes ces listes seront alors supprimées.<br /><br /> + + <h5 class="title is-5">Onglet Élection</h5> + L'onglet élection permet de créer le découpage territorial d'une + élection. Il est possible de naviguer dans l'abrorescence de + l'élection en cliquant sur les différentes zones la composant<br /><br /> + + <article class="message is-warning"> + <div class="message-header"> + <p>Attention</p> + </div> + <div class="message-body"> + Il est necessaire pour que l'application fonctionne corrctement + que chaque élection ai au moins une circonscription composée elle + même d'au moins une ville avec au minimum un bureau. + </div> + </article> + + <strong>Élection</strong> + Pour créer une nouvelle élection cliquer sur + <span class="icon is-small"> <i class="fas fa-plus"></i> </span>, une + fenêtre s'ouvre alors, il faut saisir le nom de cette élection, + choisir le système de vote qui sera appliqué (pour calculer les + résultats), selectionner la carte pour les cirocnscriptions et celle + pour les villes. En cliquant sur "Sauvegarder" l'élection sera ajoutée + et en cliquant sur "Sauvegarder (ajouter circonscription)" elle sera + sauvegardée et la fenêtre pour y attacher une circonscription sera + ouverte.<br /><br /> + + <strong>Circonscription</strong> + Une circonscription peut être ajoutée à l'élection selectionnée en + cliquant sur + <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il + faut alors saisir son nom, le nombre de siège à pourvoir et + l'identifiant qui lui correspond sur la carte des circonscriptions. + <br />En cliquant sur "Sauvegarder" la circonscription sera ajoutée et + en cliquant sur "Sauvegarder (ajouter circonscription)" elle sera + ajoutée et la fenêtre pour ajouter une nouvelle circonscription à + l'élection sera ouverte. En cliquant sur "Sauvegarder (ajouter ville)" + elle sera ajoutée et la fenêtre pour ajouter une ville à la + circonscription sera ouverte.<br /><br /> + + <strong>Ville</strong> + Une ville peut être ajoutée à une circonscription selectionnée en + cliquant sur + <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il + faut alors saisir son nom et l'identifiant qui lui correspond sur la + carte des villes. <br />En cliquant sur "Sauvegarder" la ville sera + ajoutée et en cliquant sur "Sauvegarder (ajouter ville)" elle sera + ajoutée et la fenêtre pour ajouter une nouvelle ville à la + circonscription sera ouverte. En cliquant sur "Sauvegarder (ajouter + bureau)" elle sera ajoutée et la fenêtre pour ajouter un bureau à la + ville sera ouverte.<br /><br /> + + <strong>Bureau</strong> + Un bureau peut être ajoutée à une ville selectionnée en cliquant sur + <span class="icon is-small"><i class="fas fa-plus"></i></span> . Il + faut alors saisir son nom et le nombre d'inscrit dans ce bureau. + <article class="message is-warning"> + <div class="message-header"> + <p>Attention</p> + <button class="delete" aria-label="delete"></button> + </div> + <div class="message-body"> + Le nombre d'inscrit est obligatoire car il permet de calculer le + taux d'abstention et le pourcentage de saisie en cours qui est + pondéré par le nombre d'inscrits par bureau. + </div> + </article> + En cliquant sur "Sauvegarder" le bureau sera ajouté et en cliquant sur + "Sauvegarder (ajouter bureau)" il sera ajouté et la fenêtre pour + ajouter un nouveau bureau à la ville sera ouverte.<br /><br /> + + <strong>Cloner une élection</strong> + Il est possible de cloner une élection et son arborescence en cliquant + sur + <span class="icon is-small"> + <i class="fas fa-clone"></i> + </span> + dans la liste des élections.<br /><br /> + + <h5 class="title is-5">Onglet Tour</h5> + Une élection est composée de un ou plusieurs tours, il faut donc + configuré ces tours pour utiliser l'application.<br /><br /> + + Dans l'onglet "Tour" il est possible de créer un tour pour une + élection en cliquant sur + <span class="icon is-small"><i class="fas fa-plus"></i></span>. Il + faut alors sélectionner l'élection à laquelle on crée un tour, la date + du tour et le numéro du tour. Il est possible de modifier les + informations en cliquant sur + <span class="icon is-small"><i class="fas fa-pen"></i></span> et de le + supprimer en cliquant sur + <span class="icon is-small"><i class="fas fa-trash"></i></span + ><br /><br /> + + En cliquant sur un tour, on rentre dans la configuration du tour. La + bouton sur la gauche permet de revenir à l'écran précédent. Il y a + deux sections qui peuvent être ouvertes l'une après l'autre, il faut + cliquer sur la barre de titre de l'une des deux sections pour en + changer.<br /><br /> + + <em>Options d'un tour</em><br /><br /> + + Trois options sont disponibles :<br /> + + <ul> + <li> + <em>Afficher une cartographie des résultats : </em>Permet + d'afficher une carte des résultats. Si cette option est cochée + il est nécessaire de saisir les identifiants de carte pour les + circonscriptions et les villes. + </li> + <li> + <em>Comptabiliser les votes blancs et nuls : </em>Oblige à saisir + les votes blancs et nuls pour compléter un bureau de vote. + </li> + <li> + <em>Ne pas afficher de résultats partiel : </em>Empêche la + possibilité d'afficher les résultats partiels dans l'onglet + "Résultats" + </li> + </ul> + <br /> + + <strong>Section "Bureaux de Votes"</strong><br /> + La liste de tous les bureaux de votes de l'éelection s'affiche ici. En + cliquant sur un bureau les détails de ce bureau s'affiche. Il est + possible de consulter les votes et de les modifier comme dans l'onglet + "Votes". Il est possible de "Valider" un bureau, les votes de ce + bureau ne pourront alors plus être modifié.<br /><br /> + + Pour ajouter un utilisateur à la liste des saisseurs, cliquer sur + l'icône + <span class="icon is-small"> + <i class="fas fa-user"></i> + </span> + pour ouvrir la liste des saisisseurs de ce burea, il suffit ensuite de + cliquer sur l'utilisateur dans la colonne de droite "Saisisseurs + disponibles", il va alors basculer dans la colonne de gauche + "Saisisseurs actifs". Pour le supprimer il suffit de faire de même + dans la colonne de gauche "Saisisseurs actifs", il va alors basculer + dans la colonne de droite "Saisisseurs disponibles".<br /><br /> + + <strong>Section "Liste des candidats par circonscription"</strong> + La liste des circonscription du tour est affichée, en cliquant sur une + circonscription, la liste des listes de candidats de la + circonscription s'affiche. Il est possible d'en créer une en cliquant + sur + <span class="icon is-small"> <i class="fas fa-plus"></i>. </span>Une + fenêtre s'ouvre alors pour saisir le nom de la liste et le parti + auquel elle est attachée. En cliquant sur + <span class="icon is-small"> + <i class="fas fa-download"></i> + </span> + Une fenêtre s'ouvre et permet d'importer une liste directement depuis + un tour précédent pour éviter de resaisir toutes les informations de + la listes.<br /><br /> + + En cliquant sur une liste, les canddiats s'affichent, il est possible + d'ajouter et de modifier des candidats en éditant les champs dans le + tableau et en cliquant sur + <span class="icon is-small"> + <i class="fas fa-check"></i> + </span> + pour chaque ligne éditée afin qu'elles soient prises en compte.<br /> + Le rang ordonne les candidats lors du calcul des élus, la date de + naissance permet de départager des listes et des candidats lors du + calcul des élus en cas d'égalité, la case incompatibilité potentielle + permet d'identifier les élus qui pourraient poser problème s'ils sont + élus (autre mandat en cours, fonctionnaire...). La case refusé permet + d'éliminer du calcul des élus des candidats qui se sont porter sur une + liste mais qui finalement ne souhaient pas siéger. La case supprimer + permet d'éliminer du calcul des élus les candidats qui seraient décédé + entre la date de cloture de dépôt des listes et l'élection. (La case + "Conseiller communautaire" n'est pas fonctionelle mais permettrai + d'identifier les élus qui vont siéger au conseil communautaire lors + d'élection municipale) + </div> </div> `; } diff --git a/web/components/management/rounds-card.js b/web/components/management/rounds-card.js index d23545f..0370823 100644 --- a/web/components/management/rounds-card.js +++ b/web/components/management/rounds-card.js @@ -139,6 +139,36 @@ class Round { <input class="input" type="number" id="round-modal-round" /> </div> </div> + <div class="field"> + <label class="checkbox"> + <input + class="input" + type="checkbox" + id="parameter-modal-showmap" + /> + Afficher une cartographie des résultats + </label> + </div> + <div class="field"> + <label class="checkbox"> + <input + class="input" + type="checkbox" + id="parameter-modal-countblankandnull" + /> + Comptabiliser les votes blancs et nuls + </label> + </div> + <div class="field"> + <label class="checkbox"> + <input + class="input" + type="checkbox" + id="parameter-modal-onlycompletedresults" + /> + Ne pas afficher de résultats partiel + </label> + </div> </section> <footer class="modal-card-foot"> <button id="round-modal-save" class="button is-success"> @@ -183,7 +213,7 @@ class Round { }); }); document - .getElementById(`rounds-round-${round.ID}`) + .getElementById(`rounds-round-desc-${round.ID}`) .addEventListener("click", async function () { roundHandler.activateRound(round); }); @@ -262,6 +292,13 @@ class Round { document.getElementById("round-modal-id").value = null; document.getElementById("round-modal-date").value = null; document.getElementById("round-modal-round").value = null; + document.getElementById( + "parameter-modal-countblankandnull" + ).checked = false; + document.getElementById("parameter-modal-showmap").checked = false; + document.getElementById( + "parameter-modal-onlycompletedresults" + ).checked = false; Common.toggleModal("round-modal", "round-modal-card"); } @@ -274,6 +311,12 @@ class Round { .toISOString() .split("T")[0]; document.getElementById("round-modal-round").value = round.Round; + document.getElementById("parameter-modal-countblankandnull").checked = + round.Parameter.CountBlankAndNull; + document.getElementById("parameter-modal-showmap").checked = + round.Parameter.ShowMap; + document.getElementById("parameter-modal-onlycompletedresults").checked = + round.Parameter.ShowOnlyCompleted; Common.toggleModal("round-modal", "round-modal-card"); } @@ -288,6 +331,13 @@ class Round { document.getElementById("round-modal-date").value, parseInt(document.getElementById("round-modal-round").value) ); + + await this.RoundModel.saveParameter( + round.Parameter.ID, + document.getElementById("parameter-modal-countblankandnull").checked, + document.getElementById("parameter-modal-showmap").checked, + document.getElementById("parameter-modal-onlycompletedresults").checked + ); await this.displayRounds(); Common.toggleModal("round-modal", "round-modal-card"); this.activateRound(round); diff --git a/web/components/visualization/results-general.js b/web/components/visualization/results-general.js index 3061f4d..74a7d86 100644 --- a/web/components/visualization/results-general.js +++ b/web/components/visualization/results-general.js @@ -15,7 +15,7 @@ class ResultGeneralComponent { async mount(where) { const mountpoint = where; document.getElementById(mountpoint).innerHTML = /* HTML */ ` - <div class="column is-half"> + <div class="column"> <div class="card-no-hover"> <header class="card-header"> <p class="card-header-title"> @@ -26,16 +26,6 @@ class ResultGeneralComponent { <div id="round-detaileds-results" class="content"></div> </div> </div> - <div class="column is-half"> - <div class="card-no-hover"> - <header class="card-header"> - <p class="card-header-title"> - Statistiques - </p> - </header> - <div id="stats-results" class="content"></div> - </div> - </div> `; this.handleDom(); this.displayRoundResults(); diff --git a/web/components/visualization/results-zone.js b/web/components/visualization/results-zone.js index 1279afa..f8a41a1 100644 --- a/web/components/visualization/results-zone.js +++ b/web/components/visualization/results-zone.js @@ -40,7 +40,7 @@ class ResultZoneComponent { </span> </button> </header> - <div id="map-component" class="card-content">Cartes</div> + <div id="map-component" class="card-content">Les cartes ne sont pas activées pour ce tour</div> </div> </div> <div class="column"> @@ -92,7 +92,8 @@ class ResultZoneComponent { `; this.ResultsFlow = await ResultsFlow.mount(this); this.ResultsDetaileds = await ResultsDetaileds.mount(this); - this.ResultsMap = await ResultsMap.mount(this); + if (this.parent.round.Parameter.ShowMap) + this.ResultsMap = await ResultsMap.mount(this); this.scroller = Scroller.scrollInit( "news-flow", document.getElementById("auto-scroll") @@ -232,22 +233,26 @@ class ResultZoneComponent { if (this.parent.zone === "areas") { await this.ResultsFlow.displayFlowAreas(); this.ResultsDetaileds.displayAreasResults(); - if (this.ResultsMap.map.getSource("data-source") !== undefined) { - let dataSource = await this.ResultsMap.colorAreas( - this.ResultsMap, - this.ResultsMap.dataSource - ); - this.ResultsMap.map.getSource("data-source").setData(dataSource); + if (this.parent.round.Parameter.ShowMap) { + if (this.ResultsMap.map.getSource("data-source") !== undefined) { + let dataSource = await this.ResultsMap.colorAreas( + this.ResultsMap, + this.ResultsMap.dataSource + ); + this.ResultsMap.map.getSource("data-source").setData(dataSource); + } } } else if (this.parent.zone === "sections") { await this.ResultsFlow.displayFlowSections(); this.ResultsDetaileds.displaySectionsResults(); - if (this.ResultsMap.map.getSource("data-source") !== undefined) { - let dataSource = await this.ResultsMap.colorSections( - this.ResultsMap, - this.ResultsMap.dataSource - ); - this.ResultsMap.map.getSource("data-source").setData(dataSource); + if (this.parent.round.Parameter.ShowMap) { + if (this.ResultsMap.map.getSource("data-source") !== undefined) { + let dataSource = await this.ResultsMap.colorSections( + this.ResultsMap, + this.ResultsMap.dataSource + ); + this.ResultsMap.map.getSource("data-source").setData(dataSource); + } } } } diff --git a/web/components/visualization/visualization-section.js b/web/components/visualization/visualization-section.js index 26bcfa9..b81c0de 100644 --- a/web/components/visualization/visualization-section.js +++ b/web/components/visualization/visualization-section.js @@ -46,12 +46,19 @@ class ResultComponent { </ul> </div> <div class="control filter"> + ${this.round.Parameter.ShowOnlyCompleted + ? `` + : /* HTML */ `<label class="radio"> + <input type="radio" name="filter" value="partial" checked /> + Partiel + </label>`} <label class="radio"> - <input type="radio" name="filter" value="partial" checked /> - Partiel - </label> - <label class="radio"> - <input type="radio" name="filter" value="completed" /> + <input + type="radio" + name="filter" + value="completed" + ${this.round.Parameter.ShowOnlyCompleted ? `checked` : ``} + /> Complété </label> <label class="radio"> @@ -120,15 +127,16 @@ class ResultComponent { } async hideGeneralSection() { - let resultHandler = this; document.getElementById("general").setAttribute("class", ""); document.getElementById("results-zone").style.display = "flex"; document.getElementById("results-general").style.display = "none"; - await resultHandler.calculateResults(); - if (resultHandler.zone == "areas") - await this.resultsZone.ResultsMap.displayMapAreas(); - else await this.resultsZone.ResultsMap.displayMapSections(); - await resultHandler.resultsZone.displayResults(); + await this.calculateResults(); + if (this.round.Parameter.ShowMap) { + if (this.zone == "areas") + await this.resultsZone.ResultsMap.displayMapAreas(); + else await this.resultsZone.ResultsMap.displayMapSections(); + } + await this.resultsZone.displayResults(); } async calculateResults() { diff --git a/web/components/vote/votes.js b/web/components/vote/votes.js index 6aa38a1..8fbace6 100644 --- a/web/components/vote/votes.js +++ b/web/components/vote/votes.js @@ -185,7 +185,9 @@ class Vote { .map((vote) => this.voteTemplate(vote)) .join(""); document.getElementById("votes-list").innerHTML = markup; - document.getElementById("votes-list").innerHTML += /* HTML */ ` + let round = await this.RoundModel.getRound(this.RoundID); + if (round.Parameter.CountBlankAndNull) { + document.getElementById("votes-list").innerHTML += /* HTML */ ` <tr"> <td>Votes blanc</td> <td> @@ -197,24 +199,27 @@ class Vote { </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> - `; + document.getElementById("votes-list").innerHTML += /* HTML */ ` + <tr> + <td>Votes nul</td> + <td> + <input class="input" type="number" id="null-vote-voice" /> + </td> + </tr> + `; + } let totalVotes = 0; let votesExpressed = 0; votes.forEach((vote) => { - totalVotes += vote.VoiceNumber; - if (vote.Blank) { + if (vote.Blank && round.Parameter.CountBlankAndNull) { + totalVotes += vote.VoiceNumber; document.getElementById("blank-vote-voice").value = vote.VoiceNumber; - } else if (vote.NullVote) { + } else if (vote.NullVote && round.Parameter.CountBlankAndNull) { + totalVotes += vote.VoiceNumber; document.getElementById("null-vote-voice").value = vote.VoiceNumber; - } else { + } else if (!vote.Blank && !vote.NullVote) { + totalVotes += vote.VoiceNumber; votesExpressed += vote.VoiceNumber; document.getElementById(vote.CandidateListID + "-vote-voice").value = vote.VoiceNumber; @@ -268,40 +273,35 @@ class Vote { ); } - if ( - parseInt( - document.getElementById("blank-vote-voice").value - ) < 0 - ) { - Messages.Show("is-warning", "Erreur sur les valeurs saisies"); - return; - } else { - await this.VoteModel.saveVote( - method, - this.DeskRoundID, - null, - parseInt(document.getElementById("blank-vote-voice").value), - true, - false - ); - } + let round = await this.RoundModel.getRound(this.RoundID); + if (round.Parameter.CountBlankAndNull) { + if (parseInt(document.getElementById("blank-vote-voice").value) < 0) { + Messages.Show("is-warning", "Erreur sur les valeurs saisies"); + return; + } else { + await this.VoteModel.saveVote( + method, + this.DeskRoundID, + null, + parseInt(document.getElementById("blank-vote-voice").value), + true, + false + ); + } - if ( - parseInt( - document.getElementById("null-vote-voice").value - ) < 0 - ) { - Messages.Show("is-warning", "Erreur sur les valeurs saisies"); - return; - } else { - await this.VoteModel.saveVote( - method, - this.DeskRoundID, - null, - parseInt(document.getElementById("null-vote-voice").value), - false, - true - ); + if (parseInt(document.getElementById("null-vote-voice").value) < 0) { + Messages.Show("is-warning", "Erreur sur les valeurs saisies"); + return; + } else { + await this.VoteModel.saveVote( + method, + this.DeskRoundID, + null, + parseInt(document.getElementById("null-vote-voice").value), + false, + true + ); + } } this.refreshParent(); } diff --git a/web/services/model/round-model.js b/web/services/model/round-model.js index 295b1bc..04731b1 100644 --- a/web/services/model/round-model.js +++ b/web/services/model/round-model.js @@ -46,21 +46,18 @@ class RoundModel { async saveRound(method, ID, ElectionID, Date, Round) { try { - const response = await fetch( - "/api/Round/" + ID, - { - method: method, - headers: new Headers({ - "XSRF-Token": this.current_user.xsrftoken, - }), - body: JSON.stringify({ - ID: ID, - ElectionID: ElectionID, - Date: Date, - Round: Round, - }), - } - ); + const response = await fetch("/api/Round/" + ID, { + method: method, + headers: new Headers({ + "XSRF-Token": this.current_user.xsrftoken, + }), + body: JSON.stringify({ + ID: ID, + ElectionID: ElectionID, + Date: Date, + Round: Round, + }), + }); if (response.status !== 200) { throw new Error( `Round could not be updated or created (status ${response.status})` @@ -99,4 +96,37 @@ class RoundModel { this.rounds = null; await this.getRounds(); } + + async saveParameter( + ID, + countBlankAndNull, + showMap, + showOnlyCompleted + ) { + try { + const response = await fetch("/api/Parameter/" + ID, { + method: "PUT", + headers: new Headers({ + "XSRF-Token": this.current_user.xsrftoken, + }), + body: JSON.stringify({ + ID: ID, + CountBlankAndNull: countBlankAndNull, + ShowMap: showMap, + ShowOnlyCompleted: showOnlyCompleted, + }), + }); + if (response.status !== 200) { + throw new Error( + `Parameter could not be updated (status ${response.status})` + ); + } + this.refreshRounds(); + return await response.json(); + } catch (e) { + Messages.Show("is-warning", e.message); + console.error(e); + return; + } + } } -- GitLab