feat: calculate 15% limit for book(s), blocking submission if over the limit
This commit is contained in:
39
app/main.py
39
app/main.py
@@ -4,8 +4,9 @@ import smtplib
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from xml.etree.ElementTree import Element, SubElement, tostring
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||||
|
|
||||||
|
from bibapi import catalogue
|
||||||
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, JSONResponse, RedirectResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ templates = Jinja2Templates(directory="app/templates")
|
|||||||
# Serve static files (CSS, images)
|
# Serve static files (CSS, images)
|
||||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
app.mount("/static", StaticFiles(directory="app/static"), name="static")
|
||||||
|
|
||||||
|
# Initialize catalogue for signature validation
|
||||||
|
cat = catalogue.Catalogue()
|
||||||
|
|
||||||
# add somewhere near the top-level constants
|
# add somewhere near the top-level constants
|
||||||
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
EMAIL_REGEX = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
|
|
||||||
@@ -44,6 +48,39 @@ async def elsa_form(request: Request):
|
|||||||
return templates.TemplateResponse("elsa_mono_form.html", {"request": request})
|
return templates.TemplateResponse("elsa_mono_form.html", {"request": request})
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/validate-signature")
|
||||||
|
async def validate_signature(signature: str):
|
||||||
|
"""Validate a book signature and return total pages"""
|
||||||
|
try:
|
||||||
|
book_result = cat.get_book_with_data(signature)
|
||||||
|
if book_result and hasattr(book_result, "pages") and book_result.pages:
|
||||||
|
# Try to extract numeric page count
|
||||||
|
pages_str = str(book_result.pages)
|
||||||
|
# Extract first number from pages string (e.g., "245 S." -> 245)
|
||||||
|
match = re.search(r"(\d+)", pages_str)
|
||||||
|
if match:
|
||||||
|
total_pages = int(match.group(1))
|
||||||
|
return JSONResponse(
|
||||||
|
{"valid": True, "total_pages": total_pages, "signature": signature}
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"valid": False,
|
||||||
|
"error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"valid": False,
|
||||||
|
"error": f"Fehler bei der Validierung: {str(e)}",
|
||||||
|
"signature": signature,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/submit")
|
@app.post("/submit")
|
||||||
async def handle_form(
|
async def handle_form(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -725,3 +725,52 @@ input[type="radio"] { accent-color: var(--control-accent); }
|
|||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Signature validation states */
|
||||||
|
.signature-validating {
|
||||||
|
border-color: #3b82f6 !important;
|
||||||
|
background-color: #eff6ff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .signature-validating {
|
||||||
|
background-color: #1e3a8a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-valid {
|
||||||
|
border-color: #22c55e !important;
|
||||||
|
background-color: #f0fdf4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .signature-valid {
|
||||||
|
background-color: #14532d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-invalid {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
background-color: #fef2f2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .signature-invalid {
|
||||||
|
background-color: #7f1d1d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Threshold exceeded - entire row in red */
|
||||||
|
.threshold-exceeded {
|
||||||
|
background-color: #fee2e2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .threshold-exceeded {
|
||||||
|
background-color: #991b1b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threshold-exceeded input,
|
||||||
|
.threshold-exceeded select {
|
||||||
|
border-color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled submit button styling */
|
||||||
|
button[type="submit"]:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: #9ca3af !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,6 +137,12 @@
|
|||||||
|
|
||||||
let sectionCounter = 0;
|
let sectionCounter = 0;
|
||||||
|
|
||||||
|
// Track signature validations: { signature: { totalPages: int, requestedPages: int, inputs: [elements] } }
|
||||||
|
let signatureTracking = {};
|
||||||
|
|
||||||
|
// Debounce timer for signature validation
|
||||||
|
let validationTimers = {};
|
||||||
|
|
||||||
// Theme toggle functionality (in IIFE to avoid polluting global scope)
|
// Theme toggle functionality (in IIFE to avoid polluting global scope)
|
||||||
(function() {
|
(function() {
|
||||||
const STORAGE_KEY = 'theme';
|
const STORAGE_KEY = 'theme';
|
||||||
@@ -266,6 +272,7 @@
|
|||||||
'<td><input type="number" name="monografie_pages_from[]" data-section="' + sectionId + '" required min="1"></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><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>';
|
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
|
||||||
|
attachSignatureListeners(row);
|
||||||
} else if (type === 'zeitschriftenartikel') {
|
} else if (type === 'zeitschriftenartikel') {
|
||||||
row.innerHTML = '<td><input type="text" name="zeitschrift_author[]" data-section="' + sectionId + '" required></td>' +
|
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_year[]" data-section="' + sectionId + '" required></td>' +
|
||||||
@@ -287,13 +294,19 @@
|
|||||||
'<td><input type="number" name="herausgeber_pages_from[]" data-section="' + sectionId + '" required min="1"></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><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>';
|
'<td><button type="button" class="btn-icon" onclick="removeRow(\'' + rowId + '\')" title="Zeile entfernen"><span class="mdi mdi-delete"></span></button></td>';
|
||||||
|
attachSignatureListeners(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeRow(rowId) {
|
function removeRow(rowId) {
|
||||||
const row = document.getElementById(rowId);
|
const row = document.getElementById(rowId);
|
||||||
if (row) {
|
if (row) {
|
||||||
|
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||||
|
if (signatureInput) {
|
||||||
|
cleanupSignatureTracking(signatureInput);
|
||||||
|
}
|
||||||
row.remove();
|
row.remove();
|
||||||
|
updateSubmitButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,10 +314,202 @@
|
|||||||
const section = document.getElementById(sectionId);
|
const section = document.getElementById(sectionId);
|
||||||
if (section) {
|
if (section) {
|
||||||
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) {
|
if (confirm('Möchten Sie diese Sektion wirklich entfernen?')) {
|
||||||
|
// Clean up tracking for removed rows
|
||||||
|
const rows = section.querySelectorAll('tr[id]');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||||
|
if (signatureInput) {
|
||||||
|
cleanupSignatureTracking(signatureInput);
|
||||||
|
}
|
||||||
|
});
|
||||||
section.remove();
|
section.remove();
|
||||||
|
updateSubmitButton();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateSignature(signatureInput, pagesFromInput, pagesToInput) {
|
||||||
|
const signature = signatureInput.value.trim();
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
// Clear validation state if signature is empty
|
||||||
|
signatureInput.classList.remove('signature-validating', 'signature-valid', 'signature-invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any existing timer for this input
|
||||||
|
const inputId = signatureInput.id || signatureInput.name;
|
||||||
|
if (validationTimers[inputId]) {
|
||||||
|
clearTimeout(validationTimers[inputId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show validating state
|
||||||
|
signatureInput.classList.add('signature-validating');
|
||||||
|
signatureInput.classList.remove('signature-valid', 'signature-invalid');
|
||||||
|
|
||||||
|
// Debounce the API call
|
||||||
|
validationTimers[inputId] = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/validate-signature?signature=' + encodeURIComponent(signature));
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.valid) {
|
||||||
|
// Initialize tracking for this signature if needed
|
||||||
|
if (!signatureTracking[signature]) {
|
||||||
|
signatureTracking[signature] = {
|
||||||
|
totalPages: data.total_pages,
|
||||||
|
inputs: []
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
signatureTracking[signature].totalPages = data.total_pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track this input
|
||||||
|
const existingIndex = signatureTracking[signature].inputs.findIndex(
|
||||||
|
item => item.signature === signatureInput
|
||||||
|
);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
signatureTracking[signature].inputs.push({
|
||||||
|
signature: signatureInput,
|
||||||
|
pagesFrom: pagesFromInput,
|
||||||
|
pagesTo: pagesToInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureInput.classList.remove('signature-validating');
|
||||||
|
signatureInput.classList.add('signature-valid');
|
||||||
|
signatureInput.title = 'Signatur gefunden: ' + data.total_pages + ' Seiten';
|
||||||
|
|
||||||
|
// Recalculate all pages for this signature
|
||||||
|
checkSignatureThreshold(signature);
|
||||||
|
} else {
|
||||||
|
signatureInput.classList.remove('signature-validating');
|
||||||
|
signatureInput.classList.add('signature-invalid');
|
||||||
|
signatureInput.title = data.error || 'Signatur nicht gefunden';
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
signatureInput.classList.remove('signature-validating');
|
||||||
|
signatureInput.classList.add('signature-invalid');
|
||||||
|
signatureInput.title = 'Validierungsfehler';
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkSignatureThreshold(signature) {
|
||||||
|
const tracking = signatureTracking[signature];
|
||||||
|
if (!tracking) return;
|
||||||
|
|
||||||
|
let totalRequestedPages = 0;
|
||||||
|
const threshold = Math.ceil(tracking.totalPages * 0.15);
|
||||||
|
|
||||||
|
// Calculate total requested pages across all inputs for this signature
|
||||||
|
tracking.inputs.forEach(item => {
|
||||||
|
const from = parseInt(item.pagesFrom.value) || 0;
|
||||||
|
const to = parseInt(item.pagesTo.value) || 0;
|
||||||
|
if (from > 0 && to > 0 && to >= from) {
|
||||||
|
totalRequestedPages += (to - from + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOverThreshold = totalRequestedPages > threshold;
|
||||||
|
|
||||||
|
// Update all rows with this signature
|
||||||
|
tracking.inputs.forEach(item => {
|
||||||
|
const row = item.signature.closest('tr');
|
||||||
|
if (row) {
|
||||||
|
if (isOverThreshold) {
|
||||||
|
row.classList.add('threshold-exceeded');
|
||||||
|
item.signature.title = 'Warnung: Gesamtanzahl der Seiten (' + totalRequestedPages + ') überschreitet 15% (' + threshold + ' Seiten)';
|
||||||
|
} else {
|
||||||
|
row.classList.remove('threshold-exceeded');
|
||||||
|
item.signature.title = 'Signatur gefunden: ' + tracking.totalPages + ' Seiten (Aktuell: ' + totalRequestedPages + '/' + threshold + ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateSubmitButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupSignatureTracking(signatureInput) {
|
||||||
|
const signature = signatureInput.value.trim();
|
||||||
|
if (signature && signatureTracking[signature]) {
|
||||||
|
signatureTracking[signature].inputs = signatureTracking[signature].inputs.filter(
|
||||||
|
item => item.signature !== signatureInput
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove signature from tracking if no more inputs
|
||||||
|
if (signatureTracking[signature].inputs.length === 0) {
|
||||||
|
delete signatureTracking[signature];
|
||||||
|
} else {
|
||||||
|
// Recalculate for remaining inputs
|
||||||
|
checkSignatureThreshold(signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSubmitButton() {
|
||||||
|
const submitButton = document.querySelector('button[type="submit"]');
|
||||||
|
const hasInvalidSignatures = document.querySelectorAll('.signature-invalid').length > 0;
|
||||||
|
const hasThresholdExceeded = document.querySelectorAll('.threshold-exceeded').length > 0;
|
||||||
|
|
||||||
|
if (hasInvalidSignatures || hasThresholdExceeded) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
if (hasThresholdExceeded) {
|
||||||
|
submitButton.title = 'Formular kann nicht abgesendet werden: 15%-Grenze überschritten';
|
||||||
|
} else {
|
||||||
|
submitButton.title = 'Formular kann nicht abgesendet werden: Ungültige Signaturen';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.title = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachSignatureListeners(row) {
|
||||||
|
const signatureInput = row.querySelector('input[name*="_signature"]');
|
||||||
|
const pagesFromInput = row.querySelector('input[name*="_pages_from"]');
|
||||||
|
const pagesToInput = row.querySelector('input[name*="_pages_to"]');
|
||||||
|
|
||||||
|
if (signatureInput && pagesFromInput && pagesToInput) {
|
||||||
|
// Generate unique ID if not present
|
||||||
|
if (!signatureInput.id) {
|
||||||
|
signatureInput.id = 'sig-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate on signature change
|
||||||
|
signatureInput.addEventListener('input', () => {
|
||||||
|
const oldSignature = signatureInput.dataset.lastSignature || '';
|
||||||
|
const newSignature = signatureInput.value.trim();
|
||||||
|
|
||||||
|
// Cleanup old signature tracking if it changed
|
||||||
|
if (oldSignature && oldSignature !== newSignature) {
|
||||||
|
const tempInput = document.createElement('input');
|
||||||
|
tempInput.value = oldSignature;
|
||||||
|
cleanupSignatureTracking(tempInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureInput.dataset.lastSignature = newSignature;
|
||||||
|
validateSignature(signatureInput, pagesFromInput, pagesToInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate when page range changes
|
||||||
|
pagesFromInput.addEventListener('input', () => {
|
||||||
|
const signature = signatureInput.value.trim();
|
||||||
|
if (signature && signatureTracking[signature]) {
|
||||||
|
checkSignatureThreshold(signature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
pagesToInput.addEventListener('input', () => {
|
||||||
|
const signature = signatureInput.value.trim();
|
||||||
|
if (signature && signatureTracking[signature]) {
|
||||||
|
checkSignatureThreshold(signature);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ description = "Add your description here"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bibapi>=0.0.4",
|
||||||
"fastapi>=0.117.1",
|
"fastapi>=0.117.1",
|
||||||
"jinja2>=3.1.6",
|
"jinja2>=3.1.6",
|
||||||
"pip>=25.2",
|
"pip>=25.2",
|
||||||
@@ -34,3 +35,8 @@ commit_args = ""
|
|||||||
setup_hooks = []
|
setup_hooks = []
|
||||||
pre_commit_hooks = []
|
pre_commit_hooks = []
|
||||||
post_commit_hooks = []
|
post_commit_hooks = []
|
||||||
|
|
||||||
|
|
||||||
|
[[tool.uv.index]]
|
||||||
|
name = "gitea"
|
||||||
|
url = "https://git.theprivateserver.de/api/packages/PHB/pypi/simple/"
|
||||||
|
|||||||
Reference in New Issue
Block a user