Compare commits
10 Commits
v0.13.16-dev.7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb89b434ed | |||
| ac4d125d73 | |||
| 3f6830e8c8 | |||
| 0ea5d2db26 | |||
| 41d9c0a848 | |||
| f6e3db9b4a | |||
| 7e5ee7b5ea | |||
| 2528e79895 | |||
| 542caa520f | |||
| 5136e40587 |
+8
-3
@@ -3826,11 +3826,14 @@ def api_item_detail(item_id):
|
||||
{f'<p><strong>Ausgeliehen von:</strong> {html.escape(str(borrower_value))}</p>' if borrower_value and status_label == 'Ausgeliehen' else ''}
|
||||
{borrows_html}
|
||||
"""
|
||||
|
||||
ctx = get_tenant_context()
|
||||
current_tenant_id = ctx.tenant_id if ctx else None
|
||||
|
||||
client.close()
|
||||
return jsonify({
|
||||
'html': detail_html,
|
||||
'images': item.get('Images', item.get('Bilder', []))
|
||||
'images': item.get('Images', item.get('Bilder', [])),
|
||||
'tenant': str(current_tenant_id)
|
||||
}), 200
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error fetching item detail: {e}")
|
||||
@@ -5484,7 +5487,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 +5831,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(
|
||||
|
||||
@@ -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()
|
||||
@@ -450,6 +450,37 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.detail-gallery-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin: 15px 0 20px 0;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-image-wrapper {
|
||||
background-color: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.item-image {
|
||||
max-width: 160px;
|
||||
max-height: 200px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
|
||||
@@ -1328,43 +1359,71 @@
|
||||
const detailContent = document.getElementById('detailContent');
|
||||
const detailModal = document.getElementById('detailModal');
|
||||
|
||||
// Lade-Status anzeigen und Modal öffnen
|
||||
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
|
||||
// Daten vom Backend-API-Endpoint abrufen
|
||||
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 => {
|
||||
detailContent.innerHTML = data.html;
|
||||
|
||||
const imageArray = data.images || [];
|
||||
const thumbnailInfoMap = data.thumbnailInfo || [];
|
||||
|
||||
if (Array.isArray(imageArray) && imageArray.length > 0) {
|
||||
const imagesHtml = imageArray.map((image, index) => {
|
||||
const isVideo = typeof isVideoFile === 'function' ? isVideoFile(image) : /\.(mp4|webm|ogg|mov)$/i.test(image);
|
||||
const thumbnailInfo = thumbnailInfoMap[index];
|
||||
|
||||
if (isVideo) {
|
||||
const videoSrc = image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`;
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper" style="width: 100%; height: auto;">
|
||||
<video src="${videoSrc}" class="item-image ${index === 0 ? 'active-image' : ''}" id="modal-image-${index}" controls preload="metadata" style="width: 100%; height: auto; max-height: 300px;"></video>
|
||||
</div>`;
|
||||
} else {
|
||||
const imageSrc = thumbnailInfo && thumbnailInfo.has_preview
|
||||
? thumbnailInfo.preview_url
|
||||
: (image.startsWith('/uploads/') || image.startsWith('http')
|
||||
? image
|
||||
: `/uploads/${image}`);
|
||||
|
||||
return `
|
||||
<div class="item-image-wrapper">
|
||||
<img src="${imageSrc}"
|
||||
alt="Buchcover / Bild"
|
||||
class="item-image ${index === 0 ? 'active-image' : ''}"
|
||||
id="modal-image-${index}"
|
||||
loading="lazy"
|
||||
onload="console.log('Bild geladen:', '${imageSrc}')"
|
||||
onerror="console.error('FEHLER beim Laden des Bildes im DOM:', '${imageSrc}')">
|
||||
</div>`;
|
||||
}
|
||||
}).join('');
|
||||
|
||||
const mediaHtml = `<div class="detail-gallery-container">${imagesHtml}</div>`;
|
||||
|
||||
const h2Tag = detailContent.querySelector('h2');
|
||||
if (h2Tag) {
|
||||
h2Tag.insertAdjacentHTML('afterend', mediaHtml);
|
||||
} else {
|
||||
detailContent.insertAdjacentHTML('afterbegin', mediaHtml);
|
||||
}
|
||||
}
|
||||
})
|
||||
.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>';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user