Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4d61e27f4 | |||
| 318e57475f | |||
| 6a72bd5d82 | |||
| 74fa0d8a68 |
+1
-1
@@ -1 +1 @@
|
||||
v0.8.0
|
||||
v0.8.2
|
||||
|
||||
+110
-54
@@ -3053,6 +3053,7 @@ def api_library_items():
|
||||
|
||||
query = {
|
||||
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']},
|
||||
'IsGroupedSubItem': {'$ne': True},
|
||||
'Deleted': {'$ne': True}
|
||||
}
|
||||
|
||||
@@ -3078,7 +3079,39 @@ def api_library_items():
|
||||
raw_items = list(items_db.find(query, projection).sort([('Name', 1), ('_id', 1)]).skip(offset).limit(limit))
|
||||
|
||||
# Build maps for grouping by parent-child relationship (grouped sub-items)
|
||||
all_ids = [str(itm.get('_id')) for itm in raw_items if itm.get('_id')]
|
||||
parent_ids_list = [str(itm.get('_id')) for itm in raw_items if itm.get('_id')]
|
||||
children_by_parent = {}
|
||||
child_items = []
|
||||
if parent_ids_list:
|
||||
child_projection = {
|
||||
'Name': 1,
|
||||
'Autor': 1,
|
||||
'Author': 1,
|
||||
'ISBN': 1,
|
||||
'Code_4': 1,
|
||||
'Code4': 1,
|
||||
'ItemType': 1,
|
||||
'Verfuegbar': 1,
|
||||
'Condition': 1,
|
||||
'HasDamage': 1,
|
||||
'User': 1,
|
||||
'Ort': 1,
|
||||
'Beschreibung': 1,
|
||||
'Image': 1,
|
||||
'ParentItemId': 1,
|
||||
}
|
||||
child_items = list(items_db.find({
|
||||
'ParentItemId': {'$in': parent_ids_list},
|
||||
'IsGroupedSubItem': True,
|
||||
'Deleted': {'$ne': True}
|
||||
}, child_projection))
|
||||
for child in child_items:
|
||||
parent_id = str(child.get('ParentItemId') or '')
|
||||
if not parent_id:
|
||||
continue
|
||||
children_by_parent.setdefault(parent_id, []).append(child)
|
||||
|
||||
all_ids = parent_ids_list + [str(child.get('_id')) for child in child_items if child.get('_id')]
|
||||
active_records = []
|
||||
if all_ids:
|
||||
active_records = list(ausleihungen_db.find({'Item': {'$in': all_ids}, 'Status': 'active'}, {'Item': 1, 'User': 1}))
|
||||
@@ -3093,26 +3126,10 @@ def api_library_items():
|
||||
if item_id not in active_user_by_item:
|
||||
active_user_by_item[item_id] = rec.get('User', '')
|
||||
|
||||
# Organize children under their parent (ParentItemId) and prepare parent list
|
||||
items_by_id = {}
|
||||
children_by_parent = {}
|
||||
parent_ids = set()
|
||||
for itm in raw_items:
|
||||
iid = str(itm.get('_id'))
|
||||
items_by_id[iid] = itm
|
||||
parent = str(itm.get('ParentItemId') or '')
|
||||
if parent:
|
||||
children_by_parent.setdefault(parent, []).append(itm)
|
||||
else:
|
||||
parent_ids.add(iid)
|
||||
|
||||
# Build aggregated list: for each parent id, include parent + children as one entry
|
||||
# Build aggregated list: iterate ordered parent list and include children like inventory module
|
||||
aggregated = []
|
||||
processed = set()
|
||||
for pid in list(parent_ids):
|
||||
parent = items_by_id.get(pid)
|
||||
if not parent:
|
||||
continue
|
||||
for parent in raw_items:
|
||||
pid = str(parent.get('_id'))
|
||||
children = children_by_parent.get(pid, [])
|
||||
|
||||
# Compute grouped counts and availability
|
||||
@@ -3161,40 +3178,6 @@ def api_library_items():
|
||||
doc['GroupedAllCodes'] = grouped_all_codes
|
||||
|
||||
aggregated.append(doc)
|
||||
processed.add(pid)
|
||||
for c in children:
|
||||
processed.add(str(c.get('_id')))
|
||||
|
||||
# Include any remaining items (orphans or standalones not parented)
|
||||
for itm in raw_items:
|
||||
iid = str(itm.get('_id'))
|
||||
if iid in processed:
|
||||
continue
|
||||
doc = dict(itm)
|
||||
doc['_id'] = iid
|
||||
if doc.get('Code4') in (None, '') and doc.get('Code_4') not in (None, ''):
|
||||
doc['Code4'] = doc.get('Code_4')
|
||||
|
||||
condition_value = str(doc.get('Condition', '')).strip().lower()
|
||||
has_damage = bool(doc.get('HasDamage')) or condition_value == 'destroyed'
|
||||
has_active_borrow = iid in active_item_ids
|
||||
|
||||
if has_damage and not has_active_borrow:
|
||||
doc['LibraryDisplayStatus'] = 'damaged'
|
||||
elif has_active_borrow or doc.get('Verfuegbar') is False:
|
||||
doc['LibraryDisplayStatus'] = 'borrowed'
|
||||
else:
|
||||
doc['LibraryDisplayStatus'] = 'available'
|
||||
|
||||
doc['BorrowedBy'] = active_user_by_item.get(iid) or doc.get('User', '')
|
||||
# Single item: grouped count = 1
|
||||
doc['GroupedDisplayCount'] = 1
|
||||
doc['Quantity'] = 1
|
||||
doc['AvailableGroupedCount'] = 1 if doc.get('Verfuegbar', True) else 0
|
||||
doc['GroupedAvailableUnits'] = [{'id': iid, 'code': doc.get('Code_4') or doc.get('Code4') or '', 'label': f"{doc.get('Code_4') or doc.get('Code4') or '-'} ({doc.get('Name','')})"}] if doc.get('Verfuegbar', True) else []
|
||||
doc['GroupedAllCodes'] = [doc.get('Code_4') or doc.get('Code4') or '']
|
||||
|
||||
aggregated.append(doc)
|
||||
|
||||
client.close()
|
||||
|
||||
@@ -6035,6 +6018,79 @@ def delete_item(id):
|
||||
return redirect(url_for('home_admin'))
|
||||
|
||||
|
||||
@app.route('/delete_library_item/<id>', methods=['POST'])
|
||||
def delete_library_item(id):
|
||||
"""
|
||||
Route to soft-delete library items (books/media) revisionssicher.
|
||||
Requires admin and library module enabled.
|
||||
"""
|
||||
if 'username' not in session:
|
||||
flash('Nicht angemeldet.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
if not us.check_admin(session['username']):
|
||||
flash('Administratorrechte erforderlich.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
if not cfg.MODULES.is_enabled('library'):
|
||||
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
|
||||
return redirect(url_for('home_admin'))
|
||||
|
||||
# Verify item exists and is a library item
|
||||
try:
|
||||
item_doc = it.get_item(id)
|
||||
except Exception:
|
||||
item_doc = None
|
||||
|
||||
if not item_doc:
|
||||
flash('Element nicht gefunden.', 'error')
|
||||
return redirect(url_for('library_admin'))
|
||||
|
||||
if item_doc.get('ItemType') not in LIBRARY_ITEM_TYPES:
|
||||
flash('Element ist kein Bibliotheksmedium.', 'error')
|
||||
return redirect(url_for('library_admin'))
|
||||
|
||||
group_item_ids = it.get_group_item_ids(id)
|
||||
if not group_item_ids:
|
||||
flash('Element nicht gefunden.', 'error')
|
||||
return redirect(url_for('library_admin'))
|
||||
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||
db = client[MONGODB_DB]
|
||||
result = _soft_delete_item_groups(db, [id], session.get('username', ''))
|
||||
success = bool(result.get('success'))
|
||||
soft_deleted = int(result.get('soft_deleted_items', 0))
|
||||
archived_files = int((result.get('archive') or {}).get('archived_files', 0))
|
||||
|
||||
try:
|
||||
_append_audit_event_standalone(
|
||||
event_type='library_item_deleted',
|
||||
payload={
|
||||
'root_item_id': id,
|
||||
'group_item_ids': result.get('group_item_ids', []),
|
||||
'soft_deleted_items': soft_deleted,
|
||||
'archived_files': archived_files,
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
app.logger.warning('Audit write failed for library_item_deleted')
|
||||
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error deleting library item {id}: {e}")
|
||||
success = False
|
||||
archived_files = 0
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
if success:
|
||||
flash(f'Bibliotheksmedium revisionssicher deaktiviert ({soft_deleted} Versionen, {archived_files} Mediendateien archiviert).', 'success')
|
||||
else:
|
||||
flash('Fehler beim Löschen des Bibliotheksmediums.', 'error')
|
||||
|
||||
return redirect(url_for('library_admin'))
|
||||
|
||||
|
||||
@app.route('/bulk_delete_items', methods=['POST'])
|
||||
def bulk_delete_items():
|
||||
"""Soft-delete multiple selected item groups in one request."""
|
||||
|
||||
@@ -720,6 +720,7 @@
|
||||
${actionLabel}
|
||||
</button>
|
||||
${canEditLibraryItems ? `<button class="button" style="background:#0ea5e9;color:#fff;" onclick="openEditLibraryItem('${item._id}')">Bearbeiten</button>` : ''}
|
||||
${canEditLibraryItems ? `<button class="button" style="background:#dc2626;color:#fff; margin-left:6px;" onclick="confirmDeleteLibraryItem('${item._id}')">Löschen</button>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -739,6 +740,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm and call library item delete endpoint
|
||||
function confirmDeleteLibraryItem(itemId) {
|
||||
if (!itemId) return;
|
||||
if (!confirm('Dieses Bibliotheksmedium revisionssicher löschen? Diese Aktion deaktiviert alle zugehörigen Exemplare und archiviert Medien.')) return;
|
||||
deleteLibraryItem(itemId);
|
||||
}
|
||||
|
||||
async function deleteLibraryItem(itemId) {
|
||||
if (!itemId) return;
|
||||
document.body.style.cursor = 'wait';
|
||||
try {
|
||||
const resp = await fetch(`/delete_library_item/${itemId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token }}'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const ct = resp.headers.get('Content-Type') || '';
|
||||
if (ct.includes('application/json')) {
|
||||
const j = await resp.json();
|
||||
if (!resp.ok || j.ok === false) {
|
||||
alert(j.message || 'Fehler beim Löschen');
|
||||
} else {
|
||||
alert(j.message || 'Gelöscht');
|
||||
}
|
||||
loadLibraryItems();
|
||||
} else {
|
||||
// Server redirected to HTML (flash messages). Reload to show state.
|
||||
location.reload();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Delete failed', e);
|
||||
alert('Fehler beim Löschen des Elements.');
|
||||
} finally {
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Filter toggle
|
||||
document.getElementById('filterToggleBtn').addEventListener('click', () => {
|
||||
filterPanelOpen = !filterPanelOpen;
|
||||
|
||||
Reference in New Issue
Block a user