import datetime import json from io import StringIO from unittest import mock from django.conf import settings from django.contrib.admin.utils import quote from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, Permission from django.contrib.contenttypes.models import ContentType from django.core import checks, management from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.core.handlers.wsgi import WSGIRequest from django.http import HttpRequest, HttpResponse from django.test import RequestFactory, TestCase, TransactionTestCase from django.test.utils import override_settings from django.urls import reverse from django.utils.timezone import make_aware, now from freezegun import freeze_time from taggit.models import Tag from wagtail import hooks from wagtail.admin.admin_url_finder import AdminURLFinder from wagtail.admin.forms import WagtailAdminModelForm from wagtail.admin.menu import admin_menu from wagtail.admin.panels import FieldPanel, ObjectList, get_edit_handler from wagtail.admin.widgets.button import ButtonWithDropdown from wagtail.blocks.field_block import FieldBlockAdapter from wagtail.coreutils import get_dummy_request from wagtail.models import Locale, ModelLogEntry, Revision from wagtail.signals import published, unpublished from wagtail.snippets.action_menu import ( ActionMenuItem, get_base_snippet_action_menu_items, ) from wagtail.snippets.blocks import SnippetChooserBlock from wagtail.snippets.models import SNIPPET_MODELS, register_snippet from wagtail.snippets.widgets import ( AdminSnippetChooser, SnippetChooserAdapter, SnippetListingButton, ) from wagtail.test.snippets.forms import FancySnippetForm from wagtail.test.snippets.models import ( AlphaSnippet, FancySnippet, FileUploadSnippet, NonAutocompleteSearchableSnippet, RegisterDecorator, RegisterFunction, SearchableSnippet, StandardSnippet, StandardSnippetWithCustomPrimaryKey, TranslatableSnippet, ZuluSnippet, ) from wagtail.test.testapp.models import ( Advert, AdvertWithCustomPrimaryKey, AdvertWithCustomUUIDPrimaryKey, AdvertWithTabbedInterface, DraftStateCustomPrimaryKeyModel, DraftStateModel, FullFeaturedSnippet, MultiPreviewModesModel, RevisableChildModel, RevisableModel, SnippetChooserModel, SnippetChooserModelWithCustomPrimaryKey, VariousOnDeleteModel, ) from wagtail.test.utils import WagtailTestUtils from wagtail.test.utils.template_tests import AdminTemplateTestUtils from wagtail.test.utils.timestamps import submittable_timestamp from wagtail.utils.deprecation import RemovedInWagtail70Warning from wagtail.utils.timestamps import render_timestamp class TestSnippetIndexView(AdminTemplateTestUtils, WagtailTestUtils, TestCase): def setUp(self): self.user = self.login() def get(self, params={}): return self.client.get(reverse("wagtailsnippets:index"), params) def test_get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.get() self.assertEqual(response.status_code, 302) def test_get_with_only_view_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ), Permission.objects.get( content_type__app_label="tests", codename="view_advert" ), ) self.user.save() response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/listing.html") soup = self.get_soup(response.content) link = soup.select_one("tr td a") self.assertEqual(link["href"], reverse("wagtailsnippets_tests_advert:list")) self.assertEqual(link.text.strip(), "Adverts") def test_simple(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/listing.html") self.assertBreadcrumbsItemsRendered( [{"url": "", "label": "Snippets"}], response.content, ) # Now that it uses the generic template, # it should not contain the locale selector self.assertNotContains(response, "data-locale-selector") def test_displays_snippet(self): self.assertContains(self.get(), "Adverts") def test_snippets_menu_item_shown_with_only_view_permission(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ), Permission.objects.get( content_type__app_label="tests", codename="view_advert" ), ) self.user.save() request = get_dummy_request() request.user = self.user menu_items = admin_menu.menu_items_for_request(request) snippets = [item for item in menu_items if item.name == "snippets"] self.assertEqual(len(snippets), 1) item = snippets[0] self.assertEqual(item.name, "snippets") self.assertEqual(item.label, "Snippets") self.assertEqual(item.icon_name, "snippet") self.assertEqual(item.url, reverse("wagtailsnippets:index")) class TestSnippetListView(WagtailTestUtils, TestCase): def setUp(self): self.login() user_model = get_user_model() self.user = user_model.objects.get() def get(self, params={}): return self.client.get(reverse("wagtailsnippets_tests_advert:list"), params) def test_simple(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html") self.assertEqual(response.context["header_icon"], "snippet") def get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.get() self.assertEqual(response.status_code, 302) def get_with_edit_permission_only(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ), Permission.objects.get( content_type__app_label="tests", codename="change_advert" ), ) self.user.save() response = self.get() self.assertEqual(response.status_code, 200) self.assertContains( response, "

There are no adverts to display.

", html=True, ) self.assertNotContains(response, reverse("wagtailsnippets_tests_advert:add")) def test_ordering(self): """ Listing should be ordered descending by PK if no ordering has been set on the model """ for i in range(1, 11): Advert.objects.create(pk=i, text="advert %d" % i) response = self.get() self.assertEqual(response.status_code, 200) self.assertEqual(response.context["page_obj"][0].text, "advert 10") def test_simple_pagination(self): # page numbers in range should be accepted response = self.get({"p": 1}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html") # page numbers out of range should return 404 response = self.get({"p": 9999}) self.assertEqual(response.status_code, 404) def test_displays_add_button(self): self.assertContains(self.get(), "Add advert") def test_not_searchable(self): self.assertFalse(self.get().context["is_searchable"]) def test_register_snippet_listing_buttons_hook(self): advert = Advert.objects.create(text="My Lovely advert") def snippet_listing_buttons(snippet, user, next_url=None): self.assertEqual(snippet, advert) self.assertEqual(user, self.user) self.assertEqual(next_url, reverse("wagtailsnippets_tests_advert:list")) yield SnippetListingButton( "Another useless snippet listing button", "/custom-url", priority=10 ) with hooks.register_temporarily( "register_snippet_listing_buttons", snippet_listing_buttons ): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") soup = self.get_soup(response.content) actions = soup.select_one("tbody tr td ul.actions") top_level_custom_button = actions.select_one("li > a[href='/custom-url']") self.assertIsNone(top_level_custom_button) custom_button = actions.select_one( "li [data-controller='w-dropdown'] a[href='/custom-url']" ) self.assertIsNotNone(custom_button) self.assertEqual( custom_button.text.strip(), "Another useless snippet listing button", ) def test_register_snippet_listing_buttons_hook_with_dropdown(self): advert = Advert.objects.create(text="My Lovely advert") def snippet_listing_buttons(snippet, user, next_url=None): self.assertEqual(snippet, advert) self.assertEqual(user, self.user) self.assertEqual(next_url, reverse("wagtailsnippets_tests_advert:list")) yield ButtonWithDropdown( label="Moar pls!", buttons=[SnippetListingButton("Alrighty", "/cheers", priority=10)], ) with hooks.register_temporarily( "register_snippet_listing_buttons", snippet_listing_buttons ): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") soup = self.get_soup(response.content) actions = soup.select_one("tbody tr td ul.actions") nested_dropdown = actions.select_one( "li [data-controller='w-dropdown'] [data-controller='w-dropdown']" ) self.assertIsNone(nested_dropdown) dropdown_buttons = actions.select("li > [data-controller='w-dropdown']") # Default "More" button and the custom "Moar pls!" button self.assertEqual(len(dropdown_buttons), 2) custom_dropdown = None for button in dropdown_buttons: if "Moar pls!" in button.text.strip(): custom_dropdown = button self.assertIsNotNone(custom_dropdown) self.assertEqual(custom_dropdown.select_one("button").text.strip(), "Moar pls!") # Should contain the custom button inside the custom dropdown custom_button = custom_dropdown.find("a", attrs={"href": "/cheers"}) self.assertIsNotNone(custom_button) self.assertEqual(custom_button.text.strip(), "Alrighty") def test_construct_snippet_listing_buttons_hook(self): Advert.objects.create(text="My Lovely advert") # testapp implements a construct_snippet_listing_buttons hook # that adds a dummy button with the label 'Dummy Button' which points # to '/dummy-button' and is placed inside the default "More" dropdown button response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") soup = self.get_soup(response.content) dropdowns = soup.select( "tbody tr td ul.actions > li > [data-controller='w-dropdown']" ) self.assertEqual(len(dropdowns), 1) more_dropdown = dropdowns[0] dummy_button = more_dropdown.find("a", attrs={"href": "/dummy-button"}) self.assertIsNotNone(dummy_button) self.assertEqual(dummy_button.text.strip(), "Dummy Button") def test_construct_snippet_listing_buttons_hook_contains_default_buttons(self): advert = Advert.objects.create(text="My Lovely advert") delete_url = reverse( "wagtailsnippets_tests_advert:delete", args=[quote(advert.pk)] ) def hide_delete_button_for_lovely_advert(buttons, snippet, user): # Edit, delete, dummy button, copy button self.assertEqual(len(buttons), 4) buttons[:] = [button for button in buttons if button.url != delete_url] self.assertEqual(len(buttons), 3) with hooks.register_temporarily( "construct_snippet_listing_buttons", hide_delete_button_for_lovely_advert, ): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") self.assertNotContains(response, delete_url) def test_construct_snippet_listing_buttons_hook_deprecated_context(self): advert = Advert.objects.create(text="My Lovely advert") def register_snippet_listing_button_item(buttons, snippet, user, context): self.assertEqual(snippet, advert) self.assertEqual(user, self.user) self.assertEqual(context, {}) with hooks.register_temporarily( "construct_snippet_listing_buttons", register_snippet_listing_button_item, ), self.assertWarnsMessage( RemovedInWagtail70Warning, "construct_snippet_listing_buttons hook no longer accepts a context argument", ): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/shared/buttons.html") def test_dropdown_not_rendered_when_no_child_buttons_exist(self): Advert.objects.create(text="My Lovely advert") def remove_all_buttons(buttons, snippet, user): buttons[:] = [] self.assertEqual(len(buttons), 0) with hooks.register_temporarily( "construct_snippet_listing_buttons", remove_all_buttons, ): response = self.get() soup = self.get_soup(response.content) actions = soup.select_one("tbody tr td ul.actions") self.assertIsNone(actions) def test_use_latest_draft_as_title(self): snippet = DraftStateModel.objects.create(text="Draft-enabled Foo, Published") snippet.save_revision().publish() snippet.text = "Draft-enabled Bar, In Draft" snippet.save_revision() response = self.client.get( reverse("wagtailsnippets_tests_draftstatemodel:list"), ) edit_url = reverse( "wagtailsnippets_tests_draftstatemodel:edit", args=[quote(snippet.pk)], ) # Should use the latest draft title in the listing self.assertContains( response, f""" Draft-enabled Bar, In Draft """, html=True, ) @override_settings(WAGTAIL_I18N_ENABLED=True) class TestLocaleSelectorOnList(WagtailTestUtils, TestCase): def setUp(self): self.fr_locale = Locale.objects.create(language_code="fr") self.user = self.login() @override_settings( WAGTAIL_CONTENT_LANGUAGES=[ ("ar", "Arabic"), ("en", "English"), ("fr", "French"), ] ) def test_locale_selector(self): response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:list") ) soup = self.get_soup(response.content) # Should only show languages that also have the corresponding Locale # (the Arabic locale is not created in the setup, so it should not be shown) arabic_input = soup.select_one('input[name="locale"][value="ar"]') self.assertIsNone(arabic_input) french_input = soup.select_one('input[name="locale"][value="fr"]') self.assertIsNotNone(french_input) # Check that the add URLs include the locale add_url = ( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=en" ) add_buttons = soup.select(f'a[href="{add_url}"]') self.assertEqual(len(add_buttons), 2) self.assertContains( response, f"""

There are no translatable snippets to display. Why not add one?

""", html=True, ) def test_no_locale_filter_when_only_one_locale(self): self.fr_locale.delete() response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:list") ) soup = self.get_soup(response.content) locale_input = soup.select_one('input[name="locale"]') self.assertIsNone(locale_input) # The viewset has no other filters configured, # so the filters drilldown should not be present filters_drilldown = soup.select_one("#filters-drilldown") self.assertIsNone(filters_drilldown) @override_settings(WAGTAIL_I18N_ENABLED=False) def test_locale_selector_not_present_when_i18n_disabled(self): response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:list") ) soup = self.get_soup(response.content) input_element = soup.select_one('input[name="locale"]') self.assertIsNone(input_element) # Check that the add URLs don't include the locale add_url = reverse("wagtailsnippets_snippetstests_translatablesnippet:add") soup = self.get_soup(response.content) add_buttons = soup.select(f'a[href="{add_url}"]') self.assertEqual(len(add_buttons), 2) self.assertContains( response, f"""

There are no translatable snippets to display. Why not add one?

""", html=True, ) def test_locale_selector_not_present_on_non_translatable_snippet(self): response = self.client.get(reverse("wagtailsnippets_tests_advert:list")) soup = self.get_soup(response.content) input_element = soup.select_one('input[name="locale"]') self.assertIsNone(input_element) # Check that the add URLs don't include the locale add_url = reverse("wagtailsnippets_tests_advert:add") soup = self.get_soup(response.content) add_buttons = soup.select(f'a[href="{add_url}"]') self.assertEqual(len(add_buttons), 2) self.assertContains( response, f"""

There are no adverts to display. Why not add one?

