added support for cover images to be added to Werk entries
This commit is contained in:
parent
49165a1f56
commit
cfca623f93
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 %}
|
||||
|
||||
71
the_works/views/titelbild.py
Normal file
71
the_works/views/titelbild.py
Normal 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
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user