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
Some checks failed
Docker Build (PR) / Build Docker image (pull_request) Has been cancelled
This commit is contained in:
41
app/main.py
41
app/main.py
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
main.py
5
main.py
@@ -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)
|
||||||
|
|||||||
@@ -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
24
uv.lock
generated
@@ -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" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user