From 987774a3207b676e55129ca616910cd44e4f1de0 Mon Sep 17 00:00:00 2001 From: Tero Date: Sun, 30 Aug 2026 10:29:53 +0300 Subject: [PATCH] Add support for use singe pic on post --- README.md | 2 ++ TODO.md | 3 +- __pycache__/app.cpython-314.pyc | Bin 31158 -> 34204 bytes app.py | 51 ++++++++++++++++++++++++++++---- data/eternity.db | Bin 36864 -> 36864 bytes static/css/site.css | 2 +- templates/editor.html | 3 +- templates/post.html | 3 +- 8 files changed, 54 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 099a71f..5ce20c9 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ docker compose up --build -d Open `http://localhost:8000`, create the first account, and publish a field note. The first request initializes the database automatically. +Posts can include one JPEG, PNG, or WebP image up to 8 MB. Images are converted to optimized WebP files in the persistent Docker data volume; the article shows a compact preview that opens full-size when selected. + ## Accounts and MFA The initial administrator account is `admin` with password `admin`, as requested for first-run access. Sign in, scan the displayed QR code with any iPhone or Android TOTP authenticator, and change this password before exposing the service to the internet. New registrations are held for approval in **Accounts**; accepted users must enroll a TOTP authenticator before they can publish. diff --git a/TODO.md b/TODO.md index 4679efc..6e3a6fc 100644 --- a/TODO.md +++ b/TODO.md @@ -8,4 +8,5 @@ - [x] Rotate the initial `admin` password and enroll its authenticator. - [x] Add CSRF protection to all state-changing forms. - [x] Add automated database backups and test restoration. -- [x] Configure TLS reverse proxy and production domain for `eternityproject.fi`. \ No newline at end of file +- [x] Configure TLS reverse proxy and production domain for `eternityproject.fi`. +- [x] Add validated post image uploads with an in-article preview and click-to-expand view. \ No newline at end of file diff --git a/__pycache__/app.cpython-314.pyc b/__pycache__/app.cpython-314.pyc index 916674dd7b3c6209b2ff2ddba1d1fa0e56b0d1ce..cf65d7291efd826ac6141c8b690d844e684e04d9 100644 GIT binary patch delta 9215 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 delta 6809 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") +def uploaded_image(filename): + return send_from_directory(app.config["UPLOAD_FOLDER"], filename) + + @app.route("/register", methods=("GET", "POST")) def register(): if request.method == "POST": @@ -423,10 +458,11 @@ def write(): if request.method == "POST": try: title, slug, excerpt, body, category, now = post_from_request() + image_filename = save_post_image() get_db().execute( - """INSERT INTO posts (title, slug, excerpt, body, category, author_id, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", - (title, slug, excerpt, body, category, session["user_id"], now, now), + """INSERT INTO posts (title, slug, excerpt, body, category, image_filename, author_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (title, slug, excerpt, body, category, image_filename, session["user_id"], now, now), ) get_db().commit() return redirect(url_for("post", slug=slug)) @@ -446,11 +482,14 @@ def edit_post(slug): if request.method == "POST": try: title, new_slug, excerpt, body, category, now = post_from_request(article["id"]) + image_filename = save_post_image() or article["image_filename"] get_db().execute( - """UPDATE posts SET title=?, slug=?, excerpt=?, body=?, category=?, updated_at=? WHERE id=?""", - (title, new_slug, excerpt, body, category, now, article["id"]), + """UPDATE posts SET title=?, slug=?, excerpt=?, body=?, category=?, image_filename=?, updated_at=? WHERE id=?""", + (title, new_slug, excerpt, body, category, image_filename, now, article["id"]), ) get_db().commit() + if image_filename != article["image_filename"] and article["image_filename"]: + (app.config["UPLOAD_FOLDER"] / article["image_filename"]).unlink(missing_ok=True) return redirect(url_for("post", slug=new_slug)) except ValueError as error: flash(str(error), "error") diff --git a/data/eternity.db b/data/eternity.db index 7a98f86cc1cbe59f475320418e5d234dd8932653..04115fa1fe95dce3e1bed76b82156f8a1afd3db8 100644 GIT binary patch delta 119 zcmZozz|^pSX@ayMI|Bm)ClJE`+e95>S#}1!E)ibtj|?1aa~L?v`Ooq`Vw=OeolAy$ z8Gq5nMg=ym<_H#cadC0RcK*%3xhok(6f$!Y(^KQqGILV%5_3}(LR=$4CKvO%Y%b@h QVrJnpVLZ0^D>ruw01{gsng9R* delta 79 zcmZozz|^pSX@ayM8v_FaClJE`>qH%6SvCf}E)iZn69x{Bat4la{
{{ 'EDIT TRANSMISSION' if post else 'NEW TRANSMISSION' }}AUTHOR / {{ current_user.username }}
-
+
+
diff --git a/templates/post.html b/templates/post.html index a590174..514a842 100644 --- a/templates/post.html +++ b/templates/post.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% block title %}{{ post.title }} | Eternity Project{% endblock %} {% block content %} -

{{ post.title }}

{{ post.excerpt }}

{{ post.body }}
{% if current_user and current_user.id == post.author_id %}Edit post{% endif %}
+

{{ post.title }}

{{ post.excerpt }}

{% if post.image_filename %}Full-size image for {{ post.title }}{% endif %}
{{ post.body }}
{% if current_user and current_user.id == post.author_id %}Edit post{% endif %}
+{% if post.image_filename %}{% endif %} {% endblock %}