Compare commits

..

4 Commits

2 changed files with 98 additions and 88 deletions
+97 -82
View File
@@ -5391,8 +5391,7 @@ def upload_item():
processed_count = 0
error_count = 0
skipped_count = 0
# Create a structured log entry for upload session
upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
@@ -5410,7 +5409,6 @@ def upload_item():
app.logger.info(f"{image_log_prefix} Processing: {image.filename}")
try:
# 1. Validation
is_allowed, error_message = allowed_file(image.filename, image, max_size_mb=cfg.IMAGE_MAX_UPLOAD_MB)
if not is_allowed:
app.logger.warning(f"{image_log_prefix} Validation failed: {error_message}")
@@ -5421,29 +5419,29 @@ def upload_item():
secure_name = secure_filename(image.filename)
# 2. Read directly into memory (Bypasses OS-level file quirks and iOS temp-file bugs)
image.seek(0)
image_bytes = image.read()
# 3. Process, standardize, and optimize using Pillow in-memory
if not image_bytes:
app.logger.error(f"{image_log_prefix} Failed to read image (0 bytes).")
error_count += 1
continue
optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img:
# Ensure safe color mode
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
# Standardize dimensions (e.g., max width 500px)
max_width = 500
if img.width > max_width:
ratio = max_width / img.width
new_size = (max_width, int(img.height * ratio))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# Export as WebP to a memory buffer
# (WebP naturally handles transparency, drastically reduces size, and bypasses PNG signature corruption)
img.save(optimized_io, format='WEBP', quality=85, optimize=True)
optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
file_id = fs.put(
@@ -5456,9 +5454,9 @@ def upload_item():
}
)
image_filenames.append(file_id)
processed_count += 1
image_filenames.append(new_filename)
processed_count += 1
final_size_kb = len(optimized_io.getvalue()) / 1024
app.logger.info(
f"{image_log_prefix} Saved to GridFS as {new_filename} | ID: {file_id} | Size: {final_size_kb:.1f}KB")
@@ -6085,10 +6083,10 @@ def bulk_delete_items():
def edit_item(id):
"""
Route for editing an existing inventory item.
Args:
id (str): ID of the item to edit
Returns:
flask.Response: Redirect to admin homepage with status message
"""
@@ -6099,19 +6097,19 @@ def edit_item(id):
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_edit', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion (Löschen) auszuführen.', 'error')
return redirect(url_for('home_admin'))
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('home_admin'))
if not cfg.MODULES.is_enabled('inventory'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('library_view'))
# Strip whitespace from all text fields
fs = get_gridfs()
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
# Strip whitespace from all filter values
filter1 = sanitize_form_value(request.form.getlist('filter'))
filter2 = sanitize_form_value(request.form.getlist('filter2'))
filter3 = sanitize_form_value(request.form.getlist('filter3'))
@@ -6119,7 +6117,7 @@ def edit_item(id):
# Expand special "all values" selections for predefined filters.
filter1 = expand_filter_selection(filter1, 1)
filter2 = expand_filter_selection(filter2, 2)
anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr'))
anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten'))
code_4 = sanitize_form_value(request.form.get('code_4'))
@@ -6135,91 +6133,103 @@ def edit_item(id):
return redirect(url_for('home_admin'))
if item_isbn:
item_type = 'book'
# Check if code is unique (excluding the current item)
if code_4 and not it.is_code_unique(code_4, exclude_id=id):
flash('Der Code wird bereits verwendet. Bitte wählen Sie einen anderen Code.', 'error')
return redirect(url_for('home_admin'))
# Get current item to check availability status
current_item = it.get_item(id)
if not current_item:
flash('Element nicht gefunden', 'error')
return redirect(url_for('home_admin'))
# Preserve current availability status
verfuegbar = current_item.get('Verfuegbar', True)
# Handle existing images - get list of images to keep
images_to_keep = request.form.getlist('existing_images')
# Get the original list of images from the item
original_images = current_item.get('Images', [])
# Keep only the images that weren't marked for deletion
images = [img for img in original_images if img in images_to_keep]
# Handle new image uploads
new_images = request.files.getlist('new_images')
# Process any new image uploads
for image in new_images:
if image and image.filename:
is_allowed, error_message = allowed_file(image.filename)
is_allowed, error_message = allowed_file(image.filename, image)
if is_allowed:
# Get the file extension
_, ext_part = os.path.splitext(secure_filename(image.filename))
# Generate a completely unique filename using UUID
unique_id = str(uuid.uuid4())
timestamp = time.strftime("%Y%m%d%H%M%S")
# New filename format with UUID to ensure uniqueness
filename = f"{unique_id}_{timestamp}{ext_part}"
image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
# Optimize the image
try:
opt_result = generate_optimized_versions(filename, max_original_width=500, target_size_kb=80)
if opt_result['success'] and opt_result['original']:
filename = opt_result['original']
secure_name = secure_filename(image.filename)
image.seek(0)
image_bytes = image.read()
if not image_bytes:
app.logger.error(f"Failed to read image in edit_item (0 bytes) for {secure_name}")
continue
optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img:
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
max_width = 500
if img.width > max_width:
ratio = max_width / img.width
new_size = (max_width, int(img.height * ratio))
img = img.resize(new_size, Image.Resampling.LANCZOS)
img.save(optimized_io, format='WEBP', quality=85, optimize=True)
optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
fs.put(
optimized_io,
filename=new_filename,
content_type='image/webp',
metadata={
'original_filename': secure_name,
'upload_context': 'edit_item',
'item_id': id
}
)
images.append(new_filename)
except Exception as e:
app.logger.error(f"Error optimizing image in edit_item: {e}")
images.append(filename)
app.logger.error(f"Error processing new image in edit_item: {str(e)}")
else:
flash(error_message, 'error')
return redirect(url_for('home_admin'))
# If location is not in the predefined list, maybe add it (depending on policy)
predefined_locations = it.get_predefined_locations()
if ort and ort not in predefined_locations:
it.add_predefined_location(ort)
result = it.update_item(
id=id,
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
filter1=filter1,
filter2=filter2,
id=id,
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
filter1=filter1,
filter2=filter2,
filter3=filter3,
ansch_jahr=anschaffungs_jahr,
ansch_kost=anschaffungs_kosten,
code_4=code_4,
ansch_jahr=anschaffungs_jahr,
ansch_kost=anschaffungs_kosten,
code_4=code_4,
reservierbar=reservierbar,
isbn=item_isbn,
item_type=item_type
)
if result:
flash('Element erfolgreich aktualisiert (und ggf. Gruppe synchronisiert)', 'success')
else:
flash('Fehler beim Aktualisieren des Elements', 'error')
return redirect(url_for('home_admin'))
@@ -8893,6 +8903,7 @@ def logs():
Returns:
flask.Response: Rendered template with logs or redirect if not authenticated
"""
from modules.inventarsystem.data_protection import decrypt_text
if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login'))
@@ -8911,9 +8922,6 @@ def logs():
# Get item details - from sample data, Item is an ID
item = it.get_item(ausleihung.get('Item'))
item_name = item.get('Name', 'Unknown Item') if item else 'Unknown Item'
# Get user details - from sample data, User is a username string
username = ausleihung.get('User', 'Unknown User')
# Determine (verified) status for display
@@ -8944,7 +8952,7 @@ def logs():
formatted_items.append({
'Item': item_name,
'User': username,
'User': decrypt_text(username),
'Start': start_date,
'End': end_date,
'Duration': duration,
@@ -8964,7 +8972,6 @@ def logs():
logs_collection = db['system_logs']
extra_logs = list(logs_collection.find({'type': {'$in': ['damage_report', 'damage_repair']}}))
from modules.inventarsystem.data_protection import decrypt_text
from bson.objectid import ObjectId
@@ -9786,7 +9793,8 @@ def get_period_times(booking_date, period_num):
@app.route('/my_borrowed_items')
def my_borrowed_items():
"""
Zeigt alle vom aktuellen Benutzer ausgeliehenen und geplanten Objekte an.
Zeigt alle vom aktuellen Benutzer ausgeliehenen und geplanten Objekte an,
schließt jedoch soft-gelöschte Objekte (Deleted: True) aus.
"""
if 'username' not in session:
flash('Bitte melden Sie sich an, um Ihre ausgeliehenen Objekte anzuzeigen', 'error')
@@ -9827,7 +9835,11 @@ def my_borrowed_items():
query_id = ObjectId(item_id)
else:
query_id = item_id
item_obj = items_collection.find_one({'_id': query_id})
item_obj = items_collection.find_one({
'_id': query_id,
'Deleted': {'$ne': True}
})
except Exception:
item_obj = None
@@ -9852,7 +9864,11 @@ def my_borrowed_items():
elif status == 'planned':
planned_items.append(item_obj)
all_borrowed_items = list(items_collection.find({'Verfuegbar': False}))
all_borrowed_items = list(items_collection.find({
'Verfuegbar': False,
'Deleted': {'$ne': True}
}))
for item in all_borrowed_items:
raw_item_user = item.get('User', '')
try:
@@ -9869,7 +9885,6 @@ def my_borrowed_items():
client.close()
# DEBUG Logging
app.logger.info(
f"Passing {len(active_items)} active items and {len(planned_items)} planned items to template for user {username}")
+1 -6
View File
@@ -4569,12 +4569,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="detail-label">Code:</div>
<div class="detail-value">${escapeHtml(item.Code_4 || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anzahl:</div>
<div class="detail-value">${escapeHtml(String(item.GroupedDisplayCount || 1))}</div>
</div>
${isGroupedItem ? `
<div class="detail-group">
<div class="detail-label">Verfügbar:</div>