added support for cover images to be added to Werk entries

This commit is contained in:
eclipse 2025-05-24 08:55:08 +02:00
parent 49165a1f56
commit cfca623f93
5 changed files with 233 additions and 42 deletions

View File

@ -19,7 +19,7 @@ def create_app():
init_db(app)
# register blueprints
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung
from the_works.views import home, text, werk, verlag, sprache, textform, werksform, genre, pseudonym, reihe, herausgeber, veroeffentlichung, titelbild
app.register_blueprint(home.bp)
app.register_blueprint(text.bp)
app.register_blueprint(werk.bp)
@ -32,6 +32,7 @@ def create_app():
app.register_blueprint(reihe.bp)
app.register_blueprint(herausgeber.bp)
app.register_blueprint(veroeffentlichung.bp)
app.register_blueprint(titelbild.bp)
### DEBUG
toolbar = DebugToolbarExtension(app)

View File

@ -177,3 +177,38 @@ form:not([novalidate]) input:user-valid[type="search"] {
max-width: 400px;
}
#titelbild-pic {
width: 128px;
height: 128px;
border: var(--pico-border-width) solid var(--pico-table-border-color);
text-align: center;
img {
object-fit: contain;
height: 128px;
}
}
#titelbild-placeholder {
width: fit-content;
margin: auto;
svg {
height: 128px;
}
}
#titelbild-controls {
margin-top: var(--pico-form-element-spacing-vertical);
}
#titelbild-delete {
padding: calc(var(--pico-form-element-spacing-vertical) * .5) var(--pico-form-element-spacing-horizontal);
margin-bottom: var(--pico-form-element-spacing-vertical);
}
.display-none {
display: none;
}

View File

