Merge pull request 'dev' (#16) from dev into main
Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
@@ -11,3 +11,7 @@ venv/
|
|||||||
.gitignore
|
.gitignore
|
||||||
node_modules/
|
node_modules/
|
||||||
uv.lock
|
uv.lock
|
||||||
|
test.py
|
||||||
|
result.xml
|
||||||
|
README.md
|
||||||
|
.gitea/
|
||||||
|
|||||||
@@ -2,35 +2,33 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
github_release:
|
github_release:
|
||||||
description: 'Create Gitea Release'
|
description: "Create Gitea Release"
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
docker_release:
|
docker_release:
|
||||||
description: 'Push Docker images'
|
description: "Push Docker images"
|
||||||
default: true
|
default: true
|
||||||
type: boolean
|
type: boolean
|
||||||
bump:
|
bump:
|
||||||
description: 'Bump type'
|
description: "Bump type"
|
||||||
required: false
|
required: false
|
||||||
default: 'patch'
|
default: "patch"
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- 'major'
|
- "major"
|
||||||
- 'minor'
|
- "minor"
|
||||||
- 'patch'
|
- "patch"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@master
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full history
|
fetch-depth: 0 # Fetch full history
|
||||||
fetch-tags: true # Fetch all tags (refs/tags)
|
fetch-tags: true # Fetch all tags (refs/tags)
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@v5
|
uses: astral-sh/setup-uv@v5
|
||||||
@@ -44,7 +42,7 @@ jobs:
|
|||||||
uv add pip
|
uv add pip
|
||||||
uv export --format requirements.txt -o requirements.txt
|
uv export --format requirements.txt -o requirements.txt
|
||||||
# uv run python -m pip install --upgrade pip
|
# uv run python -m pip install --upgrade pip
|
||||||
# uv run python -m pip install -r requirements.txt
|
# uv run python -m pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Set Git identity
|
- name: Set Git identity
|
||||||
run: |
|
run: |
|
||||||
@@ -61,6 +59,8 @@ jobs:
|
|||||||
echo "EOF" >> $GITHUB_ENV
|
echo "EOF" >> $GITHUB_ENV
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -78,31 +78,46 @@ jobs:
|
|||||||
echo "tag=$prev" >> "$GITHUB_OUTPUT"
|
echo "tag=$prev" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
- name: Build and store Docker image for multiple architectures
|
- name: Compute lowercased image repo
|
||||||
if: ${{ github.event.inputs.docker_release == 'true' }}
|
if: ${{ github.event.inputs.docker_release == 'true' }}
|
||||||
|
|
||||||
run: |
|
run: |
|
||||||
REPO_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
|
echo "IMAGE_REPO=${{ secrets.REGISTRY }}/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64,linux/arm/v7 \
|
- name: Docker meta
|
||||||
--tag ${{ secrets.REGISTRY }}/${REPO_NAME}:latest \
|
if: ${{ github.event.inputs.docker_release == 'true' }}
|
||||||
--tag ${{ secrets.REGISTRY }}/${REPO_NAME}:${{ env.VERSION }} \
|
id: meta
|
||||||
--push .
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.IMAGE_REPO }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=raw,value=${{ env.VERSION }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
if: ${{ github.event.inputs.docker_release == 'true' }}
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
uses: ad-m/github-push-action@master
|
uses: ad-m/github-push-action@master
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
branch: ${{ github.ref }}
|
branch: ${{ github.ref }}
|
||||||
- name: Build Changelog
|
- name: Build Changelog
|
||||||
id: build_changelog
|
id: build_changelog
|
||||||
uses: https://github.com/mikepenz/release-changelog-builder-action@v5
|
uses: https://github.com/mikepenz/release-changelog-builder-action@v5
|
||||||
with:
|
with:
|
||||||
platform: "gitea"
|
platform: "gitea"
|
||||||
baseURL: "http://gitea:3000"
|
baseURL: "http://192.168.178.110:3000"
|
||||||
configuration: ".gitea/changelog_config.json"
|
configuration: ".gitea/changelog_config.json"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
- name: Create Gitea Release
|
- name: Create Gitea Release
|
||||||
if: ${{ github.event.inputs.github_release == 'true' }}
|
if: ${{ github.event.inputs.github_release == 'true' }}
|
||||||
@@ -115,6 +130,4 @@ jobs:
|
|||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
|
|
||||||
|
|
||||||
15
Dockerfile
15
Dockerfile
@@ -1,15 +1,22 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM python:3.13
|
FROM python:3.13-slim
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
|
# Playwright won't be installed for actual browser automation
|
||||||
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy dependency list and install first (leverages Docker layer cache)
|
# Install only runtime dependencies needed for bibapi and requests
|
||||||
|
# This avoids installing Playwright browsers which are huge
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir \
|
||||||
|
--extra-index-url https://git.theprivateserver.de/api/packages/PHB/pypi/simple/ \
|
||||||
|
-r requirements.txt && \
|
||||||
|
# Clean up pip cache
|
||||||
|
rm -rf /root/.cache/pip
|
||||||
|
|
||||||
# Copy application code
|
# Copy application code
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,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() {
|
||||||
@@ -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