style: Remove tutorial upload functionality and associated template
This commit is contained in:
+29
-166
@@ -2466,62 +2466,35 @@ def my_instance_management():
|
|||||||
@login_required
|
@login_required
|
||||||
def my_tutorials():
|
def my_tutorials():
|
||||||
"""Display tutorial videos for the logged-in user."""
|
"""Display tutorial videos for the logged-in user."""
|
||||||
client = None
|
tutorials = [
|
||||||
origin = request.url_root.rstrip("/")
|
{
|
||||||
try:
|
"_id": "1",
|
||||||
client, col = _get_collection("tutorials")
|
"title": "Erste Schritte mit Invario",
|
||||||
tutorials = list(
|
"description": "Lernen Sie die Grundlagen von Invario und wie Sie Ihre erste Instanz einrichten.",
|
||||||
col.find(
|
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
{"published": True},
|
"thumbnail_url": "/static/images/tutorial-thumb-1.jpg",
|
||||||
{
|
"duration": "12:34",
|
||||||
"_id": 1,
|
"category": "Anfänger"
|
||||||
"title": 1,
|
},
|
||||||
"description": 1,
|
{
|
||||||
"video_url": 1,
|
"_id": "2",
|
||||||
"thumbnail_url": 1,
|
"title": "Inventarverwaltung",
|
||||||
"duration": 1,
|
"description": "Alles über die Verwaltung Ihres Inventars und der Ausleihvorgänge.",
|
||||||
"category": 1,
|
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
"created_at": 1,
|
"thumbnail_url": "/static/images/tutorial-thumb-2.jpg",
|
||||||
},
|
"duration": "18:45",
|
||||||
).sort("created_at", -1)
|
"category": "Funktionen"
|
||||||
)
|
},
|
||||||
except PyMongoError:
|
{
|
||||||
flash("Tutorials konnten nicht geladen werden.", "error")
|
"_id": "3",
|
||||||
tutorials = []
|
"title": "Admin-Funktionen",
|
||||||
finally:
|
"description": "Erweiterte Administratorfunktionen und Systemverwaltung.",
|
||||||
if client:
|
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
||||||
client.close()
|
"thumbnail_url": "/static/images/tutorial-thumb-3.jpg",
|
||||||
|
"duration": "22:15",
|
||||||
if not tutorials:
|
"category": "Admin"
|
||||||
tutorials = [
|
}
|
||||||
{
|
]
|
||||||
"_id": "1",
|
|
||||||
"title": "Erste Schritte mit Invario",
|
|
||||||
"description": "Lernen Sie die Grundlagen von Invario und wie Sie Ihre erste Instanz einrichten.",
|
|
||||||
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
||||||
"thumbnail_url": "/static/images/tutorial-thumb-1.jpg",
|
|
||||||
"duration": "12:34",
|
|
||||||
"category": "Anfänger"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_id": "2",
|
|
||||||
"title": "Inventarverwaltung",
|
|
||||||
"description": "Alles über die Verwaltung Ihres Inventars und der Ausleihvorgänge.",
|
|
||||||
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
||||||
"thumbnail_url": "/static/images/tutorial-thumb-2.jpg",
|
|
||||||
"duration": "18:45",
|
|
||||||
"category": "Funktionen"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_id": "3",
|
|
||||||
"title": "Admin-Funktionen",
|
|
||||||
"description": "Erweiterte Administratorfunktionen und Systemverwaltung.",
|
|
||||||
"video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
|
|
||||||
"thumbnail_url": "/static/images/tutorial-thumb-3.jpg",
|
|
||||||
"duration": "22:15",
|
|
||||||
"category": "Admin"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
normalized_tutorials = []
|
normalized_tutorials = []
|
||||||
for item in tutorials:
|
for item in tutorials:
|
||||||
@@ -2534,116 +2507,6 @@ def my_tutorials():
|
|||||||
|
|
||||||
return render_template("my_tutorials.html", tutorials=tutorials)
|
return render_template("my_tutorials.html", tutorials=tutorials)
|
||||||
|
|
||||||
@app.route('/append_video', methods=['POST'])
|
|
||||||
@admin_required
|
|
||||||
def append_video():
|
|
||||||
title = _sanitize_text(request.form.get("title") or "", 200)
|
|
||||||
description = _sanitize_text(request.form.get("description") or "", 500)
|
|
||||||
video_url = _sanitize_text(request.form.get("video_url") or "", 300)
|
|
||||||
thumbnail_url = _sanitize_text(request.form.get("thumbnail_url") or "", 300)
|
|
||||||
duration = _sanitize_text(request.form.get("duration") or "", 20)
|
|
||||||
category = _sanitize_text(request.form.get("category") or "", 50)
|
|
||||||
|
|
||||||
if not title:
|
|
||||||
flash("Bitte einen Titel für das Tutorial eingeben.", "error")
|
|
||||||
return redirect(url_for("admin_blog"))
|
|
||||||
|
|
||||||
if not video_url:
|
|
||||||
flash("Bitte eine Video-URL eingeben.", "error")
|
|
||||||
return redirect(url_for("admin_blog"))
|
|
||||||
|
|
||||||
normalized_video_url = _normalize_tutorial_video_url(video_url, request.url_root.rstrip("/"))
|
|
||||||
|
|
||||||
if not normalized_video_url:
|
|
||||||
flash(
|
|
||||||
"Ungültige Video-URL. Erlaubt sind YouTube-Links im Format watch?v=..., youtu.be/..., shorts/... oder embed/...",
|
|
||||||
"error",
|
|
||||||
)
|
|
||||||
return redirect(url_for("admin_blog"))
|
|
||||||
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("tutorials")
|
|
||||||
col.insert_one(
|
|
||||||
{
|
|
||||||
"title": title,
|
|
||||||
"description": description,
|
|
||||||
"video_url": normalized_video_url,
|
|
||||||
"thumbnail_url": thumbnail_url,
|
|
||||||
"duration": duration,
|
|
||||||
"category": category,
|
|
||||||
"published": True,
|
|
||||||
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
flash("Tutorial-Video hinzugefügt.", "success")
|
|
||||||
except PyMongoError:
|
|
||||||
flash("Tutorial-Video konnte nicht hinzugefügt werden.", "error")
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
return redirect(url_for("admin_blog"))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/tutorials/upload', methods=['GET', 'POST'])
|
|
||||||
@admin_required
|
|
||||||
def admin_tutorial_upload():
|
|
||||||
if request.method == 'POST':
|
|
||||||
title = _sanitize_text(request.form.get("title") or "", 200)
|
|
||||||
description = _sanitize_text(request.form.get("description") or "", 500)
|
|
||||||
duration = _sanitize_text(request.form.get("duration") or "", 20)
|
|
||||||
category = _sanitize_text(request.form.get("category") or "", 50)
|
|
||||||
thumbnail_url = _sanitize_text(request.form.get("thumbnail_url") or "", 300)
|
|
||||||
uploaded_file = request.files.get("video_file")
|
|
||||||
|
|
||||||
if not title:
|
|
||||||
flash("Bitte einen Titel fuer das Tutorial eingeben.", "error")
|
|
||||||
return redirect(url_for("admin_tutorial_upload"))
|
|
||||||
|
|
||||||
saved_video_path = _save_tutorial_video_file(uploaded_file, title)
|
|
||||||
if not saved_video_path:
|
|
||||||
flash("Bitte eine gueltige Videodatei hochladen (.mp4, .webm, .ogv, .ogg, .m4v).", "error")
|
|
||||||
return redirect(url_for("admin_tutorial_upload"))
|
|
||||||
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("tutorials")
|
|
||||||
col.insert_one(
|
|
||||||
{
|
|
||||||
"title": title,
|
|
||||||
"description": description,
|
|
||||||
"video_url": saved_video_path,
|
|
||||||
"thumbnail_url": thumbnail_url,
|
|
||||||
"duration": duration,
|
|
||||||
"category": category,
|
|
||||||
"published": True,
|
|
||||||
"created_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
|
||||||
"source": "upload",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
flash("Tutorial-Video wurde hochgeladen.", "success")
|
|
||||||
except PyMongoError:
|
|
||||||
flash("Tutorial-Video konnte nicht gespeichert werden.", "error")
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
return redirect(url_for("admin_tutorial_upload"))
|
|
||||||
|
|
||||||
tutorials = []
|
|
||||||
client = None
|
|
||||||
try:
|
|
||||||
client, col = _get_collection("tutorials")
|
|
||||||
tutorials = list(col.find({"source": "upload"}, {"_id": 1, "title": 1, "description": 1, "video_url": 1, "thumbnail_url": 1, "duration": 1, "category": 1, "created_at": 1}).sort("created_at", -1))
|
|
||||||
except PyMongoError:
|
|
||||||
flash("Upload-Uebersicht konnte nicht geladen werden.", "error")
|
|
||||||
finally:
|
|
||||||
if client:
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
return render_template("admin_tutorial_upload.html", tutorials=tutorials)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/chat', methods=['GET', 'POST'])
|
@app.route('/chat', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -1,276 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block title %}Admin | Tutorial-Upload{% endblock %}
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ super() }}
|
|
||||||
<style>
|
|
||||||
.upload-shell {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 0.9fr 1.1fr;
|
|
||||||
gap: 1.2rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #d7e1ea;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 14px 34px rgba(17, 43, 60, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-form {
|
|
||||||
padding: 1.2rem;
|
|
||||||
position: sticky;
|
|
||||||
top: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-form h1,
|
|
||||||
.list-panel h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: #0f354d;
|
|
||||||
font-family: "Merriweather", Georgia, serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-form p,
|
|
||||||
.list-panel p {
|
|
||||||
color: #587286;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field {
|
|
||||||
margin-top: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1f4b65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input,
|
|
||||||
.field textarea {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #c8d7e3;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0.7rem 0.8rem;
|
|
||||||
font: inherit;
|
|
||||||
color: #17384f;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field textarea {
|
|
||||||
min-height: 120px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field input:focus,
|
|
||||||
.field textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #2f79a7;
|
|
||||||
box-shadow: 0 0 0 3px rgba(58, 133, 180, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint {
|
|
||||||
margin-top: 0.4rem;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
color: #527085;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
margin-top: 1rem;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.8rem 1rem;
|
|
||||||
font-size: 0.96rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(120deg, #0a5f8f 0%, #08486c 100%);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-box {
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-radius: 14px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #d7e1ea;
|
|
||||||
background: linear-gradient(135deg, #f4f8fb 0%, #eaf1f6 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-aspect {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 56.25%;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-aspect video,
|
|
||||||
.preview-aspect img,
|
|
||||||
.preview-aspect iframe {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-empty {
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
color: #5c7485;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-list {
|
|
||||||
padding: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-list .entry {
|
|
||||||
border: 1px solid #d7e1ea;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-top: 0.85rem;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-list .entry h3 {
|
|
||||||
margin: 0 0 0.35rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
color: #113d59;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta {
|
|
||||||
font-size: 0.86rem;
|
|
||||||
color: #637b8c;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-link {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 0.55rem;
|
|
||||||
color: #0a4c74;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 0.24rem 0.55rem;
|
|
||||||
background: #e7f1f5;
|
|
||||||
color: #1f5c73;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-left: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
|
||||||
.upload-shell {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-form {
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="panel upload-form">
|
|
||||||
<h1>Tutorial-Upload</h1>
|
|
||||||
<p>Videodatei hochladen und sofort als Tutorial im Nutzerbereich bereitstellen.</p>
|
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('admin_tutorial_upload') }}" enctype="multipart/form-data">
|
|
||||||
<div class="field">
|
|
||||||
<label for="title">Titel</label>
|
|
||||||
<input id="title" name="title" type="text" placeholder="z. B. Erste Schritte mit Invario" required>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="video_file">Videodatei</label>
|
|
||||||
<input id="video_file" name="video_file" type="file" accept="video/mp4,video/webm,video/ogg,.mp4,.webm,.ogv,.ogg,.m4v" required>
|
|
||||||
<div class="hint">Erlaubte Formate: .mp4, .webm, .ogv, .ogg, .m4v</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="description">Beschreibung</label>
|
|
||||||
<textarea id="description" name="description" placeholder="Kurze Beschreibung des Tutorials"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="thumbnail_url">Thumbnail-URL (optional)</label>
|
|
||||||
<input id="thumbnail_url" name="thumbnail_url" type="url" placeholder="https://...">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="duration">Dauer (optional)</label>
|
|
||||||
<input id="duration" name="duration" type="text" placeholder="z. B. 12:34">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="category">Kategorie (optional)</label>
|
|
||||||
<input id="category" name="category" type="text" placeholder="z. B. Anfänger">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="submit-btn">Video hochladen</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="preview-box" aria-live="polite">
|
|
||||||
<div id="preview-empty" class="preview-empty">Vorschau erscheint nach Auswahl einer Videodatei.</div>
|
|
||||||
<div class="preview-aspect" id="preview-wrap" hidden>
|
|
||||||
<video id="preview-video" controls playsinline></video>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel upload-list">
|
|
||||||
<h2>Aktuelle Uploads</h2>
|
|
||||||
<p>Neu hochgeladene Tutorials erscheinen hier und im Nutzerbereich.</p>
|
|
||||||
|
|
||||||
{% if tutorials %}
|
|
||||||
{% for tutorial in tutorials %}
|
|
||||||
<article class="entry">
|
|
||||||
<h3>{{ tutorial.title }} <span class="badge">Upload</span></h3>
|
|
||||||
<div class="meta">
|
|
||||||
{{ tutorial.category or 'Allgemein' }} | {{ tutorial.duration or 'ohne Dauer' }} | {{ tutorial.created_at[:10] }}
|
|
||||||
</div>
|
|
||||||
<p>{{ tutorial.description }}</p>
|
|
||||||
<a class="video-link" href="{{ tutorial.video_url }}" target="_blank" rel="noopener">Video öffnen</a>
|
|
||||||
</article>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<article class="entry"><p>Noch keine hochgeladenen Tutorials vorhanden.</p></article>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const input = document.getElementById('video_file');
|
|
||||||
const previewWrap = document.getElementById('preview-wrap');
|
|
||||||
const previewEmpty = document.getElementById('preview-empty');
|
|
||||||
const previewVideo = document.getElementById('preview-video');
|
|
||||||
let currentObjectUrl = null;
|
|
||||||
|
|
||||||
input.addEventListener('change', function () {
|
|
||||||
const file = input.files && input.files[0];
|
|
||||||
if (currentObjectUrl) {
|
|
||||||
URL.revokeObjectURL(currentObjectUrl);
|
|
||||||
currentObjectUrl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
previewWrap.hidden = true;
|
|
||||||
previewEmpty.hidden = false;
|
|
||||||
previewVideo.removeAttribute('src');
|
|
||||||
previewVideo.load();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentObjectUrl = URL.createObjectURL(file);
|
|
||||||
previewVideo.src = currentObjectUrl;
|
|
||||||
previewVideo.load();
|
|
||||||
previewEmpty.hidden = true;
|
|
||||||
previewWrap.hidden = false;
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -523,7 +523,6 @@
|
|||||||
{% if session.get('is_admin') %}
|
{% if session.get('is_admin') %}
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a>
|
<a class="nav-user-link" href="{{ url_for('admin_invoices') }}">Rechnungsverwaltung</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
<a class="nav-user-link" href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_tutorial_upload') }}">Tutorial-Upload</a>
|
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
<a class="nav-user-link" href="{{ url_for('admin_chats') }}">Admin-Chat</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
<a class="nav-user-link" href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||||
<a class="nav-user-link" href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
<a class="nav-user-link" href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
||||||
@@ -551,7 +550,6 @@
|
|||||||
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
<a href="{{ url_for('admin_tickets') }}">Support-Center</a>
|
||||||
<a href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
<a href="{{ url_for('admin_system_tools') }}">System-Tools</a>
|
||||||
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
<a href="{{ url_for('admin_blog') }}">Blog verwalten</a>
|
||||||
<a href="{{ url_for('admin_tutorial_upload') }}">Tutorial-Upload</a>
|
|
||||||
<a href="{{ url_for('admin_instances') }}">Schul-Instanzen</a>
|
<a href="{{ url_for('admin_instances') }}">Schul-Instanzen</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
Reference in New Issue
Block a user