@ -21,11 +21,14 @@ Werk bearbeiten
{% endblock heading %}
{% block content %}
{% include "_icons.svg" %}
<form id="werk_detail_form" method="post" enctype="multipart/form-data" action="#">
<section>
<label>
<span class="required">Titel</span>
<input id="form_Titel" name="form_Titel" aria-label="Titel" placeholder="Titel" required value="{{ werk['Titel'] }}" />
<input type="text" id="form_Titel" name="form_Titel" aria-label="Titel" required value="{{ werk['Titel'] }}" />
</label>
</section>
<hr />
@ -33,7 +36,7 @@ Werk bearbeiten
<div>
<label>
Untertitel
<input id="form_Untertitel" name="form_Untertitel" aria-label="Untertitel" placeholder="Untertitel" value="{{ werk['Untertitel'] }}" />
<input id="form_Untertitel" name="form_Untertitel" aria-label="Untertitel" placeholder="kein Untertitel" value="{{ werk['Untertitel'] or '' }}" />
</label>
<label>
<span class="required">Werksform</span>
@ -51,7 +54,7 @@ Werk bearbeiten
</label>
<label>
Reihennummer
<input id="form_Reihennummer" name="form_Reihennummer" aria-label="Reihennummer" placeholder="keine Reihennummer" value="{{ werk['Reihennummer'] }}" />
<input id="form_Reihennummer" name="form_Reihennummer" aria-label="Reihennummer" placeholder="keine Reihennummer" value="{{ werk['Reihennummer'] or '' }}" />
</label>
<label>
Verlag
@ -62,29 +65,29 @@ Werk bearbeiten
</label>
<label>
Preis
<input id="form_Preis" name="form_Preis" aria-label="Preis" placeholder="kein Preis" value="{{ werk['Preis'] }}" />
<input id="form_Preis" name="form_Preis" aria-label="Preis" placeholder="kein Preis" value="{{ werk['Preis'] or '' }}" />
</label>
</div>
<div>
<label>
Erscheinungsdatum (TT-MM-JJJJ, MM-JJJJ, JJJJ oder leer)
<div class="grid">
<input type="number" min="1" max="31" id="form_Erscheinungstag" name="form_Erscheinungstag" aria-label="Erscheinungstag" placeholder="Tag" value="{{ werk['Erscheinungsdatum'][8:] }}" />
<input type="number" min="1" max="12" id="form_Erscheinungsmonat" name="form_Erscheinungsmonat" aria-label="Erscheinungsmonat" placeholder="Monat" value="{{ werk['Erscheinungsdatum'][5:7] }}" />
<input type="number" min="1980" max="2100" id="form_Erscheinungsjahr" name="form_Erscheinungsjahr" aria-label="Erscheinungsjahr" placeholder="Jahr" value="{{ werk['Erscheinungsdatum'][:4] }}" />
<input type="number" min="1" max="31" id="form_Erscheinungstag" name="form_Erscheinungstag" aria-label="Erscheinungstag" placeholder="Tag" value="{{ werk['Erscheinungsdatum'][8:] if werk['Erscheinungsdatum'] }}" />
<input type="number" min="1" max="12" id="form_Erscheinungsmonat" name="form_Erscheinungsmonat" aria-label="Erscheinungsmonat" placeholder="Monat" value="{{ werk['Erscheinungsdatum'][5:7] if werk['Erscheinungsdatum'] }}" />
<input type="number" min="1980" max="2100" id="form_Erscheinungsjahr" name="form_Erscheinungsjahr" aria-label="Erscheinungsjahr" placeholder="Jahr" value="{{ werk['Erscheinungsdatum'][:4] if werk['Erscheinungsdatum'] }}" />
</div>
</label>
<label>
ISBN-13
<input id="form_ISBN_13" name="form_ISBN_13" aria-label="ISBN-13" placeholder="keine ISBN-13" value="{{ werk['ISBN_13'] }}" />
<input id="form_ISBN_13" name="form_ISBN_13" aria-label="ISBN-13" placeholder="keine ISBN-13" value="{{ werk['ISBN_13'] or '' }}" />
</label>
<label>
ISBN-10
<input id="form_ISBN_10" name="form_ISBN_10" aria-label="ISBN-10" placeholder="keine ISBN-10" value="{{ werk['ISBN_10'] }}" />
<input id="form_ISBN_10" name="form_ISBN_10" aria-label="ISBN-10" placeholder="keine ISBN-10" value="{{ werk['ISBN_10'] or '' }}" />
</label>
<label>
ISSN
<input id="form_ISSN" name="form_ISSN" aria-label="ISSN" placeholder="keine ISSN" value="{{ werk['ISSN'] }}" />
<input id="form_ISSN" name="form_ISSN" aria-label="ISSN" placeholder="keine ISSN" value="{{ werk['ISSN'] or '' }}" />
</label>
<label>
Genre(s)
@ -130,19 +133,41 @@ Werk bearbeiten
<section class="grid">
<label>
Klappentext
<textarea id="form_Klappentext" name="form_Klappentext" aria-label="Klappentext" placeholder="kein Klappentext" rows="10">{{ werk['Klappentext'] }}</textarea>
<textarea id="form_Klappentext" name="form_Klappentext" aria-label="Klappentext" placeholder="kein Klappentext" rows="10">{{ werk['Klappentext'] or '' }}</textarea>
</label>
<label>
Anmerkungen
<textarea id="form_Anmerkungen" name="form_Anmerkungen" aria-label="Anmerkungen" placeholder="keine Anmerkungen" rows="10">
{{ werk['Anmerkungen'] }}</textarea>
{{ werk['Anmerkungen'] or '' }}</textarea>
</label>
</section>
<hr />
<section>
<label>
Titelbild
<input type="file" id="form_Titelbild" name="form_Titelbild" aria-label="Titelbild" placeholder="kein Titelbild" />
<div id="titelbild-wrapper">
<div id="titelbild-pic">
<a id="titelbild-link" href="{{ titelbild['Bild'] | default('') }}" title="zum Originalbild"{{ ' class="display-none"' | safe if not titelbild }}>
<img id="titelbild-thumbnail" alt="Thumbnail" src="{{ titelbild['Thumbnail'] | default('') }}" />
</a>
<div id="titelbild-placeholder"{{ ' class="display-none"' | safe if titelbild }}>
<svg viewbox="0 0 90 128">
<use href="#placeholder" />
</svg>
</div>
</div>
<div id="titelbild-controls">
<button id="titelbild-delete" class="contrast" type="button"{{ ' disabled' | safe if not titelbild }}>Bild löschen</button>
<input type="file" id="titelbild-upload" name="form_Titelbild" aria-label="Titelbild" placeholder="kein Titelbild" accept="image/*"/>
<input type="hidden" id="titelbild-haschanged" name="form_Titelbild_haschanged" value="" />
</div>
<div id="titelbild-info"{{ ' class="display-none"' | safe if not titelbild }}>
<small>
Dateigröße: <span id="titelbild-dateigroesse">{{ titelbild["Dateigroesse"] | default('') }}</span> Bytes<br />
Abmessungen: <span id="titelbild-breite">{{ titelbild["Breite"] | default('') }}</span>x<span id="titelbild-hoehe">{{ titelbild["Hoehe"] | default('') }}</span> px
</small>
</div>
</div>
</label>
</section>
@ -159,6 +184,68 @@ Werk bearbeiten
{% block script %}
<script src="{{ url_for('static', filename='the_works.js') }}"></script>
<script>
window.onload = () => initAllDropdownSelects();
function initFileInput(id_stub) {
document.getElementById(id_stub + "-upload").addEventListener("change", handleFileInputChange);
document.getElementById(id_stub + "-delete").addEventListener("click", handleFileDeleteClick);
}
function handleFileInputChange(event) {
console.dir(event);
let id_stub = event.target.id.split("-")[0];
// mark change
document.getElementById(id_stub + "-haschanged").value += "+";
// show placeholder and return if input element is empty
if ( ! event.target.files.length ) {
showOrHideThumbnail(id_stub, "hide");
return true;
}
// set img src to the uploaded file
let f = event.target.files[0];
let tn = document.getElementById(id_stub + "-thumbnail");
tn.src = URL.createObjectURL(f);
// add some data about the image
showOrHideThumbnail(id_stub, "show");
document.getElementById(id_stub + "-dateigroesse").innerText = f.size;
// event handler to remove image object from memory after it has loaded on the page
tn.onload = () => {
document.getElementById(id_stub + "-breite").innerText = tn.naturalWidth;
document.getElementById(id_stub + "-hoehe").innerText = tn.naturalHeight;
URL.revokeObjectURL(tn.src);
};
}
// event listener for delete element
function handleFileDeleteClick(event) {
console.dir(event);
let id_stub = event.target.id.split("-")[0];
// mark change
document.getElementById(id_stub + "-haschanged").value += "-";
document.getElementById(id_stub + "-upload").value = ""; // clear input element
showOrHideThumbnail(id_stub, "hide");
}
function showOrHideThumbnail(id_stub, mode) {
if ( mode && mode.toLowerCase() != "hide" ) {
document.getElementById(id_stub + "-link").classList.remove("display-none"); // show thumbnail
document.getElementById(id_stub + "-placeholder").classList.add("display-none"); // hide placeholder
document.getElementById(id_stub + "-info").classList.remove("display-none"); // show info section
document.getElementById(id_stub + "-delete").removeAttribute("disabled"); // enable delete link
} else {
document.getElementById(id_stub + "-link").classList.add("display-none"); // hide thumbnail element
document.getElementById(id_stub + "-placeholder").classList.remove("display-none"); // show plaeholder
document.getElementById(id_stub + "-info").classList.add("display-none"); // hide info section
document.getElementById(id_stub + "-delete").setAttribute("disabled", ""); // disable delete link
}
}
window.onload = () => {
initAllDropdownSelects();
initFileInput("titelbild");
}
</script>
{% endblock script %}

