Merge branch 'range'
This commit is contained in:
commit
47d318782b
269
app.py
269
app.py
|
@ -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,70 +112,61 @@ 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()
|
||||||
|
|
||||||
return render_template("vote.html",
|
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")
|
@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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
{% 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>
|
|
||||||
|
|
|
@ -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 %}):
|
||||||
{% for r in result %}
|
<ol class="result-items">
|
||||||
<div class="result-item">
|
{% for r in result %}
|
||||||
{{ loop.index }}. {{ r.name }}
|
<li class="result-item">
|
||||||
</div>
|
{{ r.name }} ({{r.preference}})
|
||||||
{% endfor %}
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
</div>
|
</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 %}
|
{% 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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
{% 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>
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue