From eeb80722a5972ffd7bfd02abd082173e513d96b3 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Wed, 8 Sep 2021 17:16:39 +0100 Subject: [PATCH 01/66] djangocms-versioning compatibility - Initial Model and cms_config implementation (#75) --- .github/workflows/test.yml | 9 +++---- djangocms_snippet/cms_config.py | 24 ++++++++++++++++++ .../migrations/0009_auto_20210811_0942.py | 25 +++++++++++++++++++ djangocms_snippet/models.py | 12 +++++++-- setup.py | 2 +- tests/requirements/dj11_cms40.txt | 4 +++ tests/requirements/dj22_cms40.txt | 5 ++++ tests/test_plugins.py | 18 ++++++------- 8 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 djangocms_snippet/cms_config.py create mode 100644 djangocms_snippet/migrations/0009_auto_20210811_0942.py create mode 100644 tests/requirements/dj11_cms40.txt create mode 100644 tests/requirements/dj22_cms40.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b4f6752..a3bf1414 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,13 +8,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9, ] # latest release minus two + python-version: [ 3.6, 3.7, ] requirements-file: [ - dj22_cms37.txt, - dj22_cms38.txt, - dj30_cms37.txt, - dj30_cms38.txt, - dj31_cms38.txt, + dj11_cms40.txt, + dj22_cms40.txt, ] os: [ ubuntu-20.04, diff --git a/djangocms_snippet/cms_config.py b/djangocms_snippet/cms_config.py new file mode 100644 index 00000000..cf87874d --- /dev/null +++ b/djangocms_snippet/cms_config.py @@ -0,0 +1,24 @@ +from django.conf import settings + +from cms.app_base import CMSAppConfig + +from .models import Snippet + + +class SnippetCMSAppConfig(CMSAppConfig): + djangocms_versioning_enabled = getattr( + settings, 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED', False + ) + + if djangocms_versioning_enabled: + from djangocms_versioning.datastructures import ( + VersionableItem, default_copy, + ) + + versioning = [ + VersionableItem( + content_model=Snippet, + grouper_field_name="snippet_grouper", + copy_function=default_copy, + ) + ] diff --git a/djangocms_snippet/migrations/0009_auto_20210811_0942.py b/djangocms_snippet/migrations/0009_auto_20210811_0942.py new file mode 100644 index 00000000..2107cc34 --- /dev/null +++ b/djangocms_snippet/migrations/0009_auto_20210811_0942.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.24 on 2021-08-11 09:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_snippet', '0008_auto_change_name'), + ] + + operations = [ + migrations.CreateModel( + name='SnippetGrouper', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.AddField( + model_name='snippet', + name='snippet_grouper', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + ), + ] diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index 23f27144..83c18a38 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -9,6 +9,10 @@ SEARCH_ENABLED = getattr(settings, 'DJANGOCMS_SNIPPET_SEARCH', False) +class SnippetGrouper(models.Model): + pass + + # Stores the actual data class Snippet(models.Model): """ @@ -19,6 +23,11 @@ class Snippet(models.Model): unique=True, max_length=255, ) + snippet_grouper = models.ForeignKey( + SnippetGrouper, + on_delete=models.PROTECT, + null=True, + ) html = models.TextField( verbose_name=_('HTML'), blank=True, @@ -62,8 +71,7 @@ class SnippetPtr(CMSPlugin): parent_link=True, on_delete=models.CASCADE, ) - - snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE,) + snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE) search_fields = ['snippet__html'] if SEARCH_ENABLED else [] diff --git a/setup.py b/setup.py index 9e41005e..566bc2cb 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ REQUIREMENTS = [ - 'django-cms>=3.7', + 'django-cms', 'django-treebeard>=4.3,<4.5', ] diff --git a/tests/requirements/dj11_cms40.txt b/tests/requirements/dj11_cms40.txt new file mode 100644 index 00000000..4c9d6c95 --- /dev/null +++ b/tests/requirements/dj11_cms40.txt @@ -0,0 +1,4 @@ +-r base.txt + +https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms +https://github.com/divio/djangocms-versioning/tarball/master/#egg=djangocms-versioning diff --git a/tests/requirements/dj22_cms40.txt b/tests/requirements/dj22_cms40.txt new file mode 100644 index 00000000..c84c4174 --- /dev/null +++ b/tests/requirements/dj22_cms40.txt @@ -0,0 +1,5 @@ +-r base.txt + +Django>=2.2,<3.0 +https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms +https://github.com/divio/djangocms-versioning/tarball/master/#egg=djangocms-versioning diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 6e4e6767..2e971d10 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -13,14 +13,13 @@ def setUp(self): template="page.html", language=self.language, ) - self.home.publish(self.language) self.page = create_page( title="help", template="page.html", language=self.language, ) - self.page.publish(self.language) - self.placeholder = self.page.placeholders.get(slot="content") + self.pagecontent = self.page.pagecontent_set.first() + self.placeholder = self.pagecontent.placeholders.get(slot="content") self.superuser = self.get_superuser() def tearDown(self): @@ -36,12 +35,12 @@ def test_html_rendering(self): slug="plugin_snippet", ) plugin = add_plugin( - self.page.placeholders.get(slot="content"), + self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, snippet=snippet, ) - self.page.publish(self.language) + self.assertEqual(plugin.snippet.name, "plugin_snippet") self.assertEqual(plugin.snippet.html, "

Hello World

") self.assertEqual(plugin.snippet.slug, "plugin_snippet") @@ -59,12 +58,11 @@ def test_failing_html_rendering(self): slug="plugin_snippet", ) add_plugin( - self.page.placeholders.get(slot="content"), + self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, snippet=snippet, ) - self.page.publish(self.language) with self.login_user_context(self.superuser): response = self.client.get(request_url) @@ -82,12 +80,11 @@ def test_template_rendering(self): ) snippet.save() plugin = add_plugin( - self.page.placeholders.get(slot="content"), + self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, snippet=snippet, ) - self.page.publish(self.language) self.assertEqual(plugin.snippet.name, "plugin_snippet") self.assertEqual(plugin.snippet.slug, "plugin_snippet") @@ -109,12 +106,11 @@ def test_failing_template_rendering(self): ) snippet.save() add_plugin( - self.page.placeholders.get(slot="content"), + self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, snippet=snippet, ) - self.page.publish(self.language) with self.login_user_context(self.superuser): response = self.client.get(request_url) From a441d5a46943676c5d608158d5e837c17b91c5c7 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Wed, 15 Sep 2021 16:13:56 +0100 Subject: [PATCH 02/66] feat: Snippet CMS 4.0 and djangocms-versioning Data migration (#74) --- README.rst | 5 + djangocms_snippet/conf.py | 6 + ...811_0942.py => 0009_auto_20210915_0445.py} | 9 +- ...010_cms4_grouper_version_data_migration.py | 63 +++++++++ .../0011_cms4_plugin_data_migration.py | 22 ++++ .../migrations/0012_auto_20210915_0721.py | 28 ++++ djangocms_snippet/models.py | 14 +- tests/requirements/base.txt | 1 + tests/settings.py | 3 + tests/test_models.py | 37 +++--- tests/test_plugins.py | 62 ++++++--- tests/test_templatetags.py | 34 ++--- tests/utils/factories.py | 124 ++++++++++++++++++ 13 files changed, 352 insertions(+), 56 deletions(-) create mode 100644 djangocms_snippet/conf.py rename djangocms_snippet/migrations/{0009_auto_20210811_0942.py => 0009_auto_20210915_0445.py} (55%) create mode 100644 djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py create mode 100644 djangocms_snippet/migrations/0011_cms4_plugin_data_migration.py create mode 100644 djangocms_snippet/migrations/0012_auto_20210915_0721.py create mode 100644 tests/utils/factories.py diff --git a/README.rst b/README.rst index 60bec6a6..6cedd459 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,11 @@ please set ``DJANGOCMS_SNIPPET_CACHE`` to ``False`` in your settings:: DJANGOCMS_SNIPPET_CACHE = False # default value is True +Migration 0010 requires the use of a user in order to create versions for existing snippets (if djangocms_versioning is installed and enabled), +a user can be chosen with the setting ``DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID``, the default is 1. + + DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID = 2 # Will use user with id: 2 + Template tag ------------ diff --git a/djangocms_snippet/conf.py b/djangocms_snippet/conf.py new file mode 100644 index 00000000..b4dc989e --- /dev/null +++ b/djangocms_snippet/conf.py @@ -0,0 +1,6 @@ +from django.conf import settings + + +DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID = getattr( + settings, "DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID", 1 +) diff --git a/djangocms_snippet/migrations/0009_auto_20210811_0942.py b/djangocms_snippet/migrations/0009_auto_20210915_0445.py similarity index 55% rename from djangocms_snippet/migrations/0009_auto_20210811_0942.py rename to djangocms_snippet/migrations/0009_auto_20210915_0445.py index 2107cc34..ee7ffd67 100644 --- a/djangocms_snippet/migrations/0009_auto_20210811_0942.py +++ b/djangocms_snippet/migrations/0009_auto_20210915_0445.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.24 on 2021-08-11 09:42 +# Generated by Django 2.2.24 on 2021-09-15 04:45 import django.db.models.deletion from django.db import migrations, models @@ -20,6 +20,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='snippet', name='snippet_grouper', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + ), + migrations.AddField( + model_name='snippetptr', + name='snippet_grouper', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='djangocms_snippet.SnippetGrouper'), ), ] diff --git a/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py b/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py new file mode 100644 index 00000000..dee1d352 --- /dev/null +++ b/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py @@ -0,0 +1,63 @@ +from django.apps import apps as global_apps +from django.contrib.contenttypes.management import create_contenttypes +from django.db import migrations + +from djangocms_snippet.cms_config import SnippetCMSAppConfig +from djangocms_snippet.conf import ( + DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID, +) + + +try: + from djangocms_versioning.constants import DRAFT + + djangocms_versioning_installed = True +except ImportError: + djangocms_versioning_installed = False + + +def cms4_grouper_version_migration(apps, schema_editor): + create_contenttypes(global_apps.get_app_config("djangocms_snippet")) + + djangocms_versioning_config_enabled = SnippetCMSAppConfig.djangocms_versioning_enabled + + ContentType = apps.get_model('contenttypes', 'ContentType') + Snippet = apps.get_model('djangocms_snippet', 'Snippet') + SnippetGrouper = apps.get_model('djangocms_snippet', 'SnippetGrouper') + User = apps.get_model('auth', 'User') + + snippet_contenttype = ContentType.objects.get(app_label='djangocms_snippet', model='snippet') + snippet_queryset = Snippet.objects.all() + + for snippet in snippet_queryset: + grouper = SnippetGrouper.objects.create() + snippet.snippet_grouper = grouper + snippet.save() + + # Get a migration user. + migration_user = User.objects.get(id=DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID) + + # Create initial Snippet Versions if versioning is enabled and installed. + if djangocms_versioning_config_enabled and djangocms_versioning_installed: + Version = apps.get_model('djangocms_versioning', 'Version') + Version.objects.create( + created_by=migration_user, + state=DRAFT, + number=1, + object_id=snippet.pk, + content_type=snippet_contenttype, + ) + + +class Migration(migrations.Migration): + dependencies = [ + ('cms', '0034_remove_pagecontent_placeholders'), # Run after the CMS4 migrations + ('djangocms_snippet', '0009_auto_20210915_0445'), + ] + + if djangocms_versioning_installed: + dependencies += [('djangocms_versioning', '0015_version_modified'), ] + + operations = [ + migrations.RunPython(cms4_grouper_version_migration) + ] diff --git a/djangocms_snippet/migrations/0011_cms4_plugin_data_migration.py b/djangocms_snippet/migrations/0011_cms4_plugin_data_migration.py new file mode 100644 index 00000000..449236d8 --- /dev/null +++ b/djangocms_snippet/migrations/0011_cms4_plugin_data_migration.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.24 on 2021-08-31 10:45 +from django.db import migrations + + +def cms4_migration(apps, schema_editor): + SnippetPtr = apps.get_model('djangocms_snippet', 'SnippetPtr') + + for snippet_plugin in SnippetPtr.objects.all(): + snippet = snippet_plugin.snippet + snippet_plugin.snippet_grouper = snippet.snippet_grouper + snippet_plugin.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_snippet', '0010_cms4_grouper_version_data_migration'), + ] + + operations = [ + migrations.RunPython(cms4_migration) + ] diff --git a/djangocms_snippet/migrations/0012_auto_20210915_0721.py b/djangocms_snippet/migrations/0012_auto_20210915_0721.py new file mode 100644 index 00000000..f57f106d --- /dev/null +++ b/djangocms_snippet/migrations/0012_auto_20210915_0721.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.24 on 2021-09-15 07:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_snippet', '0011_cms4_plugin_data_migration'), + ] + + operations = [ + migrations.RemoveField( + model_name='snippetptr', + name='snippet', + ), + migrations.AlterField( + model_name='snippet', + name='snippet_grouper', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + ), + migrations.AlterField( + model_name='snippetptr', + name='snippet_grouper', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_snippet.SnippetGrouper'), + ), + ] diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index 83c18a38..d7094b10 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -20,13 +20,12 @@ class Snippet(models.Model): """ name = models.CharField( verbose_name=_('Name'), - unique=True, max_length=255, + unique=True, ) snippet_grouper = models.ForeignKey( SnippetGrouper, on_delete=models.PROTECT, - null=True, ) html = models.TextField( verbose_name=_('HTML'), @@ -44,10 +43,10 @@ class Snippet(models.Model): ) slug = models.SlugField( verbose_name=_('Slug'), - unique=True, blank=False, default='', max_length=255, + unique=True, ) def __str__(self): @@ -71,7 +70,10 @@ class SnippetPtr(CMSPlugin): parent_link=True, on_delete=models.CASCADE, ) - snippet = models.ForeignKey(Snippet, on_delete=models.CASCADE) + snippet_grouper = models.ForeignKey( + SnippetGrouper, + on_delete=models.CASCADE, + ) search_fields = ['snippet__html'] if SEARCH_ENABLED else [] @@ -79,6 +81,10 @@ class Meta: verbose_name = _('Snippet Ptr') verbose_name_plural = _('Snippet Ptrs') + @property + def snippet(self): + return self.snippet_grouper.snippet_set.first() + def __str__(self): # Return the referenced snippet's name rather than the default (ID #) return self.snippet.name diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 77fedde3..2ea5f4f9 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -3,3 +3,4 @@ tox coverage isort flake8 +factory-boy diff --git a/tests/settings.py b/tests/settings.py index 5656cebe..23c4edd3 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -2,6 +2,8 @@ HELPER_SETTINGS = { 'INSTALLED_APPS': [ 'tests.utils', + 'djangocms_versioning', + 'djangocms_snippet', ], 'CMS_LANGUAGES': { 1: [{ @@ -11,6 +13,7 @@ }, 'LANGUAGE_CODE': 'en', 'ALLOWED_HOSTS': ['localhost'], + 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED': True, } diff --git a/tests/test_models.py b/tests/test_models.py index 077e0010..fd3cdec6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,28 +1,32 @@ -from django.test import TestCase +from cms.test_utils.testcases import CMSTestCase from djangocms_snippet.models import SEARCH_ENABLED, Snippet, SnippetPtr +from .utils.factories import SnippetPluginFactory, SnippetWithVersionFactory -class SnippetModelTestCase(TestCase): - def setUp(self): - pass +class SnippetModelTestCase(CMSTestCase): - def tearDown(self): - pass + def setUp(self): + self.snippet = SnippetWithVersionFactory( + name="test snippet", + html="

hello world

", + slug="test_snippet", + ) + self.snippet.versions.last().publish(user=self.get_superuser()) + self.snippet_grouper = self.snippet.snippet_grouper + SnippetPluginFactory(snippet_grouper=self.snippet_grouper, language=["en"]) def test_settings(self): self.assertEqual(SEARCH_ENABLED, False) def test_snippet_instance(self): - Snippet.objects.create( - name="test snippet", - html="

hello world

", - slug="test_snippet", - ) instance = Snippet.objects.all() + self.assertEqual(instance.count(), 1) + instance = Snippet.objects.first() + self.assertEqual(instance.name, "test snippet") self.assertEqual(instance.html, "

hello world

") self.assertEqual(instance.slug, "test_snippet") @@ -30,16 +34,11 @@ def test_snippet_instance(self): self.assertEqual(str(instance), "test snippet") def test_snippet_ptr_instance(self): - snippet = Snippet.objects.create( - name="test snippet", - html="

hello world

", - slug="test_snippet", - ) - SnippetPtr.objects.create( - snippet=snippet, - ) instance = SnippetPtr.objects.all() + self.assertEqual(instance.count(), 1) + instance = SnippetPtr.objects.first() + # test strings self.assertEqual(str(instance), "test snippet") diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 2e971d10..db1a0fe3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,46 +1,70 @@ from cms.api import add_plugin, create_page from cms.test_utils.testcases import CMSTestCase -from djangocms_snippet.models import Snippet +from .utils.factories import ( + SnippetWithVersionFactory, +) class SnippetPluginsTestCase(CMSTestCase): def setUp(self): self.language = "en" + self.superuser = self.get_superuser() self.home = create_page( title="home", template="page.html", language=self.language, + created_by=self.superuser, ) self.page = create_page( title="help", template="page.html", language=self.language, + created_by=self.superuser, ) - self.pagecontent = self.page.pagecontent_set.first() - self.placeholder = self.pagecontent.placeholders.get(slot="content") - self.superuser = self.get_superuser() + # Publish our page content + self._publish(self.page) + self._publish(self.home) + self.pagecontent = self.page.pagecontent_set.last() + self.home_pagecontent = self.page.pagecontent_set.last() def tearDown(self): self.page.delete() self.home.delete() self.superuser.delete() + def _publish(self, grouper, language=None): + from djangocms_versioning.constants import DRAFT + version = self._get_version(grouper, DRAFT, language) + version.publish(self.superuser) + + def _get_version(self, grouper, version_state, language=None): + language = language or self.language + + from djangocms_versioning.models import Version + versions = Version.objects.filter_by_grouper(grouper).filter(state=version_state) + for version in versions: + if hasattr(version.content, 'language') and version.content.language == language: + return version + def test_html_rendering(self): - request_url = self.page.get_absolute_url(self.language) + "?toolbar_off=true" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="plugin_snippet", html="

Hello World

", slug="plugin_snippet", ) + snippet_grouper = snippet.snippet_grouper plugin = add_plugin( self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, - snippet=snippet, + snippet_grouper=snippet_grouper, ) + snippet.versions.last().publish(user=self.get_superuser()) + request_url = self.page.get_absolute_url("en") + self.assertEqual(plugin.snippet.name, "plugin_snippet") self.assertEqual(plugin.snippet.html, "

Hello World

