from datetime import date, datetime
from functools import wraps
from unittest import mock

from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.core import checks
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
from django.test import RequestFactory, TestCase, override_settings
from django.utils.html import json_script
from freezegun import freeze_time
from pytz import utc

from wagtail.admin.forms import WagtailAdminModelForm, WagtailAdminPageForm
from wagtail.admin.panels import (
    CommentPanel,
    FieldPanel,
    FieldRowPanel,
    InlinePanel,
    MultiFieldPanel,
    ObjectList,
    PageChooserPanel,
    TabbedInterface,
    extract_panel_definitions_from_model_class,
    get_form_for_model,
)
from wagtail.admin.rich_text import DraftailRichTextArea
from wagtail.admin.widgets import (
    AdminAutoHeightTextInput,
    AdminDateInput,
    AdminPageChooser,
)
from wagtail.models import Comment, CommentReply, Page, Site
from wagtail.test.testapp.forms import ValidatedPageForm
from wagtail.test.testapp.models import (
    EventPage,
    EventPageChooserModel,
    EventPageSpeaker,
    PageChooserModel,
    RestaurantPage,
    RestaurantTag,
    SimplePage,
    ValidatedPage,
)
from wagtail.test.utils import WagtailTestUtils


class TestGetFormForModel(TestCase):
    def test_get_form_without_model(self):
        edit_handler = ObjectList()
        with self.assertRaisesMessage(
            AttributeError,
            "ObjectList is not bound to a model yet. "
            "Use `.bind_to_model(model)` before using this method.",
        ):
            edit_handler.get_form_class()

    def test_get_form_for_model_without_explicit_fields(self):
        # Failing to pass a 'fields' argument to get_form_for_model IS valid, and should result in
        # a form with no fields.
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
        )
        self.assertTrue(issubclass(EventPageForm, WagtailAdminPageForm))
        form = EventPageForm()
        self.assertNotIn("title", form.fields)
        self.assertNotIn("path", form.fields)

    def test_get_form_for_model_without_formsets(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "date_from", "date_to"],
        )
        form = EventPageForm()

        # form should be a subclass of WagtailAdminModelForm
        self.assertTrue(issubclass(EventPageForm, WagtailAdminModelForm))
        # form should contain a title field (from the base Page)
        self.assertEqual(type(form.fields["title"]), forms.CharField)
        # and 'date_from' from EventPage
        self.assertEqual(type(form.fields["date_from"]), forms.DateField)
        # the widget should be overridden with AdminDateInput as per FORM_FIELD_OVERRIDES
        self.assertEqual(type(form.fields["date_from"].widget), AdminDateInput)

        # treebeard's 'path' field should be excluded
        self.assertNotIn("path", form.fields)

    def test_get_form_for_model_with_formsets(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "date_from", "date_to"],
            formsets=["speakers", "related_links"],
        )
        form = EventPageForm()

        # all child relations become formsets by default
        self.assertIn("speakers", form.formsets)
        self.assertIn("related_links", form.formsets)

    def test_direct_form_field_overrides(self):
        # Test that field overrides defined through DIRECT_FORM_FIELD_OVERRIDES
        # are applied

        SimplePageForm = get_form_for_model(
            SimplePage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "content"],
        )
        self.assertTrue(issubclass(SimplePageForm, WagtailAdminPageForm))
        simple_form = SimplePageForm()
        # plain TextFields should use AdminAutoHeightTextInput as the widget
        self.assertEqual(
            type(simple_form.fields["content"].widget), AdminAutoHeightTextInput
        )

        # This override should NOT be applied to subclasses of TextField such as
        # RichTextField - they should retain their default widgets
        EventPageForm = get_form_for_model(
            EventPage, form_class=WagtailAdminPageForm, fields=["title", "slug", "body"]
        )
        event_form = EventPageForm()
        self.assertEqual(type(event_form.fields["body"].widget), DraftailRichTextArea)

    def test_get_form_for_model_with_specific_fields(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["date_from"],
            formsets=["speakers"],
        )
        form = EventPageForm()

        # form should contain date_from but not title
        self.assertEqual(type(form.fields["date_from"]), forms.DateField)
        self.assertEqual(type(form.fields["date_from"].widget), AdminDateInput)
        self.assertNotIn("title", form.fields)

        # formsets should include speakers but not related_links
        self.assertIn("speakers", form.formsets)
        self.assertNotIn("related_links", form.formsets)

    def test_get_form_for_model_without_explicit_formsets(self):
        # omitting the 'formsets' argument from get_form_for_model should return a form with no
        # formsets, rather than a form with ALL OF THE FORMSETS which somehow seemed like a good
        # idea once
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["date_from"],
        )
        form = EventPageForm()

        self.assertNotIn("speakers", form.formsets)
        self.assertNotIn("related_links", form.formsets)

    def test_get_form_for_model_with_excluded_fields(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            exclude=["title"],
            exclude_formsets=["related_links"],
        )
        form = EventPageForm()

        # form should contain date_from but not title
        self.assertEqual(type(form.fields["date_from"]), forms.DateField)
        self.assertEqual(type(form.fields["date_from"].widget), AdminDateInput)
        self.assertNotIn("title", form.fields)

        # formsets should include speakers but not related_links
        self.assertIn("speakers", form.formsets)
        self.assertNotIn("related_links", form.formsets)

    def test_get_form_for_model_with_widget_overides_by_class(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["date_to", "date_from"],
            widgets={"date_from": forms.PasswordInput},
        )
        form = EventPageForm()

        self.assertEqual(type(form.fields["date_from"]), forms.DateField)
        self.assertEqual(type(form.fields["date_from"].widget), forms.PasswordInput)

    def test_get_form_for_model_with_widget_overides_by_instance(self):
        EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["date_to", "date_from"],
            widgets={"date_from": forms.PasswordInput()},
        )
        form = EventPageForm()

        self.assertEqual(type(form.fields["date_from"]), forms.DateField)
        self.assertEqual(type(form.fields["date_from"].widget), forms.PasswordInput)

    def test_tag_widget_is_passed_tag_model(self):
        RestaurantPageForm = get_form_for_model(
            RestaurantPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "tags"],
        )
        form_html = RestaurantPageForm().as_p()
        self.assertIn("/admin/tag\\u002Dautocomplete/tests/restauranttag/", form_html)

        # widget should pick up the free_tagging=False attribute on the tag model
        # and set itself to autocomplete only
        self.assertIn('"autocompleteOnly": true', form_html)

        # Free tagging should also be disabled at the form field validation level
        RestaurantTag.objects.create(name="Italian", slug="italian")
        RestaurantTag.objects.create(name="Indian", slug="indian")

        form = RestaurantPageForm(
            {
                "title": "Buonasera",
                "slug": "buonasera",
                "tags": "Italian, delicious",
            }
        )
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["tags"], ["Italian"])


