From 5cd3d4a664ab19c2de2c5531e4565a23186dc331 Mon Sep 17 00:00:00 2001
From: Alexis POYEN <apoyen@grandlyon.com>
Date: Fri, 26 Jun 2020 11:14:45 +0200
Subject: [PATCH] Resolve "Calculate the results for direct metropolitan
 election"

---
 data/test.db                                  | Bin 73728 -> 73728 bytes
 .../visualization/results-section.js          |  23 +-
 .../election/calculate-election-generic.js    | 424 ++++++++++++++++++
 3 files changed, 443 insertions(+), 4 deletions(-)
 create mode 100644 web/services/election/calculate-election-generic.js

diff --git a/data/test.db b/data/test.db
index e4f37d29268ba4499279d30c1322d4f75cac4977..2f2f142d93f55b5661c089395d795c6bd40d1d10 100644
GIT binary patch
literal 73728
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

delta 897
zcmYjPOK1~O6rGv;I!R{UBsNBqhE5U0Qd1{s&8HPgo7%dNrZI?!LWu2Xrj0Xonk+(5
zTXbOsvGJmt+K(0qB7W#BL`t!xZn{XKAk@9M68sf(<HE@_)I8qfzQ?)eoO>^Ca!o(E
zrk`=!;uwb6s|QzOWBzcPHIB7L)+zPH5KH+{kI~2xOZZ-}F}GnKKB2G-fz$Z3itjFE
ziMXRPPfai;r_+hwI1O|7w&FEy9demb3`alFXY>gvXdOL3kI^)Wp&SY;Hr9<G+{Lkb
zc6D|xG_7kwLrHO17*1yR@qjYjwcQA36CUfyoFuXlUd+h637#a{VI@j(p^mmttFMjo
z2g0G?(H73%!EsvRfFuu-l|Bq~H--u*O8=qf>2Afw9HEWp3?vqdL7(Ua;hF``i#C`M
zd*G$$f$!q6iZIxudN?6<YIi}Zr!-MNe+DP9cpP3xtrdi>+P8&+;b5R$`6VINt$La*
zn;5!}n(0+KMtz}j6m5NE4Oli+_vaR6bGCt~8!?zwA&i6%vko{u@4EMpRD&H7bNfld
zh`t6~nRmhUyCkesQT>Rn2K6mysNK)CwugQGpjt|G44RZ~gs4d_>?A{Bp<?8!6YwfH
zHRpgUvy~z=p<pQJ5BNguJM*jvEidA%L+#8+B`^f&ApMFCQza^4-BNpcXT@*j^EZjh
zMtD<l!_}e(-ma1`S+ujp!IY2|#>RLyA`PYZjGyB;FXvOYV4$#Q6jn=4_*!&pgl|RW
zfT3UBczQ{ab8N4a9jYZ{^;BhIcqgot>LFFCn3zB))wk#el9^O8E%59hFUx}H?HAG)
zc)7+bqub$zRjyCv8R28e;nelXNpYhf3FGX5wDBzM?a5xe$Y-FT+^gk8%8n+YN6v~8
z+r!IZQXc+a{wS^~lFh231C(-u##k;p=(zfM)gpNIR958E%I`AGDf;(+(QAgmf1vy#
F^AA|N5k>$2

diff --git a/web/components/visualization/results-section.js b/web/components/visualization/results-section.js
index cbee50f..b27d99c 100644
--- a/web/components/visualization/results-section.js
+++ b/web/components/visualization/results-section.js
@@ -1,8 +1,10 @@
 // Imports
+import * as results from "/services/election/calculate-election-generic.js";
 
 export async function mount(where, round) {
   const resultComponent = new ResultComponent(round);
   await resultComponent.mount(where);
+  await resultComponent.calculateResults();
 }
 
 class ResultComponent {
@@ -29,15 +31,15 @@ class ResultComponent {
       </div>
       <div class="control filter">
         <label class="radio">
-          <input type="radio" name="answer" checked />
+          <input type="radio" name="filter" value="partial" checked />
           Partiel
         </label>
         <label class="radio">
-          <input type="radio" name="answer" />
+          <input type="radio" name="filter" value="completed" />
           Complété
         </label>
         <label class="radio">
-          <input type="radio" name="answer" />
+          <input type="radio" name="filter" value="validated" />
           Validé
         </label>
       </div>
@@ -100,6 +102,7 @@ class ResultComponent {
     `;
     this.handleDom();
     document.getElementById("areas").click();
+    this.calculator = await results.mountCalculator(this.round);
   }
 
   handleDom() {
@@ -126,6 +129,13 @@ class ResultComponent {
       .addEventListener("click", function () {
         resultHandler.zoomResults();
       });
+
+    let radioButtons = document.getElementsByName("filter");
+    for (var i = 0; i < radioButtons.length; i++) {
+      radioButtons[i].addEventListener("click", (e) => {
+        this.calculateResults();
+      });
+    }
   }
 
   zoomMap() {
@@ -152,7 +162,7 @@ class ResultComponent {
       });
   }
 
-  zoomResults(){
+  zoomResults() {
     let resultHandler = this;
     document.getElementById("results-section").parentElement.className =
       "column is-full";
@@ -179,4 +189,9 @@ class ResultComponent {
 
     this.handleDom();
   }
+
+  async calculateResults() {
+    let filter = document.querySelector('input[name="filter"]:checked').value;
+    await this.calculator.calculateResults(filter);
+  }
 }
diff --git a/web/services/election/calculate-election-generic.js b/web/services/election/calculate-election-generic.js
new file mode 100644
index 0000000..5a6ccb6
--- /dev/null
+++ b/web/services/election/calculate-election-generic.js
@@ -0,0 +1,424 @@
+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 PartyModel from "/services/model/party-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 mountCalculator(round) {
+  const directMetropolitanCalculator = new DirectMetropolitanCalculator(round);
+  await directMetropolitanCalculator.initModel();
+  return directMetropolitanCalculator;
+}
+
+class DirectMetropolitanCalculator {
+  constructor(round, filter) {
+    this.round = round;
+    this.ElectionModel = ElectionModel.getElectionModel();
+    this.RoundModel = RoundModel.getRoundModel();
+    this.AreaModel = AreaModel.getAreaModel();
+    this.SectionModel = SectionModel.getSectionModel();
+    this.DeskModel = DeskModel.getDeskModel();
+    this.PartyModel = PartyModel.getPartyModel();
+    this.DeskRoundModel = DeskRoundModel.getDeskRoundModel();
+    this.VoteModel = VoteModel.getVoteModel();
+    this.CandidateListModel = CandidateListModel.getCandidateListModel();
+  }
+
+  async initModel() {
+    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.PartyModel.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();
+  }
+
+  async calculateResults(filter) {
+    this.CandidateListModel.refreshCandidateLists();
+    let calculator = this;
+    this.filter = filter;
+    this.deskRounds = await this.DeskRoundModel.getDeskRounds();
+    this.deskRounds = this.deskRounds.filter(function (deskRound) {
+      return deskRound.RoundID === calculator.round.ID;
+    });
+    this.candidateLists = await this.CandidateListModel.getCandidateLists();
+    this.candidateLists = this.candidateLists.filter((candidateList) => {
+      return candidateList.RoundID === calculator.round.ID;
+    });
+
+    this.stats = await this.calculateStats(this.deskRounds);
+
+    let flag = true;
+    switch (this.filter) {
+      case "partial":
+        if (this.stats.VotesExpressed === 0) {
+          this.status = "no_results";
+        } else {
+          this.roundResults = await this.calculateRoundResults();
+          this.status = "partial";
+        }
+        break;
+      case "completed":
+        flag = true;
+        this.deskRounds.forEach((deskRound) => {
+          if (!deskRound.Completed) flag = false;
+        });
+        if (flag) {
+          this.roundResults = await this.calculateRoundResults();
+          this.stats = await this.calculateStats(this.deskRounds);
+          this.status = "completed";
+        } else {
+          this.roundResults = null;
+          this.stats = null;
+          this.status = "incompleted";
+        }
+        break;
+      case "validated":
+        flag = true;
+        this.deskRounds.forEach((deskRound) => {
+          if (!deskRound.Validated) flag = false;
+        });
+        if (flag) {
+          this.roundResults = await this.calculateRoundResults();
+          this.stats = await this.calculateStats(this.deskRounds);
+          this.status = "validated";
+        } else {
+          this.roundResults = null;
+          this.stats = null;
+          this.status = "not validated";
+        }
+    }
+
+    this.areasResults = await this.calculateAreasResults();
+    console.log(this);
+  }
+
+  async calculateRoundResults() {
+    let partiesIDToKeep = [];
+    this.candidateLists.forEach((candidateList) => {
+      partiesIDToKeep.push(candidateList.PartyID);
+    });
+    partiesIDToKeep = partiesIDToKeep.filter(function (item, index) {
+      return partiesIDToKeep.indexOf(item) >= index;
+    });
+
+    let parties = await this.PartyModel.getParties();
+    parties = parties.filter((party) => partiesIDToKeep.includes(party.ID));
+    parties.forEach((party) => {
+      party.VoiceNumber = 0;
+      let currentParty = party;
+      this.candidateLists.forEach((candidateList) => {
+        if (candidateList.PartyID == currentParty.ID) {
+          currentParty.VoiceNumber = candidateList.Votes.reduce(
+            (voiceNumber, vote) => {
+              return voiceNumber + vote.VoiceNumber;
+            },
+            currentParty.VoiceNumber
+          );
+        }
+      });
+      party = currentParty;
+    });
+    parties.sort((a, b) => {
+      return b.VoiceNumber - a.VoiceNumber;
+    });
+    return parties;
+  }
+
+  async calculateAreasResults() {
+    let calculator = this;
+    this.AreaModel.refreshAreas();
+    let areas = await this.AreaModel.getAreas();
+    areas = areas.filter(function (area) {
+      return area.ElectionID == calculator.round.ElectionID;
+    });
+
+    let areasCalculated = [];
+    for (let i in areas) {
+      areasCalculated.push(await this.calculateAreaResults(areas[i]));
+    }
+    areas = areasCalculated;
+    return areas;
+  }
+
+  async calculateAreaResults(area) {
+    let deskRounds = [];
+    for (let i in area.Sections) {
+      let section = await this.SectionModel.getSection(area.Sections[i].ID);
+      for (let j in section.Desks) {
+        for (let k in this.deskRounds) {
+          if (section.Desks[j].ID == this.deskRounds[k].DeskID)
+            deskRounds.push(this.deskRounds[k]);
+        }
+      }
+    }
+
+    let candidateListToKeep = this.candidateLists.filter((candidateList) => {
+      return candidateList.AreaID === area.ID;
+    });
+
+    let sections = [];
+    for (let i in area.Sections) {
+      sections.push(
+        await this.calculateSectionResults(
+          area.Sections[i],
+          candidateListToKeep.map((a) => ({ ...a }))
+        )
+      );
+    }
+    area.Sections = sections;
+
+    area.stats = await this.calculateStats(deskRounds);
+
+    let flag = true;
+    switch (this.filter) {
+      case "partial":
+        if (area.stats.VotesExpressed === 0) {
+          area.status = "no_results";
+          return;
+        }
+        area.status = "partial";
+        break;
+      case "completed":
+        flag = true;
+        deskRounds.forEach((deskRound) => {
+          if (!deskRound.Completed) flag = false;
+        });
+        if (flag) {
+          area.status = "completed";
+        } else {
+          area.status = "incompleted";
+          return;
+        }
+        break;
+      case "validated":
+        flag = true;
+        deskRounds.forEach((deskRound) => {
+          if (!deskRound.Validated) flag = false;
+        });
+        if (flag) {
+          area.status = "validated";
+        } else {
+          area.status = "not validated";
+          return;
+        }
+    }
+
+    candidateListToKeep.forEach((candidateList) => {
+      candidateList.VoiceNumber = candidateList.Votes.reduce(
+        (voiceNumber, vote) => {
+          return voiceNumber + vote.VoiceNumber;
+        },
+        0
+      );
+      candidateList.Percentage =
+        (candidateList.VoiceNumber / area.stats.VotesExpressed) * 100;
+    });
+    area.candidateLists = candidateListToKeep;
+    area.candidateLists.sort((a, b) => {
+      return b.VoiceNumber - a.VoiceNumber;
+    });
+
+    area.Electeds = this.getElecteds(area);
+
+    area.candidateLists.sort((a, b) => {
+      return b.VoiceNumber - a.VoiceNumber;
+    });
+
+    return area;
+  }
+
+  async calculateSectionResults(section, candidateLists) {
+    section = await this.SectionModel.getSection(section.ID);
+    let deskRounds = [];
+    for (let i in section.Desks) {
+      for (let j in this.deskRounds) {
+        if (section.Desks[i].ID == this.deskRounds[j].DeskID)
+          deskRounds.push(this.deskRounds[j]);
+      }
+    }
+
+    section.stats = await this.calculateStats(deskRounds);
+    let flag = true;
+    switch (this.filter) {
+      case "partial":
+        if (section.stats.VotesExpressed === 0) {
+          section.status = "no_results";
+          return;
+        }
+        section.status = "partial";
+        break;
+      case "completed":
+        flag = true;
+        deskRounds.forEach((deskRound) => {
+          if (!deskRound.Completed) flag = false;
+        });
+        if (flag) {
+          section.status = "completed";
+        } else {
+          section.status = "incompleted";
+          return;
+        }
+        break;
+      case "validated":
+        flag = true;
+        deskRounds.forEach((deskRound) => {
+          if (!deskRound.Validated) flag = false;
+        });
+        if (flag) {
+          section.status = "validated";
+        } else {
+          section.status = "not validated";
+          return;
+        }
+    }
+
+    candidateLists.forEach((candidateList) => {
+      candidateList.Votes = candidateList.Votes.filter((vote) => {
+        return deskRounds
+          .map((deskRound) => deskRound.ID)
+          .includes(vote.DeskRoundID);
+      });
+      candidateList.VoiceNumber = candidateList.Votes.reduce(
+        (voiceNumber, vote) => {
+          return voiceNumber + vote.VoiceNumber;
+        },
+        0
+      );
+      candidateList.Percentage =
+        (candidateList.VoiceNumber / section.stats.VotesExpressed) * 100;
+    });
+    section.candidateLists = candidateLists;
+    section.candidateLists.sort((a, b) => {
+      return b.VoiceNumber - a.VoiceNumber;
+    });
+    return section;
+  }
+
+  async calculateStats(deskRounds) {
+    let subscribed = 0;
+    let blank = 0;
+    let nullVote = 0;
+    let totalVotes = 0;
+    let VotesExpressed = 0;
+
+    for (let i in deskRounds) {
+      let desk = await this.DeskModel.getDesk(deskRounds[i].DeskID);
+      subscribed += desk.Subscribed;
+      deskRounds[i].Votes.forEach((vote) => {
+        totalVotes += vote.VoiceNumber;
+        if (vote.Blank) blank += vote.VoiceNumber;
+        else if (vote.NullVote) nullVote += vote.VoiceNumber;
+        else VotesExpressed += vote.VoiceNumber;
+      });
+    }
+    return {
+      Abstention: Number(
+        ((subscribed - totalVotes) / subscribed) * 100
+      ).toFixed(2),
+      Blank: Number((blank / totalVotes) * 100).toFixed(2),
+      NullVote: Number((nullVote / totalVotes) * 100).toFixed(2),
+      VotesExpressed: VotesExpressed,
+    };
+  }
+
+  getElecteds(area) {
+    let electeds = [];
+
+    // order candidates by rank and remove refused or removed candidates
+    area.candidateLists.forEach((candidateList) => {
+      candidateList.Candidates.sort(function (a, b) {
+        return a.Rank - b.Rank;
+      });
+      for (let i = 0; i < candidateList.Candidates.length; i++) {
+        if (
+          candidateList.Candidates[i].Refused ||
+          candidateList.Candidates[i].Removed
+        ) {
+          candidateList.Candidates.splice(i, 1);
+        }
+      }
+    });
+
+    // première étape
+    let seatForFirst = parseInt(area.SeatNumber / 2);
+    if ((parseInt(area.seatNumber) / 2) % 2 != 0) seatForFirst += 1;
+    electeds = electeds.concat(
+      area.candidateLists[0].Candidates.splice(0, seatForFirst)
+    );
+
+    // deuxième étape
+    let leftSeats = area.SeatNumber - seatForFirst;
+    let electoralQuotien = area.stats.VotesExpressed / leftSeats;
+
+    area.candidateLists.forEach((candidateList) => {
+      let seatsAttributed = parseInt(
+        candidateList.VoiceNumber / electoralQuotien
+      );
+      candidateList.SeatsAttributed = seatsAttributed;
+      leftSeats -= seatsAttributed;
+      if (seatsAttributed > 0) {
+        electeds = electeds.concat(
+          candidateList.Candidates.splice(0, seatsAttributed)
+        );
+      }
+    });
+
+    // //troisème étape
+    var day = new Date();
+    while (leftSeats > 0) {
+      area.candidateLists.forEach((candidateList) => {
+        candidateList.Average =
+          candidateList.VoiceNumber /
+          (parseInt(candidateList.SeatsAttributed) + 1);
+      });
+      area.candidateLists.sort(function (a, b) {
+        return b.Average - a.Average;
+      });
+      if (area.candidateLists[0].Average === area.candidateLists[1].Average) {
+        if (area.candidateLists[1].vote > area.candidateLists[0].vote)
+          [area.candidateLists[0], area.candidateLists[1]] = [
+            area.candidateLists[1],
+            area.candidateLists[0],
+          ];
+        if (area.candidateLists[0].vote == area.candidateLists[1].vote) {
+          if (
+            area.candidateLists[0].Candidates[0].Birthdate == "" ||
+            area.candidateLists[1].Candidates[0].Birthdate == ""
+          ) {
+            area.errorAgeAverage = true;
+          }
+          ageCandidate1 = ageCount(
+            day,
+            new Date(area.candidateLists[0].Candidates[0].Birthdate)
+          );
+          ageCandidate2 = ageCount(
+            day,
+            new Date(area.candidateLists[1].Candidates[0].Birthdate)
+          );
+          if (ageCandidate2 > ageCandidate1) {
+            [area.candidateLists[0], area.candidateLists[1]] = [
+              area.candidateLists[1],
+              area.candidateLists[0],
+            ];
+          }
+        }
+      }
+      electeds = electeds.concat(
+        area.candidateLists[0].Candidates.splice(0, 1)
+      );
+      area.candidateLists[0].SeatsAttributed += 1;
+      leftSeats -= 1;
+    }
+
+    return electeds;
+  }
+}
-- 
GitLab