View File

@ -0,0 +1,71 @@
from flask import Blueprint, send_file, flash
from the_works.database import db
from the_works.models import Titelbild
from io import BytesIO
from werkzeug.utils import secure_filename
from PIL import Image
import random
import sys
bp = Blueprint("titelbild", __name__)
@bp.route("/cover/<int:id>")
def image(id):
titelbild = db.session.get(Titelbild, id)
if titelbild and titelbild.Bild:
return send_file(BytesIO(titelbild.Bild), mimetype=titelbild.Mimetype)
else:
return "requested image data not found", 404
@bp.route("/cover/<int:id>/thumb")
def thumbnail(id):
titelbild = db.session.get(Titelbild, id)
if titelbild and titelbild.Thumbnail:
return send_file(BytesIO(titelbild.Thumbnail), mimetype=titelbild.Mimetype)
else:
return "requested image data not found", 404
def create(f):
if f.filename == "":
raise TypeError("FileStorage object expected")
filesize = sys.getsizeof(f.read())
f.seek(0)
# Image.open() has a filename input requirement; BytesIO as a wrapper emulates the filename
with BytesIO(f.read()) as bytes_like:
# open image in memory
img = Image.open(bytes_like)
format = img.format
# prepare values for database entry
titelbild = Titelbild(
Mimetype = img.get_format_mimetype(),
Dateiname = secure_filename(f.filename),
Dateigroesse = filesize,
Breite = img.width,
Hoehe = img.height,
Bild = bytes_like.getvalue()
)
# add thumbnail to table entry
img.thumbnail([128, 128])
t = BytesIO()
img.save(t, format=format)
titelbild.Thumbnail = t.getvalue()
# add record to DB
db.session.add(titelbild)
db.session.flush()
id = titelbild.ID
db.session.commit()
# return assigned ID
flash("Bilddatei erfolgreich hochgeladen")
return id
# def delete(id):
# titelbild = db.session.get(Titelbild, id)
# db.session.delete(titelbild)
# db.session.commit()
# flash("Bilddatei erfolgreich gelöscht")
# return

View File

