Merge branch 'range'

This commit is contained in:
Peter J. Holzer 2024-10-15 01:21:09 +02:00
commit 47d318782b
16 changed files with 355 additions and 265 deletions

265
app.py
View File

@ -1,8 +1,10 @@
import collections
import datetime import datetime
import email.message import email.message
import logging import logging
import logging.config import logging.config
import os import os
import re
import smtplib import smtplib
import psycopg import psycopg
@ -110,27 +112,27 @@ def vote(key):
csr.execute( 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 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,)) (session["user"]["id"], meet.id,))
dates = csr.fetchall() dates = csr.fetchall()
csr.execute( 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 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,)) (session["user"]["id"], meet.id,))
times = csr.fetchall() times = csr.fetchall()
csr.execute( 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 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,)) (session["user"]["id"], meet.id,))
places = csr.fetchall() places = csr.fetchall()
@ -141,39 +143,30 @@ def vote(key):
@app.post("/vote/date") @app.post("/vote/date")
def vote_date(): def vote_date():
log.debug("form = %s", request.form) 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() 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( csr.execute(
"delete from date_vote where date = any (%s) and bod = %s", "delete from date_vote where date = any (%s) and bod = %s",
(date_ids, session["user"]["id"])) (date_ids, session["user"]["id"]))
for pos, date_id in enumerate(date_ids): for date_id, pref in preferences.items():
csr.execute( csr.execute(
"insert into date_vote(date, bod, position) values(%s, %s, %s)", "insert into date_vote(date, bod, preference) values(%s, %s, %s)",
(date_id, session["user"]["id"], pos) (date_id, session["user"]["id"], pref)
) )
# XXX - (almost) duplicate # XXX - (almost) duplicate
csr.execute( 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 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,)) (session["user"]["id"], meet_id,))
dates = csr.fetchall() dates = csr.fetchall()
result = instantrunoff_backward(meet_id, "date") result = simple_sum(meet_id, "date")
log.debug("result = %s", result) log.debug("result = %s", result)
csr.execute( csr.execute(
@ -194,7 +187,7 @@ def vote_date():
@app.get("/result/<int:meet_id>/date") @app.get("/result/<int:meet_id>/date")
def result_date(meet_id): def result_date(meet_id):
result = instantrunoff_backward(meet_id, "date") result = simple_sum(meet_id, "date")
log.debug("result = %s", result) log.debug("result = %s", result)
csr = get_cursor() csr = get_cursor()
@ -219,39 +212,30 @@ def result_date(meet_id):
@app.post("/vote/time") @app.post("/vote/time")
def vote_time(): def vote_time():
log.debug("form = %s", request.form) 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() 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( csr.execute(
"delete from time_vote where time = any (%s) and bod = %s", "delete from time_vote where time = any (%s) and bod = %s",
(time_ids, session["user"]["id"])) (time_ids, session["user"]["id"]))
for pos, time_id in enumerate(time_ids): for time_id, pref in preferences.items():
csr.execute( csr.execute(
"insert into time_vote(time, bod, position) values(%s, %s, %s)", "insert into time_vote(time, bod, preference) values(%s, %s, %s)",
(time_id, session["user"]["id"], pos) (time_id, session["user"]["id"], pref)
) )
# XXX - (almost) duplicate # XXX - (almost) duplicate
csr.execute( 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 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,)) (session["user"]["id"], meet_id,))
times = csr.fetchall() times = csr.fetchall()
result = instantrunoff_backward(meet_id, "time") result = simple_sum(meet_id, "time")
log.debug("result = %s", result) log.debug("result = %s", result)
csr.execute( csr.execute(
@ -272,7 +256,7 @@ def vote_time():
@app.get("/result/<int:meet_id>/time") @app.get("/result/<int:meet_id>/time")
def result_time(meet_id): def result_time(meet_id):
result = instantrunoff_backward(meet_id, "time") result = simple_sum(meet_id, "time")
log.debug("result = %s", result) log.debug("result = %s", result)
csr = get_cursor() csr = get_cursor()
@ -295,39 +279,30 @@ def result_time(meet_id):
@app.post("/vote/place") @app.post("/vote/place")
def vote_place(): def vote_place():
log.debug("form = %s", request.form) 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() 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( csr.execute(
"delete from place_vote where place = any (%s) and bod = %s", "delete from place_vote where place = any (%s) and bod = %s",
(place_ids, session["user"]["id"])) (place_ids, session["user"]["id"]))
for pos, place_id in enumerate(place_ids): for place_id, pref in preferences.items():
csr.execute( csr.execute(
"insert into place_vote(place, bod, position) values(%s, %s, %s)", "insert into place_vote(place, bod, preference) values(%s, %s, %s)",
(place_id, session["user"]["id"], pos) (place_id, session["user"]["id"], pref)
) )
# XXX - (almost) duplicate # XXX - (almost) duplicate
csr.execute( 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 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,)) (session["user"]["id"], meet_id,))
places = csr.fetchall() places = csr.fetchall()
result = instantrunoff_backward(meet_id, "place") result = simple_sum(meet_id, "place")
log.debug("result = %s", result) log.debug("result = %s", result)
csr.execute( csr.execute(
@ -349,7 +324,7 @@ def vote_place():
@app.get("/result/<int:meet_id>/place") @app.get("/result/<int:meet_id>/place")
def result_place(meet_id): def result_place(meet_id):
result = instantrunoff_backward(meet_id, "place") result = simple_sum(meet_id, "place")
log.debug("result = %s", result) log.debug("result = %s", result)
csr = get_cursor() csr = get_cursor()
@ -370,32 +345,35 @@ def result_place(meet_id):
result=result, voters=voters) result=result, voters=voters)
@app.get("/result/<int:meet_id>/date/ballot") @app.get("/result/<int:meet_id>/date/matrix")
def result_ballots_date(meet_id): def result_matrix_date(meet_id):
ballots = get_ballots(meet_id, "date") candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "date")
result = instantrunoff_backward(meet_id, "date")
return render_template( return render_template(
"date_result_ballots.html", "date_result_matrix.html",
result=result, ballots=ballots) candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
@app.get("/result/<int:meet_id>/time/ballot") @app.get("/result/<int:meet_id>/time/matrix")
def result_ballots_time(meet_id): def result_matrix_time(meet_id):
ballots = get_ballots(meet_id, "time") candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "time")
result = instantrunoff_backward(meet_id, "time")
return render_template( return render_template(
"time_result_ballots.html", "time_result_matrix.html",
result=result, ballots=ballots) candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
@app.get("/result/<int:meet_id>/place/ballot") @app.get("/result/<int:meet_id>/place/matrix")
def result_ballots_place(meet_id): def result_matrix_place(meet_id):
ballots = get_ballots(meet_id, "place") candidates, voters, preference, candidate_sum, voter_max = get_matrix(meet_id, "place")
result = instantrunoff_backward(meet_id, "place")
return render_template( return render_template(
"place_result_ballots.html", "place_result_matrix.html",
result=result, ballots=ballots) candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
def send_mail(email_address, confirmation_url): def send_mail(email_address, confirmation_url):
@ -408,96 +386,69 @@ def send_mail(email_address, confirmation_url):
mta = smtplib.SMTP(host="localhost") mta = smtplib.SMTP(host="localhost")
mta.send_message(msg) mta.send_message(msg)
def get_ballots(meet_id, kind): def get_matrix(meet_id, kind):
csr = get_cursor() csr = get_cursor()
q = f""" q = f"""
select {kind}.*, bod, position, email, select {kind}.*, preference, email, short
min(w) over(partition by bod order by position) as vote_w
from {kind} from {kind}
join {kind}_vote on {kind}.id = {kind}_vote.{kind} join {kind}_vote on {kind}.id = {kind}_vote.{kind}
join bod on bod = bod.id join bod on bod = bod.id
where meet = %s where meet = %s
order by bod, position order by preference desc
""" """
csr.execute(q, (meet_id,)) 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 = {} candidates = {}
for ballot in ballots: voters = {}
for r in ballot: voter_max = collections.defaultdict(float)
if r.id not in count or len(count[r.id]) < len(ballot): candidate_sum = collections.defaultdict(float)
log.debug("count[%d] <- %d elements", r.id, len(ballot)) preference = collections.defaultdict(dict)
count[r.id] = [0] * len(ballot) for r in csr:
candidates[r.id] = r candidates[r.id] = r
for ballot in ballots: voters[r.email] = r
weight = max(r.vote_w for r in ballot) preference[r.id][r.email] = r.preference
for pos, r in enumerate(ballot): if r.preference > voter_max[r.email]:
log.debug("count = %s", count) voter_max[r.email] = r.preference
log.debug("r.id = %s", r.id) candidate_sum[r.id] += r.preference
log.debug("pos = %s", pos) app.logger.debug(f"{candidates=}")
count[r.id][pos] += weight return candidates, voters, preference, candidate_sum, voter_max
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
def instantrunoff_forward(meet_id, kind): def simple_sum(meet_id, kind):
ballots = get_ballots(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 = [] def get_preferences(kind):
while max(len(b) for b in ballots): preferences = {}
dump_ballots(ballots) for k, v in request.form.items():
loser, ballots = runoff(ballots, "forward") if m := re.match(kind + r"_(\d+)", k):
result.append(loser) candidate = int(m.group(1))
result = list(reversed(result)) preference = float(v)
log.debug("final result") if not (0 <= preference <= 100):
for r in result: # this should never happen. Is the client messing around?
log.debug(r) log.warning(f"Preference {preference} not in range [0, 100]")
return result abort(400)
preferences[candidate] = preference
def instantrunoff_backward(meet_id, kind): csr = get_cursor()
ballots = get_ballots(meet_id, kind) # Retrieve the meet.id from the kind ids. This also ensures that all
# refer to the same meet.
result = [] csr.execute(f"select distinct meet from {kind} where id = any (%s)",
while max(len(b) for b in ballots): (list(preferences.keys()),))
dump_ballots(ballots) r = csr.fetchall()
loser, ballots = runoff(ballots, "backward") if len(r) != 1:
result.append(loser) # this should never happen. Is the client messing around?
result = list(reversed(result)) log.warning(f"{kind} ids {list(preferences.keys())} map to meets {r}")
log.debug("final result") abort(400)
for r in result: meet_id = r[0].meet
log.debug(r) return meet_id, preferences
return result
def get_cursor(): def get_cursor():
db = get_db() db = get_db()

View File

@ -23,20 +23,21 @@ column place meet int references meet not null
table bod table bod
column bod id serial primary key column bod id serial primary key
column bod email text not null unique column bod email text not null unique
column bod short text
column bod key text unique column bod key text unique
column bod keychange timestamptz default now() column bod keychange timestamptz default now()
table date_vote table date_vote
column date_vote date int not null references date column date_vote date int not null references date
column date_vote bod int not null references bod 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 table time_vote
column time_vote time int not null references time column time_vote time int not null references time
column time_vote bod int not null references bod 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 table place_vote
column place_vote place int not null references place column place_vote place int not null references place
column place_vote bod int not null references bod column place_vote bod int not null references bod
column place_vote position int not null column place_vote preference float4 not null

View File

@ -51,12 +51,18 @@ h2 {
} }
.result-item { .result-item {
padding: 1em;
border: #CCC 1px solid;
border-radius: 0.2em;
color: #888; color: #888;
} }
.result-item:first-child {
color: #484;
font-size: 1.5rem;
}
li.result-item::marker {
font-size: 1.0rem;
}
.ballot { .ballot {
border: 1px solid black; border: 1px solid black;
margin: 1em; margin: 1em;
@ -126,3 +132,25 @@ h2 {
opacity: 100% 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;
}

View File

@ -1,8 +1,10 @@
<div id="r-day" hx-swap-oob="true"> <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 %} {% for r in result %}
<div class="result-item"> <li class="result-item">
{{ loop.index }}. {{ r.display or r.date }} {{ r.display or r.date }} ({{r.preference}})
</div> </li>
{% endfor %} {% endfor %}
</ol>
</div> </div>

View File

@ -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>

View File

@ -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 %}

View File

@ -1,14 +1,2 @@
{% for d in dates %} {% include "date_vote_form.html" %}
<div class="sort-item"> {% include "date_result_fragment.html" %}
{{ 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>

View File

@ -1,8 +1,10 @@
<div id="r-place" hx-swap-oob="true"> <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 %} {% for r in result %}
<div class="result-item"> <li class="result-item">
{{ loop.index }}. {{ r.name }} {{ r.name }} ({{r.preference}})
</div> </li>
{% endfor %} {% endfor %}
</ol>
</div> </div>

View File

@ -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>

View File

@ -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 %}

View File

@ -1,14 +1,2 @@
{% for d in places %} {% include "place_vote_form.html" %}
<div class="sort-item"> {% include "place_result_fragment.html" %}
{{ 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>

View File

@ -1,8 +1,10 @@
<div id="r-time" hx-swap-oob="true"> <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 %} {% for r in result %}
<div class="result-item"> <li class="result-item">
{{ loop.index }}. {{ r.display or r.time }} {{ r.display or r.time }} ({{r.preference}})
</div> </li>
{% endfor %} {% endfor %}
</ol>
</div> </div>

View File

@ -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>

View File

@ -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 %}

View File

@ -1,14 +1,2 @@
{% for d in times %} {% include "time_vote_form.html" %}
<div class="sort-item"> {% include "time_result_fragment.html" %}
{{ 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>

View File

@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <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/htmx.min.js"></script>
<script src="/static/Sortable.min.js"></script>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
</head> </head>
<body> <body>
@ -21,74 +21,43 @@
{{ voter.email }}{% if not loop.last %},{% endif %} {{ voter.email }}{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
</p> </p>
<h2 id="h-day"> <h2 id="h-day">
An welchem Tag? An welchem Tag?
<a href="/result/{{meet.id}}/date/ballot">[details]</a> <a href="/result/{{meet.id}}/date/matrix">[details]</a>
</h2> </h2>
<div> <div>
Ordne die Optionen nach Präferenz: <form id="v-day" hx-post="/vote/date" hx-trigger="change">
<form id="v-day" class="sortable" hx-post="/vote/date" hx-trigger="end"> {% include "date_vote_form.html" %}
{% 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> </form>
</div> </div>
<div id="r-day" hx-get="/result/{{meet.id}}/date" hx-trigger="load"> <div id="r-day" hx-get="/result/{{meet.id}}/date" hx-trigger="load">
</div> </div>
<h2 id="h-time"> <h2 id="h-time">
Zu welcher Zeit? Zu welcher Zeit?
<a href="/result/{{meet.id}}/time/ballot">[details]</a> <a href="/result/{{meet.id}}/time/matrix">[details]</a>
</h2> </h2>
<div> <div>
Ordne die Optionen nach Präferenz: <form id="v-time" hx-post="/vote/time" hx-trigger="change">
<form id="v-time" class="sortable" hx-post="/vote/time" hx-trigger="end"> {% include "time_vote_form.html" %}
{% 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> </form>
</div> </div>
<div id="r-time" hx-get="/result/{{meet.id}}/time" hx-trigger="load"> <div id="r-time" hx-get="/result/{{meet.id}}/time" hx-trigger="load">
</div> </div>
<h2 id="h-place"> <h2 id="h-place">
An welchem Ort? An welchem Ort?
<a href="/result/{{meet.id}}/place/ballot">[details]</a> <a href="/result/{{meet.id}}/place/matrix">[details]</a>
</h2> </h2>
<div> <div>
Ordne die Optionen nach Präferenz: <form id="v-place" hx-post="/vote/place" hx-trigger="change">
<form id="v-place" class="sortable" hx-post="/vote/place" hx-trigger="end"> {% include "place_vote_form.html" %}
{% for d in places %}
<div class="sort-item">
{{ loop.index }}. {{ d.name }}
<input type="hidden" name="place" value="{{d.id}}">
</div>
{% endfor %}
</form> </form>
</div> </div>
<div id="r-place" hx-get="/result/{{meet.id}}/place" hx-trigger="load"> <div id="r-place" hx-get="/result/{{meet.id}}/place" hx-trigger="load">
</div> </div>
</div> </div>
</body> </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> </html>