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