From 5e37b57c9bd478fc30642e5ea33965aa0385622b Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Fri, 10 Oct 2025 09:53:49 +0200 Subject: [PATCH] Feat: update form, allow some fields to be optional; add dauerapp check; get mail setup working using env variables --- app/main.py | 41 ++++++++++++++++++++++++++++++++--------- app/static/styles.css | 5 +++++ app/templates/form.html | 17 ++++++++++------- main.py | 5 ++--- pyproject.toml | 1 + uv.lock | 24 +++++++++++++++++++++++- 6 files changed, 73 insertions(+), 20 deletions(-) diff --git a/app/main.py b/app/main.py index 8930e82..f339833 100755 --- a/app/main.py +++ b/app/main.py @@ -2,13 +2,16 @@ import os import re import smtplib from email.mime.text import MIMEText +from typing import Optional from xml.etree.ElementTree import Element, SubElement, tostring +from dotenv import load_dotenv from fastapi import FastAPI, Form, HTTPException, Request, status from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates +load_dotenv() app = FastAPI() templates = Jinja2Templates(directory="app/templates") @@ -21,8 +24,15 @@ EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") # SMTP / email configuration via environment SMTP_HOST = os.getenv("SMTP_HOST", "smtp") SMTP_PORT = int(os.getenv("SMTP_PORT", "25")) -MAIL_FROM = os.getenv("MAIL_FROM", "noreply@example.com") +SENDER_MAIL = os.getenv("SENDER_MAIL", "noreply@example.com") MAIL_TO = os.getenv("MAIL_TO", "destination@example.com") +MAIL_USERNAME = os.getenv("MAIL_USERNAME") +MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") + +print( + f"Using SMTP server {SMTP_HOST}:{SMTP_PORT} with user {MAIL_USERNAME}" + f" to send from {SENDER_MAIL} to {MAIL_TO}" +) @app.get("/", response_class=HTMLResponse) @@ -35,17 +45,18 @@ async def handle_form( request: Request, name: str = Form(...), lastname: str = Form(...), - title: str = Form(...), + title: Optional[str] = Form(None), telno: str = Form(...), mail: str = Form(...), apparatsname: str = Form(...), subject: str = Form(...), semester_type: str = Form(...), # "summer" or "winter" semester_year: str = Form(...), + dauerapparat: bool = Form(False), authorname: list[str] = Form(...), year: list[str] = Form(...), booktitle: list[str] = Form(...), - signature: list[str] = Form(...), + signature: Optional[list[str]] = Form(None), ): # Build XML root = Element("form_submission") @@ -53,12 +64,14 @@ async def handle_form( static_data = SubElement(root, "static") SubElement(static_data, "name").text = name SubElement(static_data, "lastname").text = lastname - SubElement(static_data, "title").text = title + # Title is optional + SubElement(static_data, "title").text = title or "" SubElement(static_data, "telno").text = telno SubElement(static_data, "mail").text = mail SubElement(static_data, "apparatsname").text = apparatsname SubElement(static_data, "subject").text = subject SubElement(static_data, "semester").text = f"{semester_type} {semester_year}" + SubElement(static_data, "dauerapparat").text = "true" if dauerapparat else "false" # inside handle_form(), right after parameters are received, before building XML: # Basic email validation (server-side) if not EMAIL_REGEX.match(mail): @@ -72,17 +85,27 @@ async def handle_form( SubElement(book, "authorname").text = authorname[i] SubElement(book, "year").text = year[i] SubElement(book, "title").text = booktitle[i] - SubElement(book, "signature").text = signature[i] + # Signature is optional; write empty string if not provided + sig_value = "" + if signature and i < len(signature): + sig_raw = signature[i] or "" + sig_value = sig_raw.strip() + SubElement(book, "signature").text = sig_value xml_data = tostring(root, encoding="unicode") # Send mail msg = MIMEText(xml_data, "xml") - msg["Subject"] = "New Form Submission" - msg["From"] = MAIL_FROM + msg["Subject"] = "Antrag für neuen Semesterapparat" + msg["From"] = SENDER_MAIL msg["To"] = MAIL_TO - with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: - server.send_message(msg) + with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: + server.connect(SMTP_HOST, SMTP_PORT) + # Login only if credentials are provided + if MAIL_USERNAME and MAIL_PASSWORD: + server.login(MAIL_USERNAME, MAIL_PASSWORD) + + server.send_message(msg, from_addr=SENDER_MAIL, to_addrs=[MAIL_TO]) return RedirectResponse("/", status_code=303) diff --git a/app/static/styles.css b/app/static/styles.css index e519780..7832fa7 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -53,6 +53,7 @@ body { display: flex; align-items: center; gap: 14px; + height: 72px; padding: 14px 0; justify-content: space-between; } @@ -273,6 +274,10 @@ input[type="radio"] { accent-color: var(--control-accent); } justify-content: center; text-align: center; } +.inline-controls.start { + justify-content: flex-start; + text-align: left; +} .inline-controls input[type="number"] { width: 120px; text-align: center; diff --git a/app/templates/form.html b/app/templates/form.html index ad0ecd2..16cee9e 100755 --- a/app/templates/form.html +++ b/app/templates/form.html @@ -38,8 +38,8 @@
- - + +
@@ -89,23 +89,26 @@
- - + +
+
+ +

Medien

- + - +
AutorennameJahr/AuflageTitelSignaturAutorennameJahr/AuflageTitelSignatur (wenn vorhanden)
@@ -136,7 +139,7 @@ - + `; } diff --git a/main.py b/main.py index f0d68d3..c9cb8f5 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,5 @@ -from app.main import app - if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=5001) + # Run the FastAPI app defined in app/main.py as variable `app` + uvicorn.run("app.main:app", host="0.0.0.0", port=5001, reload=False) diff --git a/pyproject.toml b/pyproject.toml index ac0b2c7..1c4ae5f 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "fastapi>=0.117.1", "jinja2>=3.1.6", "pip>=25.2", + "python-dotenv>=1.1.1", "python-multipart>=0.0.20", "uvicorn>=0.37.0", ] diff --git a/uv.lock b/uv.lock index 45e4604..7600653 100755 --- a/uv.lock +++ b/uv.lock @@ -116,6 +116,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] +[[package]] +name = "pip" +version = "25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557 }, +] + [[package]] name = "pydantic" version = "2.11.9" @@ -159,6 +168,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, ] +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -170,11 +188,13 @@ wheels = [ [[package]] name = "semapform" -version = "0.1.0" +version = "0.1.2" source = { virtual = "." } dependencies = [ { name = "fastapi" }, { name = "jinja2" }, + { name = "pip" }, + { name = "python-dotenv" }, { name = "python-multipart" }, { name = "uvicorn" }, ] @@ -183,6 +203,8 @@ dependencies = [ requires-dist = [ { name = "fastapi", specifier = ">=0.117.1" }, { name = "jinja2", specifier = ">=3.1.6" }, + { name = "pip", specifier = ">=25.2" }, + { name = "python-dotenv", specifier = ">=1.1.1" }, { name = "python-multipart", specifier = ">=0.0.20" }, { name = "uvicorn", specifier = ">=0.37.0" }, ]