Repository: wagtail/wagtail

Issue:
Flash message "View live" button is broken if a page has no URL
### Issue Summary

Editing pages in the admin that are not under a site - and therefore do not have a live url - result in a broken 'View live' button in the flash message.

### Steps to Reproduce

Create a page using the admin that is *not* under a site root:

1. In the admin, click the "Pages >" menu item, then click the "Pages" header to navigate to the root of the page tree.
2. Click "Add child page" in the header, and create any page here.
3. After the page has been created, a flash message with a "View live" button will be shown. Clicking the "View live" button will navigate to a 404 page.

### Suggested fix

Only show the "View live" button if `page.url is not None`

### Technical details

* Python version: All versions
* Django version: All versions
* Wagtail version: 1.13
* Browser version: All browsers


Discussion:

Resolving patch:
diff --git a/wagtail/wagtailadmin/messages.py b/wagtail/wagtailadmin/messages.py
--- a/wagtail/wagtailadmin/messages.py
+++ b/wagtail/wagtailadmin/messages.py
@@ -62,4 +62,6 @@ def validation_error(request, message, form, buttons=None):
 
 
 def button(url, text, new_window=False):
+    if url is None:
+        raise ValueError("Button URLs must not be None")
     return url, text, new_window
diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py
--- a/wagtail/wagtailadmin/views/pages.py
+++ b/wagtail/wagtailadmin/views/pages.py
@@ -222,10 +222,11 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
                         messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit'))
                     ])
                 else:
-                    messages.success(request, _("Page '{0}' created and published.").format(page.get_admin_display_title()), buttons=[
-                        messages.button(page.url, _('View live'), new_window=True),
-                        messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit'))
-                    ])
+                    buttons = []
+                    if page.url is not None:
+                        buttons.append(messages.button(page.url, _('View live'), new_window=True))
+                    buttons.append(messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')))
+                    messages.success(request, _("Page '{0}' created and published.").format(page.get_admin_display_title()), buttons=buttons)
             elif is_submitting:
                 messages.success(
                     request,
@@ -386,17 +387,11 @@ def edit(request, page_id):
                             page.get_admin_display_title()
                         )
 
-                    messages.success(request, message, buttons=[
-                        messages.button(
-                            page.url,
-                            _('View live'),
-                            new_window=True
-                        ),
-                        messages.button(
-                            reverse('wagtailadmin_pages:edit', args=(page_id,)),
-                            _('Edit')
-                        )
-                    ])
+                    buttons = []
+                    if page.url is not None:
+                        buttons.append(messages.button(page.url, _('View live'), new_window=True))
+                    buttons.append(messages.button(reverse('wagtailadmin_pages:edit', args=(page_id,)), _('Edit')))
+                    messages.success(request, message, buttons=buttons)
 
             elif is_submitting:
 
@@ -905,10 +900,14 @@ def approve_moderation(request, revision_id):
 
     if request.method == 'POST':
         revision.approve_moderation()
-        messages.success(request, _("Page '{0}' published.").format(revision.page.get_admin_display_title()), buttons=[
-            messages.button(revision.page.url, _('View live'), new_window=True),
-            messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit'))
-        ])
+
+        message = _("Page '{0}' published.").format(revision.page.get_admin_display_title())
+        buttons = []
+        if revision.page.url is not None:
+            buttons.append(messages.button(revision.page.url, _('View live'), new_window=True))
+        buttons.append(messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit')))
+        messages.success(request, message, buttons=buttons)
+
         if not send_notification(revision.id, 'approved', request.user.pk):
             messages.error(request, _("Failed to send approval notifications"))
 


Test patch:
diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py
--- a/wagtail/wagtailadmin/tests/test_pages_views.py
+++ b/wagtail/wagtailadmin/tests/test_pages_views.py
@@ -3837,6 +3837,120 @@ def test_page_edit_post_publish_url(self):
             break
 
 
+class TestIssue3982(TestCase, WagtailTestUtils):
+    """
+    Pages that are not associated with a site, and thus do not have a live URL,
+    should not display a "View live" link in the flash message after being
+    edited.
+    """
+
+    def setUp(self):
+        super().setUp()
+        self.login()
+
+    def _create_page(self, parent):
+        response = self.client.post(
+            reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', parent.pk)),
+            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "publish"},
+            follow=True)
+        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(parent.pk,)))
+        page = SimplePage.objects.get()
+        self.assertTrue(page.live)
+        return response, page
+
+    def test_create_accessible(self):
+        """
+        Create a page under the site root, check the flash message has a valid
+        "View live" button.
+        """
+        response, page = self._create_page(Page.objects.get(pk=2))
+        self.assertIsNotNone(page.url)
+        self.assertTrue(any(
+            'View live' in message.message and page.url in message.message
+            for message in response.context['messages']))
+
+    def test_create_inaccessible(self):
+        """
+        Create a page outside of the site root, check the flash message does
+        not have a "View live" button.
+        """
+        response, page = self._create_page(Page.objects.get(pk=1))
+        self.assertIsNone(page.url)
+        self.assertFalse(any(
+            'View live' in message.message
+            for message in response.context['messages']))
+
+    def _edit_page(self, parent):
+        page = parent.add_child(instance=SimplePage(title='Hello, world!', content='Some content'))
+        response = self.client.post(
+            reverse('wagtailadmin_pages:edit', args=(page.pk,)),
+            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "publish"},
+            follow=True)
+        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(parent.pk,)))
+        page = SimplePage.objects.get(pk=page.pk)
+        self.assertTrue(page.live)
+        return response, page
+
+    def test_edit_accessible(self):
+        """
+        Edit a page under the site root, check the flash message has a valid
+        "View live" button.
+        """
+        response, page = self._edit_page(Page.objects.get(pk=2))
+        self.assertIsNotNone(page.url)
+        self.assertTrue(any(
+            'View live' in message.message and page.url in message.message
+            for message in response.context['messages']))
+
+    def test_edit_inaccessible(self):
+        """
+        Edit a page outside of the site root, check the flash message does
+        not have a "View live" button.
+        """
+        response, page = self._edit_page(Page.objects.get(pk=1))
+        self.assertIsNone(page.url)
+        self.assertFalse(any(
+            'View live' in message.message
+            for message in response.context['messages']))
+
+    def _approve_page(self, parent):
+        response = self.client.post(
+            reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', parent.pk)),
+            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world', 'action-submit': "submit"},
+            follow=True)
+        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(parent.pk,)))
+        page = SimplePage.objects.get()
+        self.assertFalse(page.live)
+        revision = PageRevision.objects.get(page=page)
+        response = self.client.post(reverse('wagtailadmin_pages:approve_moderation', args=(revision.pk,)), follow=True)
+        page = SimplePage.objects.get()
+        self.assertTrue(page.live)
+        self.assertRedirects(response, reverse('wagtailadmin_home'))
+        return response, page
+
+    def test_approve_accessible(self):
+        """
+        Edit a page under the site root, check the flash message has a valid
+        "View live" button.
+        """
+        response, page = self._approve_page(Page.objects.get(pk=2))
+        self.assertIsNotNone(page.url)
+        self.assertTrue(any(
+            'View live' in message.message and page.url in message.message
+            for message in response.context['messages']))
+
+    def test_approve_inaccessible(self):
+        """
+        Edit a page outside of the site root, check the flash message does
+        not have a "View live" button.
+        """
+        response, page = self._approve_page(Page.objects.get(pk=1))
+        self.assertIsNone(page.url)
+        self.assertFalse(any(
+            'View live' in message.message
+            for message in response.context['messages']))
+
+
 class TestInlinePanelMedia(TestCase, WagtailTestUtils):
     """
     Test that form media required by InlinePanels is correctly pulled in to the edit page