@ -1,7 +1,8 @@
from flask import Blueprint, render_template, request, redirect, flash, url_for
from sqlalchemy import select, insert, update, delete
from the_works.database import db
from the_works.models import Werk, Reihe, Verlag, Werksform, Werk_Genre, Genre, Werk_Herausgeber, Herausgeber
from the_works.models import Werk, Reihe, Verlag, Werksform, Werk_Genre, Genre, Werk_Herausgeber, Herausgeber, Titelbild
from the_works.views import titelbild as cover
bp = Blueprint("werk", __name__)
@ -26,7 +27,8 @@ def all():
"ISBN_10": row.Werk.ISBN_10 or "",
"ISSN": row.Werk.ISSN or "",
"Preis": row.Werk.Preis or "",
"Titelbild": True if row.Werk.Titelbild else False,
# Titelbild: return URL for the thumbnail
"Titelbild": url_for("titelbild.thumbnail", id=row.Werk.Titelbild) if row.Werk.Titelbild else "",
"Klappentext": row.Werk.Klappentext or "",
"Anmerkungen": row.Werk.Anmerkungen or "",
"Herausgeber_list": [wh.herausgeber.Name for wh in row.Werk.werk_herausgeber],
@ -38,29 +40,20 @@ def all():
def read(id):
# id of zero -> return empty data
if id == 0:
return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)))
# all other ids -> update existing entry
w = db.session.get(Werk, id)
werk = {
"ID": w.ID,
"Titel": w.Titel,
"Untertitel": w.Untertitel or "",
"Werksform": w.Werksform or "",
"Verlag": w.Verlag or "",
"Reihe": w.Reihe or "",
"Reihennummer": w.Reihennummer or "",
"Erscheinungsdatum": w.Erscheinungsdatum or "",
"ISBN_13": w.ISBN_13 or "",
"ISBN_10": w.ISBN_10 or "",
"ISSN": w.ISSN or "",
"Preis": w.Preis or "",
"Titelbild": "",
"Klappentext": w.Klappentext or "",
"Anmerkungen": w.Anmerkungen or "",
"Herausgeber": w.herausgeber,
"Genres": w.genres
}
return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)))
return render_template("views/werk_detail.html", werk={"ID": 0, "Erscheinungsdatum": ""}, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbild=None)
# all other ids -> read existing entry from DB and return as dict
werk = db.session.get(Werk, id)
if not werk:
return "Werk not found", 404
werk = werk._asdict()
# prepare Titelbild as dict including URLs for thumbnail and full pic
if werk["Titelbild"]:
titelbild = db.session.get(Titelbild, werk["Titelbild"])._asdict()
titelbild["Bild"] = url_for("titelbild.image", id=titelbild["ID"])
titelbild["Thumbnail"] = url_for("titelbild.thumbnail", id=titelbild["ID"])
else:
titelbild = None
return render_template("views/werk_detail.html", werk=werk, reihen=db.session.scalars(select(Reihe)), verlage=db.session.scalars(select(Verlag)), werksformen=db.session.scalars(select(Werksform)), genres=db.session.scalars(select(Genre)), hrsg=db.session.scalars(select(Herausgeber)), titelbild=titelbild)
@bp.route("/werk/create", methods=["POST"])
def create():
@ -76,7 +69,7 @@ def create():
ISBN_10 = request.form["form_ISBN_10"] or None,
ISSN = request.form["form_ISSN"] or None,
Preis = request.form["form_Preis"] or None,
Titelbild = None,
Titelbild = cover.create(request.files["form_Titelbild"]) if request.files["form_Titelbild"].filename else None,
Klappentext = request.form["form_Klappentext"] or None,
Anmerkungen = request.form["form_Anmerkungen"] or None
)
@ -89,6 +82,7 @@ def create():
flash("Eintrag erfolgreich hinzugefügt")
return redirect(url_for("werk.all"))
@bp.route("/werk/update/<int:id>", methods=["POST"])
def update(id):
# get record
@ -106,10 +100,13 @@ def update(id):
werk.ISBN_10 = request.form["form_ISBN_10"] or None
werk.ISSN = request.form["form_ISSN"] or None
werk.Preis = request.form["form_Preis"] or None
werk.Titelbild = None
werk.Klappentext = request.form["form_Klappentext"] or None
werk.Anmerkungen = request.form["form_Anmerkungen"] or None
# update Titelbild
if request.form["form_Titelbild_haschanged"]:
werk.Titelbild = cover.create(request.files["form_Titelbild"]) if request.files["form_Titelbild"].filename else None
# update associated values: Genre
form_set = set(map(lambda g: int(g), request.form.getlist("form_Genre")))
for g in set(werk.genres) - form_set: