Merge pull request 'feat: add elsa form with a modular addition of content type' (#15) from dev into main

Reviewed-on: #15
This commit was merged in pull request #15.
This commit is contained in:
2025-11-19 13:27:58 +00:00
4 changed files with 681 additions and 5 deletions

View File

@@ -38,6 +38,12 @@ async def semesterapparat_form(request: Request):
return templates.TemplateResponse("semesterapparat_form.html", {"request": request})
@app.get("/elsa", response_class=HTMLResponse)
async def elsa_form(request: Request):
"""ELSA form page"""
return templates.TemplateResponse("elsa_mono_form.html", {"request": request})
@app.post("/submit")
async def handle_form(
request: Request,
@@ -107,3 +113,236 @@ async def handle_form(
print("=" * 80)
return RedirectResponse("/?success=true", status_code=303)
@app.post("/elsa/submit")
async def handle_elsa_form(request: Request):
"""Handle ELSA form submission with multiple media types"""
form_data = await request.form()
# Extract general information
name = str(form_data.get("name", ""))
lastname = str(form_data.get("lastname", ""))
title_field = str(form_data.get("title", ""))
mail = str(form_data.get("mail", ""))
subject = str(form_data.get("subject", ""))
classname = str(form_data.get("classname", ""))
usage_date_from = str(form_data.get("usage_date_from", ""))
usage_date_to = str(form_data.get("usage_date_to", ""))
availability_date = str(form_data.get("availability_date", ""))
message = str(form_data.get("message", ""))
# Basic email validation
if not EMAIL_REGEX.match(mail):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid email address format.",
)
# Build XML structure
root = Element("elsa_submission")
# General information
general_info = SubElement(root, "general_info")
SubElement(general_info, "name").text = name
SubElement(general_info, "lastname").text = lastname
SubElement(general_info, "title").text = title_field
SubElement(general_info, "mail").text = mail
SubElement(general_info, "subject").text = subject
SubElement(general_info, "classname").text = classname
SubElement(general_info, "usage_date_from").text = usage_date_from
SubElement(general_info, "usage_date_to").text = usage_date_to
SubElement(general_info, "availability_date").text = availability_date
if message:
SubElement(general_info, "message").text = message
# Process media sections
media_root = SubElement(root, "media")
# Process Monografie entries
if "monografie_author[]" in form_data:
monografie_authors = [str(v) for v in form_data.getlist("monografie_author[]")]
monografie_years = [str(v) for v in form_data.getlist("monografie_year[]")]
monografie_editions = [
str(v) for v in form_data.getlist("monografie_edition[]")
]
monografie_titles = [str(v) for v in form_data.getlist("monografie_title[]")]
monografie_signatures = [
str(v) for v in form_data.getlist("monografie_signature[]")
]
monografie_pages_from = [
str(v) for v in form_data.getlist("monografie_pages_from[]")
]
monografie_pages_to = [
str(v) for v in form_data.getlist("monografie_pages_to[]")
]
# Get section IDs from the form (assuming they're in data-section attributes)
# Since we can't directly access data-section from form_data, we'll process sequentially
monografie_section = SubElement(media_root, "monografien")
for i in range(len(monografie_authors)):
entry = SubElement(monografie_section, "entry")
SubElement(entry, "author").text = (
monografie_authors[i] if i < len(monografie_authors) else ""
)
SubElement(entry, "year").text = (
monografie_years[i] if i < len(monografie_years) else ""
)
SubElement(entry, "edition").text = (
monografie_editions[i] if i < len(monografie_editions) else ""
)
SubElement(entry, "title").text = (
monografie_titles[i] if i < len(monografie_titles) else ""
)
SubElement(entry, "signature").text = (
monografie_signatures[i] if i < len(monografie_signatures) else ""
)
SubElement(entry, "pages_from").text = (
monografie_pages_from[i] if i < len(monografie_pages_from) else ""
)
SubElement(entry, "pages_to").text = (
monografie_pages_to[i] if i < len(monografie_pages_to) else ""
)
# Process Zeitschriftenartikel entries
if "zeitschrift_author[]" in form_data:
zeitschrift_authors = [
str(v) for v in form_data.getlist("zeitschrift_author[]")
]
zeitschrift_years = [str(v) for v in form_data.getlist("zeitschrift_year[]")]
zeitschrift_volumes = [
str(v) for v in form_data.getlist("zeitschrift_volume[]")
]
zeitschrift_article_titles = [
str(v) for v in form_data.getlist("zeitschrift_article_title[]")
]
zeitschrift_journal_titles = [
str(v) for v in form_data.getlist("zeitschrift_journal_title[]")
]
zeitschrift_signatures = [
str(v) for v in form_data.getlist("zeitschrift_signature[]")
]
zeitschrift_pages_from = [
str(v) for v in form_data.getlist("zeitschrift_pages_from[]")
]
zeitschrift_pages_to = [
str(v) for v in form_data.getlist("zeitschrift_pages_to[]")
]
zeitschrift_section = SubElement(media_root, "zeitschriftenartikel")
for i in range(len(zeitschrift_authors)):
entry = SubElement(zeitschrift_section, "entry")
SubElement(entry, "author").text = (
zeitschrift_authors[i] if i < len(zeitschrift_authors) else ""
)
SubElement(entry, "year").text = (
zeitschrift_years[i] if i < len(zeitschrift_years) else ""
)
SubElement(entry, "volume").text = (
zeitschrift_volumes[i] if i < len(zeitschrift_volumes) else ""
)
SubElement(entry, "article_title").text = (
zeitschrift_article_titles[i]
if i < len(zeitschrift_article_titles)
else ""
)
SubElement(entry, "journal_title").text = (
zeitschrift_journal_titles[i]
if i < len(zeitschrift_journal_titles)
else ""
)
SubElement(entry, "signature").text = (
zeitschrift_signatures[i] if i < len(zeitschrift_signatures) else ""
)
SubElement(entry, "pages_from").text = (
zeitschrift_pages_from[i] if i < len(zeitschrift_pages_from) else ""
)
SubElement(entry, "pages_to").text = (
zeitschrift_pages_to[i] if i < len(zeitschrift_pages_to) else ""
)
# Process Herausgeberwerk entries
if "herausgeber_publisher[]" in form_data:
herausgeber_publishers = [
str(v) for v in form_data.getlist("herausgeber_publisher[]")
]
herausgeber_work_titles = [
str(v) for v in form_data.getlist("herausgeber_work_title[]")
]
herausgeber_years = [str(v) for v in form_data.getlist("herausgeber_year[]")]
herausgeber_editions = [
str(v) for v in form_data.getlist("herausgeber_edition[]")
]
herausgeber_article_authors = [
str(v) for v in form_data.getlist("herausgeber_article_author[]")
]
herausgeber_article_titles = [
str(v) for v in form_data.getlist("herausgeber_article_title[]")
]
herausgeber_signatures = [
str(v) for v in form_data.getlist("herausgeber_signature[]")
]
herausgeber_pages_from = [
str(v) for v in form_data.getlist("herausgeber_pages_from[]")
]
herausgeber_pages_to = [
str(v) for v in form_data.getlist("herausgeber_pages_to[]")
]
herausgeber_section = SubElement(media_root, "herausgeberwerke")
for i in range(len(herausgeber_publishers)):
entry = SubElement(herausgeber_section, "entry")
SubElement(entry, "publisher").text = (
herausgeber_publishers[i] if i < len(herausgeber_publishers) else ""
)
SubElement(entry, "work_title").text = (
herausgeber_work_titles[i] if i < len(herausgeber_work_titles) else ""
)
SubElement(entry, "year").text = (
herausgeber_years[i] if i < len(herausgeber_years) else ""
)
SubElement(entry, "edition").text = (
herausgeber_editions[i] if i < len(herausgeber_editions) else ""
)
SubElement(entry, "article_author").text = (
herausgeber_article_authors[i]
if i < len(herausgeber_article_authors)
else ""
)
SubElement(entry, "article_title").text = (
herausgeber_article_titles[i]
if i < len(herausgeber_article_titles)
else ""
)
SubElement(entry, "signature").text = (
herausgeber_signatures[i] if i < len(herausgeber_signatures) else ""
)
SubElement(entry, "pages_from").text = (
herausgeber_pages_from[i] if i < len(herausgeber_pages_from) else ""
)
SubElement(entry, "pages_to").text = (
herausgeber_pages_to[i] if i < len(herausgeber_pages_to) else ""
)
xml_data = tostring(root, encoding="unicode")
# Send or print email
msg = MIMEText(xml_data, "xml")
msg["Subject"] = "New ELSA Form Submission"
msg["From"] = MAIL_FROM
msg["To"] = MAIL_TO
if MAIL_ENABLED:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.send_message(msg)
else:
print("=" * 80)
print("MAIL SENDING DISABLED - Would have sent:")
print(f"From: {MAIL_FROM}")
print(f"To: {MAIL_TO}")
print(f"Subject: {msg['Subject']}")
print("-" * 80)
print(xml_data)
print("=" * 80)
return RedirectResponse("/?success=true", status_code=303)

View File

@@ -598,3 +598,130 @@ input[type="radio"] { accent-color: var(--control-accent); }
transform: translateY(0);
}
}
/* ELSA specific styles */
.legal-notice {
background: #eff6ff;
border: 1px solid #93c5fd;
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 24px;
}
[data-theme="dark"] .legal-notice {
background: #0f2942;
border-color: #1e3a52;
}
.legal-notice h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0 0 8px;
font-size: 1rem;
color: #1e40af;
}
[data-theme="dark"] .legal-notice h3 {
color: #93c5fd;
}
.legal-notice p {
margin: 0;
line-height: 1.6;
font-size: 0.95rem;
color: var(--text);
}
.media-controls {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.media-section {
background: var(--table-head-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
}
.media-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.media-section-header h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
font-size: 1.15rem;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--card-bg);
color: var(--text);
cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease;
}
.btn-icon:hover {
background: var(--table-head-bg);
color: #ef4444;
}
.btn-sm {
font-size: 0.9rem;
padding: 8px 14px;
}
.media-table {
margin-bottom: 12px;
}
.media-table th {
font-size: 0.85rem;
white-space: nowrap;
}
.media-table td {
padding: 8px 10px;
}
.media-table input[type="text"],
.media-table input[type="number"] {
font-size: 0.9rem;
padding: 8px 10px;
}
.media-table input[type="number"] {
width: 80px;
}
@media (max-width: 1024px) {
.media-controls {
flex-direction: column;
}
.media-table {
font-size: 0.85rem;
}
.media-table th,
.media-table td {
padding: 6px 8px;
}
}