") self.assertEqual(plugin.snippet.slug, "plugin_snippet") @@ -52,16 +76,19 @@ def test_html_rendering(self): def test_failing_html_rendering(self): request_url = self.page.get_absolute_url(self.language) + "?toolbar_off=true" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="plugin_snippet", html="{% import weirdness %}", slug="plugin_snippet", ) + snippet_grouper = snippet.snippet_grouper + snippet.versions.last().publish(user=self.get_superuser()) + add_plugin( self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, - snippet=snippet, + snippet_grouper=snippet_grouper, ) with self.login_user_context(self.superuser): @@ -71,20 +98,22 @@ def test_failing_html_rendering(self): self.assertContains(response, "Did you forget to register or load this tag?") def test_template_rendering(self): - request_url = self.page.get_absolute_url(self.language) + "?toolbar_off=true" + request_url = self.page.get_absolute_url() template = "snippet.html" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="plugin_snippet", template=template, slug="plugin_snippet", ) - snippet.save() + snippet_grouper = snippet.snippet_grouper + snippet.versions.last().publish(user=self.get_superuser()) plugin = add_plugin( self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, - snippet=snippet, + snippet_grouper=snippet_grouper, ) + self.assertEqual(plugin.snippet.name, "plugin_snippet") self.assertEqual(plugin.snippet.slug, "plugin_snippet") @@ -99,17 +128,18 @@ def test_template_rendering(self): def test_failing_template_rendering(self): request_url = self.page.get_absolute_url(self.language) + "?toolbar_off=true" template = "some_template" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="plugin_snippet", template=template, slug="plugin_snippet", ) - snippet.save() + snippet_grouper = snippet.snippet_grouper + snippet.versions.last().publish(user=self.get_superuser()) add_plugin( self.pagecontent.placeholders.get(slot="content"), "SnippetPlugin", self.language, - snippet=snippet, + snippet_grouper=snippet_grouper, ) with self.login_user_context(self.superuser): diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index b1bb45b5..468a7408 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -1,22 +1,23 @@ from django.core.exceptions import ObjectDoesNotExist from django.template import Context, Template from django.template.exceptions import TemplateSyntaxError -from django.test import TestCase -from djangocms_snippet.models import Snippet, SnippetPtr +from cms.test_utils.testcases import CMSTestCase +from .utils.factories import SnippetPluginFactory, SnippetWithVersionFactory -class SnippetTemplateTagTestCase(TestCase): + +class SnippetTemplateTagTestCase(CMSTestCase): def test_html_rendered(self): - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="test snippet", html="

hello {{ title }}

", slug="test_snippet", ) - SnippetPtr.objects.create( - snippet=snippet, - ) + snippet.versions.last().publish(user=self.get_superuser()) + snippet_grouper = snippet.snippet_grouper + SnippetPluginFactory(snippet_grouper=snippet_grouper, language=["en"]) context = Context({"title": "world"}) template_to_render = Template( @@ -24,6 +25,7 @@ def test_html_rendered(self): '{% snippet_fragment "test_snippet" %}' ) rendered_template = template_to_render.render(context) + self.assertInHTML('

hello world

', rendered_template) # test html errors @@ -38,14 +40,15 @@ def test_html_rendered(self): def test_template_rendered(self): template = "snippet.html" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="test snippet", + html="

hello {{ title }}

", template=template, slug="test_snippet", ) - SnippetPtr.objects.create( - snippet=snippet, - ) + snippet.versions.last().publish(user=self.get_superuser()) + snippet_grouper = snippet.snippet_grouper + SnippetPluginFactory(snippet_grouper=snippet_grouper, language=["en"]) # use a string to identify context = Context({}) @@ -76,14 +79,15 @@ def test_template_rendered(self): def test_template_errors(self): template = "does_not_exist.html" - snippet = Snippet.objects.create( + snippet = SnippetWithVersionFactory( name="test snippet", + html="

hello {{ title }}

