Feat: update form, allow some fields to be optional; add dauerapp check; get mail setup working using env variables
Some checks failed
Docker Build (PR) / Build Docker image (pull_request) Has been cancelled

This commit is contained in:
2025-10-10 09:53:49 +02:00
parent ea9c13ae91
commit 5e37b57c9b
6 changed files with 73 additions and 20 deletions

View File

@@ -2,13 +2,16 @@ import os
import re import re
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from typing import Optional
from xml.etree.ElementTree import Element, SubElement, tostring from xml.etree.ElementTree import Element, SubElement, tostring
from dotenv import load_dotenv
from fastapi import FastAPI, Form, HTTPException, Request, status from fastapi import FastAPI, Form, HTTPException, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
load_dotenv()
app = FastAPI() app = FastAPI()
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@@ -21,8 +24,15 @@ EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
# SMTP / email configuration via environment # SMTP / email configuration via environment
SMTP_HOST = os.getenv("SMTP_HOST", "smtp") SMTP_HOST = os.getenv("SMTP_HOST", "smtp")
SMTP_PORT = int(os.getenv("SMTP_PORT", "25")) 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_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) @app.get("/", response_class=HTMLResponse)
@@ -35,17 +45,18 @@ async def handle_form(
request: Request, request: Request,
name: str = Form(...), name: str = Form(...),
lastname: str = Form(...), lastname: str = Form(...),
title: str = Form(...), title: Optional[str] = Form(None),
telno: str = Form(...), telno: str = Form(...),
mail: str = Form(...), mail: str = Form(...),
apparatsname: str = Form(...), apparatsname: str = Form(...),
subject: str = Form(...), subject: str = Form(...),
semester_type: str = Form(...), # "summer" or "winter" semester_type: str = Form(...), # "summer" or "winter"
semester_year: str = Form(...), semester_year: str = Form(...),
dauerapparat: bool = Form(False),
authorname: list[str] = Form(...), authorname: list[str] = Form(...),
year: list[str] = Form(...), year: list[str] = Form(...),
booktitle: list[str] = Form(...), booktitle: list[str] = Form(...),
signature: list[str] = Form(...), signature: Optional[list[str]] = Form(None),
): ):
# Build XML # Build XML
root = Element("form_submission") root = Element("form_submission")
@@ -53,12 +64,14 @@ async def handle_form(
static_data = SubElement(root, "static") static_data = SubElement(root, "static")
SubElement(static_data, "name").text = name SubElement(static_data, "name").text = name
SubElement(static_data, "lastname").text = lastname 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, "telno").text = telno
SubElement(static_data, "mail").text = mail SubElement(static_data, "mail").text = mail
SubElement(static_data, "apparatsname").text = apparatsname SubElement(static_data, "apparatsname").text = apparatsname
SubElement(static_data, "subject").text = subject SubElement(static_data, "subject").text = subject
SubElement(static_data, "semester").text = f"{semester_type} {semester_year}" 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: # inside handle_form(), right after parameters are received, before building XML:
# Basic email validation (server-side) # Basic email validation (server-side)
if not EMAIL_REGEX.match(mail): if not EMAIL_REGEX.match(mail):
@@ -72,17 +85,27 @@ async def handle_form(
SubElement(book, "authorname").text = authorname[i] SubElement(book, "authorname").text = authorname[i]
SubElement(book, "year").text = year[i] SubElement(book, "year").text = year[i]
SubElement(book, "title").text = booktitle[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") xml_data = tostring(root, encoding="unicode")
# Send mail # Send mail
msg = MIMEText(xml_data, "xml") msg = MIMEText(xml_data, "xml")
msg["Subject"] = "New Form Submission" msg["Subject"] = "Antrag für neuen Semesterapparat"
msg["From"] = MAIL_FROM msg["From"] = SENDER_MAIL
msg["To"] = MAIL_TO msg["To"] = MAIL_TO
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server:
server.send_message(msg) 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) return RedirectResponse("/", status_code=303)

View File

@@ -53,6 +53,7 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 14px;
height: 72px;
padding: 14px 0; padding: 14px 0;
justify-content: space-between; justify-content: space-between;
} }
@@ -273,6 +274,10 @@ input[type="radio"] { accent-color: var(--control-accent); }
justify-content: center; justify-content: center;
text-align: center; text-align: center;
} }
.inline-controls.start {
justify-content: flex-start;
text-align: left;
}
.inline-controls input[type="number"] { .inline-controls input[type="number"] {
width: 120px; width: 120px;
text-align: center; text-align: center;

View File

@@ -38,8 +38,8 @@
<input type="text" name="lastname" id="lastname" required> <input type="text" name="lastname" id="lastname" required>
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="title">Titel</label> <label for="title">Titel (optional)</label>
<input type="text" name="title" id="title" required> <input type="text" name="title" id="title">
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="telno">Telefonnummer</label> <label for="telno">Telefonnummer</label>
@@ -89,23 +89,26 @@
<div class="form-field"> <div class="form-field">
<label>Semester</label> <label>Semester</label>
<div class="inline-controls"> <div class="inline-controls">
<label class="radio"><input type="radio" name="semester_type" value="summer" required>Sommer</label> <label class="radio"><input type="radio" name="semester_type" value="SoSe" required>Sommer</label>
<label class="radio"><input type="radio" name="semester_type" value="winter" required>Winter</label> <label class="radio"><input type="radio" name="semester_type" value="WiSe" required>Winter</label>
<input type="number" name="semester_year" placeholder="Jahr" required> <input type="number" name="semester_year" placeholder="Jahr" required>
</div> </div>
<div class="inline-controls start" style="margin-top: 0.5rem;">
<label class="checkbox"><input type="checkbox" name="dauerapparat" value="true"> Dauerapparat</label>
</div>
</div> </div>
</div> </div>
<h2>Medien</h2> <h2>Medien</h2>
<table id="book-table" class="data-table"> <table id="book-table" class="data-table">
<tr> <tr>
<th>Autorenname</th><th>Jahr/Auflage</th><th>Titel</th><th>Signatur</th> <th>Autorenname</th><th>Jahr/Auflage</th><th>Titel</th><th>Signatur (wenn vorhanden)</th>
</tr> </tr>
<tr> <tr>
<td><input type="text" name="authorname" required></td> <td><input type="text" name="authorname" required></td>
<td><input type="text" name="year" required></td> <td><input type="text" name="year" required></td>
<td><input type="text" name="booktitle" required></td> <td><input type="text" name="booktitle" required></td>
<td><input type="text" name="signature" required></td> <td><input type="text" name="signature"></td>
</tr> </tr>
</table> </table>
<button type="button" class="btn btn-secondary" onclick="addRow()">+ Medium hinzufügen</button> <button type="button" class="btn btn-secondary" onclick="addRow()">+ Medium hinzufügen</button>
@@ -136,7 +139,7 @@
<td><input type="text" name="authorname" required></td> <td><input type="text" name="authorname" required></td>
<td><input type="text" name="year" required></td> <td><input type="text" name="year" required></td>
<td><input type="text" name="booktitle" required></td> <td><input type="text" name="booktitle" required></td>
<td><input type="text" name="signature" required></td> <td><input type="text" name="signature"></td>
`; `;
} }

