From 63ac71cf9ae35d96801a50d2ef444fc9b7597eee Mon Sep 17 00:00:00 2001
From: "Peter J. Holzer"
Date: Sun, 6 Oct 2024 13:37:30 +0200
Subject: [PATCH 1/2] Specify preference on a 0..100 scale
---
app.py | 269 +++++++++++----------------
meeat.procrusql | 7 +-
static/style.css | 34 +++-
templates/date_result_fragment.html | 10 +-
templates/date_result_matrix.html | 47 +++++
templates/date_vote_form.html | 10 +
templates/date_vote_fragment.html | 16 +-
templates/place_result_fragment.html | 14 +-
templates/place_result_matrix.html | 47 +++++
templates/place_vote_form.html | 10 +
templates/place_vote_fragment.html | 16 +-
templates/time_result_fragment.html | 10 +-
templates/time_result_matrix.html | 47 +++++
templates/time_vote_form.html | 10 +
templates/time_vote_fragment.html | 16 +-
templates/vote.html | 56 ++----
16 files changed, 354 insertions(+), 265 deletions(-)
create mode 100644 templates/date_result_matrix.html
create mode 100644 templates/date_vote_form.html
create mode 100644 templates/place_result_matrix.html
create mode 100644 templates/place_vote_form.html
create mode 100644 templates/time_result_matrix.html
create mode 100644 templates/time_vote_form.html
diff --git a/app.py b/app.py
index 3f1e083..7595d3c 100644
--- a/app.py
+++ b/app.py
@@ -1,8 +1,10 @@
+import collections
import datetime
import email.message
import logging
import logging.config
import os
+import re
import smtplib
import psycopg
@@ -110,70 +112,61 @@ def vote(key):
csr.execute(
"""
- select d.id, d.date, d.display, position
+ select d.id, d.date, d.display, preference
from date d left join date_vote v on d.id = v.date and v.bod = %s
- where meet = %s order by position, d.date
+ where meet = %s order by d.date
""",
(session["user"]["id"], meet.id,))
dates = csr.fetchall()
csr.execute(
"""
- select d.id, d.time, d.display, position
+ select d.id, d.time, d.display, preference
from time d left join time_vote v on d.id = v.time and v.bod = %s
- where meet = %s order by position, d.time
+ where meet = %s order by d.time
""",
(session["user"]["id"], meet.id,))
times = csr.fetchall()
csr.execute(
"""
- select d.id, d.name, position
+ select d.id, d.name, preference
from place d left join place_vote v on d.id = v.place and v.bod = %s
- where meet = %s order by position, d.name
+ where meet = %s order by d.name
""",
(session["user"]["id"], meet.id,))
places = csr.fetchall()
return render_template("vote.html",
- meet=meet, voters=voters,dates=dates, times=times, places=places)
+ meet=meet, voters=voters, dates=dates, times=times, places=places)
@app.post("/vote/date")
def vote_date():
log.debug("form = %s", request.form)
- date_ids = request.form.getlist("date")
+ meet_id, preferences = get_preferences("date")
+ date_ids = list(preferences.keys())
csr = get_cursor()
- # Retrieve the meet.id from the date ids. This also ensures that all the
- # dates refer to the same meet.
- csr.execute("select distinct meet from date where id = any (%s)",
- (date_ids,))
- r = csr.fetchall()
- if len(r) != 1:
- # this should never happen. Is the client messing around?
- log.warning("Date ids %s map to meets %s", date_ids, r)
- abort(400)
- meet_id = r[0].meet
-
csr.execute(
"delete from date_vote where date = any (%s) and bod = %s",
(date_ids, session["user"]["id"]))
- for pos, date_id in enumerate(date_ids):
+ for date_id, pref in preferences.items():
csr.execute(
- "insert into date_vote(date, bod, position) values(%s, %s, %s)",
- (date_id, session["user"]["id"], pos)
+ "insert into date_vote(date, bod, preference) values(%s, %s, %s)",
+ (date_id, session["user"]["id"], pref)
)
+
# XXX - (almost) duplicate
csr.execute(
"""
- select d.id, d.date, d.display, position
+ select d.id, d.date, d.display, preference
from date d left join date_vote v on d.id = v.date and v.bod = %s
- where meet = %s order by position, d.date
+ where meet = %s order by d.date
""",
(session["user"]["id"], meet_id,))
dates = csr.fetchall()
- result = instantrunoff_backward(meet_id, "date")
+ result = simple_sum(meet_id, "date")
log.debug("result = %s", result)
csr.execute(
@@ -194,7 +187,7 @@ def vote_date():
@app.get("/result//date")
def result_date(meet_id):
- result = instantrunoff_backward(meet_id, "date")
+ result = simple_sum(meet_id, "date")
log.debug("result = %s", result)
csr = get_cursor()
@@ -219,39 +212,30 @@ def result_date(meet_id):
@app.post("/vote/time")
def vote_time():
log.debug("form = %s", request.form)
- time_ids = request.form.getlist("time")
+ meet_id, preferences = get_preferences("time")
+ time_ids = list(preferences.keys())
csr = get_cursor()
- # Retrieve the meet.id from the time ids. This also ensures that all the
- # times refer to the same meet.
- csr.execute("select distinct meet from time where id = any (%s)",
- (time_ids,))
- r = csr.fetchall()
- if len(r) != 1:
- # this should never happen. Is the client messing around?
- log.warning("Time ids %s map to meets %s", time_ids, r)
- abort(400)
- meet_id = r[0].meet
-
csr.execute(
"delete from time_vote where time = any (%s) and bod = %s",
(time_ids, session["user"]["id"]))
- for pos, time_id in enumerate(time_ids):
+ for time_id, pref in preferences.items():
csr.execute(
- "insert into time_vote(time, bod, position) values(%s, %s, %s)",
- (time_id, session["user"]["id"], pos)
+ "insert into time_vote(time, bod, preference) values(%s, %s, %s)",
+ (time_id, session["user"]["id"], pref)
)
+
# XXX - (almost) duplicate
csr.execute(
"""
- select d.id, d.time, d.display, position
+ select d.id, d.time, d.display, preference
from time d left join time_vote v on d.id = v.time and v.bod = %s
- where meet = %s order by position, d.time
+ where meet = %s order by d.time
""",
(session["user"]["id"], meet_id,))
times = csr.fetchall()
- result = instantrunoff_backward(meet_id, "time")
+ result = simple_sum(meet_id, "time")
log.debug("result = %s", result)
csr.execute(
@@ -272,7 +256,7 @@ def vote_time():
@app.get("/result//time")
def result_time(meet_id):
- result = instantrunoff_backward(meet_id, "time")
+ result = simple_sum(meet_id, "time")
log.debug("result = %s", result)
csr = get_cursor()
@@ -295,39 +279,30 @@ def result_time(meet_id):
@app.post("/vote/place")
def vote_place():
log.debug("form = %s", request.form)
- place_ids = request.form.getlist("place")
+ meet_id, preferences = get_preferences("place")
+ place_ids = list(preferences.keys())
csr = get_cursor()
- # Retrieve the meet.id from the place ids. This also ensures that all the
- # places refer to the same meet.
- csr.execute("select distinct meet from place where id = any (%s)",
- (place_ids,))
- r = csr.fetchall()
- if len(r) != 1:
- # this should never happen. Is the client messing around?
- log.warning("Place ids %s map to meets %s", place_ids, r)
- abort(400)
- meet_id = r[0].meet
-
csr.execute(
"delete from place_vote where place = any (%s) and bod = %s",
(place_ids, session["user"]["id"]))
- for pos, place_id in enumerate(place_ids):
+ for place_id, pref in preferences.items():
csr.execute(
- "insert into place_vote(place, bod, position) values(%s, %s, %s)",
- (place_id, session["user"]["id"], pos)
+ "insert into place_vote(place, bod, preference) values(%s, %s, %s)",
+ (place_id, session["user"]["id"], pref)
)
+
# XXX - (almost) duplicate
csr.execute(
"""
- select d.id, d.name, position
+ select d.id, d.name, preference
from place d left join place_vote v on d.id = v.place and v.bod = %s
- where meet = %s order by position, d.name
+ where meet = %s order by d.name
""",
(session["user"]["id"], meet_id,))
places = csr.fetchall()
- result = instantrunoff_backward(meet_id, "place")
+ result = simple_sum(meet_id, "place")
log.debug("result = %s", result)
csr.execute(
@@ -349,7 +324,7 @@ def vote_place():
@app.get("/result//place")
def result_place(meet_id):
- result = instantrunoff_backward(meet_id, "place")
+ result = simple_sum(meet_id, "place")
log.debug("result = %s", result)
csr = get_cursor()
@@ -370,32 +345,35 @@ def result_place(meet_id):
result=result, voters=voters)
-@app.get("/result//date/ballot")
-def result_ballots_date(meet_id):
- ballots = get_ballots(meet_id, "date")
- result = instantrunoff_backward(meet_id, "date")
+@app.get("/result//date/matrix")
+def result_matrix_date(meet_id):
+ candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "date")
return render_template(
- "date_result_ballots.html",
- result=result, ballots=ballots)
+ "date_result_matrix.html",
+ candidates=candidates, voters=voters, preference=preference,
+ candidate_sum=candidate_sum, voter_max=voter_max
+ )
-@app.get("/result//time/ballot")
-def result_ballots_time(meet_id):
- ballots = get_ballots(meet_id, "time")
- result = instantrunoff_backward(meet_id, "time")
+@app.get("/result//time/matrix")
+def result_matrix_time(meet_id):
+ candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "time")
return render_template(
- "time_result_ballots.html",
- result=result, ballots=ballots)
+ "time_result_matrix.html",
+ candidates=candidates, voters=voters, preference=preference,
+ candidate_sum=candidate_sum, voter_max=voter_max
+ )
-@app.get("/result//place/ballot")
-def result_ballots_place(meet_id):
- ballots = get_ballots(meet_id, "place")
- result = instantrunoff_backward(meet_id, "place")
+@app.get("/result//place/matrix")
+def result_matrix_place(meet_id):
+ candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "place")
return render_template(
- "place_result_ballots.html",
- result=result, ballots=ballots)
+ "place_result_matrix.html",
+ candidates=candidates, voters=voters, preference=preference,
+ candidate_sum=candidate_sum, voter_max=voter_max
+ )
def send_mail(email_address, confirmation_url):
@@ -408,96 +386,69 @@ def send_mail(email_address, confirmation_url):
mta = smtplib.SMTP(host="localhost")
mta.send_message(msg)
-def get_ballots(meet_id, kind):
+def get_matrix(meet_id, kind):
csr = get_cursor()
q = f"""
- select {kind}.*, bod, position, email,
- min(w) over(partition by bod order by position) as vote_w
+ select {kind}.*, preference, email, short
from {kind}
join {kind}_vote on {kind}.id = {kind}_vote.{kind}
join bod on bod = bod.id
where meet = %s
- order by bod, position
+ order by preference desc
"""
csr.execute(q, (meet_id,))
-
- last_bod = None
- ballots = []
- for r in csr:
- if r.bod != last_bod:
- ballot = []
- ballots.append(ballot)
- last_bod = r.bod
- ballot.append(r)
- return ballots
-
-def dump_ballots(ballots):
- for ballot in ballots:
- log.debug ("---")
- for r in ballot:
- log.debug(r)
-
-def runoff(ballots, direction):
- count = {}
candidates = {}
- for ballot in ballots:
- for r in ballot:
- if r.id not in count or len(count[r.id]) < len(ballot):
- log.debug("count[%d] <- %d elements", r.id, len(ballot))
- count[r.id] = [0] * len(ballot)
- candidates[r.id] = r
- for ballot in ballots:
- weight = max(r.vote_w for r in ballot)
- for pos, r in enumerate(ballot):
- log.debug("count = %s", count)
- log.debug("r.id = %s", r.id)
- log.debug("pos = %s", pos)
- count[r.id][pos] += weight
- log.debug("count[%d][%d]) = %d", r.id, pos, count[r.id][pos])
- if direction == "backward":
- result = sorted(count.keys(), key=lambda i: list(reversed(count[i])), reverse=True)
- else:
- result = sorted(count.keys(), key=lambda i: count[i])
- log.debug("result of this round:")
- for r in result:
- log.debug("%s %s", r, count[r])
- log.debug("striking %d", result[0])
- loser = candidates[result[0]]
- new_ballots = [
- [
- r for r in ballot if r.id != loser.id
- ] for ballot in ballots
- ]
- return loser, new_ballots
+ voters = {}
+ voter_max = collections.defaultdict(float)
+ candidate_sum = collections.defaultdict(float)
+ preference = collections.defaultdict(dict)
+ for r in csr:
+ candidates[r.id] = r
+ voters[r.email] = r
+ preference[r.id][r.email] = r.preference
+ if r.preference > voter_max[r.email]:
+ voter_max[r.email] = r.preference
+ candidate_sum[r.id] += r.preference
+ app.logger.debug(f"{candidates=}")
+ return candidates, voters, preference, candidate_sum, voter_max
-def instantrunoff_forward(meet_id, kind):
- ballots = get_ballots(meet_id, kind)
+def simple_sum(meet_id, kind):
+ q = \
+ f"""
+ select k.*, sum(preference) as preference
+ from {kind} k join {kind}_vote v on k.id = v.{kind}
+ where k.meet = %s
+ group by k.id order by preference desc
+ """
+ csr = get_cursor()
+ csr.execute(q, (meet_id,))
+ return csr.fetchall()
- result = []
- while max(len(b) for b in ballots):
- dump_ballots(ballots)
- loser, ballots = runoff(ballots, "forward")
- result.append(loser)
- result = list(reversed(result))
- log.debug("final result")
- for r in result:
- log.debug(r)
- return result
+def get_preferences(kind):
+ preferences = {}
+ for k, v in request.form.items():
+ if m := re.match(kind + r"_(\d+)", k):
+ candidate = int(m.group(1))
+ preference = float(v)
+ if not (0 <= preference <= 100):
+ # this should never happen. Is the client messing around?
+ log.warning(f"Preference {preference} not in range [0, 100]")
+ abort(400)
+ preferences[candidate] = preference
-def instantrunoff_backward(meet_id, kind):
- ballots = get_ballots(meet_id, kind)
-
- result = []
- while max(len(b) for b in ballots):
- dump_ballots(ballots)
- loser, ballots = runoff(ballots, "backward")
- result.append(loser)
- result = list(reversed(result))
- log.debug("final result")
- for r in result:
- log.debug(r)
- return result
+ csr = get_cursor()
+ # Retrieve the meet.id from the kind ids. This also ensures that all
+ # refer to the same meet.
+ csr.execute(f"select distinct meet from {kind} where id = any (%s)",
+ (list(preferences.keys()),))
+ r = csr.fetchall()
+ if len(r) != 1:
+ # this should never happen. Is the client messing around?
+ log.warning(f"{kind} ids {list(preferences.keys())} map to meets {r}")
+ abort(400)
+ meet_id = r[0].meet
+ return meet_id, preferences
def get_cursor():
db = get_db()
diff --git a/meeat.procrusql b/meeat.procrusql
index c2c7f8b..e0f9670 100644
--- a/meeat.procrusql
+++ b/meeat.procrusql
@@ -23,20 +23,21 @@ column place meet int references meet not null
table bod
column bod id serial primary key
column bod email text not null unique
+column bod short text
column bod key text unique
column bod keychange timestamptz default now()
table date_vote
column date_vote date int not null references date
column date_vote bod int not null references bod
-column date_vote position int not null
+column date_vote preference float4 not null
table time_vote
column time_vote time int not null references time
column time_vote bod int not null references bod
-column time_vote position int not null
+column time_vote preference float4 not null
table place_vote
column place_vote place int not null references place
column place_vote bod int not null references bod
-column place_vote position int not null
+column place_vote preference float4 not null
diff --git a/static/style.css b/static/style.css
index a53cc93..a08ae3f 100644
--- a/static/style.css
+++ b/static/style.css
@@ -51,12 +51,18 @@ h2 {
}
.result-item {
- padding: 1em;
- border: #CCC 1px solid;
- border-radius: 0.2em;
color: #888;
}
+.result-item:first-child {
+ color: #484;
+ font-size: 1.5rem;
+}
+
+li.result-item::marker {
+ font-size: 1.0rem;
+}
+
.ballot {
border: 1px solid black;
margin: 1em;
@@ -126,3 +132,25 @@ h2 {
opacity: 100%
}
}
+
+.vote-item {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-block: 1rem;
+}
+
+label {
+ font-size: 1.5rem;
+}
+.emoji {
+ font-size: 2rem;
+}
+
+table.matrix th {
+ text-align: right;
+}
+
+table.matrix td {
+ text-align: right;
+}
diff --git a/templates/date_result_fragment.html b/templates/date_result_fragment.html
index 73514ef..c7d0565 100644
--- a/templates/date_result_fragment.html
+++ b/templates/date_result_fragment.html
@@ -1,8 +1,10 @@
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
+
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
+
{% for r in result %}
-
- {{ loop.index }}. {{ r.display or r.date }}
-
+
+ {{ r.display or r.date }} ({{r.preference}})
+
{% endfor %}
+
diff --git a/templates/date_result_matrix.html b/templates/date_result_matrix.html
new file mode 100644
index 0000000..8ac79c6
--- /dev/null
+++ b/templates/date_result_matrix.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for c in candidates.values() %}
+
+ {{ c.display or c.date }}
+
+ {% endfor %}
+
+ {% for v in voters.values() %}
+
+
+ {{ v.short or v.email }}
+
+ {% for c in candidates.values() %}
+
+ {{ preference[c.id][v.email] }}
+
+ {% endfor %}
+
+ {{ voter_max[v.email] }}
+
+
+ {% endfor %}
+
+
+ {% for c in candidates.values() %}
+
+ {{ candidate_sum[c.id] }}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/templates/date_vote_form.html b/templates/date_vote_form.html
new file mode 100644
index 0000000..36a4792
--- /dev/null
+++ b/templates/date_vote_form.html
@@ -0,0 +1,10 @@
+{% for d in dates %}
+
+
{{ d.display or d.date }}
+
+ 😞
+
+ 😀
+
+
+{% endfor %}
diff --git a/templates/date_vote_fragment.html b/templates/date_vote_fragment.html
index d4645f1..c292afb 100644
--- a/templates/date_vote_fragment.html
+++ b/templates/date_vote_fragment.html
@@ -1,14 +1,2 @@
- {% for d in dates %}
-
- {{ loop.index }}. {{ d.display or d.date }}
-
-
- {% endfor %}
-
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
- {% for r in result %}
-
- {{ loop.index }}. {{ r.display or r.date }}
-
- {% endfor %}
-
+{% include "date_vote_form.html" %}
+{% include "date_result_fragment.html" %}
diff --git a/templates/place_result_fragment.html b/templates/place_result_fragment.html
index 4b8823e..195a933 100644
--- a/templates/place_result_fragment.html
+++ b/templates/place_result_fragment.html
@@ -1,8 +1,10 @@
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
- {% for r in result %}
-
- {{ loop.index }}. {{ r.name }}
-
- {% endfor %}
+
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
+
+ {% for r in result %}
+
+ {{ r.name }} ({{r.preference}})
+
+ {% endfor %}
+
diff --git a/templates/place_result_matrix.html b/templates/place_result_matrix.html
new file mode 100644
index 0000000..a6f8786
--- /dev/null
+++ b/templates/place_result_matrix.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for c in candidates.values() %}
+
+ {{ c.name }}
+
+ {% endfor %}
+
+ {% for v in voters.values() %}
+
+
+ {{ v.short or v.email }}
+
+ {% for c in candidates.values() %}
+
+ {{ preference[c.id][v.email] }}
+
+ {% endfor %}
+
+ {{ voter_max[v.email] }}
+
+
+ {% endfor %}
+
+
+ {% for c in candidates.values() %}
+
+ {{ candidate_sum[c.id] }}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/templates/place_vote_form.html b/templates/place_vote_form.html
new file mode 100644
index 0000000..412a417
--- /dev/null
+++ b/templates/place_vote_form.html
@@ -0,0 +1,10 @@
+{% for d in places %}
+
+
{{ d.name }}
+
+ 😞
+
+ 😀
+
+
+{% endfor %}
diff --git a/templates/place_vote_fragment.html b/templates/place_vote_fragment.html
index 7d15936..899949e 100644
--- a/templates/place_vote_fragment.html
+++ b/templates/place_vote_fragment.html
@@ -1,14 +1,2 @@
- {% for d in places %}
-
- {{ loop.index }}. {{ d.name }}
-
-
- {% endfor %}
-
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
- {% for r in result %}
-
- {{ loop.index }}. {{ r.name }}
-
- {% endfor %}
-
+{% include "place_vote_form.html" %}
+{% include "place_result_fragment.html" %}
diff --git a/templates/time_result_fragment.html b/templates/time_result_fragment.html
index 35368ff..a638f4e 100644
--- a/templates/time_result_fragment.html
+++ b/templates/time_result_fragment.html
@@ -1,8 +1,10 @@
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
+
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
+
{% for r in result %}
-
- {{ loop.index }}. {{ r.display or r.time }}
-
+
+ {{ r.display or r.time }} ({{r.preference}})
+
{% endfor %}
+
diff --git a/templates/time_result_matrix.html b/templates/time_result_matrix.html
new file mode 100644
index 0000000..e6417d0
--- /dev/null
+++ b/templates/time_result_matrix.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for c in candidates.values() %}
+
+ {{ c.display or c.time }}
+
+ {% endfor %}
+
+ {% for v in voters.values() %}
+
+
+ {{ v.short or v.email }}
+
+ {% for c in candidates.values() %}
+
+ {{ preference[c.id][v.email] }}
+
+ {% endfor %}
+
+ {{ voter_max[v.email] }}
+
+
+ {% endfor %}
+
+
+ {% for c in candidates.values() %}
+
+ {{ candidate_sum[c.id] }}
+
+ {% endfor %}
+
+
+
+
+
+
diff --git a/templates/time_vote_form.html b/templates/time_vote_form.html
new file mode 100644
index 0000000..747f16c
--- /dev/null
+++ b/templates/time_vote_form.html
@@ -0,0 +1,10 @@
+{% for d in times %}
+
+
{{ d.display or d.time }}
+
+ 😞
+
+ 😀
+
+
+{% endfor %}
diff --git a/templates/time_vote_fragment.html b/templates/time_vote_fragment.html
index 6e35d9c..576fc44 100644
--- a/templates/time_vote_fragment.html
+++ b/templates/time_vote_fragment.html
@@ -1,14 +1,2 @@
- {% for d in times %}
-
- {{ loop.index }}. {{ d.display or d.time }}
-
-
- {% endfor %}
-
- Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
- {% for r in result %}
-
- {{ loop.index }}. {{ r.display or r.time }}
-
- {% endfor %}
-
+{% include "time_vote_form.html" %}
+{% include "time_result_fragment.html" %}
diff --git a/templates/vote.html b/templates/vote.html
index 4cf509f..cf41793 100644
--- a/templates/vote.html
+++ b/templates/vote.html
@@ -4,7 +4,6 @@
-
@@ -21,74 +20,43 @@
{{ voter.email }}{% if not loop.last %},{% endif %}
{% endfor %}
+
- Ordne die Optionen nach Präferenz:
-
+
- Ordne die Optionen nach Präferenz:
-
+
- Ordne die Optionen nach Präferenz:
-
-