Compare commits

..

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

16 changed files with 265 additions and 355 deletions

269
app.py
View File

@ -1,10 +1,8 @@
import collections
import datetime
import email.message
import logging
import logging.config
import os
import re
import smtplib
import psycopg
@ -112,61 +110,70 @@ def vote(key):
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
where meet = %s order by d.date
where meet = %s order by position, d.date
""",
(session["user"]["id"], meet.id,))
dates = csr.fetchall()
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
where meet = %s order by d.time
where meet = %s order by position, d.time
""",
(session["user"]["id"], meet.id,))
times = csr.fetchall()
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
where meet = %s order by d.name
where meet = %s order by position, 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)
meet_id, preferences = get_preferences("date")
date_ids = list(preferences.keys())
date_ids = request.form.getlist("date")
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 date_id, pref in preferences.items():
for pos, date_id in enumerate(date_ids):
csr.execute(
"insert into date_vote(date, bod, preference) values(%s, %s, %s)",
(date_id, session["user"]["id"], pref)
"insert into date_vote(date, bod, position) values(%s, %s, %s)",
(date_id, session["user"]["id"], pos)
)
# XXX - (almost) duplicate
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
where meet = %s order by d.date
where meet = %s order by position, d.date
""",
(session["user"]["id"], meet_id,))
dates = csr.fetchall()
result = simple_sum(meet_id, "date")
result = instantrunoff_backward(meet_id, "date")
log.debug("result = %s", result)
csr.execute(
@ -187,7 +194,7 @@ def vote_date():
@app.get("/result/<int:meet_id>/date")
def result_date(meet_id):
result = simple_sum(meet_id, "date")
result = instantrunoff_backward(meet_id, "date")
log.debug("result = %s", result)
csr = get_cursor()
@ -212,30 +219,39 @@ def result_date(meet_id):
@app.post("/vote/time")
def vote_time():
log.debug("form = %s", request.form)
meet_id, preferences = get_preferences("time")
time_ids = list(preferences.keys())
time_ids = request.form.getlist("time")
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 time_id, pref in preferences.items():
for pos, time_id in enumerate(time_ids):
csr.execute(
"insert into time_vote(time, bod, preference) values(%s, %s, %s)",
(time_id, session["user"]["id"], pref)
"insert into time_vote(time, bod, position) values(%s, %s, %s)",
(time_id, session["user"]["id"], pos)
)
# XXX - (almost) duplicate
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
where meet = %s order by d.time
where meet = %s order by position, d.time
""",
(session["user"]["id"], meet_id,))
times = csr.fetchall()
result = simple_sum(meet_id, "time")
result = instantrunoff_backward(meet_id, "time")
log.debug("result = %s", result)
csr.execute(
@ -256,7 +272,7 @@ def vote_time():
@app.get("/result/<int:meet_id>/time")
def result_time(meet_id):
result = simple_sum(meet_id, "time")
result = instantrunoff_backward(meet_id, "time")
log.debug("result = %s", result)
csr = get_cursor()
@ -279,30 +295,39 @@ def result_time(meet_id):
@app.post("/vote/place")
def vote_place():
log.debug("form = %s", request.form)
meet_id, preferences = get_preferences("place")
place_ids = list(preferences.keys())
place_ids = request.form.getlist("place")
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 place_id, pref in preferences.items():
for pos, place_id in enumerate(place_ids):
csr.execute(
"insert into place_vote(place, bod, preference) values(%s, %s, %s)",
(place_id, session["user"]["id"], pref)
"insert into place_vote(place, bod, position) values(%s, %s, %s)",
(place_id, session["user"]["id"], pos)
)
# XXX - (almost) duplicate
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
where meet = %s order by d.name
where meet = %s order by position, d.name
""",
(session["user"]["id"], meet_id,))
places = csr.fetchall()
result = simple_sum(meet_id, "place")
result = instantrunoff_backward(meet_id, "place")
log.debug("result = %s", result)
csr.execute(
@ -324,7 +349,7 @@ def vote_place():
@app.get("/result/<int:meet_id>/place")
def result_place(meet_id):
result = simple_sum(meet_id, "place")
result = instantrunoff_backward(meet_id, "place")
log.debug("result = %s", result)
csr = get_cursor()
@ -345,35 +370,32 @@ def result_place(meet_id):
result=result, voters=voters)
@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")
@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")
return render_template(
"date_result_matrix.html",
candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
"date_result_ballots.html",
result=result, ballots=ballots)
@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")
@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")
return render_template(
"time_result_matrix.html",
candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
"time_result_ballots.html",
result=result, ballots=ballots)
@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")
@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")
return render_template(
"place_result_matrix.html",
candidates=candidates, voters=voters, preference=preference,
candidate_sum=candidate_sum, voter_max=voter_max
)
"place_result_ballots.html",
result=result, ballots=ballots)
def send_mail(email_address, confirmation_url):
@ -386,69 +408,96 @@ def send_mail(email_address, confirmation_url):
mta = smtplib.SMTP(host="localhost")
mta.send_message(msg)
def get_matrix(meet_id, kind):
def get_ballots(meet_id, kind):
csr = get_cursor()
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}
join {kind}_vote on {kind}.id = {kind}_vote.{kind}
join bod on bod = bod.id
where meet = %s
order by preference desc
order by bod, position
"""
csr.execute(q, (meet_id,))
candidates = {}
voters = {}
voter_max = collections.defaultdict(float)
candidate_sum = collections.defaultdict(float)
preference = collections.defaultdict(dict)
last_bod = None
ballots = []
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
if r.bod != last_bod:
ballot = []
ballots.append(ballot)
last_bod = r.bod
ballot.append(r)
return ballots
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()
def dump_ballots(ballots):
for ballot in ballots:
log.debug ("---")
for r in ballot:
log.debug(r)
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 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
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 instantrunoff_forward(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, "forward")
result.append(loser)
result = list(reversed(result))
log.debug("final result")
for r in result:
log.debug(r)
return result
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
def get_cursor():
db = get_db()

View File

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

View File

@ -51,18 +51,12 @@ 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;
@ -132,25 +126,3 @@ li.result-item::marker {
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">
<p>Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
<ol class="result-items">
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
{% for r in result %}
<li class="result-item">
{{ r.display or r.date }} ({{r.preference}})
</li>
<div class="result-item">
{{ loop.index }}. {{ r.display or r.date }}
</div>
{% endfor %}
</ol>
</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" %}
{% include "date_result_fragment.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 %}
<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">
<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 %}
<li class="result-item">
{{ r.name }} ({{r.preference}})
</li>
{% endfor %}
</ol>
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,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" %}
{% include "place_result_fragment.html" %}
{% 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>

View File

@ -1,10 +1,8 @@
<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 %}):
<ol class="result-items">
Ergebnis ({% for v in voters %}{{ v.short or v.email }}{% if not loop.last %}, {% endif %}{% endfor %}):
{% for r in result %}
<li class="result-item">
{{ r.display or r.time }} ({{r.preference}})
</li>
<div class="result-item">
{{ loop.index }}. {{ r.display or r.time }}
</div>
{% endfor %}
</ol>
</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" %}
{% include "time_result_fragment.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 %}
<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>
<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,43 +21,74 @@
{{ voter.email }}{% if not loop.last %},{% endif %}
{% endfor %}
</p>
<h2 id="h-day">
An welchem Tag?
<a href="/result/{{meet.id}}/date/matrix">[details]</a>
<a href="/result/{{meet.id}}/date/ballot">[details]</a>
</h2>
<div>
<form id="v-day" hx-post="/vote/date" hx-trigger="change">
{% include "date_vote_form.html" %}
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>
</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/matrix">[details]</a>
<a href="/result/{{meet.id}}/time/ballot">[details]</a>
</h2>
<div>
<form id="v-time" hx-post="/vote/time" hx-trigger="change">
{% include "time_vote_form.html" %}
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>
</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/matrix">[details]</a>
<a href="/result/{{meet.id}}/place/ballot">[details]</a>
</h2>
<div>
<form id="v-place" hx-post="/vote/place" hx-trigger="change">
{% include "place_vote_form.html" %}
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>
</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>