View File

@@ -0,0 +1,310 @@
<!DOCTYPE html>
<html>
<head>
<title>ELSA - Elektronischer Semesterapparat</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/static/styles.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
</head>
<body>
<header class="site-header">
<div class="container header-inner">
<div class="header-left">
<img class="logo" src="https://www.ph-freiburg.de/_assets/cc3dd7db45300ecc1f3aeed85bda5532/Images/logo-big-blue.svg" alt="PH Freiburg Logo" />
<div class="brand">
<div class="brand-title">Bibliothek der Pädagogischen Hochschule Freiburg</div>
<div class="brand-sub">Hochschulbibliothek · ELSA</div>
</div>
</div>
<button type="button" id="theme-toggle" class="theme-toggle" aria-label="Switch to dark mode" title="Switch to dark mode">
<span class="mdi mdi-theme-light-dark" aria-hidden="true"></span>
</button>
</div>
</header>
<main class="container">
<section class="card">
<h1>ELSA - Elektronischer Semesterapparat</h1>
<div class="legal-notice">
<h3><span class="mdi mdi-information"></span> Rechtlicher Hinweis</h3>
<p>
Das Urheberrecht gestattet gemäß § 60a UrhG zur Veranschaulichung des Unterrichts die Bereitstellung elektronischer Kopien bis max. 15% von veröffentlichten Werken, von einzelnen Aufsätzen aus Fachzeitschriften und von Werken geringen Umfangs (max. 25 S. Gesamtumfang) für einen genau abgegrenzten Benutzerkreis wie den Teilnehmenden einer Lehrveranstaltung, die sich auf ILIAS angemeldet haben.
</p>
</div>
<form method="post" action="/elsa/submit" class="request-form">
<h2>Allgemeine Informationen</h2>
<div class="grid-form">
<div class="form-field">
<label for="name">Name</label>
<input type="text" name="name" id="name" required>
</div>
<div class="form-field">
<label for="lastname">Nachname</label>
<input type="text" name="lastname" id="lastname" required>
</div>
<div class="form-field">
<label for="title">Titel (optional)</label>
<input type="text" name="title" id="title">
</div>
<div class="form-field">
<label for="mail">Email</label>
<input type="email" name="mail" id="mail" required>
</div>
<div class="form-field">
<label for="subject">Fach</label>
<select name="subject" id="subject" required>
<option value="">-- Auswählen --</option>
<option>Biologie</option>
<option>Chemie</option>
<option>Deutsch</option>
<option>Englisch</option>
<option>Erziehungswirtschaft</option>
<option>Französisch</option>
<option>Geographie</option>
<option>Geschichte</option>
<option>Gesundheitspädagogik</option>
<option>Haushalt / Textil</option>
<option>Kunst</option>
<option>Mathematik / Informatik</option>
<option>Medien in der Bildung</option>
<option>Musik</option>
<option>Philosophie</option>
<option>Physik</option>
<option>Politikwissenschaft</option>
<option>Prorektorat Lehre und Studium</option>
<option>Psychologie</option>
<option>Soziologie</option>
<option>Sport</option>
<option>Technik</option>
<option>Theologie</option>
<option>Wirtschaftslehre</option>
</select>
</div>
<div class="form-field">
<label for="classname">Lehrveranstaltungsname</label>
<input type="text" name="classname" id="classname" required>
</div>
<div class="form-field">
<label for="usage_date_from">Nutzungszeitraum von</label>
<input type="date" name="usage_date_from" id="usage_date_from" required>
</div>
<div class="form-field">
<label for="usage_date_to">Nutzungszeitraum bis</label>
<input type="date" name="usage_date_to" id="usage_date_to" required>
</div>
<div class="form-field">
<label for="availability_date">Bereitstellungsdatum</label>
<input type="date" name="availability_date" id="availability_date" required>
</div>
</div>
<h2>Medien</h2>
<div class="media-controls">
<button type="button" class="btn btn-secondary" onclick="addMediaType('monografie')">
<span class="mdi mdi-book"></span> + Monografie
</button>
<button type="button" class="btn btn-secondary" onclick="addMediaType('zeitschriftenartikel')">
<span class="mdi mdi-newspaper"></span> + Zeitschriftenartikel
</button>
<button type="button" class="btn btn-secondary" onclick="addMediaType('herausgeberwerk')">
<span class="mdi mdi-book-multiple"></span> + Herausgeberwerk
</button>
</div>
<div id="media-sections"></div>
<div class="form-field" style="margin-top: 20px;">
<label for="message">Nachricht (optional)</label>
<textarea name="message" id="message" rows="4" placeholder="Zusätzliche Anmerkungen oder Hinweise..."></textarea>
</div>
<div class="actions">
<button type="submit" class="btn btn-primary">Absenden</button>
</div>
</form>
</section>
</main>
<script>
let mediaCounter = {
monografie: 0,
zeitschriftenartikel: 0,
herausgeberwerk: 0
};
let sectionCounter = 0;
// Theme toggle functionality (in IIFE to avoid polluting global scope)
(function() {
const STORAGE_KEY = 'theme';
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const saved = localStorage.getItem(STORAGE_KEY);
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem(STORAGE_KEY, theme);
const btn = document.getElementById('theme-toggle');
if (btn) {
const label = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
btn.setAttribute('aria-label', label);
btn.title = label;
}
}
setTheme(saved || (prefersDark ? 'dark' : 'light'));
const btn = document.getElementById('theme-toggle');
if (btn) {
btn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
setTheme(current === 'dark' ? 'light' : 'dark');
});
}
})();
// Media management functions (global scope for onclick handlers)
function addMediaType(type) {
const container = document.getElementById('media-sections');
const sectionId = 'section-' + sectionCounter++;
const section = document.createElement('div');
section.className = 'media-section';
section.id = sectionId;
section.setAttribute('data-type', type);
let title = '';
let tableHtml = '';
if (type === 'monografie') {
title = 'Monografie';
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
'<tr>' +
'<th>Autor (Nachname, Vorname)</th>' +
'<th>Jahr</th>' +
'<th>Auflage</th>' +
'<th>Titel</th>' +
'<th>Signatur</th>' +
'<th>Seiten von</th>' +
'<th>Seiten bis</th>' +
'<th></th>' +
'</tr>' +
'</table>';
} else if (type === 'zeitschriftenartikel') {
title = 'Zeitschriftenartikel';
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
'<tr>' +
'<th>Autor (Nachname, Vorname)</th>' +
'<th>Jahr</th>' +
'<th>Band</th>' +
'<th>Titel des Artikels</th>' +
'<th>Titel der Zeitschrift</th>' +
'<th>Signatur</th>' +
'<th>Seiten von</th>' +
'<th>Seiten bis</th>' +
'<th></th>' +
'</tr>' +
'</table>';
} else if (type === 'herausgeberwerk') {
title = 'Herausgeberwerk';
tableHtml = '<table class="data-table media-table" id="table-' + sectionId + '">' +
'<tr>' +
'<th>Herausgeber (Nachname, Vorname)</th>' +
'<th>Titel des Werks</th>' +
'<th>Jahr</th>' +
'<th>Auflage</th>' +
'<th>Autor des Artikels (Nachname, Vorname)</th>' +
'<th>Titel des Artikels</th>' +
'<th>Signatur</th>' +
'<th>Seiten von</th>' +
'<th>Seiten bis</th>' +
'<th></th>' +
'</tr>' +
'</table>';
}
section.innerHTML = '<div class="media-section-header">' +
'<h3><span class="mdi ' + getIconForType(type) + '"></span> ' + title + '</h3>' +
'<button type="button" class="btn-icon" onclick="removeMediaSection(\'' + sectionId + '\')" title="Sektion entfernen">' +
'<span class="mdi mdi-close"></span>' +
'</button>' +
'</div>' +
tableHtml +
'<button type="button" class="btn btn-secondary btn-sm" onclick="addMediaRow(\'' + sectionId + '\', \'' + type + '\')">' +
'+ Eintrag hinzufügen' +
'</button>';
container.appendChild(section);
// Add first row automatically
addMediaRow(sectionId, type);
}
function getIconForType(type) {
const icons = {
'monografie': 'mdi-book',
'zeitschriftenartikel': 'mdi-newspaper',
'herausgeberwerk': 'mdi-book-multiple'
};
return icons[type] || 'mdi-file';
}
function addMediaRow(sectionId, type) {
const table = document.getElementById('table-' + sectionId);
const row = table.insertRow(-1);
const rowId = sectionId + '-row-' + mediaCounter[type]++;
row.id = rowId;
if (type === 'monografie') {
row.innerHTML = '<td><input type="text" name="monografie_author[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="monografie_year[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="monografie_edition[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="monografie_title[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="monografie_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' +
'<td><input type="number" name="monografie_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><input type="number" name="monografie_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
} else if (type === 'zeitschriftenartikel') {
row.innerHTML = '<td><input type="text" name="zeitschrift_author[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_year[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_volume[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_article_title[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_journal_title[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="zeitschrift_signature[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="number" name="zeitschrift_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><input type="number" name="zeitschrift_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
} else if (type === 'herausgeberwerk') {
row.innerHTML = '<td><input type="text" name="herausgeber_publisher[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_work_title[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_year[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_edition[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_article_author[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_article_title[]" data-section="' + sectionId + '" required></td>' +
'<td><input type="text" name="herausgeber_signature[]" data-section="' + sectionId + '" placeholder="Optional"></td>' +
'<td><input type="number" name="herausgeber_pages_from[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><input type="number" name="herausgeber_pages_to[]" data-section="' + sectionId + '" required min="1"></td>' +
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
}
}
function removeRow(rowId) {
const row = document.getElementById(rowId);
if (row) {
row.remove();
}
}
function removeMediaSection(sectionId) {
const section = document.getElementById(sectionId);
if (section) {
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) {
section.remove();
}
}
}
</script>
</body>
</html>

View File

@@ -40,16 +40,16 @@
</div>
</a>
<div class="service-card service-card-disabled" onclick="showConstructionWarning()">
<a href="/elsa" class="service-card">
<div class="service-icon">
<span class="mdi mdi-library-shelves"></span>
</div>
<h2>ELSA</h2>
<p>Elektronischer Semesterapparat</p>
<div class="service-badge">
<span class="mdi mdi-hammer-wrench"></span> Im Aufbau
<div class="service-arrow">
<span class="mdi mdi-arrow-right"></span>
</div>
</div>
</a>
</div>
<section class="info-section">