diff --git a/adhocracy4/ckeditor/fields.py b/adhocracy4/ckeditor/fields.py index 9bcbb1246..cdb63c43c 100644 --- a/adhocracy4/ckeditor/fields.py +++ b/adhocracy4/ckeditor/fields.py @@ -1,23 +1,10 @@ from ckeditor.fields import RichTextField from ckeditor_uploader.fields import RichTextUploadingField -_extra_plugins = ["collapsibleItem", "embed", "embedbase"] -_external_plugin_resources = [ - ( - "collapsibleItem", - "/static/ckeditor_collapsible/", - "plugin.js", - ) -] - +# FIXME: remove these fields / file class RichTextCollapsibleMixin: - def __init__(self, *args, **kwargs): - kwargs["extra_plugins"] = kwargs.get("extra_plugins", []) + _extra_plugins - kwargs["external_plugin_resources"] = ( - kwargs.get("external_plugin_resources", []) + _external_plugin_resources - ) - super().__init__(*args, **kwargs) + pass class RichTextCollapsibleField(RichTextCollapsibleMixin, RichTextField): diff --git a/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/collapsibleitem.png b/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/collapsibleitem.png deleted file mode 100644 index 18014ffa3..000000000 Binary files a/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/collapsibleitem.png and /dev/null differ diff --git a/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/hidpi/collapsibleitem.png b/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/hidpi/collapsibleitem.png deleted file mode 100644 index 5fc10752d..000000000 Binary files a/adhocracy4/ckeditor/static/ckeditor_collapsible/icons/hidpi/collapsibleitem.png and /dev/null differ diff --git a/adhocracy4/ckeditor/static/ckeditor_collapsible/plugin.js b/adhocracy4/ckeditor/static/ckeditor_collapsible/plugin.js deleted file mode 100644 index e6bd2cf77..000000000 --- a/adhocracy4/ckeditor/static/ckeditor_collapsible/plugin.js +++ /dev/null @@ -1,44 +0,0 @@ -// CKEditor collapsibleItem plugin based on https://github.com/pkerspe/ckeditor-bootstrap-collapsibleItem -/* globals CKEDITOR django */ - -CKEDITOR.dtd.$editable.a = 1 - -CKEDITOR.plugins.add('collapsibleItem', { - requires: 'widget', - icons: 'collapsibleitem', - hidpi: true, - init: function (editor) { - editor.widgets.add('collapsibleItem', { - button: django.gettext('Insert Collapsible Item'), - template: '
' + - '
' + django.gettext('Title') + '
' + - '
' + django.gettext('Body text') + '
' + - '
', - editables: { - title: { - selector: '.collapsible-item-title', - allowedContent: 'strong em u' - }, - content: { - selector: '.collapsible-item-body', - allowedContent: 'p;br;span(*)[*];ul;ol;li;strong;em;u;hr;a;a[*];a(*)[*];img(*)[*];' - } - }, - allowedContent: 'div(!collapsible-item*)[*]', - requiredContent: 'div(collapsible-item);', - upcast: function (element) { - return element.name === 'div' && element.hasClass('collapsible-item') - } - }) - }, - - onLoad: function () { - CKEDITOR.addCss( - '.collapsible-item::before {font-size:10px;color:#000;content: "' + django.gettext('Collapsible element') + '"}' + - '.collapsible-item {padding: 8px;margin: 10px;background: #eee;border-radius: 8px;border: 1px solid #ddd;box-shadow: 0 1px 1px #fff inset, 0 -1px 0px #ccc inset;}' + - '.collapsible-item-title, .collapsible-item-body {box-shadow: 0 1px 1px #ddd inset;border: 1px solid #cccccc;border-radius: 5px;background: #fff;}' + - '.collapsible-item-title {margin: 0 0 8px;padding: 5px 8px;}' + - '.collapsible-item-body {padding: 0 8px;}' - ) - } -}) diff --git a/adhocracy4/ckeditor/storage.py b/adhocracy4/ckeditor/storage.py new file mode 100644 index 000000000..70a4fe2f8 --- /dev/null +++ b/adhocracy4/ckeditor/storage.py @@ -0,0 +1,12 @@ +import os +from urllib.parse import urljoin + +from django.conf import settings +from django.core.files.storage import FileSystemStorage + + +class CustomStorage(FileSystemStorage): + """Custom storage to store uploads in a subfolder called uploads""" + + location = os.path.join(settings.MEDIA_ROOT, "uploads") + base_url = urljoin(settings.MEDIA_URL, "uploads/") diff --git a/adhocracy4/projects/admin.py b/adhocracy4/projects/admin.py index b5f96a7f7..4413e3e5b 100644 --- a/adhocracy4/projects/admin.py +++ b/adhocracy4/projects/admin.py @@ -1,7 +1,7 @@ -from ckeditor_uploader.widgets import CKEditorUploadingWidget from django import forms from django.contrib import admin from django.utils.translation import gettext_lazy as _ +from django_ckeditor_5.widgets import CKEditor5Widget from . import models @@ -37,10 +37,10 @@ class ProjectAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["information"].widget = CKEditorUploadingWidget( + self.fields["information"].widget = CKEditor5Widget( config_name="collapsible-image-editor" ) - self.fields["result"].widget = CKEditorUploadingWidget( + self.fields["result"].widget = CKEditor5Widget( config_name="collapsible-image-editor" ) diff --git a/adhocracy4/projects/migrations/0040_auto_20230919_0952.py b/adhocracy4/projects/migrations/0040_auto_20230919_0952.py new file mode 100644 index 000000000..3e2326935 --- /dev/null +++ b/adhocracy4/projects/migrations/0040_auto_20230919_0952.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.20 on 2023-09-19 09:52 + +from django.db import migrations +import django_ckeditor_5.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("a4projects", "0039_add_alt_text_to_field"), + ] + + operations = [ + migrations.AlterField( + model_name="project", + name="information", + field=django_ckeditor_5.fields.CKEditor5Field( + blank=True, + help_text="This description should tell participants what the goal of the project is, how the project’s participation will look like. It will be always visible in the „Info“ tab on your project’s page.", + verbose_name="Description of your project", + ), + ), + migrations.AlterField( + model_name="project", + name="result", + field=django_ckeditor_5.fields.CKEditor5Field( + blank=True, + help_text="Here you should explain what the expected outcome of the project will be and how you are planning to use the results. If the project is finished you should add a summary of the results.", + verbose_name="Results of your project", + ), + ), + ] diff --git a/adhocracy4/projects/migrations/0041_ckeditor5_iframes.py b/adhocracy4/projects/migrations/0041_ckeditor5_iframes.py new file mode 100644 index 000000000..ebed56fe1 --- /dev/null +++ b/adhocracy4/projects/migrations/0041_ckeditor5_iframes.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.20 on 2023-11-16 11:35 + +from bs4 import BeautifulSoup +from django.db import migrations + + +def replace_iframe_with_figur(apps, schema_editor): + template = ( + '
' + ) + Project = apps.get_model("a4projects", "Project") + informations = 0 + results = 0 + for project in Project.objects.all(): + soup = BeautifulSoup(project.information, "html.parser") + iframes = soup.findAll("iframe") + changed = False + for iframe in iframes: + figure = BeautifulSoup( + template.format(url=iframe.attrs["src"]), "html.parser" + ) + iframe.replaceWith(figure) + informations += 1 + if iframes: + project.information = soup.prettify(formatter="html") + changed = True + soup = BeautifulSoup(project.result, "html.parser") + iframes = soup.findAll("iframe") + for iframe in iframes: + figure = BeautifulSoup( + template.format(url=iframe.attrs["src"]), "html.parser" + ) + iframe.replaceWith(figure) + results += 1 + if iframes: + project.result = soup.prettify(formatter="html") + changed = True + if changed: + project.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("a4projects", "0040_auto_20230919_0952"), + ] + + operations = [ + migrations.RunPython( + replace_iframe_with_figur, + ), + ] diff --git a/adhocracy4/projects/models.py b/adhocracy4/projects/models.py index 7324228b3..6fbf92c11 100644 --- a/adhocracy4/projects/models.py +++ b/adhocracy4/projects/models.py @@ -9,11 +9,11 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from django_ckeditor_5.fields import CKEditor5Field from django_enumfield.enum import EnumField from adhocracy4 import transforms as html_transforms from adhocracy4.administrative_districts.models import AdministrativeDistrict -from adhocracy4.ckeditor.fields import RichTextCollapsibleUploadingField from adhocracy4.images import fields from adhocracy4.maps.fields import PointField from adhocracy4.models import base @@ -194,7 +194,7 @@ class Project( "in max. 250 chars." ), ) - information = RichTextCollapsibleUploadingField( + information = CKEditor5Field( blank=True, config_name="collapsible-image-editor", verbose_name=_("Description of your project"), @@ -205,7 +205,7 @@ class Project( "in the „Info“ tab on your project’s page." ), ) - result = RichTextCollapsibleUploadingField( + result = CKEditor5Field( blank=True, config_name="collapsible-image-editor", verbose_name=_("Results of your project"), diff --git a/changelog/7274.md b/changelog/7274.md new file mode 100644 index 000000000..cde6fc96a --- /dev/null +++ b/changelog/7274.md @@ -0,0 +1,14 @@ +### Added + +- custom migration for iframes to make them work with ckeditor5 (WARNING: + backing up your database before running is recommended). +- added dependency beautifulsoup4 + +### Changed + +- replace django-ckeditor with django-ckeditor-5 + +### Removed + +- removed RichTextCollapsibleField +- removed RichtTextCollapsibleMixin diff --git a/pyproject.toml b/pyproject.toml index 853bfc912..8f714a11e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ dependencies = [ "django-allauth", "django-autoslug", "django-background-tasks", - "django-ckeditor", + "django-ckeditor-5", "django-enumfield", "django-filter", "django-multiselectfield", diff --git a/requirements/base.txt b/requirements/base.txt index 7c2fd7a50..970e7aa17 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,8 +1,10 @@ +beautifulsoup4==4.12.2 bleach[css]==6.0.0 Django==3.2.20 django-allauth==0.54.0 git+https://github.com/liqd/django-autoslug.git@liqd2212#egg=django-autoslug django-background-tasks==1.2.5 +https://github.com/liqd/django-ckeditor-5/releases/download/v0.2.11-liqd/django_ckeditor_5-0.2.11-py3-none-any.whl django-ckeditor==6.6.1 django-enumfield==3.1 django-filter==22.1 diff --git a/tests/apps/ideas/migrations/0006_alter_idea_description.py b/tests/apps/ideas/migrations/0006_alter_idea_description.py new file mode 100644 index 000000000..368ecacf7 --- /dev/null +++ b/tests/apps/ideas/migrations/0006_alter_idea_description.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.20 on 2023-09-19 13:10 + +from django.db import migrations +import django_ckeditor_5.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("a4test_ideas", "0005_idea_is_bool_test"), + ] + + operations = [ + migrations.AlterField( + model_name="idea", + name="description", + field=django_ckeditor_5.fields.CKEditor5Field( + blank=True, verbose_name="Description" + ), + ), + ] diff --git a/tests/apps/ideas/models.py b/tests/apps/ideas/models.py index 18f84fe97..6eb1aec76 100644 --- a/tests/apps/ideas/models.py +++ b/tests/apps/ideas/models.py @@ -1,6 +1,6 @@ -from ckeditor.fields import RichTextField from django.contrib.contenttypes.fields import GenericRelation from django.db import models +from django_ckeditor_5.fields import CKEditor5Field from adhocracy4.categories.fields import CategoryField from adhocracy4.comments.models import Comment @@ -19,7 +19,7 @@ class IdeaQuerySet(RateableQuerySet, CommentableQuerySet): class Idea(Item): name = models.CharField(max_length=120, default="Can i haz cheezburger, pls?") - description = RichTextField(verbose_name="Description", blank=True) + description = CKEditor5Field(verbose_name="Description", blank=True) moderator_status = models.CharField(max_length=256, blank=True) moderator_feedback_text = models.OneToOneField( ModeratorFeedback, diff --git a/tests/apps/moderatorfeedback/migrations/0005_alter_moderatorfeedback_feedback_text.py b/tests/apps/moderatorfeedback/migrations/0005_alter_moderatorfeedback_feedback_text.py new file mode 100644 index 000000000..b55aa799c --- /dev/null +++ b/tests/apps/moderatorfeedback/migrations/0005_alter_moderatorfeedback_feedback_text.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.20 on 2023-09-19 13:10 + +from django.db import migrations +import django_ckeditor_5.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("moderatorfeedback", "0004_verbose_name_created_modified"), + ] + + operations = [ + migrations.AlterField( + model_name="moderatorfeedback", + name="feedback_text", + field=django_ckeditor_5.fields.CKEditor5Field(blank=True), + ), + ] diff --git a/tests/apps/moderatorfeedback/models.py b/tests/apps/moderatorfeedback/models.py index 3469b1b42..c4206f735 100644 --- a/tests/apps/moderatorfeedback/models.py +++ b/tests/apps/moderatorfeedback/models.py @@ -1,9 +1,9 @@ -from ckeditor.fields import RichTextField +from django_ckeditor_5.fields import CKEditor5Field from adhocracy4.models.base import UserGeneratedContentModel class ModeratorFeedback(UserGeneratedContentModel): - feedback_text = RichTextField( + feedback_text = CKEditor5Field( blank=True, ) diff --git a/tests/ckeditor/test_ckeditor.py b/tests/ckeditor/test_ckeditor.py new file mode 100644 index 000000000..8355778ab --- /dev/null +++ b/tests/ckeditor/test_ckeditor.py @@ -0,0 +1,8 @@ +from django.conf import settings + +from adhocracy4.ckeditor.storage import CustomStorage + + +def test_ckeditor_storage_backend(): + storage = CustomStorage() + assert storage.path("test.txt").endswith(settings.MEDIA_ROOT + "/uploads/test.txt") diff --git a/tests/ckeditor/test_ckeditor_fields.py b/tests/ckeditor/test_ckeditor_fields.py deleted file mode 100644 index c62c43841..000000000 --- a/tests/ckeditor/test_ckeditor_fields.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest - - -@pytest.mark.django_db -def test_ckeditor_collapsible_field(project): - ckeditor_field = project._meta.get_field("information") - assert "collapsibleItem" in ckeditor_field.extra_plugins - assert ( - "collapsibleItem", - "/static/ckeditor_collapsible/", - "plugin.js", - ) in ckeditor_field.external_plugin_resources