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'