diff --git a/cmsplugin_cascade/clipboard/admin.py b/cmsplugin_cascade/clipboard/admin.py index 44b2680ea..3f8301d4d 100644 --- a/cmsplugin_cascade/clipboard/admin.py +++ b/cmsplugin_cascade/clipboard/admin.py @@ -6,6 +6,7 @@ from django.contrib.admin.templatetags.admin_static import static from django.forms import widgets from django.forms.utils import flatatt +from django.http import HttpResponse from django.utils.encoding import force_text from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ @@ -60,6 +61,9 @@ class Media: css = {'all': ('cascade/css/admin/clipboard.css',)} js = ('cascade/js/admin/clipboard.js',) + def get_changeform_initial_data(self, request): + return {'identifier': "Clipboard {}".format(CascadeClipboard.objects.all().count()+1)} + def save_clipboard(self, obj): return format_html('', _("Insert Data")) @@ -73,7 +77,7 @@ def restore_clipboard(self, obj): def save_model(self, request, obj, form, change): language = get_language_from_request(request) if request.POST.get('save_clipboard'): - obj.data = self._serialize_from_clipboard(language) + obj.data = self._serialize_from_clipboard(request, language) request.POST = request.POST.copy() request.POST['_continue'] = True if request.POST.get('restore_clipboard'): @@ -81,9 +85,13 @@ def save_model(self, request, obj, form, change): request.POST['_continue'] = True super(CascadeClipboardAdmin, self).save_model(request, obj, form, change) if request.POST.get('restore_clipboard'): - self._deserialize_to_clipboard(request, obj.data) + if len(obj.data['plugins']) >= 2: + is_placeholder=True + else: + is_placeholder=None + self._deserialize_to_clipboard(request, obj.data, is_placeholder) - def _serialize_from_clipboard(self, language): + def _serialize_from_clipboard(self, request, language, clipboard=None): """ Create a serialized representation of all the plugins belonging to the clipboard. """ @@ -105,11 +113,14 @@ def populate_data(parent, data): ref = PlaceholderReference.objects.last() if ref: clipboard = ref.placeholder_ref + elif request.toolbar.clipboard.cmsplugin_set.last(): + clipboard = request.toolbar.clipboard + if clipboard is not None: plugin_qs = clipboard.cmsplugin_set.all() populate_data(None, data['plugins']) return data - def _deserialize_to_clipboard(self, request, data): + def _deserialize_to_clipboard(self, request, data, is_placeholder): """ Restore clipboard by creating plugins from given data. """ @@ -142,12 +153,29 @@ def plugins_from_data(placeholder, parent, data): clipboard = request.toolbar.clipboard ref_plugin = clipboard.cmsplugin_set.first() - if ref_plugin is None: + if ref_plugin is None and is_placeholder is True: # the clipboard is empty root_plugin = add_plugin(clipboard, 'PlaceholderPlugin', language, name='clipboard') - else: + root_plugin=root_plugin.placeholder_ref + elif is_placeholder is True: # remove old entries from the clipboard root_plugin = ref_plugin.cms_placeholderreference inst = ref_plugin.get_plugin_instance()[0] inst.placeholder_ref.get_plugins().delete() - plugins_from_data(root_plugin.placeholder_ref, None, data['plugins']) + root_plugin=root_plugin.placeholder_ref + elif is_placeholder is None: + root_plugin=clipboard + if ref_plugin: + inst = ref_plugin.get_plugin_instance()[0] + inst.placeholder.get_plugins().delete() + plugins_from_data(root_plugin, None, data['plugins']) + + def response_change(self, request, obj): + #Little hack to reload the clipboard modified in Django administration and the sideframe Django-CMS. + #TODO find a better way to reload clipboard potentially with request Ajax + #js = static_with_version('cms/js/dist/bundle.admin.base.min.js') + if "restore_clipboard" in request.POST: + return HttpResponse( + format_html('')) + return super().response_change(request, obj) + diff --git a/docs/source/clipboard.rst b/docs/source/clipboard.rst index fc6b339cd..b927a291a 100644 --- a/docs/source/clipboard.rst +++ b/docs/source/clipboard.rst @@ -22,6 +22,10 @@ Since the content of the clipboard is overridden by every operation which cuts o plugins, **djangocms-cascade** offers some functionality to persist the clipboard's content. To do this, locate **Persited Clipboard Content** in Django's administration backend. +It is also possible to copy plugins or a single plugin with children without reference to a +placeholder, these can also be copied into a persistent clipboard. They will have to be placed in +their proper locations with the djangocms-cascade logic that will tell you where they can be placed. + |persist-clipboard| .. |persist-clipboard| image:: _static/persist-clipboard.png diff --git a/tests/templates/testing.html b/tests/templates/testing.html index 6223f6426..ad47ec2fb 100644 --- a/tests/templates/testing.html +++ b/tests/templates/testing.html @@ -9,5 +9,6 @@ {% placeholder "Main Content" %} + {% placeholder "Main Content2" %} diff --git a/tests/test_base.py b/tests/test_base.py index 9edab8e80..1e92b6d45 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -20,7 +20,7 @@ def setUp(self): self.home_page.set_as_homepage() self.placeholder = self.home_page.placeholders.get(slot='Main Content') - + self.placeholder2 = self.home_page.placeholders.get(slot='Main Content2') self.request = self.get_request(self.home_page, 'en') self.admin_site = admin.sites.AdminSite() diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py index af21fe5ce..5248539f5 100644 --- a/tests/test_clipboard.py +++ b/tests/test_clipboard.py @@ -14,47 +14,181 @@ from cmsplugin_cascade.models import CascadeElement, CascadeClipboard from cmsplugin_cascade.bootstrap3.container import (BootstrapContainerPlugin, BootstrapRowPlugin, BootstrapRowForm, BootstrapColumnPlugin, BS3_BREAKPOINT_KEYS) +from cmsplugin_cascade.bootstrap3.jumbotron import BootstrapJumbotronPlugin from .test_base import CascadeTestCase class ClipboardPluginTest(CascadeTestCase): maxDiff = None identifier = "Test saved clipboard" - placeholder_data = {'plugins': [['BootstrapContainerPlugin', { - 'glossary': {'media_queries': {'md': ['(min-width: 992px)'], 'sm': ['(max-width: 992px)']}, - 'container_max_widths': {'md': 970, 'sm': 750}, 'fluid': '', - 'breakpoints': ['sm', 'md']}}, - [['BootstrapRowPlugin', {'glossary': {}}, [ - ['BootstrapColumnPlugin', - {'glossary': {'sm-responsive-utils': '', - 'md-column-offset': '', - 'sm-column-width': 'col-sm-3', - 'md-responsive-utils': '', - 'md-column-ordering': '', - 'sm-column-ordering': '', - 'sm-column-offset': 'col-sm-offset-1', - 'container_max_widths': {'md': 212.5, - 'sm': 157.5}, - 'md-column-width': ''}}, []], - ['BootstrapColumnPlugin', { - 'glossary': {'sm-responsive-utils': 'hidden-sm', - 'md-column-offset': '', - 'sm-column-width': 'col-sm-4', - 'md-responsive-utils': '', - 'md-column-ordering': '', - 'sm-column-ordering': '', - 'sm-column-offset': '', - 'container_max_widths': {'md': 293.33, - 'sm': 220.0}, - 'md-column-width': ''}}, []], - ['BootstrapColumnPlugin', { - 'glossary': { - 'container_max_widths': { - 'md': 293.33, - 'sm': 220.0}, - 'sm-column-width': 'col-sm-4' - }}, - []]]]]]]} + placeholder_data = { + 'plugins': [ + ['BootstrapContainerPlugin', + { + 'glossary': { + 'breakpoints': [ + 'sm', + 'md' + ], + 'fluid': '', + 'container_max_widths': { + 'sm': 750, + 'md': 970 + }, + 'media_queries': { + 'sm': [ + '(max-width: 992px)' + ], + 'md': [ + '(min-width: 992px)' + ] + } + } + }, + [ + ['BootstrapRowPlugin', + { + 'glossary': { + } + }, + [ + ['BootstrapColumnPlugin', + { + 'glossary': { + 'sm-column-width': 'col-sm-3', + 'md-column-width': '', + 'sm-column-offset': 'col-sm-offset-1', + 'md-column-offset': '', + 'sm-column-ordering': '', + 'md-column-ordering': '', + 'sm-responsive-utils': '', + 'md-responsive-utils': '', + 'container_max_widths': { + 'sm': 157.5, + 'md': 212.5 + } + } + }, + [ + ]], + [ + 'BootstrapColumnPlugin', + { + 'glossary': { + 'sm-column-width': 'col-sm-4', + 'md-column-width': '', + 'sm-column-offset': '', + 'md-column-offset': '', + 'sm-column-ordering': '', + 'md-column-ordering': '', + 'sm-responsive-utils': 'hidden-sm', + 'md-responsive-utils': '', + 'container_max_widths': { + 'sm': 220.0, + 'md': 293.33 + } + } + }, + [ + ] + ], + [ + 'BootstrapColumnPlugin', + { + 'glossary': { + 'container_max_widths': { + 'sm': 220.0, + 'md': 293.33 + }, + 'sm-column-width': 'col-sm-4' + } + }, + [ + ] + ] + ]] + ]], + [ + 'BootstrapContainerPlugin', + { + 'glossary': { + 'breakpoints': [ + 'xs', + 'sm', + 'md', + 'lg' + ], + 'container_max_widths': { + 'xs': 750, + 'sm': 750, + 'md': 970, + 'lg': 1170 + }, + 'media_queries': { + 'xs': [ + '(max-width: 768px)' + ], + 'sm': [ + '(min-width: 768px)', + '(max-width: 992px)' + ], + 'md': [ + '(min-width: 992px)', + '(max-width: 1200px)' + ], + 'lg': [ + '(min-width: 1200px)' + ] + } + } + }, + [ + ] + ] + ] + } + + plugins_data ={ + 'plugins': [ + ['BootstrapJumbotronPlugin', + { + 'glossary': { + 'breakpoints': [ + 'xs', + 'sm', + 'md', + 'lg' + ], + 'fluid': True, + 'container_max_widths': { + 'xs': 768, + 'sm': 992, + 'md': 1200, + 'lg': 1980 + }, + 'media_queries': { + 'xs': [ + '(max-width: 768px)' + ], + 'sm': [ + '(min-width: 768px)', + '(max-width: 992px)' + ], + 'md': [ + '(min-width: 992px)', + '(max-width: 1200px)' + ], + 'lg': [ + '(min-width: 1200px)' + ] + } + } + }, + [ + ]] + ] + } + def setUp(self): super(ClipboardPluginTest, self).setUp() @@ -144,7 +278,22 @@ def setUp(self): '
' + '') - def test_save_clipboard(self): + # add a Bootstrap Container Plugin 2 + container_model2 = add_plugin(self.placeholder, BootstrapContainerPlugin, 'en', + glossary={'breakpoints': BS3_BREAKPOINT_KEYS}) + self.assertIsInstance(container_model2, CascadeElement) + container_plugin2 = container_model2.get_plugin_class_instance(self.admin_site) + self.assertIsInstance(container_plugin2, BootstrapContainerPlugin) + + # add a Bootstrap Jumbotron Plugin in placeholder Main Content2 + jumbotron_model = add_plugin(self.placeholder2, BootstrapJumbotronPlugin, 'en', + glossary={'breakpoints': BS3_BREAKPOINT_KEYS}) + self.assertIsInstance(jumbotron_model, CascadeElement) + jumbotron_plugin = jumbotron_model.get_plugin_class_instance(self.admin_site) + self.assertIsInstance(jumbotron_plugin, BootstrapJumbotronPlugin) + + + def test_save_clipboard_placeholder(self): with self.login_user_context(self.admin_user): request = self.get_request('/') request.toolbar = CMSToolbar(request) @@ -173,12 +322,48 @@ def test_save_clipboard(self): self.assertEqual(ul.li.text, 'The Persited Clipboard Content "Test saved clipboard" was added successfully. You may edit it again below.') self.assertEqual(CascadeClipboard.objects.all().count(), 1) - # now examine the serialized data in the clipboard + # now examine the serialized data in the clipboard placeholder cascade_clipboard = CascadeClipboard.objects.get(identifier=self.identifier) self.remove_primary_keys(cascade_clipboard.data['plugins']) self.assertDictEqual(cascade_clipboard.data, self.placeholder_data) - def test_restore_clipboard(self): + + def test_save_clipboard_plugins(self): + with self.login_user_context(self.admin_user): + request = self.get_request('/') + request.toolbar = CMSToolbar(request) + self.assertIsNotNone(request.toolbar.clipboard) + data = {'source_placeholder_id': self.placeholder2.pk, 'source_plugin_id': '', + 'source_language': 'en', 'target_plugin_id': '', + 'target_placeholder_id': request.toolbar.clipboard.pk, 'target_language': 'en'} + + # check that clipboard is empty + self.assertEqual(request.toolbar.clipboard.cmsplugin_set.count(), 0) + + # copy plugins from placeholder to clipboard + copy_plugins_url = reverse('admin:cms_page_copy_plugins') # + '?cms_path=%2Fen%2F' + response = self.client.post(copy_plugins_url, data) + self.assertEqual(response.status_code, 200) + + # serialize and persist clipboard content + add_clipboard_url = reverse('admin:cmsplugin_cascade_cascadeclipboard_add') + data = {'identifier': self.identifier, 'save_clipboard': 'Save', 'data': {}} + response = self.client.post(add_clipboard_url, data) + self.assertEqual(response.status_code, 302) + change_clipboard_url = response['location'] + response = self.client.get(change_clipboard_url, data) + soup = BeautifulSoup(response.content) + ul = soup.find('ul', class_='messagelist') + self.assertEqual(ul.li.text, 'The Persited Clipboard Content "Test saved clipboard" was added successfully. You may edit it again below.') + self.assertEqual(CascadeClipboard.objects.all().count(), 1) + + # now examine the serialized data in the clipboard placeholder + cascade_clipboard = CascadeClipboard.objects.get(identifier=self.identifier) + self.remove_primary_keys(cascade_clipboard.data['plugins']) + self.assertDictEqual(cascade_clipboard.data, self.plugins_data) + + + def test_restore_clipboard_placeholder(self): with self.login_user_context(self.admin_user): cascade_clipboard = CascadeClipboard.objects.create(identifier=self.identifier, data=self.placeholder_data) cascade_clipboard.save() @@ -193,6 +378,11 @@ def test_restore_clipboard(self): change_clipboard_url = reverse('admin:cmsplugin_cascade_cascadeclipboard_change', args=(cascade_clipboard.pk,)) data = {'identifier': self.identifier, 'restore_clipboard': 'Restore', 'data': json.dumps(self.placeholder_data)} response = self.client.post(change_clipboard_url, data) + #Little hack to reload the clipboard + self.assertEqual(response.status_code, 200) + response = self.client.get(change_clipboard_url, data) + self.assertEqual(response.status_code, 200) + """ self.assertEqual(response.status_code, 302) change_clipboard_url = response['location'] response = self.client.get(change_clipboard_url, data) @@ -200,18 +390,59 @@ def test_restore_clipboard(self): soup = BeautifulSoup(response.content, 'html.parser') ul = soup.find('ul', class_='messagelist') self.assertEqual(ul.li.text, 'The Persited Clipboard Content "Test saved clipboard" was changed successfully. You may edit it again below.') - + """ + # check if clipboard has been populated with plugins from serialized data ref_plugin = request.toolbar.clipboard.get_plugins().first() self.assertEqual(ref_plugin.plugin_type, 'PlaceholderPlugin') inst = ref_plugin.get_plugin_instance()[0] plugins = inst.placeholder_ref.get_plugins() - self.assertEqual(plugins.count(), 5) + self.assertEqual(plugins.count(), 6) self.assertEqual(plugins[0].plugin_type, 'BootstrapContainerPlugin') self.assertEqual(plugins[1].plugin_type, 'BootstrapRowPlugin') self.assertEqual(plugins[2].plugin_type, 'BootstrapColumnPlugin') + self.assertEqual(plugins[5].plugin_type, 'BootstrapContainerPlugin') + + + def test_restore_clipboard_plugins(self): + with self.login_user_context(self.admin_user): + cascade_clipboard = CascadeClipboard.objects.create(identifier=self.identifier, data=self.plugins_data) + cascade_clipboard.save() + request = self.get_request('/') + request.toolbar = CMSToolbar(request) + self.assertIsNotNone(request.toolbar.clipboard) + + # check that clipboard is empty + self.assertEqual(request.toolbar.clipboard.cmsplugin_set.count(), 0) + + # copy plugins from CascadeClipboard to CMS clipboard + change_clipboard_url = reverse('admin:cmsplugin_cascade_cascadeclipboard_change', args=(cascade_clipboard.pk,)) + data = {'identifier': self.identifier, 'restore_clipboard': 'Restore', 'data': json.dumps(self.plugins_data)} + response = self.client.post(change_clipboard_url, data) + #Little hack to reload the clipboard + self.assertEqual(response.status_code, 200) + response = self.client.get(change_clipboard_url, data) + self.assertEqual(response.status_code, 200) + """ + self.assertEqual(response.status_code, 302) + change_clipboard_url = response['location'] + response = self.client.get(change_clipboard_url, data) + self.assertEqual(response.status_code, 200) + soup = BeautifulSoup(response.content, 'html.parser') + ul = soup.find('ul', class_='messagelist') + self.assertEqual(ul.li.text, 'The Persited Clipboard Content "Test saved clipboard" was changed successfully. You may edit it again below.') + """ + + # check if clipboard has been populated with plugins from serialized data + ref_plugin = request.toolbar.clipboard.get_plugins().first() + self.assertEqual(ref_plugin.plugin_type, 'BootstrapJumbotronPlugin') + inst = ref_plugin.get_plugin_instance()[0] + plugins = inst.placeholder.get_plugins() + self.assertEqual(plugins.count(), 1) + self.assertEqual(plugins[0].plugin_type, 'BootstrapJumbotronPlugin') + def remove_primary_keys(self, plugin_data): for plugin_type, data, children_data in plugin_data: data.pop('pk', None) - self.remove_primary_keys(children_data) \ No newline at end of file + self.remove_primary_keys(children_data)