diff --git a/.eslintrc.js b/.eslintrc.js index cc8bbe144a..213839b54f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,7 +38,7 @@ module.exports = { "interpolate": "readonly", "bootstrap": "readonly", "videojs": "readonly", - "CKEDITOR": "readonly", + "tinyMCE": "readonly", "send_form_data": "writable", "showalert": "writable", "showLoader": "writable", diff --git a/.github/workflows/pod_dev.yml b/.github/workflows/pod_dev.yml index a0b21c0536..70f9e176e9 100644 --- a/.github/workflows/pod_dev.yml +++ b/.github/workflows/pod_dev.yml @@ -5,14 +5,12 @@ run-name: ${{ github.actor }} is testing Pod encoding in Dev 🚀 on: push: branches: - - develop - dev_v4 - - features/** - dependabot/** pull_request: branches: - - develop - dev_v4 + - pod_v4 workflow_dispatch: env: diff --git a/pod/completion/models.py b/pod/completion/models.py index e4867ad097..cac2812e32 100644 --- a/pod/completion/models.py +++ b/pod/completion/models.py @@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from django.template.defaultfilters import slugify -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField from pod.video.models import Video from pod.video.utils import verify_field_length from pod.main.models import get_nextautoincrement @@ -352,7 +352,7 @@ class Overlay(models.Model): default=2, help_text=_("End time of the overlay, in seconds."), ) - content = RichTextField(_("Content"), null=False, blank=False, config_name="complete") + content = HTMLField(_("Content"), null=False, blank=False) position = models.CharField( _("Position"), max_length=100, diff --git a/pod/enrichment/models.py b/pod/enrichment/models.py index a74dbc044d..902f6aea42 100755 --- a/pod/enrichment/models.py +++ b/pod/enrichment/models.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext as _ from django.template.defaultfilters import slugify -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField from tempfile import NamedTemporaryFile from webvtt import WebVTT, Caption @@ -156,7 +156,7 @@ class Enrichment(models.Model): on_delete=models.CASCADE, help_text=_("Integrate a document (PDF, text, html)"), ) - richtext = RichTextField(_("Richtext"), config_name="complete", blank=True) + richtext = HTMLField(_("Richtext"), blank=True) weblink = models.URLField(_("Web link"), max_length=200, null=True, blank=True) embed = models.TextField( _("Embed code"), diff --git a/pod/live/models.py b/pod/live/models.py index dd58d22339..4ffcf11db0 100644 --- a/pod/live/models.py +++ b/pod/live/models.py @@ -3,7 +3,7 @@ import hashlib import os -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User @@ -146,7 +146,7 @@ class Broadcaster(models.Model): building = models.ForeignKey( "Building", verbose_name=_("Building"), on_delete=models.CASCADE ) - description = RichTextField(_("description"), config_name="complete", blank=True) + description = HTMLField(_("description"), blank=True) poster = models.ForeignKey( CustomImageModel, models.SET_NULL, blank=True, null=True, verbose_name=_("Poster") ) @@ -331,9 +331,8 @@ class Event(models.Model): "of the content. (max length: 250 characters)" ), ) - description = RichTextField( + description = HTMLField( _("Description"), - config_name="complete", blank=True, help_text=_( "In this field you can describe your content, " diff --git a/pod/main/admin.py b/pod/main/admin.py index d6234d83f5..1bf8298de0 100644 --- a/pod/main/admin.py +++ b/pod/main/admin.py @@ -1,6 +1,6 @@ """Esup-Pod main admin page.""" -from ckeditor.widgets import CKEditorWidget +from tinymce.widgets import TinyMCE from django.contrib import admin from django import forms from django.contrib.flatpages.admin import FlatpageForm @@ -18,9 +18,7 @@ SITE_ID = getattr(settings, "SITE_ID", 1) content_widget = {} for key, value in settings.LANGUAGES: - content_widget["content_%s" % key.replace("-", "_")] = CKEditorWidget( - config_name="complete" - ) + content_widget["content_%s" % key.replace("-", "_")] = TinyMCE() class PageForm(FlatpageForm): diff --git a/pod/main/models.py b/pod/main/models.py index 2e4cf4bc6a..55584299fe 100644 --- a/pod/main/models.py +++ b/pod/main/models.py @@ -12,7 +12,7 @@ from django.db.models import Max import os import mimetypes -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField FILES_DIR = getattr(settings, "FILES_DIR", "files") @@ -289,8 +289,7 @@ class Block(models.Model): help_text=_("Select the playlist you want to link with."), ) - html = RichTextField( - config_name="complete", + html = HTMLField( verbose_name=_("HTML"), null=True, blank=True, diff --git a/pod/main/settings.py b/pod/main/settings.py index a00641b644..2016a7154e 100644 --- a/pod/main/settings.py +++ b/pod/main/settings.py @@ -133,60 +133,6 @@ # WARNING: this folder must have previously been created. MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") -## -# CKeditor settings -# -# CKEDITOR_BASEPATH = os.path.join(STATIC_URL, 'ckeditor', "/") -CKEDITOR_UPLOAD_PATH = os.path.join(MEDIA_ROOT, "uploads") -CKEDITOR_CONFIGS = { - "complete": {"toolbar": "full", "height": 300, "width": "100%"}, - "default": { - "height": 300, - "width": "100%", - "toolbar": "custom", - "language": "fr", - "toolbar_custom": [ - { - "name": "basicstyles", - "items": [ - "Bold", - "Italic", - "Underline", - "Strike", - "Subscript", - "Superscript", - "-", - "RemoveFormat", - ], - }, - { - "name": "paragraph", - "items": [ - "NumberedList", - "BulletedList", - "-", - "Outdent", - "Indent", - "-", - "Blockquote", - "CreateDiv", - "-", - "JustifyLeft", - "JustifyCenter", - "JustifyRight", - "JustifyBlock", - "-", - "BidiLtr", - "BidiRtl", - ], - }, - {"name": "links", "items": ["Link", "Unlink", "Anchor"]}, - {"name": "tools", "items": ["Maximize"]}, - ], - "removePlugins": "exportpdf", - }, -} - ## # Video tiers apps settings diff --git a/pod/main/static/css/pod-admin.css b/pod/main/static/css/pod-admin.css index c0ae01257c..2345656bea 100644 --- a/pod/main/static/css/pod-admin.css +++ b/pod/main/static/css/pod-admin.css @@ -63,3 +63,8 @@ fieldset.collapse:not(.show) { display: block; } + +/* Corrections on Django admin base.css */ +select { + height: auto; +} diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index 87bbbac328..235975fd47 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -719,11 +719,6 @@ div.card a img { content: "▲"; } -/** ckeditor **/ -.django-ckeditor-widget { - width: 100%; -} - /** Dashboard **/ #bulk-update-container { background-color: var(--pod-background-neutre2-bloc); diff --git a/pod/main/static/js/main.js b/pod/main/static/js/main.js index 47ad0f2740..67c7f9049e 100644 --- a/pod/main/static/js/main.js +++ b/pod/main/static/js/main.js @@ -148,10 +148,9 @@ var slideToggle = (target, duration = 500) => { }; /** - * [fadeIn description] - * @param {[type]} el [description] - * @param {[type]} display [description] - * @return {[type]} [description] + * Show HTML element whith fade effect + * @param {HTMLElement} el Dom element to show + * @param {string} display Display property (default: block) */ function fadeIn(el, display) { el.style.opacity = 0; @@ -159,7 +158,7 @@ function fadeIn(el, display) { el.style.display = display || "block"; (function fade() { var val = parseFloat(el.style.opacity); - if (!((val += 0.1) > 1)) { + if (!((val += 0.04) > 1)) { el.style.opacity = val; requestAnimationFrame(fade); } @@ -167,15 +166,14 @@ function fadeIn(el, display) { } /** - * [fadeOut description] - * @param {[type]} elem [description] + * Hide element whith fade effect + * @param {HTMLElement} elem Dom element to hide * @param {[type]} speed [description] - * @return {[type]} [description] */ function fadeOut(elem, speed) { if (!elem.style.opacity) { elem.style.opacity = 1; - } // end if + } var outInterval = setInterval(function () { elem.style.opacity -= 0.02; @@ -185,9 +183,10 @@ function fadeOut(elem, speed) { elem.style.opacity = Number(elem.style.opacity) + 0.02; if (elem.style.opacity >= 1) clearInterval(inInterval); }, speed / 50); - } // end if + } }, speed / 50); } + /** * [isJson description] * @param {[type]} str [description] @@ -1104,6 +1103,7 @@ var append_picture_form = async function (data) { userPictureModal.show(); } }; + /** * [show_form_theme description] * @param {[type]} data [description] @@ -1112,7 +1112,31 @@ var append_picture_form = async function (data) { function show_form_theme(data) { let div_form = document.getElementById("div_form_theme"); div_form.style.display = "none"; + + // Destroy all WYSIWYG before replacing content + var theme_descriptions = document.querySelectorAll( + "textarea[id^='id_description']", + ); + + theme_descriptions.forEach((el) => { + let t=tinyMCE.get(el.id); + if (t) { + t.remove(); + } + }); + div_form.innerHTML = data; + + // Reinit WYSIWYG on newly loaded textarea + theme_descriptions = document.querySelectorAll( + "textarea[id^='id_description']", + ); + + theme_descriptions.forEach((el) => { + let mce_conf = JSON.parse(el.dataset.mceConf); + tinyMCE.init(mce_conf); + }); + fadeIn(div_form); if (data != "") document.querySelector("form.get_form_theme").style.display = "none"; @@ -1120,14 +1144,6 @@ function show_form_theme(data) { top: parseInt(document.getElementById("div_form_theme").offsetTop, 10), behavior: "smooth", }); - // Add CKEditor when edit a theme - // For all descriptions, except description help - const theme_descriptions = document.querySelectorAll( - "textarea[id^='id_description']", - ); - theme_descriptions.forEach((theme_description) => { - CKEDITOR.replace(theme_description.id); - }); } /** * [show_list_theme description] diff --git a/pod/recorder/models.py b/pod/recorder/models.py index 979510f812..f5a735dc51 100644 --- a/pod/recorder/models.py +++ b/pod/recorder/models.py @@ -3,7 +3,7 @@ import os import importlib -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField from django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -89,7 +89,7 @@ class Recorder(models.Model): # Recorder name name = models.CharField(_("name"), max_length=200, unique=True) # Description of the recorder - description = RichTextField(_("description"), config_name="complete", blank=True) + description = HTMLField(_("description"), blank=True) # IP address of the recorder address_ip = models.GenericIPAddressField( _("Address IP"), unique=True, help_text=_("IP address of the recorder.") diff --git a/pod/settings.py b/pod/settings.py index 7dbfdb1891..21fab78681 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -43,7 +43,7 @@ "django.contrib.sites", "django.contrib.flatpages", # Exterior Applications - "ckeditor", + "tinymce", "sorl.thumbnail", # "tagging", "tagulous", diff --git a/pod/urls.py b/pod/urls.py index 75abf501bb..dbe642e094 100644 --- a/pod/urls.py +++ b/pod/urls.py @@ -48,6 +48,8 @@ path("robots.txt", robots_txt), path("info_pod.json", info_pod), re_path(r"^admin/", admin.site.urls), + # WYSIWYG editor + path('tinymce/', include('tinymce.urls')), # Translation path("i18n/", include("django.conf.urls.i18n")), path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), diff --git a/pod/video/admin.py b/pod/video/admin.py index aa09747e68..2cc052bf3c 100644 --- a/pod/video/admin.py +++ b/pod/video/admin.py @@ -211,6 +211,7 @@ def get_form(self, request, obj=None, **kwargs): exclude += ( "video", "owner", + "thumbnail" ) if not USE_TRANSCRIPTION: exclude += ("transcript",) @@ -228,19 +229,19 @@ def get_form(self, request, obj=None, **kwargs): actions = ["encode_video", "draft_video"] @admin.action(description=_("Set as draft")) - def draft_video(self, request, queryset): + def draft_video(self, request, queryset) -> None: for item in queryset: item.is_draft = True item.save() @admin.action(description=_("Encode selected")) - def encode_video(self, request, queryset): + def encode_video(self, request, queryset) -> None: for item in queryset: item.launch_encode = True item.save() @admin.action(description=_("Transcript selected")) - def transcript_video(self, request, queryset): + def transcript_video(self, request, queryset) -> None: for item in queryset: if item.get_video_mp3() and not item.encoding_in_progress: transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) diff --git a/pod/video/forms.py b/pod/video/forms.py index 68337a0cb4..64a7f71610 100644 --- a/pod/video/forms.py +++ b/pod/video/forms.py @@ -28,7 +28,7 @@ from django.contrib.sites.shortcuts import get_current_site from pod.main.forms_utils import add_placeholder_and_asterisk, add_describedby_attr -from ckeditor.widgets import CKEditorWidget +from tinymce.widgets import TinyMCE from collections import OrderedDict from django_select2 import forms as s2forms @@ -959,7 +959,7 @@ def __init__(self, *args, **kwargs) -> None: ) self.__init_instance__() - def __init_instance__(self): + def __init_instance__(self) -> None: """Initialize a new VideoForm instance for visibility field.""" if self.instance: if self.instance.is_draft: @@ -993,10 +993,10 @@ def set_nostaff_config(self) -> None: if self.is_staff is False: del self.fields["thumbnail"] - self.fields["description"].widget = CKEditorWidget(config_name="default") + self.fields["description"].widget = TinyMCE() for key, _value in settings.LANGUAGES: self.fields["description_%s" % key.replace("-", "_")].widget = ( - CKEditorWidget(config_name="default") + TinyMCE() ) if self.fields.get("date_delete"): if self.is_staff is False or USE_OBSOLESCENCE is False: @@ -1164,10 +1164,10 @@ def __init__(self, *args, **kwargs) -> None: self.is_staff is False and self.is_superuser is False ): del self.fields["headband"] - self.fields["description"].widget = CKEditorWidget(config_name="default") + self.fields["description"].widget = TinyMCE() for key, _value in settings.LANGUAGES: self.fields["description_%s" % key.replace("-", "_")].widget = ( - CKEditorWidget(config_name="default") + TinyMCE() ) # hide default langage self.fields["description_%s" % settings.LANGUAGE_CODE].widget = ( @@ -1234,12 +1234,10 @@ def __init__(self, *args, **kwargs) -> None: self.fields["channel"].widget = forms.HiddenInput() # self.fields["parentId"].label = _('Theme parent') - # Add CKEditor when edit a theme - self.fields["description"].widget = CKEditorWidget(config_name="complete") + # Add WYSIWYG when edit a theme + self.fields["description"].widget = TinyMCE() for key, _value in settings.LANGUAGES: - self.fields["description_%s" % key.replace("-", "_")].widget = CKEditorWidget( - config_name="complete" - ) + self.fields["description_%s" % key.replace("-", "_")].widget = TinyMCE() if "channel" in self.initial.keys(): themes_queryset = Theme.objects.filter(channel=self.initial["channel"]) diff --git a/pod/video/models.py b/pod/video/models.py index 81bc167b85..e2fb3a114c 100644 --- a/pod/video/models.py +++ b/pod/video/models.py @@ -31,7 +31,7 @@ from django.utils import timezone from django.utils.html import format_html, escape from django.utils.text import capfirst -from ckeditor.fields import RichTextField +from tinymce.models import HTMLField from django.contrib.sites.models import Site from django.db.models.signals import post_save from django.db.models.signals import pre_save @@ -304,9 +304,8 @@ class Channel(models.Model): ), editable=False, ) - description = RichTextField( + description = HTMLField( _("Description"), - config_name="complete", blank=True, help_text=_( "In this field you can describe your content, " @@ -726,9 +725,8 @@ class Video(models.Model): + "that they can’t delete this media." ), ) - description = RichTextField( + description = HTMLField( _("Description"), - config_name="complete", blank=True, help_text=_( "Describe your content, add all needed related information, " diff --git a/pod/video/static/js/dashboard.js b/pod/video/static/js/dashboard.js index 5ffb93ce84..f74e5529b3 100644 --- a/pod/video/static/js/dashboard.js +++ b/pod/video/static/js/dashboard.js @@ -147,7 +147,7 @@ async function bulkUpdate() { dashboardValue = element.checked; break; case "textarea": - dashboardValue = CKEDITOR.instances[element.id].getData(); + dashboardValue = tinyMCE.get("id_" + element.getAttribute("name")).getContent(); break; default: dashboardValue = document.getElementById( diff --git a/pod/video/templates/channel/form_theme.html b/pod/video/templates/channel/form_theme.html index 33781befa8..ede7e6a40c 100644 --- a/pod/video/templates/channel/form_theme.html +++ b/pod/video/templates/channel/form_theme.html @@ -1,7 +1,7 @@ {# HTML for theme form. Don't use this file alone it must be integrated into another template! #} {% load i18n %} {% load static %} - +{{form_theme.media.css}}
-{{form_theme.media}} +{{form_theme.media.js}} diff --git a/requirements.txt b/requirements.txt index e1e7162a26..dfcd2ab535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -r requirements-encode.txt -Django==4.2.16 -django-ckeditor==6.7.1 +Django==4.2.17 Pillow==10.3.0 +django-tinymce==4.1.0 django-tagulous==2.1.0 django-modeltranslation==0.19.11 django-cas-client==1.5.3