View File

@@ -1,6 +1,5 @@
from app.main import app
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn 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)

View File

@@ -8,6 +8,7 @@ dependencies = [
"fastapi>=0.117.1", "fastapi>=0.117.1",
"jinja2>=3.1.6", "jinja2>=3.1.6",
"pip>=25.2", "pip>=25.2",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20", "python-multipart>=0.0.20",
"uvicorn>=0.37.0", "uvicorn>=0.37.0",
] ]

24
uv.lock generated
View File

@@ -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 }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.11.9" 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 }, { 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]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.20" version = "0.0.20"
@@ -170,11 +188,13 @@ wheels = [
[[package]] [[package]]
name = "semapform" name = "semapform"
version = "0.1.0" version = "0.1.2"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "fastapi" }, { name = "fastapi" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "pip" },
{ name = "python-dotenv" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -183,6 +203,8 @@ dependencies = [
requires-dist = [ requires-dist = [
{ name = "fastapi", specifier = ">=0.117.1" }, { name = "fastapi", specifier = ">=0.117.1" },
{ name = "jinja2", specifier = ">=3.1.6" }, { 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 = "python-multipart", specifier = ">=0.0.20" },
{ name = "uvicorn", specifier = ">=0.37.0" }, { name = "uvicorn", specifier = ">=0.37.0" },
] ]