Compare commits

...

4 Commits

Author SHA1 Message Date
Aiirondev_dev 7e5ee7b5ea Slight fix of the displaying of the detailed view, for the bibliothek focusing on the style
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-17 00:02:49 +02:00
Aiirondev_dev 2528e79895 Slight fix of the displaying of the detailed view, for the bibliothek
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-16 23:50:24 +02:00
Aiirondev_dev 542caa520f fix of a redirect error
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 23:21:31 +02:00
Aiirondev_dev 5136e40587 Implementation of a clean up function for a stray collection processing
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 23:08:10 +02:00
3 changed files with 214 additions and 32 deletions
+3 -1
View File
@@ -5484,7 +5484,7 @@ def upload_item():
fs = get_gridfs()
if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other":
success_redirect_endpoint = 'library'
success_redirect_endpoint = 'library_view'
else:
success_redirect_endpoint = 'home_admin'
@@ -5828,7 +5828,9 @@ def upload_item():
if item_id:
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
fs = get_gridfs() # Deine GridFS Verbindung
it.cleanup_orphaned_images(fs, dry_run=True)
if upload_mode == 'library':
try:
_append_audit_event_standalone(
+135 -1
View File
@@ -25,6 +25,7 @@ import datetime
import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
import logging
def is_library_item(item):
@@ -1292,4 +1293,137 @@ def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
return True
except Exception as e:
print(f"Error syncing group codes: {e}")
return False
return False
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
def cleanup_orphaned_images(fs, dry_run=True):
"""
Finds images in GridFS that are no longer referenced by any item
and optionally deletes them.
Supported item fields:
- book_cover_image
- image
- images
The fields are expected to contain GridFS file ObjectIds.
:param fs: GridFS instance, e.g. gridfs.GridFS(db)
:param dry_run: If True, only reports what would be deleted.
If False, actually deletes the files.
"""
logging.info("Starte Cleanup-Skript...")
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
db = client[cfg.MONGODB_DB]
items_collection = db["items"]
referenced_files = set()
for item in items_collection.find(
{},
{
"Images": 1
}
):
images = item.get("Images")
if isinstance(images, list):
for img in images:
if img:
referenced_files.add(img)
referenced_files.discard(None)
logging.info(
f"{len(referenced_files)} referenzierte GridFS-Dateien gefunden."
)
orphaned_files = []
for grid_file in fs.find():
file_id = grid_file._id
filename = grid_file.filename or ""
if not filename.lower().endswith(
(".jpg", ".jpeg", ".png", ".gif", ".webp")
):
continue
if file_id not in referenced_files:
orphaned_files.append(
{
"_id": file_id,
"filename": filename,
"upload_date": grid_file.upload_date,
}
)
logging.info(
f"Gefundene verwaiste Bilder: {len(orphaned_files)}"
)
if dry_run:
logging.info(
"--- DRY RUN AKTIV - Es wird nichts gelöscht ---"
)
for file in orphaned_files:
logging.info(
f"Würde löschen: "
f"{file['filename']} "
f"(ID: {file['_id']}, "
f"Hochgeladen: {file['upload_date']})"
)
logging.info(
"--- Setze dry_run=False, um physisch zu löschen ---"
)
else:
logging.warning("--- LÖSCHVORGANG AKTIV ---")
deleted_count = 0
failed_count = 0
for file in orphaned_files:
try:
fs.delete(file["_id"])
deleted_count += 1
logging.info(
f"Gelöscht: {file['filename']} "
f"(ID: {file['_id']})"
)
except Exception as e:
failed_count += 1
logging.error(
f"Fehler beim Löschen von "
f"{file['filename']} "
f"(ID: {file['_id']}): {e}"
)
logging.info(
f"Cleanup beendet. "
f"{deleted_count} Bilder gelöscht, "
f"{failed_count} Fehler."
)
return {
"status": "success",
"referenced_count": len(referenced_files),
"orphaned_count": len(orphaned_files),
"dry_run": dry_run,
}
finally:
client.close()
+76 -30
View File
@@ -450,6 +450,58 @@
width: 100%;
height: 100%;
}
.detail-gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 16px;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid var(--ui-surface-soft, #e5e7eb);
}
.item-image-wrapper {
position: relative;
aspect-ratio: 1 / 1;
border-radius: 10px;
overflow: hidden;
background-color: #f3f4f6;
border: 1px solid #e5e7eb;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.item-image-wrapper:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.item-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.item-image-wrapper:hover .item-image {
transform: scale(1.03);
}
/* Fallback / No-Image Styling im Detail-Modal */
.detail-no-image {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
background: #f9fafb;
border-radius: 8px;
border: 1px dashed #d1d5db;
color: #6b7280;
font-size: 0.9em;
margin-bottom: 20px;
}
</style>
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
@@ -1331,40 +1383,34 @@
detailContent.innerHTML = '<p>Lade Details...</p>';
detailModal.style.display = 'flex';
// Sicherer Zugriff auf das Item mit Fallback
const item = libraryItems.find(i => i._id === itemId);
let mediaHtml = '';
if (item) {
// Prüfe gängige Array-Namen aus dem Backend
const imageArray = item.Images || item.Bilder || item.images;
if (Array.isArray(imageArray) && imageArray.length > 0) {
const imagesHtml = imageArray.map(image => {
// Direkter, robuster Pfad zur Upload-Route
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
? image
: `/uploads/${image}`;
return `<img src="${imageSrc}" alt="${escapeHtml(item.Name || 'Medium')}" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
}).join('');
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
}
}
// Zusätzliche Details vom Backend laden
fetch(`/api/item_detail/${itemId}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.text();
if (!response.ok) {
throw new Error('Fehler beim Laden der Artikeldetails');
}
return response.json();
})
.then(html => {
detailContent.innerHTML = mediaHtml + DOMPurify.sanitize(html);
.then(data => {
let mediaHtml = '';
const imageArray = data.images || [];
if (Array.isArray(imageArray) && imageArray.length > 0) {
const imagesHtml = imageArray.map(image => {
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
? image
: `/uploads/${image}`;
return `<img src="${imageSrc}" alt="Buchcover / Bild" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
}).join('');
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
}
detailContent.innerHTML = mediaHtml + data.html;
})
.catch(err => {
console.error('Error loading detail:', err);
detailContent.innerHTML = '<p style="color: red;">Entschuldigung, die Details konnten nicht geladen werden.</p>';
.catch(error => {
console.error('Error fetching item detail:', error);
detailContent.innerHTML = '<p style="color: red;">Fehler beim Laden der Artikeldetails.</p>';
});
}