""", html=True, ) class TestModelOrdering(WagtailTestUtils, TestCase): def setUp(self): for i in range(1, 10): AdvertWithTabbedInterface.objects.create(text="advert %d" % i) AdvertWithTabbedInterface.objects.create(text="aaaadvert") self.login() def test_listing_respects_model_ordering(self): response = self.client.get( reverse("wagtailsnippets_tests_advertwithtabbedinterface:list") ) self.assertEqual(response.status_code, 200) self.assertEqual(response.context["page_obj"][0].text, "aaaadvert") def test_chooser_respects_model_ordering(self): response = self.client.get( reverse("wagtailsnippetchoosers_tests_advertwithtabbedinterface:choose") ) self.assertEqual(response.status_code, 200) self.assertEqual(response.context["results"][0].text, "aaaadvert") class TestListViewOrdering(WagtailTestUtils, TestCase): @classmethod def setUpTestData(cls): for i in range(1, 10): advert = Advert.objects.create(text=f"{i*'a'}dvert {i}") draft = DraftStateModel.objects.create(text=f"{i*'d'}raft {i}", live=False) if i % 2 == 0: ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(Advert), label="Test Advert", action="wagtail.create", timestamp=now(), object_id=advert.pk, ) draft.save_revision().publish() def setUp(self): self.login() def test_listing_orderable_columns_with_no_mixin(self): list_url = reverse("wagtailsnippets_tests_advert:list") response = self.client.get(list_url) sort_updated_url = list_url + "?ordering=_updated_at" sort_live_url = list_url + "?ordering=live" self.assertEqual(response.status_code, 200) # Should use the tables framework self.assertTemplateUsed(response, "wagtailadmin/tables/table.html") # The Updated column header should be a link with the correct query param self.assertContains( response, f'Updated', html=True, ) # Should not contain the Status column header self.assertNotContains( response, f'Status', html=True, ) def test_listing_orderable_columns_with_draft_state_mixin(self): list_url = reverse("wagtailsnippets_tests_draftstatemodel:list") response = self.client.get(list_url) sort_updated_url = list_url + "?ordering=_updated_at" sort_live_url = list_url + "?ordering=live" self.assertEqual(response.status_code, 200) # Should use the tables framework self.assertTemplateUsed(response, "wagtailadmin/tables/table.html") # The Updated column header should be a link with the correct query param self.assertContains( response, f'Updated', html=True, ) # The Status column header should be a link with the correct query param self.assertContains( response, f'Status', html=True, ) def test_order_by_updated_at_with_no_mixin(self): list_url = reverse("wagtailsnippets_tests_advert:list") response = self.client.get(list_url + "?ordering=_updated_at") self.assertEqual(response.status_code, 200) # With ascending order, empty updated_at information should be shown first self.assertIsNone(response.context["page_obj"][0]._updated_at) # The most recently updated should be at the bottom self.assertEqual(response.context["page_obj"][-1].text, "aaaaaaaadvert 8") self.assertIsNotNone(response.context["page_obj"][-1]._updated_at) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=-_updated_at") response = self.client.get(list_url + "?ordering=-_updated_at") self.assertEqual(response.status_code, 200) # With descending order, the first object should be the one that was last updated self.assertEqual(response.context["page_obj"][0].text, "aaaaaaaadvert 8") self.assertIsNotNone(response.context["page_obj"][0]._updated_at) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=_updated_at") def test_order_by_updated_at_with_draft_state_mixin(self): list_url = reverse("wagtailsnippets_tests_draftstatemodel:list") response = self.client.get(list_url + "?ordering=_updated_at") self.assertEqual(response.status_code, 200) # With ascending order, empty updated_at information should be shown first self.assertIsNone(response.context["page_obj"][0]._updated_at) # The most recently updated should be at the bottom self.assertEqual(response.context["page_obj"][-1].text, "ddddddddraft 8") self.assertIsNotNone(response.context["page_obj"][-1]._updated_at) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=-_updated_at") response = self.client.get(list_url + "?ordering=-_updated_at") self.assertEqual(response.status_code, 200) # With descending order, the first object should be the one that was last updated self.assertEqual(response.context["page_obj"][0].text, "ddddddddraft 8") self.assertIsNotNone(response.context["page_obj"][0]._updated_at) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=_updated_at") def test_order_by_live(self): list_url = reverse("wagtailsnippets_tests_draftstatemodel:list") response = self.client.get(list_url + "?ordering=live") self.assertEqual(response.status_code, 200) # With ascending order, live=False should be shown first self.assertFalse(response.context["page_obj"][0].live) # The last one should be live=True self.assertTrue(response.context["page_obj"][-1].live) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=-live") response = self.client.get(list_url + "?ordering=-live") self.assertEqual(response.status_code, 200) # With descending order, live=True should be shown first self.assertTrue(response.context["page_obj"][0].live) # The last one should be live=False self.assertFalse(response.context["page_obj"][-1].live) # Should contain a link to reverse the order self.assertContains(response, list_url + "?ordering=live") class TestSnippetListViewWithSearchableSnippet(WagtailTestUtils, TransactionTestCase): def setUp(self): self.login() # Create some instances of the searchable snippet for testing self.snippet_a = SearchableSnippet.objects.create(text="Hello") self.snippet_b = SearchableSnippet.objects.create(text="World") self.snippet_c = SearchableSnippet.objects.create(text="Hello World") def get(self, params={}): return self.client.get( reverse("wagtailsnippets_snippetstests_searchablesnippet:list"), params, ) def test_simple(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html") # All snippets should be in items items = list(response.context["page_obj"].object_list) self.assertIn(self.snippet_a, items) self.assertIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) # The search box should not raise an error self.assertNotContains(response, "This field is required.") def test_empty_q(self): response = self.get({"q": ""}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html") # All snippets should be in items items = list(response.context["page_obj"].object_list) self.assertIn(self.snippet_a, items) self.assertIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) # The search box should not raise an error self.assertNotContains(response, "This field is required.") def test_is_searchable(self): self.assertTrue(self.get().context["is_searchable"]) def test_search_hello(self): response = self.get({"q": "Hello"}) # Just snippets with "Hello" should be in items items = list(response.context["page_obj"].object_list) self.assertIn(self.snippet_a, items) self.assertNotIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) def test_search_world_autocomplete(self): response = self.get({"q": "wor"}) # Just snippets with "World" should be in items items = list(response.context["page_obj"].object_list) self.assertNotIn(self.snippet_a, items) self.assertIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) class TestSnippetListViewWithNonAutocompleteSearchableSnippet( WagtailTestUtils, TransactionTestCase ): """ Test that searchable snippets with no AutocompleteFields defined can still be searched using full words """ def setUp(self): self.login() # Create some instances of the searchable snippet for testing self.snippet_a = NonAutocompleteSearchableSnippet.objects.create(text="Hello") self.snippet_b = NonAutocompleteSearchableSnippet.objects.create(text="World") self.snippet_c = NonAutocompleteSearchableSnippet.objects.create( text="Hello World" ) def get(self, params={}): return self.client.get( reverse( "wagtailsnippets_snippetstests_nonautocompletesearchablesnippet:list" ), params, ) def test_search_hello(self): with self.assertWarnsRegex( RuntimeWarning, "does not specify any AutocompleteFields" ): response = self.get({"q": "Hello"}) # Just snippets with "Hello" should be in items items = list(response.context["page_obj"].object_list) self.assertIn(self.snippet_a, items) self.assertNotIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) class TestSnippetCreateView(WagtailTestUtils, TestCase): def setUp(self): self.user = self.login() def get(self, params={}, model=Advert): return self.client.get( reverse(model.snippet_viewset.get_url_name("add")), params ) def post(self, post_data={}, model=Advert): return self.client.post( reverse(model.snippet_viewset.get_url_name("add")), post_data ) def test_get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.get() self.assertEqual(response.status_code, 302) def test_simple(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html") self.assertNotContains(response, 'role="tablist"', html=True) def test_snippet_with_tabbed_interface(self): response = self.client.get( reverse("wagtailsnippets_tests_advertwithtabbedinterface:add") ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html") self.assertContains(response, 'role="tablist"') self.assertContains( response, '', ) self.assertContains( response, '', ) self.assertContains(response, "Other panels help text") self.assertContains(response, "Top-level help text") def test_create_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.post( post_data={"text": "test text", "url": "http://www.example.com/"} ) self.assertEqual(response.status_code, 302) def test_create_invalid(self): response = self.post(post_data={"foo": "bar"}) self.assertContains(response, "The advert could not be created due to errors.") self.assertContains(response, "error-message", count=1) self.assertContains(response, "This field is required", count=1) def test_create(self): response = self.post( post_data={"text": "test_advert", "url": "http://www.example.com/"} ) self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list")) snippets = Advert.objects.filter(text="test_advert") self.assertEqual(snippets.count(), 1) self.assertEqual(snippets.first().url, "http://www.example.com/") def test_create_with_tags(self): tags = ["hello", "world"] response = self.post( post_data={ "text": "test_advert", "url": "http://example.com/", "tags": ", ".join(tags), } ) self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list")) snippet = Advert.objects.get(text="test_advert") expected_tags = list(Tag.objects.order_by("name").filter(name__in=tags)) self.assertEqual(len(expected_tags), 2) self.assertEqual(list(snippet.tags.order_by("name")), expected_tags) def test_create_file_upload_multipart(self): response = self.get(model=FileUploadSnippet) self.assertContains(response, 'enctype="multipart/form-data"') response = self.post( model=FileUploadSnippet, post_data={"file": SimpleUploadedFile("test.txt", b"Uploaded file")}, ) self.assertRedirects( response, reverse("wagtailsnippets_snippetstests_fileuploadsnippet:list"), ) snippet = FileUploadSnippet.objects.get() self.assertEqual(snippet.file.read(), b"Uploaded file") def test_create_with_revision(self): response = self.post( model=RevisableModel, post_data={"text": "create_revisable"} ) self.assertRedirects( response, reverse("wagtailsnippets_tests_revisablemodel:list") ) snippets = RevisableModel.objects.filter(text="create_revisable") snippet = snippets.first() self.assertEqual(snippets.count(), 1) # The revision should be created revisions = snippet.revisions revision = revisions.first() self.assertEqual(revisions.count(), 1) self.assertEqual(revision.content["text"], "create_revisable") # The log entry should have the revision attached log_entries = ModelLogEntry.objects.for_instance(snippet).filter( action="wagtail.create" ) self.assertEqual(log_entries.count(), 1) self.assertEqual(log_entries.first().revision, revision) def test_before_create_snippet_hook_get(self): def hook_func(request, model): self.assertIsInstance(request, HttpRequest) self.assertEqual(model, Advert) return HttpResponse("Overridden!") with self.register_hook("before_create_snippet", hook_func): response = self.get() self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") def test_before_create_snippet_hook_post(self): def hook_func(request, model): self.assertIsInstance(request, HttpRequest) self.assertEqual(model, Advert) return HttpResponse("Overridden!") with self.register_hook("before_create_snippet", hook_func): post_data = {"text": "Hook test", "url": "http://www.example.com/"} response = self.post(post_data=post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted before advert was created self.assertFalse(Advert.objects.exists()) def test_after_create_snippet_hook(self): def hook_func(request, instance): self.assertIsInstance(request, HttpRequest) self.assertEqual(instance.text, "Hook test") self.assertEqual(instance.url, "http://www.example.com/") return HttpResponse("Overridden!") with self.register_hook("after_create_snippet", hook_func): post_data = {"text": "Hook test", "url": "http://www.example.com/"} response = self.post(post_data=post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted after advert was created self.assertTrue(Advert.objects.exists()) def test_register_snippet_action_menu_item(self): class TestSnippetActionMenuItem(ActionMenuItem): label = "Test" name = "test" icon_name = "check" classname = "action-secondary" def is_shown(self, context): return True def hook_func(model): return TestSnippetActionMenuItem(order=0) with self.register_hook("register_snippet_action_menu_item", hook_func): get_base_snippet_action_menu_items.cache_clear() response = self.get() get_base_snippet_action_menu_items.cache_clear() self.assertContains( response, '', html=True, ) def test_register_snippet_action_menu_item_as_none(self): def hook_func(model): return None with self.register_hook("register_snippet_action_menu_item", hook_func): get_base_snippet_action_menu_items.cache_clear() response = self.get() get_base_snippet_action_menu_items.cache_clear() self.assertEqual(response.status_code, 200) def test_construct_snippet_action_menu(self): class TestSnippetActionMenuItem(ActionMenuItem): label = "Test" name = "test" icon_name = "check" classname = "action-secondary" def is_shown(self, context): return True def hook_func(menu_items, request, context): self.assertIsInstance(menu_items, list) self.assertIsInstance(request, WSGIRequest) self.assertEqual(context["view"], "create") self.assertEqual(context["model"], Advert) # Replace save menu item menu_items[:] = [TestSnippetActionMenuItem(order=0)] with self.register_hook("construct_snippet_action_menu", hook_func): response = self.get() self.assertContains( response, '', html=True, ) self.assertNotContains(response, "'Save'") class TestSnippetCopyView(WagtailTestUtils, TestCase): def setUp(self): self.snippet = StandardSnippet.objects.create(text="Test snippet") self.url = reverse( StandardSnippet.snippet_viewset.get_url_name("copy"), args=(self.snippet.pk,), ) self.user = self.login() def test_without_permission(self): self.user.is_superuser = False self.user.save() admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) self.user.user_permissions.add(admin_permission) response = self.client.get(self.url) self.assertEqual(response.status_code, 302) self.assertRedirects(response, reverse("wagtailadmin_home")) def test_form_is_prefilled(self): response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html") # Ensure form is prefilled soup = self.get_soup(response.content) text_input = soup.select_one('input[name="text"]') self.assertEqual(text_input.attrs.get("value"), "Test snippet") @override_settings(WAGTAIL_I18N_ENABLED=True) class TestLocaleSelectorOnCreate(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.fr_locale = Locale.objects.create(language_code="fr") self.user = self.login() def test_locale_selector(self): response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") ) self.assertContains(response, "Switch locales") switch_to_french_url = ( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=fr" ) self.assertContains( response, f'', ) def test_locale_selector_with_existing_locale(self): response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=fr" ) self.assertContains(response, "Switch locales") switch_to_english_url = ( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=en" ) self.assertContains( response, f'', ) @override_settings(WAGTAIL_I18N_ENABLED=False) def test_locale_selector_not_present_when_i18n_disabled(self): response = self.client.get( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") ) self.assertNotContains(response, "Switch locales") switch_to_french_url = ( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=fr" ) self.assertNotContains( response, f'', ) def test_locale_selector_not_present_on_non_translatable_snippet(self): response = self.client.get(reverse("wagtailsnippets_tests_advert:add")) self.assertNotContains(response, "Switch locales") switch_to_french_url = ( reverse("wagtailsnippets_snippetstests_translatablesnippet:add") + "?locale=fr" ) self.assertNotContains( response, f'', ) class TestCreateDraftStateSnippet(WagtailTestUtils, TestCase): STATUS_TOGGLE_BADGE_REGEX = ( r'data-side-panel-toggle="status"[^<]+]+w-bg-critical-200[^>]+>\s*%(num_errors)s\s*" ) def setUp(self): self.user = self.login() def get(self): return self.client.get(reverse("wagtailsnippets_tests_draftstatemodel:add")) def post(self, post_data={}): return self.client.post( reverse("wagtailsnippets_tests_draftstatemodel:add"), post_data, ) def test_get(self): add_url = reverse("wagtailsnippets_tests_draftstatemodel:add") response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/create.html") # The save button should be labelled "Save draft" self.assertContains(response, "Save draft") # The publish button should exist self.assertContains(response, "Publish") # The publish button should have name="action-publish" self.assertContains( response, '
', html, count=1, allow_extra_attrs=True, ) self.assertTagInHTML( '
', html, count=1, allow_extra_attrs=True, ) # Should show the correct subtitle in the dialog self.assertContains( response, "Choose when this draft state model should go live and/or expire" ) # Should not show the Unpublish action menu item unpublish_url = "/admin/snippets/tests/draftstatemodel/unpublish/" self.assertNotContains(response, unpublish_url) self.assertNotContains(response, "Unpublish") def test_save_draft(self): response = self.post(post_data={"text": "Draft-enabled Foo"}) snippet = DraftStateModel.objects.get(text="Draft-enabled Foo") self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatemodel:edit", args=[snippet.pk]), ) # The instance should be created self.assertEqual(snippet.text, "Draft-enabled Foo") # The instance should be a draft self.assertFalse(snippet.live) self.assertTrue(snippet.has_unpublished_changes) self.assertIsNone(snippet.first_published_at) self.assertIsNone(snippet.last_published_at) self.assertIsNone(snippet.live_revision) # A revision should be created and set as latest_revision self.assertIsNotNone(snippet.latest_revision) # The revision content should contain the data self.assertEqual(snippet.latest_revision.content["text"], "Draft-enabled Foo") def test_publish(self): # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: timestamp = now() with freeze_time(timestamp): response = self.post( post_data={ "text": "Draft-enabled Foo, Published", "action-publish": "action-publish", } ) snippet = DraftStateModel.objects.get(text="Draft-enabled Foo, Published") self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatemodel:list") ) # The instance should be created self.assertEqual(snippet.text, "Draft-enabled Foo, Published") # The instance should be live self.assertTrue(snippet.live) self.assertFalse(snippet.has_unpublished_changes) self.assertEqual(snippet.first_published_at, timestamp) self.assertEqual(snippet.last_published_at, timestamp) # A revision should be created and set as both latest_revision and live_revision self.assertIsNotNone(snippet.live_revision) self.assertEqual(snippet.live_revision, snippet.latest_revision) # The revision content should contain the new data self.assertEqual( snippet.live_revision.content["text"], "Draft-enabled Foo, Published", ) # Check that the published signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateModel) self.assertEqual(mock_call["instance"], snippet) self.assertIsInstance(mock_call["instance"], DraftStateModel) finally: published.disconnect(mock_handler) def test_publish_bad_permissions(self): # Only add create and edit permission self.user.is_superuser = False add_permission = Permission.objects.get( content_type__app_label="tests", codename="add_draftstatemodel", ) edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatemodel", ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin", ) self.user.user_permissions.add( add_permission, edit_permission, admin_permission, ) self.user.save() # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: response = self.post( post_data={ "text": "Draft-enabled Foo", "action-publish": "action-publish", } ) snippet = DraftStateModel.objects.get(text="Draft-enabled Foo") # Should be taken to the edit page self.assertRedirects( response, reverse( "wagtailsnippets_tests_draftstatemodel:edit", args=[snippet.pk], ), ) # The instance should still be created self.assertEqual(snippet.text, "Draft-enabled Foo") # The instance should not be live self.assertFalse(snippet.live) self.assertTrue(snippet.has_unpublished_changes) # A revision should be created and set as latest_revision, but not live_revision self.assertIsNotNone(snippet.latest_revision) self.assertIsNone(snippet.live_revision) # The revision content should contain the data self.assertEqual( snippet.latest_revision.content["text"], "Draft-enabled Foo", ) # Check that the published signal was not fired self.assertEqual(mock_handler.call_count, 0) finally: published.disconnect(mock_handler) def test_publish_with_publish_permission(self): # Use create and publish permissions instead of relying on superuser flag self.user.is_superuser = False add_permission = Permission.objects.get( content_type__app_label="tests", codename="add_draftstatemodel", ) publish_permission = Permission.objects.get( content_type__app_label="tests", codename="publish_draftstatemodel", ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin", ) self.user.user_permissions.add( add_permission, publish_permission, admin_permission, ) self.user.save() # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: timestamp = now() with freeze_time(timestamp): response = self.post( post_data={ "text": "Draft-enabled Foo, Published", "action-publish": "action-publish", } ) snippet = DraftStateModel.objects.get(text="Draft-enabled Foo, Published") self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatemodel:list") ) # The instance should be created self.assertEqual(snippet.text, "Draft-enabled Foo, Published") # The instance should be live self.assertTrue(snippet.live) self.assertFalse(snippet.has_unpublished_changes) self.assertEqual(snippet.first_published_at, timestamp) self.assertEqual(snippet.last_published_at, timestamp) # A revision should be created and set as both latest_revision and live_revision self.assertIsNotNone(snippet.live_revision) self.assertEqual(snippet.live_revision, snippet.latest_revision) # The revision content should contain the new data self.assertEqual( snippet.live_revision.content["text"], "Draft-enabled Foo, Published", ) # Check that the published signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateModel) self.assertEqual(mock_call["instance"], snippet) self.assertIsInstance(mock_call["instance"], DraftStateModel) finally: published.disconnect(mock_handler) def test_create_scheduled(self): go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "Some content", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) snippet = DraftStateModel.objects.get(text="Some content") # Should be redirected to the edit page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatemodel:edit", args=[snippet.pk]), ) # Should be saved as draft with the scheduled publishing dates self.assertEqual(snippet.go_live_at.date(), go_live_at.date()) self.assertEqual(snippet.expire_at.date(), expire_at.date()) self.assertIs(snippet.expired, False) self.assertEqual(snippet.status_string, "draft") # No revisions with approved_go_live_at self.assertFalse( Revision.objects.for_instance(snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) def test_create_scheduled_go_live_before_expiry(self): response = self.post( post_data={ "text": "Some content", "go_live_at": submittable_timestamp(now() + datetime.timedelta(days=2)), "expire_at": submittable_timestamp(now() + datetime.timedelta(days=1)), } ) self.assertEqual(response.status_code, 200) # Check that a form error was raised self.assertFormError( response.context["form"], "go_live_at", "Go live date/time must be before expiry date/time", ) self.assertFormError( response.context["form"], "expire_at", "Go live date/time must be before expiry date/time", ) self.assertContains( response, '
Invalid schedule
', html=True, ) num_errors = 2 # Should show the correct number on the badge of the toggle button self.assertRegex( response.content.decode(), self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors}, ) def test_create_scheduled_expire_in_the_past(self): response = self.post( post_data={ "text": "Some content", "expire_at": submittable_timestamp(now() + datetime.timedelta(days=-1)), } ) self.assertEqual(response.status_code, 200) # Check that a form error was raised self.assertFormError( response.context["form"], "expire_at", "Expiry date/time must be in the future", ) self.assertContains( response, '
Invalid schedule
', html=True, ) num_errors = 1 # Should show the correct number on the badge of the toggle button self.assertRegex( response.content.decode(), self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors}, ) def test_create_post_publish_scheduled(self): go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatemodel:list") ) # Find the object and check it snippet = DraftStateModel.objects.get(text="Some content") self.assertEqual(snippet.go_live_at.date(), go_live_at.date()) self.assertEqual(snippet.expire_at.date(), expire_at.date()) self.assertIs(snippet.expired, False) # A revision with approved_go_live_at should exist now self.assertTrue( Revision.objects.for_instance(snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # But snippet won't be live self.assertFalse(snippet.live) self.assertFalse(snippet.first_published_at) self.assertEqual(snippet.status_string, "scheduled") class BaseTestSnippetEditView(WagtailTestUtils, TestCase): def get_edit_url(self): snippet = self.test_snippet args = [quote(snippet.pk)] return reverse(snippet.snippet_viewset.get_url_name("edit"), args=args) def get(self, params={}): return self.client.get(self.get_edit_url(), params) def post(self, post_data={}): return self.client.post(self.get_edit_url(), post_data) def setUp(self): self.user = self.login() def assertSchedulingDialogRendered(self, response, label="Edit schedule"): # Should show the "Edit schedule" button html = response.content.decode() self.assertTagInHTML( f'', html, count=1, allow_extra_attrs=True, ) # Should show the dialog template pointing to the [data-edit-form] selector as the root soup = self.get_soup(html) dialog = soup.select_one( """ template[data-controller="w-teleport"][data-w-teleport-target-value="[data-edit-form]"] #schedule-publishing-dialog """ ) self.assertIsNotNone(dialog) # Should render the main form with data-edit-form attribute self.assertTagInHTML( f'', html, count=1, allow_extra_attrs=True, ) self.assertTagInHTML( '
', html, count=1, allow_extra_attrs=True, ) class TestSnippetEditView(BaseTestSnippetEditView): fixtures = ["test.json"] def setUp(self): super().setUp() self.test_snippet = Advert.objects.get(pk=1) ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(Advert), label="Test Advert", action="wagtail.create", timestamp=now() - datetime.timedelta(weeks=3), user=self.user, object_id="1", ) def test_get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.get() self.assertEqual(response.status_code, 302) def test_simple(self): response = self.get() html = response.content.decode() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") self.assertNotContains(response, 'role="tablist"') # Without DraftStateMixin, there should be no "No publishing schedule set" info self.assertNotContains(response, "No publishing schedule set") history_url = reverse( "wagtailsnippets_tests_advert:history", args=[quote(self.test_snippet.pk)] ) # History link should be present, one in the header and one in the status side panel self.assertContains(response, history_url, count=2) usage_url = reverse( "wagtailsnippets_tests_advert:usage", args=[quote(self.test_snippet.pk)] ) # Usage link should be present in the status side panel self.assertContains(response, usage_url) # Live status and last updated info should be shown, with a link to the history page self.assertContains(response, "3\xa0weeks ago") self.assertTagInHTML( f'View history', html, allow_extra_attrs=True, ) url_finder = AdminURLFinder(self.user) expected_url = "/admin/snippets/tests/advert/edit/%d/" % self.test_snippet.pk self.assertEqual(url_finder.get_edit_url(self.test_snippet), expected_url) def test_non_existent_model(self): response = self.client.get( f"/admin/snippets/tests/foo/edit/{quote(self.test_snippet.pk)}/" ) self.assertEqual(response.status_code, 404) def test_nonexistent_id(self): response = self.client.get( reverse("wagtailsnippets_tests_advert:edit", args=[999999]) ) self.assertEqual(response.status_code, 404) def test_edit_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.post( post_data={"text": "test text", "url": "http://www.example.com/"} ) self.assertEqual(response.status_code, 302) url_finder = AdminURLFinder(self.user) self.assertIsNone(url_finder.get_edit_url(self.test_snippet)) def test_edit_invalid(self): response = self.post(post_data={"foo": "bar"}) self.assertContains(response, "The advert could not be saved due to errors.") self.assertContains(response, "error-message", count=1) self.assertContains(response, "This field is required", count=1) def test_edit(self): response = self.post( post_data={ "text": "edited_test_advert", "url": "http://www.example.com/edited", } ) self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list")) snippets = Advert.objects.filter(text="edited_test_advert") self.assertEqual(snippets.count(), 1) self.assertEqual(snippets.first().url, "http://www.example.com/edited") def test_edit_with_tags(self): tags = ["hello", "world"] response = self.post( post_data={ "text": "edited_test_advert", "url": "http://www.example.com/edited", "tags": ", ".join(tags), } ) self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list")) snippet = Advert.objects.get(text="edited_test_advert") expected_tags = list(Tag.objects.order_by("name").filter(name__in=tags)) self.assertEqual(len(expected_tags), 2) self.assertEqual(list(snippet.tags.order_by("name")), expected_tags) def test_before_edit_snippet_hook_get(self): def hook_func(request, instance): self.assertIsInstance(request, HttpRequest) self.assertEqual(instance.text, "test_advert") self.assertEqual(instance.url, "http://www.example.com") return HttpResponse("Overridden!") with self.register_hook("before_edit_snippet", hook_func): response = self.get() self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") def test_before_edit_snippet_hook_post(self): def hook_func(request, instance): self.assertIsInstance(request, HttpRequest) self.assertEqual(instance.text, "test_advert") self.assertEqual(instance.url, "http://www.example.com") return HttpResponse("Overridden!") with self.register_hook("before_edit_snippet", hook_func): response = self.post( post_data={ "text": "Edited and runs hook", "url": "http://www.example.com/hook-enabled-edited", } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted before advert was updated self.assertEqual(Advert.objects.get().text, "test_advert") def test_after_edit_snippet_hook(self): def hook_func(request, instance): self.assertIsInstance(request, HttpRequest) self.assertEqual(instance.text, "Edited and runs hook") self.assertEqual(instance.url, "http://www.example.com/hook-enabled-edited") return HttpResponse("Overridden!") with self.register_hook("after_edit_snippet", hook_func): response = self.post( post_data={ "text": "Edited and runs hook", "url": "http://www.example.com/hook-enabled-edited", } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted after advert was updated self.assertEqual(Advert.objects.get().text, "Edited and runs hook") def test_register_snippet_action_menu_item(self): class TestSnippetActionMenuItem(ActionMenuItem): label = "Test" name = "test" icon_name = "check" classname = "action-secondary" def is_shown(self, context): return True def hook_func(model): return TestSnippetActionMenuItem(order=0) with self.register_hook("register_snippet_action_menu_item", hook_func): get_base_snippet_action_menu_items.cache_clear() response = self.get() get_base_snippet_action_menu_items.cache_clear() self.assertContains( response, '', html=True, ) def test_construct_snippet_action_menu(self): def hook_func(menu_items, request, context): self.assertIsInstance(menu_items, list) self.assertIsInstance(request, WSGIRequest) self.assertEqual(context["view"], "edit") self.assertEqual(context["instance"], self.test_snippet) self.assertEqual(context["model"], Advert) # Remove the save item del menu_items[0] with self.register_hook("construct_snippet_action_menu", hook_func): response = self.get() self.assertNotContains(response, "Save") class TestEditTabbedSnippet(BaseTestSnippetEditView): def setUp(self): super().setUp() self.test_snippet = AdvertWithTabbedInterface.objects.create( text="test_advert", url="http://www.example.com", something_else="Model with tabbed interface", ) def test_snippet_with_tabbed_interface(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") self.assertContains(response, 'role="tablist"') self.assertContains( response, '', ) self.assertContains( response, '', ) class TestEditFileUploadSnippet(BaseTestSnippetEditView): def setUp(self): super().setUp() self.test_snippet = FileUploadSnippet.objects.create( file=ContentFile(b"Simple text document", "test.txt") ) def test_edit_file_upload_multipart(self): response = self.get() self.assertContains(response, 'enctype="multipart/form-data"') response = self.post( post_data={ "file": SimpleUploadedFile("replacement.txt", b"Replacement document") } ) self.assertRedirects( response, reverse("wagtailsnippets_snippetstests_fileuploadsnippet:list"), ) snippet = FileUploadSnippet.objects.get() self.assertEqual(snippet.file.read(), b"Replacement document") @override_settings(WAGTAIL_I18N_ENABLED=True) class TestLocaleSelectorOnEdit(BaseTestSnippetEditView): fixtures = ["test.json"] LOCALE_SELECTOR_LABEL = "Switch locales" LOCALE_INDICATOR_HTML = '

]+w-bg-critical-200[^>]+>\s*%(num_errors)s\s*

