Compare commits

...

3 Commits

8 changed files with 271 additions and 33 deletions
+52
View File
@@ -8589,11 +8589,15 @@ def manage_filters():
filter2_values = it.get_predefined_filter_values(2)
filter3_values = it.get_predefined_filter_values(3)
# Get custom filter names
filter_names = it.get_filter_names()
return render_template(
'manage_filters.html',
filter1_values=filter1_values,
filter2_values=filter2_values,
filter3_values=filter3_values,
filter_names=filter_names,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards')
)
@@ -8657,6 +8661,41 @@ def remove_filter_value(filter_num, value):
return redirect(url_for('manage_filters'))
@app.route('/edit_filter_value/<int:filter_num>/<string:old_value>', methods=['POST'])
def edit_filter_value(filter_num, old_value):
"""
Edit a predefined value from the specified filter.
Args:
filter_num (int): Filter number (1, 2 or 3)
old_value (str): Value to edit
Returns:
flask.Response: Redirect to filter management page
"""
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({'success': False, 'error': 'Not authorized'}), 403
new_value = sanitize_form_value(request.form.get('new_value'))
if not new_value:
flash('Bitte geben Sie einen neuen Wert ein', 'error')
return redirect(url_for('manage_filters'))
if new_value == FILTER_SELECT_ALL_TOKEN:
flash('Dieser Wert ist reserviert und kann nicht als Filterwert gespeichert werden.', 'error')
return redirect(url_for('manage_filters'))
# Update the value from the filter
success = it.edit_predefined_filter_value(filter_num, old_value, new_value)
if success:
flash(f'Wert "{old_value}" wurde in "{new_value}" bei Filter {filter_num} geändert', 'success')
else:
flash(f'Fehler beim Ändern des Wertes oder Wert existiert bereits in Filter {filter_num}', 'error')
return redirect(url_for('manage_filters'))
@app.route('/get_predefined_filter_values/<int:filter_num>')
def get_predefined_filter_values(filter_num):
"""
@@ -9623,14 +9662,27 @@ def admin_school_settings():
try:
updated_school = cfg.update_school_info(school_info)
current_school = updated_school
# Also process filter names
filter_name_1 = request.form.get('filter_name_1')
if filter_name_1: it.set_filter_name(1, filter_name_1.strip())
filter_name_2 = request.form.get('filter_name_2')
if filter_name_2: it.set_filter_name(2, filter_name_2.strip())
filter_name_3 = request.form.get('filter_name_3')
if filter_name_3: it.set_filter_name(3, filter_name_3.strip())
flash('Schulstammdaten wurden erfolgreich gespeichert.', 'success')
except Exception as exc:
app.logger.error(f'Could not update school settings: {exc}\n{traceback.format_exc()}')
flash('Die Schulstammdaten konnten nicht gespeichert werden.', 'error')
# Get current filter names to display in form
filter_names = it.get_filter_names()
return render_template(
'admin_school_settings.html',
school_info=current_school,
filter_names=filter_names,
tenant_id=tenant_id,
tenant_db=tenant_db,
library_module_enabled=cfg.MODULES.is_enabled('library'),
+74
View File
@@ -794,6 +794,80 @@ def remove_predefined_filter_value(filter_num, value):
return result.modified_count > 0
def edit_predefined_filter_value(filter_num, old_value, new_value):
"""
Edit a predefined value from a filter and update all matching items.
Args:
filter_num (int): Filter number (1 for Unterrichtsfach, 2 for Jahrgangsstufe)
old_value (str): Value to replace
new_value (str): New value
Returns:
bool: True if value was updated, False otherwise
"""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
filter_presets = db['filter_presets']
# Check if the new value already exists
existing = filter_presets.find_one({
'filter_num': filter_num,
'values': new_value
})
if existing and old_value != new_value:
client.close()
return False
# Update the value in the filter
result = filter_presets.update_one(
{'filter_num': filter_num, 'values': old_value},
{'$set': {'values.$': new_value}}
)
if result.modified_count > 0:
items = db['items']
filter_field = 'Filter' if filter_num == 1 else f'Filter{filter_num}'
# Also update all items that use this filter
items.update_many(
{filter_field: old_value},
{'$set': {filter_field: new_value}}
)
client.close()
return result.modified_count > 0
def get_filter_names():
"""Get customized filter category names."""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names_doc = db.settings.find_one({'setting_type': 'filter_names'})
client.close()
if names_doc and 'names' in names_doc:
return names_doc['names']
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
}
def set_filter_name(filter_num, name):
"""Set custom name for a filter category."""
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = get_filter_names()
names[str(filter_num)] = name
db.settings.update_one(
{'setting_type': 'filter_names'},
{'$set': {'names': names}},
upsert=True
)
client.close()
return True
# === LOCATION MANAGEMENT ===
def get_predefined_locations():
+19
View File
@@ -71,6 +71,25 @@
<small>Upload ersetzt das bisherige Logo. Wenn kein neues Logo gewählt wird, bleibt das aktuelle erhalten.</small>
</div>
<hr style="margin: 24px 0; border: 0; border-top: 1px solid #e5e7eb;">
<h3 style="font-size: 1.1rem; margin-bottom: 16px; color: #111827;">Globales Filter-Wording</h3>
<p style="font-size: 0.9rem; color: #6b7280; margin-bottom: 16px;">Hier kannst du die globalen Bezeichnungen für die drei Haupt-Kategorien (Filter) der Anwendung ändern.</p>
<div class="form-row">
<div class="form-group">
<label for="filter_name_1">Name Filter 1</label>
<input type="text" id="filter_name_1" name="filter_name_1" value="{{ filter_names.get('1', 'Fach/Kategorie') }}" placeholder="z. B. Fach/Kategorie">
</div>
<div class="form-group">
<label for="filter_name_2">Name Filter 2</label>
<input type="text" id="filter_name_2" name="filter_name_2" value="{{ filter_names.get('2', 'System/Bereich') }}" placeholder="z. B. System/Bereich">
</div>
<div class="form-group">
<label for="filter_name_3">Name Filter 3</label>
<input type="text" id="filter_name_3" name="filter_name_3" value="{{ filter_names.get('3', 'Typ/Art') }}" placeholder="z. B. Typ/Art">
</div>
</div>
<div class="actions-row">
<button type="submit" class="btn btn-primary">Schulstammdaten speichern</button>
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
+1 -1
View File
@@ -329,7 +329,7 @@
}
/* iPad/Tablet responsive adjustments */
@media (max-width: 1024px) {
@media (max-width: 768px) {
.navbar .container-fluid {
gap: 6px;
padding-left: max(env(safe-area-inset-left, 0), 10px);
+11 -4
View File
@@ -760,11 +760,14 @@
if (!mainItemsLoadingIndicator) {
mainItemsLoadingIndicator = document.createElement('div');
mainItemsLoadingIndicator.id = 'main-items-loading-indicator';
mainItemsLoadingIndicator.className = 'items-loading-indicator';
mainItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
mainItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
`;
mainItemsLoadingIndicator.style.display = 'flex';
mainItemsLoadingIndicator.style.width = '100%';
}
itemsContainer.appendChild(mainItemsLoadingIndicator);
@@ -1200,6 +1203,7 @@
card.innerHTML = card.dataset.savedHtml;
card.dataset.deloaded = 'false';
card.classList.remove('is-deloaded');
card.classList.remove('empty-frame');
const favBtn = card.querySelector('.bookmark-btn');
if (favBtn) {
favBtn.addEventListener('click', (ev) => {
@@ -1227,12 +1231,15 @@
card.innerHTML = '';
card.dataset.deloaded = 'true';
card.classList.add('is-deloaded');
card.classList.add('empty-frame');
}
}
}
});
}, {
rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px'
// Keep approx ~120 items rendered in DOM relative to scroll position.
// Assuming ~100-400px per item/row, this margin unloads elements beyond that range.
rootMargin: isMobileLayout ? '18000px 0px 18000px 0px' : '9000px 0px 9000px 0px'
});
}
+11 -4
View File
@@ -3383,11 +3383,14 @@ document.addEventListener('DOMContentLoaded', ()=>{
if (!mainAdminItemsLoadingIndicator) {
mainAdminItemsLoadingIndicator = document.createElement('div');
mainAdminItemsLoadingIndicator.id = 'main-admin-items-loading-indicator';
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator';
mainAdminItemsLoadingIndicator.className = 'items-loading-indicator empty-frames-placeholder';
mainAdminItemsLoadingIndicator.innerHTML = `
<div class="loading-track"><div class="loading-fill"></div></div>
<div class="loading-label">Weitere Objekte werden vorbereitet...</div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px;"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-md-block"></div>
<div class="empty-frame p-3 mx-2" style="flex: 1; height: 100px; border-radius: 12px; background: rgba(255,255,255,0.02); min-width: 250px; display: none;" class="d-none d-lg-block"></div>
`;
mainAdminItemsLoadingIndicator.style.display = 'flex';
mainAdminItemsLoadingIndicator.style.width = '100%';
}
itemsContainer.appendChild(mainAdminItemsLoadingIndicator);
@@ -3812,6 +3815,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
card.innerHTML = card.dataset.savedHtml;
card.dataset.deloaded = 'false';
card.classList.remove('is-deloaded');
card.classList.remove('empty-frame');
const favBtn = card.querySelector('.bookmark-btn');
if (favBtn) {
favBtn.addEventListener('click', (ev) => {
@@ -3840,12 +3844,15 @@ document.addEventListener('DOMContentLoaded', ()=>{
card.innerHTML = '';
card.dataset.deloaded = 'true';
card.classList.add('is-deloaded');
card.classList.add('empty-frame');
}
}
}
});
}, {
rootMargin: isMobileLayout ? '1500px 0px 1500px 0px' : '0px 2500px 0px 2500px'
// Keep approx ~120 items rendered in DOM relative to scroll position.
// Assuming ~100-400px per item/row, this margin unloads elements beyond that range.
rootMargin: isMobileLayout ? '18000px 0px 18000px 0px' : '9000px 0px 9000px 0px'
});
}
+71 -24
View File
@@ -19,7 +19,7 @@
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Fach/Kategorie (Filter 1)</h2>
<h2 class="card-title h5 mb-0">{{ filter_names.get('1', 'Fach/Kategorie') }} (Filter 1)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=1) }}" class="mb-4">
@@ -33,13 +33,25 @@
{% if filter1_values %}
<div class="list-group">
{% for value in filter1_values %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ value }}
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-1-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=1, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-1-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=1, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-1-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
@@ -55,7 +67,7 @@
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">System/Bereich (Filter 2)</h2>
<h2 class="card-title h5 mb-0">{{ filter_names.get('2', 'System/Bereich') }} (Filter 2)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=2) }}" class="mb-4">
@@ -69,13 +81,25 @@
{% if filter2_values %}
<div class="list-group">
{% for value in filter2_values %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ value }}
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-2-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=2, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-2-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=2, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-2-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
@@ -91,7 +115,7 @@
<div class="col-md-4">
<div class="card mb-4">
<div class="card-header">
<h2 class="card-title h5 mb-0">Typ/Art (Filter 3)</h2>
<h2 class="card-title h5 mb-0">{{ filter_names.get('3', 'Typ/Art') }} (Filter 3)</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('add_filter_value', filter_num=3) }}" class="mb-4">
@@ -105,13 +129,25 @@
{% if filter3_values %}
<div class="list-group">
{% for value in filter3_values %}
<div class="list-group-item d-flex justify-content-between align-items-center">
{{ value }}
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center mb-2">
<span>{{ value }}</span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1" onclick="toggleEdit('edit-3-{{ loop.index }}')">Bearbeiten</button>
<form method="POST" action="{{ url_for('remove_filter_value', filter_num=3, value=value) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('Sind Sie sicher, dass Sie den Wert \"' + '{{ value }}'.replace(/'/g, '\\\'') + '\" löschen möchten?');">
Entfernen
</button>
</form>
</div>
</div>
<form id="edit-3-{{ loop.index }}" method="POST" action="{{ url_for('edit_filter_value', filter_num=3, old_value=value) }}" style="display: none;" class="mt-2">
<div class="input-group input-group-sm">
<input type="text" name="new_value" class="form-control" value="{{ value }}" required>
<button type="submit" class="btn btn-primary" onclick="return confirm('Tipp: Das Ändern des Namens aktualisiert auch alle Einträge in der Datenbank, die diesen Filter verwenden. Fortfahren?');">Speichern</button>
<button type="button" class="btn btn-secondary" onclick="toggleEdit('edit-3-{{ loop.index }}')">Abbrechen</button>
</div>
</form>
</div>
{% endfor %}
@@ -133,4 +169,15 @@
<a href="{{ url_for('home_admin') }}" class="btn btn-secondary">Zurück zur Admin-Übersicht</a>
</div>
</div>
<script>
function toggleEdit(id) {
const el = document.getElementById(id);
if (el.style.display === 'none') {
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
</script>
{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
from pymongo import MongoClient
import Web.settings as cfg
def get_filter_names():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = db.settings.find_one({'setting_type': 'filter_names'})
client.close()
if names:
return names.get('names', {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
})
return {
'1': 'Fach/Kategorie',
'2': 'System/Bereich',
'3': 'Typ/Art'
}
def set_filter_name(filter_num, name):
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
names = get_filter_names()
names[str(filter_num)] = name
db.settings.update_one(
{'setting_type': 'filter_names'},
{'$set': {'names': names}},
upsert=True
)
client.close()
return True