feat: Bearbeitungssperren und Versionskonfliktschutz ergänzen

This commit is contained in:
2026-07-23 13:13:34 +02:00
parent df11d0eb05
commit 601c7308ce
11 changed files with 234 additions and 5 deletions
@@ -135,12 +135,48 @@
});
}
});
{% if object.pk %}
const lockToken = document.getElementById('id_edit_lock_token');
const editForm = document.querySelector('form.object-edit');
const csrfToken = document.querySelector('input[name=csrfmiddlewaretoken]');
const lockUrl = "{% url 'plugins:netbox_documentation:document_lock' pk=object.pk %}";
let submitting = false;
const heartbeat = () => {
if (!lockToken || !lockToken.value || submitting) return;
const data = new URLSearchParams({token: lockToken.value, action: 'heartbeat'});
fetch(lockUrl, {
method: 'POST', credentials: 'same-origin', body: data,
headers: {'X-CSRFToken': csrfToken.value, 'Content-Type': 'application/x-www-form-urlencoded'}
}).then(response => {
if (response.status === 409) {
const warning = document.createElement('div');
warning.className = 'alert alert-danger position-fixed top-0 start-50 translate-middle-x mt-3';
warning.style.zIndex = '2000';
warning.textContent = 'Die Bearbeitungssperre ist abgelaufen oder wurde übernommen. Bitte Inhalt sichern und die Seite neu laden.';
document.body.appendChild(warning);
}
});
};
const heartbeatTimer = window.setInterval(heartbeat, 60000);
if (editForm) editForm.addEventListener('submit', () => { submitting = true; window.clearInterval(heartbeatTimer); });
window.addEventListener('beforeunload', () => {
if (submitting || !lockToken || !lockToken.value) return;
const data = new FormData();
data.append('csrfmiddlewaretoken', csrfToken.value);
data.append('token', lockToken.value);
data.append('action', 'release');
navigator.sendBeacon(lockUrl, data);
});
{% endif %}
});
</script>
{% endblock javascript %}
{% block pre_form_fields %}
{% if not object.pk %}
{% if object.pk %}
<div class="alert alert-success py-2"><i class="mdi mdi-lock-check"></i> Diese Dokumentation ist während der Bearbeitung für dich reserviert.</div>
{% else %}
<div class="alert alert-info">Speichere die neue Dokumentation einmal. Danach können Bilder direkt über die Editor-Werkzeugleiste hochgeladen werden.</div>
{% endif %}
{% endblock pre_form_fields %}
@@ -0,0 +1,24 @@
{% extends 'base/layout.html' %}
{% block title %}Bearbeitungskonflikt{% endblock %}
{% block content %}
<div class="alert alert-danger">
<h4><i class="mdi mdi-content-save-alert"></i> Neuere Version erkannt</h4>
<p>Die Dokumentation wurde seit dem Öffnen verändert. Deine Fassung wurde nicht gespeichert und die neuere Version wurde nicht überschrieben.</p>
</div>
<div class="row g-4">
<div class="col-lg-6">
<div class="card"><h5 class="card-header">Aktuelle gespeicherte Version</h5><div class="card-body">
<a href="{{ object.get_absolute_url }}" class="btn btn-primary" target="_blank">Aktuelle Version öffnen</a>
</div></div>
</div>
<div class="col-lg-6">
<div class="card"><h5 class="card-header">Dein nicht gespeicherter Inhalt</h5><div class="card-body">
<p><strong>Titel:</strong> {{ submitted_title }}</p>
<textarea class="form-control font-monospace" rows="24" readonly>{{ submitted_body }}</textarea>
<p class="text-muted mt-2">Diesen Inhalt kannst du kopieren und nach Prüfung in die aktuelle Version übernehmen.</p>
</div></div>
</div>
</div>
{% endblock content %}
@@ -0,0 +1,25 @@
{% extends 'base/layout.html' %}
{% block title %}Dokumentation wird bearbeitet{% endblock %}
{% block content %}
<div class="row justify-content-center"><div class="col-lg-7">
<div class="card border-warning">
<h5 class="card-header"><i class="mdi mdi-lock-clock"></i> Bearbeitung derzeit gesperrt</h5>
<div class="card-body">
{% if lost %}
<p>Deine Bearbeitungssperre ist nicht mehr gültig. Der Inhalt wurde nicht überschrieben.</p>
{% elif lock.user %}
<p><strong>{{ lock.user }}</strong> bearbeitet „{{ object.title }}“ momentan.</p>
{% else %}
<p>Diese Dokumentation besitzt momentan eine aktive Bearbeitungssperre.</p>
{% endif %}
<p class="text-muted">Ohne Heartbeat läuft eine Sperre nach {{ timeout }} Minuten automatisch ab.</p>
<div class="btn-list">
<a href="{{ object.get_absolute_url }}" class="btn btn-outline-secondary">Zur Dokumentation</a>
<a href="{% url 'plugins:netbox_documentation:document_edit' pk=object.pk %}" class="btn btn-primary">Erneut versuchen</a>
</div>
</div>
</div>
</div></div>
{% endblock content %}