From 339281e64964c12b6de062d15a4b8f61ef0718ba Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 10 Jan 2022 14:26:18 +0000 Subject: [PATCH 01/13] Preview icon renders content in read only mode --- CHANGELOG.rst | 1 + djangocms_snippet/admin.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a911a881..c35fa0c7 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.dev2 (2021-12-22) ======================= diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index fc7b6e0b..ed4a94b5 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -3,6 +3,7 @@ from django.contrib import admin from django.db import models from django.forms import Textarea +from django.urls import reverse from cms.utils.permissions import get_model_permission_codename @@ -68,5 +69,31 @@ def has_delete_permission(self, request, obj=None): ) return False + def has_change_permission(self, request, obj=None): + return False + + def _get_preview_url(self, obj): + """ + Return the preview method if available, otherwise return None + :return: method or None + """ + change_url = reverse( + "admin:{app}_{model}_change".format( + app=obj._meta.app_label, model=self.model._meta.model_name + ), + args=(obj.pk,), + ) + return change_url + + def get_list_actions(self): + """ + Collect rendered actions from implemented methods and return as list + """ + return [ + self._get_preview_link, + self._get_edit_link, + self._get_manage_versions_link, + ] + admin.site.register(Snippet, SnippetAdmin) From f5f0a55c3519ac9946754c6795d85c0359a5115f Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 10 Jan 2022 14:35:36 +0000 Subject: [PATCH 02/13] Updated method comment --- djangocms_snippet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index ed4a94b5..a1dd87e0 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -74,7 +74,7 @@ def has_change_permission(self, request, obj=None): def _get_preview_url(self, obj): """ - Return the preview method if available, otherwise return None + Return the change url which will be rendered in read only mode :return: method or None """ change_url = reverse( From 59dcb78670227722653be8ce0b2a8fe493212aa5 Mon Sep 17 00:00:00 2001 From: bernard Date: Tue, 11 Jan 2022 14:41:47 +0000 Subject: [PATCH 03/13] Updated permissions check to show snippet in edit mode if owner is logged in --- CHANGELOG.rst | 2 +- djangocms_snippet/admin.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c35fa0c7..0621d33f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog Unreleased ========== -* feat: Preview icon renders form in read only mode +* feat: Preview icon renders form in read only mode when a user does not own snippet 4.0.0.dev2 (2021-12-22) ======================= diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index a1dd87e0..f3b48a00 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -70,6 +70,15 @@ def has_delete_permission(self, request, obj=None): return False def has_change_permission(self, request, obj=None): + """ + Return edit mode if current user is the author, otherwise display snippet in read only mode + """ + if obj and djangocms_versioning_enabled: + created_by_id = self.get_version(obj).created_by_id + logged_in_id = request.user.id + version_state = self.get_version(obj).state + if logged_in_id == created_by_id and version_state != 'published': + return True return False def _get_preview_url(self, obj): From 833a0d1ff1eed61f0f55e510fabbe67eb05353c5 Mon Sep 17 00:00:00 2001 From: bernard Date: Tue, 11 Jan 2022 14:52:11 +0000 Subject: [PATCH 04/13] Removed unused list actions method --- djangocms_snippet/admin.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index f3b48a00..efcd7934 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -94,15 +94,5 @@ def _get_preview_url(self, obj): ) return change_url - def get_list_actions(self): - """ - Collect rendered actions from implemented methods and return as list - """ - return [ - self._get_preview_link, - self._get_edit_link, - self._get_manage_versions_link, - ] - admin.site.register(Snippet, SnippetAdmin) From fa9dbe4dfbca126a7ce7d31db0044905f83761df Mon Sep 17 00:00:00 2001 From: bernard Date: Fri, 14 Jan 2022 15:27:26 +0000 Subject: [PATCH 05/13] Added get paremeter to know when user selected preview --- djangocms_snippet/admin.py | 16 +++++++--------- tests/test_admin.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 150c4125..353326cb 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -98,19 +98,17 @@ def has_delete_permission(self, request, obj=None): def has_change_permission(self, request, obj=None): """ - Return edit mode if current user is the author, otherwise display snippet in read only mode + Return read only mode if q parameter is in URL, otherwise return edit mode """ if obj and djangocms_versioning_enabled: - created_by_id = self.get_version(obj).created_by_id - logged_in_id = request.user.id - version_state = self.get_version(obj).state - if logged_in_id == created_by_id and version_state != 'published': - return True - return False + param = request.GET.get("q") + if param == "read_only": + return False + return True def _get_preview_url(self, obj): """ - Return the change url which will be rendered in read only mode + Return the preview url in read only mode :return: method or None """ change_url = reverse( @@ -119,7 +117,7 @@ def _get_preview_url(self, obj): ), args=(obj.pk,), ) - return change_url + return f"{change_url}?q=read_only" admin.site.register(Snippet, SnippetAdmin) diff --git a/tests/test_admin.py b/tests/test_admin.py index ed121022..367b5f7a 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -173,7 +173,7 @@ def test_admin_form_edit_when_locked(self): 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) + response = self.client.get(edit_url + "?q=read_only") # Check that we are loading in readonly mode self.assertContains(response, '
Test Snippet
') From a9a3d1953ddb226bad48bf84dec5436dfddc0ae6 Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 31 Jan 2022 13:30:20 +0000 Subject: [PATCH 06/13] Added custom preview endpoint to render form in a read only view --- CHANGELOG.rst | 2 +- djangocms_snippet/admin.py | 105 ++++++++++++++++++++++++++----------- djangocms_snippet/views.py | 26 --------- tests/test_admin.py | 2 +- tests/test_views.py | 38 -------------- 5 files changed, 75 insertions(+), 98 deletions(-) delete mode 100644 djangocms_snippet/views.py delete mode 100644 tests/test_views.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f4c4c7f9..334faed5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog Unreleased ========== -* feat: Preview icon renders form in read only mode when a user does not own snippet +* 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 353326cb..7d5bf839 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -1,17 +1,18 @@ from django.conf import settings from django.conf.urls import url from django.contrib import admin +from django.contrib.admin.exceptions import DisallowedModelAdminToField +from django.contrib.admin.utils import (quote, unquote, flatten_fieldsets) +from django.contrib.admin import helpers from django.db import models from django.forms import Textarea -from django.urls import reverse +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 snippet_admin_classes = [admin.ModelAdmin] @@ -19,11 +20,15 @@ try: from djangocms_versioning.admin import ExtendedVersionAdminMixin + if djangocms_versioning_enabled: snippet_admin_classes.insert(0, ExtendedVersionAdminMixin) except ImportError: djangocms_versioning_enabled = False +TO_FIELD_VAR = '_to_field' +IS_POPUP_VAR = '_popup' + class SnippetAdmin(*snippet_admin_classes): list_display = ('name',) @@ -74,15 +79,74 @@ 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 + """ + 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): """ @@ -96,28 +160,5 @@ def has_delete_permission(self, request, obj=None): ) return False - def has_change_permission(self, request, obj=None): - """ - Return read only mode if q parameter is in URL, otherwise return edit mode - """ - if obj and djangocms_versioning_enabled: - param = request.GET.get("q") - if param == "read_only": - return False - return True - - def _get_preview_url(self, obj): - """ - Return the preview url in read only mode - :return: method or None - """ - change_url = reverse( - "admin:{app}_{model}_change".format( - app=obj._meta.app_label, model=self.model._meta.model_name - ), - args=(obj.pk,), - ) - return f"{change_url}?q=read_only" - admin.site.register(Snippet, SnippetAdmin) 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 367b5f7a..ed121022 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -173,7 +173,7 @@ def test_admin_form_edit_when_locked(self): 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 + "?q=read_only") + response = self.client.get(edit_url) # Check that we are loading in readonly mode self.assertContains(response, '
Test Snippet
') 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 162156811a158fde091e8156a96c4f2f8c8a8bd5 Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 31 Jan 2022 13:39:03 +0000 Subject: [PATCH 07/13] isort and flake errors --- djangocms_snippet/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 7d5bf839..cc797f1c 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -1,9 +1,9 @@ from django.conf import settings from django.conf.urls import url from django.contrib import admin -from django.contrib.admin.exceptions import DisallowedModelAdminToField -from django.contrib.admin.utils import (quote, unquote, flatten_fieldsets) from django.contrib.admin import helpers +from django.contrib.admin.exceptions import DisallowedModelAdminToField +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 837b19ad4c975ed06f062ee59cc157bd46c78dbc Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 31 Jan 2022 13:40:29 +0000 Subject: [PATCH 08/13] isort spacing --- djangocms_snippet/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index cc797f1c..01068124 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -14,6 +14,7 @@ from .forms import SnippetForm from .models import Snippet + # Use the version mixin if djangocms-versioning is installed and enabled snippet_admin_classes = [admin.ModelAdmin] djangocms_versioning_enabled = SnippetCMSAppConfig.djangocms_versioning_enabled From dbc49a69d0381fe1c20d5dfef26dc020bac65cf6 Mon Sep 17 00:00:00 2001 From: bernard Date: Mon, 31 Jan 2022 14:03:13 +0000 Subject: [PATCH 09/13] Added reference to django for the solution --- djangocms_snippet/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 01068124..9a44619b 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -83,6 +83,8 @@ def get_list_display_links(self, request, list_display): 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/main/django/contrib/admin/options.py#L1553 """ to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) From 1377bb916f160a55b6b86518fb4449041a3bf5f8 Mon Sep 17 00:00:00 2001 From: bernard Date: Thu, 3 Feb 2022 14:23:19 +0000 Subject: [PATCH 10/13] Added a test to ensure preview endpoint fields are rendered read only --- djangocms_snippet/admin.py | 16 +++++++--------- tests/test_admin.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 9a44619b..92cdd28e 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -3,6 +3,7 @@ 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 @@ -27,9 +28,6 @@ except ImportError: djangocms_versioning_enabled = False -TO_FIELD_VAR = '_to_field' -IS_POPUP_VAR = '_popup' - class SnippetAdmin(*snippet_admin_classes): list_display = ('name',) @@ -144,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( - r"^(?P\d+)/preview/$", - self.admin_site.admin_view(self.preview_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/tests/test_admin.py b/tests/test_admin.py index ed121022..62621264 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -4,6 +4,7 @@ from django.shortcuts import reverse from django.test import RequestFactory, override_settings +from cms.utils.urlutils import admin_reverse from cms.test_utils.testcases import CMSTestCase from djangocms_versioning.models import Version @@ -13,6 +14,8 @@ from djangocms_snippet.forms import SnippetForm from djangocms_snippet.models import Snippet, SnippetGrouper +from .utils.factories import SnippetWithVersionFactory + class SnippetAdminTestCase(CMSTestCase): def setUp(self): @@ -209,3 +212,23 @@ 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, '
') + + From d12521eb7557b67775981ee698e39ed9515710af Mon Sep 17 00:00:00 2001 From: bernard Date: Thu, 3 Feb 2022 14:25:37 +0000 Subject: [PATCH 11/13] Removed unused imports --- tests/test_admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_admin.py b/tests/test_admin.py index 62621264..448bc504 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -4,7 +4,6 @@ from django.shortcuts import reverse from django.test import RequestFactory, override_settings -from cms.utils.urlutils import admin_reverse from cms.test_utils.testcases import CMSTestCase from djangocms_versioning.models import Version @@ -14,8 +13,6 @@ from djangocms_snippet.forms import SnippetForm from djangocms_snippet.models import Snippet, SnippetGrouper -from .utils.factories import SnippetWithVersionFactory - class SnippetAdminTestCase(CMSTestCase): def setUp(self): From 8decb3a93493eaed47d9f391b66a06606d0dd99d Mon Sep 17 00:00:00 2001 From: bernard Date: Thu, 3 Feb 2022 14:26:34 +0000 Subject: [PATCH 12/13] Removed extra blank lines --- tests/test_admin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_admin.py b/tests/test_admin.py index 448bc504..d03d2358 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -227,5 +227,3 @@ def test_preview_renders_read_only_fields(self): self.assertContains(response, '
<h1>This is a test</h1>
') # Snippet template self.assertContains(response, '
') - - From 0c47c8094dbccd07486b3aa717dc9b291164c021 Mon Sep 17 00:00:00 2001 From: Bernard Van Der Vyver Date: Thu, 3 Feb 2022 14:40:26 +0000 Subject: [PATCH 13/13] Update djangocms_snippet/admin.py Co-authored-by: Aiky30 --- djangocms_snippet/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocms_snippet/admin.py b/djangocms_snippet/admin.py index 92cdd28e..6769d9ce 100644 --- a/djangocms_snippet/admin.py +++ b/djangocms_snippet/admin.py @@ -82,7 +82,7 @@ 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/main/django/contrib/admin/options.py#L1553 + 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))