" ) def setUp(self): super().setUp() self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.create( custom_id="custom/1", text="Draft-enabled Foo", live=False ) def test_get(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # The save button should be labelled "Save draft" self.assertContains(response, "Save draft") # The publish button should exist self.assertContains(response, "Publish") # The publish button should have name="action-publish" self.assertContains( response, '', ) self.assertNotContains(response, "Unpublish") def test_save_draft(self): response = self.post(post_data={"text": "Draft-enabled Bar"}) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet) latest_revision = self.test_snippet.latest_revision self.assertRedirects(response, self.get_edit_url()) # The instance should be updated, since it is still a draft self.assertEqual(self.test_snippet.text, "Draft-enabled Bar") # The instance should be a draft self.assertFalse(self.test_snippet.live) self.assertTrue(self.test_snippet.has_unpublished_changes) self.assertIsNone(self.test_snippet.first_published_at) self.assertIsNone(self.test_snippet.last_published_at) self.assertIsNone(self.test_snippet.live_revision) # The revision should be created and set as latest_revision self.assertEqual(revisions.count(), 1) self.assertEqual(latest_revision, revisions.first()) # The revision content should contain the new data self.assertEqual(latest_revision.content["text"], "Draft-enabled Bar") def test_publish(self): # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: timestamp = now() with freeze_time(timestamp): response = self.post( post_data={ "text": "Draft-enabled Bar, Published", "action-publish": "action-publish", } ) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet) latest_revision = self.test_snippet.latest_revision log_entries = ModelLogEntry.objects.filter( content_type=ContentType.objects.get_for_model( DraftStateCustomPrimaryKeyModel ), action="wagtail.publish", object_id=self.test_snippet.pk, ) log_entry = log_entries.first() self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # The instance should be updated self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published") # The instance should be live self.assertTrue(self.test_snippet.live) self.assertFalse(self.test_snippet.has_unpublished_changes) self.assertEqual(self.test_snippet.first_published_at, timestamp) self.assertEqual(self.test_snippet.last_published_at, timestamp) self.assertEqual(self.test_snippet.live_revision, latest_revision) # The revision should be created and set as latest_revision self.assertEqual(revisions.count(), 1) self.assertEqual(latest_revision, revisions.first()) # The revision content should contain the new data self.assertEqual( latest_revision.content["text"], "Draft-enabled Bar, Published", ) # A log entry with wagtail.publish action should be created self.assertEqual(log_entries.count(), 1) self.assertEqual(log_entry.timestamp, timestamp) # Check that the published signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel) self.assertEqual(mock_call["instance"], self.test_snippet) self.assertIsInstance( mock_call["instance"], DraftStateCustomPrimaryKeyModel ) finally: published.disconnect(mock_handler) def test_publish_bad_permissions(self): # Only add edit permission self.user.is_superuser = False edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatecustomprimarykeymodel", ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin", ) self.user.user_permissions.add(edit_permission, admin_permission) self.user.save() # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: response = self.post( post_data={ "text": "Edited draft Foo", "action-publish": "action-publish", } ) self.test_snippet.refresh_from_db() # Should remain on the edit page self.assertRedirects(response, self.get_edit_url()) # The instance should be edited, since it is still a draft self.assertEqual(self.test_snippet.text, "Edited draft Foo") # The instance should not be live self.assertFalse(self.test_snippet.live) self.assertTrue(self.test_snippet.has_unpublished_changes) # A revision should be created and set as latest_revision, but not live_revision self.assertIsNotNone(self.test_snippet.latest_revision) self.assertIsNone(self.test_snippet.live_revision) # The revision content should contain the data self.assertEqual( self.test_snippet.latest_revision.content["text"], "Edited draft Foo", ) # Check that the published signal was not fired self.assertEqual(mock_handler.call_count, 0) finally: published.disconnect(mock_handler) def test_publish_with_publish_permission(self): # Only add edit and publish permissions self.user.is_superuser = False edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatecustomprimarykeymodel", ) publish_permission = Permission.objects.get( content_type__app_label="tests", codename="publish_draftstatecustomprimarykeymodel", ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) self.user.user_permissions.add( edit_permission, publish_permission, admin_permission, ) self.user.save() # Connect a mock signal handler to published signal mock_handler = mock.MagicMock() published.connect(mock_handler) try: timestamp = now() with freeze_time(timestamp): response = self.post( post_data={ "text": "Draft-enabled Bar, Published", "action-publish": "action-publish", } ) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet) latest_revision = self.test_snippet.latest_revision log_entries = ModelLogEntry.objects.filter( content_type=ContentType.objects.get_for_model( DraftStateCustomPrimaryKeyModel ), action="wagtail.publish", object_id=self.test_snippet.pk, ) log_entry = log_entries.first() self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # The instance should be updated self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published") # The instance should be live self.assertTrue(self.test_snippet.live) self.assertFalse(self.test_snippet.has_unpublished_changes) self.assertEqual(self.test_snippet.first_published_at, timestamp) self.assertEqual(self.test_snippet.last_published_at, timestamp) self.assertEqual(self.test_snippet.live_revision, latest_revision) # The revision should be created and set as latest_revision self.assertEqual(revisions.count(), 1) self.assertEqual(latest_revision, revisions.first()) # The revision content should contain the new data self.assertEqual( latest_revision.content["text"], "Draft-enabled Bar, Published", ) # A log entry with wagtail.publish action should be created self.assertEqual(log_entries.count(), 1) self.assertEqual(log_entry.timestamp, timestamp) # Check that the published signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel) self.assertEqual(mock_call["instance"], self.test_snippet) self.assertIsInstance( mock_call["instance"], DraftStateCustomPrimaryKeyModel ) finally: published.disconnect(mock_handler) def test_save_draft_then_publish(self): save_timestamp = now() with freeze_time(save_timestamp): self.test_snippet.text = "Draft-enabled Bar, In Draft" self.test_snippet.save_revision() publish_timestamp = now() with freeze_time(publish_timestamp): response = self.post( post_data={ "text": "Draft-enabled Bar, Now Published", "action-publish": "action-publish", } ) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk") latest_revision = self.test_snippet.latest_revision self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # The instance should be updated self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Now Published") # The instance should be live self.assertTrue(self.test_snippet.live) self.assertFalse(self.test_snippet.has_unpublished_changes) self.assertEqual(self.test_snippet.first_published_at, publish_timestamp) self.assertEqual(self.test_snippet.last_published_at, publish_timestamp) self.assertEqual(self.test_snippet.live_revision, latest_revision) # The revision should be created and set as latest_revision self.assertEqual(revisions.count(), 2) self.assertEqual(latest_revision, revisions.last()) # The revision content should contain the new data self.assertEqual( latest_revision.content["text"], "Draft-enabled Bar, Now Published", ) def test_publish_then_save_draft(self): publish_timestamp = now() with freeze_time(publish_timestamp): self.test_snippet.text = "Draft-enabled Bar, Published" self.test_snippet.save_revision().publish() save_timestamp = now() with freeze_time(save_timestamp): response = self.post( post_data={"text": "Draft-enabled Bar, Published and In Draft"} ) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk") latest_revision = self.test_snippet.latest_revision self.assertRedirects(response, self.get_edit_url()) # The instance should be updated with the last published changes self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published") # The instance should be live self.assertTrue(self.test_snippet.live) # The instance should have unpublished changes self.assertTrue(self.test_snippet.has_unpublished_changes) self.assertEqual(self.test_snippet.first_published_at, publish_timestamp) self.assertEqual(self.test_snippet.last_published_at, publish_timestamp) # The live revision should be the first revision self.assertEqual(self.test_snippet.live_revision, revisions.first()) # The second revision should be created and set as latest_revision self.assertEqual(revisions.count(), 2) self.assertEqual(latest_revision, revisions.last()) # The revision content should contain the new data self.assertEqual( latest_revision.content["text"], "Draft-enabled Bar, Published and In Draft", ) def test_publish_twice(self): first_timestamp = now() with freeze_time(first_timestamp): self.test_snippet.text = "Draft-enabled Bar, Published Once" self.test_snippet.save_revision().publish() second_timestamp = now() + datetime.timedelta(days=1) with freeze_time(second_timestamp): response = self.post( post_data={ "text": "Draft-enabled Bar, Published Twice", "action-publish": "action-publish", } ) self.test_snippet.refresh_from_db() revisions = Revision.objects.for_instance(self.test_snippet).order_by("pk") latest_revision = self.test_snippet.latest_revision self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # The instance should be updated with the last published changes self.assertEqual(self.test_snippet.text, "Draft-enabled Bar, Published Twice") # The instance should be live self.assertTrue(self.test_snippet.live) self.assertFalse(self.test_snippet.has_unpublished_changes) # The first_published_at and last_published_at should be set correctly self.assertEqual(self.test_snippet.first_published_at, first_timestamp) self.assertEqual(self.test_snippet.last_published_at, second_timestamp) # The live revision should be the second revision self.assertEqual(self.test_snippet.live_revision, revisions.last()) # The second revision should be created and set as latest_revision self.assertEqual(revisions.count(), 2) self.assertEqual(latest_revision, revisions.last()) # The revision content should contain the new data self.assertEqual( latest_revision.content["text"], "Draft-enabled Bar, Published Twice", ) def test_get_after_save_draft(self): self.post(post_data={"text": "Draft-enabled Bar"}) response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # Should not show the Live status self.assertNotContains( response, '

