Compare commits

..

No commits in common. "47d318782bac81fdf4d52f9d6ecbb63e5ee34747" and "b0b656dd9d3c048fc41d333fbe56b3cd01d34212" have entirely different histories.

16 changed files with 265 additions and 355 deletions

265
app.py
View File

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

View File

@ -23,21 +23,20 @@ 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 preference float4 not null column date_vote position int 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 preference float4 not null column time_vote position int 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 preference float4 not null column place_vote position int not null

View File

@ -51,18 +51,12 @@ 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;
@ -132,25 +126,3 @@ li.result-item::marker {
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,10 +1,8 @@
<div id="r-day" hx-swap-oob="true"> <div id="r-day" hx-swap-oob="true">
<p>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 %}):
<ol class="result-items">
{% for r in result %} {% for r in result %}
<li class="result-item"> <div class="result-item">
{{ r.display or r.date }} ({{r.preference}}) {{ loop.index }}. {{ r.display or r.date }}
</li> </div>
{% endfor %} {% endfor %}
</ol>
</div> </div>

View File

@ -1,47 +0,0 @@
<!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

@ -1,10 +0,0 @@
{% 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,2 +1,14 @@
{% include "date_vote_form.html" %} {% for d in dates %}
{% include "date_result_fragment.html" %} <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>

View File

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

View File

@ -1,47 +0,0 @@
<!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

@ -1,10 +0,0 @@
{% 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,2 +1,14 @@
{% include "place_vote_form.html" %} {% for d in places %}
{% include "place_result_fragment.html" %} <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>

View File

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

View File

@ -1,47 +0,0 @@
<!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

@ -1,10 +0,0 @@
{% 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,2 +1,14 @@
{% include "time_vote_form.html" %} {% for d in times %}
{% include "time_result_fragment.html" %} <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>

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,43 +21,74 @@
{{ 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/matrix">[details]</a> <a href="/result/{{meet.id}}/date/ballot">[details]</a>
</h2> </h2>
<div> <div>
<form id="v-day" hx-post="/vote/date" hx-trigger="change"> Ordne die Optionen nach Präferenz:
{% include "date_vote_form.html" %} <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> </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/matrix">[details]</a> <a href="/result/{{meet.id}}/time/ballot">[details]</a>
</h2> </h2>
<div> <div>
<form id="v-time" hx-post="/vote/time" hx-trigger="change"> Ordne die Optionen nach Präferenz:
{% include "time_vote_form.html" %} <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> </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/matrix">[details]</a> <a href="/result/{{meet.id}}/place/ballot">[details]</a>
</h2> </h2>
<div> <div>
<form id="v-place" hx-post="/vote/place" hx-trigger="change"> Ordne die Optionen nach Präferenz:
{% include "place_vote_form.html" %} <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> </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>