Merge branch 'range'
This commit is contained in:
commit
47d318782b
267
app.py
267
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/<int:meet_id>/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/<int:meet_id>/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/<int:meet_id>/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/<int:meet_id>/date/ballot")
|
||||
def result_ballots_date(meet_id):
|
||||
ballots = get_ballots(meet_id, "date")
|
||||
result = instantrunoff_backward(meet_id, "date")
|
||||
@app.get("/result/<int:meet_id>/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/<int:meet_id>/time/ballot")
|
||||
def result_ballots_time(meet_id):
|
||||
ballots = get_ballots(meet_id, "time")
|
||||
result = instantrunoff_backward(meet_id, "time")
|
||||
@app.get("/result/<int:meet_id>/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/<int:meet_id>/place/ballot")
|
||||
def result_ballots_place(meet_id):
|
||||
ballots = get_ballots(meet_id, "place")
|
||||
result = instantrunoff_backward(meet_id, "place")
|
||||
@app.get("/result/<int:meet_id>/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)
|
||||
voters = {}
|
||||
voter_max = collections.defaultdict(float)
|
||||
candidate_sum = collections.defaultdict(float)
|
||||
preference = collections.defaultdict(dict)
|
||||
for r in csr:
|
||||
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[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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<div id="r-day" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<p>Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<ol class="result-items">
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.display or r.date }}
|
||||
</div>
|
||||
<li class="result-item">
|
||||
{{ r.display or r.date }} ({{r.preference}})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/Sortable.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<table class="matrix">
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<th>
|
||||
{{ c.display or c.date }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for v in voters.values() %}
|
||||
<tr>
|
||||
<th>
|
||||
{{ v.short or v.email }}
|
||||
</th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ preference[c.id][v.email] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
{{ voter_max[v.email] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ candidate_sum[c.id] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{% for d in dates %}
|
||||
<div class="vote-item">
|
||||
<label for="date_{{d.id}}">{{ d.display or d.date }}</label>
|
||||
<div>
|
||||
<span class="emoji">😞</span>
|
||||
<input type="range" name="date_{{d.id}}" value="{{d.preference}}">
|
||||
<span class="emoji">😀</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -1,14 +1,2 @@
|
|||
{% for d in dates %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.display or d.date }}
|
||||
<input type="hidden" name="date" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="r-day" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.display or r.date }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include "date_vote_form.html" %}
|
||||
{% include "date_result_fragment.html" %}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<div id="r-place" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<p>Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<ol class="result-items">
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.name }}
|
||||
</div>
|
||||
<li class="result-item">
|
||||
{{ r.name }} ({{r.preference}})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/Sortable.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<table class="matrix">
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<th>
|
||||
{{ c.name }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for v in voters.values() %}
|
||||
<tr>
|
||||
<th>
|
||||
{{ v.short or v.email }}
|
||||
</th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ preference[c.id][v.email] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
{{ voter_max[v.email] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ candidate_sum[c.id] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{% for d in places %}
|
||||
<div class="vote-item">
|
||||
<label for="place_{{d.id}}">{{ d.name }}</label>
|
||||
<div>
|
||||
<span class="emoji">😞</span>
|
||||
<input type="range" name="place_{{d.id}}" value="{{d.preference}}">
|
||||
<span class="emoji">😀</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -1,14 +1,2 @@
|
|||
{% for d in places %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.name }}
|
||||
<input type="hidden" name="place" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="r-place" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.name }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include "place_vote_form.html" %}
|
||||
{% include "place_result_fragment.html" %}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<div id="r-time" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<p>Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
<ol class="result-items">
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.display or r.time }}
|
||||
</div>
|
||||
<li class="result-item">
|
||||
{{ r.display or r.time }} ({{r.preference}})
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/Sortable.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<table class="matrix">
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<th>
|
||||
{{ c.display or c.time }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for v in voters.values() %}
|
||||
<tr>
|
||||
<th>
|
||||
{{ v.short or v.email }}
|
||||
</th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ preference[c.id][v.email] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
<td>
|
||||
{{ voter_max[v.email] }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for c in candidates.values() %}
|
||||
<td>
|
||||
{{ candidate_sum[c.id] }}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{% for d in times %}
|
||||
<div class="vote-item">
|
||||
<label for="time_{{d.id}}">{{ d.display or d.time }}</label>
|
||||
<div>
|
||||
<span class="emoji">😞</span>
|
||||
<input type="range" name="time_{{d.id}}" value="{{d.preference}}">
|
||||
<span class="emoji">😀</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
|
@ -1,14 +1,2 @@
|
|||
{% for d in times %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.display or d.time }}
|
||||
<input type="hidden" name="time" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="r-time" hx-swap-oob="true">
|
||||
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
|
||||
{% for r in result %}
|
||||
<div class="result-item">
|
||||
{{ loop.index }}. {{ r.display or r.time }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include "time_vote_form.html" %}
|
||||
{% include "time_result_fragment.html" %}
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title> MEEAT ⋄ {{ meet.title }} </title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script src="/static/Sortable.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -21,74 +21,43 @@
|
|||
{{ voter.email }}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
|
||||
<h2 id="h-day">
|
||||
An welchem Tag?
|
||||
<a href="/result/{{meet.id}}/date/ballot">[details]</a>
|
||||
<a href="/result/{{meet.id}}/date/matrix">[details]</a>
|
||||
</h2>
|
||||
<div>
|
||||
Ordne die Optionen nach Präferenz:
|
||||
<form id="v-day" class="sortable" hx-post="/vote/date" hx-trigger="end">
|
||||
{% for d in dates %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.display or d.date }}
|
||||
<input type="hidden" name="date" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form id="v-day" hx-post="/vote/date" hx-trigger="change">
|
||||
{% include "date_vote_form.html" %}
|
||||
</form>
|
||||
</div>
|
||||
<div id="r-day" hx-get="/result/{{meet.id}}/date" hx-trigger="load">
|
||||
</div>
|
||||
|
||||
<h2 id="h-time">
|
||||
Zu welcher Zeit?
|
||||
<a href="/result/{{meet.id}}/time/ballot">[details]</a>
|
||||
<a href="/result/{{meet.id}}/time/matrix">[details]</a>
|
||||
</h2>
|
||||
<div>
|
||||
Ordne die Optionen nach Präferenz:
|
||||
<form id="v-time" class="sortable" hx-post="/vote/time" hx-trigger="end">
|
||||
{% for d in times %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.display or d.time }}
|
||||
<input type="hidden" name="time" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form id="v-time" hx-post="/vote/time" hx-trigger="change">
|
||||
{% include "time_vote_form.html" %}
|
||||
</form>
|
||||
</div>
|
||||
<div id="r-time" hx-get="/result/{{meet.id}}/time" hx-trigger="load">
|
||||
</div>
|
||||
|
||||
<h2 id="h-place">
|
||||
An welchem Ort?
|
||||
<a href="/result/{{meet.id}}/place/ballot">[details]</a>
|
||||
<a href="/result/{{meet.id}}/place/matrix">[details]</a>
|
||||
</h2>
|
||||
<div>
|
||||
Ordne die Optionen nach Präferenz:
|
||||
<form id="v-place" class="sortable" hx-post="/vote/place" hx-trigger="end">
|
||||
{% for d in places %}
|
||||
<div class="sort-item">
|
||||
{{ loop.index }}. {{ d.name }}
|
||||
<input type="hidden" name="place" value="{{d.id}}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
<form id="v-place" hx-post="/vote/place" hx-trigger="change">
|
||||
{% include "place_vote_form.html" %}
|
||||
</form>
|
||||
</div>
|
||||
<div id="r-place" hx-get="/result/{{meet.id}}/place" hx-trigger="load">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script>
|
||||
function activateSortables(element) {
|
||||
var sortables = document.querySelectorAll(".sortable");
|
||||
for (const sortable of sortables) {
|
||||
console.debug("making", sortable, "sortable")
|
||||
new Sortable(sortable, {
|
||||
animation: 150,
|
||||
ghostClass: 'blue-background-class'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
activateSortables(document)
|
||||
//htmx.onLoad(function(content) { activateSortables(content) }
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
||||
|
|
Loading…
Reference in New Issue