Implement basics
This commit is contained in:
commit
4d721ddafa
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__
|
||||||
|
config.py
|
|
@ -0,0 +1,23 @@
|
||||||
|
Wieder mal ein Anlauf
|
||||||
|
|
||||||
|
Eintragen muss ganz einfach gehen.
|
||||||
|
Mehrere Wege:
|
||||||
|
Webformular
|
||||||
|
CLI
|
||||||
|
Mobile?
|
||||||
|
|
||||||
|
Jede Seite bekommt ID (hash?)
|
||||||
|
ID ändert sich, wenn editiert wird, aber per default gibt es redirect
|
||||||
|
auf aktuelle Version, wenn man auf alte ID zugreift (es gibt einen
|
||||||
|
speziellen Permalink, der das verhindert).
|
||||||
|
|
||||||
|
Außerdem Zugriff über Tags: Wenn die Tags eindeutig sind, bekommt man
|
||||||
|
die eine Seite, sonst eine Liste von Seiten (und weiteren Tags)
|
||||||
|
|
||||||
|
Ich hätte außerdem gerne eindeutige (HTML-)Ids für jeden Absatz. Aber
|
||||||
|
das schlägt sich mit dem oben geschilderten dynamischen Verhalten, d das
|
||||||
|
Fragment nicht an den Server geschickt wird und daher nicht in einem
|
||||||
|
Redirekt erhalten werden kann.
|
||||||
|
|
||||||
|
Oder man kann einen Bereich markieren und bekommt dann einen Link auf
|
||||||
|
diesen Bereich? Feature-Creep-Alert!
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import psycopg
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--author", default="hjp")
|
||||||
|
ap.add_argument("--content_type", default="text/plain")
|
||||||
|
ap.add_argument("--tags", nargs="+")
|
||||||
|
g = ap.add_mutually_exclusive_group(required=True)
|
||||||
|
g.add_argument("--content")
|
||||||
|
g.add_argument("--url")
|
||||||
|
g.add_argument("--file")
|
||||||
|
g.add_argument("--editor", action="store_true")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.content:
|
||||||
|
content = args.content
|
||||||
|
elif args.file:
|
||||||
|
with open(args.file) as fh:
|
||||||
|
content = fh.read()
|
||||||
|
elif args.url:
|
||||||
|
r = requests.get(args.url)
|
||||||
|
if r.status_code != 200:
|
||||||
|
print(f"GETting {args.url} failed with {r.status_code} {r.message}",
|
||||||
|
file=sys.stderr)
|
||||||
|
exit(1)
|
||||||
|
soup = BeautifulSoup(r.content, "lxml")
|
||||||
|
title = soup.find("title")
|
||||||
|
title = title.text
|
||||||
|
content = f"[{title}]({args.url})"
|
||||||
|
args.content_type = "text/markdown"
|
||||||
|
elif args.editor:
|
||||||
|
with tempfile.NamedTemporaryFile(delete_on_close=False) as tfh:
|
||||||
|
tfh.close()
|
||||||
|
subprocess.run(["sensible-editor", tfh.name])
|
||||||
|
with open(tfh.name, "r") as fh:
|
||||||
|
content = fh.read()
|
||||||
|
if len(content) == 0:
|
||||||
|
print("Content is empty. Aborting.", file=sys.stderr)
|
||||||
|
|
||||||
|
conn = psycopg.connect(dbname="zettel")
|
||||||
|
csr = conn.cursor(row_factory=psycopg.rows.namedtuple_row)
|
||||||
|
csr.execute("select * from role where name = %s", (args.author,))
|
||||||
|
author_id = csr.fetchone().id
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
insert into post(text, ts, content_type, author) values(%s, now(), %s, %s)
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(content, args.content_type, author_id,))
|
||||||
|
post_id = csr.fetchone().id
|
||||||
|
for tag in args.tags:
|
||||||
|
csr.execute("select * from tag where lower(name) = lower(%s)",
|
||||||
|
(tag,)) # XXX - use citext?
|
||||||
|
r = csr.fetchone()
|
||||||
|
if not r:
|
||||||
|
csr.execute("insert into tag(name) values(%s) returning id",
|
||||||
|
(tag,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
tag_id = r.id
|
||||||
|
csr.execute("insert into tag_post(tag, post) values(%s, %s)",
|
||||||
|
(tag_id, post_id,))
|
||||||
|
csr.execute("insert into acl(post, role) values(%s, %s)", (post_id, author_id,))
|
||||||
|
conn.commit()
|
|
@ -0,0 +1,219 @@
|
||||||
|
import base64
|
||||||
|
import email.message
|
||||||
|
import os
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
from flask import Flask, render_template, g, url_for, session, request
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(config)
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
# HTTP endpoints
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def index():
|
||||||
|
return show_posts("")
|
||||||
|
|
||||||
|
@app.get("/<path:p>")
|
||||||
|
def show_posts(p):
|
||||||
|
roles = get_roles()
|
||||||
|
app.logger.info(f"{p}")
|
||||||
|
tags = p.split("/")
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("select * from tag where lower(name) = any(%s)", (tags,))
|
||||||
|
tag_ids = [r.id for r in csr]
|
||||||
|
# XXX - Check that tag_ids has the same length as tags.
|
||||||
|
# Otherwise
|
||||||
|
# - return 404?
|
||||||
|
# - redirect?
|
||||||
|
q = "select distinct p.* from post p"
|
||||||
|
for tag_id in tag_ids:
|
||||||
|
# we got tag_id from the database so we know it's an int
|
||||||
|
q += f" join tag_post t{tag_id} on p.id = t{tag_id}.post and t{tag_id}.tag = {tag_id}"
|
||||||
|
# and the user_id is an int, too
|
||||||
|
q += f" join acl on p.id = acl.post and acl.role = any(%s)"
|
||||||
|
q += " order by p.ts desc"
|
||||||
|
csr.execute(q, (roles,))
|
||||||
|
posts = csr.fetchall()
|
||||||
|
# XXX find all tags for each post
|
||||||
|
post_ids = [p.id for p in posts]
|
||||||
|
csr.execute(
|
||||||
|
"select * from tag_post tp join tag t on tp.tag = t.id where post = any(%s) order by name",
|
||||||
|
(post_ids,))
|
||||||
|
tags_by_id = {}
|
||||||
|
tags_by_post = {}
|
||||||
|
for r in csr:
|
||||||
|
if r.id not in tags_by_id:
|
||||||
|
selected = r.id in tag_ids
|
||||||
|
if selected:
|
||||||
|
# XXX - should really use id
|
||||||
|
new_tags = [t for t in tags if t != r.name.lower()]
|
||||||
|
else:
|
||||||
|
new_tags = tags + [r.name.lower()]
|
||||||
|
link = url_for("show_posts", p="/".join(new_tags))
|
||||||
|
tags_by_id[r.id] = { "id": r.id, "name": r.name, "selected": selected, "link": link }
|
||||||
|
tags_by_post.setdefault(r.post, []).append(tags_by_id[r.id])
|
||||||
|
tags = tags_by_id.values()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"posts.html",
|
||||||
|
posts=posts, tags=tags, tags_by_post=tags_by_post,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# XXX - do we need a prefix for paths?
|
||||||
|
# Or use a separate domain for the api (no)
|
||||||
|
# Or use an impossible prefix for the api (what would that be?)
|
||||||
|
#@get("/api")
|
||||||
|
|
||||||
|
#@get("/@/add_post")
|
||||||
|
#@post("/@/add_post")
|
||||||
|
# also json
|
||||||
|
|
||||||
|
#@get("/@/tag/<tag_name_or_id>")
|
||||||
|
# plus one or more posts for operations, like editing implies, bulk
|
||||||
|
# updates, etc.
|
||||||
|
|
||||||
|
@app.get("/@/login")
|
||||||
|
def login_form():
|
||||||
|
# could duplicate as registration
|
||||||
|
return render_template("login_form.html")
|
||||||
|
|
||||||
|
@app.post("/@/login")
|
||||||
|
def login_step1():
|
||||||
|
csr = get_cursor()
|
||||||
|
email = request.form["email"].strip()
|
||||||
|
csr.execute("select * from role where email = %s", (email,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
if not r:
|
||||||
|
csr.execute("insert into role(email) values(%s) returning *", (email,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
user_id = r.id
|
||||||
|
key = base64.urlsafe_b64encode(os.urandom(15)).decode('us-ascii')
|
||||||
|
csr.execute("insert into confirm_token(role, token) values(%s, %s) returning *", (user_id, key,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
csr.connection.commit()
|
||||||
|
send_mail(email, request.root_url + url_for("login_confirm", key=key))
|
||||||
|
return render_template("mail_sent.html")
|
||||||
|
|
||||||
|
@app.get("/@/confirm")
|
||||||
|
def login_confirm():
|
||||||
|
# XXX - Check that key isn't expired
|
||||||
|
return render_template("login_confirm.html")
|
||||||
|
|
||||||
|
@app.post("/@/confirm")
|
||||||
|
def do_confirm():
|
||||||
|
csr = get_cursor()
|
||||||
|
csr.execute("delete from confirm_token where expires <= now()")
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
select id, name from confirm_token t join role r on t.role = r.id
|
||||||
|
where token = %s
|
||||||
|
""",
|
||||||
|
(request.form["key"],))
|
||||||
|
r = csr.fetchone()
|
||||||
|
if r:
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
update role
|
||||||
|
set first_confirmed = coalesce(first_confirmed, now()),
|
||||||
|
last_confirmed = now()
|
||||||
|
where id = %s
|
||||||
|
""",
|
||||||
|
(r.id,))
|
||||||
|
|
||||||
|
session["user_id"] = r.id
|
||||||
|
session["user_name"] = r.name
|
||||||
|
return render_template("welcome.html")
|
||||||
|
else:
|
||||||
|
return render_template("no_token.html")
|
||||||
|
|
||||||
|
@app.route("/@/logout", methods=["GET", "POST"])
|
||||||
|
def logout():
|
||||||
|
del session["user_id"]
|
||||||
|
del session["user_name"]
|
||||||
|
return redirect("/", code=303)
|
||||||
|
|
||||||
|
#@get("/~user")
|
||||||
|
# maybe too oldskool? use @user?
|
||||||
|
#@get("/~user/token")
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Content generation
|
||||||
|
|
||||||
|
@app.template_filter("markdown")
|
||||||
|
def markdown_to_html(m):
|
||||||
|
return Markup(markdown.markdown(m))
|
||||||
|
|
||||||
|
def send_mail(email_address, confirmation_url):
|
||||||
|
msg = email.message.EmailMessage()
|
||||||
|
msg["From"] ="noreply@hjp.at"
|
||||||
|
msg["To"] = email_address
|
||||||
|
msg["Subject"] = "Zettel confirmation"
|
||||||
|
body = confirmation_url # Really minimalistic
|
||||||
|
msg.set_content(body)
|
||||||
|
mta = smtplib.SMTP(host="localhost")
|
||||||
|
mta.send_message(msg)
|
||||||
|
|
||||||
|
#-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Plumbing
|
||||||
|
|
||||||
|
def get_roles():
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
csr = get_cursor()
|
||||||
|
if not user_id:
|
||||||
|
csr.execute("select * from role where name = %s", ("public",))
|
||||||
|
r = csr.fetchone()
|
||||||
|
user_id = session["user_id"] = r.id
|
||||||
|
session["user_name"] = r.name
|
||||||
|
assert type(user_id) == int
|
||||||
|
roles = [user_id]
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
with recursive r as (
|
||||||
|
select group_role from role_member where member_role = %s
|
||||||
|
union
|
||||||
|
select role_member.group_role from role_member join r on role_member.member_role = r.group_role
|
||||||
|
)
|
||||||
|
select * from r
|
||||||
|
""",
|
||||||
|
(user_id,))
|
||||||
|
for r in csr:
|
||||||
|
roles.append(r.group_role)
|
||||||
|
return roles
|
||||||
|
|
||||||
|
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, user=config.dbuser)
|
||||||
|
return g.db
|
||||||
|
|
||||||
|
@app.teardown_appcontext
|
||||||
|
def teardown_db(exception):
|
||||||
|
app.logger.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,70 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import html
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
import psycopg
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--author",default="hjp")
|
||||||
|
ap.add_argument("file")
|
||||||
|
args = ap.parse_args()
|
||||||
|
if args.file.endswith(".md"):
|
||||||
|
file_type = "markdown"
|
||||||
|
elif args.file.endswith(".html"):
|
||||||
|
file_type = "html"
|
||||||
|
else:
|
||||||
|
file_type = "plain"
|
||||||
|
|
||||||
|
with open(args.file) as fh:
|
||||||
|
section = None
|
||||||
|
frontmatter = ""
|
||||||
|
body = ""
|
||||||
|
for ln in fh:
|
||||||
|
if section is None and ln == "---\n":
|
||||||
|
section = "frontmatter"
|
||||||
|
elif section == "frontmatter" and ln == "---\n":
|
||||||
|
section = "body"
|
||||||
|
elif section == "frontmatter":
|
||||||
|
frontmatter += ln
|
||||||
|
elif section == "body":
|
||||||
|
body += ln
|
||||||
|
else:
|
||||||
|
raise RuntimeError("oops")
|
||||||
|
frontmatter = yaml.safe_load(frontmatter)
|
||||||
|
|
||||||
|
# We don't have a dedicated title. Just prepend it to the body
|
||||||
|
if file_type == "html":
|
||||||
|
body = "<h1>" + html.escape(frontmatter["title"]) + "</h1>" + "\n" \
|
||||||
|
+ body
|
||||||
|
else:
|
||||||
|
body = frontmatter["title"] + "\n" \
|
||||||
|
+ "=" * len(frontmatter["title"]) + "\n" \
|
||||||
|
+ "\n" \
|
||||||
|
+ body
|
||||||
|
ts = frontmatter.get("lastmod") or frontmatter.get("date")
|
||||||
|
|
||||||
|
conn = psycopg.connect(dbname="zettel")
|
||||||
|
csr = conn.cursor(row_factory=psycopg.rows.namedtuple_row)
|
||||||
|
csr.execute("select * from role where name = %s", (args.author,))
|
||||||
|
author_id = csr.fetchone().id
|
||||||
|
csr.execute(
|
||||||
|
"""
|
||||||
|
insert into post(text, ts, content_type, author) values(%s, %s, %s, %s)
|
||||||
|
returning id
|
||||||
|
""",
|
||||||
|
(body, ts, "text/" + file_type, author_id,))
|
||||||
|
post_id = csr.fetchone().id
|
||||||
|
for tag in frontmatter["tags"]:
|
||||||
|
csr.execute("select * from tag where lower(name) = lower(%s)",
|
||||||
|
(tag,)) # XXX - use citext?
|
||||||
|
r = csr.fetchone()
|
||||||
|
if not r:
|
||||||
|
csr.execute("insert into tag(name) values(%s) returning id",
|
||||||
|
(tag,))
|
||||||
|
r = csr.fetchone()
|
||||||
|
tag_id = r.id
|
||||||
|
csr.execute("insert into tag_post(tag, post) values(%s, %s)",
|
||||||
|
(tag_id, post_id,))
|
||||||
|
conn.commit()
|
|
@ -0,0 +1,8 @@
|
||||||
|
flask
|
||||||
|
ProcruSQL
|
||||||
|
psycopg
|
||||||
|
pyyaml
|
||||||
|
markdown
|
||||||
|
requests
|
||||||
|
beautifulsoup4
|
||||||
|
lxml
|
|
@ -0,0 +1,14 @@
|
||||||
|
table post
|
||||||
|
column post id
|
||||||
|
|
||||||
|
table attachment
|
||||||
|
|
||||||
|
table tag
|
||||||
|
|
||||||
|
table implies
|
||||||
|
|
||||||
|
table tag_post
|
||||||
|
|
||||||
|
table role
|
||||||
|
|
||||||
|
table acl
|
|
@ -0,0 +1,83 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--
|
||||||
|
Replace with local copy (11)
|
||||||
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Economica:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Economica:ital,wght@0,400;0,700;1,400;1,700&family=Josefin+Sans:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- end of (11) -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Josefin Sans", serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
|
||||||
|
line-height: 1.5;
|
||||||
|
background-color: oklch(0.9 0.025 200);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
inline-size: fit-content;
|
||||||
|
grid-row: 1/2;
|
||||||
|
grid-column: 2/3;
|
||||||
|
width: min-content;;
|
||||||
|
text-align: right;
|
||||||
|
justify-self: right;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: "Economica", serif;
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.0;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
/* border: 1px solid oklch(0.1 0.5 200); */
|
||||||
|
margin-block: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: oklch(1 0 0);
|
||||||
|
}
|
||||||
|
.superceded {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.tagcloud {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-row: 1/2;
|
||||||
|
grid-column: 1/2;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
border: 1px solid oklch(0.1 0.5 200);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 0.25em 1.0em;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: oklch(0.3 0.5 200);
|
||||||
|
background-color: oklch(1 0 0);
|
||||||
|
}
|
||||||
|
.tag.selected {
|
||||||
|
background-color: oklch(0.3 0.5 200);
|
||||||
|
color: oklch(0.95 0.5 200);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
grid-row: 2/3;
|
||||||
|
grid-column: 1/3;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
max-width: 40em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<form method="post">
|
||||||
|
<label>
|
||||||
|
<input type="text" name="key" value="{{request.args.key}}">
|
||||||
|
</label>
|
||||||
|
<input type="submit" value="Confirm">
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<form method="post">
|
||||||
|
<label>
|
||||||
|
Email
|
||||||
|
<input name="email" type="email">
|
||||||
|
</label>
|
||||||
|
<input type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
Mail sent.
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
Token could not be found, Maybe it has already expired?
|
||||||
|
<a href="/">Go Home!</a> (J/K)
|
||||||
|
<a href="/@/login">Try again</a>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
{% if session.user_id == 1 %}
|
||||||
|
<a href="{{url_for('login_form')}}">Login</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{url_for('logout')}}">Logout</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="tagcloud">
|
||||||
|
|
||||||
|
{% for tag in tags %}
|
||||||
|
<a href="{{tag.link}}" class="tag {% if tag['selected'] %}selected{% endif %}">{{tag['name']}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
<p>{{posts | length}} posts:</p>
|
||||||
|
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="{%if post.superceded_by%}superceded{%endif%}">
|
||||||
|
<div class="tagcloud">
|
||||||
|
{% for tag in tags_by_post[post.id] %}
|
||||||
|
<a href="{{tag.link}}" class="tag {% if tag['selected'] %}selected{% endif %}">{{tag['name']}}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if post.content_type == "text/html" %}
|
||||||
|
<!-- for now I trust myself -->
|
||||||
|
{{ post.text | safe }}
|
||||||
|
{% elif post.content_type == "text/markdown" %}
|
||||||
|
{{post.text | markdown}}
|
||||||
|
{% else %}
|
||||||
|
<pre>{{post.text}}</pre>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</main>
|
||||||
|
{% endblock content %}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="info">
|
||||||
|
{{ session }}
|
||||||
|
{{ session.user_name }}
|
||||||
|
</div>
|
||||||
|
<main>
|
||||||
|
Welcome, {{ session.user_name }}!
|
||||||
|
<a href="/">Go Home!</a> (J/K)
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue