From 5931b7c5cdb682992197dad8620405856f706ae6 Mon Sep 17 00:00:00 2001
From: Alexis POYEN <apoyen@grandlyon.com>
Date: Thu, 30 Jul 2020 14:17:32 +0200
Subject: [PATCH] Resolve "Select maps for elections"

---
 README.md                                   |  4 ++
 docker-compose.yml                          |  8 ++-
 internal/models/election.go                 |  2 +
 internal/models/models.go                   | 35 +++++++---
 internal/rootmux/admin_test.go              |  8 +--
 internal/rootmux/capturer_test.go           |  8 +--
 internal/rootmux/rootmux_test.go            |  2 +-
 internal/rootmux/visualizer_test.go         |  8 +--
 web/components/management/election.js       | 77 +++++++++++++++++++--
 web/components/management/round-desks.js    |  2 +-
 web/components/visualization/results-map.js | 17 ++++-
 web/components/vote/vote-page.js            |  2 +-
 web/services/model/election-model.js        | 19 +++--
 web/style.css                               |  2 +-
 14 files changed, 156 insertions(+), 38 deletions(-)

diff --git a/README.md b/README.md
index 5823fdf..e904069 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,10 @@ La démo est accessible avec l'url https://elections.127.0.0.1.nip.io
 - `./miscellaneous/keycloack` contient un environnement Keycloack qui peut être utilisé pour déployer un environnement OAuth2
 - `./web` est le répertoire où est stocké l'application front-end en JavaScript natif et avec le framework CSS Bulma publié par le serveur back-end.
 
+Le répertoire `./web/assets/maps` contient les cartes qui peuvent être utilisé dans l'application. Pour en ajouter de nouvelles en production copier les fichiers désirés dans ce répertoire.
+
+Avec docker-compose : docker cp <fichier-à-importer> <nom-du-conteneur>:/app/web/assets/maps/<fichier-à-importer>
+
 ### Utilisateurs et droits
 **Utilisateurs techniques**
 Les utilisateurs techniques permettent de s'authentifier à l'application et d'accéder aux API en fonction du rôle de l'utilisateur qui définit alors ses droits.
diff --git a/docker-compose.yml b/docker-compose.yml
index 15fb484..c2be700 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,19 +4,20 @@ services:
   elections-container:
     image: elections
     build: .
-    #command: -debug
+    command: -debug
     restart: unless-stopped
     volumes:
       - /etc/localtime:/etc/localtime:ro
       - ./configs:/app/configs
       - ./letsencrypt_cache:/app/letsencrypt_cache
       - ./data:/app/data
+      - maps-volume:/app/web/assets/maps
     ports:
       - 443:443
       - 80:80
     environment:
       - HOSTNAME=${HOSTNAME}
-      - ADMIN_ROLE=${ADMIN_ROLE}
+      - ADMIN_ROLE=${ADMIN_GROUP}
       - REDIRECT_URL=${REDIRECT_URL}
       - CLIENT_ID=${CLIENT_ID}
       - CLIENT_SECRET=${CLIENT_SECRET}
@@ -24,3 +25,6 @@ services:
       - TOKEN_URL=${TOKEN_URL}
       - USERINFO_URL=${USERINFO_URL}
       - LOGOUT_URL=${LOGOUT_URL}
+
+volumes:
+  maps-volume:
diff --git a/internal/models/election.go b/internal/models/election.go
index 01f72a3..dfc9a5a 100644
--- a/internal/models/election.go
+++ b/internal/models/election.go
@@ -104,6 +104,8 @@ func (d *DataHandler) putElection(w http.ResponseWriter, r *http.Request, id int
 	}
 	o.Name = election.Name
 	o.BallotType = election.BallotType
+	o.MapAreaFile = election.MapAreaFile
+	o.MapSectionFile = election.MapSectionFile
 	d.db.Save(&o)
 	json.NewEncoder(w).Encode(o)
 
diff --git a/internal/models/models.go b/internal/models/models.go
index 5ae35b1..f389279 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -1,7 +1,10 @@
 package models
 
 import (
+	"encoding/json"
+	"fmt"
 	"net/http"
+	"path/filepath"
 	"strings"
 	"time"
 
@@ -46,14 +49,16 @@ const ErrorValidatedVote = "Error the vote have already been validated and can't
 
 // Election represent an election divided in areas with 1 or several rounds
 type Election struct {
-	ID         uint       `gorm:"primary_key"`
-	CreatedAt  time.Time  `json:"-"`
-	UpdatedAt  time.Time  `json:"-"`
-	DeletedAt  *time.Time `json:"-"`
-	Name       string
-	BallotType string
-	Areas      []Area
-	Rounds     []Round
+	ID             uint       `gorm:"primary_key"`
+	CreatedAt      time.Time  `json:"-"`
+	UpdatedAt      time.Time  `json:"-"`
+	DeletedAt      *time.Time `json:"-"`
+	Name           string
+	BallotType     string
+	MapAreaFile    string
+	MapSectionFile string
+	Areas          []Area
+	Rounds         []Round
 }
 
 // Area represent an area of an election divided in one or several Sections
@@ -251,6 +256,8 @@ func (d *DataHandler) ProcessAPI(w http.ResponseWriter, r *http.Request) {
 		d.handleCandidate(w, r)
 	case "Vote":
 		d.handleVote(w, r)
+	case "Maps":
+		d.handleMaps(w, r)
 	}
 
 }
@@ -270,3 +277,15 @@ func (d *DataHandler) getLoggedUser(w http.ResponseWriter, r *http.Request) inte
 
 	return nil
 }
+
+func (d *DataHandler) handleMaps(w http.ResponseWriter, r *http.Request) {
+	switch method := r.Method; method {
+	case "GET":
+		matches, _ := filepath.Glob("web/assets/maps/*")
+		fmt.Println(matches)
+		json.NewEncoder(w).Encode(matches)
+	default:
+		http.Error(w, "method not allowed", 400)
+	}
+
+}
diff --git a/internal/rootmux/admin_test.go b/internal/rootmux/admin_test.go
index 76c5914..9e2ae85 100644
--- a/internal/rootmux/admin_test.go
+++ b/internal/rootmux/admin_test.go
@@ -34,13 +34,13 @@ func AdminTests(t *testing.T) {
 		do("DELETE", "/api/Capturer/3", xsrfHeader, ``, 200, ``)
 
 		// Create an Election
-		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct"}`, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":null,"Rounds":null}`)
+		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":null,"Rounds":null}`)
 		// Get the election
-		do("GET", "/api/Election/1", xsrfHeader, ``, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[],"Rounds":null}`)
+		do("GET", "/api/Election/1", xsrfHeader, ``, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[],"Rounds":null}`)
 		// Get all the elections
-		do("GET", "/api/Election/", xsrfHeader, ``, 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[],"Rounds":null}]`)
+		do("GET", "/api/Election/", xsrfHeader, ``, 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[],"Rounds":null}]`)
 		// Update an election
-		do("PUT", "/api/Election/1", xsrfHeader, `{"ID":1,"Name":"Grand-Lyon 2020", "BallotType":"metropolitan-direct"}`, 200, `{"ID":1,"Name":"Grand-Lyon 2020","BallotType":"metropolitan-direct","Areas":[],"Rounds":null}`)
+		do("PUT", "/api/Election/1", xsrfHeader, `{"ID":1,"Name":"Grand-Lyon 2020", "BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 200, `{"ID":1,"Name":"Grand-Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[],"Rounds":null}`)
 
 		// Create an Area
 		do("POST", "/api/Area", xsrfHeader, `{"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1"}`, 200, `{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}`)
diff --git a/internal/rootmux/capturer_test.go b/internal/rootmux/capturer_test.go
index 7a13671..335d887 100644
--- a/internal/rootmux/capturer_test.go
+++ b/internal/rootmux/capturer_test.go
@@ -36,13 +36,13 @@ func CapturerTests(t *testing.T) {
 		do("DELETE", "/api/Capturer/1", xsrfHeader, ``, 405, `You're not authorize to execute this method on this ressource.`)
 
 		// Create an election should fail with 405
-		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct"}`, 405, `You're not authorize to execute this method on this ressource.`)
+		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 405, `You're not authorize to execute this method on this ressource.`)
 		// Get an Election
-		do("GET", "/api/Election/1", xsrfHeader, "", 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}`)
+		do("GET", "/api/Election/1", xsrfHeader, "", 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}`)
 		// Get all the elections
-		do("GET", "/api/Election/", xsrfHeader, "", 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}]`)
+		do("GET", "/api/Election/", xsrfHeader, "", 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}]`)
 		// Update an election should fail with 405
