diff --git a/frontend/src/main.less b/frontend/src/main.less index 331d6ede0dbbc3e23f931292c08cc9bb6292236e..89c0e5389c3cf04c199ab00d8964d84ca7b59c06 100644 --- a/frontend/src/main.less +++ b/frontend/src/main.less @@ -128,7 +128,7 @@ main { } .preview { - width: 95vw; + width: min(95vw, 100rem); /* A4 aspect ratio: √2:1 */ aspect-ratio: 1.4142135623730951; @@ -143,3 +143,68 @@ main { .preview-label { place-self: center; } + +form { + display: inline-block; +} + +@label-padding: 90px; + +label.for-text { + display: inline-block; + min-width: @label-padding; + text-align: right; +} + +input[type="text"], textarea { + width: 300px; +} + +textarea { + height: 5em; + vertical-align: top; +} + +fieldset { + display: flex; + flex-wrap: wrap; +} + +fieldset > div { + display: flex; + gap: 0.5rem; + flex-direction: column; +} + +.imageselect > div { + width: 10rem; + display: flex; + gap: 0.5rem; +} + +fieldset label { + flex-grow: 1; +} + +.imageselect img { + width: 100%; + background-color: #fff; +} + +fieldset input { + display: none; +} + +fieldset img, +fieldset iframe, +.preview-small { + border: 3px solid lightgray; + border-radius: 10px; +} + +input[type="radio"]:checked { + & + label > img, + & + iframe { + border:3px solid red; + } +} diff --git a/pdm.lock b/pdm.lock index e3ff23dde2226475d84796e9a83b8f5e683edfc4..0a80dc942e57abdc36bdb4f5399d2475c8830a99 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:4254d9a7fba04b352e6ddf4d85e4ce62abbf60c4d39127493dd18cb4b4dbc110" +content_hash = "sha256:618addcae7a6ab39fb74f18ca29c1a64f762aea504459d0db220bc7e886efe59" [[metadata.targets]] requires_python = ">=3.12" @@ -1304,21 +1304,6 @@ files = [ {file = "wtforms-3.1.2.tar.gz", hash = "sha256:f8d76180d7239c94c6322f7990ae1216dae3659b7aa1cee94b6318bdffb474b9"}, ] -[[package]] -name = "wtforms-sqlalchemy" -version = "0.4.1" -requires_python = ">=3.8" -summary = "SQLAlchemy tools for WTForms" -groups = ["default"] -dependencies = [ - "SQLAlchemy>=1.4", - "WTForms>=3.1", -] -files = [ - {file = "WTForms-SQLAlchemy-0.4.1.tar.gz", hash = "sha256:370f52b738527cf6d8ab78d3488afb9342666144da2637e0cf8b5ae522e8a4db"}, - {file = "WTForms_SQLAlchemy-0.4.1-py3-none-any.whl", hash = "sha256:df3965015b60de172f4b35e691a47d9913d56bb0abb62c620edd04c1376979e3"}, -] - [[package]] name = "zopfli" version = "0.2.3" diff --git a/pyproject.toml b/pyproject.toml index d42a50e688a88a5d8f4a1b9b961bc0e259786069..a325737ae3b34ea2ab9bd0af60dce42b99a75949 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ "flask", "flask-sqlalchemy", "flask-wtf", - "wtforms-sqlalchemy", "uuid7", "psycopg", "flask-weasyprint", diff --git a/schilder2000/instance.py b/schilder2000/instance.py index adef3a19e93bfff48be7c9340a74ff94a31734d6..d869e36189cc8e98a29662e169dc494bb9dc837f 100644 --- a/schilder2000/instance.py +++ b/schilder2000/instance.py @@ -2,6 +2,8 @@ from . import db from .models import Schild from .helpers import Blueprint, get_template_attribute +from collections import namedtuple +from pathlib import Path from flask import url_for @@ -19,7 +21,6 @@ bp = Blueprint( @bp.route("/schild/<ident>.html") def schild_html(ident): schild = db.get_or_404(Schild, ident) - # raise Exception return bp.render_template(schild.template, schild=schild) @@ -38,13 +39,16 @@ def sample_pdf(template): return render_pdf(url_for(".sample_html", template=template)) +Template = namedtuple("Template", "name description") + + def list_templates(): schild = Schild() loader = bp.jinja_loader for t in loader.list_templates(): if t.startswith("_"): continue - yield dict( + yield Template( name=t, description=get_template_attribute( bp.real_template_name(t), @@ -53,3 +57,9 @@ def list_templates(): default=None, ), ) + + +def list_images(): + base = Path(bp.static_folder) / "img" + for f in base.iterdir(): + yield f.name diff --git a/schilder2000/models.py b/schilder2000/models.py index 2c82aa503573a388dd5d1c886ad275562260d194..322f0fd3a225dd083fa2e1e13ed6c642c2c54015 100644 --- a/schilder2000/models.py +++ b/schilder2000/models.py @@ -5,6 +5,10 @@ from uuid_extensions import uuid7 from sqlalchemy import String, Text, Uuid from sqlalchemy.orm import Mapped, mapped_column +from flask_wtf import FlaskForm + +from wtforms import fields, validators + from . import db @@ -14,3 +18,25 @@ class Schild(db.Model): text: Mapped[str] = mapped_column(Text) image: Mapped[str] = mapped_column(String(255), nullable=True) template: Mapped[str] = mapped_column(String(255)) + + +class SchildForm(FlaskForm): + title = fields.StringField( + "Titel", + validators=[ + validators.InputRequired(), + validators.Length(max=Schild.title.type.length), + ], + ) + text = fields.TextAreaField( + "Text", + validators=[validators.InputRequired()], + ) + image = fields.RadioField( + "Bild", + validators=[validators.Optional()], + ) + template = fields.RadioField( + "Vorlage", + validators=[validators.DataRequired()], + ) diff --git a/schilder2000/templates/schild.html.j2 b/schilder2000/templates/schild.html.j2 index ceea6134c00880dc6b23d3d4d4416a8cee011692..c6a1008f84876fe739894af2e1b95216fc11eb96 100644 --- a/schilder2000/templates/schild.html.j2 +++ b/schilder2000/templates/schild.html.j2 @@ -5,7 +5,7 @@ {{ field }} {%- else -%} <div class="box"> - {{ field.label }} + {{ field.label(class="for-text") }} {{ field(class="input-dispatch") }} </div> {%- endif -%} @@ -21,16 +21,57 @@ {% block main -%} <form method="post" action=""> - {%- for field in form -%} - {{ render_field(field) }} - {%- endfor -%} - <select name="template"> - {%- for t in templates -%} - <option value="{{ t.name }}"> - {{ t.description or t.name }} - </option> + {{ form.csrf_token }} + + {{ render_field(form.title) }} + {{ render_field(form.text) }} + + <fieldset class="templateselect"> + <legend>Vorlage</legend> + {%- for t in form.template.choices -%} + <div> + <input + type="radio" + name="template" + id="template:{{ t.name }}" + value="{{ t.name }}" + {% if t.name == (schild | default(None)).template -%} + checked + {%- endif -%} + /> + <iframe + class="preview preview-small" + src="{{ url_for('instance.sample_html', template=t.name) }}" + id="template-preview:{{ t.name }}" + > + </iframe> + <label for="template:{{ t.name }}" class="preview-label"> + {{ t.description or t.name }} + </label> + </div> {% endfor %} - </select> + </fieldset> + + <fieldset class="imageselect"> + <legend>Bild</legend> + {%- for img in form.image.choices -%} + <div> + <input + type="radio" + name="image" + id="img:{{ img }}" + value="{{ img }}" + {% if img == (schild | default(None)).image -%} + checked + {%- endif -%} + /> + <label for="img:{{ img }}"> + <img src="{{ url_for('instance.static', filename='img/'+img) }}" title="{{ img }}" /> + </label> + </div> + {%- endfor -%} + </fieldset> + <div class="box"> {%- if schild -%} <input type="submit" value="Speichern" /> @@ -44,6 +85,7 @@ {%- endif -%} </div> </form> + {%- if schild %} <div> <a href="{{ url_for('instance.schild_pdf', ident=schild.ident) }}"> @@ -56,7 +98,8 @@ <iframe class="preview" id="preview" - src="{{ url_for('instance.schild_html', ident=schild.ident) }}" /> + src="{{ url_for('instance.schild_html', ident=schild.ident) }}"> + </iframe> </div> </div> {% endif %} diff --git a/schilder2000/views.py b/schilder2000/views.py index 953afa3b75507e1ae6dc82e3d89da1c9787548ca..935e4d9f276c9ed7f7be30eb74a933dc54017b73 100644 --- a/schilder2000/views.py +++ b/schilder2000/views.py @@ -1,49 +1,49 @@ from . import db -from .instance import list_templates -from .models import Schild +from .instance import list_templates, list_images +from .models import Schild, SchildForm from flask import Blueprint, render_template, request, redirect, url_for -from flask_wtf import FlaskForm - -from wtforms_sqlalchemy.orm import model_form bp = Blueprint("views", __name__) -SchildForm = model_form(Schild, base_class=FlaskForm) - - @bp.route("/") def index(): schilder = db.session.execute(db.select(Schild)).scalars() return render_template("index.html.j2", schilder=schilder) +def _do_schild_form(schild): + form = SchildForm(request.form, obj=schild) + form.image.choices = list(list_images()) + form.template.choices = list(list_templates()) + if request.method == "POST" and form.validate(): + form.populate_obj(schild) + db.session.add(schild) + db.session.commit() + return form + + @bp.route("/schild/<ident>", methods=["GET", "POST"]) def schild(ident): schild = db.get_or_404(Schild, ident) - if request.method == "POST": - form = SchildForm(request.form, obj=schild) - if form.validate(): - form.populate_obj(schild) - db.session.add(schild) - db.session.commit() - else: - form = SchildForm(obj=schild) - return render_template("schild.html.j2", schild=schild, form=form, templates=list_templates()) + form = _do_schild_form(schild) + return render_template( + "schild.html.j2", + schild=schild, + form=form, + ) @bp.route("/create", methods=["GET", "POST"]) def create(): schild = Schild() + form = _do_schild_form(schild) if request.method == "POST": - form = SchildForm(request.form, obj=schild) - if form.validate(): - form.populate_obj(schild) - db.session.add(schild) - db.session.commit() - return redirect(url_for(".schild", ident=schild.ident)) + return redirect(url_for(".schild", ident=schild.ident)) else: - form = SchildForm(obj=schild) - return render_template("schild.html.j2", form=form) + return render_template( + "schild.html.j2", + form=form, + )