def clear_edit_handler(page_cls):
    def decorator(fn):
        @wraps(fn)
        def decorated(*args, **kwargs):
            # Clear any old panel definitions generated
            page_cls.get_edit_handler.cache_clear()
            try:
                fn(*args, **kwargs)
            finally:
                # Clear the bad panel definition generated just now
                page_cls.get_edit_handler.cache_clear()

        return decorated

    return decorator


class TestPageEditHandlers(TestCase):
    @clear_edit_handler(EventPage)
    def test_get_edit_handler(self):
        """
        Forms for pages should have a base class of WagtailAdminPageForm.
        """
        edit_handler = EventPage.get_edit_handler()
        EventPageForm = edit_handler.get_form_class()

        # The generated form should inherit from WagtailAdminPageForm
        self.assertTrue(issubclass(EventPageForm, WagtailAdminPageForm))

    @clear_edit_handler(ValidatedPage)
    def test_get_form_for_page_with_custom_base(self):
        """
        ValidatedPage sets a custom base_form_class. This should be used as the
        base class when constructing a form for ValidatedPages
        """
        edit_handler = ValidatedPage.get_edit_handler()
        GeneratedValidatedPageForm = edit_handler.get_form_class()

        # The generated form should inherit from ValidatedPageForm, because
        # ValidatedPage.base_form_class == ValidatedPageForm
        self.assertTrue(issubclass(GeneratedValidatedPageForm, ValidatedPageForm))

    @clear_edit_handler(ValidatedPage)
    def test_check_invalid_base_form_class(self):
        class BadFormClass:
            pass

        invalid_base_form = checks.Error(
            "ValidatedPage.base_form_class does not extend WagtailAdminPageForm",
            hint="Ensure that wagtail.admin.tests.test_edit_handlers.BadFormClass extends WagtailAdminPageForm",
            obj=ValidatedPage,
            id="wagtailadmin.E001",
        )

        invalid_edit_handler = checks.Error(
            "ValidatedPage.get_edit_handler().get_form_class() does not extend WagtailAdminPageForm",
            hint="Ensure that the panel definition for ValidatedPage creates a subclass of WagtailAdminPageForm",
            obj=ValidatedPage,
            id="wagtailadmin.E002",
        )

        with mock.patch.object(ValidatedPage, "base_form_class", new=BadFormClass):
            errors = checks.run_checks()

            # Only look at errors (e.g. ignore warnings about CSS not being built)
            errors = [e for e in errors if e.level >= checks.ERROR]

            # Errors may appear out of order, so sort them by id
            errors.sort(key=lambda e: e.id)

            self.assertEqual(errors, [invalid_base_form, invalid_edit_handler])

    @clear_edit_handler(ValidatedPage)
    def test_custom_edit_handler_form_class(self):
        """
        Set a custom edit handler on a Page class, but dont customise
        ValidatedPage.base_form_class, or provide a custom form class for the
        edit handler. Check the generated form class is of the correct type.
        """
        ValidatedPage.edit_handler = TabbedInterface()
        with mock.patch.object(
            ValidatedPage, "edit_handler", new=TabbedInterface(), create=True
        ):
            form_class = ValidatedPage.get_edit_handler().get_form_class()
            self.assertTrue(issubclass(form_class, WagtailAdminPageForm))
            errors = ValidatedPage.check()
            self.assertEqual(errors, [])

    @clear_edit_handler(ValidatedPage)
    def test_repr(self):
        edit_handler = ValidatedPage.get_edit_handler()

        handler_repr = repr(edit_handler)

        self.assertIn(
            "model=<class 'wagtail.test.testapp.models.ValidatedPage'>",
            handler_repr,
        )

        bound_handler = edit_handler.get_bound_panel(
            instance=None, request=None, form=None
        )
        bound_handler_repr = repr(bound_handler)
        self.assertIn(
            "model=<class 'wagtail.test.testapp.models.ValidatedPage'>",
            bound_handler_repr,
        )

        self.assertIn("instance=None", bound_handler_repr)
        self.assertIn("request=None", bound_handler_repr)
        self.assertIn("form=None", bound_handler_repr)


class TestExtractPanelDefinitionsFromModelClass(TestCase):
    def test_can_extract_panel_property(self):
        # A class with a 'panels' property defined should return that list
        result = extract_panel_definitions_from_model_class(EventPageSpeaker)
        self.assertEqual(len(result), 5)
        self.assertTrue(any([isinstance(panel, MultiFieldPanel) for panel in result]))

    def test_exclude(self):
        panels = extract_panel_definitions_from_model_class(Site, exclude=["hostname"])
        for panel in panels:
            self.assertNotEqual(panel.field_name, "hostname")

    def test_can_build_panel_list(self):
        # EventPage has no 'panels' definition, so one should be derived from the field list
        panels = extract_panel_definitions_from_model_class(EventPage)

        self.assertTrue(
            any(
                [
                    isinstance(panel, FieldPanel) and panel.field_name == "date_from"
                    for panel in panels
                ]
            )
        )


class TestTabbedInterface(TestCase, WagtailTestUtils):
    def setUp(self):
        self.request = RequestFactory().get("/")
        user = self.create_superuser(username="admin")
        self.request.user = user
        self.user = self.login()
        # a custom tabbed interface for EventPage
        self.event_page_tabbed_interface = TabbedInterface(
            [
                ObjectList(
                    [
                        FieldPanel("title", widget=forms.Textarea),
                        FieldPanel("date_from"),
                        FieldPanel("date_to"),
                    ],
                    heading="Event details",
                    classname="shiny",
                ),
                ObjectList(
                    [
                        InlinePanel("speakers", label="Speakers"),
                    ],
                    heading="Speakers",
                ),
                ObjectList(
                    [
                        FieldPanel("cost", permission="superuser"),
                    ],
                    heading="Secret",
                ),
            ]
        ).bind_to_model(EventPage)

    def test_get_form_class(self):
        EventPageForm = self.event_page_tabbed_interface.get_form_class()
        form = EventPageForm()

        # form must include the 'speakers' formset required by the speakers InlinePanel
        self.assertIn("speakers", form.formsets)

        # form must respect any overridden widgets
        self.assertEqual(type(form.fields["title"].widget), forms.Textarea)

    def test_render(self):
        EventPageForm = self.event_page_tabbed_interface.get_form_class()
        event = EventPage(title="Abergavenny sheepdog trials")
        form = EventPageForm(instance=event)

        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
            instance=event,
            form=form,
            request=self.request,
        )

        result = tabbed_interface.render_html()

        # result should contain tab buttons
        self.assertIn(
            '<a id="tab-label-event-details" href="#tab-event-details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
            result,
        )
        self.assertIn(
            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
            result,
        )

        # result should contain tab panels
        self.assertIn('aria-labelledby="tab-label-event-details"', result)
        self.assertIn('aria-labelledby="tab-label-speakers"', result)

        # result should contain rendered content from descendants
        self.assertIn("Abergavenny sheepdog trials</textarea>", result)

        # this result should not include fields that are not covered by the panel definition
        self.assertNotIn("signup_link", result)

    def test_required_fields(self):
        # get_form_options should report the set of form fields to be rendered recursively by children of TabbedInterface
        result = set(self.event_page_tabbed_interface.get_form_options()["fields"])
        self.assertEqual(result, {"title", "date_from", "date_to", "cost"})

    def test_render_form_content(self):
        EventPageForm = self.event_page_tabbed_interface.get_form_class()
        event = EventPage(title="Abergavenny sheepdog trials")
        form = EventPageForm(instance=event)

        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
            instance=event,
            form=form,
            request=self.request,
        )

        result = tabbed_interface.render_form_content()
        # rendered output should contain field content as above
        self.assertIn("Abergavenny sheepdog trials</textarea>", result)
        # rendered output should NOT include fields that are in the model but not represented
        # in the panel definition
        self.assertNotIn("signup_link", result)

    def test_tabs_permissions(self):
        """
        test that three tabs show when the current user has permission to see all three
        test that two tabs show when the current user does not have permission to see all three
        """

        EventPageForm = self.event_page_tabbed_interface.get_form_class()
        event = EventPage(title="Abergavenny sheepdog trials")
        form = EventPageForm(instance=event)

        # when signed in as a superuser all three tabs should be visible
        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
            instance=event,
            form=form,
            request=self.request,
        )
        result = tabbed_interface.render_html()
        self.assertIn(
            '<a id="tab-label-event-details" href="#tab-event-details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
            result,
        )
        self.assertIn(
            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
            result,
        )
        self.assertIn(
            '<a id="tab-label-secret" href="#tab-secret" ',
            result,
        )

        # Login as non superuser to check that the third tab does not show
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user
        tabbed_interface = self.event_page_tabbed_interface.get_bound_panel(
            instance=event,
            form=form,
            request=self.request,
        )
        result = tabbed_interface.render_html()
        self.assertIn(
            '<a id="tab-label-event-details" href="#tab-event-details" class="w-tabs__tab shiny" role="tab" aria-selected="false" tabindex="-1">',
            result,
        )
        self.assertIn(
            '<a id="tab-label-speakers" href="#tab-speakers" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
            result,
        )
        self.assertNotIn(
            '<a id="tab-label-secret" href="#tab-secret" ',
            result,
        )


class TestObjectList(TestCase):
    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user
        # a custom ObjectList for EventPage
        self.event_page_object_list = ObjectList(
            [
                FieldPanel("title", widget=forms.Textarea),
                FieldPanel("date_from"),
                FieldPanel("date_to"),
                InlinePanel("speakers", label="Speakers"),
            ],
            heading="Event details",
            classname="shiny",
        ).bind_to_model(EventPage)

    def test_get_form_class(self):
        EventPageForm = self.event_page_object_list.get_form_class()
        form = EventPageForm()

        # form must include the 'speakers' formset required by the speakers InlinePanel
        self.assertIn("speakers", form.formsets)

        # form must respect any overridden widgets
        self.assertEqual(type(form.fields["title"].widget), forms.Textarea)

    def test_render(self):
        EventPageForm = self.event_page_object_list.get_form_class()
        event = EventPage(title="Abergavenny sheepdog trials")
        form = EventPageForm(instance=event)

        object_list = self.event_page_object_list.get_bound_panel(
            instance=event,
            form=form,
            request=self.request,
        )

        result = object_list.render_html()

        # result should contain ObjectList furniture
        self.assertIn('<ul class="objects">', result)

        # result should contain labels for children
        self.assertInHTML('<label for="id_date_from">Start date</label>', result)

        # result should include help text for children
        self.assertInHTML(
            '<div class="object-help help"> <svg class="icon icon-help default" aria-hidden="true"><use href="#icon-help"></use></svg> Not required if event is on a single day</div>',
            result,
        )

        # result should contain rendered content from descendants
        self.assertIn("Abergavenny sheepdog trials</textarea>", result)

        # this result should not include fields that are not covered by the panel definition
        self.assertNotIn("signup_link", result)


class TestFieldPanel(TestCase):
    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user

        self.EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "date_from", "date_to"],
            formsets=[],
        )
        self.event = EventPage(
            title="Abergavenny sheepdog trials",
            date_from=date(2014, 7, 20),
            date_to=date(2014, 7, 21),
        )

        self.end_date_panel = FieldPanel(
            "date_to", classname="full-width"
        ).bind_to_model(EventPage)

    def test_non_model_field(self):
        # defining a FieldPanel for a field which isn't part of a model is OK,
        # because it might be defined on the form instead
        field_panel = FieldPanel("barbecue").bind_to_model(Page)

        # however, accessing db_field will fail
        with self.assertRaises(FieldDoesNotExist):
            field_panel.db_field

    def test_override_heading(self):
        # unless heading is specified in keyword arguments, an edit handler with bound form should take its
        # heading from the bound field label
        bound_panel = self.end_date_panel.get_bound_panel(
            form=self.EventPageForm(), request=self.request, instance=self.event
        )
        self.assertEqual(bound_panel.heading, bound_panel.bound_field.label)

        # if heading is explicitly provided to constructor, that heading should be taken in
        # preference to the field's label
        end_date_panel_with_overridden_heading = FieldPanel(
            "date_to", classname="full-width", heading="New heading"
        ).bind_to_model(EventPage)
        end_date_panel_with_overridden_heading = (
            end_date_panel_with_overridden_heading.get_bound_panel(
                request=self.request, form=self.EventPageForm(), instance=self.event
            )
        )
        self.assertEqual(end_date_panel_with_overridden_heading.heading, "New heading")
        self.assertEqual(
            end_date_panel_with_overridden_heading.bound_field.label, "New heading"
        )

    def test_render_as_object(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-22",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.end_date_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_object()

        # check that label appears as a legend in the 'object' wrapper,
        # but not as a field label (that would be provided by ObjectList instead)
        self.assertIn("<legend>End date</legend>", result)
        self.assertNotIn('<label for="id_date_to">End date:</label>', result)

        # check that help text is not included (it's provided by ObjectList instead)
        self.assertNotIn("Not required if event is on a single day", result)

        # check that the populated form field is included
        self.assertIn('value="2014-07-22"', result)

        # there should be no errors on this field
        self.assertNotIn('<p class="error-message">', result)

    def test_render_as_field(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-22",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.end_date_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_field()

        # check that label is output in the 'field' style
        self.assertIn('<label for="id_date_to">End date:</label>', result)
        self.assertNotIn("<legend>End date</legend>", result)

        # check that help text is included
        self.assertIn("Not required if event is on a single day", result)

        # check that the populated form field is included
        self.assertIn('value="2014-07-22"', result)

        # there should be no errors on this field
        self.assertNotIn('<p class="error-message">', result)

    def test_required_fields(self):
        result = self.end_date_panel.get_form_options()["fields"]
        self.assertEqual(result, ["date_to"])

    def test_error_message_is_rendered(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-33",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.end_date_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_field()

        self.assertIn('<p class="error-message">', result)
        self.assertIn("<span>Enter a valid date.</span>", result)

    def test_repr(self):
        form = self.EventPageForm()
        field_panel = self.end_date_panel.get_bound_panel(
            form=form,
            instance=self.event,
            request=self.request,
        )

        field_panel_repr = repr(field_panel)

        self.assertIn(
            "model=<class 'wagtail.test.testapp.models.EventPage'>", field_panel_repr
        )
        self.assertIn("instance=Abergavenny sheepdog trials", field_panel_repr)
        self.assertIn("request=<WSGIRequest: GET '/'>", field_panel_repr)
        self.assertIn("form=EventPageForm", field_panel_repr)


class TestFieldRowPanel(TestCase):
    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user

        self.EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "date_from", "date_to"],
            formsets=[],
        )
        self.event = EventPage(
            title="Abergavenny sheepdog trials",
            date_from=date(2014, 7, 20),
            date_to=date(2014, 7, 21),
        )

        self.dates_panel = FieldRowPanel(
            [
                FieldPanel("date_from", classname="col4", heading="Start"),
                FieldPanel("date_to", classname="coltwo"),
            ]
        ).bind_to_model(EventPage)

    def test_render_as_object(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-22",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_object()

        # check that the populated form field is included
        self.assertIn('value="2014-07-22"', result)

        # there should be no errors on this field
        self.assertNotIn('<p class="error-message">', result)

    def test_render_as_field(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-22",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_field()

        # check that label is output in the 'field' style
        self.assertIn('<label for="id_date_to">End date:</label>', result)
        self.assertNotIn("<legend>End date</legend>", result)

        # check that label is overridden with the 'heading' argument
        self.assertIn('<label for="id_date_from">Start:</label>', result)

        # check that help text is included
        self.assertIn("Not required if event is on a single day", result)

        # check that the populated form field is included
        self.assertIn('value="2014-07-22"', result)

        # there should be no errors on this field
        self.assertNotIn('<p class="error-message">', result)

    def test_error_message_is_rendered(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-33",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_field()

        self.assertIn('<p class="error-message">', result)
        self.assertIn("<span>Enter a valid date.</span>", result)

    def test_add_col_when_wrong_in_panel_def(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-33",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )

        result = field_panel.render_as_field()

        self.assertIn('<li class="field-col coltwo error date_field col6">', result)

    def test_added_col_doesnt_change_siblings(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-33",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )

        result = field_panel.render_as_field()

        self.assertIn('<li class="field-col col4', result)


class TestFieldRowPanelWithChooser(TestCase):
    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user

        self.EventPageForm = get_form_for_model(
            EventPage,
            form_class=WagtailAdminPageForm,
            fields=["title", "slug", "date_from", "date_to"],
            formsets=[],
        )
        self.event = EventPage(
            title="Abergavenny sheepdog trials",
            date_from=date(2014, 7, 19),
            date_to=date(2014, 7, 21),
        )

        self.dates_panel = FieldRowPanel(
            [
                FieldPanel("date_from"),
                FieldPanel("feed_image"),
            ]
        ).bind_to_model(EventPage)

    def test_render_as_object(self):
        form = self.EventPageForm(
            {
                "title": "Pontypridd sheepdog trials",
                "date_from": "2014-07-20",
                "date_to": "2014-07-22",
            },
            instance=self.event,
        )

        form.is_valid()

        field_panel = self.dates_panel.get_bound_panel(
            instance=self.event,
            form=form,
            request=self.request,
        )
        result = field_panel.render_as_object()

        # check that the populated form field is included
        self.assertIn('value="2014-07-20"', result)

        # there should be no errors on this field
        self.assertNotIn('<p class="error-message">', result)


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

    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user

        model = PageChooserModel  # a model with a foreign key to Page which we want to render as a page chooser

        # a PageChooserPanel class that works on PageChooserModel's 'page' field
        self.edit_handler = ObjectList([PageChooserPanel("page")]).bind_to_model(
            PageChooserModel
        )
        self.my_page_chooser_panel = self.edit_handler.children[0]

        # build a form class containing the fields that MyPageChooserPanel wants
        self.PageChooserForm = self.edit_handler.get_form_class()

        # a test instance of PageChooserModel, pointing to the 'christmas' page
        self.christmas_page = Page.objects.get(slug="christmas")
        self.events_index_page = Page.objects.get(slug="events")
        self.test_instance = model.objects.create(page=self.christmas_page)

        self.form = self.PageChooserForm(instance=self.test_instance)
        self.page_chooser_panel = self.my_page_chooser_panel.get_bound_panel(
            instance=self.test_instance, form=self.form, request=self.request
        )

    def test_page_chooser_uses_correct_widget(self):
        self.assertEqual(type(self.form.fields["page"].widget), AdminPageChooser)

    def test_render_js_init(self):
        result = self.page_chooser_panel.render_as_field()
        expected_js = 'createPageChooser("{id}", {parent}, {{"model_names": ["{model}"], "can_choose_root": false, "user_perms": null}});'.format(
            id="id_page", model="wagtailcore.page", parent=self.events_index_page.id
        )

        self.assertIn(expected_js, result)

    def test_render_js_init_with_can_choose_root_true(self):
        # construct an alternative page chooser panel object, with can_choose_root=True

        my_page_object_list = ObjectList(
            [PageChooserPanel("page", can_choose_root=True)]
        ).bind_to_model(PageChooserModel)
        my_page_chooser_panel = my_page_object_list.children[0]
        PageChooserForm = my_page_object_list.get_form_class()

        form = PageChooserForm(instance=self.test_instance)
        page_chooser_panel = my_page_chooser_panel.get_bound_panel(
            instance=self.test_instance, form=form, request=self.request
        )
        result = page_chooser_panel.render_as_field()

        # the canChooseRoot flag on createPageChooser should now be true
        expected_js = 'createPageChooser("{id}", {parent}, {{"model_names": ["{model}"], "can_choose_root": true, "user_perms": null}});'.format(
            id="id_page", model="wagtailcore.page", parent=self.events_index_page.id
        )
        self.assertIn(expected_js, result)

    def test_render_as_field(self):
        result = self.page_chooser_panel.render_as_field()
        self.assertIn('<p class="help">help text</p>', result)
        self.assertIn('<span class="title">Christmas</span>', result)
        self.assertIn(
            '<a href="/admin/pages/%d/edit/" class="edit-link button button-small button-secondary" target="_blank" rel="noreferrer">'
            "Edit this page</a>" % self.christmas_page.id,
            result,
        )

    def test_render_as_empty_field(self):
        test_instance = PageChooserModel()
        form = self.PageChooserForm(instance=test_instance)
        page_chooser_panel = self.my_page_chooser_panel.get_bound_panel(
            instance=test_instance, form=form, request=self.request
        )
        result = page_chooser_panel.render_as_field()

        self.assertIn('<p class="help">help text</p>', result)
        self.assertIn('<span class="title"></span>', result)
        self.assertIn("Choose a page", result)

    def test_render_error(self):
        form = self.PageChooserForm({"page": ""}, instance=self.test_instance)
        self.assertFalse(form.is_valid())

        page_chooser_panel = self.my_page_chooser_panel.get_bound_panel(
            instance=self.test_instance, form=form, request=self.request
        )
        self.assertIn(
            "<span>This field is required.</span>", page_chooser_panel.render_as_field()
        )

    def test_override_page_type(self):
        # Model has a foreign key to Page, but we specify EventPage in the PageChooserPanel
        # to restrict the chooser to that page type
        my_page_object_list = ObjectList(
            [PageChooserPanel("page", "tests.EventPage")]
        ).bind_to_model(EventPageChooserModel)
        my_page_chooser_panel = my_page_object_list.children[0]
        PageChooserForm = my_page_object_list.get_form_class()
        form = PageChooserForm(instance=self.test_instance)
        page_chooser_panel = my_page_chooser_panel.get_bound_panel(
            instance=self.test_instance, form=form, request=self.request
        )

        result = page_chooser_panel.render_as_field()
        expected_js = 'createPageChooser("{id}", {parent}, {{"model_names": ["{model}"], "can_choose_root": false, "user_perms": null}});'.format(
            id="id_page", model="tests.eventpage", parent=self.events_index_page.id
        )

        self.assertIn(expected_js, result)

    def test_autodetect_page_type(self):
        # Model has a foreign key to EventPage, which we want to autodetect
        # instead of specifying the page type in PageChooserPanel
        my_page_object_list = ObjectList([PageChooserPanel("page")]).bind_to_model(
            EventPageChooserModel,
        )
        my_page_chooser_panel = my_page_object_list.children[0]
        PageChooserForm = my_page_object_list.get_form_class()
        form = PageChooserForm(instance=self.test_instance)
        page_chooser_panel = my_page_chooser_panel.get_bound_panel(
            instance=self.test_instance, form=form, request=self.request
        )

        result = page_chooser_panel.render_as_field()
        expected_js = 'createPageChooser("{id}", {parent}, {{"model_names": ["{model}"], "can_choose_root": false, "user_perms": null}});'.format(
            id="id_page", model="tests.eventpage", parent=self.events_index_page.id
        )

        self.assertIn(expected_js, result)

    def test_target_models(self):
        panel = PageChooserPanel("page", "wagtailcore.site").bind_to_model(
            PageChooserModel
        )
        widget = panel.get_form_options()["widgets"]["page"]
        self.assertEqual(widget.target_models, [Site])

    def test_target_models_malformed_type(self):
        panel = PageChooserPanel("page", "snowman").bind_to_model(PageChooserModel)
        self.assertRaises(ImproperlyConfigured, panel.get_form_options)

    def test_target_models_nonexistent_type(self):
        panel = PageChooserPanel("page", "snowman.lorry").bind_to_model(
            PageChooserModel
        )
        self.assertRaises(ImproperlyConfigured, panel.get_form_options)


class TestInlinePanel(TestCase, WagtailTestUtils):
    fixtures = ["test.json"]

    def setUp(self):
        self.request = RequestFactory().get("/")
        user = AnonymousUser()  # technically, Anonymous users cannot access the admin
        self.request.user = user

    def test_render(self):
        """
        Check that the inline panel renders the panels set on the model
        when no 'panels' parameter is passed in the InlinePanel definition
        """
        speaker_object_list = ObjectList(
            [
                InlinePanel(
                    "speakers", label="Speakers", classname="classname-for-speakers"
                )
            ]
        ).bind_to_model(EventPage)
        EventPageForm = speaker_object_list.get_form_class()

        # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset
        self.assertEqual(["speakers"], list(EventPageForm.formsets.keys()))

        event_page = EventPage.objects.get(slug="christmas")

        form = EventPageForm(instance=event_page)
        panel = speaker_object_list.get_bound_panel(
            instance=event_page, form=form, request=self.request
        )

        result = panel.render_as_field()

        self.assertIn('<li class="object classname-for-speakers">', result)
        self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result)
        self.assertIn('value="Father"', result)
        self.assertIn('<label for="id_speakers-0-last_name">Surname:</label>', result)
        self.assertIn('<label for="id_speakers-0-image">Image:</label>', result)
        self.assertIn("Choose an image", result)

        # rendered panel must also contain hidden fields for id, DELETE and ORDER
        self.assertTagInHTML(
            '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">',
            result,
            allow_extra_attrs=True,
        )
        self.assertTagInHTML(
            '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">',
            result,
            allow_extra_attrs=True,
        )
        self.assertTagInHTML(
            '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">',
            result,
            allow_extra_attrs=True,
        )

        # rendered panel must contain maintenance form for the formset
        self.assertTagInHTML(
            '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">',
            result,
            allow_extra_attrs=True,
        )

        # rendered panel must include the JS initializer
        self.assertIn("var panel = InlinePanel({", result)

    def test_render_with_panel_overrides(self):
        """
        Check that inline panel renders the panels listed in the InlinePanel definition
        where one is specified
        """
        speaker_object_list = ObjectList(
            [
                InlinePanel(
                    "speakers",
                    label="Speakers",
                    panels=[
                        FieldPanel("first_name", widget=forms.Textarea),
                        FieldPanel("image"),
                    ],
                ),
            ]
        ).bind_to_model(EventPage)
        speaker_inline_panel = speaker_object_list.children[0]
        EventPageForm = speaker_object_list.get_form_class()

        # speaker_inline_panel should instruct the form class to include a 'speakers' formset
        self.assertEqual(["speakers"], list(EventPageForm.formsets.keys()))

        event_page = EventPage.objects.get(slug="christmas")

        form = EventPageForm(instance=event_page)
        panel = speaker_inline_panel.get_bound_panel(
            instance=event_page, form=form, request=self.request
        )

        result = panel.render_as_field()

        # rendered panel should contain first_name rendered as a text area, but no last_name field
        self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result)
        self.assertIn("Father</textarea>", result)
        self.assertNotIn(
            '<label for="id_speakers-0-last_name">Surname:</label>', result
        )

        # test for #338: surname field should not be rendered as a 'stray' label-less field
        self.assertTagInHTML(
            '<input id="id_speakers-0-last_name">',
            result,
            count=0,
            allow_extra_attrs=True,
        )

        self.assertIn('<label for="id_speakers-0-image">Image:</label>', result)
        self.assertIn("Choose an image", result)

        # rendered panel must also contain hidden fields for id, DELETE and ORDER
        self.assertTagInHTML(
            '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">',
            result,
            allow_extra_attrs=True,
        )
        self.assertTagInHTML(
            '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">',
            result,
            allow_extra_attrs=True,
        )
        self.assertTagInHTML(
            '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">',
            result,
            allow_extra_attrs=True,
        )

        # rendered panel must contain maintenance form for the formset
        self.assertTagInHTML(
            '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">',
            result,
            allow_extra_attrs=True,
        )

        # render_js_init must provide the JS initializer
        self.assertIn("var panel = InlinePanel({", panel.render_html())

    @override_settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True)
    def test_no_thousand_separators_in_js(self):
        """
        Test that the USE_THOUSAND_SEPARATOR setting does not screw up the rendering of numbers
        (specifically maxForms=1000) in the JS initializer:
        https://github.com/wagtail/wagtail/pull/2699
        https://github.com/wagtail/wagtail/issues/3227
        """
        speaker_object_list = ObjectList(
            [
                InlinePanel(
                    "speakers",
                    label="Speakers",
                    panels=[
                        FieldPanel("first_name", widget=forms.Textarea),
                        FieldPanel("image"),
                    ],
                ),
            ]
        ).bind_to_model(EventPage)
        speaker_inline_panel = speaker_object_list.children[0]
        EventPageForm = speaker_object_list.get_form_class()
        event_page = EventPage.objects.get(slug="christmas")
        form = EventPageForm(instance=event_page)
        panel = speaker_inline_panel.get_bound_panel(
            instance=event_page, form=form, request=self.request
        )

        self.assertIn("maxForms: 1000", panel.render_html())

    def test_invalid_inlinepanel_declaration(self):
        with self.ignore_deprecation_warnings():
            self.assertRaises(TypeError, lambda: InlinePanel(label="Speakers"))
            self.assertRaises(
                TypeError,
                lambda: InlinePanel(
                    EventPage, "speakers", label="Speakers", bacon="chunky"
                ),
            )


class TestInlinePanelRelatedModelPanelConfigChecks(TestCase):
    def setUp(self):
        self.original_panels = EventPageSpeaker.panels
        delattr(EventPageSpeaker, "panels")

        def get_checks_result():
            # run checks only with the 'panels' tag
            checks_result = checks.run_checks(tags=["panels"])
            return [
                warning for warning in checks_result if warning.obj == EventPageSpeaker
            ]

        self.warning_id = "wagtailadmin.W002"
        self.get_checks_result = get_checks_result

    def tearDown(self):
        EventPageSpeaker.panels = self.original_panels

    def test_page_with_inline_model_with_tabbed_panel_only(self):
        """Test that checks will warn against setting single tabbed panel on InlinePanel model"""

        EventPageSpeaker.settings_panels = [
            FieldPanel("first_name"),
            FieldPanel("last_name"),
        ]

        warning = checks.Warning(
            "EventPageSpeaker.settings_panels will have no effect on InlinePanel model editing",
            hint="""Ensure that EventPageSpeaker uses `panels` instead of `settings_panels`.
There are no tabs on non-Page model editing within InlinePanels.""",
            obj=EventPageSpeaker,
            id=self.warning_id,
        )

        checks_results = self.get_checks_result()

        self.assertIn(warning, checks_results)

        delattr(EventPageSpeaker, "settings_panels")

    def test_page_with_inline_model_with_two_tabbed_panels(self):
        """Test that checks will warn against multiple tabbed panels on InlinePanel models"""

        EventPageSpeaker.content_panels = [FieldPanel("first_name")]
        EventPageSpeaker.promote_panels = [FieldPanel("last_name")]

        warning_1 = checks.Warning(
            "EventPageSpeaker.content_panels will have no effect on InlinePanel model editing",
            hint="""Ensure that EventPageSpeaker uses `panels` instead of `content_panels`.
There are no tabs on non-Page model editing within InlinePanels.""",
            obj=EventPageSpeaker,
            id=self.warning_id,
        )
        warning_2 = checks.Warning(
            "EventPageSpeaker.promote_panels will have no effect on InlinePanel model editing",
            hint="""Ensure that EventPageSpeaker uses `panels` instead of `promote_panels`.
There are no tabs on non-Page model editing within InlinePanels.""",
            obj=EventPageSpeaker,
            id=self.warning_id,
        )

        checks_results = self.get_checks_result()

        self.assertIn(warning_1, checks_results)
        self.assertIn(warning_2, checks_results)

        delattr(EventPageSpeaker, "content_panels")
        delattr(EventPageSpeaker, "promote_panels")

    def test_page_with_inline_model_with_edit_handler(self):
        """Checks should NOT warn if InlinePanel models use tabbed panels AND edit_handler"""

        EventPageSpeaker.content_panels = [FieldPanel("first_name")]
        EventPageSpeaker.edit_handler = TabbedInterface(
            [ObjectList([FieldPanel("last_name")], heading="test")]
        )

        # should not be any errors
        self.assertEqual(self.get_checks_result(), [])

        # clean up for future checks
        delattr(EventPageSpeaker, "edit_handler")
        delattr(EventPageSpeaker, "content_panels")


class TestCommentPanel(TestCase, WagtailTestUtils):
    fixtures = ["test.json"]

    def setUp(self):
        self.commenting_user = get_user_model().objects.get(pk=7)
        self.other_user = get_user_model().objects.get(pk=6)
        self.request = RequestFactory().get("/")
        self.request.user = self.commenting_user

        unbound_object_list = ObjectList([CommentPanel()])
        self.object_list = unbound_object_list.bind_to_model(EventPage)
        self.tabbed_interface = TabbedInterface([unbound_object_list]).bind_to_model(
            EventPage
        )

        self.EventPageForm = self.object_list.get_form_class()
        self.event_page = EventPage.objects.get(slug="christmas")
        self.comment = Comment.objects.create(
            page=self.event_page,
            text="test",
            user=self.other_user,
            contentpath="test_contentpath",
        )
        self.reply_1 = CommentReply.objects.create(
            comment=self.comment, text="reply_1", user=self.other_user
        )
        self.reply_2 = CommentReply.objects.create(
            comment=self.comment, text="reply_2", user=self.commenting_user
        )

    def test_comments_toggle_enabled(self):
        """
        Test that the comments toggle is enabled for a TabbedInterface containing CommentPanel, and disabled otherwise
        """
        form_class = self.tabbed_interface.get_form_class()
        form = form_class()
        self.assertTrue(form.show_comments_toggle)

        tabbed_interface_without_content_panel = TabbedInterface(
            [ObjectList(self.event_page.content_panels)]
        ).bind_to_model(EventPage)
        form_class = tabbed_interface_without_content_panel.get_form_class()
        form = form_class()
        self.assertFalse(form.show_comments_toggle)

    @override_settings(WAGTAILADMIN_COMMENTS_ENABLED=False)
    def test_comments_disabled_setting(self):
        """
        Test that the comment panel is missing if WAGTAILADMIN_COMMENTS_ENABLED=False
        """
        self.assertFalse(
            any(isinstance(panel, CommentPanel) for panel in Page.settings_panels)
        )
        form_class = Page.get_edit_handler().get_form_class()
        form = form_class()
        self.assertFalse(form.show_comments_toggle)

    def test_comments_enabled_setting(self):
        """
        Test that the comment panel is present by default
        """
        self.assertTrue(
            any(isinstance(panel, CommentPanel) for panel in Page.settings_panels)
        )
        form_class = Page.get_edit_handler().get_form_class()
        form = form_class()
        self.assertTrue(form.show_comments_toggle)

    def test_context(self):
        """
        Test that the context contains the data about existing comments necessary to initialize the commenting app
        """
        form = self.EventPageForm(instance=self.event_page)
        panel = self.object_list.get_bound_panel(
            request=self.request, instance=self.event_page, form=form
        ).children[0]
        data = panel.get_context_data()["comments_data"]

        self.assertEqual(data["user"], self.commenting_user.pk)

        self.assertEqual(len(data["comments"]), 1)
        self.assertEqual(data["comments"][0]["user"], self.comment.user.pk)

        self.assertEqual(len(data["comments"][0]["replies"]), 2)
        self.assertEqual(
            data["comments"][0]["replies"][0]["user"], self.reply_1.user.pk
        )
        self.assertEqual(
            data["comments"][0]["replies"][1]["user"], self.reply_2.user.pk
        )

        self.assertIn(str(self.commenting_user.pk), data["authors"])
        self.assertIn(str(self.other_user.pk), data["authors"])

        try:
            json_script(data, "comments-data")
        except TypeError:
            self.fail(
                "Failed to serialize comments data. This is likely due to a custom user model using an unsupported field."
            )

    def test_form(self):
        """
        Check that the form has the comments/replies formsets, and that the
        user has been set on each CommentForm/CommentReplyForm instance
        """
        form = self.EventPageForm(
            instance=self.event_page, for_user=self.commenting_user
        )

        self.assertIn("comments", form.formsets)

        comments_formset = form.formsets["comments"]
        self.assertEqual(len(comments_formset.forms), 1)
        self.assertEqual(comments_formset.forms[0].for_user, self.commenting_user)

        replies_formset = comments_formset.forms[0].formsets["replies"]
        self.assertEqual(len(replies_formset.forms), 2)
        self.assertEqual(replies_formset.forms[0].for_user, self.commenting_user)

    def test_comment_form_validation(self):

        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 2,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": "edited text",  # Try to edit an existing comment from another user
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-replies-TOTAL_FORMS": 0,
                "comments-0-replies-INITIAL_FORMS": 0,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
                "comments-1-id": "",
                "comments-1-text": "new comment",  # Add a new comment
                "comments-1-contentpath": "new.path",
                "comments-1-replies-TOTAL_FORMS": 0,
                "comments-1-replies-INITIAL_FORMS": 0,
                "comments-1-replies-MIN_NUM_FORMS": 0,
                "comments-1-replies-MAX_NUM_FORMS": 1000,
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )

        comment_form = form.formsets["comments"].forms[0]
        self.assertFalse(comment_form.is_valid())
        # The existing comment was from another user, so should not be editable

        comment_form = form.formsets["comments"].forms[1]
        self.assertTrue(comment_form.is_valid())
        self.assertEqual(comment_form.instance.user, self.commenting_user)
        # The commenting user should be able to add a new comment, and the new comment's user should be set to request.user

        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 1,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": self.comment.text,
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-DELETE": 1,  # Try to delete a comment from another user
                "comments-0-replies-TOTAL_FORMS": 0,
                "comments-0-replies-INITIAL_FORMS": 0,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )

        comment_form = form.formsets["comments"].forms[0]
        self.assertFalse(comment_form.is_valid())
        # Users cannot delete comments from other users

    def test_users_can_edit_comment_positions(self):
        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 1,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": self.comment.text,
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-position": "a_new_position",  # Try to change the position of a comment
                "comments-0-DELETE": 0,
                "comments-0-replies-TOTAL_FORMS": 0,
                "comments-0-replies-INITIAL_FORMS": 0,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )

        comment_form = form.formsets["comments"].forms[0]
        self.assertTrue(comment_form.is_valid())
        # Users can change the positions of other users' comments within a field
        # eg by editing a rich text field

    @freeze_time("2017-01-01 12:00:00")
    def test_comment_resolve(self):
        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 1,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": self.comment.text,
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-resolved": 1,
                "comments-0-replies-TOTAL_FORMS": 0,
                "comments-0-replies-INITIAL_FORMS": 0,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )
        comment_form = form.formsets["comments"].forms[0]
        self.assertTrue(comment_form.is_valid())
        comment_form.save()
        resolved_comment = Comment.objects.get(pk=self.comment.pk)
        self.assertEqual(resolved_comment.resolved_by, self.commenting_user)

        if settings.USE_TZ:
            self.assertEqual(
                resolved_comment.resolved_at, datetime(2017, 1, 1, 12, 0, 0, tzinfo=utc)
            )
        else:
            self.assertEqual(
                resolved_comment.resolved_at, datetime(2017, 1, 1, 12, 0, 0)
            )

    def test_comment_reply_form_validation(self):

        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 1,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": self.comment.text,
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-replies-TOTAL_FORMS": 3,
                "comments-0-replies-INITIAL_FORMS": 2,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
                "comments-0-replies-0-id": self.reply_1.pk,
                "comments-0-replies-0-text": "edited_text",  # Try to edit someone else's reply
                "comments-0-replies-1-id": self.reply_2.pk,
                "comments-0-replies-1-text": "Edited text 2",  # Try to edit own reply
                "comments-0-replies-2-id": "",  # Add new reply
                "comments-0-replies-2-text": "New reply",
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )

        comment_form = form.formsets["comments"].forms[0]

        reply_forms = comment_form.formsets["replies"].forms

        self.assertFalse(reply_forms[0].is_valid())
        # The existing reply was from another user, so should not be editable

        self.assertTrue(reply_forms[1].is_valid())
        # The existing reply was from the same user, so should be editable

        self.assertTrue(reply_forms[2].is_valid())
        self.assertEqual(reply_forms[2].instance.user, self.commenting_user)
        # Should be able to add new reply, and user should be set correctly

        form = self.EventPageForm(
            {
                "comments-TOTAL_FORMS": 1,
                "comments-INITIAL_FORMS": 1,
                "comments-MIN_NUM_FORMS": 0,
                "comments-MAX_NUM_FORMS": 1000,
                "comments-0-id": self.comment.pk,
                "comments-0-text": self.comment.text,
                "comments-0-contentpath": self.comment.contentpath,
                "comments-0-replies-TOTAL_FORMS": 2,
                "comments-0-replies-INITIAL_FORMS": 2,
                "comments-0-replies-MIN_NUM_FORMS": 0,
                "comments-0-replies-MAX_NUM_FORMS": 1000,
                "comments-0-replies-0-id": self.reply_1.pk,
                "comments-0-replies-0-text": self.reply_1.text,
                "comments-0-replies-0-DELETE": 1,  # Try to delete someone else's reply
                "comments-0-replies-1-id": self.reply_2.pk,
                "comments-0-replies-1-text": "Edited text 2",
                "comments-0-replies-1-DELETE": 1,  # Try to delete own reply
            },
            instance=self.event_page,
            for_user=self.commenting_user,
        )

        comment_form = form.formsets["comments"].forms[0]

        reply_forms = comment_form.formsets["replies"].forms

        self.assertFalse(reply_forms[0].is_valid())
        # The existing reply was from another user, so should not be deletable

        self.assertTrue(reply_forms[1].is_valid())
        # The existing reply was from the same user, so should be deletable
