From 5263d3011d6257bf33692a0fc7794b01590fe94e Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Wed, 26 Aug 2026 15:48:15 +0200 Subject: [PATCH] Brukersider: /brukere/ og /brukere// med beskrivelse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR --- README.md | 2 + soapbox/admin.py | 21 +++++++- soapbox/build.py | 50 +++++++++++++++--- soapbox/content.py | 1 + soapbox/db.py | 17 ++++-- soapbox/templates/admin/profile.html | 2 + soapbox/templates/admin/users.html | 2 +- soapbox/themes/green/static/style.css | 7 +++ soapbox/themes/green/templates/base.html | 2 +- soapbox/themes/green/templates/post.html | 2 +- soapbox/themes/green/templates/user.html | 27 ++++++++++ soapbox/themes/green/templates/users.html | 14 +++++ tests/test_app.py | 64 +++++++++++++++++++++-- 13 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 soapbox/themes/green/templates/user.html create mode 100644 soapbox/themes/green/templates/users.html diff --git a/README.md b/README.md index d89822b..8ccc7eb 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ data/ * **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester. * **Tagger** settes kommaseparert i editoren. `/tag//` lister alle innlegg med taggen (også gjesters), `/tag/` alle tagger. Tagger får slug på samme måte som titler. +* **Brukersider**: `/brukere/` lister hovedbruker øverst og gjester alfabetisk; `/brukere//` + viser visningsnavn, beskrivelse (Markdown, settes under Profil), e-post og innlegg. * **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. diff --git a/soapbox/admin.py b/soapbox/admin.py index 41464e6..c14d59c 100644 --- a/soapbox/admin.py +++ b/soapbox/admin.py @@ -64,7 +64,17 @@ bp = Blueprint( USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$") EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") # 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(): @@ -434,6 +444,7 @@ def users(): elif action == "delete": db.delete_user(g.db, int(request.form.get("user_id", 0))) flash("Brukeren er slettet. Innleggene ligger fortsatt i git.", "ok") + build_site(_cfg()) # /brukere/ genereres fra brukertabellen return redirect(url_for("admin.users")) 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): flash("Ugyldig e-postadresse.", "error") 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 flash("Profilen er lagret.", "ok") return redirect(url_for("admin.profile")) diff --git a/soapbox/build.py b/soapbox/build.py index b27bb20..12dd142 100644 --- a/soapbox/build.py +++ b/soapbox/build.py @@ -116,8 +116,27 @@ def build_site(cfg: Config) -> int: ) _write(tmp / "tag" / "index.html", render("tags.html", "tag/", tags=tags)) + # Brukere: /brukere/ (hovedbruker øverst) og /brukere// 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 / "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). for d in reversed(cfg.theme_dirs): @@ -136,6 +155,23 @@ def build_site(cfg: Config) -> int: 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: """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 verdien fra frontmatter som den er. """ - conn = db.connect(cfg.db_path) - try: - users = db.authors(conn) - finally: - conn.close() + users = _load_users(cfg) for p in posts: key = p.author or (p.guest or "") 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) 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_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.""" 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(f"{g}/") for g in guests] entries += [url(f"tag/{t}/") for t in tags] + entries += [url("brukere/")] + [url(f"brukere/{u}/") for u in users] return ( '\n' '' diff --git a/soapbox/content.py b/soapbox/content.py index a2b5947..34e1950 100644 --- a/soapbox/content.py +++ b/soapbox/content.py @@ -40,6 +40,7 @@ class Post: author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her) author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen author_email: str = "" + author_username: str = "" # for lenke til /brukere//, tomt hvis ukjent tags: list[str] = field(default_factory=list) # visningsform, f.eks. "Friluftsliv" guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn path: Path | None = None diff --git a/soapbox/db.py b/soapbox/db.py index 0424171..59b29da 100644 --- a/soapbox/db.py +++ b/soapbox/db.py @@ -41,6 +41,7 @@ class User: display_name: str role: str email: str = "" + bio: str = "" @property def is_owner(self) -> bool: @@ -61,7 +62,9 @@ def connect(path: Path) -> sqlite3.Connection: cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)")} if "email" not in cols: conn.execute("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''") - conn.commit() + if "bio" not in cols: + conn.execute("ALTER TABLE users ADD COLUMN bio TEXT NOT NULL DEFAULT ''") + conn.commit() return conn @@ -93,7 +96,9 @@ def verify_password(password: str, stored: str) -> bool: def _row_to_user(row: sqlite3.Row | None) -> User | None: if row is 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( @@ -130,10 +135,12 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> User 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( - "UPDATE users SET display_name=?, email=? WHERE id=?", - (display_name.strip(), email.strip(), user_id), + "UPDATE users SET display_name=?, email=?, bio=? WHERE id=?", + (display_name.strip(), email.strip(), bio.strip(), user_id), ) conn.commit() diff --git a/soapbox/templates/admin/profile.html b/soapbox/templates/admin/profile.html index 67956f6..37ce578 100644 --- a/soapbox/templates/admin/profile.html +++ b/soapbox/templates/admin/profile.html @@ -8,6 +8,8 @@ +

Brukernavn: {{ me.username }}{% if not me.is_owner %} (URL-prefiks /{{ me.username }}/){% endif %}

diff --git a/soapbox/templates/admin/users.html b/soapbox/templates/admin/users.html index 4b38443..f572036 100644 --- a/soapbox/templates/admin/users.html +++ b/soapbox/templates/admin/users.html @@ -33,7 +33,7 @@
- + diff --git a/soapbox/themes/green/static/style.css b/soapbox/themes/green/static/style.css index 607318b..92f2127 100644 --- a/soapbox/themes/green/static/style.css +++ b/soapbox/themes/green/static/style.css @@ -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); } .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; } .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; } diff --git a/soapbox/themes/green/templates/base.html b/soapbox/themes/green/templates/base.html index 605fc04..c2c5d6c 100644 --- a/soapbox/themes/green/templates/base.html +++ b/soapbox/themes/green/templates/base.html @@ -28,7 +28,7 @@