", template=template, slug="test_snippet", ) - SnippetPtr.objects.create( - snippet=snippet, - ) + snippet.versions.last().publish(user=self.get_superuser()) + snippet_grouper = snippet.snippet_grouper + SnippetPluginFactory(snippet_grouper=snippet_grouper, language=["en"]) context = Context({}) template_to_render = Template( diff --git a/tests/utils/factories.py b/tests/utils/factories.py new file mode 100644 index 00000000..9d6210c6 --- /dev/null +++ b/tests/utils/factories.py @@ -0,0 +1,124 @@ +import string + +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType + +from cms.models import Placeholder + +import factory +from djangocms_versioning.models import Version +from factory.fuzzy import FuzzyInteger, FuzzyText + +from djangocms_snippet.models import Snippet, SnippetGrouper, SnippetPtr + + +class UserFactory(factory.django.DjangoModelFactory): + username = FuzzyText(length=12) + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + email = factory.LazyAttribute( + lambda u: "%s.%s@example.com" % (u.first_name.lower(), u.last_name.lower()) + ) + + class Meta: + model = User + + @classmethod + def _create(cls, model_class, *args, **kwargs): + """Override the default ``_create`` with our custom call.""" + manager = cls._get_manager(model_class) + # The default would use ``manager.create(*args, **kwargs)`` + return manager.create_user(*args, **kwargs) + + +class AbstractVersionFactory(factory.django.DjangoModelFactory): + object_id = factory.SelfAttribute("content.id") + content_type = factory.LazyAttribute( + lambda o: ContentType.objects.get_for_model(o.content) + ) + created_by = factory.SubFactory(UserFactory) + + class Meta: + exclude = ["content"] + abstract = True + + +class PlaceholderFactory(factory.django.DjangoModelFactory): + default_width = FuzzyInteger(0, 25) + slot = FuzzyText(length=2, chars=string.digits) + # NOTE: When using this factory you will probably want to set + # the source field manually + + class Meta: + model = Placeholder + + +class SnippetGrouperFactory(factory.django.DjangoModelFactory): + + class Meta: + model = SnippetGrouper + + +class AbstractSnippetFactory(factory.django.DjangoModelFactory): + name = FuzzyText(length=12) + slug = FuzzyText(length=12) + snippet_grouper = factory.SubFactory(SnippetGrouperFactory) + html = "" + template = "" + + class Meta: + abstract = True + + +class SnippetFactory(AbstractSnippetFactory): + class Meta: + model = Snippet + + +class SnippetVersionFactory(AbstractVersionFactory): + content = factory.SubFactory(SnippetFactory) + + class Meta: + model = Version + + +class SnippetWithVersionFactory(AbstractSnippetFactory): + @factory.post_generation + def version(self, create, extracted, **kwargs): + # NOTE: Use this method as below to define version attributes: + # PageContentWithVersionFactory(version__label='label1') + if not create: + # Simple build, do nothing. + return + SnippetVersionFactory(content=self, **kwargs) + + class Meta: + model = Snippet + + +def get_plugin_position(plugin): + """Helper function to correctly calculate the plugin position. + Use this in plugin factory classes + """ + offset = plugin.placeholder.get_last_plugin_position(plugin.language) or 0 + return offset + 1 + + +def get_plugin_language(plugin): + """Helper function to get the language from a plugin's relationships. + Use this in plugin factory classes + """ + if plugin.placeholder.source: + return plugin.placeholder.source.language + + +class SnippetPluginFactory(factory.django.DjangoModelFactory): + plugin_type = "SnippetPlugin" + parent = None + snippet_grouper = factory.SubFactory(SnippetGrouperFactory) + placeholder = factory.SubFactory(PlaceholderFactory) + position = factory.LazyAttribute(get_plugin_position) + language = factory.LazyAttribute(get_plugin_language) + + class Meta: + model = SnippetPtr From 4fd5662eee9d035df1bff507034b9e748be9d523 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Fri, 17 Sep 2021 17:20:28 +0100 Subject: [PATCH 03/66] feat: Moderation compatibility and version copy method implementation (#77) --- djangocms_snippet/cms_config.py | 16 +++++++++- .../migrations/0013_auto_20210915_0751.py | 23 ++++++++++++++ djangocms_snippet/models.py | 2 -- tests/settings.py | 1 + tests/test_config.py | 30 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 djangocms_snippet/migrations/0013_auto_20210915_0751.py create mode 100644 tests/test_config.py diff --git a/djangocms_snippet/cms_config.py b/djangocms_snippet/cms_config.py index cf87874d..c23f2cd1 100644 --- a/djangocms_snippet/cms_config.py +++ b/djangocms_snippet/cms_config.py @@ -2,13 +2,27 @@ from cms.app_base import CMSAppConfig -from .models import Snippet +from djangocms_snippet.models import Snippet + + +try: + from djangocms_moderation import __version__ # NOQA + + djangocms_moderation_installed = True +except ImportError: + djangocms_moderation_installed = False class SnippetCMSAppConfig(CMSAppConfig): djangocms_versioning_enabled = getattr( settings, 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED', False ) + djangocms_moderation_enabled = getattr( + settings, 'DJANGOCMS_SNIPPET_MODERATION_ENABLED', False + ) + + if djangocms_moderation_enabled and djangocms_moderation_installed: + moderated_models = [Snippet] if djangocms_versioning_enabled: from djangocms_versioning.datastructures import ( diff --git a/djangocms_snippet/migrations/0013_auto_20210915_0751.py b/djangocms_snippet/migrations/0013_auto_20210915_0751.py new file mode 100644 index 00000000..de813001 --- /dev/null +++ b/djangocms_snippet/migrations/0013_auto_20210915_0751.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.24 on 2021-09-15 07:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_snippet', '0012_auto_20210915_0721'), + ] + + operations = [ + migrations.AlterField( + model_name='snippet', + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='snippet', + name='slug', + field=models.SlugField(default='', max_length=255, verbose_name='Slug'), + ), + ] diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index d7094b10..c5b0a34e 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -21,7 +21,6 @@ class Snippet(models.Model): name = models.CharField( verbose_name=_('Name'), max_length=255, - unique=True, ) snippet_grouper = models.ForeignKey( SnippetGrouper, @@ -46,7 +45,6 @@ class Snippet(models.Model): blank=False, default='', max_length=255, - unique=True, ) def __str__(self): diff --git a/tests/settings.py b/tests/settings.py index 23c4edd3..67f86952 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -14,6 +14,7 @@ 'LANGUAGE_CODE': 'en', 'ALLOWED_HOSTS': ['localhost'], 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED': True, + 'DJANGOCMS_SNIPPET_MODERATION_ENABLED': True, } diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..9f93e773 --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,30 @@ +from django.apps import apps + +from cms.test_utils.testcases import CMSTestCase + +from djangocms_snippet.models import Snippet, SnippetGrouper + +from .utils.factories import SnippetWithVersionFactory + + +class VersioningConfigTestCase(CMSTestCase): + + def test_snippet_copy_method(self): + """ + App should use the default copy method, and return an identical model (apart from PK) + """ + snippet_cms_config = apps.get_app_config("djangocms_snippet").cms_config + old_snippet = SnippetWithVersionFactory( + name="snippet", + html="

Hello World

", + slug="snippet", + ) + + new_snippet = snippet_cms_config.versioning[0].copy_function(old_snippet) + + self.assertNotEqual(old_snippet, new_snippet) + self.assertEqual(old_snippet.name, new_snippet.name) + self.assertEqual(old_snippet.html, new_snippet.html) + self.assertEqual(old_snippet.snippet_grouper, new_snippet.snippet_grouper) + self.assertEqual(1, SnippetGrouper.objects.count()) + self.assertEqual(2, Snippet._base_manager.count()) From e215e01be429e6fd24a6e97233ead2ccc497ee62 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Thu, 14 Oct 2021 14:19:15 +0100 Subject: [PATCH 04/66] feat: Versioning Compatibility Changes (#79) --- djangocms_snippet/admin.py | 51 +++++- djangocms_snippet/cms_config.py | 9 +- djangocms_snippet/forms.py | 61 +++++++ djangocms_snippet/models.py | 23 ++- djangocms_snippet/rendering.py | 7 + .../djangocms_snippet/admin/preview.html | 18 ++ djangocms_snippet/views.py | 26 +++ tests/requirements/dj22_cms37.txt | 4 - tests/requirements/dj22_cms38.txt | 4 - tests/requirements/dj30_cms37.txt | 4 - tests/requirements/dj30_cms38.txt | 4 - tests/requirements/dj31_cms38.txt | 4 - tests/settings.py | 3 + tests/test_admin.py | 53 ++++++ tests/test_forms.py | 159 ++++++++++++++++++ tests/test_models.py | 2 +- tests/test_plugins.py | 4 +- tests/test_views.py | 38 +++++ 18 files changed, 442 insertions(+), 32 deletions(-) create mode 100644 djangocms_snippet/forms.py create mode 100644 djangocms_snippet/rendering.py create mode 100644 djangocms_snippet/templates/djangocms_snippet/admin/preview.html create mode 100644 djangocms_snippet/views.py delete mode 100644 tests/requirements/dj22_cms37.txt delete mode 100644 tests/requirements/dj22_cms38.txt delete mode 100644 tests/requirements/dj30_cms37.txt delete mode 100644 tests/requirements/dj30_cms38.txt delete mode 100644 tests/requirements/dj31_cms38.txt create mode 100644 tests/test_admin.py create mode 100644 tests/test_forms.py create mode 100644 tests/test_views.py diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 9187fadd..48e02314 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -1,12 +1,28 @@ from django.conf import settings +from django.conf.urls import url from django.contrib import admin from django.db import models from django.forms import Textarea +from .cms_config import SnippetCMSAppConfig from .models import Snippet +from .views import SnippetPreviewView -class SnippetAdmin(admin.ModelAdmin): +# Use the version mixin if djangocms-versioning is installed and enabled +snippet_admin_classes = [admin.ModelAdmin] +djangocms_versioning_enabled = SnippetCMSAppConfig.djangocms_versioning_enabled + +try: + from djangocms_versioning.admin import ExtendedVersionAdminMixin + + if djangocms_versioning_enabled: + snippet_admin_classes.insert(0, ExtendedVersionAdminMixin) +except ImportError: + pass + + +class SnippetAdmin(*snippet_admin_classes): list_display = ('slug', 'name') search_fields = ['slug', 'name'] prepopulated_fields = {'slug': ('name',)} @@ -22,5 +38,38 @@ class SnippetAdmin(admin.ModelAdmin): models.TextField: {'widget': Textarea(attrs=text_area_attrs)} } + class Meta: + model = Snippet + + def get_urls(self): + info = self.model._meta.app_label, self.model._meta.model_name + return [ + url( + r"^$", + self.admin_site.admin_view(self.changelist_view), + name="{}_{}_changelist".format(*info), + ), + url( + r"^(?P\d+)/$", + self.admin_site.admin_view(self.changelist_view), + name="{}_{}_list".format(*info), + ), + url( + r"^add/$", + self.admin_site.admin_view(self.add_view), + name="{}_{}_add".format(*info), + ), + url( + r"^(?P\d+)/change/$", + self.admin_site.admin_view(self.change_view), + name="{}_{}_change".format(*info), + ), + url( + r"^(?P\d+)/preview/$", + self.admin_site.admin_view(SnippetPreviewView.as_view()), + name="{}_{}_preview".format(*info), + ), + ] + admin.site.register(Snippet, SnippetAdmin) diff --git a/djangocms_snippet/cms_config.py b/djangocms_snippet/cms_config.py index c23f2cd1..9d9d4629 100644 --- a/djangocms_snippet/cms_config.py +++ b/djangocms_snippet/cms_config.py @@ -3,6 +3,7 @@ from cms.app_base import CMSAppConfig from djangocms_snippet.models import Snippet +from djangocms_snippet.rendering import render_snippet try: @@ -15,12 +16,16 @@ class SnippetCMSAppConfig(CMSAppConfig): djangocms_versioning_enabled = getattr( - settings, 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED', False + settings, 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED', True ) djangocms_moderation_enabled = getattr( - settings, 'DJANGOCMS_SNIPPET_MODERATION_ENABLED', False + settings, 'DJANGOCMS_SNIPPET_MODERATION_ENABLED', True ) + cms_enabled = True + # cms toolbar enabled to allow for versioning compare view + cms_toolbar_enabled_models = [(Snippet, render_snippet), ] + if djangocms_moderation_enabled and djangocms_moderation_installed: moderated_models = [Snippet] diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py new file mode 100644 index 00000000..8798c362 --- /dev/null +++ b/djangocms_snippet/forms.py @@ -0,0 +1,61 @@ +from django import forms +from django.db import transaction +from django.utils.translation import ugettext_lazy as _ + +from djangocms_snippet.cms_config import SnippetCMSAppConfig +from djangocms_snippet.models import Snippet, SnippetGrouper + + +try: + from djangocms_versioning import __version__ # NOQA + is_versioning_installed = True +except ImportError: + is_versioning_installed = False + +djangocms_versioning_enabled = SnippetCMSAppConfig.djangocms_versioning_enabled + + +class SnippetForm(forms.ModelForm): + class Meta: + model = Snippet + fields = ( + "name", + "html", + "slug", + "snippet_grouper", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["snippet_grouper"].required = False + + def clean(self): + data = super().clean() + name = data.get("name") + slug = data.get("slug") + snippet_grouper = data.get("snippet_grouper") + snippet_queryset = Snippet.objects.all() + + if djangocms_versioning_enabled and is_versioning_installed: + if snippet_grouper: + snippet_queryset = snippet_queryset.exclude(snippet_grouper=snippet_grouper) + + for snippet in snippet_queryset: + if snippet.name == name: + self.add_error( + "name", _("A Snippet with this name already exists") + ) + elif snippet.slug == slug: + self.add_error( + "slug", _("A Snippet with this slug already exists") + ) + + return data + + @transaction.atomic + def save(self, **kwargs): + if not self.cleaned_data.get("snippet_grouper"): + super().save(commit=False) + self.save_m2m() + self.instance.snippet_grouper = SnippetGrouper.objects.create() + return super().save() diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index c5b0a34e..459cea08 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -1,5 +1,6 @@ from django.conf import settings from django.db import models +from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ from cms.models import CMSPlugin @@ -10,7 +11,15 @@ class SnippetGrouper(models.Model): - pass + @property + def name(self): + snippet_qs = Snippet._base_manager.filter( + snippet_grouper=self + ) + return snippet_qs.first().name or super().__str__ + + def __str__(self): + return self.name # Stores the actual data @@ -50,6 +59,14 @@ class Snippet(models.Model): def __str__(self): return self.name + def get_preview_url(self): + return reverse( + "admin:{app}_{model}_preview".format( + app=self._meta.app_label, model=self._meta.model_name, + ), + args=[self.id], + ) + class Meta: ordering = ['name'] verbose_name = _('Snippet') @@ -82,7 +99,3 @@ class Meta: @property def snippet(self): return self.snippet_grouper.snippet_set.first() - - def __str__(self): - # Return the referenced snippet's name rather than the default (ID #) - return self.snippet.name diff --git a/djangocms_snippet/rendering.py b/djangocms_snippet/rendering.py new file mode 100644 index 00000000..9525b4d5 --- /dev/null +++ b/djangocms_snippet/rendering.py @@ -0,0 +1,7 @@ +from django.template.response import TemplateResponse + + +def render_snippet(request, snippet): + template = 'djangocms_snippet/admin/preview.html' + context = {'snippet': snippet} + return TemplateResponse(request, template, context) diff --git a/djangocms_snippet/templates/djangocms_snippet/admin/preview.html b/djangocms_snippet/templates/djangocms_snippet/admin/preview.html new file mode 100644 index 00000000..723b71b5 --- /dev/null +++ b/djangocms_snippet/templates/djangocms_snippet/admin/preview.html @@ -0,0 +1,18 @@ +{% extends "admin/base_site.html" %} +{% load static %} +{{ snippet.html|safe|escape }} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + +{% block coltype %}flex{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} + +{% block content %} + {% autoescape off %} + {{ snippet.html }} + {% endautoescape %} +{% endblock %} diff --git a/djangocms_snippet/views.py b/djangocms_snippet/views.py new file mode 100644 index 00000000..26edbe5f --- /dev/null +++ b/djangocms_snippet/views.py @@ -0,0 +1,26 @@ +from django.http import Http404 +from django.views.generic import TemplateView + +from djangocms_snippet.models import Snippet + + +class SnippetPreviewView(TemplateView): + template_name = "djangocms_snippet/admin/preview.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + snippet_id = kwargs.get("snippet_id", None) + + if not snippet_id: + Http404("snippet_id must be provided.") + + try: + snippet = Snippet._base_manager.get(pk=self.kwargs.get("snippet_id")) + except Snippet.DoesNotExist: + raise Http404 + + context.update({ + "snippet": snippet, + "opts": Snippet._meta + }) + return context diff --git a/tests/requirements/dj22_cms37.txt b/tests/requirements/dj22_cms37.txt deleted file mode 100644 index 6b96a102..00000000 --- a/tests/requirements/dj22_cms37.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -Django>=2.2,<3.0 -django-cms>=3.7,<3.8 diff --git a/tests/requirements/dj22_cms38.txt b/tests/requirements/dj22_cms38.txt deleted file mode 100644 index 22c4fef2..00000000 --- a/tests/requirements/dj22_cms38.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -Django>=2.2,<3.0 -django-cms>=3.8,<3.9 diff --git a/tests/requirements/dj30_cms37.txt b/tests/requirements/dj30_cms37.txt deleted file mode 100644 index edcfd450..00000000 --- a/tests/requirements/dj30_cms37.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -Django>=3.0,<3.1 -django-cms>=3.7,<3.8 diff --git a/tests/requirements/dj30_cms38.txt b/tests/requirements/dj30_cms38.txt deleted file mode 100644 index 7bc3c0be..00000000 --- a/tests/requirements/dj30_cms38.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -Django>=3.0,<3.1 -django-cms>=3.8,<3.9 diff --git a/tests/requirements/dj31_cms38.txt b/tests/requirements/dj31_cms38.txt deleted file mode 100644 index d17fdb42..00000000 --- a/tests/requirements/dj31_cms38.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -Django>=3.1,<3.2 -django-cms>=3.8,<3.9 diff --git a/tests/settings.py b/tests/settings.py index 67f86952..60b52c6d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -15,6 +15,9 @@ 'ALLOWED_HOSTS': ['localhost'], 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED': True, 'DJANGOCMS_SNIPPET_MODERATION_ENABLED': True, + 'CMS_TEMPLATES': ( + ("page.html", "Normal page"), + ), } diff --git a/tests/test_admin.py b/tests/test_admin.py new file mode 100644 index 00000000..0aced601 --- /dev/null +++ b/tests/test_admin.py @@ -0,0 +1,53 @@ +from importlib import reload + +from django.contrib import admin +from django.test import RequestFactory, override_settings + +from cms.test_utils.testcases import CMSTestCase + +from djangocms_snippet import admin as snippet_admin +from djangocms_snippet import cms_config +from djangocms_snippet.models import Snippet + +from .utils.factories import SnippetWithVersionFactory + + +class SnippetAdminTestCase(CMSTestCase): + def setUp(self): + self.snippet = SnippetWithVersionFactory() + self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) + self.snippet_admin_request = RequestFactory().get("/admin/djangocms_snippet") + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) + def test_admin_list_display_without_versioning(self): + """ + Without versioning enabled, list_display should not be extended with version related items + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) + + list_display = self.snippet_admin.get_list_display(self.snippet_admin_request) + + self.assertEqual(self.snippet_admin.__class__.__bases__, (admin.ModelAdmin, )) + self.assertEqual(list_display, ('slug', 'name')) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_admin_list_display_with_versioning(self): + """ + With versioning enabled, list_display should be populated with both versioning related items, and the + list actions items + """ + from djangocms_versioning.admin import ExtendedVersionAdminMixin + list_display = self.snippet_admin.get_list_display(self.snippet_admin_request) + + # Mixins should always come first in the class bases + self.assertEqual( + self.snippet_admin.__class__.__bases__, (ExtendedVersionAdminMixin, admin.ModelAdmin) + ) + self.assertEqual( + list_display[:-1], ('slug', 'name', 'get_author', 'get_modified_date', 'get_versioning_state') + ) + self.assertEqual(list_display[-1].short_description, 'actions') + self.assertIn("function ExtendedVersionAdminMixin._list_actions", list_display[-1].__str__()) diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 00000000..d8653d99 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,159 @@ +from django.test import override_settings + +from cms.test_utils.testcases import CMSTestCase + +from djangocms_snippet.forms import SnippetForm +from djangocms_snippet.models import Snippet, SnippetGrouper + +from .utils.factories import SnippetWithVersionFactory + + +class SnippetFormTestCase(CMSTestCase): + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) + def test_snippet_form_creates_grouper_no_versioning(self): + """ + Without versioning enabled, the application still has the grouper implemented, therefore the form + should be creating one for each new snippet created. + """ + form_data = { + "name": "test_snippet", + "slug": "test_snippet", + "html": "

Test Title

" + } + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + form.clean() + form.save() + + self.assertEqual(SnippetGrouper.objects.count(), 1) + self.assertEqual(Snippet._base_manager.count(), 1) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_snippet_form_creates_grouper_with_versioning(self): + """ + With versioning enabled, groupers should also be created in the background. + """ + form_data = { + "name": "test_snippet", + "slug": "test_snippet", + "html": "

Test Title

" + } + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + form.clean() + form.save() + + self.assertEqual(SnippetGrouper.objects.count(), 1) + self.assertEqual(Snippet._base_manager.count(), 1) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_snippet_form_adds_to_existing_grouper_with_versioning(self): + """ + With versioning enabled, if a grouper already exists, a new one shouldn't be created + """ + + grouper = SnippetGrouper.objects.create() + form_data = { + "name": "test_snippet", + "slug": "test_snippet", + "html": "

Test Title

", + "snippet_grouper": grouper.id, + } + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + form.clean() + form.save() + + self.assertEqual(SnippetGrouper.objects.count(), 1) + self.assertEqual(Snippet._base_manager.count(), 1) + + form_data["html"] = "

Test Title

" + + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + form.clean() + form.save() + + self.assertEqual(SnippetGrouper.objects.count(), 1) + self.assertEqual(Snippet._base_manager.count(), 2) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_snippet_form_versioning_enabled(self): + """ + With versioning enabled, the snippet form doesn't have to create groupers, but does have to validate + that no other active (i.e. the latest published snippet from a given grouper) shares the same name or slug. + """ + form_data = { + "name": "test_snippet", + "slug": "test_snippet", + "html": "

Test Title

", + } + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + # Clean and save the form + form.clean() + form.save() + + # Publish the old created version + snippet = Snippet._base_manager.last() + version = snippet.versions.create(created_by=self.get_superuser()) + version.publish(user=self.get_superuser()) + + new_form_data = { + "name": "test_snippet1", + "slug": "test_snippet", + "html": "

Another Test Title

", + } + + new_form = SnippetForm(new_form_data) + + self.assertFalse(new_form.is_valid()) + + new_form.clean() + + self.assertDictEqual(new_form.errors, {'slug': ['A Snippet with this slug already exists']}) + + def test_snippet_form_validation_multiple_version_states_in_grouper(self): + """ + Snippet forms should be valid regardless of the versions, or states which already exist within its grouper. + """ + # snippet_to_archive starts as draft + snippet_to_archive = SnippetWithVersionFactory() + # Then it is published it + snippet_to_archive.versions.first().publish(user=self.get_superuser()) + # snippet_to_publish starts as a draft + snippet_to_publish = SnippetWithVersionFactory( + name=snippet_to_archive.name, + slug=snippet_to_archive.slug, + snippet_grouper=snippet_to_archive.snippet_grouper + ) + # Snippet_to_publish is published, archiving snippet_to_archive + snippet_to_publish.versions.first().publish(user=self.get_superuser()) + # Create a new draft in the same grouper + SnippetWithVersionFactory( + name=snippet_to_archive.name, + slug=snippet_to_archive.slug, + snippet_grouper=snippet_to_archive.snippet_grouper + ) + + form_data = { + "name": snippet_to_archive.name, + "slug": snippet_to_archive.slug, + "html": "

Hello World!

", + "snippet_grouper": snippet_to_archive.snippet_grouper.id, + } + + form = SnippetForm(form_data) + + self.assertTrue(form.is_valid()) diff --git a/tests/test_models.py b/tests/test_models.py index fd3cdec6..c10f5cae 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -41,4 +41,4 @@ def test_snippet_ptr_instance(self): instance = SnippetPtr.objects.first() # test strings - self.assertEqual(str(instance), "test snippet") + self.assertEqual(instance.snippet_grouper.name, "test snippet") diff --git a/tests/test_plugins.py b/tests/test_plugins.py index db1a0fe3..ba901439 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,9 +1,7 @@ from cms.api import add_plugin, create_page from cms.test_utils.testcases import CMSTestCase -from .utils.factories import ( - SnippetWithVersionFactory, -) +from .utils.factories import SnippetWithVersionFactory class SnippetPluginsTestCase(CMSTestCase): diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..ce3c7e5a --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,38 @@ +from cms.test_utils.testcases import CMSTestCase +from cms.utils.urlutils import admin_reverse + +from .utils.factories import SnippetWithVersionFactory + + +class PreviewViewTestCase(CMSTestCase): + def setUp(self): + self.snippet = SnippetWithVersionFactory(html="

Test Title


Test paragraph

") + self.user = self.get_superuser() + + def test_preview_renders_html(self): + """ + Check that our snippet HTML is rendered, unescaped, on the page + """ + preview_url = admin_reverse( + "djangocms_snippet_snippet_preview", + kwargs={"snippet_id": self.snippet.id}, + ) + with self.login_user_context(self.user): + response = self.client.get(preview_url) + + self.assertEqual(self.snippet.html, "

Test Title


Test paragraph

") + self.assertEqual(response.status_code, 200) + self.assertContains(response, "

Test Title


Test paragraph

") + + def test_preview_raises_404_no_snippet(self): + """ + With no Snippet to preview, a 404 will be raised + """ + preview_url = admin_reverse( + "djangocms_snippet_snippet_preview", + kwargs={"snippet_id": 999}, # Non existent PK! + ) + with self.login_user_context(self.user): + response = self.client.get(preview_url) + + self.assertEqual(response.status_code, 404) From d2b1b63562e6a99998ebb59ac9c355cbdddb57a0 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Thu, 14 Oct 2021 17:57:49 +0100 Subject: [PATCH 05/66] fix: Fixed bug that causes ChangeView not to use SnippetForm (#80) --- djangocms_snippet/admin.py | 3 ++- djangocms_snippet/forms.py | 1 + tests/test_admin.py | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 48e02314..2e4cdb1e 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -5,6 +5,7 @@ from django.forms import Textarea from .cms_config import SnippetCMSAppConfig +from .forms import SnippetForm from .models import Snippet from .views import SnippetPreviewView @@ -33,7 +34,7 @@ class SnippetAdmin(*snippet_admin_classes): 'data-mode': getattr(settings, 'DJANGOCMS_SNIPPET_THEME', 'html'), 'data-theme': getattr(settings, 'DJANGOCMS_SNIPPET_MODE', 'github'), } - + form = SnippetForm formfield_overrides = { models.TextField: {'widget': Textarea(attrs=text_area_attrs)} } diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 8798c362..1afac618 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -28,6 +28,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["snippet_grouper"].required = False + self.fields["snippet_grouper"].widget = forms.HiddenInput() def clean(self): data = super().clean() diff --git a/tests/test_admin.py b/tests/test_admin.py index 0aced601..2e007de9 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -7,6 +7,7 @@ from djangocms_snippet import admin as snippet_admin from djangocms_snippet import cms_config +from djangocms_snippet.forms import SnippetForm from djangocms_snippet.models import Snippet from .utils.factories import SnippetWithVersionFactory @@ -51,3 +52,10 @@ def test_admin_list_display_with_versioning(self): ) self.assertEqual(list_display[-1].short_description, 'actions') self.assertIn("function ExtendedVersionAdminMixin._list_actions", list_display[-1].__str__()) + + def test_admin_uses_form(self): + """ + The SnippetForm provides functionality to make SnippetGroupers irrelevant to the user, + ensure the admin uses this. + """ + self.assertEqual(self.snippet_admin.form, SnippetForm) From c3eb8d9f862b753a61f7b1a71147f3fcb97293f3 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Wed, 20 Oct 2021 20:15:47 +0100 Subject: [PATCH 06/66] feat: Reworked form fields and added the ability to show draft snippets (#81) --- djangocms_snippet/admin.py | 2 + djangocms_snippet/cms_plugins.py | 14 +- djangocms_snippet/forms.py | 13 +- .../migrations/0014_auto_20211019_0522.py | 17 +++ djangocms_snippet/models.py | 20 ++- djangocms_snippet/utils.py | 11 ++ tests/test_admin.py | 11 +- tests/test_forms.py | 63 ++++++--- tests/test_plugins.py | 131 +++++++++++++----- tests/utils/factories.py | 5 +- 10 files changed, 216 insertions(+), 71 deletions(-) create mode 100644 djangocms_snippet/migrations/0014_auto_20211019_0522.py create mode 100644 djangocms_snippet/utils.py diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 2e4cdb1e..ba5c9f2f 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -38,6 +38,8 @@ class SnippetAdmin(*snippet_admin_classes): formfield_overrides = { models.TextField: {'widget': Textarea(attrs=text_area_attrs)} } + # This was move here from model, otherwise first() and last() return the same when handling grouper queries + ordering = ('name',) class Meta: model = Snippet diff --git a/djangocms_snippet/cms_plugins.py b/djangocms_snippet/cms_plugins.py index bf432da9..63e691fc 100644 --- a/djangocms_snippet/cms_plugins.py +++ b/djangocms_snippet/cms_plugins.py @@ -8,6 +8,7 @@ from cms.plugin_pool import plugin_pool from .models import SnippetPtr +from .utils import show_draft_content CACHE_ENABLED = getattr(settings, "DJANGOCMS_SNIPPET_CACHE", False) @@ -22,19 +23,20 @@ class SnippetPlugin(CMSPluginBase): cache = CACHE_ENABLED def render(self, context, instance, placeholder): + snippet = instance.snippet_grouper.snippet(show_editable=show_draft_content(context["request"])) try: - if instance.snippet.template: + if snippet.template: context = context.flatten() - context.update({"html": mark_safe(instance.snippet.html)}) - t = template.loader.get_template(instance.snippet.template) + context.update({"html": mark_safe(snippet.html)}) + t = template.loader.get_template(snippet.template) content = t.render(context) else: # only html provided - t = template.Template(instance.snippet.html) + t = template.Template(snippet.html) content = t.render(context) except template.TemplateDoesNotExist: content = _("Template %(template)s does not exist.") % { - "template": instance.snippet.template + "template": snippet.template } except Exception as e: content = escape(str(e)) @@ -43,7 +45,7 @@ def render(self, context, instance, placeholder): { "placeholder": placeholder, "object": instance, - "html": mark_safe(instance.snippet.html), + "html": mark_safe(snippet.html), "content": content, } ) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 1afac618..55b5f993 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -23,6 +23,7 @@ class Meta: "html", "slug", "snippet_grouper", + "template", ) def __init__(self, *args, **kwargs): @@ -55,8 +56,10 @@ def clean(self): @transaction.atomic def save(self, **kwargs): - if not self.cleaned_data.get("snippet_grouper"): - super().save(commit=False) - self.save_m2m() - self.instance.snippet_grouper = SnippetGrouper.objects.create() - return super().save() + commit = kwargs.get("commit", True) + snippet = super().save(commit=False) + if commit: + if not hasattr(snippet, "snippet_grouper"): + snippet.snippet_grouper = SnippetGrouper.objects.create() + snippet.save() + return snippet diff --git a/djangocms_snippet/migrations/0014_auto_20211019_0522.py b/djangocms_snippet/migrations/0014_auto_20211019_0522.py new file mode 100644 index 00000000..1f70ec08 --- /dev/null +++ b/djangocms_snippet/migrations/0014_auto_20211019_0522.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.24 on 2021-10-19 10:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('djangocms_snippet', '0013_auto_20210915_0751'), + ] + + operations = [ + migrations.AlterModelOptions( + name='snippet', + options={'verbose_name': 'Snippet', 'verbose_name_plural': 'Snippets'}, + ), + ] diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index 459cea08..177caddc 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -5,12 +5,17 @@ from cms.models import CMSPlugin +from djangocms_versioning.constants import DRAFT, PUBLISHED + # Search is enabled by default to keep backwards compatibility. SEARCH_ENABLED = getattr(settings, 'DJANGOCMS_SNIPPET_SEARCH', False) class SnippetGrouper(models.Model): + """ + The Grouper model for snippet, this is required for versioning + """ @property def name(self): snippet_qs = Snippet._base_manager.filter( @@ -18,6 +23,16 @@ def name(self): ) return snippet_qs.first().name or super().__str__ + def snippet(self, show_editable=False): + if show_editable: + # When in "edit" or "preview" mode we should be able to see the latest content + return Snippet._base_manager.filter( + versions__state__in=[DRAFT, PUBLISHED], + snippet_grouper=self, + ).order_by("-pk").first() + # When in "live" mode we should only be able to see the default published version + return Snippet.objects.filter(snippet_grouper=self).first() + def __str__(self): return self.name @@ -68,7 +83,6 @@ def get_preview_url(self): ) class Meta: - ordering = ['name'] verbose_name = _('Snippet') verbose_name_plural = _('Snippets') @@ -95,7 +109,3 @@ class SnippetPtr(CMSPlugin): class Meta: verbose_name = _('Snippet Ptr') verbose_name_plural = _('Snippet Ptrs') - - @property - def snippet(self): - return self.snippet_grouper.snippet_set.first() diff --git a/djangocms_snippet/utils.py b/djangocms_snippet/utils.py new file mode 100644 index 00000000..13d88b7f --- /dev/null +++ b/djangocms_snippet/utils.py @@ -0,0 +1,11 @@ +from cms.toolbar.utils import get_toolbar_from_request + + +def show_draft_content(request=None): + """ + Returns True if draft contents should be shown. + """ + if not request: + return False + request_toolbar = get_toolbar_from_request(request) + return request_toolbar.edit_mode_active or request_toolbar.preview_mode_active diff --git a/tests/test_admin.py b/tests/test_admin.py index 2e007de9..e1ea1563 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -8,14 +8,17 @@ from djangocms_snippet import admin as snippet_admin from djangocms_snippet import cms_config from djangocms_snippet.forms import SnippetForm -from djangocms_snippet.models import Snippet - -from .utils.factories import SnippetWithVersionFactory +from djangocms_snippet.models import Snippet, SnippetGrouper class SnippetAdminTestCase(CMSTestCase): def setUp(self): - self.snippet = SnippetWithVersionFactory() + self.snippet = Snippet.objects.create( + name="Test Snippet", + slug="test-snippet", + html="

This is a test

", + snippet_grouper=SnippetGrouper.objects.create(), + ) self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) self.snippet_admin_request = RequestFactory().get("/admin/djangocms_snippet") diff --git a/tests/test_forms.py b/tests/test_forms.py index d8653d99..a1b806fb 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,8 +1,10 @@ +from importlib import reload + from django.test import override_settings from cms.test_utils.testcases import CMSTestCase -from djangocms_snippet.forms import SnippetForm +from djangocms_snippet import cms_config, forms from djangocms_snippet.models import Snippet, SnippetGrouper from .utils.factories import SnippetWithVersionFactory @@ -16,17 +18,19 @@ def test_snippet_form_creates_grouper_no_versioning(self): Without versioning enabled, the application still has the grouper implemented, therefore the form should be creating one for each new snippet created. """ + reload(cms_config) + reload(forms) form_data = { "name": "test_snippet", "slug": "test_snippet", "html": "

Test Title

" } - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) form.clean() - form.save() + form.save(commit=True) self.assertEqual(SnippetGrouper.objects.count(), 1) self.assertEqual(Snippet._base_manager.count(), 1) @@ -36,27 +40,52 @@ def test_snippet_form_creates_grouper_with_versioning(self): """ With versioning enabled, groupers should also be created in the background. """ + reload(cms_config) + reload(forms) form_data = { "name": "test_snippet", "slug": "test_snippet", "html": "

Test Title

" } - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) form.clean() - form.save() + form.save(commit=True) self.assertEqual(SnippetGrouper.objects.count(), 1) self.assertEqual(Snippet._base_manager.count(), 1) + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_snippet_form_doesnt_create_grouper_or_snippet_with_no_commit(self): + """ + With versioning enabled, but commit=False, models should not be created + """ + reload(cms_config) + reload(forms) + form_data = { + "name": "test_snippet", + "slug": "test_snippet", + "html": "

Test Title

" + } + form = forms.SnippetForm(form_data) + + self.assertTrue(form.is_valid()) + + form.clean() + form.save(commit=False) + + self.assertEqual(SnippetGrouper.objects.count(), 0) + self.assertEqual(Snippet._base_manager.count(), 0) + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) def test_snippet_form_adds_to_existing_grouper_with_versioning(self): """ With versioning enabled, if a grouper already exists, a new one shouldn't be created """ - + reload(cms_config) + reload(forms) grouper = SnippetGrouper.objects.create() form_data = { "name": "test_snippet", @@ -64,24 +93,24 @@ def test_snippet_form_adds_to_existing_grouper_with_versioning(self): "html": "

Test Title

", "snippet_grouper": grouper.id, } - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) form.clean() - form.save() + form.save(commit=True) self.assertEqual(SnippetGrouper.objects.count(), 1) self.assertEqual(Snippet._base_manager.count(), 1) form_data["html"] = "

Test Title

" - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) form.clean() - form.save() + form.save(commit=True) self.assertEqual(SnippetGrouper.objects.count(), 1) self.assertEqual(Snippet._base_manager.count(), 2) @@ -92,21 +121,21 @@ def test_snippet_form_versioning_enabled(self): With versioning enabled, the snippet form doesn't have to create groupers, but does have to validate that no other active (i.e. the latest published snippet from a given grouper) shares the same name or slug. """ + reload(cms_config) + reload(forms) form_data = { "name": "test_snippet", "slug": "test_snippet", "html": "

Test Title

", } - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) # Clean and save the form form.clean() - form.save() + snippet = form.save(commit=True) - # Publish the old created version - snippet = Snippet._base_manager.last() version = snippet.versions.create(created_by=self.get_superuser()) version.publish(user=self.get_superuser()) @@ -116,7 +145,7 @@ def test_snippet_form_versioning_enabled(self): "html": "

Another Test Title

", } - new_form = SnippetForm(new_form_data) + new_form = forms.SnippetForm(new_form_data) self.assertFalse(new_form.is_valid()) @@ -128,6 +157,8 @@ def test_snippet_form_validation_multiple_version_states_in_grouper(self): """ Snippet forms should be valid regardless of the versions, or states which already exist within its grouper. """ + reload(cms_config) + reload(forms) # snippet_to_archive starts as draft snippet_to_archive = SnippetWithVersionFactory() # Then it is published it @@ -154,6 +185,6 @@ def test_snippet_form_validation_multiple_version_states_in_grouper(self): "snippet_grouper": snippet_to_archive.snippet_grouper.id, } - form = SnippetForm(form_data) + form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index ba901439..c3ef7e7d 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,5 +1,12 @@ +import datetime + from cms.api import add_plugin, create_page +from cms.models import PageContent from cms.test_utils.testcases import CMSTestCase +from cms.toolbar.utils import get_object_edit_url + +from djangocms_snippet.models import Snippet, SnippetGrouper +from djangocms_versioning.models import Version from .utils.factories import SnippetWithVersionFactory @@ -9,12 +16,6 @@ class SnippetPluginsTestCase(CMSTestCase): def setUp(self): self.language = "en" self.superuser = self.get_superuser() - self.home = create_page( - title="home", - template="page.html", - language=self.language, - created_by=self.superuser, - ) self.page = create_page( title="help", template="page.html", @@ -22,30 +23,10 @@ def setUp(self): created_by=self.superuser, ) # Publish our page content - self._publish(self.page) - self._publish(self.home) - self.pagecontent = self.page.pagecontent_set.last() - self.home_pagecontent = self.page.pagecontent_set.last() - - def tearDown(self): - self.page.delete() - self.home.delete() - self.superuser.delete() - - def _publish(self, grouper, language=None): - from djangocms_versioning.constants import DRAFT - version = self._get_version(grouper, DRAFT, language) + self.pagecontent = PageContent._base_manager.filter(page=self.page, language=self.language).first() + version = self.pagecontent.versions.first() version.publish(self.superuser) - def _get_version(self, grouper, version_state, language=None): - language = language or self.language - - from djangocms_versioning.models import Version - versions = Version.objects.filter_by_grouper(grouper).filter(state=version_state) - for version in versions: - if hasattr(version.content, 'language') and version.content.language == language: - return version - def test_html_rendering(self): snippet = SnippetWithVersionFactory( name="plugin_snippet", @@ -62,10 +43,11 @@ def test_html_rendering(self): snippet.versions.last().publish(user=self.get_superuser()) request_url = self.page.get_absolute_url("en") + result_snippet = plugin.snippet_grouper.snippet(True) - self.assertEqual(plugin.snippet.name, "plugin_snippet") - self.assertEqual(plugin.snippet.html, "

Hello World

") - self.assertEqual(plugin.snippet.slug, "plugin_snippet") + self.assertEqual(result_snippet.name, "plugin_snippet") + self.assertEqual(result_snippet.html, "

Hello World

") + self.assertEqual(result_snippet.slug, "plugin_snippet") with self.login_user_context(self.superuser): response = self.client.get(request_url) @@ -111,9 +93,9 @@ def test_template_rendering(self): self.language, snippet_grouper=snippet_grouper, ) - - self.assertEqual(plugin.snippet.name, "plugin_snippet") - self.assertEqual(plugin.snippet.slug, "plugin_snippet") + result_snippet = plugin.snippet_grouper.snippet(True) + self.assertEqual(result_snippet.name, "plugin_snippet") + self.assertEqual(result_snippet.slug, "plugin_snippet") with self.login_user_context(self.superuser): response = self.client.get(request_url) @@ -144,3 +126,84 @@ def test_failing_template_rendering(self): response = self.client.get(request_url) self.assertContains(response, "Template some_template does not exist") + + +class SnippetPluginVersioningRenderTestCase(CMSTestCase): + def setUp(self): + self.language = "en" + self.superuser = self.get_superuser() + snippet_grouper = SnippetGrouper.objects.create() + # Create a draft snippet, to be published later + self.snippet = Snippet.objects.create( + name="plugin_snippet", + html="

live content

", + slug="plugin_snippet", + snippet_grouper=snippet_grouper, + ) + + # Publish the snippet + snippet_version = Version.objects.create( + content=self.snippet, + created_by=self.superuser, + created=datetime.datetime.now() + ) + snippet_version.publish(user=self.superuser) + # Copy the snippet to create a draft + draft_user = self.get_staff_page_user() + draft_snippet_version = snippet_version.copy(draft_user) + self.draft_snippet = draft_snippet_version.content + self.draft_snippet.html = "

draft content

" + self.draft_snippet.save() + + # Create a page + self.page = create_page( + title="help", + template="page.html", + language=self.language, + created_by=self.superuser, + ) + # Publish its page content + self.pagecontent = PageContent._base_manager.filter(page=self.page, language=self.language).first() + self.pagecontent_version = self.pagecontent.versions.first() + self.pagecontent_version.publish(self.superuser) + + # Copy our published pagecontent to make a draft + draft_pagecontent_version = self.pagecontent_version.copy(self.superuser) + self.draft_pagecontent = draft_pagecontent_version.content + + # Add plugin to our published page! + add_plugin( + self.pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=self.snippet.snippet_grouper, + ) + # Add plugin to our draft page + add_plugin( + self.draft_pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=self.draft_snippet.snippet_grouper, + ) + + def test_correct_versioning_state_published_snippet_and_page(self): + """ + If a page is published, the published snippet should be rendered + """ + # Request for published page + request_url = self.page.get_absolute_url(self.language) + with self.login_user_context(self.superuser): + response = self.client.get(request_url) + + self.assertContains(response, "

live content

") + + def test_correct_versioning_state_draft_snippet_and_page(self): + """ + If we have a draft, the draft snippet should be rendered. + """ + # Request for draft page + request_url = get_object_edit_url(self.draft_pagecontent, "en") + with self.login_user_context(self.superuser): + response = self.client.get(request_url) + + self.assertContains(response, "

draft content

") diff --git a/tests/utils/factories.py b/tests/utils/factories.py index 9d6210c6..af8d18ce 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -6,8 +6,11 @@ from cms.models import Placeholder import factory +from factory.fuzzy import ( + FuzzyInteger, + FuzzyText, +) from djangocms_versioning.models import Version -from factory.fuzzy import FuzzyInteger, FuzzyText from djangocms_snippet.models import Snippet, SnippetGrouper, SnippetPtr From 5a3323c38076ca573ee2f4ee63fe3887e6e01188 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Thu, 21 Oct 2021 14:31:05 +0100 Subject: [PATCH 07/66] fix: Adding valid HTML to the add snippet form throws an error (#82) --- djangocms_snippet/forms.py | 4 ++-- tests/test_admin.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_forms.py | 8 ++++---- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 55b5f993..64337d95 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -58,8 +58,8 @@ def clean(self): def save(self, **kwargs): commit = kwargs.get("commit", True) snippet = super().save(commit=False) + if not hasattr(snippet, "snippet_grouper"): + snippet.snippet_grouper = SnippetGrouper.objects.create() if commit: - if not hasattr(snippet, "snippet_grouper"): - snippet.snippet_grouper = SnippetGrouper.objects.create() snippet.save() return snippet diff --git a/tests/test_admin.py b/tests/test_admin.py index e1ea1563..69fe4d50 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,10 +1,13 @@ from importlib import reload from django.contrib import admin +from django.shortcuts import reverse from django.test import RequestFactory, override_settings from cms.test_utils.testcases import CMSTestCase +from djangocms_versioning.models import Version + from djangocms_snippet import admin as snippet_admin from djangocms_snippet import cms_config from djangocms_snippet.forms import SnippetForm @@ -30,6 +33,7 @@ def test_admin_list_display_without_versioning(self): admin.site.unregister(Snippet) reload(cms_config) reload(snippet_admin) + # This has to be declared again, since it will now be constructed without the versioning extension self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) list_display = self.snippet_admin.get_list_display(self.snippet_admin_request) @@ -62,3 +66,34 @@ def test_admin_uses_form(self): ensure the admin uses this. """ self.assertEqual(self.snippet_admin.form, SnippetForm) + + +class SnippetAdminFormTestCase(CMSTestCase): + def setUp(self): + self.add_url = reverse("admin:djangocms_snippet_snippet_add") + self.changelist_url = reverse("admin:djangocms_snippet_snippet_changelist") + self.superuser = self.get_superuser() + self.snippet_grouper = SnippetGrouper.objects.create() + self.snippet = Snippet.objects.create( + name="Test Snippet", + slug="test-snippet", + html="

This is a test

", + snippet_grouper=self.snippet_grouper, + ) + self.snippet_version = Version.objects.create(content=self.snippet, created_by=self.superuser) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_admin_form_save_method(self): + with self.login_user_context(self.superuser): + response = self.client.post( + self.add_url, + { + "name": "Test Snippet 2", + "html": "

Test Save Snippet

", + "slug": "test-snippet-2", + }) + self.assertRedirects(response, self.changelist_url) + + # We should have 2 groupers and snippets, due to the creation of the others in setUp + self.assertEqual(Snippet._base_manager.count(), 2) + self.assertEqual(SnippetGrouper._base_manager.count(), 2) diff --git a/tests/test_forms.py b/tests/test_forms.py index a1b806fb..399db3d0 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -60,7 +60,7 @@ def test_snippet_form_creates_grouper_with_versioning(self): @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) def test_snippet_form_doesnt_create_grouper_or_snippet_with_no_commit(self): """ - With versioning enabled, but commit=False, models should not be created + With versioning enabled, but no commit flag, models should still be created """ reload(cms_config) reload(forms) @@ -74,10 +74,10 @@ def test_snippet_form_doesnt_create_grouper_or_snippet_with_no_commit(self): self.assertTrue(form.is_valid()) form.clean() - form.save(commit=False) + form.save() - self.assertEqual(SnippetGrouper.objects.count(), 0) - self.assertEqual(Snippet._base_manager.count(), 0) + self.assertEqual(SnippetGrouper.objects.count(), 1) + self.assertEqual(Snippet._base_manager.count(), 1) @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) def test_snippet_form_adds_to_existing_grouper_with_versioning(self): From 98ddf0c55d23793f3cc1136fc8af63ba315c7416 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Fri, 22 Oct 2021 13:40:33 +0100 Subject: [PATCH 08/66] fix: form initialisation error in read-only mode (#83) --- djangocms_snippet/forms.py | 5 +++-- tests/test_admin.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 64337d95..e503b9d5 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -28,8 +28,9 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["snippet_grouper"].required = False - self.fields["snippet_grouper"].widget = forms.HiddenInput() + if self.fields.get("snippet_grouper"): + self.fields["snippet_grouper"].required = False + self.fields["snippet_grouper"].widget = forms.HiddenInput() def clean(self): data = super().clean() diff --git a/tests/test_admin.py b/tests/test_admin.py index 69fe4d50..54d37120 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -97,3 +97,19 @@ def test_admin_form_save_method(self): # We should have 2 groupers and snippets, due to the creation of the others in setUp self.assertEqual(Snippet._base_manager.count(), 2) self.assertEqual(SnippetGrouper._base_manager.count(), 2) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_admin_form_edit_when_locked(self): + """ + When a form is initialised in read-only mode, it should not require self.fields to be populated, and + should return a read-only form. + """ + self.snippet_version.publish(user=self.superuser) + with self.login_user_context(self.superuser): + edit_url = reverse("admin:djangocms_snippet_snippet_change", args=(self.snippet.id,),) + response = self.client.get(edit_url) + + # Check that we are loading in readonly mode + self.assertContains(response, '
Test Snippet
') + # We should have the same number of snippets as before + self.assertEqual(Snippet.objects.count(), 1) From aa450b38ba69ff96b30d52fef80f3f380e5e0f4a Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Tue, 26 Oct 2021 16:07:29 +0100 Subject: [PATCH 09/66] fix: History URL button broken link on the Snippets change form (#84) --- djangocms_snippet/admin.py | 36 +++++++++------------ tests/test_admin.py | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 21 deletions(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index ba5c9f2f..fc7b6e0b 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -4,6 +4,8 @@ from django.db import models from django.forms import Textarea +from cms.utils.permissions import get_model_permission_codename + from .cms_config import SnippetCMSAppConfig from .forms import SnippetForm from .models import Snippet @@ -47,32 +49,24 @@ class Meta: def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - url( - r"^$", - self.admin_site.admin_view(self.changelist_view), - name="{}_{}_changelist".format(*info), - ), - url( - r"^(?P\d+)/$", - self.admin_site.admin_view(self.changelist_view), - name="{}_{}_list".format(*info), - ), - url( - r"^add/$", - self.admin_site.admin_view(self.add_view), - name="{}_{}_add".format(*info), - ), - url( - r"^(?P\d+)/change/$", - self.admin_site.admin_view(self.change_view), - name="{}_{}_change".format(*info), - ), url( r"^(?P\d+)/preview/$", self.admin_site.admin_view(SnippetPreviewView.as_view()), name="{}_{}_preview".format(*info), ), - ] + ] + super().get_urls() + + def has_delete_permission(self, request, obj=None): + """ + When versioning is enabled, delete option is not available. + If versioning is disabled, it may be possible to delete, as long as a user also has add permissions, and they + are not in use. + """ + if obj and not djangocms_versioning_enabled: + return request.user.has_perm( + get_model_permission_codename(self.model, 'add'), + ) + return False admin.site.register(Snippet, SnippetAdmin) diff --git a/tests/test_admin.py b/tests/test_admin.py index 54d37120..f29b29c7 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -16,14 +16,18 @@ class SnippetAdminTestCase(CMSTestCase): def setUp(self): + self.superuser = self.get_superuser() self.snippet = Snippet.objects.create( name="Test Snippet", slug="test-snippet", html="

This is a test

", snippet_grouper=SnippetGrouper.objects.create(), ) + self.snippet_version = Version.objects.create(content=self.snippet, created_by=self.superuser) self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) self.snippet_admin_request = RequestFactory().get("/admin/djangocms_snippet") + self.edit_url = reverse("admin:djangocms_snippet_snippet_change", args=(self.snippet.id,),) + self.delete_url = reverse("admin:djangocms_snippet_snippet_delete", args=(self.snippet.id,),) @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) def test_admin_list_display_without_versioning(self): @@ -67,6 +71,68 @@ def test_admin_uses_form(self): """ self.assertEqual(self.snippet_admin.form, SnippetForm) + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_admin_delete_button_disabled_versioning_enabled(self): + """ + If versioning is enabled, the delete button should not be rendered on the change form + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.superuser): + response = self.client.get(self.edit_url) + + self.assertNotContains( + response, 'Delete' + ) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) + def test_admin_delete_button_available_versioning_disabled(self): + """ + If versioning is disabled, the delete button should be rendered on the change form + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.superuser): + response = self.client.get(self.edit_url) + + self.assertContains( + response, 'Delete' + ) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_admin_delete_endpoint_inaccessible_versioning_enabled(self): + """ + If versioning is enabled, the delete endpoint should not be accessible. + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.superuser): + response = self.client.post(self.delete_url) + + # The delete endpoint should return a 403 forbidden if we try to access it with versioning enabled + self.assertEqual(response.status_code, 403) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) + def test_admin_delete_endpoint_accessible_versioning_disabled(self): + """ + If versioning is disabled, the delete endpoint should be accessible. + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.superuser): + response = self.client.post(self.delete_url) + + # The delete endpoint should return a 200 success if we try to access it with versioning disabled + self.assertEqual(response.status_code, 200) + class SnippetAdminFormTestCase(CMSTestCase): def setUp(self): From 7d752e69a6dc0bf0ebab58833a1f5621e962757d Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Mon, 29 Nov 2021 17:44:54 +0000 Subject: [PATCH 10/66] fix: Draft snippet render error due to draft snippets (#86) --- CHANGELOG.rst | 7 ++ djangocms_snippet/cms_plugins.py | 5 + ...010_cms4_grouper_version_data_migration.py | 18 ++-- .../migrations/0012_auto_20210915_0721.py | 18 +++- .../migrations/0013_auto_20210915_0751.py | 23 ----- .../migrations/0014_auto_20211019_0522.py | 17 ---- tests/test_plugins.py | 91 ++++++++++++++++++- 7 files changed, 126 insertions(+), 53 deletions(-) delete mode 100644 djangocms_snippet/migrations/0013_auto_20210915_0751.py delete mode 100644 djangocms_snippet/migrations/0014_auto_20211019_0522.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0112ad5b..06cd54cf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,13 @@ Changelog ========= +Unreleased +================== +* fix: Error when rendering a Draft Snippet plugin on a Published page +* fix: Publish snippets by default as they were already in that state pre-versioning and cleanup unnecessary migration files before release! +* feat: djangocms-versioning support added, including model restructure and configuration +* feat: django-cms v4.0.x support added + 3.0.0 (2020-09-02) ================== diff --git a/djangocms_snippet/cms_plugins.py b/djangocms_snippet/cms_plugins.py index 63e691fc..a6802626 100644 --- a/djangocms_snippet/cms_plugins.py +++ b/djangocms_snippet/cms_plugins.py @@ -24,6 +24,11 @@ class SnippetPlugin(CMSPluginBase): def render(self, context, instance, placeholder): snippet = instance.snippet_grouper.snippet(show_editable=show_draft_content(context["request"])) + + # Handle the potential for no snippet to be found i.e. Draft + if not snippet: + return context + try: if snippet.template: context = context.flatten() diff --git a/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py b/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py index dee1d352..17e9ecab 100644 --- a/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py +++ b/djangocms_snippet/migrations/0010_cms4_grouper_version_data_migration.py @@ -1,4 +1,5 @@ from django.apps import apps as global_apps +from django.conf import settings from django.contrib.contenttypes.management import create_contenttypes from django.db import migrations @@ -9,7 +10,7 @@ try: - from djangocms_versioning.constants import DRAFT + from djangocms_versioning.constants import DRAFT, PUBLISHED djangocms_versioning_installed = True except ImportError: @@ -24,25 +25,28 @@ def cms4_grouper_version_migration(apps, schema_editor): ContentType = apps.get_model('contenttypes', 'ContentType') Snippet = apps.get_model('djangocms_snippet', 'Snippet') SnippetGrouper = apps.get_model('djangocms_snippet', 'SnippetGrouper') - User = apps.get_model('auth', 'User') + User = apps.get_model(*settings.AUTH_USER_MODEL.split('.')) snippet_contenttype = ContentType.objects.get(app_label='djangocms_snippet', model='snippet') snippet_queryset = Snippet.objects.all() + # Get a migration user to create a version. + if djangocms_versioning_config_enabled and djangocms_versioning_installed and len(snippet_queryset): + Version = apps.get_model('djangocms_versioning', 'Version') + + migration_user = User.objects.get(id=DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID) + for snippet in snippet_queryset: grouper = SnippetGrouper.objects.create() snippet.snippet_grouper = grouper snippet.save() - # Get a migration user. - migration_user = User.objects.get(id=DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID) - # Create initial Snippet Versions if versioning is enabled and installed. + # Publish the snippet because all snippets were assumed published before if djangocms_versioning_config_enabled and djangocms_versioning_installed: - Version = apps.get_model('djangocms_versioning', 'Version') Version.objects.create( created_by=migration_user, - state=DRAFT, + state=PUBLISHED, number=1, object_id=snippet.pk, content_type=snippet_contenttype, diff --git a/djangocms_snippet/migrations/0012_auto_20210915_0721.py b/djangocms_snippet/migrations/0012_auto_20210915_0721.py index f57f106d..bf228431 100644 --- a/djangocms_snippet/migrations/0012_auto_20210915_0721.py +++ b/djangocms_snippet/migrations/0012_auto_20210915_0721.py @@ -17,12 +17,26 @@ class Migration(migrations.Migration): ), migrations.AlterField( model_name='snippet', - name='snippet_grouper', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + name='name', + field=models.CharField(max_length=255, verbose_name='Name'), + ), + migrations.AlterField( + model_name='snippet', + name='slug', + field=models.SlugField(default='', max_length=255, verbose_name='Slug'), + ), + migrations.AlterModelOptions( + name='snippet', + options={'verbose_name': 'Snippet', 'verbose_name_plural': 'Snippets'}, ), migrations.AlterField( model_name='snippetptr', name='snippet_grouper', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='djangocms_snippet.SnippetGrouper'), ), + migrations.AlterField( + model_name='snippet', + name='snippet_grouper', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='djangocms_snippet.SnippetGrouper'), + ), ] diff --git a/djangocms_snippet/migrations/0013_auto_20210915_0751.py b/djangocms_snippet/migrations/0013_auto_20210915_0751.py deleted file mode 100644 index de813001..00000000 --- a/djangocms_snippet/migrations/0013_auto_20210915_0751.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.24 on 2021-09-15 07:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('djangocms_snippet', '0012_auto_20210915_0721'), - ] - - operations = [ - migrations.AlterField( - model_name='snippet', - name='name', - field=models.CharField(max_length=255, verbose_name='Name'), - ), - migrations.AlterField( - model_name='snippet', - name='slug', - field=models.SlugField(default='', max_length=255, verbose_name='Slug'), - ), - ] diff --git a/djangocms_snippet/migrations/0014_auto_20211019_0522.py b/djangocms_snippet/migrations/0014_auto_20211019_0522.py deleted file mode 100644 index 1f70ec08..00000000 --- a/djangocms_snippet/migrations/0014_auto_20211019_0522.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.24 on 2021-10-19 10:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('djangocms_snippet', '0013_auto_20210915_0751'), - ] - - operations = [ - migrations.AlterModelOptions( - name='snippet', - options={'verbose_name': 'Snippet', 'verbose_name_plural': 'Snippets'}, - ), - ] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c3ef7e7d..540922ed 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -171,6 +171,10 @@ def setUp(self): draft_pagecontent_version = self.pagecontent_version.copy(self.superuser) self.draft_pagecontent = draft_pagecontent_version.content + def test_correct_versioning_state_published_snippet_and_page(self): + """ + If a page is published, the published snippet should be rendered + """ # Add plugin to our published page! add_plugin( self.pagecontent.placeholders.get(slot="content"), @@ -186,24 +190,103 @@ def setUp(self): snippet_grouper=self.draft_snippet.snippet_grouper, ) - def test_correct_versioning_state_published_snippet_and_page(self): - """ - If a page is published, the published snippet should be rendered - """ # Request for published page request_url = self.page.get_absolute_url(self.language) with self.login_user_context(self.superuser): response = self.client.get(request_url) self.assertContains(response, "

live content

") + self.assertNotIn("draft content", str(response.content)) def test_correct_versioning_state_draft_snippet_and_page(self): """ If we have a draft, the draft snippet should be rendered. """ + # Add plugin to our published page! + add_plugin( + self.pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=self.snippet.snippet_grouper, + ) + # Add plugin to our draft page + add_plugin( + self.draft_pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=self.draft_snippet.snippet_grouper, + ) + # Request for draft page request_url = get_object_edit_url(self.draft_pagecontent, "en") with self.login_user_context(self.superuser): response = self.client.get(request_url) self.assertContains(response, "

draft content

") + self.assertNotIn("live content", str(response.content)) + + def test_draft_snippet_and_page_live_url_rendering(self): + """ + If a page is published with a draft snippet created + nothing should be rendered! + """ + snippet_grouper = SnippetGrouper.objects.create() + snippet = Snippet.objects.create( + name="plugin_snippet", + html="

Draft snippet

", + slug="plugin_snippet", + snippet_grouper=snippet_grouper, + ) + Version.objects.create( + content=snippet, + created_by=self.superuser, + created=datetime.datetime.now() + ) + + add_plugin( + self.pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=snippet_grouper, + ) + + request_url = self.page.get_absolute_url(self.language) + with self.login_user_context(self.superuser): + response = self.client.get(request_url) + + self.assertEqual(response.status_code, 200) + self.assertNotIn("Draft snippet", str(response.content)) + self.assertNotIn("Published snippet", str(response.content)) + + def test_published_snippet_and_page_live_url_rendering(self): + """ + If a page is published with a published snippet + created the snippet should be rendered! + """ + snippet_grouper = SnippetGrouper.objects.create() + snippet = Snippet.objects.create( + name="plugin_snippet", + html="

Published snippet

", + slug="plugin_snippet", + snippet_grouper=snippet_grouper, + ) + snippet_version = Version.objects.create( + content=snippet, + created_by=self.superuser, + created=datetime.datetime.now() + ) + snippet_version.publish(user=self.superuser) + + add_plugin( + self.pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=snippet_grouper, + ) + + request_url = self.page.get_absolute_url(self.language) + with self.login_user_context(self.superuser): + response = self.client.get(request_url) + + self.assertContains(response, "

Published snippet

") + self.assertNotIn("Draft snippet", str(response.content)) From 55abe6730982220fdcbd3785aa20ada4b6ed1821 Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Tue, 30 Nov 2021 15:14:16 +0000 Subject: [PATCH 11/66] feat: Default versioning import user id setting can be set via environment variable (#89) --- CHANGELOG.rst | 1 + README.rst | 1 + aldryn_config.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 06cd54cf..44c86be5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ================== +* feat: Exposed the setting DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID as an environment variable for the Divio addon * fix: Error when rendering a Draft Snippet plugin on a Published page * fix: Publish snippets by default as they were already in that state pre-versioning and cleanup unnecessary migration files before release! * feat: djangocms-versioning support added, including model restructure and configuration diff --git a/README.rst b/README.rst index 6cedd459..4fe1bf5a 100644 --- a/README.rst +++ b/README.rst @@ -82,6 +82,7 @@ please set ``DJANGOCMS_SNIPPET_CACHE`` to ``False`` in your settings:: Migration 0010 requires the use of a user in order to create versions for existing snippets (if djangocms_versioning is installed and enabled), a user can be chosen with the setting ``DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID``, the default is 1. +This setting is also exposed as an Environment variable for Divio projects using the Divio addon. DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID = 2 # Will use user with id: 2 diff --git a/aldryn_config.py b/aldryn_config.py index 3e532b2c..c97e8a56 100644 --- a/aldryn_config.py +++ b/aldryn_config.py @@ -1,3 +1,5 @@ +from functools import partial + from aldryn_client import forms @@ -17,6 +19,18 @@ class Form(forms.BaseForm): ) def to_settings(self, data, settings): + from aldryn_addons.utils import djsenv + + env = partial(djsenv, settings=settings) + + # Get a migration user if the env setting has been added + migration_user_id = env( + 'DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID', + default=False + ) + if migration_user_id: + settings['DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID'] = int(migration_user_id) + if data['editor_theme']: settings['DJANGOCMS_SNIPPET_THEME'] = data['editor_theme'] if data['editor_mode']: From 20e5ca635bd32e0be590957de19b5f1f41325cff Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Tue, 14 Dec 2021 09:58:05 +0000 Subject: [PATCH 12/66] Release 4.0.0.dev1 (#91) --- CHANGELOG.rst | 5 +++++ djangocms_snippet/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 44c86be5..1eae5a0f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,7 +3,12 @@ Changelog ========= Unreleased +========== + + +4.0.0.dev1 (2021-12-14) ================== + * feat: Exposed the setting DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID as an environment variable for the Divio addon * fix: Error when rendering a Draft Snippet plugin on a Published page * fix: Publish snippets by default as they were already in that state pre-versioning and cleanup unnecessary migration files before release! diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 22b8ebf7..446f7c9a 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '3.0.0' +__version__ = '4.0.0.dev1' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From 903be6f665dc01b16a195b128c09ea7823f34f1a Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Wed, 22 Dec 2021 15:51:38 +0000 Subject: [PATCH 13/66] fix: django-cms 4.0.x - Remove breaking django-treebeard pinning issue (#93) --- CHANGELOG.rst | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1eae5a0f..05ddf69c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,10 +4,11 @@ Changelog Unreleased ========== +* fix: Removed tight django-treebeard restriction added when 4.5.0 contained breaking changes. The core CMS and django-treebeard have since been patched to resolve the issue. 4.0.0.dev1 (2021-12-14) -================== +======================= * feat: Exposed the setting DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID as an environment variable for the Divio addon * fix: Error when rendering a Draft Snippet plugin on a Published page diff --git a/setup.py b/setup.py index 566bc2cb..08ee8285 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ REQUIREMENTS = [ 'django-cms', - 'django-treebeard>=4.3,<4.5', + 'django-treebeard>=4.3', ] From b0a0559389cad5ff513be3d5401de4481f27220a Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Thu, 23 Dec 2021 14:31:22 +0000 Subject: [PATCH 14/66] Release 4.0.0.dev2 (#95) --- CHANGELOG.rst | 5 +++++ djangocms_snippet/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05ddf69c..a911a881 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,11 @@ Changelog Unreleased ========== + + +4.0.0.dev2 (2021-12-22) +======================= + * fix: Removed tight django-treebeard restriction added when 4.5.0 contained breaking changes. The core CMS and django-treebeard have since been patched to resolve the issue. diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 446f7c9a..527fb3aa 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.0.dev1' +__version__ = '4.0.0.dev2' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From fdc9f49d3ea2d0fb638b063dcb994e634992d7ad Mon Sep 17 00:00:00 2001 From: Bernard Van Der Vyver Date: Mon, 10 Jan 2022 17:36:27 +0000 Subject: [PATCH 15/66] feat: Removing slug field and hyperlink from list display when versioning is enabled (#98) --- CHANGELOG.rst | 1 + djangocms_snippet/admin.py | 37 ++++++++++++++++++++++++++++++++----- tests/test_admin.py | 32 +++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a911a881..323a7774 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ Changelog Unreleased ========== +* fix: Slug field on list display for admin should only be displayed when versioning is not available 4.0.0.dev2 (2021-12-22) ======================= diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index fc7b6e0b..b3c53a59 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -18,17 +18,15 @@ try: from djangocms_versioning.admin import ExtendedVersionAdminMixin - if djangocms_versioning_enabled: snippet_admin_classes.insert(0, ExtendedVersionAdminMixin) except ImportError: - pass + djangocms_versioning_enabled = False class SnippetAdmin(*snippet_admin_classes): - list_display = ('slug', 'name') - search_fields = ['slug', 'name'] - prepopulated_fields = {'slug': ('name',)} + list_display = ('name',) + search_fields = ['name'] change_form_template = 'djangocms_snippet/admin/change_form.html' text_area_attrs = { 'rows': 20, @@ -46,6 +44,35 @@ class SnippetAdmin(*snippet_admin_classes): class Meta: model = Snippet + def get_list_display(self, request): + list_display = super().get_list_display(request) + list_display = list(list_display) + + if not djangocms_versioning_enabled: + list_display.insert(0, 'slug') + + list_display = tuple(list_display) + return list_display + + def get_search_fields(self, request): + search_fields = super().get_search_fields(request) + if not djangocms_versioning_enabled: + search_fields.append('slug') + return search_fields + + def get_prepopulated_fields(self, obj, request): + prepopulated_fields = super().get_prepopulated_fields(request) + if not djangocms_versioning_enabled: + prepopulated_fields = {'slug': ('name',)} + return prepopulated_fields + + def get_list_display_links(self, request, list_display): + if not djangocms_versioning_enabled: + return list(list_display)[:1] + else: + self.list_display_links = (None,) + return self.list_display_links + def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ diff --git a/tests/test_admin.py b/tests/test_admin.py index f29b29c7..ed121022 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -59,7 +59,7 @@ def test_admin_list_display_with_versioning(self): self.snippet_admin.__class__.__bases__, (ExtendedVersionAdminMixin, admin.ModelAdmin) ) self.assertEqual( - list_display[:-1], ('slug', 'name', 'get_author', 'get_modified_date', 'get_versioning_state') + list_display[:-1], ('name', 'get_author', 'get_modified_date', 'get_versioning_state') ) self.assertEqual(list_display[-1].short_description, 'actions') self.assertIn("function ExtendedVersionAdminMixin._list_actions", list_display[-1].__str__()) @@ -179,3 +179,33 @@ def test_admin_form_edit_when_locked(self): self.assertContains(response, '
Test Snippet
') # We should have the same number of snippets as before self.assertEqual(Snippet.objects.count(), 1) + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) + def test_slug_colomn_should_hyperlinked_with_versioning_disabled(self): + """ + Slug column should be visible and hyperlinked when versioning is disabled + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.changelist_url) + self.assertContains(response, 'test-snippet') + + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=True) + def test_name_colomn_should_not_be_hyperlinked_with_versioning_enabled(self): + """ + Name column should be visible and not hyperlinked when versioning is enabled. + Slug column should not be visible when versioning is enabled. + """ + admin.site.unregister(Snippet) + reload(cms_config) + reload(snippet_admin) + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.changelist_url) + self.assertContains(response, 'Test Snippet') + self.assertNotContains(response, 'test-snippet') From 90b117198437c912ade39d79081a6e05619ea686 Mon Sep 17 00:00:00 2001 From: Bernard Van Der Vyver Date: Mon, 10 Jan 2022 17:53:42 +0000 Subject: [PATCH 16/66] fix: Snippet plugin showing ID instead of name(#100) --- CHANGELOG.rst | 1 + djangocms_snippet/models.py | 4 ++++ tests/test_plugins.py | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 323a7774..b2adf37d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ Changelog Unreleased ========== +* fix: Snippet plugin added to a page now displays name instead of ID * fix: Slug field on list display for admin should only be displayed when versioning is not available 4.0.0.dev2 (2021-12-22) diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index 177caddc..b7baea5f 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -106,6 +106,10 @@ class SnippetPtr(CMSPlugin): search_fields = ['snippet__html'] if SEARCH_ENABLED else [] + def get_short_description(self): + snippet_label = SnippetGrouper.objects.filter(pk=self.snippet_grouper.pk).first() + return snippet_label + class Meta: verbose_name = _('Snippet Ptr') verbose_name_plural = _('Snippet Ptrs') diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 540922ed..d6ab5182 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -3,7 +3,7 @@ from cms.api import add_plugin, create_page from cms.models import PageContent from cms.test_utils.testcases import CMSTestCase -from cms.toolbar.utils import get_object_edit_url +from cms.toolbar.utils import get_object_edit_url, get_object_structure_url from djangocms_snippet.models import Snippet, SnippetGrouper from djangocms_versioning.models import Version @@ -290,3 +290,21 @@ def test_published_snippet_and_page_live_url_rendering(self): self.assertContains(response, "

Published snippet

") self.assertNotIn("Draft snippet", str(response.content)) + + def test_correct_name_is_displayed_for_snippet_component_on_page(self): + """ + If a component is added to the page, it should show the snippet name and not ID + """ + add_plugin( + self.draft_pagecontent.placeholders.get(slot="content"), + "SnippetPlugin", + self.language, + snippet_grouper=self.draft_snippet.snippet_grouper, + ) + + # Request structure endpoint on page + request_url = get_object_structure_url(self.draft_pagecontent, "en") + with self.login_user_context(self.superuser): + response = self.client.get(request_url) + + self.assertContains(response, "Snippet plugin_snippet") From a31c8e35d43f038b4268bb522623a7479d675996 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Tue, 11 Jan 2022 10:24:03 +0000 Subject: [PATCH 17/66] Fix: Compare view removed unnecessary template logic (#101) --- CHANGELOG.rst | 1 + .../djangocms_snippet/admin/preview.html | 17 ----------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b2adf37d..cd533c84 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ========== +* fix: Removed unused contents within templates, reducing the clutter within version compare views. * fix: Snippet plugin added to a page now displays name instead of ID * fix: Slug field on list display for admin should only be displayed when versioning is not available diff --git a/djangocms_snippet/templates/djangocms_snippet/admin/preview.html b/djangocms_snippet/templates/djangocms_snippet/admin/preview.html index 723b71b5..d737fc80 100644 --- a/djangocms_snippet/templates/djangocms_snippet/admin/preview.html +++ b/djangocms_snippet/templates/djangocms_snippet/admin/preview.html @@ -1,18 +1 @@ -{% extends "admin/base_site.html" %} -{% load static %} {{ snippet.html|safe|escape }} - -{% block extrastyle %} - {{ block.super }} - -{% endblock %} - -{% block coltype %}flex{% endblock %} - -{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-list{% endblock %} - -{% block content %} - {% autoescape off %} - {{ snippet.html }} - {% endautoescape %} -{% endblock %} From 60746af613fdbb4053cd28abde4da8bc8de1cb3d Mon Sep 17 00:00:00 2001 From: Bernard Van Der Vyver Date: Tue, 11 Jan 2022 11:20:40 +0000 Subject: [PATCH 18/66] Release 4.0.0.dev3 (#103) --- CHANGELOG.rst | 7 ++++++- djangocms_snippet/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cd533c84..6cd06bdc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,10 +4,15 @@ Changelog Unreleased ========== -* fix: Removed unused contents within templates, reducing the clutter within version compare views. + + +4.0.0.dev3 (2022-01-11) +======================= * fix: Snippet plugin added to a page now displays name instead of ID * fix: Slug field on list display for admin should only be displayed when versioning is not available +* fix: Removed unused contents within templates, reducing the clutter within version compare views. Previously this was causing a lot of junk to be included in the version comparison, this will now be reduced. + 4.0.0.dev2 (2021-12-22) ======================= diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 527fb3aa..7cc359c3 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.0.dev2' +__version__ = '4.0.0.dev3' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From 3c5af573f77744ec2092bf7566c99ffd28eda9c4 Mon Sep 17 00:00:00 2001 From: Bernard Van Der Vyver Date: Thu, 3 Feb 2022 14:43:31 +0000 Subject: [PATCH 19/66] feat: Preview icon renders content in read only (#102) --- CHANGELOG.rst | 1 + djangocms_snippet/admin.py | 80 ++++++++++++++++++++++++++++++++++---- djangocms_snippet/views.py | 26 ------------- tests/test_admin.py | 18 +++++++++ tests/test_views.py | 38 ------------------ 5 files changed, 92 insertions(+), 71 deletions(-) delete mode 100644 djangocms_snippet/views.py delete mode 100644 tests/test_views.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6cd06bdc..334faed5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,7 @@ Changelog Unreleased ========== +* feat: Preview icon renders form in read only mode 4.0.0.dev3 (2022-01-11) ======================= diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index b3c53a59..6769d9ce 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -1,15 +1,19 @@ from django.conf import settings from django.conf.urls import url from django.contrib import admin +from django.contrib.admin import helpers +from django.contrib.admin.exceptions import DisallowedModelAdminToField +from django.contrib.admin.options import IS_POPUP_VAR, TO_FIELD_VAR +from django.contrib.admin.utils import flatten_fieldsets, unquote from django.db import models from django.forms import Textarea +from django.utils.translation import gettext as _ from cms.utils.permissions import get_model_permission_codename from .cms_config import SnippetCMSAppConfig from .forms import SnippetForm from .models import Snippet -from .views import SnippetPreviewView # Use the version mixin if djangocms-versioning is installed and enabled @@ -18,6 +22,7 @@ try: from djangocms_versioning.admin import ExtendedVersionAdminMixin + if djangocms_versioning_enabled: snippet_admin_classes.insert(0, ExtendedVersionAdminMixin) except ImportError: @@ -73,15 +78,76 @@ def get_list_display_links(self, request, list_display): self.list_display_links = (None,) return self.list_display_links + def preview_view(self, request, snippet_id=None, form_url='', extra_context=None): + """ + Custom preview endpoint to display a change form in read only mode + Solution based on django changeform view implementation + https://github.com/django/django/blob/4b8e9492d9003ca357a4402f831112dd72efd2f8/django/contrib/admin/options.py#L1553 + """ + to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) + + if to_field and not self.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField("The field %s cannot be referenced." % to_field) + + model = self.model + opts = model._meta + + obj = self.get_object(request, unquote(snippet_id), to_field) + + if obj is None: + return self._get_obj_does_not_exist_redirect(request, opts, snippet_id) + + fieldsets = self.get_fieldsets(request, obj) + ModelForm = self.get_form( + request, obj, change=False, fields=flatten_fieldsets(fieldsets) + ) + form = ModelForm(instance=obj) + formsets, inline_instances = self._create_formsets(request, obj, change=True) + + readonly_fields = flatten_fieldsets(fieldsets) + + adminForm = helpers.AdminForm( + form, + list(fieldsets), + # Clear prepopulated fields on a view-only form to avoid a crash. + {}, + readonly_fields, + model_admin=self) + media = self.media + adminForm.media + + inline_formsets = self.get_inline_formsets(request, formsets, inline_instances, obj) + for inline_formset in inline_formsets: + media = media + inline_formset.media + + title = _('View %s') + context = { + **self.admin_site.each_context(request), + 'title': title % opts.verbose_name, + 'subtitle': str(obj) if obj else None, + 'adminform': adminForm, + 'object_id': snippet_id, + 'original': obj, + 'is_popup': IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET, + 'to_field': to_field, + 'media': media, + 'inline_admin_formsets': inline_formsets, + 'errors': [], + 'preserved_filters': self.get_preserved_filters(request), + } + + context.update(extra_context or {}) + + return self.render_change_form(request, context, add=False, change=False, obj=obj, form_url=form_url) + def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - url( - r"^(?P\d+)/preview/$", - self.admin_site.admin_view(SnippetPreviewView.as_view()), - name="{}_{}_preview".format(*info), - ), - ] + super().get_urls() + url( + r"^(?P\d+)/preview/$", + self.admin_site.admin_view(self.preview_view), + name="{}_{}_preview".format(*info), + ), + ] + super().get_urls() def has_delete_permission(self, request, obj=None): """ diff --git a/djangocms_snippet/views.py b/djangocms_snippet/views.py deleted file mode 100644 index 26edbe5f..00000000 --- a/djangocms_snippet/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.http import Http404 -from django.views.generic import TemplateView - -from djangocms_snippet.models import Snippet - - -class SnippetPreviewView(TemplateView): - template_name = "djangocms_snippet/admin/preview.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - snippet_id = kwargs.get("snippet_id", None) - - if not snippet_id: - Http404("snippet_id must be provided.") - - try: - snippet = Snippet._base_manager.get(pk=self.kwargs.get("snippet_id")) - except Snippet.DoesNotExist: - raise Http404 - - context.update({ - "snippet": snippet, - "opts": Snippet._meta - }) - return context diff --git a/tests/test_admin.py b/tests/test_admin.py index ed121022..d03d2358 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -209,3 +209,21 @@ def test_name_colomn_should_not_be_hyperlinked_with_versioning_enabled(self): self.assertContains(response, 'Test Snippet') self.assertNotContains(response, 'test-snippet') + + def test_preview_renders_read_only_fields(self): + """ + Check that the preview endpoint is rendered in read only mode + """ + self.snippet_version.publish(user=self.superuser) + with self.login_user_context(self.superuser): + edit_url = reverse("admin:djangocms_snippet_snippet_preview", args=(self.snippet.id,),) + response = self.client.get(edit_url) + + # Snippet name + self.assertContains(response, '
Test Snippet
') + # Snippet slug + self.assertContains(response, '
test-snippet
') + # Snippet HTML + self.assertContains(response, '
<h1>This is a test</h1>
') + # Snippet template + self.assertContains(response, '
') diff --git a/tests/test_views.py b/tests/test_views.py deleted file mode 100644 index ce3c7e5a..00000000 --- a/tests/test_views.py +++ /dev/null @@ -1,38 +0,0 @@ -from cms.test_utils.testcases import CMSTestCase -from cms.utils.urlutils import admin_reverse - -from .utils.factories import SnippetWithVersionFactory - - -class PreviewViewTestCase(CMSTestCase): - def setUp(self): - self.snippet = SnippetWithVersionFactory(html="

Test Title


Test paragraph

") - self.user = self.get_superuser() - - def test_preview_renders_html(self): - """ - Check that our snippet HTML is rendered, unescaped, on the page - """ - preview_url = admin_reverse( - "djangocms_snippet_snippet_preview", - kwargs={"snippet_id": self.snippet.id}, - ) - with self.login_user_context(self.user): - response = self.client.get(preview_url) - - self.assertEqual(self.snippet.html, "

Test Title


Test paragraph

") - self.assertEqual(response.status_code, 200) - self.assertContains(response, "

Test Title


Test paragraph

") - - def test_preview_raises_404_no_snippet(self): - """ - With no Snippet to preview, a 404 will be raised - """ - preview_url = admin_reverse( - "djangocms_snippet_snippet_preview", - kwargs={"snippet_id": 999}, # Non existent PK! - ) - with self.login_user_context(self.user): - response = self.client.get(preview_url) - - self.assertEqual(response.status_code, 404) From c25b365cc7cc0400c2f3a02bbd99d2eaa5568d18 Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Thu, 3 Feb 2022 15:18:45 +0000 Subject: [PATCH 20/66] Release 4.0.0.dev4 (#109) --- CHANGELOG.rst | 5 +++++ djangocms_snippet/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 334faed5..fc463f9b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,8 +5,13 @@ Changelog Unreleased ========== + +4.0.0.dev4 (2022-02-03) +======================= + * feat: Preview icon renders form in read only mode + 4.0.0.dev3 (2022-01-11) ======================= diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 7cc359c3..471a3ac3 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.0.dev3' +__version__ = '4.0.0.dev4' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From 59c22afaa5c60100cb6354d3cf371a7ea75bf5f9 Mon Sep 17 00:00:00 2001 From: Adam Murray Date: Mon, 14 Feb 2022 10:22:39 +0000 Subject: [PATCH 21/66] fix: Added test coverage to admin preview view (#96) --- CHANGELOG.rst | 2 +- tests/test_views.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/test_views.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc463f9b..7172ddb3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog Unreleased ========== - +* fix: Added test coverage to admin preview view 4.0.0.dev4 (2022-02-03) ======================= diff --git a/tests/test_views.py b/tests/test_views.py new file mode 100644 index 00000000..a5879542 --- /dev/null +++ b/tests/test_views.py @@ -0,0 +1,40 @@ +from cms.test_utils.testcases import CMSTestCase +from cms.utils.urlutils import admin_reverse + +from .utils.factories import SnippetWithVersionFactory + + +class PreviewViewTestCase(CMSTestCase): + def setUp(self): + self.snippet = SnippetWithVersionFactory(html="

Test Title


Test paragraph

") + self.user = self.get_superuser() + + def test_preview_renders_html(self): + """ + Check that our snippet HTML is rendered, unescaped, on the page + """ + preview_url = admin_reverse( + "djangocms_snippet_snippet_preview", + kwargs={"snippet_id": self.snippet.id}, + ) + with self.login_user_context(self.user): + response = self.client.get(preview_url) + + self.assertEqual(self.snippet.html, "

Test Title


Test paragraph

") + self.assertEqual(response.status_code, 200) + # Removing html escaping, means the content is rendered including the tags on the page, but also means that + # the response will contain character entity references. + self.assertContains(response, "<h1>Test Title</h1><br><p>Test paragraph</p>") + + def test_preview_raises_302_no_snippet(self): + """ + With no Snippet to preview, a 302 will be raised and the user will be redirected to the admin + """ + preview_url = admin_reverse( + "djangocms_snippet_snippet_preview", + kwargs={"snippet_id": 999}, # Non existent PK! + ) + with self.login_user_context(self.user): + response = self.client.get(preview_url) + + self.assertEqual(response.status_code, 302) From c8562f0e3efc0c6e0f7600c8f4e3ebd808e4e4bb Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Tue, 10 May 2022 14:12:44 +0100 Subject: [PATCH 22/66] port-feat: pre-commit config added from the v3 workstream (#117) Changes partially taken from this change: https://github.com/django-cms/djangocms-snippet/commit/a56091c7358cb712e6bd0c4186588f7ce0811530 Requires additional work when the django-cms 4.0 workstream is official, the version bump code has been ignored because it would generate official releases only: https://github.com/django-cms/djangocms-snippet/issues/116 --- .pre-commit-config.yaml | 35 +++++++++++++++++++++++++++++++++++ CHANGELOG.rst | 1 + djangocms_snippet/admin.py | 6 +++--- tests/test_plugins.py | 3 ++- tests/utils/factories.py | 5 +---- 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..37516d4a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: +# Need to drop python 3.5 and 3.6 before we include python upgrade +# - repo: https://github.com/asottile/pyupgrade +# rev: v2.31.0 +# hooks: +# - id: pyupgrade +# args: ["--py37-plus"] +# +# FIXME: Should be enabled for codebases that are minimum django 2.2+ +# - repo: https://github.com/adamchainz/django-upgrade +# rev: '1.4.0' +# hooks: +# - id: django-upgrade +# args: [--target-version, "2.2"] + + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 + hooks: + - id: flake8 + + - repo: https://github.com/asottile/yesqa + rev: v1.3.0 + hooks: + - id: yesqa + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-merge-conflict + - id: mixed-line-ending + + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7172ddb3..dd7ab2f3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ========== +* port-feat: pre-commit config added from the v3 workstream * fix: Added test coverage to admin preview view 4.0.0.dev4 (2022-02-03) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 6769d9ce..0b5f40de 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -142,12 +142,12 @@ def preview_view(self, request, snippet_id=None, form_url='', extra_context=None def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - url( + url( r"^(?P\d+)/preview/$", self.admin_site.admin_view(self.preview_view), name="{}_{}_preview".format(*info), - ), - ] + super().get_urls() + ), + ] + super().get_urls() def has_delete_permission(self, request, obj=None): """ diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d6ab5182..210af121 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -5,9 +5,10 @@ from cms.test_utils.testcases import CMSTestCase from cms.toolbar.utils import get_object_edit_url, get_object_structure_url -from djangocms_snippet.models import Snippet, SnippetGrouper from djangocms_versioning.models import Version +from djangocms_snippet.models import Snippet, SnippetGrouper + from .utils.factories import SnippetWithVersionFactory diff --git a/tests/utils/factories.py b/tests/utils/factories.py index af8d18ce..9d6210c6 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -6,11 +6,8 @@ from cms.models import Placeholder import factory -from factory.fuzzy import ( - FuzzyInteger, - FuzzyText, -) from djangocms_versioning.models import Version +from factory.fuzzy import FuzzyInteger, FuzzyText from djangocms_snippet.models import Snippet, SnippetGrouper, SnippetPtr From dbd61f5583230247a62102ef222eca5768b437d8 Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Tue, 10 May 2022 15:54:12 +0100 Subject: [PATCH 23/66] feat: django-cms 4.0.x - django 3.2 and Python 3.9 compatibility (#92) --- .github/workflows/test.yml | 4 ++-- .pre-commit-config.yaml | 11 +++++------ CHANGELOG.rst | 6 ++++++ setup.py | 5 ++--- tests/requirements/base.txt | 12 ++++++++---- tests/requirements/dj11_cms40.txt | 4 ---- tests/requirements/dj22_cms40.txt | 2 -- tests/requirements/dj32_cms40.txt | 3 +++ tests/settings.py | 2 ++ 9 files changed, 28 insertions(+), 21 deletions(-) delete mode 100644 tests/requirements/dj11_cms40.txt create mode 100644 tests/requirements/dj32_cms40.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a3bf1414..5804387b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.6, 3.7, ] + python-version: [ 3.7, 3.8, 3.9 ] requirements-file: [ - dj11_cms40.txt, dj22_cms40.txt, + dj32_cms40.txt, ] os: [ ubuntu-20.04, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 37516d4a..8f6f84b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,12 +6,11 @@ repos: # - id: pyupgrade # args: ["--py37-plus"] # -# FIXME: Should be enabled for codebases that are minimum django 2.2+ -# - repo: https://github.com/adamchainz/django-upgrade -# rev: '1.4.0' -# hooks: -# - id: django-upgrade -# args: [--target-version, "2.2"] + - repo: https://github.com/adamchainz/django-upgrade + rev: '1.4.0' + hooks: + - id: django-upgrade + args: [--target-version, "2.2"] - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dd7ab2f3..95ed4692 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,9 +4,15 @@ Changelog Unreleased ========== + +* Python 3.8, 3.9 support added +* Django 3.0, 3.1 and 3.2 support added +* Python 3.5 and 3.6 support removed +* Django 1.11 support removed * port-feat: pre-commit config added from the v3 workstream * fix: Added test coverage to admin preview view + 4.0.0.dev4 (2022-02-03) ======================= diff --git a/setup.py b/setup.py index 08ee8285..e466fb92 100644 --- a/setup.py +++ b/setup.py @@ -18,17 +18,16 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Framework :: Django', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', + 'Framework :: Django :: 3.2', 'Framework :: Django CMS', - 'Framework :: Django CMS :: 3.7', - 'Framework :: Django CMS :: 3.8', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development', diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 2ea5f4f9..96a6f304 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -1,6 +1,10 @@ -django-app-helper -tox coverage -isort -flake8 +django-app-helper factory-boy +flake8 +isort +tox + +# Unreleased django-cms 4.0 compatible packages +https://github.com/django-cms/django-cms/tarball/develop-4#egg=django-cms +https://github.com/django-cms/djangocms-versioning/tarball/master#egg=djangocms-versioning diff --git a/tests/requirements/dj11_cms40.txt b/tests/requirements/dj11_cms40.txt deleted file mode 100644 index 4c9d6c95..00000000 --- a/tests/requirements/dj11_cms40.txt +++ /dev/null @@ -1,4 +0,0 @@ --r base.txt - -https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms -https://github.com/divio/djangocms-versioning/tarball/master/#egg=djangocms-versioning diff --git a/tests/requirements/dj22_cms40.txt b/tests/requirements/dj22_cms40.txt index c84c4174..2f04bac0 100644 --- a/tests/requirements/dj22_cms40.txt +++ b/tests/requirements/dj22_cms40.txt @@ -1,5 +1,3 @@ -r base.txt Django>=2.2,<3.0 -https://github.com/django-cms/django-cms/tarball/release/4.0.x#egg=django-cms -https://github.com/divio/djangocms-versioning/tarball/master/#egg=djangocms-versioning diff --git a/tests/requirements/dj32_cms40.txt b/tests/requirements/dj32_cms40.txt new file mode 100644 index 00000000..7526db15 --- /dev/null +++ b/tests/requirements/dj32_cms40.txt @@ -0,0 +1,3 @@ +-r base.txt + +Django>=3.2,<4.0 diff --git a/tests/settings.py b/tests/settings.py index 60b52c6d..c5ec3774 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,5 +1,6 @@ #!/usr/bin/env python HELPER_SETTINGS = { + 'SECRET_KEY': "djangocmssnippetstestsuitekey", 'INSTALLED_APPS': [ 'tests.utils', 'djangocms_versioning', @@ -18,6 +19,7 @@ 'CMS_TEMPLATES': ( ("page.html", "Normal page"), ), + "DEFAULT_AUTO_FIELD": "django.db.models.AutoField", } From 5f7ff3e2c4ecdbe2e19bd7d6d2178e1c2029dc41 Mon Sep 17 00:00:00 2001 From: Aiky30 Date: Wed, 11 May 2022 10:02:08 +0100 Subject: [PATCH 24/66] Release 4.0.1.dev1 (#119) --- CHANGELOG.rst | 4 ++++ djangocms_snippet/__init__.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 95ed4692..cc0d8f72 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,10 @@ Changelog Unreleased ========== + +4.0.1.dev1 (2022-05-10) +======================= + * Python 3.8, 3.9 support added * Django 3.0, 3.1 and 3.2 support added * Python 3.5 and 3.6 support removed diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 471a3ac3..9621cc38 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.0.dev4' +__version__ = '4.0.1.dev1' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From c9c4d25adccacd212adbc9430905656a8de0e01d Mon Sep 17 00:00:00 2001 From: Michael Collins <15347726+michaeljcollinsuk@users.noreply.github.com> Date: Tue, 15 Nov 2022 14:37:24 +0000 Subject: [PATCH 25/66] feat: Enable add button on the SnippetPluginForm (#127) --- CHANGELOG.rst | 2 +- djangocms_snippet/apps.py | 1 + djangocms_snippet/cms_plugins.py | 2 ++ djangocms_snippet/forms.py | 28 +++++++++++++++++++++++++++- tests/requirements/base.txt | 2 +- tests/test_forms.py | 21 +++++++++++++++++++++ tests/test_migrations.py | 2 +- 7 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc0d8f72..3a5039b2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog Unreleased ========== - +* feat: Enable add button to crate a snippet when adding a SnippetPlugin 4.0.1.dev1 (2022-05-10) ======================= diff --git a/djangocms_snippet/apps.py b/djangocms_snippet/apps.py index 898fdf2c..8a6cda20 100644 --- a/djangocms_snippet/apps.py +++ b/djangocms_snippet/apps.py @@ -5,3 +5,4 @@ class SnippetConfig(AppConfig): name = 'djangocms_snippet' verbose_name = _('Snippets') + default_auto_field = 'django.db.models.AutoField' diff --git a/djangocms_snippet/cms_plugins.py b/djangocms_snippet/cms_plugins.py index a6802626..bbaa2692 100644 --- a/djangocms_snippet/cms_plugins.py +++ b/djangocms_snippet/cms_plugins.py @@ -7,6 +7,7 @@ from cms.plugin_base import CMSPluginBase from cms.plugin_pool import plugin_pool +from .forms import SnippetPluginForm from .models import SnippetPtr from .utils import show_draft_content @@ -21,6 +22,7 @@ class SnippetPlugin(CMSPluginBase): text_enabled = True text_editor_preview = False cache = CACHE_ENABLED + form = SnippetPluginForm def render(self, context, instance, placeholder): snippet = instance.snippet_grouper.snippet(show_editable=show_draft_content(context["request"])) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index e503b9d5..0a7d1952 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -1,9 +1,12 @@ from django import forms +from django.contrib import admin from django.db import transaction from django.utils.translation import ugettext_lazy as _ +from cms.utils.urlutils import admin_reverse + from djangocms_snippet.cms_config import SnippetCMSAppConfig -from djangocms_snippet.models import Snippet, SnippetGrouper +from djangocms_snippet.models import Snippet, SnippetGrouper, SnippetPtr try: @@ -64,3 +67,26 @@ def save(self, **kwargs): if commit: snippet.save() return snippet + + +class SnippetPluginForm(forms.ModelForm): + + class Meta: + model = SnippetPtr + fields = ("cmsplugin_ptr", "snippet_grouper") + + def __init__(self, *args, **kwargs): + """ + Initialise the form with the add button enabled to allow adding a new snippet from the plugin form. To enable + this the get_related_url method on the widget is overridden to build a URL for the Snippet admin instead of + the SnippetGrouper, as this is not enabled in the admin. + """ + super().__init__(*args, **kwargs) + self.fields["snippet_grouper"].widget.can_add_related = True + self.fields["snippet_grouper"].widget.get_related_url = self.get_related_url_for_snippet + + def get_related_url_for_snippet(self, info, action, *args): + """ + Build URL to the Snippet admin for the given action + """ + return admin_reverse(f"djangocms_snippet_snippet_{action}", current_app=admin.site.name, args=args) diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 96a6f304..95b3f3f1 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -6,5 +6,5 @@ isort tox # Unreleased django-cms 4.0 compatible packages -https://github.com/django-cms/django-cms/tarball/develop-4#egg=django-cms +http://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms https://github.com/django-cms/djangocms-versioning/tarball/master#egg=djangocms-versioning diff --git a/tests/test_forms.py b/tests/test_forms.py index 399db3d0..83c71a8e 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -5,6 +5,7 @@ from cms.test_utils.testcases import CMSTestCase from djangocms_snippet import cms_config, forms +from djangocms_snippet.forms import SnippetPluginForm from djangocms_snippet.models import Snippet, SnippetGrouper from .utils.factories import SnippetWithVersionFactory @@ -188,3 +189,23 @@ def test_snippet_form_validation_multiple_version_states_in_grouper(self): form = forms.SnippetForm(form_data) self.assertTrue(form.is_valid()) + + +class SnippetPluginFormTestCase(CMSTestCase): + + def setUp(self): + self.form = SnippetPluginForm() + + def test_get_related_url_for_snippet(self): + """ + Check that the url to add a snippet in the admin is returned + """ + self.assertEqual(self.form.get_related_url_for_snippet("", "add"), "/en/admin/djangocms_snippet/snippet/add/") + + def test_get_related_url_for_snippet_used(self): + """ + Checks that the get_related_url widget is overridden + """ + snippet_grouper_widget = self.form.fields["snippet_grouper"].widget + self.assertEqual(snippet_grouper_widget.get_related_url, self.form.get_related_url_for_snippet) + self.assertTrue(snippet_grouper_widget.can_add_related) diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 29c4a94b..d098d35d 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -19,7 +19,7 @@ def test_for_missing_migrations(self): } try: - call_command('makemigrations', **options) + call_command('makemigrations', 'djangocms_snippet', **options) except SystemExit as e: status_code = str(e) else: From 1cd77a8cb37da18eb5dd61b8d272daa79f040e07 Mon Sep 17 00:00:00 2001 From: Michael Collins <15347726+michaeljcollinsuk@users.noreply.github.com> Date: Tue, 15 Nov 2022 14:59:37 +0000 Subject: [PATCH 26/66] Release 4.0.1.dev2 (#128) --- CHANGELOG.rst | 5 ++++- djangocms_snippet/__init__.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a5039b2..4587b36e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,10 @@ Changelog Unreleased ========== -* feat: Enable add button to crate a snippet when adding a SnippetPlugin + +4.0.1.dev2 (2022-11-15) +======================= +* feat: Enable add button to create a snippet when adding a SnippetPlugin 4.0.1.dev1 (2022-05-10) ======================= diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index 9621cc38..d548be3e 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.1.dev1' +__version__ = '4.0.1.dev2' default_app_config = 'djangocms_snippet.apps.SnippetConfig' From 1fb6fca1eec03bec2fe97c2be330ec64c856605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=82=85=E7=91=9E=E6=9B=BC?= Date: Thu, 14 Mar 2024 15:35:15 +0800 Subject: [PATCH 27/66] init support for django4.2 (#150) * init support for django4.2 * add change log for add compatiable for django 4.2 and python 3.10 * Update CHANGELOG.rst * Update .pre-commit-config.yaml * fix lint config issue * remove test code * add blackline for flake8 * add pyproject.toml file * add requirement files * fix ci failed issue * format setup.py import section * upgrade isort version to fix pre commit ci issue * upgrade the django-upgrade package to pass through the pre commit ci test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * upgrade packages in pre commit * resort init file changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * comment the django-upgrade from ci * fix: add missing migration file * fix: update previous file instead of creating new file --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Josh Peng Yu --- .github/workflows/lint.yml | 4 +- .github/workflows/test.yml | 4 +- .pre-commit-config.yaml | 22 +-- CHANGELOG.rst | 4 + djangocms_snippet/admin.py | 14 +- djangocms_snippet/forms.py | 2 +- ...0005_set_related_name_for_cmsplugin_ptr.py | 2 +- djangocms_snippet/settings.py | 32 ++++ pyproject.toml | 141 ++++++++++++++++++ requirements.in | 6 + requirements.txt | 83 +++++++++++ setup.py | 15 +- tests/requirements/base.txt | 4 - tests/requirements/dj22_cms40.txt | 3 - tests/requirements/dj32_cms40.txt | 4 + tests/requirements/dj42_cms40.txt | 7 + tox.ini | 12 +- 17 files changed, 314 insertions(+), 45 deletions(-) create mode 100644 djangocms_snippet/settings.py create mode 100644 pyproject.toml create mode 100644 requirements.in create mode 100644 requirements.txt delete mode 100644 tests/requirements/dj22_cms40.txt create mode 100644 tests/requirements/dj42_cms40.txt diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 95283181..1d16b2e1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' - name: Install flake8 run: pip install --upgrade flake8 - name: Run flake8 @@ -29,7 +29,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' - run: python -m pip install isort - name: isort uses: liskin/gh-problem-matcher-wrap@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5804387b..a9b84fd2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,10 +8,10 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9 ] + python-version: [ 3.8, 3.9, '3.10' ] requirements-file: [ - dj22_cms40.txt, dj32_cms40.txt, + dj42_cms40.txt, ] os: [ ubuntu-20.04, diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f6f84b3..59ff7c12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,31 +4,33 @@ repos: # rev: v2.31.0 # hooks: # - id: pyupgrade -# args: ["--py37-plus"] +# args: ["--py38-plus"] # - - repo: https://github.com/adamchainz/django-upgrade - rev: '1.4.0' - hooks: - - id: django-upgrade - args: [--target-version, "2.2"] + # manually run the upgrade command to fix the issue, no need for ci to run. + # - repo: https://github.com/adamchainz/django-upgrade + # rev: '1.16.0' + # hooks: + # - id: django-upgrade + # args: [--target-version, "4.2"] - repo: https://github.com/PyCQA/flake8 - rev: 4.0.1 + rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/asottile/yesqa - rev: v1.3.0 + rev: v1.5.0 hooks: - id: yesqa - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.5.0 hooks: - id: check-merge-conflict - id: mixed-line-ending + # upgrade the isort version to fix compatiable issue withe peotry: https://stackoverflow.com/questions/75269700/pre-commit-fails-to-install-isort-5-11-4-with-error-runtimeerror-the-poetry-co - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4587b36e..a1056c19 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,10 @@ Changelog Unreleased ========== +* add support for python 3.10 +* add support for django 4.2 +* drop support for django < 3.2 +* drop support python < 3.8 4.0.1.dev2 (2022-11-15) ======================= diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 0b5f40de..de55dc41 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -1,5 +1,4 @@ from django.conf import settings -from django.conf.urls import url from django.contrib import admin from django.contrib.admin import helpers from django.contrib.admin.exceptions import DisallowedModelAdminToField @@ -7,6 +6,7 @@ from django.contrib.admin.utils import flatten_fieldsets, unquote from django.db import models from django.forms import Textarea +from django.urls import path from django.utils.translation import gettext as _ from cms.utils.permissions import get_model_permission_codename @@ -29,6 +29,7 @@ djangocms_versioning_enabled = False +@admin.register(Snippet) class SnippetAdmin(*snippet_admin_classes): list_display = ('name',) search_fields = ['name'] @@ -92,10 +93,10 @@ def preview_view(self, request, snippet_id=None, form_url='', extra_context=None model = self.model opts = model._meta - obj = self.get_object(request, unquote(snippet_id), to_field) + obj = self.get_object(request, unquote(str(snippet_id)), to_field) if obj is None: - return self._get_obj_does_not_exist_redirect(request, opts, snippet_id) + return self._get_obj_does_not_exist_redirect(request, opts, str(snippet_id)) fieldsets = self.get_fieldsets(request, obj) ModelForm = self.get_form( @@ -142,8 +143,8 @@ def preview_view(self, request, snippet_id=None, form_url='', extra_context=None def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - url( - r"^(?P\d+)/preview/$", + path( + "/preview/", self.admin_site.admin_view(self.preview_view), name="{}_{}_preview".format(*info), ), @@ -160,6 +161,3 @@ def has_delete_permission(self, request, obj=None): get_model_permission_codename(self.model, 'add'), ) return False - - -admin.site.register(Snippet, SnippetAdmin) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 0a7d1952..8a0d97b6 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -1,7 +1,7 @@ from django import forms from django.contrib import admin from django.db import transaction -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from cms.utils.urlutils import admin_reverse diff --git a/djangocms_snippet/migrations/0005_set_related_name_for_cmsplugin_ptr.py b/djangocms_snippet/migrations/0005_set_related_name_for_cmsplugin_ptr.py index 77ca9123..f27fa954 100644 --- a/djangocms_snippet/migrations/0005_set_related_name_for_cmsplugin_ptr.py +++ b/djangocms_snippet/migrations/0005_set_related_name_for_cmsplugin_ptr.py @@ -12,6 +12,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='snippetptr', name='cmsplugin_ptr', - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='djangocms_snippet_snippetptr', serialize=False, to='cms.CMSPlugin'), + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='%(app_label)s_%(class)s', serialize=False, to='cms.cmsplugin'), ), ] diff --git a/djangocms_snippet/settings.py b/djangocms_snippet/settings.py new file mode 100644 index 00000000..c5ec3774 --- /dev/null +++ b/djangocms_snippet/settings.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +HELPER_SETTINGS = { + 'SECRET_KEY': "djangocmssnippetstestsuitekey", + 'INSTALLED_APPS': [ + 'tests.utils', + 'djangocms_versioning', + 'djangocms_snippet', + ], + 'CMS_LANGUAGES': { + 1: [{ + 'code': 'en', + 'name': 'English', + }] + }, + 'LANGUAGE_CODE': 'en', + 'ALLOWED_HOSTS': ['localhost'], + 'DJANGOCMS_SNIPPET_VERSIONING_ENABLED': True, + 'DJANGOCMS_SNIPPET_MODERATION_ENABLED': True, + 'CMS_TEMPLATES': ( + ("page.html", "Normal page"), + ), + "DEFAULT_AUTO_FIELD": "django.db.models.AutoField", +} + + +def run(): + from app_helper import runner + runner.cms('djangocms_snippet') + + +if __name__ == '__main__': + run() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dc94fb1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,141 @@ +[build-system] +requires = ["setuptools >= 40.6.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "djangocms-snippet" +version = "4.0.1.dev2" +authors = [ + {name = "Divio AG", email = "info@divio.ch"}, +] +maintainers = [ + {name = "Django CMS Association and contributors", email = "info@django-cms.org"} +] +license = {file = "LICENSE"} +description = "Adds snippet plugin to django CMS." +readme = "README.rst" +requires-python = ">=3.8" +dependencies = [ + 'django-cms>=4.0', +] +classifiers=[ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django CMS", + "Framework :: Django CMS :: 4.0", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", +] + +[project.optional-dependencies] +static-ace = ["djangocms-static-ace"] + +[tool.setuptools.packages.find] +where = ["djangocms_snippet"] +exclude = ["tests"] + +[project.urls] +"Bug Tracker" = "https://github.com/django-cms/djangocms-snippet/issues" +Changelog = "https://github.com/django-cms/djangocms-snippet/blob/master/CHANGELOG.rst" +Repository = "https://github.com/django-cms/djangocms-snippet" +Support = "https://www.django-cms.org/slack/" + +[tool.coverage.run] +branch = true +parallel = true +source = [ + "djangocms_snippet", + "tests", +] + +[tool.coverage.paths] +source = [ + "djangocms_snippet", + ".tox/**/site-packages", +] + +[tool.coverage.report] +show_missing = true + +[tool.mypy] +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_defs = true +mypy_path = "djangocms_snippet/" +no_implicit_optional = true +show_error_codes = true +warn_unreachable = true +warn_unused_ignores = true + +[[tool.mypy.overrides]] +module = "tests.*" +allow_untyped_defs = true + + +[tool.ruff] +# https://beta.ruff.rs/docs/configuration/ +line-length = 79 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "C", # flake8-comprehensions + "DJ", # flake8-django + "INT", # flake8-gettext + "PIE", # flake8-pie + "SIM", # flake8-simplify + "PGH", # pygrep-hooks + "PLE", # pylint error + "PLR", # pylint refactor + "PLW", # pylint warning + "UP", # pyupgrade + "C901", # mccabe + "N", # pep8-naming + "YTT", # flake8-2020, + "RUF" +] + +exclude = [ + ".eggs", + ".git", + ".mypy_cache", + ".ruff_cache", + ".env", + ".venv", + "**migrations/**", + "venv", +] + +ignore = [ + "E501", # line-too-long + "W191", # tab-indentation +] + +[tool.ruff.per-file-ignores] +"__init__.py" = [ + "F401" # unused-import +] + +[tool.ruff.isort] +combine-as-imports = true +known-first-party = [ + "djangocms_snippet", +] +extra-standard-library = ["dataclasses"] \ No newline at end of file diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..0c959bcd --- /dev/null +++ b/requirements.in @@ -0,0 +1,6 @@ +bump2version +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +django-treebeard +pip-tools +pre-commit +wheel diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c30d3b52 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,83 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile +# +asgiref==3.7.2 + # via django +build==1.1.1 + # via pip-tools +bump2version==1.0.1 + # via -r requirements.in +cfgv==3.4.0 + # via pre-commit +click==8.1.7 + # via pip-tools +distlib==0.3.8 + # via virtualenv +django==4.2.11 + # via + # django-classy-tags + # django-cms + # django-formtools + # django-sekizai + # django-treebeard + # djangocms-admin-style +django-classy-tags==4.1.0 + # via + # django-cms + # django-sekizai +django-cms @ https://github.com/django-cms/django-cms/tarball/release/4.0.1.x + # via -r requirements.in +django-formtools==2.5.1 + # via django-cms +django-sekizai==4.1.0 + # via django-cms +django-treebeard==4.7.1 + # via + # -r requirements.in + # django-cms +djangocms-admin-style==3.3.1 + # via django-cms +filelock==3.13.1 + # via virtualenv +identify==2.5.35 + # via pre-commit +nodeenv==1.8.0 + # via pre-commit +packaging==24.0 + # via + # build + # django-cms +pip-tools==7.4.1 + # via -r requirements.in +platformdirs==4.2.0 + # via virtualenv +pre-commit==3.6.2 + # via -r requirements.in +pyproject-hooks==1.0.0 + # via + # build + # pip-tools +pyyaml==6.0.1 + # via pre-commit +sqlparse==0.4.4 + # via django +tomli==2.0.1 + # via + # build + # pip-tools + # pyproject-hooks +typing-extensions==4.10.0 + # via asgiref +virtualenv==20.25.1 + # via pre-commit +wheel==0.43.0 + # via + # -r requirements.in + # pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/setup.py b/setup.py index e466fb92..de80e82e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,12 @@ #!/usr/bin/env python +import os +import sys + from setuptools import find_packages, setup -from djangocms_snippet import __version__ + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from djangocms_snippet import __version__ # noqa:E402 REQUIREMENTS = [ @@ -18,16 +23,14 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Framework :: Django', - 'Framework :: Django :: 2.2', - 'Framework :: Django :: 3.0', - 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.2', 'Framework :: Django CMS', + 'Framework :: Django CMS :: 4.0', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development', diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt index 95b3f3f1..95964b6b 100644 --- a/tests/requirements/base.txt +++ b/tests/requirements/base.txt @@ -4,7 +4,3 @@ factory-boy flake8 isort tox - -# Unreleased django-cms 4.0 compatible packages -http://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms -https://github.com/django-cms/djangocms-versioning/tarball/master#egg=djangocms-versioning diff --git a/tests/requirements/dj22_cms40.txt b/tests/requirements/dj22_cms40.txt deleted file mode 100644 index 2f04bac0..00000000 --- a/tests/requirements/dj22_cms40.txt +++ /dev/null @@ -1,3 +0,0 @@ --r base.txt - -Django>=2.2,<3.0 diff --git a/tests/requirements/dj32_cms40.txt b/tests/requirements/dj32_cms40.txt index 7526db15..3408845a 100644 --- a/tests/requirements/dj32_cms40.txt +++ b/tests/requirements/dj32_cms40.txt @@ -1,3 +1,7 @@ -r base.txt Django>=3.2,<4.0 + +# Unreleased django 3.2 & django-cms 4.0.x compatible packages +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning \ No newline at end of file diff --git a/tests/requirements/dj42_cms40.txt b/tests/requirements/dj42_cms40.txt new file mode 100644 index 00000000..96c85a01 --- /dev/null +++ b/tests/requirements/dj42_cms40.txt @@ -0,0 +1,7 @@ +-r base.txt + +Django>=4.2,<5.0 + +# Unreleased django 4.2 & django-cms 4.0.x compatible packages +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +https://github.com/joshyu/djangocms-versioning/tarball/feat/django-42-compatible#egg=djangocms-versioning \ No newline at end of file diff --git a/tox.ini b/tox.ini index 4d1596db..744c0de9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,7 @@ envlist = flake8 isort - py{35,36,37,38}-dj{22}-cms{37,38} - py{36,37,38}-dj{30}-cms{37,38} - py{36,37,38}-dj{31}-cms{38} + py{38,39,'3.10'}-dj{32,42}-cms{40} skip_missing_interpreters=True @@ -40,11 +38,9 @@ known_django = django [testenv] deps = -r{toxinidir}/tests/requirements/base.txt - dj22: Django>=2.2,<3.0 - dj30: Django>=3.0,<3.1 - dj31: Django>=3.1,<3.2 - cms37: django-cms>=3.7,<3.8 - cms38: django-cms>=3.8,<3.9 + dj32: Django>=3.2,<4 + dj42: Django>=4.2,<5 + cms40: django-cms>=4.0.0,<4.1 commands = {envpython} --version {env:COMMAND:coverage} erase From aaec61955cd7991b0e23e8858fb165a03fa4607d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=82=85=E7=91=9E=E6=9B=BC?= Date: Mon, 1 Apr 2024 16:23:04 +0800 Subject: [PATCH 28/66] fix pyproject.toml config error (#151) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dc94fb1b..3992ef84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ classifiers=[ static-ace = ["djangocms-static-ace"] [tool.setuptools.packages.find] -where = ["djangocms_snippet"] +where = ["."] exclude = ["tests"] [project.urls] From eabef4732a4b88f6022a4bd2da72272b51a234c1 Mon Sep 17 00:00:00 2001 From: Vipul Narang <61502917+vipulnarang95@users.noreply.github.com> Date: Wed, 15 May 2024 00:45:14 +0530 Subject: [PATCH 29/66] Added site field in models n site filter in admin (#159) * Added site field in models n site filter in admin * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * updated versioning branch * Add test cases and updated the implementation * Update CHANGELOG.rst Co-authored-by: Mark Walker * corrected formatting * fixed flake8 issues * removed list_filter --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Mark Walker --- CHANGELOG.rst | 1 + djangocms_snippet/admin.py | 8 ++++ djangocms_snippet/forms.py | 1 + .../migrations/0013_snippet_site.py | 20 +++++++++ djangocms_snippet/models.py | 2 + tests/requirements/dj42_cms40.txt | 2 +- tests/test_admin.py | 44 ++++++++++++++++++- 7 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 djangocms_snippet/migrations/0013_snippet_site.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a1056c19..76a464e8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ========== +* feat: Added sites support for Snippets * add support for python 3.10 * add support for django 4.2 * drop support for django < 3.2 diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index de55dc41..c6e13e53 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -9,6 +9,7 @@ from django.urls import path from django.utils.translation import gettext as _ +from cms.utils import get_current_site from cms.utils.permissions import get_model_permission_codename from .cms_config import SnippetCMSAppConfig @@ -50,6 +51,13 @@ class SnippetAdmin(*snippet_admin_classes): class Meta: model = Snippet + def get_queryset(self, request): + site = get_current_site() + queryset = super().get_queryset(request) + # Filter queryset with current site and no site + queryset = queryset.filter(models.Q(site=site) | models.Q(site=None)) + return queryset + def get_list_display(self, request): list_display = super().get_list_display(request) list_display = list(list_display) diff --git a/djangocms_snippet/forms.py b/djangocms_snippet/forms.py index 8a0d97b6..a616f265 100644 --- a/djangocms_snippet/forms.py +++ b/djangocms_snippet/forms.py @@ -27,6 +27,7 @@ class Meta: "slug", "snippet_grouper", "template", + "site", ) def __init__(self, *args, **kwargs): diff --git a/djangocms_snippet/migrations/0013_snippet_site.py b/djangocms_snippet/migrations/0013_snippet_site.py new file mode 100644 index 00000000..a7948851 --- /dev/null +++ b/djangocms_snippet/migrations/0013_snippet_site.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.11 on 2024-05-07 03:43 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ('djangocms_snippet', '0012_auto_20210915_0721'), + ] + + operations = [ + migrations.AddField( + model_name='snippet', + name='site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='sites.site'), + ), + ] diff --git a/djangocms_snippet/models.py b/djangocms_snippet/models.py index b7baea5f..20a1578d 100644 --- a/djangocms_snippet/models.py +++ b/djangocms_snippet/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.sites.models import Site from django.db import models from django.shortcuts import reverse from django.utils.translation import gettext_lazy as _ @@ -70,6 +71,7 @@ class Snippet(models.Model): default='', max_length=255, ) + site = models.ForeignKey(Site, on_delete=models.CASCADE, null=True, blank=True) def __str__(self): return self.name diff --git a/tests/requirements/dj42_cms40.txt b/tests/requirements/dj42_cms40.txt index 96c85a01..75e2b129 100644 --- a/tests/requirements/dj42_cms40.txt +++ b/tests/requirements/dj42_cms40.txt @@ -4,4 +4,4 @@ Django>=4.2,<5.0 # Unreleased django 4.2 & django-cms 4.0.x compatible packages https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms -https://github.com/joshyu/djangocms-versioning/tarball/feat/django-42-compatible#egg=djangocms-versioning \ No newline at end of file +https://github.com/django-cms/djangocms-versioning/tarball/support/django-cms-4.0.x#egg=djangocms-versioning diff --git a/tests/test_admin.py b/tests/test_admin.py index d03d2358..8440e3e6 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,10 +1,12 @@ from importlib import reload from django.contrib import admin +from django.contrib.sites.models import Site from django.shortcuts import reverse from django.test import RequestFactory, override_settings from cms.test_utils.testcases import CMSTestCase +from cms.utils import get_current_site from djangocms_versioning.models import Version @@ -23,12 +25,52 @@ def setUp(self): html="

This is a test

", snippet_grouper=SnippetGrouper.objects.create(), ) - self.snippet_version = Version.objects.create(content=self.snippet, created_by=self.superuser) + self.snippet_version = Version.objects.create( + content=self.snippet, + created_by=self.superuser, + state='published' + ) self.snippet_admin = snippet_admin.SnippetAdmin(Snippet, admin) self.snippet_admin_request = RequestFactory().get("/admin/djangocms_snippet") self.edit_url = reverse("admin:djangocms_snippet_snippet_change", args=(self.snippet.id,),) self.delete_url = reverse("admin:djangocms_snippet_snippet_delete", args=(self.snippet.id,),) + def test_get_queryset(self): + current_site = get_current_site() + another_site = Site.objects.create(domain='http://www.django-cms.org', name='Django CMS', pk=3) + current_site_snippet = Snippet.objects.create( + name="Test Snippet 1", + slug="test-snippet-one", + html="

This is a test snippet one

", + snippet_grouper=SnippetGrouper.objects.create(), + site=current_site + ) + another_site_snippet = Snippet.objects.create( + name="Test Snippet 2", + slug="test-snippet-two", + html="

This is a test snippet two

", + snippet_grouper=SnippetGrouper.objects.create(), + site=another_site + ) + # Create versions of snippets + Version.objects.create( + content=current_site_snippet, + created_by=self.superuser, + state='published' + ) + Version.objects.create( + content=another_site_snippet, + created_by=self.superuser, + state='published' + ) + queryset = self.snippet_admin.get_queryset(self.snippet_admin_request) + # Test for snippet of current site + self.assertIn(current_site_snippet, queryset) + # Test for snippet with no site + self.assertIn(self.snippet, queryset) + # Test for snippet with another site + self.assertNotIn(another_site_snippet, queryset) + @override_settings(DJANGOCMS_SNIPPET_VERSIONING_ENABLED=False) def test_admin_list_display_without_versioning(self): """ From 48daa6a10f804a3bef4c41e511b19f5de5da93b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=82=85=E7=91=9E=E6=9B=BC?= Date: Thu, 16 May 2024 18:13:48 +0800 Subject: [PATCH 30/66] Release/4.1.0 (#155) * bump version to 4.1.0 * bump version to 4.1.0 pyproject.toml * replace test requirement url * update changelog date * update release date. --- CHANGELOG.rst | 11 ++++++++++- djangocms_snippet/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76a464e8..fdfbf9a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,18 +2,27 @@ Changelog ========= + +========== Unreleased ========== + +4.1.0 (2024-05-16) +================== + * feat: Added sites support for Snippets * add support for python 3.10 -* add support for django 4.2 +* add support for django >= 4.2 * drop support for django < 3.2 * drop support python < 3.8 + 4.0.1.dev2 (2022-11-15) ======================= + * feat: Enable add button to create a snippet when adding a SnippetPlugin + 4.0.1.dev1 (2022-05-10) ======================= diff --git a/djangocms_snippet/__init__.py b/djangocms_snippet/__init__.py index d548be3e..e15211e3 100644 --- a/djangocms_snippet/__init__.py +++ b/djangocms_snippet/__init__.py @@ -1,3 +1,3 @@ -__version__ = '4.0.1.dev2' +__version__ = '4.1.0' default_app_config = 'djangocms_snippet.apps.SnippetConfig' diff --git a/pyproject.toml b/pyproject.toml index 3992ef84..771854a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "djangocms-snippet" -version = "4.0.1.dev2" +version = "4.1.0" authors = [ {name = "Divio AG", email = "info@divio.ch"}, ] From f9147e559336d3921aa561df0caffa3cb747c989 Mon Sep 17 00:00:00 2001 From: Nicolai <69975577+NicolaiRidani@users.noreply.github.com> Date: Fri, 15 Oct 2021 15:56:56 +0200 Subject: [PATCH 31/66] Update README.rst --- README.rst | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 4fe1bf5a..6478d061 100644 --- a/README.rst +++ b/README.rst @@ -8,11 +8,11 @@ django CMS Snippet **django CMS Snippet** provides a plugin for `django CMS `_ to inject HTML, CSS or JavaScript snippets into your website. -This addon is compatible with `Divio Cloud `_ and is also available on the -`django CMS Marketplace `_ -for easy installation. +This project is endorsed by the `django CMS Association `_. Join us on `Slack `_. -We recommend using this plugin only during development:: + + +Warning: We recommend using this plugin only during development:: This plugin is a potential security hazard, since it allows authorized- users to place custom markup or Javascript on pages bypassing all of @@ -24,16 +24,22 @@ We recommend using this plugin only during development:: .. image:: preview.gif -Contributing -============ +******************************************* +Contribute to this project and win rewards +******************************************* + +Because this is a an open-source project, we welcome everyone to +`get involved in the project `_ and +`receive a reward `_ for their contribution. +Become part of a fantastic community and help us make django CMS the best CMS in the world. -This is a an open-source project. We'll be delighted to receive your +We'll be delighted to receive your feedback in the form of issues and pull requests. Before submitting your pull request, please review our `contribution guidelines `_. We're grateful to all contributors who have helped create and maintain this package. -Contributors are listed at the `contributors `_ +Contributors are listed at the `contributors `_ section. One of the easiest contributions you can make is helping to translate this addon on From 27610d9f14614469930d7476f74f723bdbda8e69 Mon Sep 17 00:00:00 2001 From: Nicolai <69975577+NicolaiRidani@users.noreply.github.com> Date: Wed, 27 Oct 2021 16:44:25 +0200 Subject: [PATCH 32/66] Update README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 6478d061..a209a2a7 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ pull request, please review our `contribution guidelines `_. We're grateful to all contributors who have helped create and maintain this package. -Contributors are listed at the `contributors `_ +Contributors are listed at the `contributors `_ section. One of the easiest contributions you can make is helping to translate this addon on From 3252fc902d4bc33fe54817121076fddde86b1101 Mon Sep 17 00:00:00 2001 From: Kaushal Dhungel <57295839+Kaushal-Dhungel@users.noreply.github.com> Date: Tue, 9 Nov 2021 22:36:06 +0545 Subject: [PATCH 33/66] Create PULL_REQUEST_TEMPLATE.md (#85) Add PR template --- .github/PULL_REQUEST_TEMPLATE.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..dc334a20 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +## Description + + + +## Related resources + + + +* #... +* #... + +## Checklist + + + +* [ ] I have opened this pull request against ``master`` +* [ ] I have added or modified the tests when changing logic +* [ ] I have followed [the conventional commits guidelines](https://www.conventionalcommits.org/) to add meaningful information into the changelog +* [ ] I have read the [contribution guidelines ](https://github.com/django-cms/django-cms/blob/develop/CONTRIBUTING.rst) and I have joined #workgroup-pr-review on +[Slack](https://www.django-cms.org/slack) to find a “pr review buddy” who is going to review my pull request. From 17db78910efdab31b4a034f413dba9071c49a63e Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Sun, 23 Jan 2022 15:21:36 +0000 Subject: [PATCH 34/66] Only include badges at the top to avoid confusion & removed old badges like travis. (#106) --- README.rst | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index a209a2a7..da6dd5eb 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ django CMS Snippet ================== -|pypi| |build| |coverage| +|pypi| |coverage| |python| |django| |djangocms| **django CMS Snippet** provides a plugin for `django CMS `_ @@ -30,8 +30,8 @@ Contribute to this project and win rewards Because this is a an open-source project, we welcome everyone to `get involved in the project `_ and -`receive a reward `_ for their contribution. -Become part of a fantastic community and help us make django CMS the best CMS in the world. +`receive a reward `_ for their contribution. +Become part of a fantastic community and help us make django CMS the best CMS in the world. We'll be delighted to receive your feedback in the form of issues and pull requests. Before submitting your @@ -50,9 +50,7 @@ Documentation ============= See ``REQUIREMENTS`` in the `setup.py `_ -file for additional dependencies: - -|python| |django| |djangocms| +file for additional dependencies. Installation @@ -126,11 +124,8 @@ You can run tests by executing:: .. |pypi| image:: https://badge.fury.io/py/djangocms-snippet.svg :target: http://badge.fury.io/py/djangocms-snippet -.. |build| image:: https://travis-ci.org/divio/djangocms-snippet.svg?branch=master - :target: https://travis-ci.org/divio/djangocms-snippet -.. |coverage| image:: https://codecov.io/gh/divio/djangocms-snippet/branch/master/graph/badge.svg - :target: https://codecov.io/gh/divio/djangocms-snippet - +.. |coverage| image:: https://codecov.io/gh/django-cms/djangocms-snippet/branch/master/graph/badge.svg + :target: https://codecov.io/gh/django-cms/djangocms-snippet .. |python| image:: https://img.shields.io/badge/python-3.5+-blue.svg :target: https://pypi.org/project/djangocms-snippet/ .. |django| image:: https://img.shields.io/badge/django-2.2,%203.0,%203.1-blue.svg From 893ed0ba1446a54982b0fd28b44faa160ad5d55b Mon Sep 17 00:00:00 2001 From: Mark Walker Date: Sun, 23 Jan 2022 18:36:59 +0000 Subject: [PATCH 35/66] Added pypi github actions (#108) --- .github/workflows/publish-to-live-pypi.yml | 39 +++++++++++++++++++++ .github/workflows/publish-to-test-pypi.yml | 40 ++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 .github/workflows/publish-to-live-pypi.yml create mode 100644 .github/workflows/publish-to-test-pypi.yml diff --git a/.github/workflows/publish-to-live-pypi.yml b/.github/workflows/publish-to-live-pypi.yml new file mode 100644 index 00000000..1607f772 --- /dev/null +++ b/.github/workflows/publish-to-live-pypi.yml @@ -0,0 +1,39 @@ +name: Publish Python 🐍 distributions 📦 to pypi + +on: + release: + types: + - published + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to pypi + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution 📦 to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 00000000..d590f480 --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,40 @@ +name: Publish Python 🐍 distributions 📦 to TestPyPI + +on: + push: + branches: + - master + +jobs: + build-n-publish: + name: Build and publish Python 🐍 distributions 📦 to TestPyPI + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + + - name: Install pypa/build + run: >- + python -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + build + --sdist + --wheel + --outdir dist/ + . + + - name: Publish distribution 📦 to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + skip_existing: true From de97346d46141296dc6c9b5e4efc28fd9c5bc08b Mon Sep 17 00:00:00 2001 From: Simon Krull Date: Sun, 13 Feb 2022 14:01:48 +0100 Subject: [PATCH 36/66] FEATURE: adjust setup metadata (#110) --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de80e82e..360ec30e 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,9 @@ version=__version__, author='Divio AG', author_email='info@divio.ch', - url='https://github.com/divio/djangocms-snippet', + maintainer='Django CMS Association and contributors', + maintainer_email='info@django-cms.org', + url='https://github.com/django-cms/djangocms-snippet', license='BSD-3-Clause', description='Adds snippet plugin to django CMS.', long_description=open('README.rst').read(), From a26c4438c993fdffae7e5c53be6a3df79aa68c6b Mon Sep 17 00:00:00 2001 From: Liumeo Date: Sun, 19 Jun 2022 08:45:35 -0400 Subject: [PATCH 37/66] correct doc about default value of DJANGOCMS_SNIPPET_CACHE (#120) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index da6dd5eb..a324b34d 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,7 @@ to edit the snippet content. You can customize the If dynamic content is inserted (for example ``{% show_menu ... %}``), the plugin cache must be disabled, please set ``DJANGOCMS_SNIPPET_CACHE`` to ``False`` in your settings:: - DJANGOCMS_SNIPPET_CACHE = False # default value is True + DJANGOCMS_SNIPPET_CACHE = False # default value is False Migration 0010 requires the use of a user in order to create versions for existing snippets (if djangocms_versioning is installed and enabled), a user can be chosen with the setting ``DJANGOCMS_SNIPPET_VERSIONING_MIGRATION_USER_ID``, the default is 1. From cbff0226146727148cba33fa608fdf50b2974adb Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Mon, 29 Aug 2022 22:00:59 +0200 Subject: [PATCH 38/66] Update to ace 1.9.6, load ace editor from static files if djangocms_static_ace is installed, add dark mode (#123) Fix: Load ace editor from static files if `djangocms-static-ace` is installed Fix: Respect if user has set dark mode Add: Weak dependency on djangocms-static-ace Doc: optional static-ace dependency --- CHANGELOG.rst | 2 ++ README.rst | 5 +++++ djangocms_snippet/admin.py | 7 +++++++ .../djangocms_snippet/admin/change_form.html | 21 +++++++++++++++---- setup.py | 5 +++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fdfbf9a4..8c04591e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -63,6 +63,8 @@ Unreleased * feat: djangocms-versioning support added, including model restructure and configuration * feat: django-cms v4.0.x support added +* Add support for ace editor loaded from static files through djangocms-static-ace +* Add dark mode support 3.0.0 (2020-09-02) ================== diff --git a/README.rst b/README.rst index a324b34d..a44a0f2f 100644 --- a/README.rst +++ b/README.rst @@ -62,6 +62,11 @@ For a manual install: * add ``djangocms_snippet`` to your ``INSTALLED_APPS`` * run ``python manage.py migrate djangocms_snippet`` +Djangocms-snippet uses the ace code editor which normally is loaded from a CDN. +If you prefer your application to provide the editor locally, you can change +the requirement from `djangocms_snippet` to `djangocms_snippet[static-ace]` and +add `djangocms_static_ace` to your project's `INSTALLED_APPS`. + Configuration ------------- diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index c6e13e53..f0c55536 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -32,6 +32,13 @@ @admin.register(Snippet) class SnippetAdmin(*snippet_admin_classes): + class Media: + js = ( + "admin/vendor/ace/ace.js" + if "djangocms_static_ace" in settings.INSTALLED_APPS + else "https://cdnjs.cloudflare.com/ajax/libs/ace/1.9.6/ace.js", + ) + list_display = ('name',) search_fields = ['name'] change_form_template = 'djangocms_snippet/admin/change_form.html' diff --git a/djangocms_snippet/templates/djangocms_snippet/admin/change_form.html b/djangocms_snippet/templates/djangocms_snippet/admin/change_form.html index 0eebd0ea..152df895 100644 --- a/djangocms_snippet/templates/djangocms_snippet/admin/change_form.html +++ b/djangocms_snippet/templates/djangocms_snippet/admin/change_form.html @@ -3,8 +3,6 @@ {% block object-tools %} {{ block.super }} - -