-		do("PUT", "/api/Election/1", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct"}`, 405, `You're not authorize to execute this method on this ressource.`)
+		do("PUT", "/api/Election/1", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 405, `You're not authorize to execute this method on this ressource.`)
 		// Delete an election should fail with 405
 		do("DELETE", "/api/Election/1", xsrfHeader, ``, 405, `You're not authorize to execute this method on this ressource.`)
 
diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go
index e1b47c0..19e36dd 100644
--- a/internal/rootmux/rootmux_test.go
+++ b/internal/rootmux/rootmux_test.go
@@ -292,7 +292,7 @@ func resetDataWithData(t *testing.T) {
 		do("POST", "/api/Capturer", xsrfHeader, `{"UserID":2,"Name":"Capturer"}`, 200, `{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}`)
 		do("POST", "/api/Capturer", xsrfHeader, `{"UserID":3,"Name":"Capturer"}`, 200, `{"ID":2,"UserID":3,"Name":"Capturer","DeskRounds":null}`)
 
-		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct"}`, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":null,"Rounds":null}`)
+		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":null,"Rounds":null}`)
 		do("POST", "/api/Area", xsrfHeader, `{"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1"}`, 200, `{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}`)
 		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}`)
diff --git a/internal/rootmux/visualizer_test.go b/internal/rootmux/visualizer_test.go
index 48ab80d..77c1c57 100644
--- a/internal/rootmux/visualizer_test.go
+++ b/internal/rootmux/visualizer_test.go
@@ -34,13 +34,13 @@ func VisualizerTests(t *testing.T) {
 		do("DELETE", "/api/Capturer/1", xsrfHeader, ``, 405, `You're not authorize to execute this method on this ressource.`)
 
 		// Create an election should fail with 405
-		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct"}`, 405, `You're not authorize to execute this method on this ressource.`)
+		do("POST", "/api/Election", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 405, `You're not authorize to execute this method on this ressource.`)
 		// Get an Election
-		do("GET", "/api/Election/1", xsrfHeader, "", 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}`)
+		do("GET", "/api/Election/1", xsrfHeader, "", 200, `{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}`)
 		// Get all the elections
-		do("GET", "/api/Election/", xsrfHeader, "", 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}]`)
+		do("GET", "/api/Election/", xsrfHeader, "", 200, `[{"ID":1,"Name":"Grand Lyon 2020","BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json","Areas":[{"ID":1,"ElectionID":1,"Name":"Area 1","SeatNumber":9,"MapID":"1","Sections":null}],"Rounds":null}]`)
 		// Update an election should fail with 405
