Fix Vorlagen editor: drop Summernote, use code editor for all templates (STI-82)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled

Summernote WYSIWYG was mangling Django template syntax ({{ }}, {% %})
on save, causing content to revert to corrupted state. Switched all
template types to the plain code editor textarea which preserves
content exactly as-is.

Also removed jQuery/Summernote JS dependencies from the editor page,
and fixed getEditorContent reference in preview code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-21 21:55:17 +00:00
parent b8fb35db7a
commit fe2c657586
2 changed files with 27 additions and 111 deletions

View File

@@ -57,10 +57,9 @@ def vorlage_editor(request, pk):
html_json = json.dumps(vorlage.html_inhalt) html_json = json.dumps(vorlage.html_inhalt)
html_json = html_json.replace("<", "\\u003c").replace(">", "\\u003e") html_json = html_json.replace("<", "\\u003c").replace(">", "\\u003e")
# Serienbrief templates are full HTML documents with Django template tags # All templates contain Django template tags ({{ }}, {% %}) that
# ({% for %}, {% if %}) — Summernote WYSIWYG mangles these. # Summernote WYSIWYG mangles on save. Use plain code editor for all.
# Use a plain code editor textarea instead. use_code_editor = True
use_code_editor = vorlage.kategorie == "serienbrief"
return render(request, "stiftung/vorlage_editor.html", { return render(request, "stiftung/vorlage_editor.html", {
"vorlage": vorlage, "vorlage": vorlage,

View File

@@ -1,11 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %}
{% block title %}{{ vorlage.bezeichnung }} Vorlage bearbeiten{% endblock %} {% block title %}{{ vorlage.bezeichnung }} Vorlage bearbeiten{% endblock %}
{% block extra_css %} {% block extra_css %}
<!-- Summernote WYSIWYG (lokal) -->
<link rel="stylesheet" href="{% static 'stiftung/vendor/summernote/summernote-bs5.min.css' %}">
<style> <style>
.preview-frame { .preview-frame {
width: 100%; width: 100%;
@@ -26,9 +22,6 @@
.var-item:hover { .var-item:hover {
background-color: #e9ecef; background-color: #e9ecef;
} }
.note-editor.note-frame {
border-radius: 4px;
}
.code-editor-textarea { .code-editor-textarea {
width: 100%; width: 100%;
height: 70vh; height: 70vh;
@@ -113,7 +106,7 @@
<div class="{% if variablen %}col-lg-9{% else %}col-12{% endif %}"> <div class="{% if variablen %}col-lg-9{% else %}col-12{% endif %}">
<form method="post" id="editor-form"> <form method="post" id="editor-form">
{% csrf_token %} {% csrf_token %}
<textarea name="html_inhalt" id="code-editor"{% if use_code_editor %} class="code-editor-textarea"{% endif %}>{{ vorlage.html_inhalt }}</textarea> <textarea name="html_inhalt" id="code-editor" class="code-editor-textarea">{{ vorlage.html_inhalt }}</textarea>
<script type="application/json" id="vorlage-html-inhalt">{{ html_inhalt_json }}</script> <script type="application/json" id="vorlage-html-inhalt">{{ html_inhalt_json }}</script>
</form> </form>
</div> </div>
@@ -179,11 +172,6 @@
{% endblock %} {% endblock %}
{% block javascript %} {% block javascript %}
<!-- jQuery (lokal) -->
<script src="{% static 'stiftung/vendor/jquery/jquery.min.js' %}"></script>
<!-- Summernote WYSIWYG (lokal) -->
<script src="{% static 'stiftung/vendor/summernote/summernote-bs5.min.js' %}"></script>
<script src="{% static 'stiftung/vendor/summernote/summernote-de-DE.min.js' %}"></script>
<script> <script>
(function() { (function() {
var initialContent; var initialContent;
@@ -193,22 +181,11 @@
initialContent = null; initialContent = null;
} }
var useCodeEditor = {{ use_code_editor|yesno:"true,false" }};
var editor = document.getElementById('code-editor'); var editor = document.getElementById('code-editor');
var summernoteActive = false;
// Returns current HTML content regardless of editor mode // ---- Editor setup: plain code editor for all templates ----
function getEditorContent() { // Always load content from JSON (the textarea's Django-rendered value may be HTML-escaped)
if (summernoteActive) { if (initialContent !== null) {
return $('#code-editor').summernote('code');
}
return editor.value;
}
// ---- Editor setup ----
if (useCodeEditor) {
// Plain textarea for full HTML documents (Serienbrief)
if (initialContent) {
editor.value = initialContent; editor.value = initialContent;
} }
editor.addEventListener('keydown', function(e) { editor.addEventListener('keydown', function(e) {
@@ -220,7 +197,7 @@
this.selectionStart = this.selectionEnd = start + 4; this.selectionStart = this.selectionEnd = start + 4;
} }
}); });
// Variable insertion for code editor // Variable insertion
document.querySelectorAll('.var-item').forEach(function(row) { document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() { row.addEventListener('click', function() {
var varName = this.getAttribute('data-var'); var varName = this.getAttribute('data-var');
@@ -231,73 +208,13 @@
editor.focus(); editor.focus();
}); });
}); });
} else if (typeof $ !== 'undefined' && typeof $.fn.summernote !== 'undefined') {
// Summernote WYSIWYG for HTML fragment templates
$('#code-editor').summernote({
lang: 'de-DE',
height: 520,
toolbar: [
['style', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
['para', ['style', 'ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'hr']],
['view', ['fullscreen', 'codeview', 'undo', 'redo']],
],
callbacks: {
onInit: function() {
if (initialContent) {
$('#code-editor').summernote('code', initialContent);
}
}
}
});
summernoteActive = true;
// Variable insertion for Summernote
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
$('#code-editor').summernote('focus');
$('#code-editor').summernote('insertText', placeholder);
});
});
// Sync Summernote to textarea on form submit
document.getElementById('editor-form').addEventListener('submit', function() {
document.querySelector('textarea[name=html_inhalt]').value = getEditorContent();
});
} else {
// Fallback: Summernote not loaded — style textarea as code editor
if (editor) {
editor.style.height = '70vh';
editor.style.fontFamily = "'SFMono-Regular', Consolas, monospace";
editor.style.fontSize = '13px';
editor.style.padding = '12px';
editor.style.background = '#f8f9fa';
}
if (initialContent) {
editor.value = initialContent;
}
// Variable insertion for plain textarea fallback
document.querySelectorAll('.var-item').forEach(function(row) {
row.addEventListener('click', function() {
var varName = this.getAttribute('data-var');
var placeholder = String.fromCharCode(123,123) + ' ' + varName + ' ' + String.fromCharCode(125,125);
var start = editor.selectionStart;
editor.value = editor.value.substring(0, start) + placeholder + editor.value.substring(editor.selectionEnd);
editor.selectionStart = editor.selectionEnd = start + placeholder.length;
editor.focus();
});
});
}
// ---- Preview (always set up, independent of editor mode) ---- // ---- Preview (always set up, independent of editor mode) ----
var previewFrame = document.getElementById('preview-frame'); var previewFrame = document.getElementById('preview-frame');
var previewLoading = document.getElementById('preview-loading'); var previewLoading = document.getElementById('preview-loading');
function loadPreview() { function loadPreview() {
var content = getEditorContent(); var content = editor.value;
var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]'); var csrfEl = document.querySelector('[name=csrfmiddlewaretoken]');
if (!csrfEl) { if (!csrfEl) {
previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>CSRF-Token nicht gefunden.'; previewLoading.innerHTML = '<i class="fas fa-exclamation-triangle text-danger fa-2x mb-2 d-block"></i>CSRF-Token nicht gefunden.';