fix: TinyMCE über lokalen Plugin-Endpunkt ausliefern

This commit is contained in:
2026-07-22 10:51:23 +02:00
parent a7c0db2685
commit da0774b3aa
6 changed files with 28 additions and 9 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ Ein in NetBox integriertes Markdown-Wiki für Betriebsdokumentationen und Anleit
## Kompatibilität ## Kompatibilität
Die Version `0.2.1` zielt auf NetBox 4.x (mindestens 4.0). Vor einem produktiven Rollout sollte das Plugin gegen die konkret eingesetzte NetBox-Minor-Version in einer Testinstanz geprüft werden. Die Version `0.2.2` zielt auf NetBox 4.x (mindestens 4.0). Vor einem produktiven Rollout sollte das Plugin gegen die konkret eingesetzte NetBox-Minor-Version in einer Testinstanz geprüft werden.
## Installation ## Installation
@@ -78,7 +78,7 @@ PLUGINS_CONFIG = {
} }
``` ```
TinyMCE wird als Python-Abhängigkeit des Plugins installiert und durch `collectstatic` lokal bereitgestellt. Der Editor benötigt deshalb weder Zugriff auf ein CDN noch einen API-Key. Das verwendete TinyMCE wird im GPL-Modus betrieben. Kann das Script nicht geladen werden, bleibt als Rückfall ein normales HTML-Textfeld verfügbar. TinyMCE wird als Python-Abhängigkeit des Plugins installiert und über einen lokalen, gecachten Plugin-Endpunkt bereitgestellt. `collectstatic` übernimmt die Dateien zusätzlich in NetBox. Der Editor benötigt deshalb weder Zugriff auf ein CDN noch einen API-Key. Das verwendete TinyMCE wird im GPL-Modus betrieben. Kann das Script nicht geladen werden, bleibt als Rückfall ein normales HTML-Textfeld verfügbar.
Danach die Migrationen und statischen Dateien aktualisieren und NetBox neu starten: Danach die Migrationen und statischen Dateien aktualisieren und NetBox neu starten:
+1 -1
View File
@@ -5,7 +5,7 @@ class DocumentationConfig(PluginConfig):
name = "netbox_documentation" name = "netbox_documentation"
verbose_name = "Dokumentation" verbose_name = "Dokumentation"
description = "Wiki und Office-Dokumentation direkt in NetBox" description = "Wiki und Office-Dokumentation direkt in NetBox"
version = "0.2.1" version = "0.2.2"
author = "NetBox Documentation Contributors" author = "NetBox Documentation Contributors"
base_url = "documentation" base_url = "documentation"
min_version = "4.0.0" min_version = "4.0.0"
@@ -1,23 +1,24 @@
{% extends 'generic/object_edit.html' %} {% extends 'generic/object_edit.html' %}
{% load static %}
{% block javascript %} {% block javascript %}
{{ block.super }} {{ block.super }}
<script src="{% static 'tinymce/tinymce.min.js' %}"></script> <script src="{% url 'plugins:netbox_documentation:editor_asset' asset_path='tinymce.min.js' %}"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
const editorAssetBase = "{% url 'plugins:netbox_documentation:editor_asset' asset_path='_' %}".replace(/_$/, '');
if (typeof tinymce === 'undefined') { if (typeof tinymce === 'undefined') {
const editor = document.querySelector('[data-rich-text-editor]'); const editor = document.querySelector('[data-rich-text-editor]');
if (editor) { if (editor) {
const warning = document.createElement('div'); const warning = document.createElement('div');
warning.className = 'alert alert-warning'; warning.className = 'alert alert-warning';
warning.textContent = 'Der Rich-Text-Editor konnte nicht geladen werden. Der Inhalt kann als HTML bearbeitet werden.'; warning.textContent = 'Der Rich-Text-Editor konnte nicht vom lokalen Plugin-Endpunkt geladen werden. Bitte Browser-Konsole und Netzwerkprotokoll prüfen.';
editor.parentNode.insertBefore(warning, editor); editor.parentNode.insertBefore(warning, editor);
} }
return; return;
} }
tinymce.init({ tinymce.init({
selector: 'textarea[data-rich-text-editor]', selector: 'textarea[data-rich-text-editor]',
base_url: editorAssetBase.replace(/\/$/, ''),
suffix: '.min',
license_key: 'gpl', license_key: 'gpl',
height: 650, height: 650,
menubar: 'file edit view insert format tools table help', menubar: 'file edit view insert format tools table help',
+1 -1
View File
@@ -3,6 +3,7 @@ from netbox.views.generic import ObjectChangeLogView
from . import models, views from . import models, views
urlpatterns = ( urlpatterns = (
path("editor-assets/<path:asset_path>", views.EditorAssetView.as_view(), name="editor_asset"),
path("", views.DocumentListView.as_view(), name="document_list"), path("", views.DocumentListView.as_view(), name="document_list"),
path("documents/add/", views.DocumentEditView.as_view(), name="document_add"), path("documents/add/", views.DocumentEditView.as_view(), name="document_add"),
path("documents/import/", views.DocumentImportView.as_view(), name="document_import"), path("documents/import/", views.DocumentImportView.as_view(), name="document_import"),
@@ -15,4 +16,3 @@ urlpatterns = (
path("assignments/<int:pk>/edit/", views.AssignmentEditView.as_view(), name="documentassignment_edit"), path("assignments/<int:pk>/edit/", views.AssignmentEditView.as_view(), name="documentassignment_edit"),
path("assignments/<int:pk>/delete/", views.AssignmentDeleteView.as_view(), name="documentassignment_delete"), path("assignments/<int:pk>/delete/", views.AssignmentDeleteView.as_view(), name="documentassignment_delete"),
) )
+18
View File
@@ -1,8 +1,11 @@
from pathlib import Path from pathlib import Path
import mimetypes
import tinymce
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db import transaction from django.db import transaction
from django.http import FileResponse, Http404
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.text import slugify from django.utils.text import slugify
from django.views import View from django.views import View
@@ -14,6 +17,21 @@ from .models import Document, DocumentAssignment, DocumentAttachment
from .tables import DocumentTable, AssignmentTable from .tables import DocumentTable, AssignmentTable
class EditorAssetView(View):
"""Serve the bundled editor assets when NetBox's static proxy is unavailable."""
def get(self, request, asset_path):
root = (Path(tinymce.__file__).resolve().parent / "static" / "tinymce").resolve()
candidate = (root / asset_path).resolve()
if root not in candidate.parents or not candidate.is_file():
raise Http404
content_type = mimetypes.guess_type(candidate.name)[0] or "application/octet-stream"
response = FileResponse(candidate.open("rb"), content_type=content_type)
response["Cache-Control"] = "public, max-age=31536000, immutable"
response["X-Content-Type-Options"] = "nosniff"
return response
class DocumentListView(generic.ObjectListView): class DocumentListView(generic.ObjectListView):
queryset = Document.objects.prefetch_related("assignments") queryset = Document.objects.prefetch_related("assignments")
table = DocumentTable table = DocumentTable
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "netbox-documentation" name = "netbox-documentation"
version = "0.2.1" version = "0.2.2"
description = "Integrated Markdown wiki and office document importer for NetBox" description = "Integrated Markdown wiki and office document importer for NetBox"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"