Status: Live

', html=True, ) # Should show the Draft status self.assertContains( response, '

Status: Draft

', html=True, ) # Should not show the Unpublish action menu item unpublish_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish", args=(quote(self.test_snippet.pk),), ) self.assertNotContains( response, f'
', ) self.assertNotContains(response, "Unpublish") def test_get_after_publish(self): self.post( post_data={ "text": "Draft-enabled Bar, Published", "action-publish": "action-publish", } ) response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # Should show the Live status self.assertContains( response, '

Status: Live

', html=True, ) # Should not show the Draft status self.assertNotContains( response, '

Status: Draft

', html=True, ) # Should show the Unpublish action menu item unpublish_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish", args=(quote(self.test_snippet.pk),), ) self.assertContains( response, f'
', ) self.assertContains(response, "Unpublish") def test_get_after_publish_and_save_draft(self): self.post( post_data={ "text": "Draft-enabled Bar, Published", "action-publish": "action-publish", } ) self.post(post_data={"text": "Draft-enabled Bar, In Draft"}) response = self.get() html = response.content.decode() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # Should show the Live status self.assertContains( response, '

Status: Live

', html=True, ) # Should show the Draft status self.assertContains( response, '

Status: Draft

', html=True, ) # Should show the Unpublish action menu item unpublish_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish", args=(quote(self.test_snippet.pk),), ) self.assertContains( response, f'
', ) self.assertContains(response, "Unpublish") soup = self.get_soup(response.content) h2 = soup.select_one("#header-title") self.assertIsNotNone(h2) icon = h2.select_one("svg use") self.assertIsNotNone(icon) self.assertEqual(icon["href"], "#icon-snippet") self.assertEqual(h2.text.strip(), "Draft-enabled Bar, In Draft") # Should use the latest draft content for the form self.assertTagInHTML( '', html, allow_extra_attrs=True, ) def test_edit_post_scheduled(self): self.test_snippet.save_revision().publish() # put go_live_at and expire_at several days away from the current date, to avoid # false matches in content__ tests go_live_at = now() + datetime.timedelta(days=10) expire_at = now() + datetime.timedelta(days=20) response = self.post( post_data={ "text": "Some content", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the edit page self.assertRedirects( response, reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:edit", args=[quote(self.test_snippet.pk)], ), ) self.test_snippet.refresh_from_db() # The object will still be live self.assertTrue(self.test_snippet.live) # A revision with approved_go_live_at should not exist self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # But a revision with go_live_at and expire_at in their content json *should* exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .filter( content__go_live_at__startswith=str(go_live_at.date()), ) .exists() ) self.assertTrue( Revision.objects.for_instance(self.test_snippet) .filter( content__expire_at__startswith=str(expire_at.date()), ) .exists() ) # Get the edit page again response = self.get() # Should show the draft go_live_at and expire_at under the "Once published" label self.assertContains( response, '
Once published:
', html=True, count=1, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) def test_edit_scheduled_go_live_before_expiry(self): response = self.post( post_data={ "text": "Some content", "go_live_at": submittable_timestamp(now() + datetime.timedelta(days=2)), "expire_at": submittable_timestamp(now() + datetime.timedelta(days=1)), } ) self.assertEqual(response.status_code, 200) # Check that a form error was raised self.assertFormError( response.context["form"], "go_live_at", "Go live date/time must be before expiry date/time", ) self.assertFormError( response.context["form"], "expire_at", "Go live date/time must be before expiry date/time", ) self.assertContains( response, '
Invalid schedule
', html=True, ) num_errors = 2 # Should show the correct number on the badge of the toggle button self.assertRegex( response.content.decode(), self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors}, ) def test_edit_scheduled_expire_in_the_past(self): response = self.post( post_data={ "text": "Some content", "expire_at": submittable_timestamp(now() + datetime.timedelta(days=-1)), } ) self.assertEqual(response.status_code, 200) # Check that a form error was raised self.assertFormError( response.context["form"], "expire_at", "Expiry date/time must be in the future", ) self.assertContains( response, '
Invalid schedule
', html=True, ) num_errors = 1 # Should show the correct number on the badge of the toggle button self.assertRegex( response.content.decode(), self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors}, ) def test_edit_post_invalid_schedule_with_existing_draft_schedule(self): self.test_snippet.go_live_at = now() + datetime.timedelta(days=1) self.test_snippet.expire_at = now() + datetime.timedelta(days=2) latest_revision = self.test_snippet.save_revision() go_live_at = now() + datetime.timedelta(days=10) expire_at = now() + datetime.timedelta(days=-20) response = self.post( post_data={ "text": "Some edited content", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should render the edit page with errors instead of redirecting self.assertEqual(response.status_code, 200) self.test_snippet.refresh_from_db() # The snippet will not be live self.assertFalse(self.test_snippet.live) # No new revision should have been created self.assertEqual(self.test_snippet.latest_revision_id, latest_revision.pk) # Should not show the draft go_live_at and expire_at under the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertNotContains( response, 'Go-live:', html=True, ) self.assertNotContains( response, 'Expiry:', html=True, ) # Should show the "Edit schedule" button html = response.content.decode() self.assertTagInHTML( '', html, count=1, allow_extra_attrs=True, ) self.assertContains( response, '
Invalid schedule
', html=True, ) num_errors = 2 # Should show the correct number on the badge of the toggle button self.assertRegex( response.content.decode(), self.STATUS_TOGGLE_BADGE_REGEX % {"num_errors": num_errors}, ) def test_first_published_at_editable(self): """Test that we can update the first_published_at via the edit form, for models that expose it.""" self.test_snippet.save_revision().publish() self.test_snippet.refresh_from_db() initial_delta = self.test_snippet.first_published_at - now() first_published_at = now() - datetime.timedelta(days=2) self.post( post_data={ "text": "I've been edited!", "action-publish": "action-publish", "first_published_at": submittable_timestamp(first_published_at), } ) self.test_snippet.refresh_from_db() # first_published_at should have changed. new_delta = self.test_snippet.first_published_at - now() self.assertNotEqual(new_delta.days, initial_delta.days) # first_published_at should be 3 days ago. self.assertEqual(new_delta.days, -3) def test_edit_post_publish_scheduled_unpublished(self): go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should not be live self.assertFalse(self.test_snippet.live) # Instead a revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # The object SHOULD have the "has_unpublished_changes" flag set, # because the changes are not visible as a live object yet self.assertTrue( self.test_snippet.has_unpublished_changes, msg="An object scheduled for future publishing should have has_unpublished_changes=True", ) self.assertEqual(self.test_snippet.status_string, "scheduled") response = self.get() # Should show the go_live_at and expire_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) def test_edit_post_publish_now_an_already_scheduled_unpublished(self): # First let's publish an object with a go_live_at in the future go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should not be live self.assertFalse(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "scheduled") # Instead a revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # Now, let's edit it and publish it right now response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": "", } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should be live self.assertTrue(self.test_snippet.live) # The revision with approved_go_live_at should no longer exist self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) response = self.get() self.assertSchedulingDialogRendered(response) def test_edit_post_publish_scheduled_published(self): self.test_snippet.save_revision().publish() self.test_snippet.refresh_from_db() live_revision = self.test_snippet.live_revision go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "I've been edited!", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get( pk=self.test_snippet.pk ) # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live + scheduled") # A revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # The object SHOULD have the "has_unpublished_changes" flag set, # because the changes are not visible as a live object yet self.assertTrue( self.test_snippet.has_unpublished_changes, msg="An object scheduled for future publishing should have has_unpublished_changes=True", ) self.assertNotEqual( self.test_snippet.get_latest_revision(), live_revision, "An object scheduled for future publishing should have a new revision, that is not the live revision", ) self.assertEqual( self.test_snippet.text, "Draft-enabled Foo", "A live object with a scheduled revision should still have the original content", ) response = self.get() # Should show the go_live_at and expire_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) def test_edit_post_publish_now_an_already_scheduled_published(self): self.test_snippet.save_revision().publish() # First let's publish an object with a go_live_at in the future go_live_at = now() + datetime.timedelta(days=1) expire_at = now() + datetime.timedelta(days=2) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should still be live self.assertTrue(self.test_snippet.live) # A revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) self.assertEqual( self.test_snippet.text, "Draft-enabled Foo", "A live object with scheduled revisions should still have original content", ) # Now, let's edit it and publish it right now response = self.post( post_data={ "text": "I've been updated!", "action-publish": "Publish", "go_live_at": "", } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should be live self.assertTrue(self.test_snippet.live) # The scheduled revision should no longer exist self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # The content should be updated self.assertEqual(self.test_snippet.text, "I've been updated!") def test_edit_post_save_schedule_before_a_scheduled_expire(self): # First let's publish an object with *just* an expire_at in the future expire_at = now() + datetime.timedelta(days=20) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live") # The live object should have the expire_at field set self.assertEqual( self.test_snippet.expire_at, expire_at.replace(second=0, microsecond=0), ) # Now, let's save an object with a go_live_at in the future, # but before the existing expire_at go_live_at = now() + datetime.timedelta(days=10) new_expire_at = now() + datetime.timedelta(days=15) response = self.post( post_data={ "text": "Some content", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(new_expire_at), } ) # Should be redirected to the edit page self.assertRedirects( response, reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:edit", args=[quote(self.test_snippet.pk)], ), ) self.test_snippet.refresh_from_db() # The object will still be live self.assertTrue(self.test_snippet.live) # A revision with approved_go_live_at should not exist self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) # But a revision with go_live_at and expire_at in their content json *should* exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .filter(content__go_live_at__startswith=str(go_live_at.date())) .exists() ) self.assertTrue( Revision.objects.for_instance(self.test_snippet) .filter(content__expire_at__startswith=str(expire_at.date())) .exists() ) response = self.get() # Should still show the active expire_at in the live object self.assertContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, count=1, ) # Should also show the draft go_live_at and expire_at under the "Once published" label self.assertContains( response, '
Once published:
', html=True, count=1, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(new_expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) def test_edit_post_publish_schedule_before_a_scheduled_expire(self): # First let's publish an object with *just* an expire_at in the future expire_at = now() + datetime.timedelta(days=20) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live") # The live object should have the expire_at field set self.assertEqual( self.test_snippet.expire_at, expire_at.replace(second=0, microsecond=0), ) # Now, let's publish an object with a go_live_at in the future, # but before the existing expire_at go_live_at = now() + datetime.timedelta(days=10) new_expire_at = now() + datetime.timedelta(days=15) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(new_expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get( pk=self.test_snippet.pk ) # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live + scheduled") # A revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) response = self.get() # Should not show the active expire_at in the live object because the # scheduled revision is before the existing expire_at, which means it will # override the existing expire_at when it goes live self.assertNotContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, ) # Should show the go_live_at and expire_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(new_expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) def test_edit_post_publish_schedule_after_a_scheduled_expire(self): # First let's publish an object with *just* an expire_at in the future expire_at = now() + datetime.timedelta(days=20) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "expire_at": submittable_timestamp(expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet.refresh_from_db() # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live") # The live object should have the expire_at field set self.assertEqual( self.test_snippet.expire_at, expire_at.replace(second=0, microsecond=0), ) # Now, let's publish an object with a go_live_at in the future, # but after the existing expire_at go_live_at = now() + datetime.timedelta(days=23) new_expire_at = now() + datetime.timedelta(days=25) response = self.post( post_data={ "text": "Some content", "action-publish": "Publish", "go_live_at": submittable_timestamp(go_live_at), "expire_at": submittable_timestamp(new_expire_at), } ) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.get( pk=self.test_snippet.pk ) # The object should still be live self.assertTrue(self.test_snippet.live) self.assertEqual(self.test_snippet.status_string, "live + scheduled") # Instead a revision with approved_go_live_at should now exist self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) response = self.get() # Should still show the active expire_at in the live object because the # scheduled revision is after the existing expire_at, which means the # new expire_at won't take effect until the revision goes live. # This means the object will be: # unpublished (expired) -> published (scheduled) -> unpublished (expired again) self.assertContains( response, f'Expiry: {render_timestamp(expire_at)}', html=True, count=1, ) # Should show the go_live_at and expire_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(go_live_at)}', html=True, count=1, ) self.assertContains( response, f'Expiry: {render_timestamp(new_expire_at)}', html=True, count=1, ) self.assertSchedulingDialogRendered(response) class TestScheduledForPublishLock(BaseTestSnippetEditView): def setUp(self): super().setUp() self.test_snippet = DraftStateModel.objects.create( text="Draft-enabled Foo", live=False ) self.go_live_at = now() + datetime.timedelta(days=1) self.test_snippet.text = "I've been edited!" self.test_snippet.go_live_at = self.go_live_at self.latest_revision = self.test_snippet.save_revision() self.latest_revision.publish() self.test_snippet.refresh_from_db() def test_edit_get_scheduled_for_publishing_with_publish_permission(self): self.user.is_superuser = False edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatemodel" ) publish_permission = Permission.objects.get( content_type__app_label="tests", codename="publish_draftstatemodel" ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) self.user.user_permissions.add( edit_permission, publish_permission, admin_permission, ) self.user.save() response = self.get() # Should show the go_live_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(self.go_live_at)}', html=True, count=1, ) # Should show the lock message self.assertContains( response, "Draft state model 'I've been edited!' is locked and has been scheduled to go live at", count=1, ) # Should show the lock information in the status side panel self.assertContains(response, "Locked by schedule") self.assertContains( response, '
Currently locked and will go live on the scheduled date
', html=True, count=1, ) html = response.content.decode() # Should not show the "Edit schedule" button self.assertTagInHTML( '', html, count=0, allow_extra_attrs=True, ) # Should show button to cancel scheduled publishing unschedule_url = reverse( "wagtailsnippets_tests_draftstatemodel:revisions_unschedule", args=[self.test_snippet.pk, self.latest_revision.pk], ) self.assertTagInHTML( f'', html, count=1, allow_extra_attrs=True, ) def test_edit_get_scheduled_for_publishing_without_publish_permission(self): self.user.is_superuser = False edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatemodel" ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) self.user.user_permissions.add(edit_permission, admin_permission) self.user.save() response = self.get() # Should show the go_live_at without the "Once published" label self.assertNotContains( response, '
Once published:
', html=True, ) self.assertContains( response, f'Go-live: {render_timestamp(self.go_live_at)}', html=True, count=1, ) # Should show the lock message self.assertContains( response, "Draft state model 'I've been edited!' is locked and has been scheduled to go live at", count=1, ) # Should show the lock information in the status side panel self.assertContains(response, "Locked by schedule") self.assertContains( response, '
Currently locked and will go live on the scheduled date
', html=True, count=1, ) html = response.content.decode() # Should not show the "Edit schedule" button self.assertTagInHTML( '', html, count=0, allow_extra_attrs=True, ) # Should not show button to cancel scheduled publishing unschedule_url = reverse( "wagtailsnippets_tests_draftstatemodel:revisions_unschedule", args=[self.test_snippet.pk, self.latest_revision.pk], ) self.assertTagInHTML( f'', html, count=0, allow_extra_attrs=True, ) def test_edit_post_scheduled_for_publishing(self): response = self.post( post_data={ "text": "I'm edited while it's locked for scheduled publishing!", "go_live_at": submittable_timestamp(self.go_live_at), } ) self.test_snippet.refresh_from_db() # Should not create a new revision, # so the latest revision's content should still be the same self.assertEqual(self.test_snippet.latest_revision, self.latest_revision) self.assertEqual( self.test_snippet.latest_revision.content["text"], "I've been edited!", ) # Should show a message explaining why the changes were not saved self.assertContains( response, "The draft state model could not be saved as it is locked", count=1, ) # Should not show the lock message, as we already have the error message self.assertNotContains( response, "Draft state model 'I've been edited!' is locked and has been scheduled to go live at", ) # Should show the lock information in the status side panel self.assertContains(response, "Locked by schedule") self.assertContains( response, '
Currently locked and will go live on the scheduled date
', html=True, count=1, ) html = response.content.decode() # Should not show the "Edit schedule" button self.assertTagInHTML( '', html, count=0, allow_extra_attrs=True, ) # Should not show button to cancel scheduled publishing as the lock message isn't shown unschedule_url = reverse( "wagtailsnippets_tests_draftstatemodel:revisions_unschedule", args=[self.test_snippet.pk, self.latest_revision.pk], ) self.assertTagInHTML( f'', html, count=0, allow_extra_attrs=True, ) class TestSnippetUnschedule(WagtailTestUtils, TestCase): def setUp(self): self.user = self.login() self.test_snippet = DraftStateCustomPrimaryKeyModel.objects.create( custom_id="custom/1", text="Draft-enabled Foo", live=False ) self.go_live_at = now() + datetime.timedelta(days=1) self.test_snippet.text = "I've been edited!" self.test_snippet.go_live_at = self.go_live_at self.latest_revision = self.test_snippet.save_revision() self.latest_revision.publish() self.test_snippet.refresh_from_db() self.unschedule_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:revisions_unschedule", args=[quote(self.test_snippet.pk), self.latest_revision.pk], ) def set_permissions(self, set_publish_permission): self.user.is_superuser = False permissions = [ Permission.objects.get( content_type__app_label="tests", codename="change_draftstatecustomprimarykeymodel", ), Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ), ] if set_publish_permission: permissions.append( Permission.objects.get( content_type__app_label="tests", codename="publish_draftstatecustomprimarykeymodel", ) ) self.user.user_permissions.add(*permissions) self.user.save() def test_get_unschedule_view_with_publish_permissions(self): self.set_permissions(True) # Get unschedule page response = self.client.get(self.unschedule_url) # Check that the user received a confirmation page self.assertEqual(response.status_code, 200) self.assertTemplateUsed( response, "wagtailadmin/shared/revisions/confirm_unschedule.html" ) def test_get_unschedule_view_bad_permissions(self): self.set_permissions(False) # Get unschedule page response = self.client.get(self.unschedule_url) # Check that the user is redirected to the admin homepage self.assertRedirects(response, reverse("wagtailadmin_home")) def test_post_unschedule_view_with_publish_permissions(self): self.set_permissions(True) # Post unschedule page response = self.client.post(self.unschedule_url) # Check that the user was redirected to the history page self.assertRedirects( response, reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:history", args=[quote(self.test_snippet.pk)], ), ) self.test_snippet.refresh_from_db() self.latest_revision.refresh_from_db() # Check that the revision is no longer scheduled self.assertIsNone(self.latest_revision.approved_go_live_at) # No revisions with approved_go_live_at self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) def test_post_unschedule_view_bad_permissions(self): self.set_permissions(False) # Post unschedule page response = self.client.post(self.unschedule_url) # Check that the user is redirected to the admin homepage self.assertRedirects(response, reverse("wagtailadmin_home")) self.test_snippet.refresh_from_db() self.latest_revision.refresh_from_db() # Check that the revision is still scheduled self.assertIsNotNone(self.latest_revision.approved_go_live_at) # Revision with approved_go_live_at exists self.assertTrue( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) def test_post_unschedule_view_with_next_url(self): self.set_permissions(True) edit_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:edit", args=[quote(self.test_snippet.pk)], ) # Post unschedule page response = self.client.post(self.unschedule_url + f"?next={edit_url}") # Check that the user was redirected to the next url self.assertRedirects(response, edit_url) self.test_snippet.refresh_from_db() self.latest_revision.refresh_from_db() # Check that the revision is no longer scheduled self.assertIsNone(self.latest_revision.approved_go_live_at) # No revisions with approved_go_live_at self.assertFalse( Revision.objects.for_instance(self.test_snippet) .exclude(approved_go_live_at__isnull=True) .exists() ) class TestSnippetUnpublish(WagtailTestUtils, TestCase): def setUp(self): self.user = self.login() self.snippet = DraftStateCustomPrimaryKeyModel.objects.create( custom_id="custom/1", text="to be unpublished" ) self.unpublish_url = reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish", args=(quote(self.snippet.pk),), ) def test_unpublish_view(self): """ This tests that the unpublish view responds with an unpublish confirm page """ # Get unpublish page response = self.client.get(self.unpublish_url) # Check that the user received an unpublish confirm page self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/confirm_unpublish.html") def test_unpublish_view_invalid_pk(self): """ This tests that the unpublish view returns an error if the object pk is invalid """ # Get unpublish page response = self.client.get( reverse( "wagtailsnippets_tests_draftstatecustomprimarykeymodel:unpublish", args=(quote(12345),), ) ) # Check that the user received a 404 response self.assertEqual(response.status_code, 404) def test_unpublish_view_get_bad_permissions(self): """ This tests that the unpublish view doesn't allow users without unpublish permissions """ # Remove privileges from user self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() # Get unpublish page response = self.client.get(self.unpublish_url) # Check that the user received a 302 redirected response self.assertEqual(response.status_code, 302) def test_unpublish_view_post_bad_permissions(self): """ This tests that the unpublish view doesn't allow users without unpublish permissions """ # Connect a mock signal handler to unpublished signal mock_handler = mock.MagicMock() unpublished.connect(mock_handler) try: # Remove privileges from user self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() # Post to the unpublish view response = self.client.post(self.unpublish_url) # Should be redirected to the home page self.assertRedirects(response, reverse("wagtailadmin_home")) # Check that the object was not unpublished self.assertTrue( DraftStateCustomPrimaryKeyModel.objects.get(pk=self.snippet.pk).live ) # Check that the unpublished signal was not fired self.assertEqual(mock_handler.call_count, 0) finally: unpublished.disconnect(mock_handler) def test_unpublish_view_post_with_publish_permission(self): """ This posts to the unpublish view and checks that the object was unpublished, using a specific publish permission instead of relying on the superuser flag """ # Connect a mock signal handler to unpublished signal mock_handler = mock.MagicMock() unpublished.connect(mock_handler) try: # Only add edit and publish permissions self.user.is_superuser = False edit_permission = Permission.objects.get( content_type__app_label="tests", codename="change_draftstatecustomprimarykeymodel", ) publish_permission = Permission.objects.get( content_type__app_label="tests", codename="publish_draftstatecustomprimarykeymodel", ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) self.user.user_permissions.add( edit_permission, publish_permission, admin_permission, ) self.user.save() # Post to the unpublish view response = self.client.post(self.unpublish_url) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # Check that the object was unpublished self.assertFalse( DraftStateCustomPrimaryKeyModel.objects.get(pk=self.snippet.pk).live ) # Check that the unpublished signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel) self.assertEqual(mock_call["instance"], self.snippet) self.assertIsInstance( mock_call["instance"], DraftStateCustomPrimaryKeyModel ) finally: unpublished.disconnect(mock_handler) def test_unpublish_view_post(self): """ This posts to the unpublish view and checks that the object was unpublished """ # Connect a mock signal handler to unpublished signal mock_handler = mock.MagicMock() unpublished.connect(mock_handler) try: # Post to the unpublish view response = self.client.post(self.unpublish_url) # Should be redirected to the listing page self.assertRedirects( response, reverse("wagtailsnippets_tests_draftstatecustomprimarykeymodel:list"), ) # Check that the object was unpublished self.assertFalse( DraftStateCustomPrimaryKeyModel.objects.get(pk=self.snippet.pk).live ) # Check that the unpublished signal was fired self.assertEqual(mock_handler.call_count, 1) mock_call = mock_handler.mock_calls[0][2] self.assertEqual(mock_call["sender"], DraftStateCustomPrimaryKeyModel) self.assertEqual(mock_call["instance"], self.snippet) self.assertIsInstance( mock_call["instance"], DraftStateCustomPrimaryKeyModel ) finally: unpublished.disconnect(mock_handler) def test_after_unpublish_hook(self): def hook_func(request, snippet): self.assertIsInstance(request, HttpRequest) self.assertEqual(snippet.pk, self.snippet.pk) return HttpResponse("Overridden!") with self.register_hook("after_unpublish", hook_func): post_data = {} response = self.client.post(self.unpublish_url, post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") self.snippet.refresh_from_db() self.assertEqual(self.snippet.status_string, "draft") def test_before_unpublish(self): def hook_func(request, snippet): self.assertIsInstance(request, HttpRequest) self.assertEqual(snippet.pk, self.snippet.pk) return HttpResponse("Overridden!") with self.register_hook("before_unpublish", hook_func): post_data = {} response = self.client.post(self.unpublish_url, post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # The hook response is served before unpublish is called. self.snippet.refresh_from_db() self.assertEqual(self.snippet.status_string, "live") class TestSnippetDelete(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.test_snippet = Advert.objects.get(pk=1) self.user = self.login() def test_delete_get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.client.get( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) ) self.assertEqual(response.status_code, 302) def test_delete_get(self): delete_url = reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) response = self.client.get(delete_url) self.assertEqual(response.status_code, 200) self.assertContains(response, "Yes, delete") self.assertContains(response, delete_url) @override_settings(WAGTAIL_I18N_ENABLED=True) def test_delete_get_with_i18n_enabled(self): delete_url = reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) response = self.client.get(delete_url) self.assertEqual(response.status_code, 200) self.assertContains(response, "Yes, delete") self.assertContains(response, delete_url) def test_delete_get_with_protected_reference(self): VariousOnDeleteModel.objects.create( text="Undeletable", on_delete_protect=self.test_snippet ) delete_url = reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) response = self.client.get(delete_url) self.assertEqual(response.status_code, 200) self.assertContains(response, "This advert is referenced 1 time.") self.assertContains( response, "One or more references to this advert prevent it from being deleted.", ) self.assertContains( response, reverse( "wagtailsnippets_tests_advert:usage", args=[quote(self.test_snippet.pk)], ) + "?describe_on_delete=1", ) self.assertNotContains(response, "Yes, delete") self.assertNotContains(response, delete_url) def test_delete_post_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.client.post( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) ) self.assertEqual(response.status_code, 302) def test_delete_post(self): response = self.client.post( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) ) # Should be redirected to the listing page self.assertRedirects(response, reverse("wagtailsnippets_tests_advert:list")) # Check that the page is gone self.assertEqual(Advert.objects.filter(text="test_advert").count(), 0) def test_delete_post_with_protected_reference(self): VariousOnDeleteModel.objects.create( text="Undeletable", on_delete_protect=self.test_snippet ) delete_url = reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) response = self.client.post(delete_url) # Should throw a PermissionDenied error and redirect to the dashboard self.assertEqual(response.status_code, 302) self.assertRedirects(response, reverse("wagtailadmin_home")) # Check that the snippet is still here self.assertTrue(Advert.objects.filter(pk=self.test_snippet.pk).exists()) def test_usage_link(self): output = StringIO() management.call_command("rebuild_references_index", stdout=output) response = self.client.get( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(self.test_snippet.pk)], ) ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/confirm_delete.html") self.assertContains(response, "This advert is referenced 2 times") self.assertContains( response, reverse( "wagtailsnippets_tests_advert:usage", args=[quote(self.test_snippet.pk)], ) + "?describe_on_delete=1", ) def test_before_delete_snippet_hook_get(self): advert = Advert.objects.create( url="http://www.example.com/", text="Test hook", ) def hook_func(request, instances): self.assertIsInstance(request, HttpRequest) self.assertQuerySetEqual(instances, [""], transform=repr) return HttpResponse("Overridden!") with self.register_hook("before_delete_snippet", hook_func): response = self.client.get( reverse("wagtailsnippets_tests_advert:delete", args=[quote(advert.pk)]) ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") def test_before_delete_snippet_hook_post(self): advert = Advert.objects.create( url="http://www.example.com/", text="Test hook", ) def hook_func(request, instances): self.assertIsInstance(request, HttpRequest) self.assertQuerySetEqual(instances, [""], transform=repr) return HttpResponse("Overridden!") with self.register_hook("before_delete_snippet", hook_func): response = self.client.post( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(advert.pk)], ) ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted before advert was deleted self.assertTrue(Advert.objects.filter(pk=advert.pk).exists()) def test_after_delete_snippet_hook(self): advert = Advert.objects.create( url="http://www.example.com/", text="Test hook", ) def hook_func(request, instances): self.assertIsInstance(request, HttpRequest) self.assertQuerySetEqual(instances, [""], transform=repr) return HttpResponse("Overridden!") with self.register_hook("after_delete_snippet", hook_func): response = self.client.post( reverse( "wagtailsnippets_tests_advert:delete", args=[quote(advert.pk)], ) ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"Overridden!") # Request intercepted after advert was deleted self.assertFalse(Advert.objects.filter(pk=advert.pk).exists()) class TestSnippetChooserPanel(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.request = RequestFactory().get("/") user = AnonymousUser() # technically, Anonymous users cannot access the admin self.request.user = user model = SnippetChooserModel self.advert_text = "Test advert text" test_snippet = model.objects.create( advert=Advert.objects.create(text=self.advert_text) ) self.edit_handler = get_edit_handler(model) self.form_class = self.edit_handler.get_form_class() form = self.form_class(instance=test_snippet) edit_handler = self.edit_handler.get_bound_panel( instance=test_snippet, form=form, request=self.request ) self.snippet_chooser_panel = [ panel for panel in edit_handler.children if getattr(panel, "field_name", None) == "advert" ][0] def test_render_html(self): field_html = self.snippet_chooser_panel.render_html() self.assertIn(self.advert_text, field_html) self.assertIn("Choose advert", field_html) self.assertIn("Choose another advert", field_html) self.assertIn("icon icon-snippet icon", field_html) def test_render_as_empty_field(self): test_snippet = SnippetChooserModel() form = self.form_class(instance=test_snippet) edit_handler = self.edit_handler.get_bound_panel( instance=test_snippet, form=form, request=self.request ) snippet_chooser_panel = [ panel for panel in edit_handler.children if getattr(panel, "field_name", None) == "advert" ][0] field_html = snippet_chooser_panel.render_html() self.assertIn("Choose advert", field_html) self.assertIn("Choose another advert", field_html) def test_render_js(self): self.assertIn( 'new SnippetChooser("id_advert", {"modalUrl": "/admin/snippets/choose/tests/advert/"});', self.snippet_chooser_panel.render_html(), ) def test_target_model_autodetected(self): edit_handler = ObjectList([FieldPanel("advert")]).bind_to_model( SnippetChooserModel ) form_class = edit_handler.get_form_class() form = form_class() widget = form.fields["advert"].widget self.assertIsInstance(widget, AdminSnippetChooser) self.assertEqual(widget.model, Advert) class TestSnippetRegistering(TestCase): def test_register_function(self): self.assertIn(RegisterFunction, SNIPPET_MODELS) def test_register_decorator(self): # Misbehaving decorators often return None self.assertIsNotNone(RegisterDecorator) self.assertIn(RegisterDecorator, SNIPPET_MODELS) class TestSnippetOrdering(TestCase): def setUp(self): register_snippet(ZuluSnippet) register_snippet(AlphaSnippet) def test_snippets_ordering(self): # Ensure AlphaSnippet is before ZuluSnippet # Cannot check first and last position as other snippets # may get registered elsewhere during test self.assertLess( SNIPPET_MODELS.index(AlphaSnippet), SNIPPET_MODELS.index(ZuluSnippet) ) class TestSnippetHistory(WagtailTestUtils, TestCase): fixtures = ["test.json"] def get(self, snippet, params={}): return self.client.get(self.get_url(snippet, "history"), params) def get_url(self, snippet, url_name, args=None): if args is None: args = [quote(snippet.pk)] return reverse(snippet.snippet_viewset.get_url_name(url_name), args=args) def setUp(self): self.user = self.login() self.non_revisable_snippet = Advert.objects.get(pk=1) ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(Advert), label="Test Advert", action="wagtail.create", timestamp=make_aware(datetime.datetime(2021, 9, 30, 10, 1, 0)), object_id="1", ) ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(Advert), label="Test Advert Updated", action="wagtail.edit", timestamp=make_aware(datetime.datetime(2022, 5, 10, 12, 34, 0)), object_id="1", ) self.revisable_snippet = FullFeaturedSnippet.objects.create(text="Foo") self.initial_revision = self.revisable_snippet.save_revision(user=self.user) ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(FullFeaturedSnippet), label="Foo", action="wagtail.create", timestamp=make_aware(datetime.datetime(2022, 5, 10, 20, 22, 0)), object_id=self.revisable_snippet.pk, revision=self.initial_revision, content_changed=True, ) self.revisable_snippet.text = "Bar" self.edit_revision = self.revisable_snippet.save_revision( user=self.user, log_action=True ) def test_simple(self): response = self.get(self.non_revisable_snippet) self.assertEqual(response.status_code, 200) self.assertContains(response, "Created", html=True) self.assertContains( response, 'data-w-tooltip-content-value="Sept. 30, 2021, 10:01 a.m."', ) def test_filters(self): # Should work on both non-revisable and revisable snippets snippets = [self.non_revisable_snippet, self.revisable_snippet] for snippet in snippets: with self.subTest(snippet=snippet): response = self.get(snippet, {"action": "wagtail.edit"}) self.assertEqual(response.status_code, 200) self.assertContains(response, "Edited", count=1) self.assertNotContains(response, "Created") soup = self.get_soup(response.content) filter = soup.select_one(".w-active-filters .w-pill") clear_button = filter.select_one(".w-pill__remove") self.assertEqual( filter.get_text(separator=" ", strip=True), "Action: Edit", ) self.assertIsNotNone(clear_button) url, params = clear_button.attrs.get("data-w-swap-src-value").split("?") self.assertEqual(url, self.get_url(snippet, "history_results")) self.assertNotIn("action=wagtail.edit", params) def test_should_not_show_actions_on_non_revisable_snippet(self): response = self.get(self.non_revisable_snippet) edit_url = self.get_url(self.non_revisable_snippet, "edit") self.assertNotContains( response, f'
Edit', ) def test_should_show_actions_on_revisable_snippet(self): response = self.get(self.revisable_snippet) edit_url = self.get_url(self.revisable_snippet, "edit") revert_url = self.get_url( self.revisable_snippet, "revisions_revert", args=[self.revisable_snippet.pk, self.initial_revision.pk], ) # Should not show the "live version" or "current draft" status tags self.assertNotContains( response, 'Live version' ) self.assertNotContains( response, 'Current draft' ) # The latest revision should have an "Edit" action instead of "Review" self.assertContains( response, f'Edit', count=1, ) # Any other revision should have a "Review" action self.assertContains( response, f'Review this version', count=1, ) def test_with_live_and_draft_status(self): snippet = DraftStateModel.objects.create(text="Draft-enabled Foo, Published") snippet.save_revision().publish() snippet.refresh_from_db() snippet.text = "Draft-enabled Bar, In Draft" snippet.save_revision(log_action=True) response = self.get(snippet) # Should show the "live version" status tag for the published revision self.assertContains( response, 'Live version', count=1, html=True, ) # Should show the "current draft" status tag for the draft revision self.assertContains( response, 'Current draft', count=1, html=True, ) soup = self.get_soup(response.content) sublabel = soup.select_one(".w-breadcrumbs__sublabel") # Should use the latest draft title in the breadcrumbs sublabel self.assertEqual(sublabel.get_text(strip=True), "Draft-enabled Bar, In Draft") @override_settings(WAGTAIL_I18N_ENABLED=True) def test_get_with_i18n_enabled(self): response = self.get(self.non_revisable_snippet) self.assertEqual(response.status_code, 200) response = self.get(self.revisable_snippet) self.assertEqual(response.status_code, 200) def test_num_queries(self): snippet = self.revisable_snippet # Warm up the cache self.get(snippet) with self.assertNumQueries(14): self.get(snippet) for i in range(20): revision = snippet.save_revision(user=self.user, log_action=True) if i % 5 == 0: revision.publish(user=self.user, log_action=True) # Should have the same number of queries as before (no N+1 queries) with self.assertNumQueries(14): self.get(snippet) class TestSnippetRevisions(WagtailTestUtils, TestCase): @property def revert_url(self): return self.get_url( "revisions_revert", args=[quote(self.snippet.pk), self.initial_revision.pk] ) def get(self): return self.client.get(self.revert_url) def post(self, post_data={}): return self.client.post(self.revert_url, post_data) def get_url(self, url_name, args=None): view_name = self.snippet.snippet_viewset.get_url_name(url_name) if args is None: args = [quote(self.snippet.pk)] return reverse(view_name, args=args) def setUp(self): self.user = self.login() with freeze_time("2022-05-10 11:00:00"): self.snippet = RevisableModel.objects.create(text="The original text") self.initial_revision = self.snippet.save_revision(user=self.user) ModelLogEntry.objects.create( content_type=ContentType.objects.get_for_model(RevisableModel), label="The original text", action="wagtail.create", timestamp=now(), object_id=self.snippet.pk, revision=self.initial_revision, content_changed=True, ) self.snippet.text = "The edited text" self.snippet.save() self.edit_revision = self.snippet.save_revision(user=self.user, log_action=True) def test_get_revert_revision(self): response = self.get() self.assertEqual(response.status_code, 200) if settings.USE_TZ: # the default timezone is "Asia/Tokyo", so we expect UTC +9 expected_date_string = "May 10, 2022, 8 p.m." else: expected_date_string = "May 10, 2022, 11 a.m." # Message should be shown self.assertContains( response, f"You are viewing a previous version of this Revisable model from {expected_date_string} by", count=1, ) # Form should show the content of the revision, not the current draft self.assertContains(response, "The original text", count=1) # Form action url should point to the revisions_revert view form_tag = f'' html = response.content.decode() self.assertTagInHTML(form_tag, html, count=1, allow_extra_attrs=True) # Buttons should be relabelled self.assertContains(response, "Replace current revision", count=1) def test_get_revert_revision_with_non_revisable_snippet(self): snippet = Advert.objects.create(text="foo") response = self.client.get( f"/admin/snippets/tests/advert/history/{snippet.pk}/revisions/1/revert/" ) self.assertEqual(response.status_code, 404) def test_get_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.get() self.assertEqual(response.status_code, 302) def test_get_with_draft_state_snippet(self): self.snippet = DraftStateModel.objects.create(text="Draft-enabled Foo") self.initial_revision = self.snippet.save_revision() response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # The save button should be labelled "Replace current draft" self.assertContains(response, "Replace current draft") # The publish button should exist self.assertContains(response, "Publish this version") # The publish button should have name="action-publish" self.assertContains( response, '', ) self.assertNotContains(response, "Unpublish") def test_get_with_previewable_snippet(self): self.snippet = MultiPreviewModesModel.objects.create(text="Preview-enabled foo") self.initial_revision = self.snippet.save_revision() self.snippet.text = "Preview-enabled bar" self.snippet.save_revision() response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") # Message should be shown self.assertContains( response, "You are viewing a previous version of this", count=1, ) # Form should show the content of the revision, not the current draft self.assertContains(response, "Preview-enabled foo") # Form action url should point to the revisions_revert view form_tag = f'' html = response.content.decode() self.assertTagInHTML(form_tag, html, count=1, allow_extra_attrs=True) # Buttons should be relabelled self.assertContains(response, "Replace current revision", count=1) # Should show the preview panel preview_url = self.get_url("preview_on_edit") self.assertContains(response, 'data-side-panel="preview"') self.assertContains(response, f'data-action="{preview_url}"') # Should have the preview side panel toggle button soup = self.get_soup(response.content) toggle_button = soup.find("button", {"data-side-panel-toggle": "preview"}) self.assertIsNotNone(toggle_button) self.assertEqual("w-tooltip w-kbd", toggle_button["data-controller"]) self.assertEqual("mod+p", toggle_button["data-w-kbd-key-value"]) def test_replace_revision(self): get_response = self.get() text_from_revision = get_response.context["form"].initial["text"] post_response = self.post( post_data={ "text": text_from_revision + " reverted", "revision": self.initial_revision.pk, } ) self.assertRedirects(post_response, self.get_url("list", args=[])) self.snippet.refresh_from_db() latest_revision = self.snippet.get_latest_revision() log_entry = ModelLogEntry.objects.filter(revision=latest_revision).first() # The instance should be updated self.assertEqual(self.snippet.text, "The original text reverted") # The initial revision, edited revision, and revert revision self.assertEqual(self.snippet.revisions.count(), 3) # The latest revision should be the revert revision self.assertEqual(latest_revision.content["text"], "The original text reverted") # A new log entry with "wagtail.revert" action should be created self.assertIsNotNone(log_entry) self.assertEqual(log_entry.action, "wagtail.revert") def test_replace_with_limited_permissions(self): self.user.is_superuser = False self.user.user_permissions.add( Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) ) self.user.save() response = self.post( post_data={ "text": "test text", "revision": self.initial_revision.pk, } ) self.assertEqual(response.status_code, 302) self.snippet.refresh_from_db() self.assertNotEqual(self.snippet.text, "test text") # Only the initial revision and edited revision, no revert revision self.assertEqual(self.snippet.revisions.count(), 2) def test_replace_draft(self): self.snippet = DraftStateModel.objects.create( text="Draft-enabled Foo", live=False ) self.initial_revision = self.snippet.save_revision() self.snippet.text = "Draft-enabled Foo edited" self.edit_revision = self.snippet.save_revision() get_response = self.get() text_from_revision = get_response.context["form"].initial["text"] post_response = self.post( post_data={ "text": text_from_revision + " reverted", "revision": self.initial_revision.pk, } ) self.assertRedirects(post_response, self.get_url("edit")) self.snippet.refresh_from_db() latest_revision = self.snippet.get_latest_revision() log_entry = ModelLogEntry.objects.filter(revision=latest_revision).first() publish_log_entries = ModelLogEntry.objects.filter( content_type=ContentType.objects.get_for_model(DraftStateModel), action="wagtail.publish", object_id=self.snippet.pk, ) # The instance should be updated, since it is still a draft self.assertEqual(self.snippet.text, "Draft-enabled Foo reverted") # The initial revision, edited revision, and revert revision self.assertEqual(self.snippet.revisions.count(), 3) # The latest revision should be the revert revision self.assertEqual(latest_revision.content["text"], "Draft-enabled Foo reverted") # A new log entry with "wagtail.revert" action should be created self.assertIsNotNone(log_entry) self.assertEqual(log_entry.action, "wagtail.revert") # There should be no log entries for the publish action self.assertEqual(publish_log_entries.count(), 0) # The instance should still be a draft self.assertFalse(self.snippet.live) self.assertTrue(self.snippet.has_unpublished_changes) self.assertIsNone(self.snippet.first_published_at) self.assertIsNone(self.snippet.last_published_at) self.assertIsNone(self.snippet.live_revision) def test_replace_publish(self): self.snippet = DraftStateModel.objects.create(text="Draft-enabled Foo") self.initial_revision = self.snippet.save_revision() self.snippet.text = "Draft-enabled Foo edited" self.edit_revision = self.snippet.save_revision() get_response = self.get() text_from_revision = get_response.context["form"].initial["text"] timestamp = now() with freeze_time(timestamp): post_response = self.post( post_data={ "text": text_from_revision + " reverted", "revision": self.initial_revision.pk, "action-publish": "action-publish", } ) self.assertRedirects(post_response, self.get_url("list", args=[])) self.snippet.refresh_from_db() latest_revision = self.snippet.get_latest_revision() log_entry = ModelLogEntry.objects.filter(revision=latest_revision).first() revert_log_entries = ModelLogEntry.objects.filter( content_type=ContentType.objects.get_for_model(DraftStateModel), action="wagtail.revert", object_id=self.snippet.pk, ) # The instance should be updated self.assertEqual(self.snippet.text, "Draft-enabled Foo reverted") # The initial revision, edited revision, and revert revision self.assertEqual(self.snippet.revisions.count(), 3) # The latest revision should be the revert revision self.assertEqual(latest_revision.content["text"], "Draft-enabled Foo reverted") # The latest log entry should use the "wagtail.publish" action self.assertIsNotNone(log_entry) self.assertEqual(log_entry.action, "wagtail.publish") # There should be a log entry for the revert action self.assertEqual(revert_log_entries.count(), 1) # The instance should be live self.assertTrue(self.snippet.live) self.assertFalse(self.snippet.has_unpublished_changes) self.assertEqual(self.snippet.first_published_at, timestamp) self.assertEqual(self.snippet.last_published_at, timestamp) self.assertEqual(self.snippet.live_revision, self.snippet.latest_revision) class TestCompareRevisions(WagtailTestUtils, TestCase): # Actual tests for the comparison classes can be found in test_compare.py def setUp(self): self.snippet = RevisableModel.objects.create(text="Initial revision") self.initial_revision = self.snippet.save_revision() self.initial_revision.created_at = make_aware(datetime.datetime(2022, 5, 10)) self.initial_revision.save() self.snippet.text = "First edit" self.edit_revision = self.snippet.save_revision() self.edit_revision.created_at = make_aware(datetime.datetime(2022, 5, 11)) self.edit_revision.save() self.snippet.text = "Final revision" self.final_revision = self.snippet.save_revision() self.final_revision.created_at = make_aware(datetime.datetime(2022, 5, 12)) self.final_revision.save() self.login() def get(self, revision_a_id, revision_b_id): compare_url = reverse( "wagtailsnippets_tests_revisablemodel:revisions_compare", args=(quote(self.snippet.pk), revision_a_id, revision_b_id), ) return self.client.get(compare_url) def test_compare_revisions(self): response = self.get(self.initial_revision.pk, self.edit_revision.pk) self.assertEqual(response.status_code, 200) self.assertContains( response, 'Initial revisionFirst edit', html=True, ) def test_compare_revisions_earliest(self): response = self.get("earliest", self.edit_revision.pk) self.assertEqual(response.status_code, 200) self.assertContains( response, 'Initial revisionFirst edit', html=True, ) def test_compare_revisions_latest(self): response = self.get(self.edit_revision.id, "latest") self.assertEqual(response.status_code, 200) self.assertContains( response, 'First editFinal revision', html=True, ) def test_compare_revisions_live(self): # Mess with the live version, bypassing revisions self.snippet.text = "Live edited" self.snippet.save(update_fields=["text"]) response = self.get(self.final_revision.id, "live") self.assertEqual(response.status_code, 200) self.assertContains( response, 'Final revisionLive edited', html=True, ) class TestCompareRevisionsWithPerUserPanels(WagtailTestUtils, TestCase): def setUp(self): self.snippet = RevisableChildModel.objects.create( text="Foo bar", secret_text="Secret text" ) self.old_revision = self.snippet.save_revision() self.snippet.text = "Foo baz" self.snippet.secret_text = "Secret unseen note" self.new_revision = self.snippet.save_revision() self.compare_url = reverse( "wagtailsnippets_tests_revisablechildmodel:revisions_compare", args=(quote(self.snippet.pk), self.old_revision.pk, self.new_revision.pk), ) def test_comparison_as_superuser(self): self.login() response = self.client.get(self.compare_url) self.assertEqual(response.status_code, 200) self.assertContains( response, 'Foo barbaz', html=True, ) self.assertContains( response, 'Secret textunseen note', html=True, ) def test_comparison_as_ordinary_user(self): user = self.create_user(username="editor", password="password") add_permission = Permission.objects.get( content_type__app_label="tests", codename="change_revisablechildmodel" ) admin_permission = Permission.objects.get( content_type__app_label="wagtailadmin", codename="access_admin" ) user.user_permissions.add(add_permission, admin_permission) self.login(username="editor", password="password") response = self.client.get(self.compare_url) self.assertEqual(response.status_code, 200) self.assertContains( response, 'Foo barbaz', html=True, ) self.assertNotContains(response, "unseen note") class TestSnippetChoose(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.login() self.url_args = ["tests", "advert"] def get(self, params=None): app_label, model_name = self.url_args return self.client.get( reverse(f"wagtailsnippetchoosers_{app_label}_{model_name}:choose"), params or {}, ) def test_simple(self): response = self.get() self.assertTemplateUsed(response, "wagtailadmin/generic/chooser/chooser.html") # Check locale filter doesn't exist normally self.assertNotIn( '', js_args[0] ) self.assertIn("Choose advert", js_args[0]) self.assertEqual(js_args[1], "__ID__") class TestSnippetListViewWithCustomPrimaryKey(WagtailTestUtils, TestCase): def setUp(self): self.login() # Create some instances of the searchable snippet for testing self.snippet_a = StandardSnippetWithCustomPrimaryKey.objects.create( snippet_id="snippet/01", text="Hello" ) self.snippet_b = StandardSnippetWithCustomPrimaryKey.objects.create( snippet_id="snippet/02", text="Hello" ) self.snippet_c = StandardSnippetWithCustomPrimaryKey.objects.create( snippet_id="snippet/03", text="Hello" ) def get(self, params={}): return self.client.get( reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list" ), params, ) def test_simple(self): response = self.get() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/index.html") # All snippets should be in items items = list(response.context["page_obj"].object_list) self.assertIn(self.snippet_a, items) self.assertIn(self.snippet_b, items) self.assertIn(self.snippet_c, items) class TestSnippetViewWithCustomPrimaryKey(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): super().setUp() self.login() self.snippet_a = StandardSnippetWithCustomPrimaryKey.objects.create( snippet_id="snippet/01", text="Hello" ) def get(self, snippet, params={}): args = [quote(snippet.pk)] return self.client.get( reverse(snippet.snippet_viewset.get_url_name("edit"), args=args), params, ) def post(self, snippet, post_data={}): args = [quote(snippet.pk)] return self.client.post( reverse(snippet.snippet_viewset.get_url_name("edit"), args=args), post_data, ) def create(self, snippet, post_data={}, model=Advert): return self.client.post( reverse(snippet.snippet_viewset.get_url_name("add")), post_data, ) def test_show_edit_view(self): response = self.get(self.snippet_a) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailsnippets/snippets/edit.html") def test_edit_invalid(self): response = self.post(self.snippet_a, post_data={"foo": "bar"}) self.assertContains( response, "The standard snippet with custom primary key could not be saved due to errors.", ) self.assertContains(response, "This field is required.") def test_edit(self): response = self.post( self.snippet_a, post_data={"text": "Edited snippet", "snippet_id": "snippet_id_edited"}, ) self.assertRedirects( response, reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list" ), ) snippets = StandardSnippetWithCustomPrimaryKey.objects.all() self.assertEqual(snippets.count(), 2) self.assertEqual(snippets.last().snippet_id, "snippet_id_edited") def test_create(self): response = self.create( self.snippet_a, post_data={"text": "test snippet", "snippet_id": "snippet/02"}, ) self.assertRedirects( response, reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:list" ), ) snippets = StandardSnippetWithCustomPrimaryKey.objects.all() self.assertEqual(snippets.count(), 2) self.assertEqual(snippets.last().text, "test snippet") def test_get_delete(self): response = self.client.get( reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:delete", args=[quote(self.snippet_a.pk)], ) ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/confirm_delete.html") def test_usage_link(self): response = self.client.get( reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:delete", args=[quote(self.snippet_a.pk)], ) ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "wagtailadmin/generic/confirm_delete.html") self.assertContains( response, "This standard snippet with custom primary key is referenced 0 times", ) self.assertContains( response, reverse( "wagtailsnippets_snippetstests_standardsnippetwithcustomprimarykey:usage", args=[quote(self.snippet_a.pk)], ) + "?describe_on_delete=1", ) def test_redirect_to_edit(self): with self.assertWarnsRegex( RemovedInWagtail70Warning, "`//` edit view URL pattern has been deprecated in favour of /edit//.", ): response = self.client.get( "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/snippet_2F01/" ) self.assertRedirects( response, "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/edit/snippet_2F01/", status_code=301, ) def test_redirect_to_delete(self): with self.assertWarnsRegex( RemovedInWagtail70Warning, "`//delete/` delete view URL pattern has been deprecated in favour of /delete//.", ): response = self.client.get( "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/snippet_2F01/delete/" ) self.assertRedirects( response, "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/delete/snippet_2F01/", status_code=301, ) def test_redirect_to_usage(self): with self.assertWarnsRegex( RemovedInWagtail70Warning, "`//usage/` usage view URL pattern has been deprecated in favour of /usage//.", ): response = self.client.get( "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/snippet_2F01/usage/" ) self.assertRedirects( response, "/admin/snippets/snippetstests/standardsnippetwithcustomprimarykey/usage/snippet_2F01/", status_code=301, ) class TestSnippetChooserBlockWithCustomPrimaryKey(TestCase): fixtures = ["test.json"] def test_serialize(self): """The value of a SnippetChooserBlock (a snippet instance) should serialize to an ID""" block = SnippetChooserBlock(AdvertWithCustomPrimaryKey) test_advert = AdvertWithCustomPrimaryKey.objects.get(pk="advert/01") self.assertEqual(block.get_prep_value(test_advert), test_advert.pk) # None should serialize to None self.assertIsNone(block.get_prep_value(None)) def test_deserialize(self): """The serialized value of a SnippetChooserBlock (an ID) should deserialize to a snippet instance""" block = SnippetChooserBlock(AdvertWithCustomPrimaryKey) test_advert = AdvertWithCustomPrimaryKey.objects.get(pk="advert/01") self.assertEqual(block.to_python(test_advert.pk), test_advert) # None should deserialize to None self.assertIsNone(block.to_python(None)) def test_adapt(self): block = SnippetChooserBlock( AdvertWithCustomPrimaryKey, help_text="pick an advert, any advert" ) block.set_name("test_snippetchooserblock") js_args = FieldBlockAdapter().js_args(block) self.assertEqual(js_args[0], "test_snippetchooserblock") self.assertIsInstance(js_args[1], AdminSnippetChooser) self.assertEqual(js_args[1].model, AdvertWithCustomPrimaryKey) self.assertEqual( js_args[2], { "label": "Test snippetchooserblock", "required": True, "icon": "snippet", "helpText": "pick an advert, any advert", "classname": "w-field w-field--model_choice_field w-field--admin_snippet_chooser", "showAddCommentButton": True, "strings": {"ADD_COMMENT": "Add Comment"}, }, ) def test_form_response(self): block = SnippetChooserBlock(AdvertWithCustomPrimaryKey) test_advert = AdvertWithCustomPrimaryKey.objects.get(pk="advert/01") value = block.value_from_datadict( {"advertwithcustomprimarykey": str(test_advert.pk)}, {}, "advertwithcustomprimarykey", ) self.assertEqual(value, test_advert) empty_value = block.value_from_datadict( {"advertwithcustomprimarykey": ""}, {}, "advertwithcustomprimarykey" ) self.assertIsNone(empty_value) def test_clean(self): required_block = SnippetChooserBlock(AdvertWithCustomPrimaryKey) nonrequired_block = SnippetChooserBlock( AdvertWithCustomPrimaryKey, required=False ) test_advert = AdvertWithCustomPrimaryKey.objects.get(pk="advert/01") self.assertEqual(required_block.clean(test_advert), test_advert) with self.assertRaises(ValidationError): required_block.clean(None) self.assertEqual(nonrequired_block.clean(test_advert), test_advert) self.assertIsNone(nonrequired_block.clean(None)) class TestSnippetChooserPanelWithCustomPrimaryKey(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.request = RequestFactory().get("/") user = AnonymousUser() # technically, Anonymous users cannot access the admin self.request.user = user model = SnippetChooserModelWithCustomPrimaryKey self.advert_text = "Test advert text" test_snippet = model.objects.create( advertwithcustomprimarykey=AdvertWithCustomPrimaryKey.objects.create( advert_id="advert/02", text=self.advert_text ) ) self.edit_handler = get_edit_handler(model) self.form_class = self.edit_handler.get_form_class() form = self.form_class(instance=test_snippet) edit_handler = self.edit_handler.get_bound_panel( instance=test_snippet, form=form, request=self.request ) self.snippet_chooser_panel = [ panel for panel in edit_handler.children if getattr(panel, "field_name", None) == "advertwithcustomprimarykey" ][0] def test_render_html(self): field_html = self.snippet_chooser_panel.render_html() self.assertIn(self.advert_text, field_html) self.assertIn("Choose advert with custom primary key", field_html) self.assertIn("Choose another advert with custom primary key", field_html) def test_render_as_empty_field(self): test_snippet = SnippetChooserModelWithCustomPrimaryKey() form = self.form_class(instance=test_snippet) edit_handler = self.edit_handler.get_bound_panel( instance=test_snippet, form=form, request=self.request ) snippet_chooser_panel = [ panel for panel in edit_handler.children if getattr(panel, "field_name", None) == "advertwithcustomprimarykey" ][0] field_html = snippet_chooser_panel.render_html() self.assertIn("Choose advert with custom primary key", field_html) self.assertIn("Choose another advert with custom primary key", field_html) def test_render_js(self): self.assertIn( 'new SnippetChooser("id_advertwithcustomprimarykey", {"modalUrl": "/admin/snippets/choose/tests/advertwithcustomprimarykey/"});', self.snippet_chooser_panel.render_html(), ) def test_target_model_autodetected(self): edit_handler = ObjectList( [FieldPanel("advertwithcustomprimarykey")] ).bind_to_model(SnippetChooserModelWithCustomPrimaryKey) form_class = edit_handler.get_form_class() form = form_class() widget = form.fields["advertwithcustomprimarykey"].widget self.assertIsInstance(widget, AdminSnippetChooser) self.assertEqual(widget.model, AdvertWithCustomPrimaryKey) class TestSnippetChooseWithCustomPrimaryKey(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.login() def get(self, params=None): return self.client.get( reverse("wagtailsnippetchoosers_tests_advertwithcustomprimarykey:choose"), params or {}, ) def test_simple(self): response = self.get() self.assertTemplateUsed(response, "wagtailadmin/generic/chooser/chooser.html") self.assertEqual(response.context["header_icon"], "snippet") self.assertEqual(response.context["icon"], "snippet") def test_ordering(self): """ Listing should be ordered by PK if no ordering has been set on the model """ AdvertWithCustomPrimaryKey.objects.all().delete() for i in range(10, 0, -1): AdvertWithCustomPrimaryKey.objects.create(pk=i, text="advert %d" % i) response = self.get() self.assertEqual(response.status_code, 200) self.assertEqual(response.context["results"][0].text, "advert 1") class TestSnippetChosenWithCustomPrimaryKey(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.login() def get(self, pk, params=None): return self.client.get( reverse( "wagtailsnippetchoosers_tests_advertwithcustomprimarykey:chosen", args=(quote(pk),), ), params or {}, ) def test_choose_a_page(self): response = self.get(pk=AdvertWithCustomPrimaryKey.objects.all()[0].pk) response_json = json.loads(response.content.decode()) self.assertEqual(response_json["step"], "chosen") class TestSnippetChosenWithCustomUUIDPrimaryKey(WagtailTestUtils, TestCase): fixtures = ["test.json"] def setUp(self): self.login() def get(self, pk, params=None): return self.client.get( reverse( "wagtailsnippetchoosers_tests_advertwithcustomuuidprimarykey:chosen", args=(quote(pk),), ), params or {}, ) def test_choose_a_page(self): response = self.get(pk=AdvertWithCustomUUIDPrimaryKey.objects.all()[0].pk) response_json = json.loads(response.content.decode()) self.assertEqual(response_json["step"], "chosen") class TestPanelConfigurationChecks(WagtailTestUtils, TestCase): def setUp(self): self.warning_id = "wagtailadmin.W002" def get_checks_result(): # run checks only with the 'panels' tag checks_result = checks.run_checks(tags=["panels"]) return [ warning for warning in checks_result if warning.id == self.warning_id ] self.get_checks_result = get_checks_result def test_model_with_single_tabbed_panel_only(self): StandardSnippet.content_panels = [FieldPanel("text")] warning = checks.Warning( "StandardSnippet.content_panels will have no effect on snippets editing", hint="""Ensure that StandardSnippet uses `panels` instead of `content_panels`\ or set up an `edit_handler` if you want a tabbed editing interface. There are no default tabs on non-Page models so there will be no\ Content tab for the content_panels to render in.""", obj=StandardSnippet, id="wagtailadmin.W002", ) checks_results = self.get_checks_result() self.assertEqual([warning], checks_results) # clean up for future checks delattr(StandardSnippet, "content_panels")