Skip to content
This repository was archived by the owner on Mar 26, 2025. It is now read-only.

Commit 3d09c69

Browse files
authored
feat: Universal support for django CMS v3 and v4 (#631)
* add django-cms 4 support to main branch * Fix: render plugin test to use LinkPlugin in stead of PicturePlugin (since newer versions of easythumbnailer return float instead of int) Add: Tests for django CMS 4
1 parent c8d5fce commit 3d09c69

31 files changed

+365
-616
lines changed

.github/workflows/test.yml

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,35 @@ jobs:
88
strategy:
99
fail-fast: false
1010
matrix:
11-
python-version: [ 3.7, 3.8, 3.9, '3.10']
11+
python-version: [ 3.7, 3.8, 3.9, '3.10', "3.11"]
1212
requirements-file: [
1313
dj22_cms37.txt,
1414
dj22_cms38.txt,
15+
dj22_cms40.txt,
1516
dj31_cms38.txt,
1617
dj32_cms39.txt,
17-
dj32_cms310.txt
18+
dj32_cms310.txt,
19+
dj32_cms311.txt,
20+
dj32_cms41.txt,
21+
dj40_cms311.txt,
22+
dj40_cms41.txt
1823
]
1924
os: [
2025
ubuntu-20.04,
2126
]
22-
27+
exclude:
28+
- python-version: 3.7
29+
requirements-file: dj40_cms311.txt
30+
- python-version: 3.7
31+
requirements-file: dj40_cms41.txt
32+
- python-version: 3.7
33+
requirements-file: dj41_cms311.txt
34+
- python-version: 3.7
35+
requirements-file: dj41_cms41.txt
36+
- python-version: "3.10"
37+
requirements-file: dj22_cms40.txt
38+
- python-version: "3.11"
39+
requirements-file: dj22_cms40.txt
2340
steps:
2441
- uses: actions/checkout@v1
2542
- name: Set up Python ${{ matrix.python-version }}

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
Unreleased
66
==========
77

8+
* Add suport for django CMS 4
89
* Fix `468 <https://github.com/django-cms/djangocms-text-ckeditor/issues/468>`_ via `637 <https://github.com/django-cms/djangocms-text-ckeditor/pull/637>`_: Delay importing models.CMSPlugin in utils to allow adding an HTMLField to a custom user model.
910

1011

README.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
django CMS Text CKEditor
33
========================
44

5-
|pypi| |coverage| |python| |django| |djangocms|
5+
|pypi| |coverage| |python| |django| |djangocms| |djangocms4|
66

77

88
.. note::
@@ -512,10 +512,11 @@ You can run tests by executing::
512512
:target: https://travis-ci.org/divio/djangocms-text-ckeditor
513513
.. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-text-ckeditor/branch/master/graph/badge.svg
514514
:target: https://codecov.io/gh/django-cms/djangocms-text-ckeditor
515-
516515
.. |python| image:: https://img.shields.io/badge/python-3.7+-blue.svg
517516
:target: https://pypi.org/project/djangocms-text-ckeditor/
518-
.. |django| image:: https://img.shields.io/badge/django-2.2,%203.1,%203.2-blue.svg
517+
.. |django| image:: https://img.shields.io/badge/django-2.2--4.0-blue.svg
519518
:target: https://www.djangoproject.com/
520519
.. |djangocms| image:: https://img.shields.io/badge/django%20CMS-3.7%2B-blue.svg
521520
:target: https://www.django-cms.org/
521+
.. |djangocms4| image:: https://img.shields.io/badge/django%20CMS-4-blue.svg
522+
:target: https://www.django-cms.org/

aldryn_config.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@ def to_settings(self, data, settings):
3232
else:
3333
ckeditor_settings['contentsCss'] = ['/static/css/base.css']
3434

35+
style_set = ''
3536
if data.get('style_set'):
3637
style_set = data['style_set']
37-
else:
38-
style_set = ''
3938

4039
ckeditor_settings['stylesSet'] = f'default:{style_set}'
4140

djangocms_text_ckeditor/apps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
class TextCkeditorConfig(AppConfig):
55
name = 'djangocms_text_ckeditor'
66
verbose_name = 'django CMS Text CKEditor'
7+
default_auto_field = 'django.db.models.AutoField'

djangocms_text_ckeditor/cms_plugins.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import operator
33
import re
4-
from distutils.version import LooseVersion
54

65
from django.contrib.admin.utils import unquote
76
from django.core import signing
@@ -19,7 +18,6 @@
1918
from django.views.decorators.clickjacking import xframe_options_sameorigin
2019
from django.views.decorators.http import require_POST
2120

22-
import cms
2321
from cms.models import CMSPlugin
2422
from cms.plugin_base import CMSPluginBase
2523
from cms.plugin_pool import plugin_pool
@@ -30,21 +28,12 @@
3028
from .forms import ActionTokenValidationForm, DeleteOnCancelForm, RenderPluginForm, TextForm
3129
from .models import Text
3230
from .utils import (
33-
OBJ_ADMIN_WITH_CONTENT_RE_PATTERN, _plugin_tags_to_html, plugin_tags_to_admin_html, plugin_tags_to_id_list,
34-
plugin_tags_to_user_html, plugin_to_tag, random_comment_exempt, replace_plugin_tags,
31+
OBJ_ADMIN_WITH_CONTENT_RE_PATTERN, _plugin_tags_to_html, cms_placeholder_add_plugin, plugin_tags_to_admin_html,
32+
plugin_tags_to_id_list, plugin_tags_to_user_html, plugin_to_tag, random_comment_exempt, replace_plugin_tags,
3533
)
3634
from .widgets import TextEditorWidget
3735

3836

39-
CMS_34 = LooseVersion(cms.__version__) >= LooseVersion("3.4")
40-
41-
42-
def _user_can_change_placeholder(request, placeholder):
43-
if CMS_34:
44-
return placeholder.has_change_permission(request.user)
45-
return placeholder.has_change_permission(request)
46-
47-
4837
def post_add_plugin(operation, **kwargs):
4938
from djangocms_history.actions import ADD_PLUGIN
5039
from djangocms_history.helpers import get_bound_plugins, get_plugin_data
@@ -187,10 +176,9 @@ class TextPlugin(CMSPluginBase):
187176
"pre_change_plugin": pre_change_plugin,
188177
}
189178

190-
if CMS_34:
191-
# On django CMS 3.5 this attribute is set automatically
192-
# when do_post_copy is defined in the plugin class.
193-
_has_do_post_copy = True
179+
# On django CMS 3.5 this attribute is set automatically
180+
# when do_post_copy is defined in the plugin class.
181+
_has_do_post_copy = True
194182

195183
@classmethod
196184
def do_post_copy(cls, instance, source_map):
@@ -251,6 +239,7 @@ def get_editor_widget(self, request, plugins, plugin):
251239
pk=plugin.pk,
252240
placeholder=plugin.placeholder,
253241
plugin_language=plugin.language,
242+
plugin_position=plugin.position,
254243
configuration=self.ckeditor_configuration,
255244
render_plugin_url=render_plugin_url,
256245
cancel_url=cancel_url,
@@ -331,6 +320,14 @@ def __init__(self, *args, **kwargs):
331320

332321
return TextPluginForm
333322

323+
@staticmethod
324+
def _create_ghost_plugin(placeholder, plugin):
325+
"""CMS version-save function to add a plugin to a placeholder"""
326+
if hasattr(placeholder, "add_plugin"): # available as of CMS v4
327+
placeholder.add_plugin(plugin)
328+
else: # CMS < v4
329+
plugin.save()
330+
334331
@xframe_options_sameorigin
335332
def add_view(self, request, form_url="", extra_context=None):
336333
if "plugin" in request.GET:
@@ -381,18 +378,19 @@ def add_view(self, request, form_url="", extra_context=None):
381378
# Sadly we have to create the CMSPlugin record on add GET request
382379
# because we need this record in order to allow the user to add
383380
# child plugins to the text (image, link, etc..)
384-
plugin = CMSPlugin.objects.create(
381+
plugin = CMSPlugin(
385382
language=data["plugin_language"],
386383
plugin_type=data["plugin_type"],
387-
position=data["position"],
388384
placeholder=data["placeholder_id"],
385+
position=data["position"],
389386
parent=data.get("plugin_parent"),
390387
)
388+
self._create_ghost_plugin(data["placeholder_id"], plugin)
391389

392390
query = request.GET.copy()
393391
query["plugin"] = str(plugin.pk)
394392

395-
success_url = admin_reverse("cms_page_add_plugin")
393+
success_url = admin_reverse(cms_placeholder_add_plugin) # Version dependent
396394
# Because we've created the cmsplugin record
397395
# we need to delete the plugin when a user cancels.
398396
success_url += "?delete-on-cancel&" + query.urlencode()
@@ -449,7 +447,7 @@ def render_plugin(self, request):
449447

450448
if not (
451449
plugin_class.has_change_permission(request, obj=text_plugin)
452-
and _user_can_change_placeholder(request, text_plugin.placeholder) # noqa
450+
and text_plugin.placeholder.has_change_permission(request.user) # noqa
453451
):
454452
raise PermissionDenied
455453
return HttpResponse(form.render_plugin(request))
@@ -486,7 +484,7 @@ def delete_on_cancel(self, request):
486484
# and the ckeditor plugin itself.
487485
if not (
488486
plugin_class.has_add_permission(request)
489-
and _user_can_change_placeholder(request, text_plugin.placeholder) # noqa
487+
and text_plugin.placeholder.has_change_permission(request.user) # noqa
490488
):
491489
raise PermissionDenied
492490
# Token is validated after checking permissions

djangocms_text_ckeditor/compat.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

djangocms_text_ckeditor/forms.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,23 @@ def get_child_plugins(self):
8888
queryset = queryset.exclude(pk__in=excluded_plugins)
8989
return queryset
9090

91+
@staticmethod
92+
def _delete_plugin(plugin):
93+
"""Version-safe plugin delete method"""
94+
placeholder = plugin.placeholder
95+
if hasattr(placeholder, 'delete_plugin'): # since CMS v4
96+
return placeholder.delete_plugin(plugin)
97+
else:
98+
return plugin.delete()
99+
91100
def delete(self):
92101
child_plugins = self.cleaned_data.get('child_plugins')
93102

94103
if child_plugins:
95-
child_plugins.delete()
104+
for child in child_plugins:
105+
self._delete_plugin(child)
96106
else:
97-
self.text_plugin.delete()
107+
self._delete_plugin(self.text_plugin)
98108

99109

100110
class TextForm(ModelForm):

djangocms_text_ckeditor/models.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
from copy import deepcopy
2+
13
from django.db import models
24
from django.utils.encoding import force_str
35
from django.utils.html import strip_tags
46
from django.utils.text import Truncator
57
from django.utils.translation import gettext_lazy as _
68

79
from cms.models import CMSPlugin
8-
from cms.utils.copy_plugins import copy_plugins_to
910

1011
from . import settings
1112
from .html import clean_html, extract_images
@@ -82,14 +83,22 @@ def clean_plugins(self):
8283
def copy_referenced_plugins(self):
8384
referenced_plugins = self.get_referenced_plugins()
8485
if referenced_plugins:
85-
plugins_pairs = list(copy_plugins_to(
86-
referenced_plugins,
87-
self.placeholder,
88-
to_language=self.language,
89-
parent_plugin_id=self.id,
90-
))
91-
self.add_existing_child_plugins_to_pairs(plugins_pairs)
92-
self.post_copy(self, plugins_pairs)
86+
plugin_pairs = []
87+
for source_plugin in referenced_plugins:
88+
new_plugin = deepcopy(source_plugin)
89+
new_plugin.pk = None
90+
new_plugin.id = None
91+
new_plugin._state.adding = True
92+
new_plugin.parent = self
93+
if hasattr(self.placeholder, "add_plugin"): # CMS v4
94+
new_plugin.position = self.position + 1
95+
new_plugin = self.placeholder.add_plugin(new_plugin)
96+
else:
97+
new_plugin = self.add_child(instance=new_plugin)
98+
new_plugin.copy_relations(source_plugin)
99+
plugin_pairs.append((new_plugin, source_plugin))
100+
self.add_existing_child_plugins_to_pairs(plugin_pairs)
101+
self.post_copy(self, plugin_pairs)
93102

94103
def get_referenced_plugins(self):
95104
ids_in_body = set(plugin_tags_to_id_list(self.body))

djangocms_text_ckeditor/static/djangocms_text_ckeditor/ckeditor_plugins/cmsplugins/plugin.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,23 @@
6363
init: function (editor) {
6464
var that = this;
6565

66+
CKEDITOR.on('instanceReady', function () {
67+
var widgetInstances = [];
68+
69+
for (var key in editor.widgets.instances) {
70+
if (editor.widgets.instances.hasOwnProperty(key)) {
71+
widgetInstances.push(editor.widgets.instances[key]);
72+
}
73+
}
74+
75+
that.numberOfChildren = CKEDITOR.tools.array.filter(widgetInstances, function (i) {
76+
return i.name === 'cms-widget';
77+
}).length;
78+
});
6679
/**
6780
* populated with _fresh_ child plugins
6881
*/
69-
this.child_plugins = [];
82+
this.unsaved_child_plugins = [];
7083
var settings = CMS.CKEditor.editors[editor.id].settings;
7184
this.setupCancelCleanupCallback(settings);
7285

@@ -210,9 +223,10 @@
210223
// in case it's a fresh text plugin children don't have to be
211224
// deleted separately
212225
if (!editor.config.settings.delete_on_cancel && addedChildPlugin) {
213-
that.child_plugins.push(data.plugin_id);
226+
that.unsaved_child_plugins.push(data.plugin_id);
214227
}
215228
that.insertPlugin(data, dialog.sender._.editor);
229+
that.numberOfChildren += 1
216230

217231
CMS.API.Helpers.onPluginSave = onSave;
218232
return false;
@@ -315,6 +329,7 @@
315329
plugin_type: item.attr('rel'),
316330
plugin_parent: settings.plugin_id,
317331
plugin_language: settings.plugin_language,
332+
plugin_position: settings.plugin_position + 1 + this.numberOfChildren,
318333
cms_path: window.parent.location.pathname,
319334
cms_history: 0
320335
};
@@ -391,18 +406,18 @@
391406
var that = this;
392407
var CMS = window.parent.CMS;
393408
var cancelModalCallback = function cancelModalCallback(e, opts) {
394-
if (!settings.delete_on_cancel && !that.child_plugins.length) {
409+
if (!settings.delete_on_cancel && !that.unsaved_child_plugins.length) {
395410
return;
396411
}
397-
if (that.child_plugins.length) {
412+
if (that.unsaved_child_plugins.length) {
398413
e.preventDefault();
399414
CMS.API.Toolbar.showLoader();
400415
var data = {
401416
token: settings.action_token
402417
};
403418

404419
if (!settings.delete_on_cancel) {
405-
data.child_plugins = that.child_plugins;
420+
data.child_plugins = that.unsaved_child_plugins;
406421
}
407422

408423
$.ajax({

0 commit comments

Comments
 (0)