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