Implement basic voting
This commit is contained in:
commit
65a19eb756
|
@ -0,0 +1,20 @@
|
||||||
|
Login (if necessary)
|
||||||
|
User is redirected to a form, enters email, mail is sent. User clicks on
|
||||||
|
confirmation link. Is authenticated and redirected to original URL
|
||||||
|
|
||||||
|
List of Votes
|
||||||
|
Do we need a meet-bod table or just show votes that the bod has
|
||||||
|
participated in? Go for the latter right now. We can send out
|
||||||
|
invites to meets
|
||||||
|
|
||||||
|
A vote
|
||||||
|
shows dates, times and places (in your preferential order if you
|
||||||
|
already voted). Plus current tally. Plus add button for date, time,
|
||||||
|
place
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
reorder
|
||||||
|
add
|
||||||
|
|
||||||
|
That's it for now? I can add a new vote and close a vote in the db.
|
||||||
|
Or add a simple page for that.
|
|
@ -0,0 +1,247 @@
|
||||||
|
import datetime
|
||||||
|
import email.message
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
import psycopg
|
||||||
|
import psycopg.rows
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Flask, session, redirect, url_for, request, render_template,
|
||||||
|
g, abort
|
||||||
|
)
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s %(lineno)d | %(message)s", level=logging.DEBUG)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.secret_key = config.secret_key
|
||||||
|
app.config["PERMANENT_SESSION_LIFETIME"] = datetime.timedelta(days=92)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
log.debug("in home")
|
||||||
|
log.debug("session = %s", session)
|
||||||
|
if "user" not in session:
|
||||||
|
return redirect(url_for('register'))
|
||||||
|
return render_template("home.html")
|
||||||
|
|
||||||
|
@app.route("/register", methods=["GET", "POST"])
|
||||||
|
def register():
|
||||||
|
log.debug("in register")
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template("register.html")
|
||||||
|
email_address = request.form.get("email", "")
|
||||||
|
if "@" not in email_address:
|
||||||
|
flash("Das schaut nicht wie eine E-Mail-Adresse aus")
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("select * from bod where email = %s", (email_address,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
if r:
|
||||||
|
if r.key:
|
||||||
|
key = r.key
|
||||||
|
else:
|
||||||
|
key = os.urandom(8).hex()
|
||||||
|
csr.execute(
|
||||||
|
"update bod set key = %s, keychange = now() where email = %s",
|
||||||
|
(key, email_address,))
|
||||||
|
else:
|
||||||
|
key = os.urandom(8).hex()
|
||||||
|
csr.execute(
|
||||||
|
"insert into bod(email, key, keychange) values(%s, %s, now())",
|
||||||
|
(email_address, key,))
|
||||||
|
confirmation_url = url_for("confirm",
|
||||||
|
target=request.form["target"],
|
||||||
|
key=key)
|
||||||
|
send_mail(email_address, confirmation_url)
|
||||||
|
return render_template("wait_for_confirmation.html")
|
||||||
|
|
||||||
|
@app.route("/confirm")
|
||||||
|
def confirm():
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("select * from bod where key = %s", (request.args["key"],))
|
||||||
|
bod = csr.fetchone()
|
||||||
|
if not bod:
|
||||||
|
return render_template("wrong_key.html")
|
||||||
|
session["user"] = { "id": bod.id, "email": bod.email}
|
||||||
|
return redirect(request.args["target"])
|
||||||
|
|
||||||
|
@app.route("/vote/<string:key>")
|
||||||
|
def vote(key):
|
||||||
|
log.debug("session = %s", session)
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("select * from meet where key = %s", (key,))
|
||||||
|
meet = csr.fetchone()
|
||||||
|
if not meet:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
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 position, d.date
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet.id,))
|
||||||
|
dates = csr.fetchall()
|
||||||
|
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
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 position, d.time
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet.id,))
|
||||||
|
times = csr.fetchall()
|
||||||
|
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
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 position, d.name
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet.id,))
|
||||||
|
places = csr.fetchall()
|
||||||
|
|
||||||
|
return render_template("vote.html",
|
||||||
|
meet=meet, dates=dates, times=times, places=places)
|
||||||
|
|
||||||
|
@app.post("/vote/date")
|
||||||
|
def vote_date():
|
||||||
|
log.debug("form = %s", request.form)
|
||||||
|
date_ids = request.form.getlist("date")
|
||||||
|
|
||||||
|
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)", (date_ids,))
|
||||||
|
for pos, date_id in enumerate(date_ids):
|
||||||
|
csr.execute(
|
||||||
|
"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, position
|
||||||
|
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
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet_id,))
|
||||||
|
dates = csr.fetchall()
|
||||||
|
return render_template("date_vote_fragment.html", dates=dates)
|
||||||
|
|
||||||
|
@app.post("/vote/time")
|
||||||
|
def vote_time():
|
||||||
|
log.debug("form = %s", request.form)
|
||||||
|
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)", (time_ids,))
|
||||||
|
for pos, time_id in enumerate(time_ids):
|
||||||
|
csr.execute(
|
||||||
|
"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, position
|
||||||
|
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
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet_id,))
|
||||||
|
times = csr.fetchall()
|
||||||
|
return render_template("time_vote_fragment.html", times=times)
|
||||||
|
|
||||||
|
@app.post("/vote/place")
|
||||||
|
def vote_place():
|
||||||
|
log.debug("form = %s", request.form)
|
||||||
|
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)", (place_ids,))
|
||||||
|
for pos, place_id in enumerate(place_ids):
|
||||||
|
csr.execute(
|
||||||
|
"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, position
|
||||||
|
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
|
||||||
|
""",
|
||||||
|
(session["user"]["id"], meet_id,))
|
||||||
|
places = csr.fetchall()
|
||||||
|
return render_template("place_vote_fragment.html", places=places)
|
||||||
|
|
||||||
|
|
||||||
|
def send_mail(email_address, confirmation_url):
|
||||||
|
msg = email.message.EmailMessage()
|
||||||
|
msg["From"] ="noreply@hjp.at"
|
||||||
|
msg["To"] = email_address
|
||||||
|
msg["Subject"] = "MEEAT confirmation"
|
||||||
|
body = confirmation_url # Really minimalistic
|
||||||
|
msg.set_content(body)
|
||||||
|
mta = smtplib.SMTP(host="localhost")
|
||||||
|
mta.send_message(msg)
|
||||||
|
|
||||||
|
def get_cursor():
|
||||||
|
db = get_db()
|
||||||
|
csr = db.cursor(row_factory=psycopg.rows.namedtuple_row)
|
||||||
|
return csr
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
if "db" not in g:
|
||||||
|
g.db = psycopg.connect(dbname=config.dbname)
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def teardown_db(exception):
|
||||||
|
log.debug("in teardown_db: exception = %s", exception)
|
||||||
|
db = g.pop('db', None)
|
||||||
|
if db is not None:
|
||||||
|
if not exception:
|
||||||
|
try:
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
db.rollback()
|
||||||
|
db.close()
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
table meet
|
||||||
|
column meet id serial primary key
|
||||||
|
column meet title text not null
|
||||||
|
column meet key text unique
|
||||||
|
|
||||||
|
table date
|
||||||
|
column date id serial primary key
|
||||||
|
column date date date not null
|
||||||
|
column date display text
|
||||||
|
column date meet int references meet not null
|
||||||
|
|
||||||
|
table time
|
||||||
|
column time id serial primary key
|
||||||
|
column time time time not null
|
||||||
|
column time display text
|
||||||
|
column time meet int references meet not null
|
||||||
|
|
||||||
|
table place
|
||||||
|
column place id serial primary key
|
||||||
|
column place name text not null
|
||||||
|
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 key text unique
|
||||||
|
column bod keychange timestamptz default now()
|
||||||
|
|
||||||
|
table date_vote
|
||||||
|
column date_vote date int not null references date
|
||||||
|
column date_vote bod int not null references bod
|
||||||
|
column date_vote position int not null
|
||||||
|
|
||||||
|
table time_vote
|
||||||
|
column time_vote time int not null references time
|
||||||
|
column time_vote bod int not null references bod
|
||||||
|
column time_vote position int not null
|
||||||
|
|
||||||
|
table place_vote
|
||||||
|
column place_vote place int not null references place
|
||||||
|
column place_vote bod int not null references bod
|
||||||
|
column place_vote position int not null
|
|
@ -0,0 +1,4 @@
|
||||||
|
flask
|
||||||
|
procrusql
|
||||||
|
psycopg
|
||||||
|
psycopg2
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 726 B |
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 8.4666665 8.4666669"
|
||||||
|
version="1.1"
|
||||||
|
id="svg5"
|
||||||
|
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||||
|
sodipodi:docname="fork-and-spoon.svg"
|
||||||
|
inkscape:export-filename="/home/hjp/wrk/meeat/static/fork-and-spoon.png"
|
||||||
|
inkscape:export-xdpi="192"
|
||||||
|
inkscape:export-ydpi="192"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview7"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showgrid="true"
|
||||||
|
units="px"
|
||||||
|
width="32px"
|
||||||
|
inkscape:zoom="32.78125"
|
||||||
|
inkscape:cx="15.984747"
|
||||||
|
inkscape:cy="14.932316"
|
||||||
|
inkscape:window-width="1802"
|
||||||
|
inkscape:window-height="1327"
|
||||||
|
inkscape:window-x="-4"
|
||||||
|
inkscape:window-y="43"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid824"
|
||||||
|
empspacing="8" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.05833334;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
|
||||||
|
d="M 2.1166666,1.0583333 V 7.4083332"
|
||||||
|
id="path960" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 0.52916667,1.0583333 -10e-9,2.1166667 3.17500004,0 V 1.0583333"
|
||||||
|
id="path1028"
|
||||||
|
sodipodi:nodetypes="cccc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.05833;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 6.35,3.175 -10e-8,4.2333332"
|
||||||
|
id="path1424"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#000000;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 5.0270833,2.1166666 c 1e-7,-0.7937499 0.5291667,-1.0583333 1.3229166,-1.0583333 0.7937501,0 1.3229168,0.2645834 1.3229167,1.0583333 C 7.6729167,2.9104167 6.8791667,3.175 6.3499999,3.175 5.55625,3.175 5.0270834,2.9104167 5.0270833,2.1166666 Z"
|
||||||
|
id="path1492"
|
||||||
|
sodipodi:nodetypes="ccccc" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,13 @@
|
||||||
|
body {
|
||||||
|
font-family: sans;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-item {
|
||||||
|
padding: 1em;
|
||||||
|
border: #CCC 1px solid;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% for d in dates %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.display or d.date }}
|
||||||
|
<input type="hidden" name="date" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Hallo, {{ session.user.email }}!</p>
|
||||||
|
<p>
|
||||||
|
{{ session }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ session.user }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ session.user.1 }}
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% for d in places %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.name }}
|
||||||
|
<input type="hidden" name="place" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Registriere dich:</p>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="email" name="email">
|
||||||
|
<input name="target" value="{{request.args.target}}">
|
||||||
|
<input type="submit">
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% for d in times %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.display or d.time }}
|
||||||
|
<input type="hidden" name="time" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
|
@ -0,0 +1,63 @@
|
||||||
|
<!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>
|
||||||
|
<p>Hallo, {{ session.user.email }}!</p>
|
||||||
|
<p>
|
||||||
|
{{ meet.title }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
An welchem Tag?
|
||||||
|
</p>
|
||||||
|
<form class="sortable" hx-post="/vote/date" hx-trigger="end">
|
||||||
|
{% for d in dates %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.display or d.date }}
|
||||||
|
<input type="hidden" name="date" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
Zu welcher Zeit?
|
||||||
|
</p>
|
||||||
|
<form class="sortable" hx-post="/vote/time" hx-trigger="end">
|
||||||
|
{% for d in times %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.display or d.time }}
|
||||||
|
<input type="hidden" name="time" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
An welchem Ort?
|
||||||
|
</p>
|
||||||
|
<form class="sortable" hx-post="/vote/place" hx-trigger="end">
|
||||||
|
{% for d in places %}
|
||||||
|
<div class="sort-item">
|
||||||
|
{{ d.name }}
|
||||||
|
<input type="hidden" name="place" value="{{d.id}}">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
htmx.onLoad(function(content) {
|
||||||
|
var sortables = document.querySelectorAll(".sortable");
|
||||||
|
for (const sortable of sortables) {
|
||||||
|
console.debug("making", sortable, "sortable")
|
||||||
|
new Sortable(sortable, {
|
||||||
|
animation: 150,
|
||||||
|
ghostClass: 'blue-background-class'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Confirmation-Mail gesendet</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Key nicht gefunden. Abgelaufen?</p>
|
||||||
|
<p><a href="{{url_for('register')}}">Zur Registrierung</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue