1import datetime
2import os
3
4from unittest import mock
5
6from django.conf import settings
7from django.contrib.auth.models import Group, Permission
8from django.core import mail
9from django.core.files.base import ContentFile
10from django.http import HttpRequest, HttpResponse
11from django.test import TestCase, modify_settings, override_settings
12from django.urls import reverse
13from django.utils import timezone
14from django.utils.translation import gettext_lazy as _
15
16from wagtail.admin.tests.pages.timestamps import submittable_timestamp
17from wagtail.core.exceptions import PageClassNotFoundError
18from wagtail.core.models import (
19    Comment, CommentReply, GroupPagePermission, Locale, Page, PageLogEntry, PageRevision,
20    PageSubscription, Site)
21from wagtail.core.signals import page_published
22from wagtail.tests.testapp.models import (
23    EVENT_AUDIENCE_CHOICES, Advert, AdvertPlacement, EventCategory, EventPage,
24    EventPageCarouselItem, FilePage, ManyToManyBlogPage, SimplePage, SingleEventPage, StandardIndex,
25    TaggedPage)
26from wagtail.tests.utils import WagtailTestUtils
27from wagtail.tests.utils.form_data import inline_formset, nested_form_data
28from wagtail.users.models import UserProfile
29
30
31class TestPageEdit(TestCase, WagtailTestUtils):
32    def setUp(self):
33        # Find root page
34        self.root_page = Page.objects.get(id=2)
35
36        # Add child page
37        child_page = SimplePage(
38            title="Hello world!",
39            slug="hello-world",
40            content="hello",
41        )
42        self.root_page.add_child(instance=child_page)
43        child_page.save_revision().publish()
44        self.child_page = SimplePage.objects.get(id=child_page.id)
45
46        # Add file page
47        fake_file = ContentFile("File for testing multipart")
48        fake_file.name = 'test.txt'
49        file_page = FilePage(
50            title="File Page",
51            slug="file-page",
52            file_field=fake_file,
53        )
54        self.root_page.add_child(instance=file_page)
55        file_page.save_revision().publish()
56        self.file_page = FilePage.objects.get(id=file_page.id)
57
58        # Add event page (to test edit handlers)
59        self.event_page = EventPage(
60            title="Event page", slug="event-page",
61            location='the moon', audience='public',
62            cost='free', date_from='2001-01-01',
63        )
64        self.root_page.add_child(instance=self.event_page)
65
66        # Add single event page (to test custom URL routes)
67        self.single_event_page = SingleEventPage(
68            title="Mars landing", slug="mars-landing",
69            location='mars', audience='public',
70            cost='free', date_from='2001-01-01',
71        )
72        self.root_page.add_child(instance=self.single_event_page)
73
74        self.unpublished_page = SimplePage(
75            title="Hello unpublished world!",
76            slug="hello-unpublished-world",
77            content="hello",
78            live=False,
79            has_unpublished_changes=True,
80        )
81        self.root_page.add_child(instance=self.unpublished_page)
82
83        # Login
84        self.user = self.login()
85
86    def test_page_edit(self):
87        # Tests that the edit page loads
88        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
89        self.assertEqual(response.status_code, 200)
90        self.assertEqual(response['Content-Type'], "text/html; charset=utf-8")
91        self.assertContains(response, '<li class="header-meta--status">Published</li>', html=True)
92
93        # Test InlinePanel labels/headings
94        self.assertContains(response, '<legend>Speaker lineup</legend>')
95        self.assertContains(response, 'Add speakers')
96
97        # test register_page_action_menu_item hook
98        self.assertContains(response,
99                            '<button type="submit" name="action-panic" value="Panic!" class="button">Panic!</button>')
100        self.assertContains(response, 'testapp/js/siren.js')
101
102        # test construct_page_action_menu hook
103        self.assertContains(response,
104                            '<button type="submit" name="action-relax" value="Relax." class="button">Relax.</button>')
105
106        # test that workflow actions are shown
107        self.assertContains(
108            response, '<button type="submit" name="action-submit" value="Submit to Moderators approval" class="button">'
109        )
110
111    @override_settings(WAGTAIL_WORKFLOW_ENABLED=False)
112    def test_workflow_buttons_not_shown_when_workflow_disabled(self):
113        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
114        self.assertEqual(response.status_code, 200)
115        self.assertNotContains(
116            response, 'value="Submit to Moderators approval"'
117        )
118
119    def test_edit_draft_page_with_no_revisions(self):
120        # Tests that the edit page loads
121        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.unpublished_page.id, )))
122        self.assertEqual(response.status_code, 200)
123        self.assertContains(response, '<li class="header-meta--status">Draft</li>', html=True)
124
125    def test_edit_multipart(self):
126        """
127        Test checks if 'enctype="multipart/form-data"' is added and only to forms that require multipart encoding.
128        """
129        # check for SimplePage where is no file field
130        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
131        self.assertEqual(response.status_code, 200)
132        self.assertNotContains(response, 'enctype="multipart/form-data"')
133        self.assertTemplateUsed(response, 'wagtailadmin/pages/edit.html')
134
135        # check for FilePage which has file field
136        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.file_page.id, )))
137        self.assertEqual(response.status_code, 200)
138        self.assertContains(response, 'enctype="multipart/form-data"')
139
140    @mock.patch('wagtail.core.models.ContentType.model_class', return_value=None)
141    def test_edit_when_specific_class_cannot_be_found(self, mocked_method):
142        with self.assertRaises(PageClassNotFoundError):
143            self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
144
145    def test_upload_file_publish(self):
146        """
147        Check that file uploads work when directly publishing
148        """
149        file_upload = ContentFile(b"A new file", name='published-file.txt')
150        post_data = {
151            'title': 'New file',
152            'slug': 'new-file',
153            'file_field': file_upload,
154            'action-publish': "Publish",
155        }
156        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.file_page.id]), post_data)
157
158        # Should be redirected to explorer
159        self.assertRedirects(response, reverse('wagtailadmin_explore', args=[self.root_page.id]))
160
161        # Check the new file exists
162        file_page = FilePage.objects.get()
163
164        self.assertEqual(file_page.file_field.name, file_upload.name)
165        self.assertTrue(os.path.exists(file_page.file_field.path))
166        self.assertEqual(file_page.file_field.read(), b"A new file")
167
168    def test_upload_file_draft(self):
169        """
170        Check that file uploads work when saving a draft
171        """
172        file_upload = ContentFile(b"A new file", name='draft-file.txt')
173        post_data = {
174            'title': 'New file',
175            'slug': 'new-file',
176            'file_field': file_upload,
177        }
178        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.file_page.id]), post_data)
179
180        # Should be redirected to edit page
181        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.file_page.id]))
182
183        # Check the file was uploaded
184        file_path = os.path.join(settings.MEDIA_ROOT, file_upload.name)
185        self.assertTrue(os.path.exists(file_path))
186        with open(file_path, 'rb') as saved_file:
187            self.assertEqual(saved_file.read(), b"A new file")
188
189        # Publish the draft just created
190        FilePage.objects.get().get_latest_revision().publish()
191
192        # Get the file page, check the file is set
193        file_page = FilePage.objects.get()
194        self.assertEqual(file_page.file_field.name, file_upload.name)
195        self.assertTrue(os.path.exists(file_page.file_field.path))
196        self.assertEqual(file_page.file_field.read(), b"A new file")
197
198    def test_page_edit_bad_permissions(self):
199        # Remove privileges from user
200        self.user.is_superuser = False
201        self.user.user_permissions.add(
202            Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
203        )
204        self.user.save()
205
206        # Get edit page
207        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
208
209        # Check that the user received a 302 redirected response
210        self.assertEqual(response.status_code, 302)
211
212    def test_page_edit_post(self):
213        # Tests simple editing
214        post_data = {
215            'title': "I've been edited!",
216            'content': "Some content",
217            'slug': 'hello-world',
218        }
219        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
220
221        # Should be redirected to edit page
222        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
223
224        # The page should have "has_unpublished_changes" flag set
225        child_page_new = SimplePage.objects.get(id=self.child_page.id)
226        self.assertTrue(child_page_new.has_unpublished_changes)
227
228        # Page fields should not be changed (because we just created a new draft)
229        self.assertEqual(child_page_new.title, self.child_page.title)
230        self.assertEqual(child_page_new.content, self.child_page.content)
231        self.assertEqual(child_page_new.slug, self.child_page.slug)
232
233        # The draft_title should have a new title
234        self.assertEqual(child_page_new.draft_title, post_data['title'])
235
236    def test_page_edit_post_when_locked(self):
237        # Tests that trying to edit a locked page results in an error
238
239        # Lock the page
240        self.child_page.locked = True
241        self.child_page.save()
242
243        # Post
244        post_data = {
245            'title': "I've been edited!",
246            'content': "Some content",
247            'slug': 'hello-world',
248        }
249        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
250
251        # Shouldn't be redirected
252        self.assertContains(response, "The page could not be saved as it is locked")
253
254        # The page shouldn't have "has_unpublished_changes" flag set
255        child_page_new = SimplePage.objects.get(id=self.child_page.id)
256        self.assertFalse(child_page_new.has_unpublished_changes)
257
258    def test_edit_post_scheduled(self):
259        # put go_live_at and expire_at several days away from the current date, to avoid
260        # false matches in content_json__contains tests
261        go_live_at = timezone.now() + datetime.timedelta(days=10)
262        expire_at = timezone.now() + datetime.timedelta(days=20)
263        post_data = {
264            'title': "I've been edited!",
265            'content': "Some content",
266            'slug': 'hello-world',
267            'go_live_at': submittable_timestamp(go_live_at),
268            'expire_at': submittable_timestamp(expire_at),
269        }
270        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
271
272        # Should be redirected to explorer page
273        self.assertEqual(response.status_code, 302)
274
275        child_page_new = SimplePage.objects.get(id=self.child_page.id)
276
277        # The page will still be live
278        self.assertTrue(child_page_new.live)
279
280        # A revision with approved_go_live_at should not exist
281        self.assertFalse(PageRevision.objects.filter(
282            page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
283        )
284
285        # But a revision with go_live_at and expire_at in their content json *should* exist
286        self.assertTrue(PageRevision.objects.filter(
287            page=child_page_new, content_json__contains=str(go_live_at.date())).exists()
288        )
289        self.assertTrue(
290            PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists()
291        )
292
293    def test_edit_scheduled_go_live_before_expiry(self):
294        post_data = {
295            'title': "I've been edited!",
296            'content': "Some content",
297            'slug': 'hello-world',
298            'go_live_at': submittable_timestamp(timezone.now() + datetime.timedelta(days=2)),
299            'expire_at': submittable_timestamp(timezone.now() + datetime.timedelta(days=1)),
300        }
301        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
302
303        self.assertEqual(response.status_code, 200)
304
305        # Check that a form error was raised
306        self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
307        self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
308
309        # form should be marked as having unsaved changes for the purposes of the dirty-forms warning
310        self.assertContains(response, "alwaysDirty: true")
311
312    def test_edit_scheduled_expire_in_the_past(self):
313        post_data = {
314            'title': "I've been edited!",
315            'content': "Some content",
316            'slug': 'hello-world',
317            'expire_at': submittable_timestamp(timezone.now() + datetime.timedelta(days=-1)),
318        }
319        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
320
321        self.assertEqual(response.status_code, 200)
322
323        # Check that a form error was raised
324        self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
325
326        # form should be marked as having unsaved changes for the purposes of the dirty-forms warning
327        self.assertContains(response, "alwaysDirty: true")
328
329    def test_page_edit_post_publish(self):
330        # Connect a mock signal handler to page_published signal
331        mock_handler = mock.MagicMock()
332        page_published.connect(mock_handler)
333
334        # Set has_unpublished_changes=True on the existing record to confirm that the publish action
335        # is resetting it (and not just leaving it alone)
336        self.child_page.has_unpublished_changes = True
337        self.child_page.save()
338
339        # Save current value of first_published_at so we can check that it doesn't change
340        first_published_at = SimplePage.objects.get(id=self.child_page.id).first_published_at
341
342        # Tests publish from edit page
343        post_data = {
344            'title': "I've been edited!",
345            'content': "Some content",
346            'slug': 'hello-world-new',
347            'action-publish': "Publish",
348        }
349        response = self.client.post(
350            reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data, follow=True
351        )
352
353        # Should be redirected to explorer
354        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
355
356        # Check that the page was edited
357        child_page_new = SimplePage.objects.get(id=self.child_page.id)
358        self.assertEqual(child_page_new.title, post_data['title'])
359        self.assertEqual(child_page_new.draft_title, post_data['title'])
360
361        # Check that the page_published signal was fired
362        self.assertEqual(mock_handler.call_count, 1)
363        mock_call = mock_handler.mock_calls[0][2]
364
365        self.assertEqual(mock_call['sender'], child_page_new.specific_class)
366        self.assertEqual(mock_call['instance'], child_page_new)
367        self.assertIsInstance(mock_call['instance'], child_page_new.specific_class)
368
369        # The page shouldn't have "has_unpublished_changes" flag set
370        self.assertFalse(child_page_new.has_unpublished_changes)
371
372        # first_published_at should not change as it was already set
373        self.assertEqual(first_published_at, child_page_new.first_published_at)
374
375        # The "View Live" button should have the updated slug.
376        for message in response.context['messages']:
377            self.assertIn('hello-world-new', message.message)
378            break
379
380    def test_first_published_at_editable(self):
381        """Test that we can update the first_published_at via the Page edit form,
382        for page models that expose it."""
383
384        # Add child page, of a type which has first_published_at in its form
385        child_page = ManyToManyBlogPage(
386            title="Hello world!",
387            slug="hello-again-world",
388            body="hello",
389        )
390        self.root_page.add_child(instance=child_page)
391        child_page.save_revision().publish()
392        self.child_page = ManyToManyBlogPage.objects.get(id=child_page.id)
393
394        initial_delta = self.child_page.first_published_at - timezone.now()
395
396        first_published_at = timezone.now() - datetime.timedelta(days=2)
397
398        post_data = {
399            'title': "I've been edited!",
400            'body': "Some content",
401            'slug': 'hello-again-world',
402            'action-publish': "Publish",
403            'first_published_at': submittable_timestamp(first_published_at),
404            'comments-TOTAL_FORMS': 0,
405            'comments-INITIAL_FORMS': 0,
406            'comments-MIN_NUM_FORMS': 0,
407            'comments-MAX_NUM_FORMS': 1000,
408        }
409        self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
410
411        # Get the edited page.
412        child_page_new = ManyToManyBlogPage.objects.get(id=self.child_page.id)
413
414        # first_published_at should have changed.
415        new_delta = child_page_new.first_published_at - timezone.now()
416        self.assertNotEqual(new_delta.days, initial_delta.days)
417        # first_published_at should be 3 days ago.
418        self.assertEqual(new_delta.days, -3)
419
420    def test_edit_post_publish_scheduled_unpublished_page(self):
421        # Unpublish the page
422        self.child_page.live = False
423        self.child_page.save()
424
425        go_live_at = timezone.now() + datetime.timedelta(days=1)
426        expire_at = timezone.now() + datetime.timedelta(days=2)
427        post_data = {
428            'title': "I've been edited!",
429            'content': "Some content",
430            'slug': 'hello-world',
431            'action-publish': "Publish",
432            'go_live_at': submittable_timestamp(go_live_at),
433            'expire_at': submittable_timestamp(expire_at),
434        }
435        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
436
437        # Should be redirected to explorer page
438        self.assertEqual(response.status_code, 302)
439
440        child_page_new = SimplePage.objects.get(id=self.child_page.id)
441
442        # The page should not be live anymore
443        self.assertFalse(child_page_new.live)
444
445        # Instead a revision with approved_go_live_at should now exist
446        self.assertTrue(
447            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
448        )
449
450        # The page SHOULD have the "has_unpublished_changes" flag set,
451        # because the changes are not visible as a live page yet
452        self.assertTrue(
453            child_page_new.has_unpublished_changes,
454            "A page scheduled for future publishing should have has_unpublished_changes=True"
455        )
456
457        self.assertEqual(child_page_new.status_string, "scheduled")
458
459    def test_edit_post_publish_now_an_already_scheduled_unpublished_page(self):
460        # Unpublish the page
461        self.child_page.live = False
462        self.child_page.save()
463
464        # First let's publish a page with a go_live_at in the future
465        go_live_at = timezone.now() + datetime.timedelta(days=1)
466        expire_at = timezone.now() + datetime.timedelta(days=2)
467        post_data = {
468            'title': "I've been edited!",
469            'content': "Some content",
470            'slug': 'hello-world',
471            'action-publish': "Publish",
472            'go_live_at': submittable_timestamp(go_live_at),
473            'expire_at': submittable_timestamp(expire_at),
474        }
475        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
476
477        # Should be redirected to edit page
478        self.assertEqual(response.status_code, 302)
479
480        child_page_new = SimplePage.objects.get(id=self.child_page.id)
481
482        # The page should not be live
483        self.assertFalse(child_page_new.live)
484
485        self.assertEqual(child_page_new.status_string, "scheduled")
486
487        # Instead a revision with approved_go_live_at should now exist
488        self.assertTrue(
489            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
490        )
491
492        # Now, let's edit it and publish it right now
493        go_live_at = timezone.now()
494        post_data = {
495            'title': "I've been edited!",
496            'content': "Some content",
497            'slug': 'hello-world',
498            'action-publish': "Publish",
499            'go_live_at': "",
500        }
501        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
502
503        # Should be redirected to edit page
504        self.assertEqual(response.status_code, 302)
505
506        child_page_new = SimplePage.objects.get(id=self.child_page.id)
507
508        # The page should be live now
509        self.assertTrue(child_page_new.live)
510
511        # And a revision with approved_go_live_at should not exist
512        self.assertFalse(
513            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
514        )
515
516    def test_edit_post_publish_scheduled_published_page(self):
517        # Page is live
518        self.child_page.live = True
519        self.child_page.save()
520
521        live_revision = self.child_page.live_revision
522        original_title = self.child_page.title
523
524        go_live_at = timezone.now() + datetime.timedelta(days=1)
525        expire_at = timezone.now() + datetime.timedelta(days=2)
526        post_data = {
527            'title': "I've been edited!",
528            'content': "Some content",
529            'slug': 'hello-world',
530            'action-publish': "Publish",
531            'go_live_at': submittable_timestamp(go_live_at),
532            'expire_at': submittable_timestamp(expire_at),
533        }
534        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
535
536        # Should be redirected to explorer page
537        self.assertEqual(response.status_code, 302)
538
539        child_page_new = SimplePage.objects.get(id=self.child_page.id)
540
541        # The page should still be live
542        self.assertTrue(child_page_new.live)
543
544        self.assertEqual(child_page_new.status_string, "live + scheduled")
545
546        # Instead a revision with approved_go_live_at should now exist
547        self.assertTrue(
548            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
549        )
550
551        # The page SHOULD have the "has_unpublished_changes" flag set,
552        # because the changes are not visible as a live page yet
553        self.assertTrue(
554            child_page_new.has_unpublished_changes,
555            "A page scheduled for future publishing should have has_unpublished_changes=True"
556        )
557
558        self.assertNotEqual(
559            child_page_new.get_latest_revision(), live_revision,
560            "A page scheduled for future publishing should have a new revision, that is not the live revision"
561        )
562
563        self.assertEqual(
564            child_page_new.title, original_title,
565            "A live page with scheduled revisions should still have original content"
566        )
567
568    def test_edit_post_publish_now_an_already_scheduled_published_page(self):
569        # Unpublish the page
570        self.child_page.live = True
571        self.child_page.save()
572
573        original_title = self.child_page.title
574        # First let's publish a page with a go_live_at in the future
575        go_live_at = timezone.now() + datetime.timedelta(days=1)
576        expire_at = timezone.now() + datetime.timedelta(days=2)
577        post_data = {
578            'title': "I've been edited!",
579            'content': "Some content",
580            'slug': 'hello-world',
581            'action-publish': "Publish",
582            'go_live_at': submittable_timestamp(go_live_at),
583            'expire_at': submittable_timestamp(expire_at),
584        }
585        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
586
587        # Should be redirected to edit page
588        self.assertEqual(response.status_code, 302)
589
590        child_page_new = SimplePage.objects.get(id=self.child_page.id)
591
592        # The page should still be live
593        self.assertTrue(child_page_new.live)
594
595        # Instead a revision with approved_go_live_at should now exist
596        self.assertTrue(
597            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
598        )
599
600        self.assertEqual(
601            child_page_new.title, original_title,
602            "A live page with scheduled revisions should still have original content"
603        )
604
605        # Now, let's edit it and publish it right now
606        go_live_at = timezone.now()
607        post_data = {
608            'title': "I've been edited!",
609            'content': "Some content",
610            'slug': 'hello-world',
611            'action-publish': "Publish",
612            'go_live_at': "",
613        }
614        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
615
616        # Should be redirected to edit page
617        self.assertEqual(response.status_code, 302)
618
619        child_page_new = SimplePage.objects.get(id=self.child_page.id)
620
621        # The page should be live now
622        self.assertTrue(child_page_new.live)
623
624        # And a revision with approved_go_live_at should not exist
625        self.assertFalse(
626            PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()
627        )
628
629        self.assertEqual(
630            child_page_new.title, post_data['title'],
631            "A published page should have the new title"
632        )
633
634    def test_page_edit_post_submit(self):
635        # Create a moderator user for testing email
636        self.create_superuser('moderator', 'moderator@email.com', 'password')
637
638        # Tests submitting from edit page
639        post_data = {
640            'title': "I've been edited!",
641            'content': "Some content",
642            'slug': 'hello-world',
643            'action-submit': "Submit",
644        }
645        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
646
647        # Should be redirected to explorer
648        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
649
650        # The page should have "has_unpublished_changes" flag set
651        child_page_new = SimplePage.objects.get(id=self.child_page.id)
652        self.assertTrue(child_page_new.has_unpublished_changes)
653
654        # The latest revision for the page should now be in moderation
655        self.assertEqual(child_page_new.current_workflow_state.status, child_page_new.current_workflow_state.STATUS_IN_PROGRESS)
656
657    def test_page_edit_post_existing_slug(self):
658        # This tests the existing slug checking on page edit
659
660        # Create a page
661        self.child_page = SimplePage(title="Hello world 2", slug="hello-world2", content="hello")
662        self.root_page.add_child(instance=self.child_page)
663
664        # Attempt to change the slug to one thats already in use
665        post_data = {
666            'title': "Hello world 2",
667            'slug': 'hello-world',
668            'action-submit': "Submit",
669        }
670        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
671
672        # Should not be redirected (as the save should fail)
673        self.assertEqual(response.status_code, 200)
674
675        # Check that a form error was raised
676        self.assertFormError(response, 'form', 'slug', "This slug is already in use")
677
678    def test_preview_on_edit(self):
679        post_data = {
680            'title': "I've been edited!",
681            'content': "Some content",
682            'slug': 'hello-world',
683            'action-submit': "Submit",
684        }
685        preview_url = reverse('wagtailadmin_pages:preview_on_edit',
686                              args=(self.child_page.id,))
687        response = self.client.post(preview_url, post_data)
688
689        # Check the JSON response
690        self.assertEqual(response.status_code, 200)
691        self.assertJSONEqual(response.content.decode(), {'is_valid': True})
692
693        response = self.client.get(preview_url)
694
695        # Check the HTML response
696        self.assertEqual(response.status_code, 200)
697        self.assertTemplateUsed(response, 'tests/simple_page.html')
698        self.assertContains(response, "I&#39;ve been edited!", html=True)
699
700    def test_preview_on_edit_no_session_key(self):
701        preview_url = reverse('wagtailadmin_pages:preview_on_edit',
702                              args=(self.child_page.id,))
703
704        # get() without corresponding post(), key not set.
705        response = self.client.get(preview_url)
706
707        # Check the HTML response
708        self.assertEqual(response.status_code, 200)
709
710        # We should have an error page because we are unable to
711        # preview; the page key was not in the session.
712        self.assertContains(
713            response,
714            "<title>Wagtail - Preview error</title>",
715            html=True
716        )
717        self.assertContains(
718            response,
719            "<h1>Preview error</h1>",
720            html=True
721        )
722
723    @override_settings(CACHES={
724        'default': {
725            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
726        }})
727    @modify_settings(MIDDLEWARE={
728        'append': 'django.middleware.cache.FetchFromCacheMiddleware',
729        'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
730    })
731    def test_preview_does_not_cache(self):
732        '''
733        Tests solution to issue #5975
734        '''
735        post_data = {
736            'title': "I've been edited one time!",
737            'content': "Some content",
738            'slug': 'hello-world',
739            'action-submit': "Submit",
740        }
741        preview_url = reverse('wagtailadmin_pages:preview_on_edit',
742                              args=(self.child_page.id,))
743        self.client.post(preview_url, post_data)
744        response = self.client.get(preview_url)
745        self.assertContains(response, "I&#39;ve been edited one time!", html=True)
746
747        post_data['title'] = "I've been edited two times!"
748        self.client.post(preview_url, post_data)
749        response = self.client.get(preview_url)
750        self.assertContains(response, "I&#39;ve been edited two times!", html=True)
751
752    @modify_settings(ALLOWED_HOSTS={'append': 'childpage.example.com'})
753    def test_preview_uses_correct_site(self):
754        # create a Site record for the child page
755        Site.objects.create(hostname='childpage.example.com', root_page=self.child_page)
756
757        post_data = {
758            'title': "I've been edited!",
759            'content': "Some content",
760            'slug': 'hello-world',
761            'action-submit': "Submit",
762        }
763        preview_url = reverse('wagtailadmin_pages:preview_on_edit',
764                              args=(self.child_page.id,))
765        response = self.client.post(preview_url, post_data)
766
767        # Check the JSON response
768        self.assertEqual(response.status_code, 200)
769        self.assertJSONEqual(response.content.decode(), {'is_valid': True})
770
771        response = self.client.get(preview_url)
772
773        # Check that the correct site object has been selected by the site middleware
774        self.assertEqual(response.status_code, 200)
775        self.assertTemplateUsed(response, 'tests/simple_page.html')
776        self.assertEqual(Site.find_for_request(response.context['request']).hostname, 'childpage.example.com')
777
778    def test_editor_picks_up_direct_model_edits(self):
779        # If a page has no draft edits, the editor should show the version from the live database
780        # record rather than the latest revision record. This ensures that the edit interface
781        # reflects any changes made directly on the model.
782        self.child_page.title = "This title only exists on the live database record"
783        self.child_page.save()
784
785        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
786        self.assertEqual(response.status_code, 200)
787        self.assertContains(response, "This title only exists on the live database record")
788
789    def test_editor_does_not_pick_up_direct_model_edits_when_draft_edits_exist(self):
790        # If a page has draft edits, we should always show those in the editor, not the live
791        # database record
792        self.child_page.content = "Some content with a draft edit"
793        self.child_page.save_revision()
794
795        # make an independent change to the live database record
796        self.child_page = SimplePage.objects.get(id=self.child_page.id)
797        self.child_page.title = "This title only exists on the live database record"
798        self.child_page.save()
799
800        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
801        self.assertEqual(response.status_code, 200)
802        self.assertNotContains(response, "This title only exists on the live database record")
803        self.assertContains(response, "Some content with a draft edit")
804
805    def test_editor_page_shows_live_url_in_status_when_draft_edits_exist(self):
806        # If a page has draft edits (ie. page has unpublished changes)
807        # that affect the URL (eg. slug) we  should still ensure the
808        # status button at the top of the page links to the live URL
809
810        self.child_page.content = "Some content with a draft edit"
811        self.child_page.slug = "revised-slug-in-draft-only"  # live version contains 'hello-world'
812        self.child_page.save_revision()
813
814        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
815
816        link_to_live = '<a href="/hello-world/" target="_blank" rel="noopener noreferrer" class="button button-nostroke button--live" title="Visit the live page">\n' \
817                       '<svg class="icon icon-link-external initial" aria-hidden="true" focusable="false"><use href="#icon-link-external"></use></svg>\n\n        ' \
818                       'Live\n        <span class="privacy-indicator-tag u-hidden" aria-hidden="true" title="This page is live but only available to certain users">(restricted)</span>'
819        input_field_for_draft_slug = '<input type="text" name="slug" value="revised-slug-in-draft-only" id="id_slug" maxlength="255" required />'
820        input_field_for_live_slug = '<input type="text" name="slug" value="hello-world" id="id_slug" maxlength="255" required />'
821
822        # Status Link should be the live page (not revision)
823        self.assertContains(response, link_to_live, html=True)
824        self.assertNotContains(response, 'href="/revised-slug-in-draft-only/"', html=True)
825
826        # Editing input for slug should be the draft revision
827        self.assertContains(response, input_field_for_draft_slug, html=True)
828        self.assertNotContains(response, input_field_for_live_slug, html=True)
829
830    def test_editor_page_shows_custom_live_url_in_status_when_draft_edits_exist(self):
831        # When showing a live URL in the status button that differs from the draft one,
832        # ensure that we pick up any custom URL logic defined on the specific page model
833
834        self.single_event_page.location = "The other side of Mars"
835        self.single_event_page.slug = "revised-slug-in-draft-only"  # live version contains 'hello-world'
836        self.single_event_page.save_revision()
837
838        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.single_event_page.id, )))
839
840        link_to_live = '<a href="/mars-landing/pointless-suffix/" target="_blank" rel="noopener noreferrer" class="button button-nostroke button--live" title="Visit the live page">\n' \
841                       '<svg class="icon icon-link-external initial" aria-hidden="true" focusable="false"><use href="#icon-link-external"></use></svg>\n\n        ' \
842                       'Live\n        <span class="privacy-indicator-tag u-hidden" aria-hidden="true" title="This page is live but only available to certain users">(restricted)</span>'
843        input_field_for_draft_slug = '<input type="text" name="slug" value="revised-slug-in-draft-only" id="id_slug" maxlength="255" required />'
844        input_field_for_live_slug = '<input type="text" name="slug" value="mars-landing" id="id_slug" maxlength="255" required />'
845
846        # Status Link should be the live page (not revision)
847        self.assertContains(response, link_to_live, html=True)
848        self.assertNotContains(response, 'href="/revised-slug-in-draft-only/pointless-suffix/"', html=True)
849
850        # Editing input for slug should be the draft revision
851        self.assertContains(response, input_field_for_draft_slug, html=True)
852        self.assertNotContains(response, input_field_for_live_slug, html=True)
853
854    def test_before_edit_page_hook(self):
855        def hook_func(request, page):
856            self.assertIsInstance(request, HttpRequest)
857            self.assertEqual(page.id, self.child_page.id)
858
859            return HttpResponse("Overridden!")
860
861        with self.register_hook('before_edit_page', hook_func):
862            response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
863
864        self.assertEqual(response.status_code, 200)
865        self.assertEqual(response.content, b"Overridden!")
866
867    def test_before_edit_page_hook_post(self):
868        def hook_func(request, page):
869            self.assertIsInstance(request, HttpRequest)
870            self.assertEqual(page.id, self.child_page.id)
871
872            return HttpResponse("Overridden!")
873
874        with self.register_hook('before_edit_page', hook_func):
875            post_data = {
876                'title': "I've been edited!",
877                'content': "Some content",
878                'slug': 'hello-world-new',
879                'action-publish': "Publish",
880            }
881            response = self.client.post(
882                reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data
883            )
884
885        self.assertEqual(response.status_code, 200)
886        self.assertEqual(response.content, b"Overridden!")
887
888        # page should not be edited
889        self.assertEqual(Page.objects.get(id=self.child_page.id).title, "Hello world!")
890
891    def test_after_edit_page_hook(self):
892        def hook_func(request, page):
893            self.assertIsInstance(request, HttpRequest)
894            self.assertEqual(page.id, self.child_page.id)
895
896            return HttpResponse("Overridden!")
897
898        with self.register_hook('after_edit_page', hook_func):
899            post_data = {
900                'title': "I've been edited!",
901                'content': "Some content",
902                'slug': 'hello-world-new',
903                'action-publish': "Publish",
904            }
905            response = self.client.post(
906                reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data
907            )
908
909        self.assertEqual(response.status_code, 200)
910        self.assertEqual(response.content, b"Overridden!")
911
912        # page should be edited
913        self.assertEqual(Page.objects.get(id=self.child_page.id).title, "I've been edited!")
914
915    def test_after_publish_page(self):
916        def hook_func(request, page):
917            self.assertIsInstance(request, HttpRequest)
918            self.assertEqual(page.id, self.child_page.id)
919
920            return HttpResponse("Overridden!")
921
922        with self.register_hook("after_publish_page", hook_func):
923            post_data = {
924                'title': "I've been edited!",
925                'content': "Some content",
926                'slug': 'hello-world-new',
927                'action-publish': "Publish",
928            }
929            response = self.client.post(
930                reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data
931            )
932
933        self.assertEqual(response.status_code, 200)
934        self.assertEqual(response.content, b"Overridden!")
935        self.child_page.refresh_from_db()
936        self.assertEqual(self.child_page.status_string, _("live"))
937
938    def test_before_publish_page(self):
939        def hook_func(request, page):
940            self.assertIsInstance(request, HttpRequest)
941            self.assertEqual(page.id, self.child_page.id)
942
943            return HttpResponse("Overridden!")
944
945        with self.register_hook("before_publish_page", hook_func):
946            post_data = {
947                'title': "I've been edited!",
948                'content': "Some content",
949                'slug': 'hello-world-new',
950                'action-publish': "Publish",
951            }
952            response = self.client.post(
953                reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data
954            )
955
956        self.assertEqual(response.status_code, 200)
957        self.assertEqual(response.content, b"Overridden!")
958        self.child_page.refresh_from_db()
959        self.assertEqual(self.child_page.status_string, _("live + draft"))
960
961    def test_override_default_action_menu_item(self):
962        def hook_func(menu_items, request, context):
963            for (index, item) in enumerate(menu_items):
964                if item.name == 'action-publish':
965                    # move to top of list
966                    menu_items.pop(index)
967                    menu_items.insert(0, item)
968                    break
969
970        with self.register_hook('construct_page_action_menu', hook_func):
971            response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.single_event_page.id, )))
972
973        publish_button = '''
974            <button type="submit" name="action-publish" value="action-publish" class="button button-longrunning " data-clicked-text="Publishing…">
975                <svg class="icon icon-upload button-longrunning__icon" aria-hidden="true" focusable="false"><use href="#icon-upload"></use></svg>
976
977                <svg class="icon icon-spinner icon" aria-hidden="true" focusable="false"><use href="#icon-spinner"></use></svg><em>Publish</em>
978            </button>
979        '''
980        save_button = '''
981            <button type="submit" class="button action-save button-longrunning " data-clicked-text="Saving…" >
982                <svg class="icon icon-draft button-longrunning__icon" aria-hidden="true" focusable="false"><use href="#icon-draft"></use></svg>
983
984                <svg class="icon icon-spinner icon" aria-hidden="true" focusable="false"><use href="#icon-spinner"></use></svg>
985                <em>Save draft</em>
986            </button>
987        '''
988
989        # save button should be in a <li>
990        self.assertContains(response, "<li>%s</li>" % save_button, html=True)
991
992        # publish button should be present, but not in a <li>
993        self.assertContains(response, publish_button, html=True)
994        self.assertNotContains(response, "<li>%s</li>" % publish_button, html=True)
995
996    def test_edit_alias_page(self):
997        alias_page = self.event_page.create_alias(update_slug='new-event-page')
998        response = self.client.get(reverse('wagtailadmin_pages:edit', args=[alias_page.id]))
999
1000        self.assertEqual(response.status_code, 200)
1001        self.assertEqual(response['Content-Type'], "text/html; charset=utf-8")
1002
1003        # Should still have status in the header
1004        self.assertContains(response, '<li class="header-meta--status">Published</li>', html=True)
1005
1006        # Check the edit_alias.html template was used instead
1007        self.assertTemplateUsed(response, 'wagtailadmin/pages/edit_alias.html')
1008        original_page_edit_url = reverse('wagtailadmin_pages:edit', args=[self.event_page.id])
1009        self.assertContains(response, f'<a class="button button-secondary" href="{original_page_edit_url}">Edit original page</a>', html=True)
1010
1011    def test_post_edit_alias_page(self):
1012        alias_page = self.child_page.create_alias(update_slug='new-child-page')
1013
1014        # Tests simple editing
1015        post_data = {
1016            'title': "I've been edited!",
1017            'content': "Some content",
1018            'slug': 'hello-world',
1019        }
1020        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[alias_page.id]), post_data)
1021
1022        self.assertEqual(response.status_code, 405)
1023
1024    def test_edit_after_change_language_code(self):
1025        """
1026        Verify that changing LANGUAGE_CODE with no corresponding database change does not break editing
1027        """
1028        # Add a draft revision
1029        self.child_page.title = "Hello world updated"
1030        self.child_page.save_revision()
1031
1032        # Hack the Locale model to simulate a page tree that was created with LANGUAGE_CODE = 'de'
1033        # (which is not a valid content language under the current configuration)
1034        Locale.objects.update(language_code='de')
1035
1036        # Tests that the edit page loads
1037        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
1038        self.assertEqual(response.status_code, 200)
1039
1040        # Tests simple editing
1041        post_data = {
1042            'title': "I've been edited!",
1043            'content': "Some content",
1044            'slug': 'hello-world',
1045        }
1046        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
1047
1048        # Should be redirected to edit page
1049        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
1050
1051    def test_edit_after_change_language_code_without_revisions(self):
1052        """
1053        Verify that changing LANGUAGE_CODE with no corresponding database change does not break editing
1054        """
1055        # Hack the Locale model to simulate a page tree that was created with LANGUAGE_CODE = 'de'
1056        # (which is not a valid content language under the current configuration)
1057        Locale.objects.update(language_code='de')
1058
1059        PageRevision.objects.filter(page_id=self.child_page.id).delete()
1060
1061        # Tests that the edit page loads
1062        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
1063        self.assertEqual(response.status_code, 200)
1064
1065        # Tests simple editing
1066        post_data = {
1067            'title': "I've been edited!",
1068            'content': "Some content",
1069            'slug': 'hello-world',
1070        }
1071        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )), post_data)
1072
1073        # Should be redirected to edit page
1074        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )))
1075
1076
1077class TestPageEditReordering(TestCase, WagtailTestUtils):
1078    def setUp(self):
1079        # Find root page
1080        self.root_page = Page.objects.get(id=2)
1081
1082        # Add event page
1083        self.event_page = EventPage(
1084            title="Event page", slug="event-page",
1085            location='the moon', audience='public',
1086            cost='free', date_from='2001-01-01',
1087        )
1088        self.event_page.carousel_items = [
1089            EventPageCarouselItem(caption='1234567', sort_order=1),
1090            EventPageCarouselItem(caption='7654321', sort_order=2),
1091            EventPageCarouselItem(caption='abcdefg', sort_order=3),
1092        ]
1093        self.root_page.add_child(instance=self.event_page)
1094
1095        # Login
1096        self.user = self.login()
1097
1098    def check_order(self, response, expected_order):
1099        inline_panel = response.context['edit_handler'].children[0].children[9]
1100        order = [child.form.instance.caption for child in inline_panel.children]
1101        self.assertEqual(order, expected_order)
1102
1103    def test_order(self):
1104        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
1105
1106        self.assertEqual(response.status_code, 200)
1107        self.check_order(response, ['1234567', '7654321', 'abcdefg'])
1108
1109    def test_reorder(self):
1110        post_data = {
1111            'title': "Event page",
1112            'slug': 'event-page',
1113
1114            'date_from': '01/01/2014',
1115            'cost': '$10',
1116            'audience': 'public',
1117            'location': 'somewhere',
1118
1119            'related_links-INITIAL_FORMS': 0,
1120            'related_links-MAX_NUM_FORMS': 1000,
1121            'related_links-TOTAL_FORMS': 0,
1122
1123            'speakers-INITIAL_FORMS': 0,
1124            'speakers-MAX_NUM_FORMS': 1000,
1125            'speakers-TOTAL_FORMS': 0,
1126
1127            'head_counts-INITIAL_FORMS': 0,
1128            'head_counts-MAX_NUM_FORMS': 1000,
1129            'head_counts-TOTAL_FORMS': 0,
1130
1131            'carousel_items-INITIAL_FORMS': 3,
1132            'carousel_items-MAX_NUM_FORMS': 1000,
1133            'carousel_items-TOTAL_FORMS': 3,
1134            'carousel_items-0-id': self.event_page.carousel_items.all()[0].id,
1135            'carousel_items-0-caption': self.event_page.carousel_items.all()[0].caption,
1136            'carousel_items-0-ORDER': 2,
1137            'carousel_items-1-id': self.event_page.carousel_items.all()[1].id,
1138            'carousel_items-1-caption': self.event_page.carousel_items.all()[1].caption,
1139            'carousel_items-1-ORDER': 3,
1140            'carousel_items-2-id': self.event_page.carousel_items.all()[2].id,
1141            'carousel_items-2-caption': self.event_page.carousel_items.all()[2].caption,
1142            'carousel_items-2-ORDER': 1,
1143        }
1144        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )), post_data)
1145
1146        # Should be redirected back to same page
1147        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
1148
1149        # Check order
1150        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )))
1151
1152        self.assertEqual(response.status_code, 200)
1153        self.check_order(response, ['abcdefg', '1234567', '7654321'])
1154
1155    def test_reorder_with_validation_error(self):
1156        post_data = {
1157            'title': "",  # Validation error
1158            'slug': 'event-page',
1159
1160            'date_from': '01/01/2014',
1161            'cost': '$10',
1162            'audience': 'public',
1163            'location': 'somewhere',
1164
1165            'related_links-INITIAL_FORMS': 0,
1166            'related_links-MAX_NUM_FORMS': 1000,
1167            'related_links-TOTAL_FORMS': 0,
1168
1169            'speakers-INITIAL_FORMS': 0,
1170            'speakers-MAX_NUM_FORMS': 1000,
1171            'speakers-TOTAL_FORMS': 0,
1172
1173            'head_counts-INITIAL_FORMS': 0,
1174            'head_counts-MAX_NUM_FORMS': 1000,
1175            'head_counts-TOTAL_FORMS': 0,
1176
1177            'carousel_items-INITIAL_FORMS': 3,
1178            'carousel_items-MAX_NUM_FORMS': 1000,
1179            'carousel_items-TOTAL_FORMS': 3,
1180            'carousel_items-0-id': self.event_page.carousel_items.all()[0].id,
1181            'carousel_items-0-caption': self.event_page.carousel_items.all()[0].caption,
1182            'carousel_items-0-ORDER': 2,
1183            'carousel_items-1-id': self.event_page.carousel_items.all()[1].id,
1184            'carousel_items-1-caption': self.event_page.carousel_items.all()[1].caption,
1185            'carousel_items-1-ORDER': 3,
1186            'carousel_items-2-id': self.event_page.carousel_items.all()[2].id,
1187            'carousel_items-2-caption': self.event_page.carousel_items.all()[2].caption,
1188            'carousel_items-2-ORDER': 1,
1189        }
1190        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.event_page.id, )), post_data)
1191
1192        self.assertEqual(response.status_code, 200)
1193        self.check_order(response, ['abcdefg', '1234567', '7654321'])
1194
1195
1196class TestIssue197(TestCase, WagtailTestUtils):
1197    def test_issue_197(self):
1198        # Find root page
1199        self.root_page = Page.objects.get(id=2)
1200
1201        # Create a tagged page with no tags
1202        self.tagged_page = self.root_page.add_child(instance=TaggedPage(
1203            title="Tagged page",
1204            slug='tagged-page',
1205            live=False,
1206        ))
1207
1208        # Login
1209        self.user = self.login()
1210
1211        # Add some tags and publish using edit view
1212        post_data = {
1213            'title': "Tagged page",
1214            'slug': 'tagged-page',
1215            'tags': "hello, world",
1216            'action-publish': "Publish",
1217        }
1218        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.tagged_page.id, )), post_data)
1219
1220        # Should be redirected to explorer
1221        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
1222
1223        # Check that both tags are in the pages tag set
1224        page = TaggedPage.objects.get(id=self.tagged_page.id)
1225        self.assertIn('hello', page.tags.slugs())
1226        self.assertIn('world', page.tags.slugs())
1227
1228
1229class TestChildRelationsOnSuperclass(TestCase, WagtailTestUtils):
1230    # In our test models we define AdvertPlacement as a child relation on the Page model.
1231    # Here we check that this behaves correctly when exposed on the edit form of a Page
1232    # subclass (StandardIndex here).
1233    fixtures = ['test.json']
1234
1235    def setUp(self):
1236        # Find root page
1237        self.root_page = Page.objects.get(id=2)
1238        self.test_advert = Advert.objects.get(id=1)
1239
1240        # Add child page
1241        self.index_page = StandardIndex(
1242            title="My lovely index",
1243            slug="my-lovely-index",
1244            advert_placements=[AdvertPlacement(advert=self.test_advert)]
1245        )
1246        self.root_page.add_child(instance=self.index_page)
1247
1248        # Login
1249        self.login()
1250
1251    def test_get_create_form(self):
1252        response = self.client.get(
1253            reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id))
1254        )
1255        self.assertEqual(response.status_code, 200)
1256        # Response should include an advert_placements formset labelled Adverts
1257        self.assertContains(response, "Adverts")
1258        self.assertContains(response, "id_advert_placements-TOTAL_FORMS")
1259
1260    def test_post_create_form(self):
1261        post_data = {
1262            'title': "New index!",
1263            'slug': 'new-index',
1264            'advert_placements-TOTAL_FORMS': '1',
1265            'advert_placements-INITIAL_FORMS': '0',
1266            'advert_placements-MAX_NUM_FORMS': '1000',
1267            'advert_placements-0-advert': '1',
1268            'advert_placements-0-colour': 'yellow',
1269            'advert_placements-0-id': '',
1270        }
1271        response = self.client.post(
1272            reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id)), post_data
1273        )
1274
1275        # Find the page and check it
1276        page = Page.objects.get(path__startswith=self.root_page.path, slug='new-index').specific
1277
1278        # Should be redirected to edit page
1279        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(page.id, )))
1280
1281        self.assertEqual(page.advert_placements.count(), 1)
1282        self.assertEqual(page.advert_placements.first().advert.text, 'test_advert')
1283
1284    def test_post_create_form_with_validation_error_in_formset(self):
1285        post_data = {
1286            'title': "New index!",
1287            'slug': 'new-index',
1288            'advert_placements-TOTAL_FORMS': '1',
1289            'advert_placements-INITIAL_FORMS': '0',
1290            'advert_placements-MAX_NUM_FORMS': '1000',
1291            'advert_placements-0-advert': '1',
1292            'advert_placements-0-colour': '',  # should fail as colour is a required field
1293            'advert_placements-0-id': '',
1294        }
1295        response = self.client.post(
1296            reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id)), post_data
1297        )
1298
1299        # Should remain on the edit page with a validation error
1300        self.assertEqual(response.status_code, 200)
1301        self.assertContains(response, "This field is required.")
1302        # form should be marked as having unsaved changes
1303        self.assertContains(response, "alwaysDirty: true")
1304
1305    def test_get_edit_form(self):
1306        response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.index_page.id, )))
1307        self.assertEqual(response.status_code, 200)
1308
1309        # Response should include an advert_placements formset labelled Adverts
1310        self.assertContains(response, "Adverts")
1311        self.assertContains(response, "id_advert_placements-TOTAL_FORMS")
1312        # the formset should be populated with an existing form
1313        self.assertContains(response, "id_advert_placements-0-advert")
1314        self.assertContains(
1315            response, '<option value="1" selected="selected">test_advert</option>', html=True
1316        )
1317
1318    def test_post_edit_form(self):
1319        post_data = {
1320            'title': "My lovely index",
1321            'slug': 'my-lovely-index',
1322            'advert_placements-TOTAL_FORMS': '2',
1323            'advert_placements-INITIAL_FORMS': '1',
1324            'advert_placements-MAX_NUM_FORMS': '1000',
1325            'advert_placements-0-advert': '1',
1326            'advert_placements-0-colour': 'yellow',
1327            'advert_placements-0-id': self.index_page.advert_placements.first().id,
1328            'advert_placements-1-advert': '1',
1329            'advert_placements-1-colour': 'purple',
1330            'advert_placements-1-id': '',
1331            'action-publish': "Publish",
1332        }
1333        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.index_page.id, )), post_data)
1334
1335        # Should be redirected to explorer
1336        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.root_page.id, )))
1337
1338        # Find the page and check it
1339        page = Page.objects.get(id=self.index_page.id).specific
1340        self.assertEqual(page.advert_placements.count(), 2)
1341        self.assertEqual(page.advert_placements.all()[0].advert.text, 'test_advert')
1342        self.assertEqual(page.advert_placements.all()[1].advert.text, 'test_advert')
1343
1344    def test_post_edit_form_with_validation_error_in_formset(self):
1345        post_data = {
1346            'title': "My lovely index",
1347            'slug': 'my-lovely-index',
1348            'advert_placements-TOTAL_FORMS': '1',
1349            'advert_placements-INITIAL_FORMS': '1',
1350            'advert_placements-MAX_NUM_FORMS': '1000',
1351            'advert_placements-0-advert': '1',
1352            'advert_placements-0-colour': '',
1353            'advert_placements-0-id': self.index_page.advert_placements.first().id,
1354            'action-publish': "Publish",
1355        }
1356        response = self.client.post(reverse('wagtailadmin_pages:edit', args=(self.index_page.id, )), post_data)
1357
1358        # Should remain on the edit page with a validation error
1359        self.assertEqual(response.status_code, 200)
1360        self.assertContains(response, "This field is required.")
1361        # form should be marked as having unsaved changes
1362        self.assertContains(response, "alwaysDirty: true")
1363
1364
1365class TestIssue2492(TestCase, WagtailTestUtils):
1366    """
1367    The publication submission message generation was performed using
1368    the Page class, as opposed to the specific_class for that Page.
1369    This test ensures that the specific_class url method is called
1370    when the 'view live' message button is created.
1371    """
1372
1373    def setUp(self):
1374        self.root_page = Page.objects.get(id=2)
1375        child_page = SingleEventPage(
1376            title="Test Event", slug="test-event", location="test location",
1377            cost="10", date_from=datetime.datetime.now(),
1378            audience=EVENT_AUDIENCE_CHOICES[0][0])
1379        self.root_page.add_child(instance=child_page)
1380        child_page.save_revision().publish()
1381        self.child_page = SingleEventPage.objects.get(id=child_page.id)
1382        self.user = self.login()
1383
1384    def test_page_edit_post_publish_url(self):
1385        post_data = {
1386            'action-publish': "Publish",
1387            'title': self.child_page.title,
1388            'date_from': self.child_page.date_from,
1389            'slug': self.child_page.slug,
1390            'audience': self.child_page.audience,
1391            'location': self.child_page.location,
1392            'cost': self.child_page.cost,
1393            'carousel_items-TOTAL_FORMS': 0,
1394            'carousel_items-INITIAL_FORMS': 0,
1395            'carousel_items-MIN_NUM_FORMS': 0,
1396            'carousel_items-MAX_NUM_FORMS': 0,
1397            'speakers-TOTAL_FORMS': 0,
1398            'speakers-INITIAL_FORMS': 0,
1399            'speakers-MIN_NUM_FORMS': 0,
1400            'speakers-MAX_NUM_FORMS': 0,
1401            'related_links-TOTAL_FORMS': 0,
1402            'related_links-INITIAL_FORMS': 0,
1403            'related_links-MIN_NUM_FORMS': 0,
1404            'related_links-MAX_NUM_FORMS': 0,
1405            'head_counts-TOTAL_FORMS': 0,
1406            'head_counts-INITIAL_FORMS': 0,
1407            'head_counts-MIN_NUM_FORMS': 0,
1408            'head_counts-MAX_NUM_FORMS': 0,
1409        }
1410        response = self.client.post(
1411            reverse('wagtailadmin_pages:edit', args=(self.child_page.id, )),
1412            post_data, follow=True)
1413
1414        # Grab a fresh copy's URL
1415        new_url = SingleEventPage.objects.get(id=self.child_page.id).url
1416
1417        # The "View Live" button should have the custom URL.
1418        for message in response.context['messages']:
1419            self.assertIn('"{}"'.format(new_url), message.message)
1420            break
1421
1422
1423class TestIssue3982(TestCase, WagtailTestUtils):
1424    """
1425    Pages that are not associated with a site, and thus do not have a live URL,
1426    should not display a "View live" link in the flash message after being
1427    edited.
1428    """
1429
1430    def setUp(self):
1431        super().setUp()
1432        self.login()
1433
1434    def _create_page(self, parent):
1435        response = self.client.post(
1436            reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', parent.pk)),
1437            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "publish"},
1438            follow=True)
1439        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(parent.pk,)))
1440        page = SimplePage.objects.get()
1441        self.assertTrue(page.live)
1442        return response, page
1443
1444    def test_create_accessible(self):
1445        """
1446        Create a page under the site root, check the flash message has a valid
1447        "View live" button.
1448        """
1449        response, page = self._create_page(Page.objects.get(pk=2))
1450        self.assertIsNotNone(page.url)
1451        self.assertTrue(any(
1452            'View live' in message.message and page.url in message.message
1453            for message in response.context['messages']))
1454
1455    def test_create_inaccessible(self):
1456        """
1457        Create a page outside of the site root, check the flash message does
1458        not have a "View live" button.
1459        """
1460        response, page = self._create_page(Page.objects.get(pk=1))
1461        self.assertIsNone(page.url)
1462        self.assertFalse(any(
1463            'View live' in message.message
1464            for message in response.context['messages']))
1465
1466    def _edit_page(self, parent):
1467        page = parent.add_child(instance=SimplePage(title='Hello, world!', content='Some content'))
1468        response = self.client.post(
1469            reverse('wagtailadmin_pages:edit', args=(page.pk,)),
1470            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "publish"},
1471            follow=True)
1472        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(parent.pk,)))
1473        page = SimplePage.objects.get(pk=page.pk)
1474        self.assertTrue(page.live)
1475        return response, page
1476
1477    def test_edit_accessible(self):
1478        """
1479        Edit a page under the site root, check the flash message has a valid
1480        "View live" button.
1481        """
1482        response, page = self._edit_page(Page.objects.get(pk=2))
1483        self.assertIsNotNone(page.url)
1484        self.assertTrue(any(
1485            'View live' in message.message and page.url in message.message
1486            for message in response.context['messages']))
1487
1488    def test_edit_inaccessible(self):
1489        """
1490        Edit a page outside of the site root, check the flash message does
1491        not have a "View live" button.
1492        """
1493        response, page = self._edit_page(Page.objects.get(pk=1))
1494        self.assertIsNone(page.url)
1495        self.assertFalse(any(
1496            'View live' in message.message
1497            for message in response.context['messages']))
1498
1499    def _approve_page(self, parent):
1500        response = self.client.post(
1501            reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', parent.pk)),
1502            {'title': "Hello, world!", 'content': "Some content", 'slug': 'hello-world'},
1503            follow=True)
1504        page = SimplePage.objects.get()
1505        self.assertFalse(page.live)
1506        revision = PageRevision.objects.get(page=page)
1507        revision.submitted_for_moderation = True
1508        revision.save()
1509        response = self.client.post(reverse('wagtailadmin_pages:approve_moderation', args=(revision.pk,)), follow=True)
1510        page = SimplePage.objects.get()
1511        self.assertTrue(page.live)
1512        self.assertRedirects(response, reverse('wagtailadmin_home'))
1513        return response, page
1514
1515    def test_approve_accessible(self):
1516        """
1517        Edit a page under the site root, check the flash message has a valid
1518        "View live" button.
1519        """
1520        response, page = self._approve_page(Page.objects.get(pk=2))
1521        self.assertIsNotNone(page.url)
1522        self.assertTrue(any(
1523            'View live' in message.message and page.url in message.message
1524            for message in response.context['messages']))
1525
1526    def test_approve_inaccessible(self):
1527        """
1528        Edit a page outside of the site root, check the flash message does
1529        not have a "View live" button.
1530        """
1531        response, page = self._approve_page(Page.objects.get(pk=1))
1532        self.assertIsNone(page.url)
1533        self.assertFalse(any(
1534            'View live' in message.message
1535            for message in response.context['messages']))
1536
1537
1538class TestParentalM2M(TestCase, WagtailTestUtils):
1539    fixtures = ['test.json']
1540
1541    def setUp(self):
1542        self.events_index = Page.objects.get(url_path='/home/events/')
1543        self.christmas_page = Page.objects.get(url_path='/home/events/christmas/')
1544        self.user = self.login()
1545        self.holiday_category = EventCategory.objects.create(name='Holiday')
1546        self.men_with_beards_category = EventCategory.objects.create(name='Men with beards')
1547
1548    def test_create_and_save(self):
1549        post_data = {
1550            'title': "Presidents' Day",
1551            'date_from': "2017-02-20",
1552            'slug': "presidents-day",
1553            'audience': "public",
1554            'location': "America",
1555            'cost': "$1",
1556            'carousel_items-TOTAL_FORMS': 0,
1557            'carousel_items-INITIAL_FORMS': 0,
1558            'carousel_items-MIN_NUM_FORMS': 0,
1559            'carousel_items-MAX_NUM_FORMS': 0,
1560            'speakers-TOTAL_FORMS': 0,
1561            'speakers-INITIAL_FORMS': 0,
1562            'speakers-MIN_NUM_FORMS': 0,
1563            'speakers-MAX_NUM_FORMS': 0,
1564            'related_links-TOTAL_FORMS': 0,
1565            'related_links-INITIAL_FORMS': 0,
1566            'related_links-MIN_NUM_FORMS': 0,
1567            'related_links-MAX_NUM_FORMS': 0,
1568            'head_counts-TOTAL_FORMS': 0,
1569            'head_counts-INITIAL_FORMS': 0,
1570            'head_counts-MIN_NUM_FORMS': 0,
1571            'head_counts-MAX_NUM_FORMS': 0,
1572            'categories': [self.holiday_category.id, self.men_with_beards_category.id]
1573        }
1574        response = self.client.post(
1575            reverse('wagtailadmin_pages:add', args=('tests', 'eventpage', self.events_index.id)),
1576            post_data
1577        )
1578        created_page = EventPage.objects.get(url_path='/home/events/presidents-day/')
1579        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(created_page.id, )))
1580        created_revision = created_page.get_latest_revision_as_page()
1581
1582        self.assertIn(self.holiday_category, created_revision.categories.all())
1583        self.assertIn(self.men_with_beards_category, created_revision.categories.all())
1584
1585    def test_create_and_publish(self):
1586        post_data = {
1587            'action-publish': "Publish",
1588            'title': "Presidents' Day",
1589            'date_from': "2017-02-20",
1590            'slug': "presidents-day",
1591            'audience': "public",
1592            'location': "America",
1593            'cost': "$1",
1594            'carousel_items-TOTAL_FORMS': 0,
1595            'carousel_items-INITIAL_FORMS': 0,
1596            'carousel_items-MIN_NUM_FORMS': 0,
1597            'carousel_items-MAX_NUM_FORMS': 0,
1598            'speakers-TOTAL_FORMS': 0,
1599            'speakers-INITIAL_FORMS': 0,
1600            'speakers-MIN_NUM_FORMS': 0,
1601            'speakers-MAX_NUM_FORMS': 0,
1602            'related_links-TOTAL_FORMS': 0,
1603            'related_links-INITIAL_FORMS': 0,
1604            'related_links-MIN_NUM_FORMS': 0,
1605            'related_links-MAX_NUM_FORMS': 0,
1606            'head_counts-TOTAL_FORMS': 0,
1607            'head_counts-INITIAL_FORMS': 0,
1608            'head_counts-MIN_NUM_FORMS': 0,
1609            'head_counts-MAX_NUM_FORMS': 0,
1610            'categories': [self.holiday_category.id, self.men_with_beards_category.id]
1611        }
1612        response = self.client.post(
1613            reverse('wagtailadmin_pages:add', args=('tests', 'eventpage', self.events_index.id)),
1614            post_data
1615        )
1616        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.events_index.id, )))
1617
1618        created_page = EventPage.objects.get(url_path='/home/events/presidents-day/')
1619        self.assertIn(self.holiday_category, created_page.categories.all())
1620        self.assertIn(self.men_with_beards_category, created_page.categories.all())
1621
1622    def test_edit_and_save(self):
1623        post_data = {
1624            'title': "Christmas",
1625            'date_from': "2017-12-25",
1626            'slug': "christmas",
1627            'audience': "public",
1628            'location': "The North Pole",
1629            'cost': "Free",
1630            'carousel_items-TOTAL_FORMS': 0,
1631            'carousel_items-INITIAL_FORMS': 0,
1632            'carousel_items-MIN_NUM_FORMS': 0,
1633            'carousel_items-MAX_NUM_FORMS': 0,
1634            'speakers-TOTAL_FORMS': 0,
1635            'speakers-INITIAL_FORMS': 0,
1636            'speakers-MIN_NUM_FORMS': 0,
1637            'speakers-MAX_NUM_FORMS': 0,
1638            'related_links-TOTAL_FORMS': 0,
1639            'related_links-INITIAL_FORMS': 0,
1640            'related_links-MIN_NUM_FORMS': 0,
1641            'related_links-MAX_NUM_FORMS': 0,
1642            'head_counts-TOTAL_FORMS': 0,
1643            'head_counts-INITIAL_FORMS': 0,
1644            'head_counts-MIN_NUM_FORMS': 0,
1645            'head_counts-MAX_NUM_FORMS': 0,
1646            'categories': [self.holiday_category.id, self.men_with_beards_category.id]
1647        }
1648        response = self.client.post(
1649            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1650            post_data
1651        )
1652        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )))
1653        updated_page = EventPage.objects.get(id=self.christmas_page.id)
1654        created_revision = updated_page.get_latest_revision_as_page()
1655
1656        self.assertIn(self.holiday_category, created_revision.categories.all())
1657        self.assertIn(self.men_with_beards_category, created_revision.categories.all())
1658
1659        # no change to live page record yet
1660        self.assertEqual(0, updated_page.categories.count())
1661
1662    def test_edit_and_publish(self):
1663        post_data = {
1664            'action-publish': "Publish",
1665            'title': "Christmas",
1666            'date_from': "2017-12-25",
1667            'slug': "christmas",
1668            'audience': "public",
1669            'location': "The North Pole",
1670            'cost': "Free",
1671            'carousel_items-TOTAL_FORMS': 0,
1672            'carousel_items-INITIAL_FORMS': 0,
1673            'carousel_items-MIN_NUM_FORMS': 0,
1674            'carousel_items-MAX_NUM_FORMS': 0,
1675            'speakers-TOTAL_FORMS': 0,
1676            'speakers-INITIAL_FORMS': 0,
1677            'speakers-MIN_NUM_FORMS': 0,
1678            'speakers-MAX_NUM_FORMS': 0,
1679            'related_links-TOTAL_FORMS': 0,
1680            'related_links-INITIAL_FORMS': 0,
1681            'related_links-MIN_NUM_FORMS': 0,
1682            'related_links-MAX_NUM_FORMS': 0,
1683            'head_counts-TOTAL_FORMS': 0,
1684            'head_counts-INITIAL_FORMS': 0,
1685            'head_counts-MIN_NUM_FORMS': 0,
1686            'head_counts-MAX_NUM_FORMS': 0,
1687            'categories': [self.holiday_category.id, self.men_with_beards_category.id]
1688        }
1689        response = self.client.post(
1690            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1691            post_data
1692        )
1693        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.events_index.id, )))
1694        updated_page = EventPage.objects.get(id=self.christmas_page.id)
1695        self.assertEqual(2, updated_page.categories.count())
1696        self.assertIn(self.holiday_category, updated_page.categories.all())
1697        self.assertIn(self.men_with_beards_category, updated_page.categories.all())
1698
1699
1700class TestValidationErrorMessages(TestCase, WagtailTestUtils):
1701    fixtures = ['test.json']
1702
1703    def setUp(self):
1704        self.events_index = Page.objects.get(url_path='/home/events/')
1705        self.christmas_page = Page.objects.get(url_path='/home/events/christmas/')
1706        self.user = self.login()
1707
1708    def test_field_error(self):
1709        """Field errors should be shown against the relevant fields, not in the header message"""
1710        post_data = {
1711            'title': "",
1712            'date_from': "2017-12-25",
1713            'slug': "christmas",
1714            'audience': "public",
1715            'location': "The North Pole",
1716            'cost': "Free",
1717            'carousel_items-TOTAL_FORMS': 0,
1718            'carousel_items-INITIAL_FORMS': 0,
1719            'carousel_items-MIN_NUM_FORMS': 0,
1720            'carousel_items-MAX_NUM_FORMS': 0,
1721            'speakers-TOTAL_FORMS': 0,
1722            'speakers-INITIAL_FORMS': 0,
1723            'speakers-MIN_NUM_FORMS': 0,
1724            'speakers-MAX_NUM_FORMS': 0,
1725            'related_links-TOTAL_FORMS': 0,
1726            'related_links-INITIAL_FORMS': 0,
1727            'related_links-MIN_NUM_FORMS': 0,
1728            'related_links-MAX_NUM_FORMS': 0,
1729            'head_counts-TOTAL_FORMS': 0,
1730            'head_counts-INITIAL_FORMS': 0,
1731            'head_counts-MIN_NUM_FORMS': 0,
1732            'head_counts-MAX_NUM_FORMS': 0,
1733        }
1734        response = self.client.post(
1735            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1736            post_data
1737        )
1738        self.assertEqual(response.status_code, 200)
1739
1740        self.assertContains(response, "The page could not be saved due to validation errors")
1741        # the error should only appear once: against the field, not in the header message
1742        self.assertContains(response, """<p class="error-message"><span>This field is required.</span></p>""", count=1, html=True)
1743        self.assertContains(response, "This field is required", count=1)
1744
1745    def test_non_field_error(self):
1746        """Non-field errors should be shown in the header message"""
1747        post_data = {
1748            'title': "Christmas",
1749            'date_from': "2017-12-25",
1750            'date_to': "2017-12-24",
1751            'slug': "christmas",
1752            'audience': "public",
1753            'location': "The North Pole",
1754            'cost': "Free",
1755            'carousel_items-TOTAL_FORMS': 0,
1756            'carousel_items-INITIAL_FORMS': 0,
1757            'carousel_items-MIN_NUM_FORMS': 0,
1758            'carousel_items-MAX_NUM_FORMS': 0,
1759            'speakers-TOTAL_FORMS': 0,
1760            'speakers-INITIAL_FORMS': 0,
1761            'speakers-MIN_NUM_FORMS': 0,
1762            'speakers-MAX_NUM_FORMS': 0,
1763            'related_links-TOTAL_FORMS': 0,
1764            'related_links-INITIAL_FORMS': 0,
1765            'related_links-MIN_NUM_FORMS': 0,
1766            'related_links-MAX_NUM_FORMS': 0,
1767            'head_counts-TOTAL_FORMS': 0,
1768            'head_counts-INITIAL_FORMS': 0,
1769            'head_counts-MIN_NUM_FORMS': 0,
1770            'head_counts-MAX_NUM_FORMS': 0,
1771        }
1772        response = self.client.post(
1773            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1774            post_data
1775        )
1776        self.assertEqual(response.status_code, 200)
1777
1778        self.assertContains(response, "The page could not be saved due to validation errors")
1779        self.assertContains(response, "<li>The end date must be after the start date</li>", count=1)
1780
1781    def test_field_and_non_field_error(self):
1782        """
1783        If both field and non-field errors exist, all errors should be shown in the header message
1784        with appropriate context to identify the field; and field errors should also be shown
1785        against the relevant fields.
1786        """
1787        post_data = {
1788            'title': "",
1789            'date_from': "2017-12-25",
1790            'date_to': "2017-12-24",
1791            'slug': "christmas",
1792            'audience': "public",
1793            'location': "The North Pole",
1794            'cost': "Free",
1795            'carousel_items-TOTAL_FORMS': 0,
1796            'carousel_items-INITIAL_FORMS': 0,
1797            'carousel_items-MIN_NUM_FORMS': 0,
1798            'carousel_items-MAX_NUM_FORMS': 0,
1799            'speakers-TOTAL_FORMS': 0,
1800            'speakers-INITIAL_FORMS': 0,
1801            'speakers-MIN_NUM_FORMS': 0,
1802            'speakers-MAX_NUM_FORMS': 0,
1803            'related_links-TOTAL_FORMS': 0,
1804            'related_links-INITIAL_FORMS': 0,
1805            'related_links-MIN_NUM_FORMS': 0,
1806            'related_links-MAX_NUM_FORMS': 0,
1807            'head_counts-TOTAL_FORMS': 0,
1808            'head_counts-INITIAL_FORMS': 0,
1809            'head_counts-MIN_NUM_FORMS': 0,
1810            'head_counts-MAX_NUM_FORMS': 0,
1811        }
1812        response = self.client.post(
1813            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1814            post_data
1815        )
1816        self.assertEqual(response.status_code, 200)
1817
1818        self.assertContains(response, "The page could not be saved due to validation errors")
1819        self.assertContains(response, "<li>The end date must be after the start date</li>", count=1)
1820
1821        # Error on title shown against the title field
1822        self.assertContains(response, """<p class="error-message"><span>This field is required.</span></p>""", count=1, html=True)
1823        # Error on title shown in the header message
1824        self.assertContains(response, "<li>Title: This field is required.</li>", count=1)
1825
1826
1827class TestNestedInlinePanel(TestCase, WagtailTestUtils):
1828    fixtures = ['test.json']
1829
1830    def setUp(self):
1831        self.events_index = Page.objects.get(url_path='/home/events/')
1832        self.christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
1833        self.speaker = self.christmas_page.speakers.first()
1834        self.speaker.awards.create(
1835            name="Beard Of The Year", date_awarded=datetime.date(1997, 12, 25)
1836        )
1837        self.speaker.save()
1838        self.user = self.login()
1839
1840    def test_get_edit_form(self):
1841        response = self.client.get(
1842            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, ))
1843        )
1844        self.assertEqual(response.status_code, 200)
1845        self.assertContains(
1846            response,
1847            """<input type="text" name="speakers-0-awards-0-name" value="Beard Of The Year" maxlength="255" id="id_speakers-0-awards-0-name">""",
1848            count=1, html=True
1849        )
1850
1851        # there should be no "extra" forms, as the nested formset should respect the extra_form_count=0 set on WagtailAdminModelForm
1852        self.assertContains(
1853            response,
1854            """<input type="hidden" name="speakers-0-awards-TOTAL_FORMS" value="1" id="id_speakers-0-awards-TOTAL_FORMS">""",
1855            count=1, html=True
1856        )
1857        self.assertContains(
1858            response,
1859            """<input type="text" name="speakers-0-awards-1-name" value="" maxlength="255" id="id_speakers-0-awards-1-name">""",
1860            count=0, html=True
1861        )
1862
1863        # date field should use AdminDatePicker
1864        self.assertContains(
1865            response,
1866            """<input type="text" name="speakers-0-awards-0-date_awarded" value="1997-12-25" autocomplete="off" id="id_speakers-0-awards-0-date_awarded">""",
1867            count=1, html=True
1868        )
1869
1870    def test_post_edit(self):
1871        post_data = nested_form_data({
1872            'title': "Christmas",
1873            'date_from': "2017-12-25",
1874            'date_to': "2017-12-25",
1875            'slug': "christmas",
1876            'audience': "public",
1877            'location': "The North Pole",
1878            'cost': "Free",
1879            'carousel_items': inline_formset([]),
1880            'speakers': inline_formset([
1881                {
1882                    'id': self.speaker.id,
1883                    'first_name': "Jeff",
1884                    'last_name': "Christmas",
1885                    'awards': inline_formset([
1886                        {
1887                            'id': self.speaker.awards.first().id,
1888                            'name': "Beard Of The Century",
1889                            'date_awarded': "1997-12-25",
1890                        },
1891                        {
1892                            'name': "Bobsleigh Olympic gold medallist",
1893                            'date_awarded': "2018-02-01",
1894                        },
1895                    ], initial=1)
1896                },
1897            ], initial=1),
1898            'related_links': inline_formset([]),
1899            'head_counts': inline_formset([]),
1900            'action-publish': "Publish",
1901        })
1902        response = self.client.post(
1903            reverse('wagtailadmin_pages:edit', args=(self.christmas_page.id, )),
1904            post_data
1905        )
1906        self.assertRedirects(response, reverse('wagtailadmin_explore', args=(self.events_index.id, )))
1907
1908        new_christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
1909        self.assertEqual(new_christmas_page.speakers.first().first_name, "Jeff")
1910        awards = new_christmas_page.speakers.first().awards.all()
1911        self.assertEqual(len(awards), 2)
1912        self.assertEqual(awards[0].name, "Beard Of The Century")
1913        self.assertEqual(awards[1].name, "Bobsleigh Olympic gold medallist")
1914
1915
1916@override_settings(WAGTAIL_I18N_ENABLED=True)
1917class TestLocaleSelector(TestCase, WagtailTestUtils):
1918    fixtures = ['test.json']
1919
1920    def setUp(self):
1921        self.christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
1922        self.fr_locale = Locale.objects.create(language_code='fr')
1923        self.translated_christmas_page = self.christmas_page.copy_for_translation(self.fr_locale, copy_parents=True)
1924        self.user = self.login()
1925
1926    def test_locale_selector(self):
1927        response = self.client.get(
1928            reverse('wagtailadmin_pages:edit', args=[self.christmas_page.id])
1929        )
1930
1931        self.assertContains(response, '<li class="header-meta--locale">')
1932
1933        edit_translation_url = reverse('wagtailadmin_pages:edit', args=[self.translated_christmas_page.id])
1934        self.assertContains(response, f'<a href="{edit_translation_url}" aria-label="French" class="u-link is-live">')
1935
1936    @override_settings(WAGTAIL_I18N_ENABLED=False)
1937    def test_locale_selector_not_present_when_i18n_disabled(self):
1938        response = self.client.get(
1939            reverse('wagtailadmin_pages:edit', args=[self.christmas_page.id])
1940        )
1941
1942        self.assertNotContains(response, '<li class="header-meta--locale">')
1943
1944        edit_translation_url = reverse('wagtailadmin_pages:edit', args=[self.translated_christmas_page.id])
1945        self.assertNotContains(response, f'<a href="{edit_translation_url}" aria-label="French" class="u-link is-live">')
1946
1947    def test_locale_dropdown_not_present_without_permission_to_edit(self):
1948        # Remove user's permissions to edit French tree
1949        en_events_index = Page.objects.get(url_path='/home/events/')
1950        group = Group.objects.get(name='Moderators')
1951        GroupPagePermission.objects.create(
1952            group=group,
1953            page=en_events_index,
1954            permission_type='edit',
1955        )
1956        self.user.is_superuser = False
1957        self.user.user_permissions.add(
1958            Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin')
1959        )
1960        self.user.groups.add(group)
1961        self.user.save()
1962
1963        # Locale indicator should exist, but the "French" option should be hidden
1964        response = self.client.get(
1965            reverse('wagtailadmin_pages:edit', args=[self.christmas_page.id])
1966        )
1967
1968        self.assertContains(response, '<li class="header-meta--locale">')
1969
1970        edit_translation_url = reverse('wagtailadmin_pages:edit', args=[self.translated_christmas_page.id])
1971        self.assertNotContains(response, f'<a href="{edit_translation_url}" aria-label="French" class="u-link is-live">')
1972
1973
1974class TestPageSubscriptionSettings(TestCase, WagtailTestUtils):
1975    def setUp(self):
1976        # Find root page
1977        self.root_page = Page.objects.get(id=2)
1978
1979        # Add child page
1980        child_page = SimplePage(
1981            title="Hello world!",
1982            slug="hello-world",
1983            content="hello",
1984        )
1985        self.root_page.add_child(instance=child_page)
1986        child_page.save_revision().publish()
1987        self.child_page = SimplePage.objects.get(id=child_page.id)
1988
1989        # Login
1990        self.user = self.login()
1991
1992    def test_commment_notifications_switched_off(self):
1993        response = self.client.get(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
1994
1995        self.assertEqual(response.status_code, 200)
1996        self.assertContains(response, '<input type="checkbox" name="comment_notifications" id="id_comment_notifications">')
1997
1998    def test_commment_notifications_switched_on(self):
1999        PageSubscription.objects.create(
2000            page=self.child_page,
2001            user=self.user,
2002            comment_notifications=True
2003        )
2004
2005        response = self.client.get(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2006
2007        self.assertEqual(response.status_code, 200)
2008        self.assertContains(response, '<input type="checkbox" name="comment_notifications" id="id_comment_notifications" checked>')
2009
2010    def test_post_with_comment_notifications_switched_on(self):
2011        post_data = {
2012            'title': "I've been edited!",
2013            'content': "Some content",
2014            'slug': 'hello-world',
2015            'comment_notifications': 'on'
2016        }
2017        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2018        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2019
2020        # Check the subscription
2021        page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
2022        subscription = page.subscribers.get()
2023
2024        self.assertEqual(subscription.user, self.user)
2025        self.assertTrue(subscription.comment_notifications)
2026
2027    def test_post_with_comment_notifications_switched_off(self):
2028        # Switch on comment notifications so we can test switching them off
2029        subscription = PageSubscription.objects.create(
2030            page=self.child_page,
2031            user=self.user,
2032            comment_notifications=True
2033        )
2034
2035        post_data = {
2036            'title': "I've been edited!",
2037            'content': "Some content",
2038            'slug': 'hello-world',
2039        }
2040        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2041        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2042
2043        # Check the subscription
2044        subscription.refresh_from_db()
2045        self.assertFalse(subscription.comment_notifications)
2046
2047    @override_settings(WAGTAILADMIN_COMMENTS_ENABLED=False)
2048    def test_comments_disabled(self):
2049        response = self.client.get(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2050
2051        self.assertEqual(response.status_code, 200)
2052        self.assertNotContains(response, '<input type="checkbox" name="comment_notifications" id="id_comment_notifications">')
2053
2054    @override_settings(WAGTAILADMIN_COMMENTS_ENABLED=False)
2055    def test_post_comments_disabled(self):
2056        post_data = {
2057            'title': "I've been edited!",
2058            'content': "Some content",
2059            'slug': 'hello-world',
2060            'comment_notifications': 'on'  # Testing that this gets ignored
2061        }
2062        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2063        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2064
2065        # Check the subscription
2066        self.assertFalse(PageSubscription.objects.get().comment_notifications)
2067
2068
2069class TestCommenting(TestCase, WagtailTestUtils):
2070    """
2071    Tests both the comment notification and audit logging logic of the edit page view.
2072    """
2073    def setUp(self):
2074        # Find root page
2075        self.root_page = Page.objects.get(id=2)
2076
2077        # Add child page
2078        child_page = SimplePage(
2079            title="Hello world!",
2080            slug="hello-world",
2081            content="hello",
2082        )
2083        self.root_page.add_child(instance=child_page)
2084        child_page.save_revision().publish()
2085        self.child_page = SimplePage.objects.get(id=child_page.id)
2086
2087        # Login
2088        self.user = self.login()
2089
2090        # Add a couple more users
2091        self.subscriber = self.create_user('subscriber')
2092        self.non_subscriber = self.create_user('non-subscriber')
2093        self.non_subscriber_2 = self.create_user('non-subscriber-2')
2094
2095        PageSubscription.objects.create(
2096            page=self.child_page,
2097            user=self.user,
2098            comment_notifications=True
2099        )
2100
2101        PageSubscription.objects.create(
2102            page=self.child_page,
2103            user=self.subscriber,
2104            comment_notifications=True
2105        )
2106
2107    def test_new_comment(self):
2108        post_data = {
2109            'title': "I've been edited!",
2110            'content': "Some content",
2111            'slug': 'hello-world',
2112            'comments-TOTAL_FORMS': '1',
2113            'comments-INITIAL_FORMS': '0',
2114            'comments-MIN_NUM_FORMS': '0',
2115            'comments-MAX_NUM_FORMS': '',
2116            'comments-0-DELETE': '',
2117            'comments-0-resolved': '',
2118            'comments-0-id': '',
2119            'comments-0-contentpath': 'title',
2120            'comments-0-text': 'A test comment',
2121            'comments-0-position': '',
2122            'comments-0-replies-TOTAL_FORMS': '0',
2123            'comments-0-replies-INITIAL_FORMS': '0',
2124            'comments-0-replies-MIN_NUM_FORMS': '0',
2125            'comments-0-replies-MAX_NUM_FORMS': '0'
2126        }
2127
2128        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2129
2130        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2131
2132        # Check the comment was added
2133        comment = self.child_page.comments.get()
2134        self.assertEqual(comment.text, 'A test comment')
2135
2136        # Check notification email
2137        self.assertEqual(len(mail.outbox), 1)
2138        self.assertEqual(mail.outbox[0].to, [self.subscriber.email])
2139        self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
2140        self.assertIn('New comments:\n - "A test comment"\n\n', mail.outbox[0].body)
2141
2142        # Check audit log
2143        log_entry = PageLogEntry.objects.get(action='wagtail.comments.create')
2144        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2145        self.assertEqual(log_entry.user, self.user)
2146        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2147        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2148        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2149        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2150
2151    def test_edit_comment(self):
2152        comment = Comment.objects.create(
2153            page=self.child_page,
2154            user=self.user,
2155            text="A test comment",
2156            contentpath="title",
2157        )
2158
2159        post_data = {
2160            'title': "I've been edited!",
2161            'content': "Some content",
2162            'slug': 'hello-world',
2163            'comments-TOTAL_FORMS': '1',
2164            'comments-INITIAL_FORMS': '1',
2165            'comments-MIN_NUM_FORMS': '0',
2166            'comments-MAX_NUM_FORMS': '',
2167            'comments-0-DELETE': '',
2168            'comments-0-resolved': '',
2169            'comments-0-id': str(comment.id),
2170            'comments-0-contentpath': 'title',
2171            'comments-0-text': 'Edited',
2172            'comments-0-position': '',
2173            'comments-0-replies-TOTAL_FORMS': '0',
2174            'comments-0-replies-INITIAL_FORMS': '0',
2175            'comments-0-replies-MIN_NUM_FORMS': '0',
2176            'comments-0-replies-MAX_NUM_FORMS': '0'
2177        }
2178
2179        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2180
2181        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2182
2183        # Check the comment was edited
2184        comment.refresh_from_db()
2185        self.assertEqual(comment.text, 'Edited')
2186
2187        # No emails should be sent for edited comments
2188        self.assertEqual(len(mail.outbox), 0)
2189
2190        # Check audit log
2191        log_entry = PageLogEntry.objects.get(action='wagtail.comments.edit')
2192        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2193        self.assertEqual(log_entry.user, self.user)
2194        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2195        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2196        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2197        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2198
2199    def test_edit_another_users_comment(self):
2200        comment = Comment.objects.create(
2201            page=self.child_page,
2202            user=self.subscriber,
2203            text="A test comment",
2204            contentpath="title",
2205        )
2206
2207        post_data = {
2208            'title': "I've been edited!",
2209            'content': "Some content",
2210            'slug': 'hello-world',
2211            'comments-TOTAL_FORMS': '1',
2212            'comments-INITIAL_FORMS': '1',
2213            'comments-MIN_NUM_FORMS': '0',
2214            'comments-MAX_NUM_FORMS': '',
2215            'comments-0-DELETE': '',
2216            'comments-0-resolved': '',
2217            'comments-0-id': str(comment.id),
2218            'comments-0-contentpath': 'title',
2219            'comments-0-text': 'Edited',
2220            'comments-0-position': '',
2221            'comments-0-replies-TOTAL_FORMS': '0',
2222            'comments-0-replies-INITIAL_FORMS': '0',
2223            'comments-0-replies-MIN_NUM_FORMS': '0',
2224            'comments-0-replies-MAX_NUM_FORMS': '0'
2225        }
2226
2227        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2228
2229        self.assertEqual(response.context['form'].formsets['comments'].errors, [{'__all__': ["You cannot edit another user's comment."]}])
2230
2231        # Check the comment was not edited
2232        comment.refresh_from_db()
2233        self.assertNotEqual(comment.text, 'Edited')
2234
2235        # Check no log entry was created
2236        self.assertFalse(PageLogEntry.objects.filter(action='wagtail.comments.edit').exists())
2237
2238    def test_resolve_comment(self):
2239        comment = Comment.objects.create(
2240            page=self.child_page,
2241            user=self.non_subscriber,
2242            text="A test comment",
2243            contentpath="title",
2244        )
2245
2246        post_data = {
2247            'title': "I've been edited!",
2248            'content': "Some content",
2249            'slug': 'hello-world',
2250            'comments-TOTAL_FORMS': '1',
2251            'comments-INITIAL_FORMS': '1',
2252            'comments-MIN_NUM_FORMS': '0',
2253            'comments-MAX_NUM_FORMS': '',
2254            'comments-0-DELETE': '',
2255            'comments-0-resolved': 'on',
2256            'comments-0-id': str(comment.id),
2257            'comments-0-contentpath': 'title',
2258            'comments-0-text': 'A test comment',
2259            'comments-0-position': '',
2260            'comments-0-replies-TOTAL_FORMS': '0',
2261            'comments-0-replies-INITIAL_FORMS': '0',
2262            'comments-0-replies-MIN_NUM_FORMS': '0',
2263            'comments-0-replies-MAX_NUM_FORMS': '0'
2264        }
2265
2266        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2267
2268        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2269
2270        # Check the comment was resolved
2271        comment.refresh_from_db()
2272        self.assertTrue(comment.resolved_at)
2273        self.assertEqual(comment.resolved_by, self.user)
2274
2275        # Check notification email
2276        self.assertEqual(len(mail.outbox), 2)
2277        # The non subscriber created the comment, so should also get an email
2278        self.assertEqual(mail.outbox[0].to, [self.non_subscriber.email])
2279        self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
2280        self.assertIn('Resolved comments:\n - "A test comment"\n\n', mail.outbox[0].body)
2281        self.assertEqual(mail.outbox[1].to, [self.subscriber.email])
2282        self.assertEqual(mail.outbox[1].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
2283        self.assertIn('Resolved comments:\n - "A test comment"\n\n', mail.outbox[1].body)
2284
2285        # Check audit log
2286        log_entry = PageLogEntry.objects.get(action='wagtail.comments.resolve')
2287        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2288        self.assertEqual(log_entry.user, self.user)
2289        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2290        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2291        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2292        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2293
2294    def test_delete_comment(self):
2295        comment = Comment.objects.create(
2296            page=self.child_page,
2297            user=self.user,
2298            text="A test comment",
2299            contentpath="title",
2300        )
2301
2302        post_data = {
2303            'title': "I've been edited!",
2304            'content': "Some content",
2305            'slug': 'hello-world',
2306            'comments-TOTAL_FORMS': '1',
2307            'comments-INITIAL_FORMS': '1',
2308            'comments-MIN_NUM_FORMS': '0',
2309            'comments-MAX_NUM_FORMS': '',
2310            'comments-0-DELETE': 'on',
2311            'comments-0-resolved': '',
2312            'comments-0-id': str(comment.id),
2313            'comments-0-contentpath': 'title',
2314            'comments-0-text': 'A test comment',
2315            'comments-0-position': '',
2316            'comments-0-replies-TOTAL_FORMS': '0',
2317            'comments-0-replies-INITIAL_FORMS': '0',
2318            'comments-0-replies-MIN_NUM_FORMS': '0',
2319            'comments-0-replies-MAX_NUM_FORMS': '0'
2320        }
2321
2322        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2323
2324        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2325
2326        # Check the comment was deleted
2327        self.assertFalse(self.child_page.comments.exists())
2328
2329        # Check notification email
2330        self.assertEqual(len(mail.outbox), 1)
2331        self.assertEqual(mail.outbox[0].to, [self.subscriber.email])
2332        self.assertEqual(mail.outbox[0].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
2333        self.assertIn('Deleted comments:\n - "A test comment"\n\n', mail.outbox[0].body)
2334
2335        # Check audit log
2336        log_entry = PageLogEntry.objects.get(action='wagtail.comments.delete')
2337        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2338        self.assertEqual(log_entry.user, self.user)
2339        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2340        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2341        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2342        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2343
2344    def test_new_reply(self):
2345        comment = Comment.objects.create(
2346            page=self.child_page,
2347            user=self.non_subscriber,
2348            text="A test comment",
2349            contentpath="title",
2350        )
2351
2352        reply = CommentReply.objects.create(
2353            comment=comment,
2354            user=self.non_subscriber_2,
2355            text='an old reply'
2356        )
2357
2358        post_data = {
2359            'title': "I've been edited!",
2360            'content': "Some content",
2361            'slug': 'hello-world',
2362            'comments-TOTAL_FORMS': '1',
2363            'comments-INITIAL_FORMS': '1',
2364            'comments-MIN_NUM_FORMS': '0',
2365            'comments-MAX_NUM_FORMS': '',
2366            'comments-0-DELETE': '',
2367            'comments-0-resolved': '',
2368            'comments-0-id': str(comment.id),
2369            'comments-0-contentpath': 'title',
2370            'comments-0-text': 'A test comment',
2371            'comments-0-position': '',
2372            'comments-0-replies-TOTAL_FORMS': '2',
2373            'comments-0-replies-INITIAL_FORMS': '1',
2374            'comments-0-replies-MIN_NUM_FORMS': '0',
2375            'comments-0-replies-MAX_NUM_FORMS': '',
2376            'comments-0-replies-0-id': str(reply.id),
2377            'comments-0-replies-0-text': 'an old reply',
2378            'comments-0-replies-1-id': '',
2379            'comments-0-replies-1-text': 'a new reply'
2380        }
2381
2382        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2383
2384        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2385
2386        # Check the comment reply was added
2387        comment.refresh_from_db()
2388        self.assertEqual(comment.replies.last().text, 'a new reply')
2389
2390        # Check notification email
2391        self.assertEqual(len(mail.outbox), 3)
2392
2393        recipients = [mail.to for mail in mail.outbox]
2394        # The other non subscriber replied in the thread, so should get an email
2395        self.assertIn([self.non_subscriber_2.email], recipients)
2396
2397        # The non subscriber created the comment, so should get an email
2398        self.assertIn([self.non_subscriber.email], recipients)
2399
2400        self.assertIn([self.subscriber.email], recipients)
2401        self.assertEqual(mail.outbox[2].subject, 'test@email.com has updated comments on "I\'ve been edited! (simple page)"')
2402        self.assertIn('  New replies to: "A test comment"\n   - "a new reply"', mail.outbox[2].body)
2403
2404        # Check audit log
2405        log_entry = PageLogEntry.objects.get(action='wagtail.comments.create_reply')
2406        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2407        self.assertEqual(log_entry.user, self.user)
2408        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2409        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2410        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2411        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2412        self.assertNotEqual(log_entry.data['reply']['id'], reply.id)
2413        self.assertEqual(log_entry.data['reply']['text'], 'a new reply')
2414
2415    def test_edit_reply(self):
2416        comment = Comment.objects.create(
2417            page=self.child_page,
2418            user=self.non_subscriber,
2419            text="A test comment",
2420            contentpath="title",
2421        )
2422
2423        reply = CommentReply.objects.create(
2424            comment=comment,
2425            user=self.user,
2426            text='an old reply'
2427        )
2428
2429        post_data = {
2430            'title': "I've been edited!",
2431            'content': "Some content",
2432            'slug': 'hello-world',
2433            'comments-TOTAL_FORMS': '1',
2434            'comments-INITIAL_FORMS': '1',
2435            'comments-MIN_NUM_FORMS': '0',
2436            'comments-MAX_NUM_FORMS': '',
2437            'comments-0-DELETE': '',
2438            'comments-0-resolved': '',
2439            'comments-0-id': str(comment.id),
2440            'comments-0-contentpath': 'title',
2441            'comments-0-text': 'A test comment',
2442            'comments-0-position': '',
2443            'comments-0-replies-TOTAL_FORMS': '1',
2444            'comments-0-replies-INITIAL_FORMS': '1',
2445            'comments-0-replies-MIN_NUM_FORMS': '0',
2446            'comments-0-replies-MAX_NUM_FORMS': '',
2447            'comments-0-replies-0-id': str(reply.id),
2448            'comments-0-replies-0-text': 'an edited reply',
2449        }
2450
2451        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2452
2453        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2454
2455        # Check the comment reply was edited
2456        reply.refresh_from_db()
2457        self.assertEqual(reply.text, 'an edited reply')
2458
2459        # Check no notification was sent
2460        self.assertEqual(len(mail.outbox), 0)
2461
2462        # Check audit log
2463        log_entry = PageLogEntry.objects.get(action='wagtail.comments.edit_reply')
2464        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2465        self.assertEqual(log_entry.user, self.user)
2466        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2467        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2468        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2469        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2470        self.assertEqual(log_entry.data['reply']['id'], reply.id)
2471        self.assertEqual(log_entry.data['reply']['text'], 'an edited reply')
2472
2473    def test_delete_reply(self):
2474        comment = Comment.objects.create(
2475            page=self.child_page,
2476            user=self.non_subscriber,
2477            text="A test comment",
2478            contentpath="title",
2479        )
2480
2481        reply = CommentReply.objects.create(
2482            comment=comment,
2483            user=self.user,
2484            text='an old reply'
2485        )
2486
2487        post_data = {
2488            'title': "I've been edited!",
2489            'content': "Some content",
2490            'slug': 'hello-world',
2491            'comments-TOTAL_FORMS': '1',
2492            'comments-INITIAL_FORMS': '1',
2493            'comments-MIN_NUM_FORMS': '0',
2494            'comments-MAX_NUM_FORMS': '',
2495            'comments-0-DELETE': '',
2496            'comments-0-resolved': '',
2497            'comments-0-id': str(comment.id),
2498            'comments-0-contentpath': 'title',
2499            'comments-0-text': 'A test comment',
2500            'comments-0-position': '',
2501            'comments-0-replies-TOTAL_FORMS': '1',
2502            'comments-0-replies-INITIAL_FORMS': '1',
2503            'comments-0-replies-MIN_NUM_FORMS': '0',
2504            'comments-0-replies-MAX_NUM_FORMS': '',
2505            'comments-0-replies-0-id': str(reply.id),
2506            'comments-0-replies-0-text': 'an old reply',
2507            'comments-0-replies-0-DELETE': 'on',
2508        }
2509
2510        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2511
2512        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2513
2514        # Check the comment reply was deleted
2515        self.assertFalse(comment.replies.exists())
2516
2517        # Check no notification was sent
2518        self.assertEqual(len(mail.outbox), 0)
2519
2520        # Check audit log
2521        log_entry = PageLogEntry.objects.get(action='wagtail.comments.delete_reply')
2522        self.assertEqual(log_entry.page, self.child_page.page_ptr)
2523        self.assertEqual(log_entry.user, self.user)
2524        self.assertEqual(log_entry.revision, self.child_page.get_latest_revision())
2525        self.assertEqual(log_entry.data['comment']['id'], comment.id)
2526        self.assertEqual(log_entry.data['comment']['contentpath'], comment.contentpath)
2527        self.assertEqual(log_entry.data['comment']['text'], comment.text)
2528        self.assertEqual(log_entry.data['reply']['id'], reply.id)
2529        self.assertEqual(log_entry.data['reply']['text'], reply.text)
2530
2531    def test_updated_comments_notifications_profile_setting(self):
2532        # Users can disable commenting notifications globally from account settings
2533        profile = UserProfile.get_for_user(self.subscriber)
2534        profile.updated_comments_notifications = False
2535        profile.save()
2536
2537        post_data = {
2538            'title': "I've been edited!",
2539            'content': "Some content",
2540            'slug': 'hello-world',
2541            'comments-TOTAL_FORMS': '1',
2542            'comments-INITIAL_FORMS': '0',
2543            'comments-MIN_NUM_FORMS': '0',
2544            'comments-MAX_NUM_FORMS': '',
2545            'comments-0-DELETE': '',
2546            'comments-0-resolved': '',
2547            'comments-0-id': '',
2548            'comments-0-contentpath': 'title',
2549            'comments-0-text': 'A test comment',
2550            'comments-0-position': '',
2551            'comments-0-replies-TOTAL_FORMS': '0',
2552            'comments-0-replies-INITIAL_FORMS': '0',
2553            'comments-0-replies-MIN_NUM_FORMS': '0',
2554            'comments-0-replies-MAX_NUM_FORMS': '0'
2555        }
2556
2557        response = self.client.post(reverse('wagtailadmin_pages:edit', args=[self.child_page.id]), post_data)
2558
2559        self.assertRedirects(response, reverse('wagtailadmin_pages:edit', args=[self.child_page.id]))
2560
2561        # Check the comment was added
2562        comment = self.child_page.comments.get()
2563        self.assertEqual(comment.text, 'A test comment')
2564
2565        # This time, no emails should be submitted because the only subscriber has disabled these emails globally
2566        self.assertEqual(len(mail.outbox), 0)
2567