-		do("PUT", "/api/Election/1", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct"}`, 405, `You're not authorize to execute this method on this ressource.`)
+		do("PUT", "/api/Election/1", xsrfHeader, `{"Name":"Grand Lyon 2020", "BallotType":"metropolitan-direct","MapAreaFile":"web/assets/maps/area.json","MapSectionFile":"web/assets/maps/section.json"}`, 405, `You're not authorize to execute this method on this ressource.`)
 		// Delete an election should fail with 405
 		do("DELETE", "/api/Election/1", xsrfHeader, ``, 405, `You're not authorize to execute this method on this ressource.`)
 
diff --git a/web/components/management/election.js b/web/components/management/election.js
index c316b22..773f8df 100644
--- a/web/components/management/election.js
+++ b/web/components/management/election.js
@@ -121,6 +121,24 @@ class Election {
               </select>
             </div>
           </div>
+          <div class="field">
+            <label>Carte de circonscriptions</label><br />
+            <div class="control select">
+              <select
+                name="map-area-selector"
+                id="election-modal-area-map"
+              ></select>
+            </div>
+          </div>
+          <div class="field">
+            <label>Carte de villes</label><br />
+            <div class="control select">
+              <select
+                name="map-section-selector"
+                id="election-modal-section-map"
+              ></select>
+            </div>
+          </div>
         </section>
         <footer class="modal-card-foot">
           <button id="election-modal-save" class="button is-success">
@@ -302,16 +320,18 @@ class Election {
       .classList.add("active-card");
   }
 
-  newElection() {
+  async newElection() {
     this.method = "POST";
+    await this.refreshMapSelect();
     document.getElementById("election-modal-id").value = null;
     document.getElementById("election-modal-name").value = null;
     document.getElementById("election-modal-ballot-type").value = null;
     Common.toggleModal("election-modal", "election-modal-card");
   }
 
-  editElection(election) {
+  async editElection(election) {
     this.method = "PUT";
+    await this.refreshMapSelect();
     document.getElementById("election-modal-id").value = election.ID;
     document.getElementById("election-modal-name").value = election.Name;
     document.getElementById("election-modal-ballot-type").value =
@@ -328,7 +348,9 @@ class Election {
       this.method,
       parseInt(document.getElementById("election-modal-id").value),
       document.getElementById("election-modal-name").value,
-      document.getElementById("election-modal-ballot-type").value
+      document.getElementById("election-modal-ballot-type").value,
+      document.getElementById("election-modal-area-map").value,
+      document.getElementById("election-modal-section-map").value
     );
 
     await this.displayElections();
@@ -346,7 +368,6 @@ class Election {
     this.parent.sectionHandler.emptySections();
     this.parent.deskHandler.emptyDesks();
     document.getElementById("areas").style.display = "none";
-
   }
 
   cloneElection(election) {
@@ -378,4 +399,52 @@ class Election {
     this.parent.areaHandler.election = electionCloned;
     await this.parent.areaHandler.displayAreas();
   }
+
+  async refreshMapSelect() {
+    let selectMapAreas = document.getElementById("election-modal-area-map");
+    let selectMapSections = document.getElementById(
+      "election-modal-section-map"
+    );
+    let maps;
+    try {
+      const response = await fetch("/api/Maps/", {
+        method: "GET",
+        headers: new Headers({
+          "XSRF-Token": this.ElectionModel.current_user.xsrftoken,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `Maps could not be fetched (status ${response.status})`
+        );
+      }
+      maps = await response.json();
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+    }
+
+    this.addMapsInSelect(maps, selectMapAreas);
+    this.addMapsInSelect(maps, selectMapSections);
+  }
+
+  addMapsInSelect(maps, select) {
+    for (let i = select.options.length - 1; i >= 0; i--) {
+      select.remove(i);
+    }
+
+    let el = document.createElement("option");
+    el.textContent = "Veuillez sélectionner une carte";
+    el.value = 0;
+    select.appendChild(el);
+    maps.forEach((map) => {
+      el = document.createElement("option");
+      el.textContent = map.substring(
+        map.lastIndexOf("/") + 1,
+        map.lastIndexOf(".")
+      );
+      el.value = map;
+      select.appendChild(el);
+    });
+  }
 }
diff --git a/web/components/management/round-desks.js b/web/components/management/round-desks.js
index 5780b8a..d9ccedc 100644
--- a/web/components/management/round-desks.js
+++ b/web/components/management/round-desks.js
@@ -48,7 +48,7 @@ class RoundDesk {
           <div id="desk-round-details"></div>
         </div>
         <div class="column is-half">
-          <div id="vote-section" class="card"></div>
+          <div id="vote-section" class="card-no-hover"></div>
         </div>
       </div>
     `;
diff --git a/web/components/visualization/results-map.js b/web/components/visualization/results-map.js
index 28e7276..3fdb314 100644
--- a/web/components/visualization/results-map.js
+++ b/web/components/visualization/results-map.js
@@ -1,6 +1,7 @@
 // Imports
 import * as PartyModel from "/services/model/party-model.js";
 import * as AreaModel from "/services/model/area-model.js";
+import * as ElectionModel from "/services/model/election-model.js";
 
 export async function mount(parent) {
   const mapComponent = new MapComponent(parent);
@@ -12,14 +13,24 @@ class MapComponent {
     this.parent = parent;
     this.PartyModel = PartyModel.getPartyModel();
     this.AreaModel = AreaModel.getAreaModel();
+    this.ElectionModel = ElectionModel.getElectionModel();
   }
 
   async displayMapAreas() {
-    await this.initMap("/assets/maps/area.json", this.colorAreas);
+    let election = await this.ElectionModel.getElection(
+      this.parent.parent.round.ElectionID
+    );
+    await this.initMap(
+      election.MapAreaFile.replace("web/", ""),
+      this.colorAreas
+    );
   }
 
   async displayMapSections() {
-    await this.initMap("/assets/maps/section.json", this.colorSections);
+    let election = await this.ElectionModel.getElection(
+      this.parent.parent.round.ElectionID
+    );
+    await this.initMap(election.MapSectionFile.replace("web/", ""), this.colorSections);
   }
 
   async initMap(mapFile, colorationFunction) {
@@ -42,7 +53,7 @@ class MapComponent {
       container: "map-component", // container id
       style: "/assets/mapbox/vector.json", // stylesheet location
       center: [4.9, 45.75], // starting position [lng, lat]
-      zoom: 9.7, // starting zoom
+      zoom: 9.5, // starting zoom
     });
 
     this.map.on("load", () => {
diff --git a/web/components/vote/vote-page.js b/web/components/vote/vote-page.js
index 49424f9..be780ab 100644
--- a/web/components/vote/vote-page.js
+++ b/web/components/vote/vote-page.js
@@ -16,7 +16,7 @@ class VotePage {
     const mountpoint = where;
     document.getElementById(mountpoint).innerHTML = /* HTML */ `
       <section style="margin-bottom: 230px;">
-        <div class="container"><div id="vote-section" class="card"></div></div>
+        <div class="container"><div id="vote-section" class="card-no-hover"></div></div>
       </section>
     `;
     this.deskRoundHandler = await DeskRound.mount("vote-section", this);
diff --git a/web/services/model/election-model.js b/web/services/model/election-model.js
index a69a6c2..a00127f 100644
--- a/web/services/model/election-model.js
+++ b/web/services/model/election-model.js
@@ -1,12 +1,12 @@
 import * as Messages from "/services/messages/messages.js";
 
-let electionModel
+let electionModel;
 
-export function getElectionModel(){
-  if(electionModel == null) {
+export function getElectionModel() {
+  if (electionModel == null) {
     electionModel = new ElectionModel();
   }
-  return electionModel
+  return electionModel;
 }
 
 class ElectionModel {
@@ -44,7 +44,14 @@ class ElectionModel {
     return this.elections;
   }
 
-  async saveElection(method, ID, Name, BallotType) {
+  async saveElection(
+    method,
+    ID,
+    Name,
+    BallotType,
+    MapAreaFile,
+    MapSectionFile
+  ) {
     try {
       const response = await fetch("/api/Election/" + ID, {
         method: method,
@@ -55,6 +62,8 @@ class ElectionModel {
           ID: ID,
           Name: Name,
           BallotType: BallotType,
+          MapAreaFile: MapAreaFile,
+          MapSectionFile: MapSectionFile,
         }),
       });
       if (response.status == 409) {
diff --git a/web/style.css b/web/style.css
index 449b497..90eb4eb 100644
--- a/web/style.css
+++ b/web/style.css
@@ -214,7 +214,7 @@ select {
 }
 
 #map-section {
-  height: 70vh;
+  height: 65vh;
 }
 
 #map-component {
-- 
GitLab