Brukersider: /brukere/ og /brukere/<brukernavn>/ med beskrivelse
Alle brukere kan skrive en valgfri beskrivelse (Markdown, uten rå HTML) under Profil. /brukere/ lister hovedbruker øverst og gjester alfabetisk etter visningsnavn; brukersiden viser navn, beskrivelse, e-post og innlegg. Signaturen under innlegg lenker til brukersiden. «brukere» er reservert. Siden bygges nå også når brukere opprettes/slettes. Retter også ugyldig pattern-regex for brukernavn i admin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
parent
f0fcb875c5
commit
5263d3011d
13 changed files with 191 additions and 20 deletions
|
|
@ -29,6 +29,8 @@ data/
|
||||||
* **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
|
* **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
|
||||||
* **Tagger** settes kommaseparert i editoren. `/tag/<tagg>/` lister alle innlegg med taggen (også
|
* **Tagger** settes kommaseparert i editoren. `/tag/<tagg>/` lister alle innlegg med taggen (også
|
||||||
gjesters), `/tag/` alle tagger. Tagger får slug på samme måte som titler.
|
gjesters), `/tag/` alle tagger. Tagger får slug på samme måte som titler.
|
||||||
|
* **Brukersider**: `/brukere/` lister hovedbruker øverst og gjester alfabetisk; `/brukere/<navn>/`
|
||||||
|
viser visningsnavn, beskrivelse (Markdown, settes under Profil), e-post og innlegg.
|
||||||
* **Signatur**: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge
|
* **Signatur**: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge
|
||||||
settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg.
|
settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,17 @@ bp = Blueprint(
|
||||||
USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$")
|
||||||
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
# Reserverte navn som ville kollidert med URL-strukturen
|
# Reserverte navn som ville kollidert med URL-strukturen
|
||||||
RESERVED = {"admin", "theme", "tag", "feed.xml", "sitemap.xml", "posts", "guests", "index.html"}
|
RESERVED = {
|
||||||
|
"admin",
|
||||||
|
"theme",
|
||||||
|
"tag",
|
||||||
|
"brukere",
|
||||||
|
"feed.xml",
|
||||||
|
"sitemap.xml",
|
||||||
|
"posts",
|
||||||
|
"guests",
|
||||||
|
"index.html",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _cfg():
|
def _cfg():
|
||||||
|
|
@ -434,6 +444,7 @@ def users():
|
||||||
elif action == "delete":
|
elif action == "delete":
|
||||||
db.delete_user(g.db, int(request.form.get("user_id", 0)))
|
db.delete_user(g.db, int(request.form.get("user_id", 0)))
|
||||||
flash("Brukeren er slettet. Innleggene ligger fortsatt i git.", "ok")
|
flash("Brukeren er slettet. Innleggene ligger fortsatt i git.", "ok")
|
||||||
|
build_site(_cfg()) # /brukere/ genereres fra brukertabellen
|
||||||
return redirect(url_for("admin.users"))
|
return redirect(url_for("admin.users"))
|
||||||
return render_template("admin/users.html", users=db.list_users(g.db))
|
return render_template("admin/users.html", users=db.list_users(g.db))
|
||||||
|
|
||||||
|
|
@ -463,7 +474,13 @@ def profile():
|
||||||
if email and not EMAIL_RE.match(email):
|
if email and not EMAIL_RE.match(email):
|
||||||
flash("Ugyldig e-postadresse.", "error")
|
flash("Ugyldig e-postadresse.", "error")
|
||||||
else:
|
else:
|
||||||
db.update_profile(g.db, g.user.id, request.form.get("display_name", ""), email)
|
db.update_profile(
|
||||||
|
g.db,
|
||||||
|
g.user.id,
|
||||||
|
request.form.get("display_name", ""),
|
||||||
|
email,
|
||||||
|
request.form.get("bio", "").replace("\r\n", "\n"),
|
||||||
|
)
|
||||||
build_site(_cfg()) # signaturen ligger i de genererte sidene
|
build_site(_cfg()) # signaturen ligger i de genererte sidene
|
||||||
flash("Profilen er lagret.", "ok")
|
flash("Profilen er lagret.", "ok")
|
||||||
return redirect(url_for("admin.profile"))
|
return redirect(url_for("admin.profile"))
|
||||||
|
|
|
||||||
|
|
@ -116,8 +116,27 @@ def build_site(cfg: Config) -> int:
|
||||||
)
|
)
|
||||||
_write(tmp / "tag" / "index.html", render("tags.html", "tag/", tags=tags))
|
_write(tmp / "tag" / "index.html", render("tags.html", "tag/", tags=tags))
|
||||||
|
|
||||||
|
# Brukere: /brukere/ (hovedbruker øverst) og /brukere/<brukernavn>/ med bio og innlegg
|
||||||
|
users = _users_sorted(_load_users(cfg))
|
||||||
|
_write(tmp / "brukere" / "index.html", render("users.html", "brukere/", users=users))
|
||||||
|
for u in users:
|
||||||
|
uposts = [p for p in posts if (p.guest or None) == (None if u.is_owner else u.username)]
|
||||||
|
_write(
|
||||||
|
tmp / "brukere" / u.username / "index.html",
|
||||||
|
render(
|
||||||
|
"user.html",
|
||||||
|
f"brukere/{u.username}/",
|
||||||
|
user=u,
|
||||||
|
posts=uposts,
|
||||||
|
bio_html=render_markdown(u.bio, f"brukere/{u.username}/") if u.bio else "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
_write(tmp / "feed.xml", _feed(cfg, site, posts[:20]))
|
||||||
_write(tmp / "sitemap.xml", _sitemap(cfg, posts, sorted(guests), sorted(tags)))
|
_write(
|
||||||
|
tmp / "sitemap.xml",
|
||||||
|
_sitemap(cfg, posts, sorted(guests), sorted(tags), [u.username for u in users]),
|
||||||
|
)
|
||||||
|
|
||||||
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
# Statiske filer lagvis: innebygd først, eget tema oppå (overskriver).
|
||||||
for d in reversed(cfg.theme_dirs):
|
for d in reversed(cfg.theme_dirs):
|
||||||
|
|
@ -136,6 +155,23 @@ def build_site(cfg: Config) -> int:
|
||||||
return len(posts)
|
return len(posts)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_users(cfg: Config) -> dict[str, db.User]:
|
||||||
|
conn = db.connect(cfg.db_path)
|
||||||
|
try:
|
||||||
|
return db.authors(conn)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _users_sorted(users: dict[str, db.User]) -> list[db.User]:
|
||||||
|
"""Hovedbruker først, deretter gjester alfabetisk etter visningsnavn."""
|
||||||
|
owners = [u for u in users.values() if u.is_owner]
|
||||||
|
guests = sorted(
|
||||||
|
(u for u in users.values() if not u.is_owner), key=lambda u: u.display_name.casefold()
|
||||||
|
)
|
||||||
|
return owners + guests
|
||||||
|
|
||||||
|
|
||||||
def _resolve_authors(cfg: Config, posts: list[Post]) -> None:
|
def _resolve_authors(cfg: Config, posts: list[Post]) -> None:
|
||||||
"""Fyller author_name/author_email fra brukertabellen.
|
"""Fyller author_name/author_email fra brukertabellen.
|
||||||
|
|
||||||
|
|
@ -144,11 +180,7 @@ def _resolve_authors(cfg: Config, posts: list[Post]) -> None:
|
||||||
Finnes ikke brukeren (f.eks. innhold klonet uten brukerdatabase), vises
|
Finnes ikke brukeren (f.eks. innhold klonet uten brukerdatabase), vises
|
||||||
verdien fra frontmatter som den er.
|
verdien fra frontmatter som den er.
|
||||||
"""
|
"""
|
||||||
conn = db.connect(cfg.db_path)
|
users = _load_users(cfg)
|
||||||
try:
|
|
||||||
users = db.authors(conn)
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
for p in posts:
|
for p in posts:
|
||||||
key = p.author or (p.guest or "")
|
key = p.author or (p.guest or "")
|
||||||
user = users.get(key)
|
user = users.get(key)
|
||||||
|
|
@ -156,9 +188,12 @@ def _resolve_authors(cfg: Config, posts: list[Post]) -> None:
|
||||||
user = next((u for u in users.values() if u.is_owner), None)
|
user = next((u for u in users.values() if u.is_owner), None)
|
||||||
p.author_name = user.display_name if user else (p.author or p.guest or "")
|
p.author_name = user.display_name if user else (p.author or p.guest or "")
|
||||||
p.author_email = user.email if user else ""
|
p.author_email = user.email if user else ""
|
||||||
|
p.author_username = user.username if user else ""
|
||||||
|
|
||||||
|
|
||||||
def _sitemap(cfg: Config, posts: list[Post], guests: list[str], tags: list[str]) -> str:
|
def _sitemap(
|
||||||
|
cfg: Config, posts: list[Post], guests: list[str], tags: list[str], users: list[str]
|
||||||
|
) -> str:
|
||||||
"""sitemap.xml med forsiden, alle innlegg og gjesteoversiktene."""
|
"""sitemap.xml med forsiden, alle innlegg og gjesteoversiktene."""
|
||||||
base = cfg.site_url + "/"
|
base = cfg.site_url + "/"
|
||||||
|
|
||||||
|
|
@ -172,6 +207,7 @@ def _sitemap(cfg: Config, posts: list[Post], guests: list[str], tags: list[str])
|
||||||
entries += [url(p.url_path, p.updated or p.date) for p in posts]
|
entries += [url(p.url_path, p.updated or p.date) for p in posts]
|
||||||
entries += [url(f"{g}/") for g in guests]
|
entries += [url(f"{g}/") for g in guests]
|
||||||
entries += [url(f"tag/{t}/") for t in tags]
|
entries += [url(f"tag/{t}/") for t in tags]
|
||||||
|
entries += [url("brukere/")] + [url(f"brukere/{u}/") for u in users]
|
||||||
return (
|
return (
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class Post:
|
||||||
author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her)
|
author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her)
|
||||||
author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen
|
author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen
|
||||||
author_email: str = ""
|
author_email: str = ""
|
||||||
|
author_username: str = "" # for lenke til /brukere/<brukernavn>/, tomt hvis ukjent
|
||||||
tags: list[str] = field(default_factory=list) # visningsform, f.eks. "Friluftsliv"
|
tags: list[str] = field(default_factory=list) # visningsform, f.eks. "Friluftsliv"
|
||||||
guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn
|
guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn
|
||||||
path: Path | None = None
|
path: Path | None = None
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class User:
|
||||||
display_name: str
|
display_name: str
|
||||||
role: str
|
role: str
|
||||||
email: str = ""
|
email: str = ""
|
||||||
|
bio: str = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_owner(self) -> bool:
|
def is_owner(self) -> bool:
|
||||||
|
|
@ -61,6 +62,8 @@ def connect(path: Path) -> sqlite3.Connection:
|
||||||
cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)")}
|
cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)")}
|
||||||
if "email" not in cols:
|
if "email" not in cols:
|
||||||
conn.execute("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''")
|
conn.execute("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''")
|
||||||
|
if "bio" not in cols:
|
||||||
|
conn.execute("ALTER TABLE users ADD COLUMN bio TEXT NOT NULL DEFAULT ''")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
|
|
@ -93,7 +96,9 @@ def verify_password(password: str, stored: str) -> bool:
|
||||||
def _row_to_user(row: sqlite3.Row | None) -> User | None:
|
def _row_to_user(row: sqlite3.Row | None) -> User | None:
|
||||||
if row is None:
|
if row is None:
|
||||||
return None
|
return None
|
||||||
return User(row["id"], row["username"], row["display_name"], row["role"], row["email"])
|
return User(
|
||||||
|
row["id"], row["username"], row["display_name"], row["role"], row["email"], row["bio"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_user(
|
def create_user(
|
||||||
|
|
@ -130,10 +135,12 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> User
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def update_profile(conn: sqlite3.Connection, user_id: int, display_name: str, email: str) -> None:
|
def update_profile(
|
||||||
|
conn: sqlite3.Connection, user_id: int, display_name: str, email: str, bio: str = ""
|
||||||
|
) -> None:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE users SET display_name=?, email=? WHERE id=?",
|
"UPDATE users SET display_name=?, email=?, bio=? WHERE id=?",
|
||||||
(display_name.strip(), email.strip(), user_id),
|
(display_name.strip(), email.strip(), bio.strip(), user_id),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
<input name="display_name" value="{{ me.display_name }}" aria-describedby="dn-hint"></label>
|
<input name="display_name" value="{{ me.display_name }}" aria-describedby="dn-hint"></label>
|
||||||
<label>E-post <span class="hint" id="em-hint">Valgfritt. Vises offentlig ved signaturen så lesere kan kontakte deg.</span>
|
<label>E-post <span class="hint" id="em-hint">Valgfritt. Vises offentlig ved signaturen så lesere kan kontakte deg.</span>
|
||||||
<input type="email" name="email" value="{{ me.email }}" autocomplete="email" aria-describedby="em-hint"></label>
|
<input type="email" name="email" value="{{ me.email }}" autocomplete="email" aria-describedby="em-hint"></label>
|
||||||
|
<label>Om deg <span class="hint" id="bio-hint">Valgfritt, Markdown. Vises på din side under /brukere/.</span>
|
||||||
|
<textarea name="bio" rows="5" aria-describedby="bio-hint">{{ me.bio }}</textarea></label>
|
||||||
<p class="meta">Brukernavn: <code>{{ me.username }}</code>{% if not me.is_owner %} (URL-prefiks /{{ me.username }}/){% endif %}</p>
|
<p class="meta">Brukernavn: <code>{{ me.username }}</code>{% if not me.is_owner %} (URL-prefiks /{{ me.username }}/){% endif %}</p>
|
||||||
<button type="submit" class="primary">Lagre</button>
|
<button type="submit" class="primary">Lagre</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<form method="post" class="stack narrow">
|
<form method="post" class="stack narrow">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||||
<input type="hidden" name="action" value="create">
|
<input type="hidden" name="action" value="create">
|
||||||
<label>Brukernavn (blir URL: /brukernavn/) <input name="username" pattern="[a-z0-9][a-z0-9-]{1,30}" required autocapitalize="none"></label>
|
<label>Brukernavn (blir URL: /brukernavn/) <input name="username" pattern="[a-z0-9][a-z0-9\-]{1,30}" required autocapitalize="none"></label>
|
||||||
<label>Visningsnavn <input name="display_name"></label>
|
<label>Visningsnavn <input name="display_name"></label>
|
||||||
<label>E-post (valgfritt, vises offentlig) <input type="email" name="email"></label>
|
<label>E-post (valgfritt, vises offentlig) <input type="email" name="email"></label>
|
||||||
<label>Passord <input type="password" name="password" minlength="8" required autocomplete="new-password"></label>
|
<label>Passord <input type="password" name="password" minlength="8" required autocomplete="new-password"></label>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,13 @@ h2 { font-size: 1.4rem; line-height: 1.25; margin: 2rem 0 .5rem; }
|
||||||
}
|
}
|
||||||
.tags a:hover, .tags a:focus-visible { border-color: var(--accent); }
|
.tags a:hover, .tags a:focus-visible { border-color: var(--accent); }
|
||||||
.tag-cloud { margin-top: 1rem; } .tag-cloud a { font-size: 1rem; }
|
.tag-cloud { margin-top: 1rem; } .tag-cloud a { font-size: 1rem; }
|
||||||
|
.users { list-style: none; padding: 0; }
|
||||||
|
.users li { padding: 1rem 0; border-bottom: 1px solid var(--border); }
|
||||||
|
.users li a { font-size: 1.2rem; font-weight: 600; text-decoration: none; color: var(--accent-strong); }
|
||||||
|
.users .excerpt { margin: .25rem 0 0; }
|
||||||
|
.summary h3 { font-size: 1.2rem; margin: 0 0 .25rem; }
|
||||||
|
.summary h3 a { color: var(--accent-strong); text-decoration: none; }
|
||||||
|
.signature a { color: inherit; }
|
||||||
.signature { margin: 1.5rem 0 0; color: var(--muted); font-style: italic; }
|
.signature { margin: 1.5rem 0 0; color: var(--muted); font-style: italic; }
|
||||||
.body img { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; border-radius: 4px; }
|
.body img { max-width: 100%; height: auto; display: block; margin: 1.5rem 0; border-radius: 4px; }
|
||||||
.body pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
.body pre { background: var(--code-bg); padding: 1rem; overflow-x: auto; border-radius: 4px; }
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
</main>
|
</main>
|
||||||
<footer class="site-footer">
|
<footer class="site-footer">
|
||||||
<nav aria-label="Bunnmeny">
|
<nav aria-label="Bunnmeny">
|
||||||
<a href="{{ rel('') }}">Forsiden</a><a href="{{ rel('tag/') }}">Tagger</a><a href="{{ rel('feed.xml') }}">RSS</a><a href="{{ rel('admin/') }}">Logg inn</a>
|
<a href="{{ rel('') }}">Forsiden</a><a href="{{ rel('tag/') }}">Tagger</a><a href="{{ rel('brukere/') }}">Brukere</a><a href="{{ rel('feed.xml') }}">RSS</a><a href="{{ rel('admin/') }}">Logg inn</a>
|
||||||
</nav>
|
</nav>
|
||||||
{# AGPL §13: brukere av tjenesten skal kunne hente kildekoden. #}
|
{# AGPL §13: brukere av tjenesten skal kunne hente kildekoden. #}
|
||||||
<p class="colophon">Drevet av <a href="{{ source_url }}">Soapbox</a> (AGPL-3.0)</p>
|
<p class="colophon">Drevet av <a href="{{ source_url }}">Soapbox</a> (AGPL-3.0)</p>
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.author_name %}
|
{% if post.author_name %}
|
||||||
<p class="signature">– {{ post.author_name }}{% if post.author_email %}
|
<p class="signature">– {% if post.author_username %}<a href="{{ rel('brukere/' ~ post.author_username ~ '/') }}">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}{% if post.author_email %}
|
||||||
· <a href="mailto:{{ post.author_email }}">{{ post.author_email }}</a>{% endif %}</p>
|
· <a href="mailto:{{ post.author_email }}">{{ post.author_email }}</a>{% endif %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
27
soapbox/themes/green/templates/user.html
Normal file
27
soapbox/themes/green/templates/user.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ user.display_name }} – {{ site.title }}{% endblock %}
|
||||||
|
{% block og %}
|
||||||
|
<meta property="og:type" content="profile">
|
||||||
|
<meta property="og:site_name" content="{{ site.title }}">
|
||||||
|
<meta property="og:title" content="{{ user.display_name }}">
|
||||||
|
<meta property="og:url" content="{{ site_url }}/brukere/{{ user.username }}/">
|
||||||
|
{% if user.bio %}<meta property="og:description" content="{{ user.bio|excerpt(200) }}">{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<article class="profile">
|
||||||
|
<h1>{{ user.display_name }}</h1>
|
||||||
|
<p class="meta">
|
||||||
|
{% if user.is_owner %}Hovedbruker{% else %}Gjest{% endif %}
|
||||||
|
{% if user.email %} · <a href="mailto:{{ user.email }}">{{ user.email }}</a>{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if bio_html %}<div class="body">{{ bio_html|safe }}</div>{% endif %}
|
||||||
|
</article>
|
||||||
|
<h2>Innlegg{% if posts %} ({{ posts|length }}){% endif %}</h2>
|
||||||
|
{% if not posts %}<p>Ingen innlegg ennå.</p>{% endif %}
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="summary">
|
||||||
|
<h3><a href="{{ rel(post.url_path) }}">{{ post.title }}</a></h3>
|
||||||
|
<p class="meta"><time datetime="{{ post.date.isoformat() }}">{{ post.date|date }}</time></p>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
14
soapbox/themes/green/templates/users.html
Normal file
14
soapbox/themes/green/templates/users.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Brukere – {{ site.title }}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Brukere</h1>
|
||||||
|
<ul class="users">
|
||||||
|
{% for u in users %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ rel('brukere/' ~ u.username ~ '/') }}">{{ u.display_name }}</a>
|
||||||
|
{% if u.is_owner %}<span class="meta">· hovedbruker</span>{% endif %}
|
||||||
|
{% if u.bio %}<p class="excerpt">{{ u.bio|excerpt(160) }}</p>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -323,7 +323,9 @@ def test_signature_from_profile(client, cfg):
|
||||||
f"/admin/edit/{post_id}",
|
f"/admin/edit/{post_id}",
|
||||||
data={"csrf": token, "title": "Sign", "body": "x", "action": "publish"},
|
data={"csrf": token, "title": "Sign", "body": "x", "action": "publish"},
|
||||||
)
|
)
|
||||||
assert "– Ole" in client.get("/sign/").text # visningsnavn fra brukertabellen
|
assert re.search(
|
||||||
|
r"– <a[^>]*>Ole</a>", client.get("/sign/").text
|
||||||
|
) # visningsnavn fra brukertabellen
|
||||||
assert (cfg.content_dir / "posts/sign/index.md").read_text().count('author: "eier"') == 1
|
assert (cfg.content_dir / "posts/sign/index.md").read_text().count('author: "eier"') == 1
|
||||||
|
|
||||||
r = client.post(
|
r = client.post(
|
||||||
|
|
@ -331,7 +333,7 @@ def test_signature_from_profile(client, cfg):
|
||||||
)
|
)
|
||||||
assert r.status_code == 302
|
assert r.status_code == 302
|
||||||
html = client.get("/sign/").text
|
html = client.get("/sign/").text
|
||||||
assert "– Ole M." in html and 'href="mailto:ole@example.no"' in html
|
assert ">Ole M.</a>" in html and 'href="mailto:ole@example.no"' in html
|
||||||
assert "<dc:creator>Ole M.</dc:creator>" in client.get("/feed.xml").text
|
assert "<dc:creator>Ole M.</dc:creator>" in client.get("/feed.xml").text
|
||||||
assert (
|
assert (
|
||||||
client.post(
|
client.post(
|
||||||
|
|
@ -362,7 +364,12 @@ def test_tags(client, cfg):
|
||||||
|
|
||||||
assert 'href="../tag/friluftsliv/"' in client.get("/en/").text
|
assert 'href="../tag/friluftsliv/"' in client.get("/en/").text
|
||||||
page = client.get("/tag/friluftsliv/").text
|
page = client.get("/tag/friluftsliv/").text
|
||||||
assert "tagg: friluftsliv" in page.lower() and ">En<" in page and ">To<" in page and ">Tre<" not in page
|
assert (
|
||||||
|
"tagg: friluftsliv" in page.lower()
|
||||||
|
and ">En<" in page
|
||||||
|
and ">To<" in page
|
||||||
|
and ">Tre<" not in page
|
||||||
|
)
|
||||||
assert client.get("/tag/skog/").status_code == 200
|
assert client.get("/tag/skog/").status_code == 200
|
||||||
assert client.get("/tag/").status_code == 200 and "(2)" in client.get("/tag/").text
|
assert client.get("/tag/").status_code == 200 and "(2)" in client.get("/tag/").text
|
||||||
assert "<category>skog</category>" in client.get("/feed.xml").text
|
assert "<category>skog</category>" in client.get("/feed.xml").text
|
||||||
|
|
@ -370,3 +377,54 @@ def test_tags(client, cfg):
|
||||||
# "tag" er reservert som slug
|
# "tag" er reservert som slug
|
||||||
publish("tag", "")
|
publish("tag", "")
|
||||||
assert not (cfg.content_dir / "posts/tag").exists()
|
assert not (cfg.content_dir / "posts/tag").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_pages(client, cfg):
|
||||||
|
login(client)
|
||||||
|
token = csrf(client)
|
||||||
|
client.post(
|
||||||
|
"/admin/profile",
|
||||||
|
data={
|
||||||
|
"csrf": token,
|
||||||
|
"display_name": "Ole",
|
||||||
|
"email": "",
|
||||||
|
"bio": "Skriver om **skog**.\n\n<b>x</b>",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r = client.post("/admin/new", data={"csrf": token})
|
||||||
|
pid = r.headers["Location"].split("/admin/edit/")[1]
|
||||||
|
client.post(
|
||||||
|
f"/admin/edit/{pid}",
|
||||||
|
data={"csrf": token, "title": "Mitt", "body": "x", "action": "publish"},
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
"/admin/users",
|
||||||
|
data={
|
||||||
|
"csrf": token,
|
||||||
|
"action": "create",
|
||||||
|
"username": "aa",
|
||||||
|
"display_name": "Åse",
|
||||||
|
"password": "passord123",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
lst = client.get("/brukere/").text
|
||||||
|
order = [n for n in ("Ole", "Gjest En", "Åse") if n in lst]
|
||||||
|
assert [lst.index(n) for n in order] == sorted(
|
||||||
|
lst.index(n) for n in order
|
||||||
|
) # eier, så Gjest En, Åse
|
||||||
|
assert lst.index("Ole") < lst.index("Gjest En") < lst.index("Åse")
|
||||||
|
page = client.get("/brukere/eier/").text
|
||||||
|
assert "<strong>skog</strong>" in page and "<b>" in page # markdown, ikke rå HTML
|
||||||
|
assert ">Mitt<" in page and "Hovedbruker" in page
|
||||||
|
assert "Ingen innlegg ennå" in client.get("/brukere/gjest1/").text
|
||||||
|
assert 'href="../brukere/eier/"' in client.get("/mitt/").text # signatur lenker til brukersiden
|
||||||
|
assert "https://blog.example.no/brukere/eier/" in client.get("/sitemap.xml").text
|
||||||
|
# "brukere" er reservert som slug
|
||||||
|
r = client.post("/admin/new", data={"csrf": token})
|
||||||
|
pid = r.headers["Location"].split("/admin/edit/")[1]
|
||||||
|
client.post(
|
||||||
|
f"/admin/edit/{pid}",
|
||||||
|
data={"csrf": token, "title": "brukere", "body": "x", "action": "publish"},
|
||||||
|
)
|
||||||
|
assert not (cfg.content_dir / "posts/brukere").exists()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue