Historikk per innlegg i admin, og presisert flytte-avsnitt i README
Redigeringssiden lenker til /admin/history/<id> som lister commits som har rørt innleggets mappe; klikk på en commit viser diffen fargelagt. Samme tilgangsregler som editoren (gjester ser bare egne innlegg). README skiller nå tydelig mellom å kopiere hele data-volumet (ingen nye brukere) og å klone bare innholdsrepoet (brukere må opprettes på nytt). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcEy43fNYpwg6K6oTKakWR
This commit is contained in:
parent
6fef73793f
commit
fa8b980edd
7 changed files with 133 additions and 5 deletions
27
README.md
27
README.md
|
|
@ -23,8 +23,9 @@ data/
|
|||
både på `blog.domene.no/` og `annet-domene.com/blog/` uten rebuild.
|
||||
* **Bilder** ligger i samme mappe som innlegget og refereres som ``. Ved opplasting
|
||||
skaleres de ned (maks 2000 px) og EXIF fjernes (ingen GPS-posisjon publiseres).
|
||||
* **Hver lagring er en git-commit** med brukeren som forfatter. Se historikken under
|
||||
Innstillinger, eller `git -C data/content log`.
|
||||
* **Hver lagring er en git-commit** med brukeren som forfatter. Hvert innlegg har en
|
||||
«Historikk»-seksjon i editoren med diff per endring; hele loggen ligger under Innstillinger
|
||||
(eller `git -C data/content log`).
|
||||
* **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester.
|
||||
|
||||
## Kjøre lokalt
|
||||
|
|
@ -70,9 +71,25 @@ prefikset – appen monteres selv under `SOAPBOX_BASE_PATH`.
|
|||
|
||||
## Flytte bloggen (nytt domene og/eller sub-path)
|
||||
|
||||
1. Kopier `data/`-volumet (eller bare `git clone` `data/content` og opprett brukerne på nytt).
|
||||
2. Sett `SOAPBOX_SITE_URL` og eventuelt `SOAPBOX_BASE_PATH` til de nye verdiene.
|
||||
3. Start – siden bygges på nytt. Ingen innhold trenger endring.
|
||||
Ingen innhold trenger endring – alle lenker er relative. Velg én av to måter:
|
||||
|
||||
**A. Kopier hele `data/`-volumet.** Tar med innhold, brukere og cookie-nøkkel. Ingenting må
|
||||
opprettes på nytt.
|
||||
|
||||
```sh
|
||||
podman volume export soapbox-data | ssh ny-server 'podman volume import soapbox-data -'
|
||||
```
|
||||
|
||||
**B. Klon bare innholdsrepoet.** Brukere ligger i sqlite, ikke i git, så de må opprettes på nytt
|
||||
(hovedbruker via `SOAPBOX_OWNER_USER/_PASSWORD` eller `soapbox create-user`, gjester i admin med
|
||||
**samme brukernavn** som før – brukernavnet er URL-prefikset `/gjest/`).
|
||||
|
||||
```sh
|
||||
git clone gammel-server:/sti/til/data/content ./data/content
|
||||
```
|
||||
|
||||
Deretter, uansett metode: sett `SOAPBOX_SITE_URL` og eventuelt `SOAPBOX_BASE_PATH` til de nye
|
||||
verdiene og start. Siden bygges på nytt ved oppstart.
|
||||
|
||||
## Tema
|
||||
|
||||
|
|
|
|||
|
|
@ -266,6 +266,23 @@ def preview(post_id: str):
|
|||
return re.sub(r'src="([^"]+)"', fix, html)
|
||||
|
||||
|
||||
@bp.route("/history/<path:post_id>")
|
||||
@login_required
|
||||
def history(post_id: str):
|
||||
"""Commit-liste for innlegget, og diff for én commit hvis ``sha`` er gitt."""
|
||||
cfg = _cfg()
|
||||
post = _load_or_404(post_id)
|
||||
rel = str(post.path.parent.relative_to(cfg.content_dir))
|
||||
entries = gitstore.path_log(cfg.content_dir, rel)
|
||||
sha = request.args.get("sha")
|
||||
diff = None
|
||||
if sha:
|
||||
diff = gitstore.path_diff(cfg.content_dir, sha, rel)
|
||||
if diff is None:
|
||||
abort(404)
|
||||
return render_template("admin/history.html", post=post, entries=entries, sha=sha, diff=diff)
|
||||
|
||||
|
||||
@bp.route("/media/<path:post_id>/<filename>")
|
||||
@login_required
|
||||
def media(post_id: str, filename: str):
|
||||
|
|
|
|||
|
|
@ -43,3 +43,33 @@ def commit_all(repo: Path, message: str, author: str) -> bool:
|
|||
def log(repo: Path, limit: int = 20) -> list[str]:
|
||||
out = _git(repo, "log", f"-{limit}", "--format=%h %ad %an: %s", "--date=short", check=False)
|
||||
return out.stdout.splitlines() if out.returncode == 0 else []
|
||||
|
||||
|
||||
def path_log(repo: Path, path: str, limit: int = 50) -> list[dict]:
|
||||
"""Commits som har rørt ``path`` (relativ til repo), nyeste først.
|
||||
|
||||
Merk: en katalog som ble omdøpt (slug endret mens innlegget var utkast)
|
||||
mister historikken fra før omdøpingen – git følger bare filer, ikke mapper.
|
||||
"""
|
||||
out = _git(
|
||||
repo,
|
||||
"log",
|
||||
f"-{limit}",
|
||||
"--format=%H%x1f%aI%x1f%an%x1f%s",
|
||||
"--",
|
||||
path,
|
||||
check=False,
|
||||
)
|
||||
entries = []
|
||||
for line in out.stdout.splitlines():
|
||||
sha, date, author, subject = line.split("\x1f", 3)
|
||||
entries.append({"sha": sha, "date": date, "author": author, "subject": subject})
|
||||
return entries
|
||||
|
||||
|
||||
def path_diff(repo: Path, sha: str, path: str) -> str | None:
|
||||
"""Diff for ``path`` i én commit, eller None hvis commit-en ikke finnes."""
|
||||
if not all(c in "0123456789abcdef" for c in sha) or len(sha) < 7:
|
||||
return None
|
||||
out = _git(repo, "show", "--format=", "--stat", "--patch", sha, "--", path, check=False)
|
||||
return out.stdout if out.returncode == 0 else None
|
||||
|
|
|
|||
|
|
@ -69,3 +69,15 @@ textarea#body.dragover { outline: 3px dashed var(--accent); }
|
|||
.danger-zone { margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1rem; }
|
||||
pre.log { background: var(--code-bg); padding: 1rem; overflow-x: auto; font-size: .85rem; }
|
||||
dl.meta dt { font-weight: 600; margin-top: .5rem; } dl.meta dd { margin: 0; }
|
||||
.history { padding-left: 1.2rem; }
|
||||
.history li { padding: .4rem 0; border-bottom: 1px solid var(--border); }
|
||||
.history li[aria-current] { font-weight: 600; }
|
||||
.history .meta { display: block; font-weight: normal; }
|
||||
pre.diff { background: var(--code-bg); padding: 1rem; overflow-x: auto; font-size: .85rem; line-height: 1.4; }
|
||||
pre.diff .add { color: #14632c; background: #d8f2df; display: inline-block; width: 100%; }
|
||||
pre.diff .del { color: #9a1c1c; background: #f8dcdc; display: inline-block; width: 100%; }
|
||||
pre.diff .hunk { color: var(--muted); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
pre.diff .add { color: #b8f0c6; background: #163d21; }
|
||||
pre.diff .del { color: #f5b5b5; background: #4a1c1c; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@
|
|||
</details>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="{{ url_for('admin.history', post_id=post.id) }}">Historikk for dette innlegget</a></p>
|
||||
|
||||
<form method="post" action="{{ url_for('admin.delete', post_id=post.id) }}" class="danger-zone" data-confirm="Slette hele innlegget «{{ post.title }}»?">
|
||||
<input type="hidden" name="csrf" value="{{ csrf }}">
|
||||
<button type="submit" class="link danger">Slett innlegget</button>
|
||||
|
|
|
|||
23
soapbox/templates/admin/history.html
Normal file
23
soapbox/templates/admin/history.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "admin/base.html" %}
|
||||
{% block title %}Historikk – {{ post.title }}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Historikk: {{ post.title }}</h1>
|
||||
<p><a href="{{ url_for('admin.edit', post_id=post.id) }}">← Tilbake til editoren</a></p>
|
||||
|
||||
{% if not entries %}<p>Ingen historikk ennå.</p>{% endif %}
|
||||
<ol class="history">
|
||||
{% for e in entries %}
|
||||
<li{% if e.sha == sha %} aria-current="true"{% endif %}>
|
||||
<a href="{{ url_for('admin.history', post_id=post.id, sha=e.sha) }}">{{ e.subject }}</a>
|
||||
<span class="meta"><time datetime="{{ e.date }}">{{ e.date[:16].replace('T', ' ') }}</time> · {{ e.author }} · <code>{{ e.sha[:7] }}</code></span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
||||
{% if diff is not none %}
|
||||
<h2>Endring <code>{{ sha[:7] }}</code></h2>
|
||||
{# Diffen vises linje for linje slik at + og − kan farges uten å tolke innholdet som HTML. #}
|
||||
<pre class="diff">{% for line in diff.splitlines() %}<span class="{% if line.startswith('+') and not line.startswith('+++') %}add{% elif line.startswith('-') and not line.startswith('---') %}del{% elif line.startswith('@@') %}hunk{% endif %}">{{ line }}</span>
|
||||
{% endfor %}</pre>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -162,3 +162,30 @@ def test_no_inline_js_with_content(client, cfg):
|
|||
html = client.get(r.headers["Location"]).text
|
||||
assert "onsubmit=" not in html and "onclick=" not in html
|
||||
assert "Ola's" in html
|
||||
|
||||
|
||||
def test_post_history(client, cfg):
|
||||
post_id = _create_post(client)
|
||||
token = csrf(client)
|
||||
r = client.post(
|
||||
f"/admin/edit/{post_id}",
|
||||
data={"csrf": token, "title": "Hist", "body": "første", "action": "publish"},
|
||||
)
|
||||
pid = r.headers["Location"].split("/admin/edit/")[1]
|
||||
client.post(
|
||||
f"/admin/edit/{pid}",
|
||||
data={"csrf": token, "title": "Hist", "body": "andre", "action": "save"},
|
||||
)
|
||||
|
||||
html = client.get(f"/admin/history/{pid}").text
|
||||
assert "Publiserte: Hist" in html and "Lagret: Hist" in html
|
||||
sha = re.search(r"sha=([0-9a-f]{40})", html).group(1)
|
||||
diff = client.get(f"/admin/history/{pid}?sha={sha}").text
|
||||
assert '<span class="add">+andre</span>' in diff and '<span class="del">-første</span>' in diff
|
||||
assert client.get(f"/admin/history/{pid}?sha=deadbeef").status_code == 404
|
||||
assert client.get(f"/admin/history/{pid}?sha=;rm").status_code == 404
|
||||
|
||||
# Gjest får ikke se eierens historikk
|
||||
client.post("/admin/logout", data={"csrf": token})
|
||||
login(client, "gjest1")
|
||||
assert client.get(f"/admin/history/{pid}").status_code == 403
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue