import datetime
import unittest
from unittest.mock import Mock

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser, Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.http import Http404
from django.test import Client, TestCase, override_settings
from django.test.client import RequestFactory
from django.urls import reverse
from django.utils import timezone, translation
from freezegun import freeze_time

from wagtail.actions.copy_for_translation import ParentNotTranslatedError
from wagtail.coreutils import get_dummy_request
from wagtail.locks import BasicLock, ScheduledForPublishLock, WorkflowLock
from wagtail.models import (
    Comment,
    GroupApprovalTask,
    Locale,
    Page,
    PageLogEntry,
    PageManager,
    PageViewRestriction,
    Site,
    Workflow,
    WorkflowTask,
    get_page_models,
    get_translatable_models,
)
from wagtail.signals import page_published
from wagtail.test.testapp.models import (
    AbstractPage,
    Advert,
    AlwaysShowInMenusPage,
    BlogCategory,
    BlogCategoryBlogPage,
    BusinessChild,
    BusinessIndex,
    BusinessNowherePage,
    BusinessSubIndex,
    CustomManager,
    CustomManagerPage,
    CustomPageQuerySet,
    EventCategory,
    EventIndex,
    EventPage,
    EventPageSpeaker,
    GenericSnippetPage,
    ManyToManyBlogPage,
    MTIBasePage,
    MTIChildPage,
    MyCustomPage,
    OneToOnePage,
    PageWithExcludedCopyField,
    PageWithGenericRelation,
    RelatedGenericRelation,
    SimpleChildPage,
    SimplePage,
    SimpleParentPage,
    SingleEventPage,
    SingletonPage,
    StandardIndex,
    StreamPage,
    TaggedGrandchildPage,
    TaggedPage,
)
from wagtail.test.utils import WagtailTestUtils
from wagtail.url_routing import RouteResult


def get_ct(model):
    return ContentType.objects.get_for_model(model)


class TestValidation(TestCase):
    fixtures = ["test.json"]

    def test_can_create(self):
        """
        Check that basic page creation works
        """
        homepage = Page.objects.get(url_path="/home/")
        hello_page = SimplePage(
            title="Hello world", slug="hello-world", content="hello"
        )
        homepage.add_child(instance=hello_page)

        # check that hello_page exists in the db
        retrieved_page = Page.objects.get(id=hello_page.id)
        self.assertEqual(retrieved_page.title, "Hello world")

    def test_title_is_required(self):
        homepage = Page.objects.get(url_path="/home/")

        hello_page = SimplePage(slug="hello-world", content="hello")
        with self.assertRaises(ValidationError):
            homepage.add_child(instance=hello_page)

        hello_page = SimplePage(title="", slug="hello-world", content="hello")
        with self.assertRaises(ValidationError):
            homepage.add_child(instance=hello_page)

    def test_slug_is_autogenerated(self):
        homepage = Page.objects.get(url_path="/home/")

        # slug should be auto-assigned to a slugified version of the title
        hello_page = SimplePage(title="Hello world", content="hello")
        homepage.add_child(instance=hello_page)
        retrieved_page = Page.objects.get(id=hello_page.id)
        self.assertEqual(retrieved_page.slug, "hello-world")

        # auto-generated slug should receive a suffix to make it unique
        events_page = SimplePage(title="Events", content="hello")
        homepage.add_child(instance=events_page)
        retrieved_page = Page.objects.get(id=events_page.id)
        self.assertEqual(retrieved_page.slug, "events-2")

    def test_slug_must_be_unique_within_parent(self):
        homepage = Page.objects.get(url_path="/home/")

        events_page = SimplePage(title="Events", slug="events", content="hello")
        with self.assertRaises(ValidationError):
            homepage.add_child(instance=events_page)

    def test_slug_can_duplicate_other_sections(self):
        homepage = Page.objects.get(url_path="/home/")

        # the Events section has a page with slug='christmas', but we still allow
        # it as a slug elsewhere
        christmas_page = SimplePage(
            title="Christmas", slug="christmas", content="hello"
        )
        homepage.add_child(instance=christmas_page)
        self.assertTrue(Page.objects.filter(id=christmas_page.id).exists())

    @override_settings(WAGTAIL_ALLOW_UNICODE_SLUGS=True)
    def test_slug_generation_respects_unicode_setting_true(self):
        page = Page(title="A mööse bit me önce")
        Page.get_first_root_node().add_child(instance=page)
        self.assertEqual(page.slug, "a-mööse-bit-me-önce")

    @override_settings(WAGTAIL_ALLOW_UNICODE_SLUGS=False)
    def test_slug_generation_respects_unicode_setting_false(self):
        page = Page(title="A mööse bit me önce")
        Page.get_first_root_node().add_child(instance=page)
        self.assertEqual(page.slug, "a-moose-bit-me-once")

    def test_get_admin_display_title(self):
        homepage = Page.objects.get(url_path="/home/")
        self.assertEqual(homepage.draft_title, homepage.get_admin_display_title())

    def test_get_admin_display_title_with_blank_draft_title(self):
        # Display title should fall back on the live title if draft_title is blank;
        # this can happen if the page was created through fixtures or migrations that
        # didn't explicitly account for draft_title
        # (since draft_title doesn't get populated automatically on save in those cases)
        Page.objects.filter(url_path="/home/").update(
            title="live title", draft_title=""
        )
        homepage = Page.objects.get(url_path="/home/")

        self.assertEqual(homepage.get_admin_display_title(), "live title")

    def test_draft_title_is_autopopulated(self):
        homepage = Page.objects.get(url_path="/home/")

        hello_page = SimplePage(title="Hello world", content="hello")
        homepage.add_child(instance=hello_page)
        retrieved_page = Page.objects.get(id=hello_page.id)
        self.assertEqual(retrieved_page.draft_title, "Hello world")

        hello_page = SimplePage(
            title="Hello world", draft_title="Hello world edited", content="hello"
        )
        homepage.add_child(instance=hello_page)
        retrieved_page = Page.objects.get(id=hello_page.id)
        self.assertEqual(retrieved_page.draft_title, "Hello world edited")


@override_settings(
    ALLOWED_HOSTS=[
        "localhost",
        "events.example.com",
        "about.example.com",
        "unknown.site.com",
    ]
)
class TestSiteRouting(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.default_site = Site.objects.get(is_default_site=True)
        events_page = Page.objects.get(url_path="/home/events/")
        about_page = Page.objects.get(url_path="/home/about-us/")
        self.events_site = Site.objects.create(
            hostname="events.example.com", root_page=events_page
        )
        self.alternate_port_events_site = Site.objects.create(
            hostname="events.example.com", root_page=events_page, port="8765"
        )
        self.about_site = Site.objects.create(
            hostname="about.example.com", root_page=about_page
        )
        self.alternate_port_default_site = Site.objects.create(
            hostname=self.default_site.hostname,
            port="8765",
            root_page=self.default_site.root_page,
        )
        self.unrecognised_port = "8000"
        self.unrecognised_hostname = "unknown.site.com"

    def test_route_for_request_query_count(self):
        request = get_dummy_request(site=self.events_site)
        with self.assertNumQueries(2):
            # expect queries for site & page
            Page.route_for_request(request, request.path)
        with self.assertNumQueries(0):
            # subsequent lookups should be cached on the request
            Page.route_for_request(request, request.path)

    def test_route_for_request_value(self):
        request = get_dummy_request(site=self.events_site)
        self.assertFalse(hasattr(request, "_wagtail_route_for_request"))
        result = Page.route_for_request(request, request.path)
        self.assertTrue(isinstance(result, RouteResult))
        self.assertEqual(
            (result[0], result[1], result[2]),
            (self.events_site.root_page.specific, [], {}),
        )
        self.assertTrue(hasattr(request, "_wagtail_route_for_request"))
        self.assertIs(request._wagtail_route_for_request, result)

    def test_route_for_request_cached(self):
        request = get_dummy_request(site=self.events_site)
        m = Mock()
        request._wagtail_route_for_request = m
        with self.assertNumQueries(0):
            self.assertEqual(Page.route_for_request(request, request.path), m)

    def test_route_for_request_suppresses_404(self):
        request = get_dummy_request(path="does-not-exist", site=self.events_site)
        self.assertIsNone(Page.route_for_request(request, request.path))

    def test_find_for_request(self):
        request_200 = get_dummy_request(site=self.events_site)
        self.assertEqual(
            Page.find_for_request(request_200, request_200.path),
            self.events_site.root_page.specific,
        )
        request_404 = get_dummy_request(path="does-not-exist", site=self.events_site)
        self.assertEqual(
            Page.find_for_request(request_404, request_404.path),
            None,
        )

    def test_valid_headers_route_to_specific_site(self):
        # requests with a known Host: header should be directed to the specific site
        request = get_dummy_request(site=self.events_site)
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.events_site)

    def test_ports_in_request_headers_are_respected(self):
        # ports in the Host: header should be respected
        request = get_dummy_request(site=self.alternate_port_events_site)
        with self.assertNumQueries(1):
            self.assertEqual(
                Site.find_for_request(request), self.alternate_port_events_site
            )

    def test_unrecognised_host_header_routes_to_default_site(self):
        # requests with an unrecognised Host: header should be directed to the default site
        request = get_dummy_request()
        request.META["HTTP_HOST"] = self.unrecognised_hostname
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.default_site)

    def test_unrecognised_port_and_default_host_routes_to_default_site(self):
        # requests to the default host on an unrecognised port should be directed to the default site
        request = get_dummy_request(site=self.default_site)
        request.META["SERVER_PORT"] = self.unrecognised_port
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.default_site)

    def test_unrecognised_port_and_unrecognised_host_routes_to_default_site(self):
        # requests with an unrecognised Host: header _and_ an unrecognised port
        # should be directed to the default site
        request = get_dummy_request()
        request.META["HTTP_HOST"] = self.unrecognised_hostname
        request.META["SERVER_PORT"] = self.unrecognised_port
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.default_site)

    def test_unrecognised_port_on_known_hostname_routes_there_if_no_ambiguity(self):
        # requests on an unrecognised port should be directed to the site with
        # matching hostname if there is no ambiguity
        request = get_dummy_request()
        request.META["HTTP_HOST"] = self.about_site.hostname
        request.META["SERVER_PORT"] = self.unrecognised_port
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.about_site)

    def test_unrecognised_port_on_known_hostname_routes_to_default_site_if_ambiguity(
        self,
    ):
        # requests on an unrecognised port should be directed to the default
        # site, even if their hostname (but not port) matches more than one
        # other entry
        request = get_dummy_request(site=self.events_site)
        request.META["SERVER_PORT"] = self.unrecognised_port
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.default_site)

    def test_port_in_http_host_header_is_ignored(self):
        # port in the HTTP_HOST header is ignored
        request = get_dummy_request()
        request.META["HTTP_HOST"] = "{}:{}".format(
            self.events_site.hostname,
            self.events_site.port,
        )
        request.META["SERVER_PORT"] = self.alternate_port_events_site.port
        with self.assertNumQueries(1):
            self.assertEqual(
                Site.find_for_request(request), self.alternate_port_events_site
            )

    def test_site_with_disallowed_host(self):
        request = get_dummy_request()
        request.META["HTTP_HOST"] = "disallowed:80"
        with self.assertNumQueries(1):
            self.assertEqual(Site.find_for_request(request), self.default_site)


class TestRouting(TestCase):
    fixtures = ["test.json"]

    # need to clear urlresolver caches before/after tests, because we override ROOT_URLCONF
    # in some tests here
    def setUp(self):
        from django.urls import clear_url_caches

        clear_url_caches()

    def tearDown(self):
        from django.urls import clear_url_caches

        clear_url_caches()

    def test_urls(self):
        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        # Basic installation only has one site configured, so page.url will return local URLs
        self.assertEqual(
            homepage.get_url_parts(), (default_site.id, "http://localhost", "/")
        )
        self.assertEqual(homepage.full_url, "http://localhost/")
        self.assertEqual(homepage.url, "/")
        self.assertEqual(homepage.relative_url(default_site), "/")
        self.assertEqual(homepage.get_site(), default_site)

        self.assertEqual(
            christmas_page.get_url_parts(),
            (default_site.id, "http://localhost", "/events/christmas/"),
        )
        self.assertEqual(christmas_page.full_url, "http://localhost/events/christmas/")
        self.assertEqual(christmas_page.url, "/events/christmas/")
        self.assertEqual(
            christmas_page.relative_url(default_site), "/events/christmas/"
        )
        self.assertEqual(christmas_page.get_site(), default_site)

    def test_page_with_no_url(self):
        root = Page.objects.get(url_path="/")
        default_site = Site.objects.get(is_default_site=True)

        self.assertIsNone(root.get_url_parts())
        self.assertIsNone(root.full_url)
        self.assertIsNone(root.url)
        self.assertIsNone(root.relative_url(default_site))
        self.assertIsNone(root.get_site())

    @override_settings(
        ALLOWED_HOSTS=[
            "localhost",
            "testserver",
            "events.example.com",
            "second-events.example.com",
        ]
    )
    def test_urls_with_multiple_sites(self):
        events_page = Page.objects.get(url_path="/home/events/")
        events_site = Site.objects.create(
            hostname="events.example.com", root_page=events_page
        )

        # An underscore is not valid according to RFC 1034/1035
        # and will raise a DisallowedHost Exception
        second_events_site = Site.objects.create(
            hostname="second-events.example.com", root_page=events_page
        )

        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        # with multiple sites, page.url will return full URLs to ensure that
        # they work across sites
        self.assertEqual(
            homepage.get_url_parts(), (default_site.id, "http://localhost", "/")
        )
        self.assertEqual(homepage.full_url, "http://localhost/")
        self.assertEqual(homepage.url, "http://localhost/")
        self.assertEqual(homepage.relative_url(default_site), "/")
        self.assertEqual(homepage.relative_url(events_site), "http://localhost/")
        self.assertEqual(homepage.get_site(), default_site)

        self.assertEqual(
            christmas_page.get_url_parts(),
            (events_site.id, "http://events.example.com", "/christmas/"),
        )
        self.assertEqual(
            christmas_page.full_url, "http://events.example.com/christmas/"
        )
        self.assertEqual(christmas_page.url, "http://events.example.com/christmas/")
        self.assertEqual(
            christmas_page.relative_url(default_site),
            "http://events.example.com/christmas/",
        )
        self.assertEqual(christmas_page.relative_url(events_site), "/christmas/")
        self.assertEqual(christmas_page.get_site(), events_site)

        request = get_dummy_request(site=events_site)

        self.assertEqual(
            christmas_page.get_url_parts(request=request),
            (events_site.id, "http://events.example.com", "/christmas/"),
        )

        request2 = get_dummy_request(site=second_events_site)
        self.assertEqual(
            christmas_page.get_url_parts(request=request2),
            (second_events_site.id, "http://second-events.example.com", "/christmas/"),
        )

    @override_settings(ROOT_URLCONF="wagtail.test.non_root_urls")
    def test_urls_with_non_root_urlconf(self):
        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        # Basic installation only has one site configured, so page.url will return local URLs
        self.assertEqual(
            homepage.get_url_parts(), (default_site.id, "http://localhost", "/site/")
        )
        self.assertEqual(homepage.full_url, "http://localhost/site/")
        self.assertEqual(homepage.url, "/site/")
        self.assertEqual(homepage.relative_url(default_site), "/site/")
        self.assertEqual(homepage.get_site(), default_site)

        self.assertEqual(
            christmas_page.get_url_parts(),
            (default_site.id, "http://localhost", "/site/events/christmas/"),
        )
        self.assertEqual(
            christmas_page.full_url, "http://localhost/site/events/christmas/"
        )
        self.assertEqual(christmas_page.url, "/site/events/christmas/")
        self.assertEqual(
            christmas_page.relative_url(default_site), "/site/events/christmas/"
        )
        self.assertEqual(christmas_page.get_site(), default_site)

    @override_settings(ROOT_URLCONF="wagtail.test.headless_urls")
    def test_urls_headless(self):
        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")

        # The page should not be routable because wagtail_serve is not registered
        # However it is still associated with a site
        self.assertEqual(homepage.get_url_parts(), (default_site.id, None, None))
        self.assertIsNone(homepage.full_url)
        self.assertIsNone(homepage.url)

    def test_request_routing(self):
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        request = get_dummy_request(path="/events/christmas/")
        (found_page, args, kwargs) = homepage.route(request, ["events", "christmas"])
        self.assertEqual(found_page, christmas_page)

    def test_request_serving(self):
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        request = get_dummy_request(site=Site.objects.first())
        request.user = AnonymousUser()

        response = christmas_page.serve(request)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context_data["self"], christmas_page)
        # confirm that the event_page.html template was used
        self.assertContains(response, "<h2>Event</h2>")

    def test_route_to_unknown_page_returns_404(self):
        homepage = Page.objects.get(url_path="/home/")

        request = get_dummy_request(path="/events/quinquagesima/")
        with self.assertRaises(Http404):
            homepage.route(request, ["events", "quinquagesima"])

    def test_route_to_unpublished_page_returns_404(self):
        homepage = Page.objects.get(url_path="/home/")

        request = get_dummy_request(path="/events/tentative-unpublished-event/")
        with self.assertRaises(Http404):
            homepage.route(request, ["events", "tentative-unpublished-event"])

    # Override CACHES so we don't generate any cache-related SQL queries (tests use DatabaseCache
    # otherwise) and so cache.get will always return None.
    @override_settings(
        CACHES={"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
    )
    @override_settings(ALLOWED_HOSTS=["dummy"])
    def test_request_scope_site_root_paths_cache(self):
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        # without a request, get_url should only issue 1 SQL query
        with self.assertNumQueries(1):
            self.assertEqual(homepage.get_url(), "/")
        # subsequent calls with the same page should generate no SQL queries
        with self.assertNumQueries(0):
            self.assertEqual(homepage.get_url(), "/")
        # subsequent calls with a different page will still generate 1 SQL query
        with self.assertNumQueries(1):
            self.assertEqual(christmas_page.get_url(), "/events/christmas/")

        # with a request, the first call to get_url should issue 1 SQL query
        request = get_dummy_request()
        # first call with "balnk" request issues a extra query for the Site.find_for_request() call
        with self.assertNumQueries(2):
            self.assertEqual(homepage.get_url(request=request), "/")
        # subsequent calls should issue no SQL queries
        with self.assertNumQueries(0):
            self.assertEqual(homepage.get_url(request=request), "/")
        # even if called on a different page
        with self.assertNumQueries(0):
            self.assertEqual(
                christmas_page.get_url(request=request), "/events/christmas/"
            )

    def test_cached_parent_obj_set(self):
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        request = get_dummy_request(path="/events/christmas/")
        (found_page, args, kwargs) = homepage.route(request, ["events", "christmas"])
        self.assertEqual(found_page, christmas_page)

        # parent cache should be set
        events_page = Page.objects.get(url_path="/home/events/").specific
        with self.assertNumQueries(0):
            parent = found_page.get_parent(update=False)
            self.assertEqual(parent, events_page)


@override_settings(
    ROOT_URLCONF="wagtail.test.urls_multilang",
    LANGUAGE_CODE="en",
    WAGTAIL_I18N_ENABLED=True,
    LANGUAGES=[
        ("en", "English"),
        ("en-us", "English (United States)"),
        ("fr", "French"),
    ],
    WAGTAIL_CONTENT_LANGUAGES=[("en", "English"), ("fr", "French")],
)
class TestRoutingWithI18N(TestRouting):
    # This inherits from TestRouting so contains all the same test cases
    # Only the test cases that behave differently under internationalisation are overridden here

    def test_urls(self, expected_language_code="en"):
        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        # Basic installation only has one site configured, so page.url will return local URLs
        # self.assertEqual(
        #     homepage.get_url_parts(),
        #     (default_site.id, 'http://localhost', f'/{expected_language_code}/')
        # )
        self.assertEqual(
            homepage.full_url, f"http://localhost/{expected_language_code}/"
        )
        self.assertEqual(homepage.url, f"/{expected_language_code}/")
        self.assertEqual(
            homepage.relative_url(default_site), f"/{expected_language_code}/"
        )
        self.assertEqual(homepage.get_site(), default_site)

        self.assertEqual(
            christmas_page.get_url_parts(),
            (
                default_site.id,
                "http://localhost",
                f"/{expected_language_code}/events/christmas/",
            ),
        )
        self.assertEqual(
            christmas_page.full_url,
            f"http://localhost/{expected_language_code}/events/christmas/",
        )
        self.assertEqual(
            christmas_page.url, f"/{expected_language_code}/events/christmas/"
        )
        self.assertEqual(
            christmas_page.relative_url(default_site),
            f"/{expected_language_code}/events/christmas/",
        )
        self.assertEqual(christmas_page.get_site(), default_site)

    def test_urls_with_translation_activated(self):
        # This should have no effect as the URL is determined from the page's locale
        # and not the active locale
        with translation.override("fr"):
            self.test_urls()

    def test_urls_with_region_specific_translation_activated(self):
        # One exception to the above rule is when the active locale
        # is a more specific one to what the page was authored in
        # and the active locale is not in WAGTAIL_CONTENT_LANGUAGES

        # This is because, in this situation, the same page will be
        # served under both /en/ and /en-us/ prefixes
        with translation.override("en-us"):
            self.test_urls(expected_language_code="en-us")

    @override_settings(
        WAGTAIL_CONTENT_LANGUAGES=[
            ("en", "English"),
            ("en-us", "English (United States)"),
            ("fr", "French"),
        ]
    )
    def test_urls_with_region_specific_translation_activated_thats_in_wagtail_content_languages(
        self,
    ):
        # But, if en-us is also a content language, then this rule doesn't apply
        # because that page won't be served under /en-us/.
        with translation.override("en-us"):
            self.test_urls()

    def test_urls_with_language_not_in_wagtail_content_languages(self):
        # If the active locale doesn't map to anything in WAGTAIL_CONTENT_LANGUAGES,
        # URL prefixes should remain the same as the page's reported locale
        with translation.override("se"):
            self.test_urls()

    def test_urls_with_different_language_tree(self):
        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        fr_locale = Locale.objects.create(language_code="fr")
        fr_homepage = homepage.copy_for_translation(fr_locale)
        fr_christmas_page = christmas_page.copy_for_translation(
            fr_locale, copy_parents=True
        )
        fr_christmas_page.slug = "noel"
        fr_christmas_page.save(update_fields=["slug"])

        # Basic installation only has one site configured, so page.url will return local URLs
        self.assertEqual(
            fr_homepage.get_url_parts(), (default_site.id, "http://localhost", "/fr/")
        )
        self.assertEqual(fr_homepage.full_url, "http://localhost/fr/")
        self.assertEqual(fr_homepage.url, "/fr/")
        self.assertEqual(fr_homepage.relative_url(default_site), "/fr/")
        self.assertEqual(fr_homepage.get_site(), default_site)

        self.assertEqual(
            fr_christmas_page.get_url_parts(),
            (default_site.id, "http://localhost", "/fr/events/noel/"),
        )
        self.assertEqual(fr_christmas_page.full_url, "http://localhost/fr/events/noel/")
        self.assertEqual(fr_christmas_page.url, "/fr/events/noel/")
        self.assertEqual(
            fr_christmas_page.relative_url(default_site), "/fr/events/noel/"
        )
        self.assertEqual(fr_christmas_page.get_site(), default_site)

    @override_settings(
        ALLOWED_HOSTS=[
            "localhost",
            "testserver",
            "events.example.com",
            "second-events.example.com",
        ]
    )
    def test_urls_with_multiple_sites(self):
        events_page = Page.objects.get(url_path="/home/events/")
        events_site = Site.objects.create(
            hostname="events.example.com", root_page=events_page
        )

        # An underscore is not valid according to RFC 1034/1035
        # and will raise a DisallowedHost Exception
        second_events_site = Site.objects.create(
            hostname="second-events.example.com", root_page=events_page
        )

        default_site = Site.objects.get(is_default_site=True)
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = Page.objects.get(url_path="/home/events/christmas/")

        # with multiple sites, page.url will return full URLs to ensure that
        # they work across sites
        self.assertEqual(
            homepage.get_url_parts(), (default_site.id, "http://localhost", "/en/")
        )
        self.assertEqual(homepage.full_url, "http://localhost/en/")
        self.assertEqual(homepage.url, "http://localhost/en/")
        self.assertEqual(homepage.relative_url(default_site), "/en/")
        self.assertEqual(homepage.relative_url(events_site), "http://localhost/en/")
        self.assertEqual(homepage.get_site(), default_site)

        self.assertEqual(
            christmas_page.get_url_parts(),
            (events_site.id, "http://events.example.com", "/en/christmas/"),
        )
        self.assertEqual(
            christmas_page.full_url, "http://events.example.com/en/christmas/"
        )
        self.assertEqual(christmas_page.url, "http://events.example.com/en/christmas/")
        self.assertEqual(
            christmas_page.relative_url(default_site),
            "http://events.example.com/en/christmas/",
        )
        self.assertEqual(christmas_page.relative_url(events_site), "/en/christmas/")
        self.assertEqual(christmas_page.get_site(), events_site)

        request = get_dummy_request(site=events_site)

        self.assertEqual(
            christmas_page.get_url_parts(request=request),
            (events_site.id, "http://events.example.com", "/en/christmas/"),
        )

        request2 = get_dummy_request(site=second_events_site)

        self.assertEqual(
            christmas_page.get_url_parts(request=request2),
            (
                second_events_site.id,
                "http://second-events.example.com",
                "/en/christmas/",
            ),
        )

    # Override CACHES so we don't generate any cache-related SQL queries (tests use DatabaseCache
    # otherwise) and so cache.get will always return None.
    @override_settings(
        CACHES={"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
    )
    @override_settings(ALLOWED_HOSTS=["dummy"])
    def test_request_scope_site_root_paths_cache(self):
        homepage = Page.objects.get(url_path="/home/")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        # without a request, get_url should only issue 2 SQL queries
        with self.assertNumQueries(2):
            self.assertEqual(homepage.get_url(), "/en/")
        # subsequent calls with the same page should generate no SQL queries
        with self.assertNumQueries(0):
            self.assertEqual(homepage.get_url(), "/en/")
        # subsequent calls with a different page will still generate 2 SQL queries
        with self.assertNumQueries(2):
            self.assertEqual(christmas_page.get_url(), "/en/events/christmas/")

        # with a request, the first call to get_url should issue 1 SQL query
        request = get_dummy_request()
        # first call with "balnk" request issues a extra query for the Site.find_for_request() call
        with self.assertNumQueries(3):
            self.assertEqual(homepage.get_url(request=request), "/en/")
        # subsequent calls should issue no SQL queries
        with self.assertNumQueries(0):
            self.assertEqual(homepage.get_url(request=request), "/en/")
        # even if called on a different page
        with self.assertNumQueries(0):
            self.assertEqual(
                christmas_page.get_url(request=request), "/en/events/christmas/"
            )


class TestServeView(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        # Explicitly clear the cache of site root paths. Normally this would be kept
        # in sync by the Site.save logic, but this is bypassed when the database is
        # rolled back between tests using transactions.
        Site.clear_site_root_paths_cache()

        # also need to clear urlresolver caches before/after tests, because we override
        # ROOT_URLCONF in some tests here
        from django.urls import clear_url_caches

        clear_url_caches()

    def tearDown(self):
        from django.urls import clear_url_caches

        clear_url_caches()

    def test_serve(self):
        response = self.client.get("/events/christmas/")

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.templates[0].name, "tests/event_page.html")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")
        self.assertEqual(response.context["self"], christmas_page)

        self.assertContains(response, "<h1>Christmas</h1>")
        self.assertContains(response, "<h2>Event</h2>")

    @override_settings(ROOT_URLCONF="wagtail.test.non_root_urls")
    def test_serve_with_non_root_urls(self):
        response = self.client.get("/site/events/christmas/")

        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.templates[0].name, "tests/event_page.html")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")
        self.assertEqual(response.context["self"], christmas_page)

        self.assertContains(response, "<h1>Christmas</h1>")
        self.assertContains(response, "<h2>Event</h2>")

    def test_serve_unknown_page_returns_404(self):
        response = self.client.get("/events/quinquagesima/")
        self.assertEqual(response.status_code, 404)

    def test_serve_unpublished_page_returns_404(self):
        response = self.client.get("/events/tentative-unpublished-event/")
        self.assertEqual(response.status_code, 404)

    @override_settings(ALLOWED_HOSTS=["localhost", "events.example.com"])
    def test_serve_with_multiple_sites(self):
        events_page = Page.objects.get(url_path="/home/events/")
        Site.objects.create(hostname="events.example.com", root_page=events_page)

        response = self.client.get("/christmas/", HTTP_HOST="events.example.com")
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.templates[0].name, "tests/event_page.html")
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")
        self.assertEqual(response.context["self"], christmas_page)

        self.assertContains(response, "<h1>Christmas</h1>")
        self.assertContains(response, "<h2>Event</h2>")

        # same request to the default host should return a 404
        c = Client()
        response = c.get("/christmas/", HTTP_HOST="localhost")
        self.assertEqual(response.status_code, 404)

    def test_serve_with_custom_context_name(self):
        EventPage.context_object_name = "event_page"
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        response = self.client.get("/events/christmas/")

        # Context should contain context_object_name key along with standard page keys
        self.assertEqual(response.context["event_page"], christmas_page)
        self.assertEqual(response.context["page"], christmas_page)
        self.assertEqual(response.context["self"], christmas_page)

    def test_serve_with_custom_context(self):
        response = self.client.get("/events/")
        self.assertEqual(response.status_code, 200)

        # should render the whole page
        self.assertContains(response, "<h1>Events</h1>")

        # response should contain data from the custom 'events' context variable
        self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')

    def test_ajax_response(self):
        response = self.client.get("/events/", HTTP_X_REQUESTED_WITH="XMLHttpRequest")
        self.assertEqual(response.status_code, 200)

        # should only render the content of includes/event_listing.html, not the whole page
        self.assertNotContains(response, "<h1>Events</h1>")
        self.assertContains(response, '<a href="/events/christmas/">Christmas</a>')

    def test_before_serve_hook(self):
        response = self.client.get("/events/", HTTP_USER_AGENT="GoogleBot")
        self.assertContains(response, "bad googlebot no cookie")


class TestMovePage(TestCase):
    fixtures = ["test.json"]

    def test_move_page(self):
        about_us_page = SimplePage.objects.get(url_path="/home/about-us/")
        events_index = EventIndex.objects.get(url_path="/home/events/")

        events_index.move(about_us_page, pos="last-child")

        # re-fetch events index to confirm that db fields have been updated
        events_index = EventIndex.objects.get(id=events_index.id)
        self.assertEqual(events_index.url_path, "/home/about-us/events/")
        self.assertEqual(events_index.depth, 4)
        self.assertEqual(events_index.get_parent().id, about_us_page.id)

        # children of events_index should also have been updated
        christmas = events_index.get_children().get(slug="christmas")
        self.assertEqual(christmas.depth, 5)
        self.assertEqual(christmas.url_path, "/home/about-us/events/christmas/")


class TestPrevNextSiblings(TestCase):
    fixtures = ["test.json"]

    def test_get_next_siblings(self):
        christmas_event = Page.objects.get(url_path="/home/events/christmas/")
        self.assertTrue(
            christmas_event.get_next_siblings()
            .filter(url_path="/home/events/final-event/")
            .exists()
        )

    def test_get_next_siblings_inclusive(self):
        christmas_event = Page.objects.get(url_path="/home/events/christmas/")

        # First element must always be the current page
        self.assertEqual(
            christmas_event.get_next_siblings(inclusive=True).first(), christmas_event
        )

    def test_get_prev_siblings(self):
        final_event = Page.objects.get(url_path="/home/events/final-event/")
        self.assertTrue(
            final_event.get_prev_siblings()
            .filter(url_path="/home/events/christmas/")
            .exists()
        )

        # First element must always be the current page
        self.assertEqual(
            final_event.get_prev_siblings(inclusive=True).first(), final_event
        )


class TestSaveRevision(TestCase):
    fixtures = ["test.json"]

    def test_raises_error_if_non_specific_page_used(self):
        christmas_event = Page.objects.get(url_path="/home/events/christmas/")

        with self.assertRaises(RuntimeError) as e:
            christmas_event.save_revision()

        self.assertEqual(
            e.exception.args[0],
            "page.save_revision() must be called on the specific version of the page. Call page.specific.save_revision() instead.",
        )


class TestLiveRevision(TestCase):
    fixtures = ["test.json"]

    @freeze_time("2017-01-01 12:00:00")
    def test_publish_method_will_set_live_revision(self):
        page = Page.objects.get(id=2)

        revision = page.save_revision()
        revision.publish()

        page.refresh_from_db()
        self.assertEqual(page.live_revision, revision)
        if settings.USE_TZ:
            self.assertEqual(
                page.last_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
            # first_published_at should not change
            self.assertEqual(
                page.first_published_at,
                datetime.datetime(2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
        else:
            self.assertEqual(
                # interpret the "2017-01-01 12:00:00" in freeze_time above as a naive local date
                page.last_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0),
            )
            # first_published_at should not change
            self.assertEqual(
                # convert the "2014-01-01T12:00:00.000Z" in the test fixture to a naive local time
                page.first_published_at,
                timezone.make_naive(
                    datetime.datetime(
                        2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
                    )
                ),
            )

    @freeze_time("2017-01-01 12:00:00")
    def test_unpublish_method_will_clean_live_revision(self):
        page = Page.objects.get(id=2)

        revision = page.save_revision()
        revision.publish()

        page.refresh_from_db()

        page.unpublish()

        page.refresh_from_db()
        self.assertIsNone(page.live_revision)
        # first_published_at / last_published_at should remain unchanged on unpublish
        if settings.USE_TZ:
            self.assertEqual(
                page.first_published_at,
                datetime.datetime(2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
            self.assertEqual(
                page.last_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
        else:
            self.assertEqual(
                # convert the "2014-01-01T12:00:00.000Z" in the test fixture to a naive local time
                page.first_published_at,
                timezone.make_naive(
                    datetime.datetime(
                        2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
                    )
                ),
            )
            self.assertEqual(
                # interpret the "2017-01-01 12:00:00" in freeze_time above as a naive local date
                page.last_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0),
            )

    @freeze_time("2017-01-01 12:00:00")
    def test_copy_method_with_keep_live_will_update_live_revision(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")
        revision = about_us.save_revision()
        revision.publish()

        new_about_us = about_us.copy(
            keep_live=True,
            update_attrs={"title": "New about us", "slug": "new-about-us"},
        )
        self.assertIsNotNone(new_about_us.live_revision)
        self.assertNotEqual(about_us.live_revision, new_about_us.live_revision)

        # first_published_at / last_published_at should reflect the current time,
        # not the source page's publish dates, since the copied page is being published
        # for the first time
        if settings.USE_TZ:
            self.assertEqual(
                new_about_us.first_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
            self.assertEqual(
                new_about_us.last_published_at,
                datetime.datetime(2017, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
        else:
            self.assertEqual(
                new_about_us.first_published_at, datetime.datetime(2017, 1, 1, 12, 0, 0)
            )
            self.assertEqual(
                new_about_us.last_published_at, datetime.datetime(2017, 1, 1, 12, 0, 0)
            )

        new_about_us.refresh_from_db()
        self.assertEqual(new_about_us.title, "New about us")
        self.assertEqual(new_about_us.draft_title, "New about us")

    def test_copy_method_without_keep_live_will_not_update_live_revision(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")
        revision = about_us.save_revision()
        revision.publish()
        about_us.refresh_from_db()
        self.assertIsNotNone(about_us.live_revision)

        new_about_us = about_us.copy(
            keep_live=False,
            update_attrs={"title": "New about us", "slug": "new-about-us"},
        )
        self.assertIsNone(new_about_us.live_revision)
        # first_published_at / last_published_at should be blank, because the copied article
        # has not been published
        self.assertIsNone(new_about_us.first_published_at)
        self.assertIsNone(new_about_us.last_published_at)
        new_about_us.refresh_from_db()
        self.assertEqual(new_about_us.title, "New about us")
        self.assertEqual(new_about_us.draft_title, "New about us")

    def test_copy_method_copies_latest_revision(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")

        # make another revision
        about_us.content = "We are even better than before"
        about_us.save_revision()
        about_us.refresh_from_db()

        self.assertEqual(
            about_us.get_latest_revision_as_object().content,
            "We are even better than before",
        )

        new_about_us = about_us.copy(
            keep_live=False,
            update_attrs={"title": "New about us", "slug": "new-about-us"},
        )
        new_about_us_draft = new_about_us.get_latest_revision_as_object()
        self.assertEqual(new_about_us_draft.content, "We are even better than before")
        new_about_us.refresh_from_db()
        self.assertEqual(new_about_us.title, "New about us")
        self.assertEqual(new_about_us.draft_title, "New about us")

    @freeze_time("2017-01-01 12:00:00")
    def test_publish_with_future_go_live_does_not_set_live_revision(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")
        if settings.USE_TZ:
            about_us.go_live_at = datetime.datetime(
                2018, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
            )
        else:
            about_us.go_live_at = datetime.datetime(2018, 1, 1, 12, 0, 0)
        revision = about_us.save_revision()
        revision.publish()
        about_us.refresh_from_db()

        self.assertFalse(about_us.live)
        self.assertIsNone(about_us.live_revision)

        # first_published_at / last_published_at should remain unchanged
        if settings.USE_TZ:
            self.assertEqual(
                about_us.first_published_at,
                datetime.datetime(2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
            self.assertEqual(
                about_us.last_published_at,
                datetime.datetime(2014, 2, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
            )
        else:
            self.assertEqual(
                about_us.first_published_at,
                timezone.make_naive(
                    datetime.datetime(
                        2014, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
                    )
                ),
            )
            self.assertEqual(
                about_us.last_published_at,
                timezone.make_naive(
                    datetime.datetime(
                        2014, 2, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
                    )
                ),
            )


class TestPageGetSpecific(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        super().setUp()
        self.page = Page.objects.get(url_path="/home/about-us/")
        self.page.foo = "ABC"
        self.page.bar = {"key": "value"}
        self.page.baz = 999

    def test_default(self):
        # Field values are fetched from the database, hence the query
        with self.assertNumQueries(1):
            result = self.page.get_specific()

        # The returned instance is the correct type
        self.assertIsInstance(result, SimplePage)

        # Generic page field values can be accessed for free
        with self.assertNumQueries(0):
            self.assertEqual(result.id, self.page.id)
            self.assertEqual(result.title, self.page.title)

        # Specific model fields values are available without additional queries
        with self.assertNumQueries(0):
            self.assertTrue(result.content)

        # All non-field attributes should have been copied over...
        for attr in ("foo", "bar", "baz"):
            with self.subTest(attribute=attr):
                self.assertIs(getattr(result, attr), getattr(self.page, attr))

    def test_deferred(self):
        # Field values are NOT fetched from the database, hence no query
        with self.assertNumQueries(0):
            result = self.page.get_specific(deferred=True)

        # The returned instance is the correct type
        self.assertIsInstance(result, SimplePage)

        # Generic page field values can be accessed for free
        with self.assertNumQueries(0):
            self.assertEqual(result.id, self.page.id)
            self.assertEqual(result.title, self.page.title)

        # But, specific model fields values are NOT available without additional queries
        with self.assertNumQueries(1):
            self.assertTrue(result.content)

        # All non-field attributes should have been copied over...
        for attr in ("foo", "bar", "baz"):
            with self.subTest(attribute=attr):
                self.assertIs(getattr(result, attr), getattr(self.page, attr))

    def test_copy_attrs(self):
        result = self.page.get_specific(copy_attrs=["foo", "bar"])

        # foo and bar should have been copied over
        self.assertIs(result.foo, self.page.foo)
        self.assertIs(result.bar, self.page.bar)

        # but baz should not have been
        self.assertFalse(hasattr(result, "baz"))

    def test_copy_attrs_with_empty_list(self):
        result = self.page.get_specific(copy_attrs=())

        # No non-field attributes should have been copied over...
        for attr in ("foo", "bar", "baz"):
            with self.subTest(attribute=attr):
                self.assertFalse(hasattr(result, attr))

    def test_copy_attrs_exclude(self):
        result = self.page.get_specific(copy_attrs_exclude=["baz"])

        # foo and bar should have been copied over
        self.assertIs(result.foo, self.page.foo)
        self.assertIs(result.bar, self.page.bar)

        # but baz should not have been
        self.assertFalse(hasattr(result, "baz"))

    def test_copy_attrs_exclude_with_empty_list(self):
        result = self.page.get_specific(copy_attrs_exclude=())

        # All non-field attributes should have been copied over...
        for attr in ("foo", "bar", "baz"):
            with self.subTest(attribute=attr):
                self.assertIs(getattr(result, attr), getattr(self.page, attr))

    def test_specific_cached_property(self):
        # invoking several times to demonstrate that field values
        # are fetched only once from the database, and each time the
        # same object is returned
        with self.assertNumQueries(1):
            result = self.page.specific
            result_2 = self.page.specific
            result_3 = self.page.specific
            self.assertIs(result, result_2)
            self.assertIs(result, result_3)

        self.assertIsInstance(result, SimplePage)
        # Specific model fields values are available without additional queries
        with self.assertNumQueries(0):
            self.assertTrue(result.content)

    def test_specific_deferred_cached_property(self):
        # invoking several times to demonstrate that the property
        # returns the same object (without any queries)
        with self.assertNumQueries(0):
            result = self.page.specific_deferred
            result_2 = self.page.specific_deferred
            result_3 = self.page.specific_deferred
            self.assertIs(result, result_2)
            self.assertIs(result, result_3)

        self.assertIsInstance(result, SimplePage)
        # Specific model fields values are not available without additional queries
        with self.assertNumQueries(1):
            self.assertTrue(result.content)


class TestCopyPage(TestCase):
    fixtures = ["test.json"]

    def test_copy_page_copies(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")

        # Copy it
        new_about_us = about_us.copy(
            update_attrs={"title": "New about us", "slug": "new-about-us"}
        )

        # Check that new_about_us is correct
        self.assertIsInstance(new_about_us, SimplePage)
        self.assertEqual(new_about_us.title, "New about us")
        self.assertEqual(new_about_us.slug, "new-about-us")

        # Check that new_about_us is a different page
        self.assertNotEqual(about_us.id, new_about_us.id)

        # Check that the url path was updated
        self.assertEqual(new_about_us.url_path, "/home/new-about-us/")

    def test_copy_page_copies_child_objects(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.speakers.count(), 1, "Child objects weren't copied"
        )

        # Check that the speakers weren't removed from old page
        self.assertEqual(
            christmas_event.speakers.count(),
            1,
            "Child objects were removed from the original page",
        )

        # Check that advert placements were also copied (there's a gotcha here, since the advert_placements
        # relation is defined on Page, not EventPage)
        self.assertEqual(
            new_christmas_event.advert_placements.count(),
            1,
            "Child objects defined on the superclass weren't copied",
        )
        self.assertEqual(
            christmas_event.advert_placements.count(),
            1,
            "Child objects defined on the superclass were removed from the original page",
        )

    def test_copy_page_copies_parental_relations(self):
        """Test that a page will be copied with parental many to many relations intact."""
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        summer_category = EventCategory.objects.create(name="Summer")
        holiday_category = EventCategory.objects.create(name="Holidays")

        # add parental many to many relations
        christmas_event.categories = (summer_category, holiday_category)
        christmas_event.save()

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # check that original eventt is untouched
        self.assertEqual(
            christmas_event.categories.count(),
            2,
            "Child objects (parental many to many) defined on the superclass were removed from the original page",
        )

        # check that parental many to many are copied
        self.assertEqual(
            new_christmas_event.categories.count(),
            2,
            "Child objects (parental many to many) weren't copied",
        )

        # check that the original and copy are related to the same categories
        self.assertEqual(
            new_christmas_event.categories.all().in_bulk(),
            christmas_event.categories.all().in_bulk(),
        )

    def test_copy_page_does_not_copy_comments(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        christmas_event.wagtail_admin_comments = [
            Comment(text="test", user=christmas_event.owner)
        ]
        christmas_event.save()

        # Copy the page as in `test_copy_page_copies_child_objects()``, but using exclude_fields
        # to prevent 'advert_placements' from being copied to the new version
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the comments weren't removed from old page
        self.assertEqual(
            christmas_event.wagtail_admin_comments.count(),
            1,
            "Comments were removed from the original page",
        )

        # Check that comments were NOT copied over
        self.assertFalse(
            new_christmas_event.wagtail_admin_comments.exists(),
            msg="Comments were copied",
        )

    def test_copy_page_does_not_copy_child_objects_if_accessor_name_in_exclude_fields(
        self,
    ):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        # Copy the page as in `test_copy_page_copies_child_objects()``, but using exclude_fields
        # to prevent 'advert_placements' from being copied to the new version
        new_christmas_event = christmas_event.copy(
            update_attrs={
                "title": "New christmas event",
                "slug": "new-christmas-event",
            },
            exclude_fields=["advert_placements"],
        )

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.speakers.count(), 1, "Child objects weren't copied"
        )

        # Check that the speakers weren't removed from old page
        self.assertEqual(
            christmas_event.speakers.count(),
            1,
            "Child objects were removed from the original page",
        )

        # Check that advert placements were NOT copied over, but were not removed from the old page
        self.assertFalse(
            new_christmas_event.advert_placements.exists(),
            msg="Child objects were copied despite accessor_name being specified in `exclude_fields`",
        )
        self.assertEqual(
            christmas_event.advert_placements.count(),
            1,
            "Child objects defined on the superclass were removed from the original page",
        )

    def test_copy_page_with_process_child_object_supplied(self):
        # We'll provide this when copying and test that it gets called twice:
        # Once for the single speaker, and another for the single advert_placement
        modify_child = Mock()

        old_event = EventPage.objects.get(url_path="/home/events/christmas/")

        # Create a child event
        child_event = old_event.copy(
            update_attrs={
                "title": "Child christmas event",
                "slug": "child-christmas-event",
            }
        )
        child_event.move(old_event, pos="last-child")

        new_event = old_event.copy(
            update_attrs={
                "title": "New christmas event",
                "slug": "new-christmas-event",
            },
            process_child_object=modify_child,
            recursive=True,
        )

        # The method should have been called with these arguments when copying
        # the advert placement
        relationship = EventPage._meta.get_field("advert_placements")
        child_object = new_event.advert_placements.get()
        modify_child.assert_any_call(old_event, new_event, relationship, child_object)

        # And again with these arguments when copying the speaker
        relationship = EventPage._meta.get_field("speaker")
        child_object = new_event.speakers.get()
        modify_child.assert_any_call(old_event, new_event, relationship, child_object)

        # Check that process_child_object was run on the child event page as well
        new_child_event = new_event.get_children().get().specific
        child_object = new_child_event.speakers.get()
        modify_child.assert_any_call(
            child_event, new_child_event, relationship, child_object
        )

    def test_copy_page_copies_revisions(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the revisions were copied
        # Copying creates a new revision so we're expecting the new page to have two revisions
        self.assertEqual(new_christmas_event.revisions.count(), 2)

        # Check that the revisions weren't removed from old page
        self.assertEqual(
            christmas_event.revisions.count(),
            1,
            "Revisions were removed from the original page",
        )

        # Check that the attributes were updated in the latest revision
        latest_revision = new_christmas_event.get_latest_revision_as_object()
        self.assertEqual(latest_revision.title, "New christmas event")
        self.assertEqual(latest_revision.slug, "new-christmas-event")

        # get_latest_revision_as_object might bypass the revisions table if it determines
        # that there are no draft edits since publish - so retrieve it explicitly from the
        # revision data, to ensure it's been updated there too
        latest_revision = new_christmas_event.get_latest_revision().as_object()
        self.assertEqual(latest_revision.title, "New christmas event")
        self.assertEqual(latest_revision.slug, "new-christmas-event")

        # Check that the ids within the revision were updated correctly
        new_revision = new_christmas_event.revisions.first()
        new_revision_content = new_revision.content
        self.assertEqual(new_revision_content["pk"], new_christmas_event.id)
        self.assertEqual(
            new_revision_content["speakers"][0]["page"], new_christmas_event.id
        )

        # Also, check that the child objects in the new revision are given new IDs
        old_speakers_ids = set(christmas_event.speakers.values_list("id", flat=True))
        new_speakers_ids = {
            speaker["pk"] for speaker in new_revision_content["speakers"]
        }
        self.assertFalse(
            old_speakers_ids.intersection(new_speakers_ids),
            msg="Child objects in revisions were not given a new primary key",
        )

    def test_copy_page_copies_revisions_and_doesnt_change_created_at(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Set the created_at of the revision to a time in the past
        revision = christmas_event.get_latest_revision()
        revision.created_at = datetime.datetime(
            2014, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
        )
        revision.save()

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the created_at time is the same
        christmas_event_created_at = (
            christmas_event.revisions.order_by("created_at").first().created_at
        )
        new_christmas_event_created_at = (
            new_christmas_event.revisions.order_by("created_at").first().created_at
        )
        self.assertEqual(christmas_event_created_at, new_christmas_event_created_at)

    def test_copy_page_copies_revisions_and_doesnt_schedule(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        if settings.USE_TZ:
            christmas_event.save_revision(
                approved_go_live_at=datetime.datetime(
                    2014, 9, 16, 9, 12, 00, tzinfo=datetime.timezone.utc
                )
            )
        else:
            christmas_event.save_revision(
                approved_go_live_at=datetime.datetime(2014, 9, 16, 9, 12, 00)
            )

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the old revision is still scheduled
        if settings.USE_TZ:
            self.assertEqual(
                christmas_event.revisions.order_by("created_at")
                .first()
                .approved_go_live_at,
                datetime.datetime(2014, 9, 16, 9, 12, 00, tzinfo=datetime.timezone.utc),
            )
        else:
            self.assertEqual(
                christmas_event.revisions.order_by("created_at")
                .first()
                .approved_go_live_at,
                datetime.datetime(2014, 9, 16, 9, 12, 00),
            )

        # Check that the new revision is not scheduled
        self.assertIsNone(
            new_christmas_event.revisions.order_by("created_at")
            .first()
            .approved_go_live_at
        )

    def test_copy_page_doesnt_copy_revisions_if_told_not_to_do_so(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={
                "title": "New christmas event",
                "slug": "new-christmas-event",
            },
            copy_revisions=False,
        )

        # Check that the revisions weren't copied
        # Copying creates a new revision so we're expecting the new page to have one revision
        self.assertEqual(new_christmas_event.revisions.count(), 1)

        # Check that the revisions weren't removed from old page
        self.assertEqual(
            christmas_event.revisions.count(),
            1,
            "Revisions were removed from the original page",
        )

    def test_copy_page_copies_child_objects_with_nonspecific_class(self):
        # Get chrismas page as Page instead of EventPage
        christmas_event = Page.objects.get(url_path="/home/events/christmas/")

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={"title": "New christmas event", "slug": "new-christmas-event"}
        )

        # Check that the type of the new page is correct
        self.assertIsInstance(new_christmas_event, EventPage)

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.speakers.count(), 1, "Child objects weren't copied"
        )

    def test_copy_page_copies_recursively(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")

        # Copy it
        new_events_index = events_index.copy(
            recursive=True,
            update_attrs={"title": "New events index", "slug": "new-events-index"},
        )

        # Get christmas event
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first()
        )
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the event exists in both places
        self.assertIsNotNone(new_christmas_event, "Child pages weren't copied")
        self.assertIsNotNone(
            old_christmas_event, "Child pages were removed from original page"
        )

        # Check that the url path was updated
        self.assertEqual(
            new_christmas_event.url_path, "/home/new-events-index/christmas/"
        )

    def test_copy_page_copies_recursively_with_child_objects(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")

        # Copy it
        new_events_index = events_index.copy(
            recursive=True,
            update_attrs={"title": "New events index", "slug": "new-events-index"},
        )

        # Get christmas event
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first()
        )
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.specific.speakers.count(),
            1,
            "Child objects weren't copied",
        )

        # Check that the speakers weren't removed from old page
        self.assertEqual(
            old_christmas_event.specific.speakers.count(),
            1,
            "Child objects were removed from the original page",
        )

    def test_copy_page_copies_recursively_with_revisions(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first().specific
        )
        old_christmas_event.save_revision()

        # Copy it
        new_events_index = events_index.copy(
            recursive=True,
            update_attrs={"title": "New events index", "slug": "new-events-index"},
        )

        # Get christmas event
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the revisions were copied
        # Copying creates a new revision so we're expecting the new page to have two revisions
        self.assertEqual(new_christmas_event.specific.revisions.count(), 2)

        # Check that the revisions weren't removed from old page
        self.assertEqual(
            old_christmas_event.specific.revisions.count(),
            1,
            "Revisions were removed from the original page",
        )

    def test_copy_page_copies_recursively_but_doesnt_copy_revisions_if_told_not_to_do_so(
        self,
    ):
        events_index = EventIndex.objects.get(url_path="/home/events/")
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first()
        )
        old_christmas_event.specific.save_revision()

        # Copy it
        new_events_index = events_index.copy(
            recursive=True,
            update_attrs={"title": "New events index", "slug": "new-events-index"},
            copy_revisions=False,
        )

        # Get christmas event
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the revisions weren't copied
        # Copying creates a new revision so we're expecting the new page to have one revision
        self.assertEqual(new_christmas_event.specific.revisions.count(), 1)

        # Check that the revisions weren't removed from old page
        self.assertEqual(
            old_christmas_event.specific.revisions.count(),
            1,
            "Revisions were removed from the original page",
        )

    def test_copy_page_copies_recursively_to_the_same_tree(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first().specific
        )
        old_christmas_event.save_revision()

        with self.assertRaises(Exception) as exception:
            events_index.copy(
                recursive=True,
                update_attrs={"title": "New events index", "slug": "new-events-index"},
                to=events_index,
            )
        self.assertEqual(
            str(exception.exception),
            "You cannot copy a tree branch recursively into itself",
        )

    def test_copy_page_updates_user(self):
        event_moderator = get_user_model().objects.get(
            email="eventmoderator@example.com"
        )
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Copy it
        new_christmas_event = christmas_event.copy(
            update_attrs={
                "title": "New christmas event",
                "slug": "new-christmas-event",
            },
            user=event_moderator,
        )

        # Check that the owner has been updated
        self.assertEqual(new_christmas_event.owner, event_moderator)

        # Check that the user on the last revision is correct
        self.assertEqual(
            new_christmas_event.get_latest_revision().user, event_moderator
        )

    def test_copy_multi_table_inheritance(self):
        saint_patrick_event = SingleEventPage.objects.get(
            url_path="/home/events/saint-patrick/"
        )

        # Copy it
        new_saint_patrick_event = saint_patrick_event.copy(
            update_attrs={"slug": "new-saint-patrick"}
        )

        # Check that new_saint_patrick_event is correct
        self.assertIsInstance(new_saint_patrick_event, SingleEventPage)
        self.assertEqual(new_saint_patrick_event.excerpt, saint_patrick_event.excerpt)

        # Check that new_saint_patrick_event is a different page, including parents from both EventPage and Page
        self.assertNotEqual(saint_patrick_event.id, new_saint_patrick_event.id)
        self.assertNotEqual(
            saint_patrick_event.eventpage_ptr.id,
            new_saint_patrick_event.eventpage_ptr.id,
        )
        self.assertNotEqual(
            saint_patrick_event.eventpage_ptr.page_ptr.id,
            new_saint_patrick_event.eventpage_ptr.page_ptr.id,
        )

        # Check that the url path was updated
        self.assertEqual(
            new_saint_patrick_event.url_path, "/home/events/new-saint-patrick/"
        )

        # Check that both parent instance exists
        self.assertIsInstance(
            EventPage.objects.get(id=new_saint_patrick_event.id), EventPage
        )
        self.assertIsInstance(Page.objects.get(id=new_saint_patrick_event.id), Page)

    def test_copy_page_copies_tags(self):
        # create and publish a TaggedPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        tagged_page = TaggedPage(title="My tagged page", slug="my-tagged-page")
        tagged_page.tags.add("wagtail", "bird")
        event_index.add_child(instance=tagged_page)
        tagged_page.save_revision().publish()

        old_tagged_item_ids = [item.id for item in tagged_page.tagged_items.all()]
        # there should be two items here, with defined (truthy) IDs
        self.assertEqual(len(old_tagged_item_ids), 2)
        self.assertTrue(all(old_tagged_item_ids))

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_tagged_page = tagged_page.copy(to=homepage)

        self.assertNotEqual(tagged_page.id, new_tagged_page.id)

        # new page should also have two tags
        new_tagged_item_ids = [item.id for item in new_tagged_page.tagged_items.all()]
        self.assertEqual(len(new_tagged_item_ids), 2)
        self.assertTrue(all(new_tagged_item_ids))

        # new tagged_item IDs should differ from old ones
        self.assertTrue(
            all(item_id not in old_tagged_item_ids for item_id in new_tagged_item_ids)
        )

    def test_copy_subclassed_page_copies_tags(self):
        # create and publish a TaggedGrandchildPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        sub_tagged_page = TaggedGrandchildPage(
            title="My very special tagged page", slug="my-special-tagged-page"
        )
        sub_tagged_page.tags.add("wagtail", "bird")
        event_index.add_child(instance=sub_tagged_page)
        sub_tagged_page.save_revision().publish()

        old_tagged_item_ids = [item.id for item in sub_tagged_page.tagged_items.all()]
        # there should be two items here, with defined (truthy) IDs
        self.assertEqual(len(old_tagged_item_ids), 2)
        self.assertTrue(all(old_tagged_item_ids))

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_sub_tagged_page = sub_tagged_page.copy(to=homepage)

        self.assertNotEqual(sub_tagged_page.id, new_sub_tagged_page.id)

        # new page should also have two tags
        new_tagged_item_ids = [
            item.id for item in new_sub_tagged_page.tagged_items.all()
        ]
        self.assertEqual(len(new_tagged_item_ids), 2)
        self.assertTrue(all(new_tagged_item_ids))

        # new tagged_item IDs should differ from old ones
        self.assertTrue(
            all(item_id not in old_tagged_item_ids for item_id in new_tagged_item_ids)
        )

    def test_copy_page_with_m2m_relations(self):
        # create and publish a ManyToManyBlogPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        category = BlogCategory.objects.create(name="Birds")
        advert = Advert.objects.create(
            url="http://www.heinz.com/", text="beanz meanz heinz"
        )

        blog_page = ManyToManyBlogPage(title="My blog page", slug="my-blog-page")
        event_index.add_child(instance=blog_page)

        blog_page.adverts.add(advert)
        BlogCategoryBlogPage.objects.create(category=category, page=blog_page)
        blog_page.save_revision().publish()

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_blog_page = blog_page.copy(to=homepage)

        # M2M relations are not formally supported, so for now we're only interested in
        # the copy operation as a whole succeeding, rather than the child objects being copied
        self.assertNotEqual(blog_page.id, new_blog_page.id)

    def test_copy_page_with_generic_foreign_key(self):
        # create and publish a GenericSnippetPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        advert = Advert.objects.create(
            url="http://www.heinz.com/", text="beanz meanz heinz"
        )

        page = GenericSnippetPage(title="My snippet page", slug="my-snippet-page")
        page.snippet_content_object = advert
        event_index.add_child(instance=page)

        page.save_revision().publish()

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_page = page.copy(to=homepage)

        self.assertNotEqual(page.id, new_page.id)
        self.assertEqual(new_page.snippet_content_object, advert)

    def test_copy_page_with_o2o_relation(self):
        event_index = Page.objects.get(url_path="/home/events/")

        page = OneToOnePage(title="My page", slug="my-page")

        event_index.add_child(instance=page)

        homepage = Page.objects.get(url_path="/home/")
        new_page = page.copy(to=homepage)

        self.assertNotEqual(page.id, new_page.id)

    def test_copy_page_with_additional_excluded_fields(self):
        homepage = Page.objects.get(url_path="/home/")
        page = homepage.add_child(
            instance=PageWithExcludedCopyField(
                title="Discovery",
                slug="disco",
                content="NCC-1031",
                special_field="Context is for Kings",
            )
        )
        page.save_revision()
        new_page = page.copy(to=homepage, update_attrs={"slug": "disco-2"})
        exclude_field = new_page.latest_revision.content["special_field"]

        self.assertEqual(page.title, new_page.title)
        self.assertNotEqual(page.id, new_page.id)
        self.assertNotEqual(page.path, new_page.path)
        # special_field is in the list to be excluded
        self.assertNotEqual(page.special_field, new_page.special_field)
        self.assertEqual(new_page.special_field, exclude_field)

    def test_page_with_generic_relation(self):
        """Test that a page with a GenericRelation will have that relation ignored when
        copying.
        """
        homepage = Page.objects.get(url_path="/home/")
        original_page = homepage.add_child(
            instance=PageWithGenericRelation(
                title="PageWithGenericRelation",
                slug="page-with-generic-relation",
                live=True,
                has_unpublished_changes=False,
            )
        )
        RelatedGenericRelation.objects.create(content_object=original_page)
        self.assertIsNotNone(original_page.generic_relation.first())
        page_copy = original_page.copy(
            to=homepage, update_attrs={"slug": f"{original_page.slug}-2"}
        )
        self.assertIsNone(page_copy.generic_relation.first())

    def test_copy_page_with_excluded_parental_and_child_relations(self):
        """Test that a page will be copied with parental and child relations removed if excluded."""

        try:
            # modify excluded fields for this test
            EventPage.exclude_fields_in_copy = [
                "advert_placements",
                "categories",
                "signup_link",
            ]

            # set up data
            christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
            summer_category = EventCategory.objects.create(name="Summer")
            holiday_category = EventCategory.objects.create(name="Holidays")

            # add URL (to test excluding a basic field)
            christmas_event.signup_link = "https://christmas-is-awesome.com/rsvp"

            # add parental many to many relations
            christmas_event.categories = (summer_category, holiday_category)
            christmas_event.save()

            # Copy it
            new_christmas_event = christmas_event.copy(
                update_attrs={
                    "title": "New christmas event",
                    "slug": "new-christmas-event",
                }
            )

            # check that the signup_link was NOT copied
            self.assertEqual(
                christmas_event.signup_link, "https://christmas-is-awesome.com/rsvp"
            )
            self.assertEqual(new_christmas_event.signup_link, "")

            # check that original event is untouched
            self.assertEqual(
                christmas_event.categories.count(),
                2,
                "Child objects (parental many to many) defined on the superclass were removed from the original page",
            )

            # check that parental many to many are NOT copied
            self.assertEqual(
                new_christmas_event.categories.count(),
                0,
                "Child objects (parental many to many) were copied but should be excluded",
            )

            # check that child objects on original event were left untouched
            self.assertEqual(
                christmas_event.advert_placements.count(),
                1,
                "Child objects defined on the original superclass were edited when copied",
            )

            # check that child objects were NOT copied
            self.assertEqual(
                new_christmas_event.advert_placements.count(),
                0,
                "Child objects defined on the superclass were copied and should not be",
            )

        finally:
            # reset excluded fields for future tests
            EventPage.exclude_fields_in_copy = []

    def test_copy_unsaved_page(self):
        """Test that unsaved page will not be copied."""
        new_page = SimplePage(slug="testpurp", title="testpurpose")
        with self.assertRaises(RuntimeError):
            new_page.copy()

    def test_copy_page_with_unique_uuids_in_orderables(self):
        """
        Test that a page with orderables can be copied and the translation
        keys are updated.
        """
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_page.speakers.add(
            EventPageSpeaker(
                first_name="Santa",
                last_name="Claus",
            )
        )
        christmas_page.save()
        # ensure there's a revision (which should capture the new speaker orderables)
        christmas_page.save_revision().publish()

        new_page = christmas_page.copy(
            update_attrs={
                "title": "Orderable Page",
                "slug": "translated-orderable-page",
            },
        )
        new_page.save_revision().publish()
        self.assertNotEqual(
            christmas_page.speakers.first().translation_key,
            new_page.speakers.first().translation_key,
        )

    def test_copy_published_emits_signal(self):
        """Test that copying of a published page emits a page_published signal."""
        christmas_page = EventPage.objects.get(url_path="/home/events/christmas/")

        signal_fired = False
        signal_page = None

        def page_published_handler(sender, instance, **kwargs):
            nonlocal signal_fired
            nonlocal signal_page
            signal_fired = True
            signal_page = instance

        page_published.connect(page_published_handler)
        try:
            copy_page = christmas_page.copy(
                update_attrs={"title": "New christmas", "slug": "new-christmas"},
            )

            self.assertTrue(signal_fired)
            self.assertEqual(signal_page, copy_page)
        finally:
            page_published.disconnect(page_published_handler)

    def test_copy_unpublished_not_emits_signal(self):
        """Test that copying of an unpublished page not emits a page_published signal."""
        homepage = Page.objects.get(url_path="/home/")
        homepage.live = False
        homepage.save()

        signal_fired = False

        def page_published_handler(sender, instance, **kwargs):
            nonlocal signal_fired
            signal_fired = True

        page_published.connect(page_published_handler)

        try:
            homepage.copy(update_attrs={"slug": "new_slug"})
            self.assertFalse(signal_fired)
        finally:
            page_published.disconnect(page_published_handler)

    def test_copy_keep_live_false_not_emits_signal(self):
        """Test that copying of a live page with keep_live=False not emits a page_published signal."""
        homepage = Page.objects.get(url_path="/home/")
        signal_fired = False

        def page_published_handler(sender, instance, **kwargs):
            nonlocal signal_fired
            signal_fired = True

        try:
            page_published.connect(page_published_handler)

            homepage.copy(keep_live=False, update_attrs={"slug": "new_slug"})
            self.assertFalse(signal_fired)
        finally:
            page_published.disconnect(page_published_handler)

    def test_copy_alias_page(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")
        about_us_alias = about_us.create_alias(update_slug="about-us-alias")

        about_us_alias_copy = about_us_alias.copy(
            update_attrs={"slug": "about-us-alias-copy"}
        )

        self.assertIsInstance(about_us_alias_copy, SimplePage)
        self.assertEqual(about_us_alias_copy.slug, "about-us-alias-copy")
        self.assertNotEqual(about_us_alias_copy.id, about_us.id)
        self.assertEqual(about_us_alias_copy.url_path, "/home/about-us-alias-copy/")

        # The copy should just be a copy of the original page, not an alias
        self.assertIsNone(about_us_alias_copy.alias_of)

    def test_copy_page_copies_restriction(self):
        """Test that view restrictions attached to a page are copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")
        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        homepage.add_child(instance=child_page_1)

        # Add PageViewRestriction to child_page_1
        PageViewRestriction.objects.create(page=child_page_1, password="hello")

        child_page_2 = child_page_1.copy(
            update_attrs={"title": "Child Page 2", "slug": "child-page-2"}
        )

        # check that the copied page child_page_2 has a view restriction
        self.assertTrue(PageViewRestriction.objects.filter(page=child_page_2).exists())

    def test_copy_page_does_not_copy_restrictions_from_parent(self):
        """Test that view restrictions on a page's ancestor are NOT copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")

        origin_parent = SimplePage(
            title="Parent 1", slug="parent-1", content="hello parent 1"
        )
        homepage.add_child(instance=origin_parent)
        PageViewRestriction.objects.create(page=origin_parent, password="hello")

        destination_parent = SimplePage(
            title="Parent 2", slug="parent-2", content="hello parent 2"
        )
        homepage.add_child(instance=destination_parent)

        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        origin_parent.add_child(instance=child_page_1)

        child_page_2 = child_page_1.copy(
            to=destination_parent,
            update_attrs={"title": "Child Page 2", "slug": "child-page-2"},
        )
        # check that the copied page child_page_2 does not have a view restriction
        self.assertFalse(PageViewRestriction.objects.filter(page=child_page_2).exists())

    def test_copy_page_does_not_copy_restrictions_when_new_parent_has_one_already(self):
        """Test that view restrictions on a page's ancestor are NOT copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")

        origin_parent = SimplePage(
            title="Parent 1", slug="parent-1", content="hello parent 1"
        )
        homepage.add_child(instance=origin_parent)

        destination_parent = SimplePage(
            title="Parent 2", slug="parent-2", content="hello parent 2"
        )
        homepage.add_child(instance=destination_parent)
        PageViewRestriction.objects.create(page=destination_parent, password="hello")

        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        origin_parent.add_child(instance=child_page_1)
        PageViewRestriction.objects.create(page=child_page_1, password="hello")

        child_page_2 = child_page_1.copy(
            to=destination_parent,
            update_attrs={"title": "Child Page 2", "slug": "child-page-2"},
        )
        # check that the copied page child_page_2 does not have a view restriction
        self.assertFalse(PageViewRestriction.objects.filter(page=child_page_2).exists())


class TestCreateAlias(TestCase):
    fixtures = ["test.json"]

    def test_create_alias(self):
        about_us = SimplePage.objects.get(url_path="/home/about-us/")

        # Set a different draft title, aliases are not supposed to
        # have a different draft_title because they don't have revisions.
        # This should be corrected when copying
        about_us.draft_title = "Draft title"
        about_us.save(update_fields=["draft_title"])

        # Copy it
        new_about_us = about_us.create_alias(update_slug="new-about-us")

        # Check that new_about_us is correct
        self.assertIsInstance(new_about_us, SimplePage)
        self.assertEqual(new_about_us.slug, "new-about-us")
        # Draft title should be changed to match the live title
        self.assertEqual(new_about_us.draft_title, "About us")

        # Check that new_about_us is a different page
        self.assertNotEqual(about_us.id, new_about_us.id)

        # Check that the url path was updated
        self.assertEqual(new_about_us.url_path, "/home/new-about-us/")

        # Check that the alias_of field was filled in
        self.assertEqual(new_about_us.alias_of, about_us)

    def test_create_alias_copies_child_objects(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        # Copy it
        new_christmas_event = christmas_event.create_alias(
            update_slug="new-christmas-event"
        )

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.speakers.count(), 1, "Child objects weren't copied"
        )

        # Check that the speakers weren't removed from old page
        self.assertEqual(
            christmas_event.speakers.count(),
            1,
            "Child objects were removed from the original page",
        )

        # Check that advert placements were also copied (there's a gotcha here, since the advert_placements
        # relation is defined on Page, not EventPage)
        self.assertEqual(
            new_christmas_event.advert_placements.count(),
            1,
            "Child objects defined on the superclass weren't copied",
        )
        self.assertEqual(
            christmas_event.advert_placements.count(),
            1,
            "Child objects defined on the superclass were removed from the original page",
        )

    def test_create_alias_copies_parental_relations(self):
        """Test that a page will be copied with parental many to many relations intact."""
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        summer_category = EventCategory.objects.create(name="Summer")
        holiday_category = EventCategory.objects.create(name="Holidays")

        # add parental many to many relations
        christmas_event.categories = (summer_category, holiday_category)
        christmas_event.save()

        # Copy it
        new_christmas_event = christmas_event.create_alias(
            update_slug="new-christmas-event"
        )

        # check that original eventt is untouched
        self.assertEqual(
            christmas_event.categories.count(),
            2,
            "Child objects (parental many to many) defined on the superclass were removed from the original page",
        )

        # check that parental many to many are copied
        self.assertEqual(
            new_christmas_event.categories.count(),
            2,
            "Child objects (parental many to many) weren't copied",
        )

        # check that the original and copy are related to the same categories
        self.assertEqual(
            new_christmas_event.categories.all().in_bulk(),
            christmas_event.categories.all().in_bulk(),
        )

    def test_create_alias_doesnt_copy_revisions(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Copy it
        new_christmas_event = christmas_event.create_alias(
            update_slug="new-christmas-event"
        )

        # Check that no revisions were created
        self.assertEqual(new_christmas_event.revisions.count(), 0)

    def test_create_alias_copies_child_objects_with_nonspecific_class(self):
        # Get chrismas page as Page instead of EventPage
        christmas_event = Page.objects.get(url_path="/home/events/christmas/")

        # Copy it
        new_christmas_event = christmas_event.create_alias(
            update_slug="new-christmas-event"
        )

        # Check that the type of the new page is correct
        self.assertIsInstance(new_christmas_event, EventPage)

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.speakers.count(), 1, "Child objects weren't copied"
        )

    def test_create_alias_copies_recursively(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")

        # Copy it
        new_events_index = events_index.create_alias(
            recursive=True, update_slug="new-events-index"
        )

        # Get christmas event
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first()
        )
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the event exists in both places
        self.assertIsNotNone(new_christmas_event, "Child pages weren't copied")
        self.assertIsNotNone(
            old_christmas_event, "Child pages were removed from original page"
        )

        # Check that the url path was updated
        self.assertEqual(
            new_christmas_event.url_path, "/home/new-events-index/christmas/"
        )

        # Check that the children were also created as aliases
        self.assertEqual(new_christmas_event.alias_of, old_christmas_event)

    def test_create_alias_copies_recursively_with_child_objects(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")

        # Copy it
        new_events_index = events_index.create_alias(
            recursive=True, update_slug="new-events-index"
        )

        # Get christmas event
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first()
        )
        new_christmas_event = (
            new_events_index.get_children().filter(slug="christmas").first()
        )

        # Check that the speakers were copied
        self.assertEqual(
            new_christmas_event.specific.speakers.count(),
            1,
            "Child objects weren't copied",
        )

        # Check that the speakers weren't removed from old page
        self.assertEqual(
            old_christmas_event.specific.speakers.count(),
            1,
            "Child objects were removed from the original page",
        )

    def test_create_alias_doesnt_copy_recursively_to_the_same_tree(self):
        events_index = EventIndex.objects.get(url_path="/home/events/")
        old_christmas_event = (
            events_index.get_children().filter(slug="christmas").first().specific
        )
        old_christmas_event.save_revision()

        with self.assertRaises(Exception) as exception:
            events_index.create_alias(recursive=True, parent=events_index)

        self.assertEqual(
            str(exception.exception),
            "You cannot copy a tree branch recursively into itself",
        )

    def test_create_alias_updates_user(self):
        event_moderator = get_user_model().objects.get(
            email="eventmoderator@example.com"
        )
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        # Copy it
        new_christmas_event = christmas_event.create_alias(
            update_slug="new-christmas-event", user=event_moderator
        )

        # Check that the owner has been updated
        self.assertEqual(new_christmas_event.owner, event_moderator)

    def test_create_alias_multi_table_inheritance(self):
        saint_patrick_event = SingleEventPage.objects.get(
            url_path="/home/events/saint-patrick/"
        )

        # Copy it
        new_saint_patrick_event = saint_patrick_event.create_alias(
            update_slug="new-saint-patrick"
        )

        # Check that new_saint_patrick_event is correct
        self.assertIsInstance(new_saint_patrick_event, SingleEventPage)
        self.assertEqual(new_saint_patrick_event.excerpt, saint_patrick_event.excerpt)

        # Check that new_saint_patrick_event is a different page, including parents from both EventPage and Page
        self.assertNotEqual(saint_patrick_event.id, new_saint_patrick_event.id)
        self.assertNotEqual(
            saint_patrick_event.eventpage_ptr.id,
            new_saint_patrick_event.eventpage_ptr.id,
        )
        self.assertNotEqual(
            saint_patrick_event.eventpage_ptr.page_ptr.id,
            new_saint_patrick_event.eventpage_ptr.page_ptr.id,
        )

        # Check that the url path was updated
        self.assertEqual(
            new_saint_patrick_event.url_path, "/home/events/new-saint-patrick/"
        )

        # Check that both parent instance exists
        self.assertIsInstance(
            EventPage.objects.get(id=new_saint_patrick_event.id), EventPage
        )
        self.assertIsInstance(Page.objects.get(id=new_saint_patrick_event.id), Page)

    def test_create_alias_copies_tags(self):
        # create and publish a TaggedPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        tagged_page = TaggedPage(title="My tagged page", slug="my-tagged-page")
        tagged_page.tags.add("wagtail", "bird")
        event_index.add_child(instance=tagged_page)
        tagged_page.save_revision().publish()

        old_tagged_item_ids = [item.id for item in tagged_page.tagged_items.all()]
        # there should be two items here, with defined (truthy) IDs
        self.assertEqual(len(old_tagged_item_ids), 2)
        self.assertTrue(all(old_tagged_item_ids))

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_tagged_page = tagged_page.create_alias(parent=homepage)

        self.assertNotEqual(tagged_page.id, new_tagged_page.id)

        # new page should also have two tags
        new_tagged_item_ids = [item.id for item in new_tagged_page.tagged_items.all()]
        self.assertEqual(len(new_tagged_item_ids), 2)
        self.assertTrue(all(new_tagged_item_ids))

        # new tagged_item IDs should differ from old ones
        self.assertTrue(
            all(item_id not in old_tagged_item_ids for item_id in new_tagged_item_ids)
        )

    def test_create_alias_with_m2m_relations(self):
        # create and publish a ManyToManyBlogPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        category = BlogCategory.objects.create(name="Birds")
        advert = Advert.objects.create(
            url="http://www.heinz.com/", text="beanz meanz heinz"
        )

        blog_page = ManyToManyBlogPage(title="My blog page", slug="my-blog-page")
        event_index.add_child(instance=blog_page)

        blog_page.adverts.add(advert)
        BlogCategoryBlogPage.objects.create(category=category, page=blog_page)
        blog_page.save_revision().publish()

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_blog_page = blog_page.create_alias(parent=homepage)

        # M2M relations are not formally supported, so for now we're only interested in
        # the copy operation as a whole succeeding, rather than the child objects being copied
        self.assertNotEqual(blog_page.id, new_blog_page.id)

    def test_create_alias_with_generic_foreign_key(self):
        # create and publish a GenericSnippetPage under Events
        event_index = Page.objects.get(url_path="/home/events/")
        advert = Advert.objects.create(
            url="http://www.heinz.com/", text="beanz meanz heinz"
        )

        page = GenericSnippetPage(title="My snippet page", slug="my-snippet-page")
        page.snippet_content_object = advert
        event_index.add_child(instance=page)

        page.save_revision().publish()

        # copy to underneath homepage
        homepage = Page.objects.get(url_path="/home/")
        new_page = page.create_alias(parent=homepage)

        self.assertNotEqual(page.id, new_page.id)
        self.assertEqual(new_page.snippet_content_object, advert)

    def test_create_alias_with_o2o_relation(self):
        event_index = Page.objects.get(url_path="/home/events/")

        page = OneToOnePage(title="My page", slug="my-page")

        event_index.add_child(instance=page)

        homepage = Page.objects.get(url_path="/home/")
        new_page = page.create_alias(parent=homepage)

        self.assertNotEqual(page.id, new_page.id)

    @unittest.expectedFailure
    def test_create_alias_with_additional_excluded_fields(self):
        homepage = Page.objects.get(url_path="/home/")
        page = homepage.add_child(
            instance=PageWithExcludedCopyField(
                title="Discovery",
                slug="disco",
                content="NCC-1031",
                special_field="Context is for Kings",
            )
        )
        new_page = page.create_alias(parent=homepage, update_slug="disco-2")

        self.assertEqual(page.title, new_page.title)
        self.assertNotEqual(page.id, new_page.id)
        self.assertNotEqual(page.path, new_page.path)
        # special_field is in the list to be excluded
        self.assertNotEqual(page.special_field, new_page.special_field)

    @unittest.expectedFailure
    def test_create_alias_with_excluded_parental_and_child_relations(self):
        """Test that a page will be copied with parental and child relations removed if excluded."""

        try:
            # modify excluded fields for this test
            EventPage.exclude_fields_in_copy = [
                "advert_placements",
                "categories",
                "signup_link",
            ]

            # set up data
            christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
            summer_category = EventCategory.objects.create(name="Summer")
            holiday_category = EventCategory.objects.create(name="Holidays")

            # add URL (to test excluding a basic field)
            christmas_event.signup_link = "https://christmas-is-awesome.com/rsvp"

            # add parental many to many relations
            christmas_event.categories = (summer_category, holiday_category)
            christmas_event.save()

            # Copy it
            new_christmas_event = christmas_event.create_alias(
                update_slug="new-christmas-event"
            )

            # check that the signup_link was NOT copied
            self.assertEqual(
                christmas_event.signup_link, "https://christmas-is-awesome.com/rsvp"
            )
            self.assertEqual(new_christmas_event.signup_link, "")

            # check that original event is untouched
            self.assertEqual(
                christmas_event.categories.count(),
                2,
                "Child objects (parental many to many) defined on the superclass were removed from the original page",
            )

            # check that parental many to many are NOT copied
            self.assertEqual(
                new_christmas_event.categories.count(),
                0,
                "Child objects (parental many to many) were copied but should be excluded",
            )

            # check that child objects on original event were left untouched
            self.assertEqual(
                christmas_event.advert_placements.count(),
                1,
                "Child objects defined on the original superclass were edited when copied",
            )

            # check that child objects were NOT copied
            self.assertEqual(
                new_christmas_event.advert_placements.count(),
                0,
                "Child objects defined on the superclass were copied and should not be",
            )

        finally:
            # reset excluded fields for future tests
            EventPage.exclude_fields_in_copy = []

    def test_alias_page_copies_restriction(self):
        """Test that view restrictions attached to a page are copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")
        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        homepage.add_child(instance=child_page_1)

        # Add PageViewRestriction to child_page_1
        group = Group.objects.create(name="Test Group")
        restriction = PageViewRestriction.objects.create(
            page=child_page_1, restriction_type=PageViewRestriction.GROUPS
        )
        restriction.groups.add(group)

        child_page_2 = child_page_1.create_alias(update_slug="child-page-2")

        # check that the copied page child_page_2 has a view restriction
        copied_restriction = PageViewRestriction.objects.get(page=child_page_2)
        # check that the copied restriction has the same groups as the original
        self.assertEqual(
            list(copied_restriction.groups.values_list("id", flat=True)), [group.pk]
        )

    def test_alias_page_does_not_copy_restrictions_from_parent(self):
        """Test that view restrictions on a page's ancestor are NOT copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")

        origin_parent = SimplePage(
            title="Parent 1", slug="parent-1", content="hello parent 1"
        )
        homepage.add_child(instance=origin_parent)
        PageViewRestriction.objects.create(page=origin_parent, password="hello")

        destination_parent = SimplePage(
            title="Parent 2", slug="parent-2", content="hello parent 2"
        )
        homepage.add_child(instance=destination_parent)

        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        origin_parent.add_child(instance=child_page_1)

        child_page_2 = child_page_1.create_alias(
            parent=destination_parent,
            update_slug="child-page-2",
        )
        # check that the copied page child_page_2 does not have a view restriction
        self.assertFalse(PageViewRestriction.objects.filter(page=child_page_2).exists())

    def test_alias_page_does_not_copy_restrictions_when_new_parent_has_one_already(
        self,
    ):
        """Test that view restrictions on a page's ancestor are NOT copied along with the page"""

        homepage = Page.objects.get(url_path="/home/")

        origin_parent = SimplePage(
            title="Parent 1", slug="parent-1", content="hello parent 1"
        )
        homepage.add_child(instance=origin_parent)

        destination_parent = SimplePage(
            title="Parent 2", slug="parent-2", content="hello parent 2"
        )
        homepage.add_child(instance=destination_parent)
        PageViewRestriction.objects.create(page=destination_parent, password="hello")

        child_page_1 = SimplePage(
            title="Child Page 1", slug="child-page-1", content="hello child page 1"
        )
        origin_parent.add_child(instance=child_page_1)
        PageViewRestriction.objects.create(page=child_page_1, password="hello")

        child_page_2 = child_page_1.create_alias(
            parent=destination_parent,
            update_slug="child-page-2",
        )
        # check that the copied page child_page_2 does not have a view restriction
        self.assertFalse(PageViewRestriction.objects.filter(page=child_page_2).exists())


class TestUpdateAliases(TestCase):
    fixtures = ["test.json"]

    def test_update_aliases(self):
        event_page = EventPage.objects.get(url_path="/home/events/christmas/")
        alias = event_page.create_alias(update_slug="new-event-page")
        alias_alias = alias.create_alias(update_slug="new-event-page-2")

        # Update the title and add a speaker
        event_page.title = "Updated title"
        event_page.draft_title = "A different draft title"
        event_page.speakers.add(
            EventPageSpeaker(
                first_name="Ted",
                last_name="Crilly",
            )
        )
        event_page.save()

        # Nothing should've happened yet
        alias.refresh_from_db()
        alias_alias.refresh_from_db()
        self.assertEqual(alias.title, "Christmas")
        self.assertEqual(alias_alias.title, "Christmas")
        self.assertEqual(alias.speakers.count(), 1)
        self.assertEqual(alias_alias.speakers.count(), 1)

        PageLogEntry.objects.all().delete()

        event_page.update_aliases()

        # Check that the aliases have been updated
        alias.refresh_from_db()
        alias_alias.refresh_from_db()
        self.assertEqual(alias.title, "Updated title")
        self.assertEqual(alias_alias.title, "Updated title")
        self.assertEqual(alias.speakers.count(), 2)
        self.assertEqual(alias_alias.speakers.count(), 2)

        # Draft titles shouldn't update as alias pages do not have drafts
        self.assertEqual(alias.draft_title, "Updated title")
        self.assertEqual(alias_alias.draft_title, "Updated title")

        # Check no log entries were created for the aliases
        self.assertFalse(
            PageLogEntry.objects.filter(page=alias, action="wagtail.publish").exists()
        )
        self.assertFalse(
            PageLogEntry.objects.filter(
                page=alias_alias, action="wagtail.publish"
            ).exists()
        )

    def test_update_aliases_publishes_drafts(self):
        event_page = EventPage.objects.get(url_path="/home/events/christmas/")

        # Unpublish the event page so that the aliases will be created in draft
        event_page.live = False
        event_page.has_unpublished_changes = True
        event_page.save(clean=False)

        alias = event_page.create_alias(update_slug="new-event-page")
        alias_alias = alias.create_alias(update_slug="new-event-page-2")

        self.assertFalse(alias.live)
        self.assertFalse(alias_alias.live)

        # Publish the event page
        event_page.live = True
        event_page.has_unpublished_changes = False
        event_page.save(clean=False)

        # Nothing should've happened yet
        alias.refresh_from_db()
        alias_alias.refresh_from_db()
        self.assertFalse(alias.live)
        self.assertFalse(alias_alias.live)

        PageLogEntry.objects.all().delete()

        event_page.update_aliases()

        # Check that the aliases have been updated
        alias.refresh_from_db()
        alias_alias.refresh_from_db()
        self.assertTrue(alias.live)
        self.assertTrue(alias_alias.live)

        # Check no log entries were created for the aliases
        self.assertFalse(
            PageLogEntry.objects.filter(page=alias, action="wagtail.publish").exists()
        )
        self.assertFalse(
            PageLogEntry.objects.filter(
                page=alias_alias, action="wagtail.publish"
            ).exists()
        )


class TestCopyForTranslation(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.en_homepage = Page.objects.get(url_path="/home/").specific
        self.en_eventindex = EventIndex.objects.get(url_path="/home/events/")
        self.en_eventpage = EventPage.objects.get(url_path="/home/events/christmas/")
        self.root_page = self.en_homepage.get_parent()
        self.fr_locale = Locale.objects.create(language_code="fr")

    def test_copy_homepage(self):
        fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)

        self.assertNotEqual(self.en_homepage.id, fr_homepage.id)
        self.assertEqual(fr_homepage.locale, self.fr_locale)
        self.assertEqual(fr_homepage.translation_key, self.en_homepage.translation_key)

        # At the top level, the language code should be appended to the slug
        self.assertEqual(fr_homepage.slug, "home-fr")

        # Translation must be in draft
        self.assertFalse(fr_homepage.live)
        self.assertTrue(fr_homepage.has_unpublished_changes)

        # Check log
        log_entry = PageLogEntry.objects.get(action="wagtail.copy_for_translation")
        self.assertEqual(log_entry.data["source_locale"]["language_code"], "en")
        self.assertEqual(log_entry.data["page"]["locale"]["language_code"], "fr")
        self.assertEqual(
            log_entry.message, "Copied for translation from Root (English)"
        )

    def test_copy_homepage_slug_exists(self):
        # This test is the same as test_copy_homepage, but we will create another page with
        # the slug "home-fr" before translating. copy_for_translation should pick a different slug
        self.root_page.add_child(
            instance=SimplePage(
                title="Old french homepage", slug="home-fr", content="Test content"
            )
        )

        fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
        self.assertEqual(fr_homepage.slug, "home-fr-1")

    def test_copy_childpage(self):
        # Create translated homepage manually
        fr_homepage = self.root_page.add_child(
            instance=Page(
                title="french homepage",
                slug="home-fr",
                locale=self.fr_locale,
                translation_key=self.en_homepage.translation_key,
            )
        )

        fr_eventindex = self.en_eventindex.copy_for_translation(self.fr_locale)

        self.assertNotEqual(self.en_eventindex.id, fr_eventindex.id)
        self.assertEqual(fr_eventindex.locale, self.fr_locale)
        self.assertEqual(
            fr_eventindex.translation_key, self.en_eventindex.translation_key
        )

        # Check that the fr event index was created under the fr homepage
        self.assertEqual(fr_eventindex.get_parent(), fr_homepage)

        # The slug should be the same when copying to another tree
        self.assertEqual(self.en_eventindex.slug, fr_eventindex.slug)

        # Check log
        log_entry = PageLogEntry.objects.get(action="wagtail.copy_for_translation")
        self.assertEqual(log_entry.data["source_locale"]["language_code"], "en")
        self.assertEqual(log_entry.data["page"]["locale"]["language_code"], "fr")
        self.assertEqual(
            log_entry.message,
            "Copied for translation from Welcome to the Wagtail test site! (English)",
        )

    def test_copy_childpage_without_parent(self):
        # This test is the same as test_copy_childpage but we won't create the parent page first

        with self.assertRaises(ParentNotTranslatedError):
            self.en_eventindex.copy_for_translation(self.fr_locale)

    def test_copy_childpage_with_copy_parents(self):
        # This time we will set copy_parents
        fr_eventindex = self.en_eventindex.copy_for_translation(
            self.fr_locale, copy_parents=True
        )

        self.assertNotEqual(self.en_eventindex.id, fr_eventindex.id)
        self.assertEqual(fr_eventindex.locale, self.fr_locale)
        self.assertEqual(
            fr_eventindex.translation_key, self.en_eventindex.translation_key
        )
        self.assertEqual(self.en_eventindex.slug, fr_eventindex.slug)

        # This should create the homepage as well
        fr_homepage = fr_eventindex.get_parent()

        self.assertNotEqual(self.en_homepage.id, fr_homepage.id)
        self.assertEqual(fr_homepage.locale, self.fr_locale)
        self.assertEqual(fr_homepage.translation_key, self.en_homepage.translation_key)
        self.assertEqual(fr_homepage.slug, "home-fr")

    def test_copy_page_with_translatable_child_objects(self):
        # Create translated homepage and event index manually
        fr_homepage = self.root_page.add_child(
            instance=Page(
                title="french homepage",
                slug="home-fr",
                locale=self.fr_locale,
                translation_key=self.en_homepage.translation_key,
            )
        )

        fr_homepage.add_child(
            instance=EventIndex(
                title="Events",
                slug="events",
                locale=self.fr_locale,
                translation_key=self.en_eventindex.translation_key,
            )
        )

        # Add an award to the speaker
        # TODO: Nested child objects not supported by page copy
        en_speaker = self.en_eventpage.speakers.get()
        # en_award = EventPageSpeakerAward.objects.create(
        #     speaker=en_speaker,
        #     name="Golden Globe"
        # )

        fr_eventpage = self.en_eventpage.copy_for_translation(self.fr_locale)

        # Check that the speakers and awards were copied for translation properly
        fr_speaker = fr_eventpage.speakers.get()
        self.assertEqual(fr_speaker.locale, self.fr_locale)
        self.assertEqual(fr_speaker.translation_key, en_speaker.translation_key)
        self.assertEqual(list(fr_speaker.get_translations()), [en_speaker])

        # TODO: Nested child objects not supported by page copy
        # fr_award = fr_speaker.awards.get()
        # self.assertEqual(ffr_award.locale, self.fr_locale)
        # self.assertEqual(ffr_award.translation_key, en_award.translation_key)
        # self.assertEqual(list(fr_award.get_translations()), [en_award])

    def test_copies_missing_parents_as_aliases(self):
        fr_eventpage = self.en_eventpage.copy_for_translation(
            self.fr_locale, copy_parents=True
        )
        fr_eventindex = fr_eventpage.get_parent()

        # Check parent is a translation of its English original
        self.assertEqual(fr_eventindex.locale, self.fr_locale)
        self.assertEqual(
            fr_eventindex.translation_key, self.en_eventindex.translation_key
        )

        # Check parent is also an alias of its English original
        self.assertEqual(fr_eventindex.alias_of, self.en_eventindex)


class TestSubpageTypeBusinessRules(WagtailTestUtils, TestCase):
    def test_allowed_subpage_models(self):
        # SimplePage does not define any restrictions on subpage types
        # SimplePage is a valid subpage of SimplePage
        self.assertIn(SimplePage, SimplePage.allowed_subpage_models())
        # BusinessIndex is a valid subpage of SimplePage
        self.assertIn(BusinessIndex, SimplePage.allowed_subpage_models())
        # BusinessSubIndex is not valid, because it explicitly omits SimplePage from parent_page_types
        self.assertNotIn(BusinessSubIndex, SimplePage.allowed_subpage_models())

        # BusinessChild has an empty subpage_types list, so does not allow anything
        self.assertNotIn(SimplePage, BusinessChild.allowed_subpage_models())
        self.assertNotIn(BusinessIndex, BusinessChild.allowed_subpage_models())
        self.assertNotIn(BusinessSubIndex, BusinessChild.allowed_subpage_models())

        # BusinessSubIndex only allows BusinessChild as subpage type
        self.assertNotIn(SimplePage, BusinessSubIndex.allowed_subpage_models())
        self.assertIn(BusinessChild, BusinessSubIndex.allowed_subpage_models())

    def test_allowed_parent_page_models(self):
        # SimplePage does not define any restrictions on parent page types
        # SimplePage is a valid parent page of SimplePage
        self.assertIn(SimplePage, SimplePage.allowed_parent_page_models())
        # BusinessChild cannot be a parent of anything
        self.assertNotIn(BusinessChild, SimplePage.allowed_parent_page_models())

        # BusinessNowherePage does not allow anything as a parent
        self.assertNotIn(SimplePage, BusinessNowherePage.allowed_parent_page_models())
        self.assertNotIn(
            StandardIndex, BusinessNowherePage.allowed_parent_page_models()
        )

        # BusinessSubIndex only allows BusinessIndex as a parent
        self.assertNotIn(SimplePage, BusinessSubIndex.allowed_parent_page_models())
        self.assertIn(BusinessIndex, BusinessSubIndex.allowed_parent_page_models())

    def test_can_exist_under(self):
        self.assertTrue(SimplePage.can_exist_under(SimplePage()))

        # StandardIndex should only be allowed under a Page
        self.assertTrue(StandardIndex.can_exist_under(Page()))
        self.assertFalse(StandardIndex.can_exist_under(SimplePage()))

        # The Business pages are quite restrictive in their structure
        self.assertTrue(BusinessSubIndex.can_exist_under(BusinessIndex()))
        self.assertTrue(BusinessChild.can_exist_under(BusinessIndex()))
        self.assertTrue(BusinessChild.can_exist_under(BusinessSubIndex()))

        self.assertFalse(BusinessSubIndex.can_exist_under(SimplePage()))
        self.assertFalse(BusinessSubIndex.can_exist_under(BusinessSubIndex()))
        self.assertFalse(BusinessChild.can_exist_under(SimplePage()))

    def test_can_create_at(self):
        # Pages are not `is_creatable`, and should not be creatable
        self.assertFalse(Page.can_create_at(Page()))

        # SimplePage can be created under a simple page
        self.assertTrue(SimplePage.can_create_at(SimplePage()))

        # StandardIndex can be created under a Page, but not a SimplePage
        self.assertTrue(StandardIndex.can_create_at(Page()))
        self.assertFalse(StandardIndex.can_create_at(SimplePage()))

        # The Business pages are quite restrictive in their structure
        self.assertTrue(BusinessSubIndex.can_create_at(BusinessIndex()))
        self.assertTrue(BusinessChild.can_create_at(BusinessIndex()))
        self.assertTrue(BusinessChild.can_create_at(BusinessSubIndex()))

        self.assertFalse(BusinessChild.can_create_at(SimplePage()))
        self.assertFalse(BusinessSubIndex.can_create_at(SimplePage()))

    def test_can_create_at_with_max_count_per_parent_limited_to_one(self):
        root_page = Page.objects.get(url_path="/home/")

        # Create 2 parent pages for our limited page model
        parent1 = root_page.add_child(
            instance=SimpleParentPage(title="simple parent", slug="simple-parent")
        )
        parent2 = root_page.add_child(
            instance=SimpleParentPage(title="simple parent", slug="simple-parent-2")
        )

        # Add a child page to one of the pages (assert just to be sure)
        self.assertTrue(SimpleChildPage.can_create_at(parent1))
        parent1.add_child(
            instance=SimpleChildPage(title="simple child", slug="simple-child")
        )

        # We already have a `SimpleChildPage` as a child of `parent1`, and since it is limited
        # to have only 1 child page, we cannot create another one. However, we should still be able
        # to create an instance for this page at a different location (as child of `parent2`)
        self.assertFalse(SimpleChildPage.can_create_at(parent1))
        self.assertTrue(SimpleChildPage.can_create_at(parent2))

    def test_can_move_to(self):
        self.assertTrue(SimplePage().can_move_to(SimplePage()))

        # StandardIndex should only be allowed under a Page
        self.assertTrue(StandardIndex().can_move_to(Page()))
        self.assertFalse(StandardIndex().can_move_to(SimplePage()))

        # The Business pages are quite restrictive in their structure
        self.assertTrue(BusinessSubIndex().can_move_to(BusinessIndex()))
        self.assertTrue(BusinessChild().can_move_to(BusinessIndex()))
        self.assertTrue(BusinessChild().can_move_to(BusinessSubIndex()))

        self.assertFalse(BusinessChild().can_move_to(SimplePage()))
        self.assertFalse(BusinessSubIndex().can_move_to(SimplePage()))

    def test_singleton_page_creation(self):
        root_page = Page.objects.get(url_path="/home/")

        # A single singleton page should be creatable
        self.assertTrue(SingletonPage.can_create_at(root_page))

        # Create a singleton page
        root_page.add_child(instance=SingletonPage(title="singleton", slug="singleton"))

        # A second singleton page should not be creatable
        self.assertFalse(SingletonPage.can_create_at(root_page))


class TestIssue735(TestCase):
    """
    Issue 735 reports that URL paths of child pages are not
    updated correctly when slugs of parent pages are updated
    """

    fixtures = ["test.json"]

    def test_child_urls_updated_on_parent_publish(self):
        event_index = Page.objects.get(url_path="/home/events/").specific
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        # Change the event index slug and publish it
        event_index.slug = "old-events"
        event_index.save_revision().publish()

        # Check that the christmas events url path updated correctly
        new_christmas_event = EventPage.objects.get(id=christmas_event.id)
        self.assertEqual(new_christmas_event.url_path, "/home/old-events/christmas/")


class TestIssue756(TestCase):
    """
    Issue 756 reports that the latest_revision_created_at
    field was getting clobbered whenever a revision was published
    """

    def test_publish_revision_doesnt_remove_latest_revision_created_at(self):
        # Create a revision
        revision = Page.objects.get(id=1).save_revision()

        # Check that latest_revision_created_at is set
        self.assertIsNotNone(Page.objects.get(id=1).latest_revision_created_at)

        # Publish the revision
        revision.publish()

        # Check that latest_revision_created_at is still set
        self.assertIsNotNone(Page.objects.get(id=1).latest_revision_created_at)


class TestIssue1216(TestCase):
    """
    Test that url paths greater than 255 characters are supported
    """

    fixtures = ["test.json"]

    def test_url_path_can_exceed_255_characters(self):
        event_index = Page.objects.get(url_path="/home/events/").specific
        christmas_event = EventPage.objects.get(
            url_path="/home/events/christmas/"
        ).specific

        # Change the christmas_event slug first - this way, we test that the process for
        # updating child url paths also handles >255 character paths correctly
        new_christmas_slug = "christmas-%s-christmas" % ("0123456789" * 20)
        christmas_event.slug = new_christmas_slug
        christmas_event.save_revision().publish()

        # Change the event index slug and publish it
        new_event_index_slug = "events-%s-events" % ("0123456789" * 20)
        event_index.slug = new_event_index_slug
        event_index.save_revision().publish()

        # Check that the url path updated correctly
        new_christmas_event = EventPage.objects.get(id=christmas_event.id)
        expected_url_path = "/home/{}/{}/".format(
            new_event_index_slug, new_christmas_slug
        )
        self.assertEqual(new_christmas_event.url_path, expected_url_path)


class TestIsCreatable(TestCase):
    def test_is_creatable_default(self):
        """By default, pages should be creatable"""
        self.assertTrue(SimplePage.is_creatable)
        self.assertIn(SimplePage, get_page_models())

    def test_is_creatable_false(self):
        """Page types should be able to disable their creation"""
        self.assertFalse(MTIBasePage.is_creatable)
        # non-creatable pages should still appear in the get_page_models list
        self.assertIn(MTIBasePage, get_page_models())

    def test_is_creatable_not_inherited(self):
        """
        is_creatable should not be inherited in the normal manner, and should
        default to True unless set otherwise
        """
        self.assertTrue(MTIChildPage.is_creatable)
        self.assertIn(MTIChildPage, get_page_models())

    def test_abstract_pages(self):
        """
        Abstract models should not be creatable
        """
        self.assertFalse(AbstractPage.is_creatable)
        self.assertNotIn(AbstractPage, get_page_models())


class TestDeferredPageClasses(TestCase):
    def test_deferred_page_classes_are_not_registered(self):
        """
        In Django <1.10, a call to `defer` such as `SimplePage.objects.defer('content')`
        will dynamically create a subclass of SimplePage. Ensure that these subclasses
        are not registered in the get_page_models() list
        """
        list(SimplePage.objects.defer("content"))
        simplepage_subclasses = [
            cls for cls in get_page_models() if issubclass(cls, SimplePage)
        ]
        self.assertEqual(simplepage_subclasses, [SimplePage])


class TestPageManager(TestCase):
    def test_page_manager(self):
        """
        Assert that the Page class uses PageManager
        """
        self.assertIs(type(Page.objects), PageManager)

    def test_page_subclass_manager(self):
        """
        Assert that Page subclasses get a PageManager without having to do
        anything special. MTI subclasses do *not* inherit their parents Manager
        by default.
        """
        self.assertIs(type(SimplePage.objects), PageManager)

    def test_custom_page_manager(self):
        """
        Subclasses should be able to override their default Manager, and
        Wagtail should respect this. It is up to the developer to ensure their
        custom Manager inherits from PageManager.
        """
        self.assertIs(type(CustomManagerPage.objects), CustomManager)

    def test_custom_page_queryset(self):
        """
        Managers that are constructed from a custom PageQuerySet
        (via PageManager.from_queryset(CustomPageQuerySet)) should return
        querysets of that type
        """
        self.assertIs(type(CustomManagerPage.objects.all()), CustomPageQuerySet)
        self.assertIs(type(CustomManagerPage.objects.about_spam()), CustomPageQuerySet)
        self.assertIs(
            type(CustomManagerPage.objects.all().about_spam()), CustomPageQuerySet
        )
        self.assertIs(
            type(CustomManagerPage.objects.about_spam().all()), CustomPageQuerySet
        )

    def test_abstract_base_page_manager(self):
        """
        Abstract base classes should be able to override their default Manager,
        and Wagtail should respect this. It is up to the developer to ensure
        their custom Manager inherits from PageManager.
        """
        self.assertIs(type(MyCustomPage.objects), CustomManager)


class TestIssue2024(TestCase):
    """
    This tests that deleting a content type can't delete any Page objects.
    """

    fixtures = ["test.json"]

    def test_delete_content_type(self):
        event_index = Page.objects.get(url_path="/home/events/")

        # Delete the content type
        event_index_content_type = event_index.content_type
        event_index_content_type.delete()

        # Fetch the page again, it should still exist
        event_index = Page.objects.get(url_path="/home/events/")

        # Check that the content_type changed to Page
        self.assertEqual(
            event_index.content_type, ContentType.objects.get_for_model(Page)
        )


class TestMakePreviewRequest(TestCase):
    fixtures = ["test.json"]

    def test_make_preview_request_for_accessible_page(self):
        event_index = Page.objects.get(url_path="/home/events/")
        response = event_index.make_preview_request()
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # request should have the correct path and hostname for this page
        self.assertEqual(request.path, "/events/")
        self.assertEqual(request.headers["host"], "localhost")

        # check other env vars required by the WSGI spec
        self.assertEqual(request.META["REQUEST_METHOD"], "GET")
        self.assertEqual(request.META["SCRIPT_NAME"], "")
        self.assertEqual(request.META["PATH_INFO"], "/events/")
        self.assertEqual(request.META["SERVER_NAME"], "localhost")
        self.assertEqual(request.META["SERVER_PORT"], 80)
        self.assertEqual(request.META["SERVER_PROTOCOL"], "HTTP/1.1")
        self.assertEqual(request.META["wsgi.version"], (1, 0))
        self.assertEqual(request.META["wsgi.url_scheme"], "http")
        self.assertIn("wsgi.input", request.META)
        self.assertIn("wsgi.errors", request.META)
        self.assertIn("wsgi.multithread", request.META)
        self.assertIn("wsgi.multiprocess", request.META)
        self.assertIn("wsgi.run_once", request.META)

    def test_make_preview_request_for_accessible_page_https(self):
        Site.objects.update(port=443)

        event_index = Page.objects.get(url_path="/home/events/")
        response = event_index.make_preview_request()
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # request should have the correct path and hostname for this page
        self.assertEqual(request.path, "/events/")
        self.assertEqual(request.headers["host"], "localhost")

        # check other env vars required by the WSGI spec
        self.assertEqual(request.META["REQUEST_METHOD"], "GET")
        self.assertEqual(request.META["SCRIPT_NAME"], "")
        self.assertEqual(request.META["PATH_INFO"], "/events/")
        self.assertEqual(request.META["SERVER_NAME"], "localhost")
        self.assertEqual(request.META["SERVER_PORT"], 443)
        self.assertEqual(request.META["SERVER_PROTOCOL"], "HTTP/1.1")
        self.assertEqual(request.META["wsgi.version"], (1, 0))
        self.assertEqual(request.META["wsgi.url_scheme"], "https")
        self.assertIn("wsgi.input", request.META)
        self.assertIn("wsgi.errors", request.META)
        self.assertIn("wsgi.multithread", request.META)
        self.assertIn("wsgi.multiprocess", request.META)
        self.assertIn("wsgi.run_once", request.META)

    def test_make_preview_request_for_accessible_page_non_standard_port(self):
        Site.objects.update(port=8888)

        event_index = Page.objects.get(url_path="/home/events/")
        response = event_index.make_preview_request()
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # request should have the correct path and hostname for this page
        self.assertEqual(request.path, "/events/")
        self.assertEqual(request.headers["host"], "localhost:8888")

        # check other env vars required by the WSGI spec
        self.assertEqual(request.META["REQUEST_METHOD"], "GET")
        self.assertEqual(request.META["SCRIPT_NAME"], "")
        self.assertEqual(request.META["PATH_INFO"], "/events/")
        self.assertEqual(request.META["SERVER_NAME"], "localhost")
        self.assertEqual(request.META["SERVER_PORT"], 8888)
        self.assertEqual(request.META["SERVER_PROTOCOL"], "HTTP/1.1")
        self.assertEqual(request.META["wsgi.version"], (1, 0))
        self.assertEqual(request.META["wsgi.url_scheme"], "http")
        self.assertIn("wsgi.input", request.META)
        self.assertIn("wsgi.errors", request.META)
        self.assertIn("wsgi.multithread", request.META)
        self.assertIn("wsgi.multiprocess", request.META)
        self.assertIn("wsgi.run_once", request.META)

    def test_make_preview_request_for_accessible_page_with_original_request(self):
        event_index = Page.objects.get(url_path="/home/events/")
        original_headers = {
            "REMOTE_ADDR": "192.168.0.1",
            "HTTP_X_FORWARDED_FOR": "192.168.0.2,192.168.0.3",
            "HTTP_COOKIE": "test=1;blah=2",
            "HTTP_USER_AGENT": "Test Agent",
            "HTTP_AUTHORIZATION": "Basic V2FndGFpbDpXYWd0YWlsCg==",
        }
        factory = RequestFactory(**original_headers)
        original_request = factory.get("/home/events/")
        response = event_index.make_preview_request(original_request)
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # request should have the all the special headers we set in original_request
        self.assertEqual(
            request.META["REMOTE_ADDR"], original_request.META["REMOTE_ADDR"]
        )
        self.assertEqual(
            request.headers["x-forwarded-for"],
            original_request.META["HTTP_X_FORWARDED_FOR"],
        )
        self.assertEqual(
            request.headers["cookie"], original_request.META["HTTP_COOKIE"]
        )
        self.assertEqual(
            request.headers["user-agent"], original_request.META["HTTP_USER_AGENT"]
        )
        self.assertEqual(
            request.headers["authorization"],
            original_request.META["HTTP_AUTHORIZATION"],
        )

        # check other env vars required by the WSGI spec
        self.assertEqual(request.META["REQUEST_METHOD"], "GET")
        self.assertEqual(request.META["SCRIPT_NAME"], "")
        self.assertEqual(request.META["PATH_INFO"], "/events/")
        self.assertEqual(request.META["SERVER_NAME"], "localhost")
        self.assertEqual(request.META["SERVER_PORT"], 80)
        self.assertEqual(request.META["SERVER_PROTOCOL"], "HTTP/1.1")
        self.assertEqual(request.META["wsgi.version"], (1, 0))
        self.assertEqual(request.META["wsgi.url_scheme"], "http")
        self.assertIn("wsgi.input", request.META)
        self.assertIn("wsgi.errors", request.META)
        self.assertIn("wsgi.multithread", request.META)
        self.assertIn("wsgi.multiprocess", request.META)
        self.assertIn("wsgi.run_once", request.META)

    @override_settings(ALLOWED_HOSTS=["production.example.com"])
    def test_make_preview_request_for_inaccessible_page_should_use_valid_host(self):
        root_page = Page.objects.get(url_path="/")
        response = root_page.make_preview_request()
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # in the absence of an actual Site record where we can access this page,
        # make_preview_request should still provide a hostname that Django's host header
        # validation won't reject
        self.assertEqual(request.headers["host"], "production.example.com")

    @override_settings(ALLOWED_HOSTS=["*"])
    def test_make_preview_request_for_inaccessible_page_with_wildcard_allowed_hosts(
        self,
    ):
        root_page = Page.objects.get(url_path="/")
        response = root_page.make_preview_request()
        self.assertEqual(response.status_code, 200)
        request = response.context_data["request"]

        # '*' is not a valid hostname, so ensure that we replace it with something sensible
        self.assertNotEqual(request.headers["host"], "*")

    def test_is_previewable(self):
        event_index = Page.objects.get(url_path="/home/events/")
        stream_page = StreamPage(title="stream page", body=[("text", "hello")])
        event_index.add_child(instance=stream_page)
        plain_stream_page = Page.objects.get(id=stream_page.id)

        # StreamPage sets preview_modes to an empty list, so stream_page is not previewable
        with self.assertNumQueries(0):
            self.assertFalse(stream_page.is_previewable())

        # is_previewable should also cope with being called on a base Page object, at the
        # cost of an extra query to access the specific object
        with self.assertNumQueries(1):
            self.assertFalse(plain_stream_page.is_previewable())

        # event_index is a plain Page object, but we should recognise that preview_modes
        # has not been overridden on EventIndexPage and avoid the extra query
        with self.assertNumQueries(0):
            self.assertTrue(event_index.is_previewable())


class TestShowInMenusDefaultOption(TestCase):
    """
    This tests that a page model can define the default for 'show_in_menus'
    """

    fixtures = ["test.json"]

    def test_show_in_menus_default(self):
        # Create a page that does not have the default init
        page = Page(title="My Awesome Page", slug="my-awesome-page")

        # Check that the page instance creates with show_in_menu as False
        self.assertFalse(page.show_in_menus)

    def test_show_in_menus_default_override(self):
        # Create a page that does have the default init
        page = AlwaysShowInMenusPage(title="My Awesome Page", slug="my-awesome-page")

        # Check that the page instance creates with show_in_menu as True
        self.assertTrue(page.show_in_menus)


class TestPageWithContentJSON(TestCase):
    fixtures = ["test.json"]

    def test_with_content_json_preserves_values(self):
        original_page = SimplePage.objects.get(url_path="/home/about-us/")
        eventpage_content_type = ContentType.objects.get_for_model(EventPage)

        # Take a json representation of the page and update it
        # with some alternative values
        content = original_page.serializable_data()
        content.update(
            title="About them",
            draft_title="About them",
            slug="about-them",
            url_path="/home/some-section/about-them/",
            pk=original_page.pk + 999,
            numchild=original_page.numchild + 999,
            depth=original_page.depth + 999,
            path=original_page.path + "ABCDEF",
            content="<p>They are not as good</p>",
            first_published_at="2000-01-01T00:00:00Z",
            last_published_at="2000-01-01T00:00:00Z",
            live=not original_page.live,
            locked=True,
            locked_by=1,
            locked_at="2000-01-01T00:00:00Z",
            has_unpublished_changes=not original_page.has_unpublished_changes,
            content_type=eventpage_content_type.id,
            show_in_menus=not original_page.show_in_menus,
            owner=1,
        )

        # Pass the values to with_content_json() to get an updated version of the page
        updated_page = original_page.with_content_json(content)

        # The following attributes values should have changed
        for attr_name in ("title", "slug", "content", "url_path", "show_in_menus"):
            self.assertNotEqual(
                getattr(original_page, attr_name), getattr(updated_page, attr_name)
            )

        # The following attribute values should have been preserved,
        # despite new values being provided in content
        for attr_name in (
            "pk",
            "path",
            "depth",
            "numchild",
            "content_type",
            "draft_title",
            "live",
            "has_unpublished_changes",
            "owner",
            "locked",
            "locked_by",
            "locked_at",
            "latest_revision_created_at",
            "first_published_at",
        ):
            self.assertEqual(
                getattr(original_page, attr_name), getattr(updated_page, attr_name)
            )

        # The url_path should reflect the new slug value, but the
        # rest of the path should have remained unchanged
        self.assertEqual(updated_page.url_path, "/home/about-them/")


class TestUnpublish(TestCase):
    fixtures = ["test.json"]

    def test_unpublish_doesnt_call_full_clean_before_save(self):
        root_page = Page.objects.get(id=1)
        home_page = root_page.add_child(
            instance=SimplePage(title="Homepage", slug="home2", content="hello")
        )
        # Empty the content - bypassing validation which would otherwise prevent it
        home_page.save(clean=False)
        # This shouldn't fail with a ValidationError.
        home_page.unpublish()

    def test_unpublish_also_unpublishes_aliases(self):
        event_page = EventPage.objects.get(url_path="/home/events/christmas/")
        alias = event_page.create_alias(update_slug="new-event-page")
        alias_alias = alias.create_alias(update_slug="new-event-page-2")

        self.assertTrue(event_page.live)
        self.assertTrue(alias.live)
        self.assertTrue(alias_alias.live)

        PageLogEntry.objects.all().delete()

        # Unpublish the event page
        event_page.unpublish()

        alias.refresh_from_db()
        alias_alias.refresh_from_db()
        self.assertFalse(event_page.live)
        self.assertFalse(alias.live)
        self.assertFalse(alias_alias.live)

        # Check no log entries were created for the aliases
        self.assertFalse(
            PageLogEntry.objects.filter(page=alias, action="wagtail.unpublish").exists()
        )
        self.assertFalse(
            PageLogEntry.objects.filter(
                page=alias_alias, action="wagtail.unpublish"
            ).exists()
        )


class TestCachedContentType(TestCase):
    """Tests for Page.cached_content_type"""

    def setUp(self):
        root_page = Page.objects.first()
        self.page = root_page.add_child(
            instance=SimplePage(title="Test1", slug="test1", content="test")
        )
        self.specific_page_ctype = ContentType.objects.get_for_model(SimplePage)

    def test_golden_path(self):
        """
        The return value should match the value you'd get
        if fetching the ContentType from the database,
        and shouldn't trigger any database queries when
        the ContentType is already in memory.
        """
        with self.assertNumQueries(0):
            result = self.page.cached_content_type
        self.assertEqual(result, ContentType.objects.get(id=self.page.content_type_id))


class TestGetTranslatableModels(TestCase):
    def test_get_translatable_models(self):
        translatable_models = get_translatable_models()

        # Only root translatable models should be included by default
        self.assertNotIn(EventPage, translatable_models)

        self.assertIn(Page, translatable_models)
        self.assertIn(EventPageSpeaker, translatable_models)
        self.assertNotIn(Site, translatable_models)
        self.assertNotIn(Advert, translatable_models)

    def test_get_translatable_models_include_subclasses(self):
        translatable_models = get_translatable_models(include_subclasses=True)

        self.assertIn(EventPage, translatable_models)

        self.assertIn(Page, translatable_models)
        self.assertIn(EventPageSpeaker, translatable_models)
        self.assertNotIn(Site, translatable_models)
        self.assertNotIn(Advert, translatable_models)


class TestDefaultLocale(TestCase):
    def setUp(self):
        self.root_page = Page.objects.first()

    def test_default_locale(self):
        page = self.root_page.add_child(
            instance=SimplePage(title="Test1", slug="test1", content="test")
        )

        self.assertEqual(page.locale, self.root_page.locale)

    def test_override_default_locale(self):
        fr_locale = Locale.objects.create(language_code="fr")

        page = self.root_page.add_child(
            instance=SimplePage(
                title="Test1", slug="test1", content="test", locale=fr_locale
            )
        )

        self.assertEqual(page.locale, fr_locale)

    def test_always_defaults_to_parent_locale(self):
        fr_locale = Locale.objects.create(language_code="fr")

        fr_page = self.root_page.add_child(
            instance=SimplePage(
                title="Test1", slug="test1", content="test", locale=fr_locale
            )
        )

        page = fr_page.add_child(
            instance=SimplePage(title="Test1", slug="test1", content="test")
        )

        self.assertEqual(page.locale, fr_locale)


@override_settings(WAGTAIL_I18N_ENABLED=True)
class TestLocalized(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.fr_locale = Locale.objects.create(language_code="fr")
        self.event_page = Page.objects.get(url_path="/home/events/christmas/")
        self.fr_event_page = self.event_page.copy_for_translation(
            self.fr_locale, copy_parents=True
        )
        self.fr_event_page.title = "Noël"
        self.fr_event_page.save(update_fields=["title"])
        self.fr_event_page.save_revision().publish()

    def test_localized_same_language(self):
        self.assertEqual(self.event_page.localized, self.event_page)
        self.assertEqual(self.event_page.localized_draft, self.event_page)

    def test_localized_different_language(self):
        with translation.override("fr"):
            self.assertEqual(self.event_page.localized, self.fr_event_page.page_ptr)
            self.assertEqual(
                self.event_page.localized_draft, self.fr_event_page.page_ptr
            )

    @override_settings(WAGTAIL_I18N_ENABLED=False)
    def test_localized_different_language_with_wagtail_i18n_enabled_false(self):
        """Should return the same page if WAGTAIL_I18N_ENABLED is False"""
        with translation.override("fr"):
            self.assertEqual(self.event_page.localized, self.event_page)
            self.assertEqual(self.event_page.localized_draft, self.event_page)

    def test_localized_different_language_unpublished(self):
        # We shouldn't autolocalize if the translation is unpublished
        self.fr_event_page.unpublish()
        self.fr_event_page.save()

        with translation.override("fr"):
            self.assertEqual(self.event_page.localized, self.event_page)
            self.assertEqual(
                self.event_page.localized_draft, self.fr_event_page.page_ptr
            )

    def test_localized_with_non_content_active_locale(self):
        # if active locale does not have a Locale record, use default locale
        with translation.override("de"):
            self.assertEqual(self.event_page.localized, self.event_page)
            self.assertEqual(self.fr_event_page.localized, self.event_page.specific)
            self.assertEqual(self.event_page.localized_draft, self.event_page)
            self.assertEqual(
                self.fr_event_page.localized_draft, self.event_page.specific
            )

    def test_localized_with_missing_default_locale(self):
        # if neither active locale nor default language code have a Locale record, return self

        # Change the 'en' locale to 'pl', so that no locale record for LANGUAGE_CODE exists.
        # This replicates a scenario where a site was originally built with LANGUAGE_CODE='pl'
        # but subsequently changed to LANGUAGE_CODE='en' (a change which was not reflected in
        # the database).
        en_locale = Locale.objects.get(language_code="en")
        en_locale.language_code = "pl"
        en_locale.save()

        with translation.override("de"):
            self.assertEqual(self.event_page.localized, self.event_page)
            self.assertEqual(self.fr_event_page.localized, self.fr_event_page)
            self.assertEqual(self.event_page.localized_draft, self.event_page)
            self.assertEqual(self.fr_event_page.localized_draft, self.fr_event_page)


class TestGetLock(TestCase):
    fixtures = ["test.json"]

    def test_when_unlocked(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")

        self.assertIsNone(christmas_event.get_lock())

    def test_when_locked(self):
        moderator = get_user_model().objects.get(email="eventmoderator@example.com")

        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.locked = True
        christmas_event.locked_by = moderator
        if settings.USE_TZ:
            christmas_event.locked_at = datetime.datetime(
                2022, 7, 29, 12, 19, 0, tzinfo=datetime.timezone.utc
            )
        else:
            christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)

        lock = christmas_event.get_lock()
        self.assertIsInstance(lock, BasicLock)
        self.assertTrue(lock.for_user(christmas_event.owner))
        self.assertFalse(lock.for_user(moderator))

        if settings.USE_TZ:
            # the default timezone is "Asia/Tokyo", so we expect UTC +9
            expected_date_string = "July 29, 2022, 9:19 p.m."
        else:
            expected_date_string = "July 29, 2022, 12:19 p.m."

        self.assertEqual(
            lock.get_message(christmas_event.owner),
            f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>{expected_date_string}</b>.",
        )
        self.assertEqual(
            lock.get_message(moderator),
            f"<b>'Christmas' was locked</b> by <b>you</b> on <b>{expected_date_string}</b>.",
        )

    def test_when_locked_without_locked_at(self):
        moderator = get_user_model().objects.get(email="eventmoderator@example.com")

        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.locked = True
        christmas_event.locked_by = moderator

        lock = christmas_event.get_lock()
        self.assertEqual(
            lock.get_message(christmas_event.owner),
            "<b>'Christmas' is locked</b>.",
        )
        self.assertEqual(
            lock.get_message(moderator),
            "<b>'Christmas' is locked</b> by <b>you</b>.",
        )

    @override_settings(WAGTAILADMIN_GLOBAL_EDIT_LOCK=True)
    def test_when_locked_globally(self):
        moderator = get_user_model().objects.get(email="eventmoderator@example.com")

        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.locked = True
        christmas_event.locked_by = moderator
        if settings.USE_TZ:
            christmas_event.locked_at = datetime.datetime(
                2022, 7, 29, 12, 19, 0, tzinfo=datetime.timezone.utc
            )
        else:
            christmas_event.locked_at = datetime.datetime(2022, 7, 29, 12, 19, 0)

        lock = christmas_event.get_lock()
        self.assertIsInstance(lock, BasicLock)
        self.assertTrue(lock.for_user(christmas_event.owner))
        self.assertTrue(lock.for_user(moderator))

        if settings.USE_TZ:
            # the default timezone is "Asia/Tokyo", so we expect UTC +9
            expected_date_string = "July 29, 2022, 9:19 p.m."
        else:
            expected_date_string = "July 29, 2022, 12:19 p.m."

        self.assertEqual(
            lock.get_message(christmas_event.owner),
            f"<b>'Christmas' was locked</b> by <b>{str(moderator)}</b> on <b>{expected_date_string}</b>.",
        )
        self.assertEqual(
            lock.get_message(moderator),
            f"<b>'Christmas' was locked</b> by <b>you</b> on <b>{expected_date_string}</b>.",
        )

    def test_when_locked_by_workflow(self):
        moderator = get_user_model().objects.get(email="eventmoderator@example.com")

        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        christmas_event.save_revision()

        workflow = Workflow.objects.create(name="test_workflow")
        task = GroupApprovalTask.objects.create(name="test_task")
        task.groups.add(Group.objects.get(name="Event moderators"))
        WorkflowTask.objects.create(workflow=workflow, task=task, sort_order=1)
        workflow.start(christmas_event, moderator)

        lock = christmas_event.get_lock()
        self.assertIsInstance(lock, WorkflowLock)
        self.assertTrue(lock.for_user(christmas_event.owner))
        self.assertFalse(lock.for_user(moderator))
        self.assertEqual(
            lock.get_message(christmas_event.owner),
            "This page is currently awaiting moderation. Only reviewers for this task can edit the page.",
        )
        self.assertIsNone(lock.get_message(moderator))

        # When visiting a page in a workflow with multiple tasks, the message displayed to users changes to show the current task the page is on

        # Add a second task to the workflow
        other_task = GroupApprovalTask.objects.create(name="another_task")
        WorkflowTask.objects.create(workflow=workflow, task=other_task, sort_order=2)

        lock = christmas_event.get_lock()
        self.assertEqual(
            lock.get_message(christmas_event.owner),
            "This page is awaiting <b>'test_task'</b> in the <b>'test_workflow'</b> workflow. Only reviewers for this task can edit the page.",
        )

    def test_when_scheduled_for_publish(self):
        christmas_event = EventPage.objects.get(url_path="/home/events/christmas/")
        if settings.USE_TZ:
            christmas_event.go_live_at = datetime.datetime(
                2030, 7, 29, 16, 32, 0, tzinfo=datetime.timezone.utc
            )
        else:
            christmas_event.go_live_at = datetime.datetime(2030, 7, 29, 16, 32, 0)
        rvn = christmas_event.save_revision()
        rvn.publish()

        lock = christmas_event.get_lock()
        self.assertIsInstance(lock, ScheduledForPublishLock)
        self.assertTrue(lock.for_user(christmas_event.owner))

        if settings.USE_TZ:
            # the default timezone is "Asia/Tokyo", so we expect UTC +9
            expected_date_string = "July 30, 2030, 1:32 a.m."
        else:
            expected_date_string = "July 29, 2030, 4:32 p.m."

        self.assertEqual(
            lock.get_message(christmas_event.owner),
            f"Page 'Christmas' is locked and has been scheduled to go live at {expected_date_string}",
        )

        # Not even superusers can break this lock
        # This is because it shouldn't be possible to create a separate draft from what is scheduled to be published
        superuser = get_user_model().objects.get(email="superuser@example.com")
        self.assertTrue(lock.for_user(superuser))


class TestPageCacheKey(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.page = Page.objects.last()
        self.other_page = Page.objects.first()

    def test_cache_key_consistent(self):
        self.assertEqual(self.page.cache_key, self.page.cache_key)
        self.assertEqual(self.other_page.cache_key, self.other_page.cache_key)

    def test_no_queries(self):
        with self.assertNumQueries(0):
            self.page.cache_key
            self.other_page.cache_key

    def test_changes_when_slug_changes(self):
        original_cache_key = self.page.cache_key
        self.page.slug = "something-else"
        self.page.save()
        self.assertNotEqual(self.page.cache_key, original_cache_key)


class TestPageCachedParentObjExists(TestCase):
    fixtures = ["test.json"]

    def test_cached_parent_obj_exists(self):
        # https://github.com/wagtail/wagtail/pull/11737

        # Test if _cached_parent_obj is set after using page.get_parent()
        # This is treebeard specific, we don't know if their API will change.
        homepage = Page.objects.get(url_path="/home/")
        homepage._cached_parent_obj = "_cached_parent_obj_exists"
        parent = homepage.get_parent(update=False)
        self.assertEqual(
            parent,
            "_cached_parent_obj_exists",
            "Page.get_parent() (treebeard) no longer uses _cached_parent_obj to cache the parent object",
        )


class TestPageServeWithPasswordRestriction(TestCase, WagtailTestUtils):
    def setUp(self):
        self.root_page = Page.objects.get(id=2)
        self.test_page = Page(
            title="Test Page",
            slug="test",
        )
        self.root_page.add_child(instance=self.test_page)

        self.password_restriction = PageViewRestriction.objects.create(
            page=self.test_page,
            restriction_type=PageViewRestriction.PASSWORD,
            password="password123",
        )

    def test_page_with_password_restriction_authenticated_has_cache_headers(self):
        auth_url = reverse(
            "wagtailcore_authenticate_with_password",
            args=[self.password_restriction.id, self.test_page.id],
        )

        post_response = self.client.post(
            auth_url,
            {
                "password": "password123",
                "return_url": "/test/",
            },
        )

        self.assertRedirects(post_response, "/test/")

        response = self.client.get("/test/")

        self.assertTrue("Cache-Control" in response)
        self.assertIn("max-age=0", response["Cache-Control"])
        self.assertIn("no-cache", response["Cache-Control"])
        self.assertIn("no-store", response["Cache-Control"])
        self.assertIn("must-revalidate", response["Cache-Control"])
        self.assertIn("private", response["Cache-Control"])
        self.assertTrue("Expires" in response)

    def test_page_with_password_restriction_has_cache_headers(self):
        response = self.client.get("/test/")

        self.assertTrue("Cache-Control" in response)
        self.assertIn("max-age=0", response["Cache-Control"])
        self.assertIn("no-cache", response["Cache-Control"])
        self.assertIn("no-store", response["Cache-Control"])
        self.assertIn("must-revalidate", response["Cache-Control"])
        self.assertIn("private", response["Cache-Control"])
        self.assertTrue("Expires" in response)

    def test_page_without_password_restriction_has_no_cache_headers(self):
        self.password_restriction.delete()

        response = self.client.get("/test/")

        self.assertFalse("Cache-Control" in response)
        self.assertFalse("Expires" in response)
