Repository: wagtail-nest/wagtail-ab-testing (language: python)

Issue:
Support wagtail42
This updates the package for Wagtail 4.2

Drops: support for Wagtail < 4.1

For completeness I have merged in the following previous work:

- https://github.com/wagtail-nest/wagtail-ab-testing/pull/48
- https://github.com/wagtail-nest/wagtail-ab-testing/pull/49
Wagtail 3.0 compatibility
### Add support for Wagtail 3.0

Support Ticket: https://projects.torchbox.com/projects/support-team/tickets/463

This also includes Django 4.0 support

It would be good if the example screenshots could be updated. I wonder if @kaedroho might have the original db or fixtures to do that?

Tasks

- [x] Update docs
- [x] Update Change Log
Support wagtail42
This updates the package for Wagtail 4.2

Drops: support for Wagtail < 4.1

For completeness I have merged in the following previous work:

- https://github.com/wagtail-nest/wagtail-ab-testing/pull/48
- https://github.com/wagtail-nest/wagtail-ab-testing/pull/49
Support wagtail42
This updates the package for Wagtail 4.2

Drops: support for Wagtail < 4.1

For completeness I have merged in the following previous work:

- https://github.com/wagtail-nest/wagtail-ab-testing/pull/48
- https://github.com/wagtail-nest/wagtail-ab-testing/pull/49


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7494e2c..0f7ba5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ## [Unreleased]
 
+- Wagtail 4.2 compatibility
+- Wagtail 4.1 compatibility
+- NO SUPPORT for Wagtail 4.0 and older, sorry
+
 ## [0.7] - 2022-03-31
 
  - [Add default_auto_field](https://github.com/torchbox/wagtail-ab-testing/pull/42)
diff --git a/README.md b/README.md
index aa8025e..fb41fee 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,8 @@ Key features:
 
 ## Usage
 
+Wagtail A/B Testing works with Django 3.2+, Wagtail 4.1+ on Python 3.7+ environments.
+
 ### Creating an A/B test
 
 Any user with the "Create A/B test" permission can create an A/B test by clicking "Save and create A/B test" from the page's action menu.
@@ -72,7 +74,7 @@ from wagtail_ab_testing import urls as ab_testing_urls
 urlpatterns = [
     ...
 
-    url(r'^abtesting/', include(ab_testing_urls)),
+    path('abtesting/', include(ab_testing_urls)),
 ]
 ```
 
@@ -99,7 +101,7 @@ add your goal type to the list of options shown to users when they create A/B te
 ```python
 # myapp/wagtail_hooks.py
 
-from wagtail.core import hooks
+from wagtail import hooks
 from wagtail_ab_testing.events import BaseEvent
 
 
@@ -143,7 +145,7 @@ Firstly, we need to register the event type. To do this, implement a handler for
 ```python
 # myapp/wagtail_hooks.py
 
-from wagtail.core import hooks
+from wagtail import hooks
 from wagtail_ab_testing.events import BaseEvent
 
 from .models import ContactUsFormPage
diff --git a/setup.py b/setup.py
index 1fe2881..f251cae 100644
--- a/setup.py
+++ b/setup.py
@@ -33,22 +33,23 @@
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
         "Framework :: Django",
-        "Framework :: Django :: 2.2",
-        "Framework :: Django :: 3.0",
         "Framework :: Django :: 3.2",
+        "Framework :: Django :: 4.0",
+        "Framework :: Django :: 4.1",
         "Framework :: Wagtail",
-        "Framework :: Wagtail :: 2",
+        "Framework :: Wagtail :: 4",
     ],
     install_requires=[
-        "Django>=2.2,<3.3",
-        "Wagtail>=2.11,<2.17",
+        "Wagtail>=4.1",
         "user-agents>=2.2,<2.3",
-        "numpy>=1.19.4,<1.20",
-        "scipy>=1.5.4,<1.6",
+        "numpy>=1.19.4,<1.25",
+        "scipy>=1.5.4,<1.10",
     ],
     extras_require={
-        "testing": ["dj-database-url==0.5.0", "freezegun==0.3.15"],
+        "testing": ["dj-database-url==0.5.0", "freezegun==1.2.1"],
     },
     zip_safe=False,
 )
diff --git a/tox.ini b/tox.ini
index fdcf839..821b40e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,7 +2,9 @@
 skipsdist = True
 usedevelop = True
 
-envlist = py{37,38,39}-dj{22,30,32,main}-wa{211,215,216,main}-{sqlite,postgres}
+envlist = 
+    py{37,38,39,310}-dj{32,40,41}-wa{41,42}-{sqlite,postgres}
+    py{311}-dj{41}-wa{41,42,main}-{sqlite,postgres}
 
 [flake8]
 # E501: Line too long
@@ -17,26 +19,28 @@ commands = coverage run testmanage.py test --deprecation all
 basepython =
     py37: python3.7
     py38: python3.8
+    py39: python3.9
+    py310: python3.10
+    py311: python3.11
 
 deps =
     coverage
 
-    dj22: Django>=2.2,<2.3
-    dj30: Django>=3.0,<3.1
     dj32: Django>=3.2,<3.3
+    dj40: Django>=4.0,<4.1
+    dj41: Django>=4.1,<4.2
     djmain: git+https://github.com/django/django.git@main#egg=Django
 
-    wa211: wagtail>=2.11,<2.12
-    wa215: wagtail>=2.15rc1,<2.16
-    wa216: wagtail>=2.16.1,<2.17
+    wa41: wagtail>=4.1,<4.2
+    wa42: wagtail>=4.2,<5.0
     wamain: git+https://github.com/wagtail/wagtail.git
 
-    postgres: psycopg2==2.8.6
+    postgres: psycopg2==2.9.3
 
 setenv =
     postgres: DATABASE_URL=postgres:///wagtail_ab_testing
 
 [testenv:flake8]
-basepython=python3.7
+basepython=python3.11
 deps=flake8>=2.2.0
 commands=flake8 wagtail_ab_testing


Test patch:
diff --git a/testmanage.py b/testmanage.py
index 581d8e5..07d49a3 100644
--- a/testmanage.py
+++ b/testmanage.py
@@ -8,6 +8,8 @@
 
 from django.core.management import execute_from_command_line
 
+from wagtail.test.settings import STATIC_ROOT, MEDIA_ROOT
+
 
 os.environ["DJANGO_SETTINGS_MODULE"] = "wagtail_ab_testing.test.settings"
 
@@ -56,8 +58,6 @@ def runtests():
     try:
         execute_from_command_line(argv)
     finally:
-        from wagtail.tests.settings import STATIC_ROOT, MEDIA_ROOT
-
         shutil.rmtree(STATIC_ROOT, ignore_errors=True)
         shutil.rmtree(MEDIA_ROOT, ignore_errors=True)
 
diff --git a/wagtail_ab_testing/__init__.py b/wagtail_ab_testing/__init__.py
index 6d160f3..e69de29 100644
--- a/wagtail_ab_testing/__init__.py
+++ b/wagtail_ab_testing/__init__.py
@@ -1,1 +0,0 @@
-default_app_config = "wagtail_ab_testing.apps.WagtailAbTestingAppConfig"
diff --git a/wagtail_ab_testing/api.py b/wagtail_ab_testing/api.py
index 6669b44..c86bc3c 100644
--- a/wagtail_ab_testing/api.py
+++ b/wagtail_ab_testing/api.py
@@ -2,7 +2,8 @@
 from rest_framework import fields, routers, serializers, status, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
-from wagtail.core.models import Page, Site
+
+from wagtail.models import Page, Site
 
 from .models import AbTest
 
@@ -64,7 +65,7 @@ def serve_variant(self, request, pk=None):
         test = self.get_object()
         request.wagtail_ab_testing_test = test
         request.wagtail_ab_testing_serving_variant = True
-        return test.variant_revision.as_page_object().serve(request)
+        return test.variant_revision.as_object().serve(request)
 
     @action(detail=True, methods=['post'])
     def add_participant(self, request, pk=None):
diff --git a/wagtail_ab_testing/events.py b/wagtail_ab_testing/events.py
index 5fb5eaf..22085dd 100644
--- a/wagtail_ab_testing/events.py
+++ b/wagtail_ab_testing/events.py
@@ -1,6 +1,6 @@
 from django.utils.translation import gettext_lazy as __
 
-from wagtail.core import hooks
+from wagtail import hooks
 
 
 class BaseEvent:
diff --git a/wagtail_ab_testing/migrations/0001_squashed_0012_abtest_variant_revision.py b/wagtail_ab_testing/migrations/0001_squashed_0012_abtest_variant_revision.py
new file mode 100644
index 0000000..9f460f9
--- /dev/null
+++ b/wagtail_ab_testing/migrations/0001_squashed_0012_abtest_variant_revision.py
@@ -0,0 +1,194 @@
+# Generated by Django 4.2.2 on 2023-06-18 11:26
+
+# PURPOSE OF THIS MIGRATION
+#
+# This squashed migration was created for compatibility with Wagtail 4.0's migration from PageRevision 
+# to a generic Revision model. The `0001_initial` migration in this project must continue to reference the old 
+# PageRevision model and wagtailcore.0052_pagelogentry dependency to maintain a consistent migration history 
+# for existing databases. On the flip side, new projects that run wagtail_ab_testing's migrations for the first time
+# will encounter a PageRevision model that no longer exists which causes errors.
+#
+# The solution for new projects is to use this squashed migration file instead.
+# This file contains no references to PageRevision and will migrate without errors.
+
+import datetime
+from django.conf import settings
+import django.core.validators
+from django.db import migrations, models
+import django.db.migrations.operations.special
+import django.db.models.deletion
+
+
+# Migrated from wagtail_ab_testing.migrations.0007_grant_moderators_add_abtest_permission
+def grant_moderators_add_abtest_permission(apps, schema_editor):
+    ContentType = apps.get_model("contenttypes.ContentType")
+    Permission = apps.get_model("auth.Permission")
+    Group = apps.get_model("auth.Group")
+
+    abtest_content_type, created = ContentType.objects.get_or_create(
+        app_label="wagtail_ab_testing", model="abtest"
+    )
+    add_abtest_permission, created = Permission.objects.get_or_create(
+        content_type=abtest_content_type, codename="add_abtest"
+    )
+
+    moderators_group = Group.objects.filter(name="Moderators").first()
+    if moderators_group:
+        moderators_group.permissions.add(add_abtest_permission)
+
+
+class Migration(migrations.Migration):
+    replaces = [
+        ("wagtail_ab_testing", "0001_initial"),
+        ("wagtail_ab_testing", "0002_abtesthourlylog"),
+        ("wagtail_ab_testing", "0003_abtest_winning_variant"),
+        ("wagtail_ab_testing", "0004_started_at_and_duration"),
+        ("wagtail_ab_testing", "0005_hypothesis_and_created_by"),
+        ("wagtail_ab_testing", "0006_sample_size_min_value"),
+        ("wagtail_ab_testing", "0007_grant_moderators_add_abtest_permission"),
+        ("wagtail_ab_testing", "0008_finished_status"),
+        ("wagtail_ab_testing", "0009_rename_variant_to_version"),
+        ("wagtail_ab_testing", "0010_rename_treatment_to_variant"),
+        ("wagtail_ab_testing", "0011_rename_treatment_to_variant_data"),
+        ("wagtail_ab_testing", "0012_abtest_variant_revision"),
+    ]
+
+    initial = True
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+        ("wagtailcore", "0078_referenceindex"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="AbTest",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                ("name", models.CharField(max_length=255)),
+                ("goal_event", models.CharField(max_length=255)),
+                (
+                    "sample_size",
+                    models.PositiveIntegerField(
+                        validators=[django.core.validators.MinValueValidator(1)]
+                    ),
+                ),
+                (
+                    "status",
+                    models.CharField(
+                        choices=[
+                            ("draft", "Draft"),
+                            ("running", "Running"),
+                            ("paused", "Paused"),
+                            ("cancelled", "Cancelled"),
+                            ("finished", "Finished"),
+                            ("completed", "Completed"),
+                        ],
+                        default="draft",
+                        max_length=20,
+                    ),
+                ),
+                (
+                    "goal_page",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name="+",
+                        to="wagtailcore.page",
+                    ),
+                ),
+                (
+                    "page",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="ab_tests",
+                        to="wagtailcore.page",
+                    ),
+                ),
+                (
+                    "variant_revision",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="wagtailcore.revision",
+                    ),
+                ),
+                (
+                    "winning_version",
+                    models.CharField(
+                        choices=[("control", "Control"), ("variant", "Variant")],
+                        max_length=9,
+                        null=True,
+                    ),
+                ),
+                ("current_run_started_at", models.DateTimeField(null=True)),
+                ("first_started_at", models.DateTimeField(null=True)),
+                (
+                    "previous_run_duration",
+                    models.DurationField(default=datetime.timedelta(0)),
+                ),
+                (
+                    "created_by",
+                    models.ForeignKey(
+                        blank=True,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        related_name="+",
+                        to=settings.AUTH_USER_MODEL,
+                    ),
+                ),
+                ("hypothesis", models.TextField(blank=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name="AbTestHourlyLog",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "version",
+                    models.CharField(
+                        choices=[("control", "Control"), ("variant", "Variant")],
+                        max_length=9,
+                    ),
+                ),
+                ("date", models.DateField()),
+                ("hour", models.PositiveSmallIntegerField()),
+                ("participants", models.PositiveIntegerField(default=0)),
+                ("conversions", models.PositiveIntegerField(default=0)),
+                (
+                    "ab_test",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="hourly_logs",
+                        to="wagtail_ab_testing.abtest",
+                    ),
+                ),
+            ],
+            options={"ordering": ["ab_test", "version", "date", "hour"]},
+        ),
+        migrations.AlterUniqueTogether(
+            name="abtesthourlylog",
+            unique_together={("ab_test", "version", "date", "hour")},
+        ),
+        migrations.RunPython(
+            code=grant_moderators_add_abtest_permission,
+            reverse_code=django.db.migrations.operations.special.RunPython.noop,
+        ),
+    ]
diff --git a/wagtail_ab_testing/migrations/0012_abtest_variant_revision.py b/wagtail_ab_testing/migrations/0012_abtest_variant_revision.py
new file mode 100644
index 0000000..86e5848
--- /dev/null
+++ b/wagtail_ab_testing/migrations/0012_abtest_variant_revision.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.1.6 on 2023-03-02 13:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('wagtailcore', '0078_referenceindex'),
+        ('wagtail_ab_testing', '0011_rename_treatment_to_variant_data'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='abtest',
+            name='variant_revision',
+            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.revision'),
+        ),
+    ]
diff --git a/wagtail_ab_testing/models.py b/wagtail_ab_testing/models.py
index 91094c2..3a8b9cd 100644
--- a/wagtail_ab_testing/models.py
+++ b/wagtail_ab_testing/models.py
@@ -12,7 +12,8 @@
 from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext as _, gettext_lazy as __
-from wagtail.core.signals import page_unpublished
+
+from wagtail.signals import page_unpublished
 
 from .events import get_event_types
 
@@ -73,7 +74,7 @@ class AbTest(models.Model):
     page = models.ForeignKey('wagtailcore.Page', on_delete=models.CASCADE, related_name='ab_tests')
     name = models.CharField(max_length=255)
     hypothesis = models.TextField(blank=True)
-    variant_revision = models.ForeignKey('wagtailcore.PageRevision', on_delete=models.CASCADE, related_name='+')
+    variant_revision = models.ForeignKey('wagtailcore.Revision', on_delete=models.CASCADE, related_name='+')
     goal_event = models.CharField(max_length=255)
     goal_page = models.ForeignKey('wagtailcore.Page', null=True, blank=True, on_delete=models.SET_NULL, related_name='+')
     sample_size = models.PositiveIntegerField(validators=[MinValueValidator(1)])
diff --git a/wagtail_ab_testing/templates/wagtail_ab_testing/add_form.html b/wagtail_ab_testing/templates/wagtail_ab_testing/add_form.html
index 0bc24f4..24aafd0 100644
--- a/wagtail_ab_testing/templates/wagtail_ab_testing/add_form.html
+++ b/wagtail_ab_testing/templates/wagtail_ab_testing/add_form.html
@@ -62,10 +62,6 @@ <h3>{% trans "Sample size" %}</h3>
     </form>
 {% endblock %}
 
-{% block extra_css %}
-    {% include "wagtailadmin/pages/_editor_css.html" %}
-{% endblock %}
-
 {% block extra_js %}
     {% include "wagtailadmin/pages/_editor_js.html" %}
     <script>
diff --git a/wagtail_ab_testing/test/__init__.py b/wagtail_ab_testing/test/__init__.py
index 3d623a3..e69de29 100644
--- a/wagtail_ab_testing/test/__init__.py
+++ b/wagtail_ab_testing/test/__init__.py
@@ -1,1 +0,0 @@
-default_app_config = "wagtail_ab_testing.test.apps.WagtailAbTestingTestAppConfig"
diff --git a/wagtail_ab_testing/test/models.py b/wagtail_ab_testing/test/models.py
index dedb55a..89f501a 100644
--- a/wagtail_ab_testing/test/models.py
+++ b/wagtail_ab_testing/test/models.py
@@ -1,4 +1,4 @@
-from wagtail.core.models import Page
+from wagtail.models import Page
 
 
 class SimplePage(Page):
diff --git a/wagtail_ab_testing/test/settings.py b/wagtail_ab_testing/test/settings.py
index 446a5ad..47a559a 100644
--- a/wagtail_ab_testing/test/settings.py
+++ b/wagtail_ab_testing/test/settings.py
@@ -49,7 +49,7 @@
     "wagtail.contrib.routable_page",
     "wagtail.contrib.styleguide",
     "wagtail.sites",
-    "wagtail.core",
+    "wagtail",
     "taggit",
     "rest_framework",
     "django.contrib.admin",
@@ -101,6 +101,13 @@
     }
 }
 
+# while search isn't used in the tests, wagtail.core checks for the presence of it
+# because it's in INSTALLED_APPS
+WAGTAILSEARCH_BACKENDS = {
+    'default': {
+        'BACKEND': 'wagtail.search.backends.database',
+    }
+}
 
 # don't use the intentionally slow default password hasher
 PASSWORD_HASHERS = ("django.contrib.auth.hashers.MD5PasswordHasher",)
@@ -158,3 +165,5 @@
 
 
 WAGTAIL_SITE_NAME = "Wagtail A/B Testing test site"
+
+WAGTAILADMIN_BASE_URL = "http://example.com"
diff --git a/wagtail_ab_testing/test/tests/test_abtest_model.py b/wagtail_ab_testing/test/tests/test_abtest_model.py
index 73f0b50..6e17d3f 100644
--- a/wagtail_ab_testing/test/tests/test_abtest_model.py
+++ b/wagtail_ab_testing/test/tests/test_abtest_model.py
@@ -2,7 +2,8 @@
 
 from django.test import TestCase
 from freezegun import freeze_time
-from wagtail.core.models import Page
+
+from wagtail.models import Page
 
 from wagtail_ab_testing.models import AbTest, AbTestHourlyLog
 
diff --git a/wagtail_ab_testing/test/tests/test_add_abtest.py b/wagtail_ab_testing/test/tests/test_add_abtest.py
index e3b0ef3..e7f447c 100644
--- a/wagtail_ab_testing/test/tests/test_add_abtest.py
+++ b/wagtail_ab_testing/test/tests/test_add_abtest.py
@@ -4,8 +4,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
-from wagtail.core.models import Page
-from wagtail.tests.utils import WagtailTestUtils
+
+from wagtail.models import Page
+from wagtail.test.utils import WagtailTestUtils
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_api.py b/wagtail_ab_testing/test/tests/test_api.py
index 6b47a59..ebb7c1b 100644
--- a/wagtail_ab_testing/test/tests/test_api.py
+++ b/wagtail_ab_testing/test/tests/test_api.py
@@ -3,7 +3,8 @@
 from django.urls import reverse
 from freezegun import freeze_time
 from rest_framework.test import APITestCase
-from wagtail.core.models import Page
+
+from wagtail.models import Page
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_compare_draft.py b/wagtail_ab_testing/test/tests/test_compare_draft.py
index 822667b..9dadbb6 100644
--- a/wagtail_ab_testing/test/tests/test_compare_draft.py
+++ b/wagtail_ab_testing/test/tests/test_compare_draft.py
@@ -1,7 +1,8 @@
 from django.test import TestCase
 from django.urls import reverse
-from wagtail.core.models import Page
-from wagtail.tests.utils import WagtailTestUtils
+
+from wagtail.models import Page
+from wagtail.test.utils import WagtailTestUtils
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_progress.py b/wagtail_ab_testing/test/tests/test_progress.py
index 8931e58..955244d 100644
--- a/wagtail_ab_testing/test/tests/test_progress.py
+++ b/wagtail_ab_testing/test/tests/test_progress.py
@@ -2,8 +2,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
-from wagtail.core.models import Page
-from wagtail.tests.utils import WagtailTestUtils
+
+from wagtail.models import Page
+from wagtail.test.utils import WagtailTestUtils
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_report.py b/wagtail_ab_testing/test/tests/test_report.py
index 11b8801..e131721 100644
--- a/wagtail_ab_testing/test/tests/test_report.py
+++ b/wagtail_ab_testing/test/tests/test_report.py
@@ -2,8 +2,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
-from wagtail.core.models import Page
-from wagtail.tests.utils import WagtailTestUtils
+
+from wagtail.models import Page
+from wagtail.test.utils import WagtailTestUtils
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_results.py b/wagtail_ab_testing/test/tests/test_results.py
index a338a8b..bade4a5 100644
--- a/wagtail_ab_testing/test/tests/test_results.py
+++ b/wagtail_ab_testing/test/tests/test_results.py
@@ -2,8 +2,9 @@
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
-from wagtail.core.models import Page
-from wagtail.tests.utils import WagtailTestUtils
+
+from wagtail.models import Page
+from wagtail.test.utils import WagtailTestUtils
 
 from wagtail_ab_testing.models import AbTest
 from wagtail_ab_testing.test.models import SimplePage
diff --git a/wagtail_ab_testing/test/tests/test_serve.py b/wagtail_ab_testing/test/tests/test_serve.py
index f3032c1..bbf2fa2 100644
--- a/wagtail_ab_testing/test/tests/test_serve.py
+++ b/wagtail_ab_testing/test/tests/test_serve.py
@@ -1,5 +1,6 @@
 from django.test import TestCase, override_settings
-from wagtail.core.models import Page
+
+from wagtail.models import Page
 
 from wagtail_ab_testing.models import AbTest
 
diff --git a/wagtail_ab_testing/test/tests/test_views.py b/wagtail_ab_testing/test/tests/test_views.py
index bfd19ca..1cf365c 100644
--- a/wagtail_ab_testing/test/tests/test_views.py
+++ b/wagtail_ab_testing/test/tests/test_views.py
@@ -4,7 +4,8 @@
 from django.urls import reverse
 from freezegun import freeze_time
 from rest_framework.test import APIClient, APITestCase
-from wagtail.core.models import Page
+
+from wagtail.models import Page
 
 from wagtail_ab_testing.models import AbTest
 
diff --git a/wagtail_ab_testing/test/urls.py b/wagtail_ab_testing/test/urls.py
index 0379c98..4ae1d5e 100644
--- a/wagtail_ab_testing/test/urls.py
+++ b/wagtail_ab_testing/test/urls.py
@@ -1,20 +1,20 @@
-from django.conf.urls import include, url
+from django.urls import include, path
 from django.contrib import admin
 
+from wagtail import urls as wagtail_urls
 from wagtail.admin import urls as wagtailadmin_urls
 from wagtail.documents import urls as wagtaildocs_urls
-from wagtail.core import urls as wagtail_urls
 
 from wagtail_ab_testing import api as ab_testing_api
 from wagtail_ab_testing import urls as ab_testing_urls
 
 
 urlpatterns = [
-    url(r"^django-admin/", admin.site.urls),
-    url(r"^admin/", include(wagtailadmin_urls)),
-    url(r"^documents/", include(wagtaildocs_urls)),
-    url(r'^abtestingapi/', include(ab_testing_api, namespace='ab_testing_api')),
-    url(r'^abtesting/', include(ab_testing_urls, namespace='wagtail_ab_testing')),
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(wagtailadmin_urls)),
+    path("documents/", include(wagtaildocs_urls)),
+    path('abtestingapi/', include(ab_testing_api, namespace='ab_testing_api')),
+    path('abtesting/', include(ab_testing_urls, namespace='wagtail_ab_testing')),
 
-    url(r"", include(wagtail_urls)),
+    path("", include(wagtail_urls)),
 ]
diff --git a/wagtail_ab_testing/test/wagtail_hooks.py b/wagtail_ab_testing/test/wagtail_hooks.py
index ff3dcca..7450a3b 100644
--- a/wagtail_ab_testing/test/wagtail_hooks.py
+++ b/wagtail_ab_testing/test/wagtail_hooks.py
@@ -1,5 +1,5 @@
+from wagtail import hooks
 
-from wagtail.core import hooks
 from wagtail_ab_testing.events import BaseEvent
 
 
diff --git a/wagtail_ab_testing/views.py b/wagtail_ab_testing/views.py
index 7589d44..d45f771 100644
--- a/wagtail_ab_testing/views.py
+++ b/wagtail_ab_testing/views.py
@@ -14,13 +14,17 @@
 import django_filters
 from django_filters.constants import EMPTY_VALUES
 from rest_framework import status
-from rest_framework.decorators import api_view, authentication_classes, permission_classes
+from rest_framework.decorators import (
+    api_view,
+    authentication_classes,
+    permission_classes,
+)
 from rest_framework.response import Response
 from wagtail.admin import messages
 from wagtail.admin.action_menu import ActionMenuItem
 from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
 from wagtail.admin.views.reports import ReportView
-from wagtail.core.models import Page, PAGE_MODEL_CLASSES, UserPagePermissionsProxy
+from wagtail.models import Page, PAGE_MODEL_CLASSES, UserPagePermissionsProxy
 
 from .models import AbTest
 from .events import get_event_types
@@ -33,9 +37,8 @@ class CreateAbTestForm(forms.ModelForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
-        self.fields['goal_event'].choices = [
-            (slug, goal.name)
-            for slug, goal in get_event_types().items()
+        self.fields["goal_event"].choices = [
+            (slug, goal.name) for slug, goal in get_event_types().items()
         ]
 
     def save(self, page, variant_revision, user):
@@ -48,7 +51,7 @@ def save(self, page, variant_revision, user):
 
     class Meta:
         model = AbTest
-        fields = ['name', 'hypothesis', 'goal_event', 'goal_page', 'sample_size']
+        fields = ["name", "hypothesis", "goal_event", "goal_page", "sample_size"]
 
 
 def add_ab_test_checks(request, page):
@@ -58,20 +61,23 @@ def add_ab_test_checks(request, page):
         raise PermissionDenied
 
     # User must have permission to add A/B tests
-    if not request.user.has_perm('wagtail_ab_testing.add_abtest'):
+    if not request.user.has_perm("wagtail_ab_testing.add_abtest"):
         raise PermissionDenied
 
     # Page must not already be running an A/B test
     if AbTest.objects.get_current_for_page(page=page):
         messages.error(request, _("This page already has a running A/B test"))
 
-        return redirect('wagtailadmin_pages:edit', page.id)
+        return redirect("wagtailadmin_pages:edit", page.id)
 
     # Page must be published and have a draft revision
     if not page.live or not page.has_unpublished_changes:
-        messages.error(request, _("To run an A/B test on this page, it must be live with draft changes."))
+        messages.error(
+            request,
+            _("To run an A/B test on this page, it must be live with draft changes."),
+        )
 
-        return redirect('wagtailadmin_pages:edit', page.id)
+        return redirect("wagtailadmin_pages:edit", page.id)
 
 
 def add_compare(request, page_id):
@@ -82,17 +88,27 @@ def add_compare(request, page_id):
     if response:
         return response
 
-    latest_revision_as_page = page.get_latest_revision().as_page_object()
-    comparison = page.get_edit_handler().get_comparison()
+    latest_revision_as_page = page.get_latest_revision_as_object()
+
+    comparison = (
+        page.get_edit_handler()
+        .get_bound_panel(page, request=request, form=None)
+        .get_comparison()
+    )
+
     comparison = [comp(page, latest_revision_as_page) for comp in comparison]
     comparison = [comp for comp in comparison if comp.has_changed()]
 
-    return render(request, 'wagtail_ab_testing/add_compare.html', {
-        'page': page,
-        'latest_revision_as_page': latest_revision_as_page,
-        'comparison': comparison,
-        'differences': any(comp.has_changed() for comp in comparison),
-    })
+    return render(
+        request,
+        "wagtail_ab_testing/add_compare.html",
+        {
+            "page": page,
+            "latest_revision_as_page": latest_revision_as_page,
+            "comparison": comparison,
+            "differences": any(comp.has_changed() for comp in comparison),
+        },
+    )
 
 
 def add_form(request, page_id):
@@ -103,110 +119,155 @@ def add_form(request, page_id):
     if response:
         return response
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = CreateAbTestForm(request.POST)
 
         if form.is_valid():
             ab_test = form.save(page, page.get_latest_revision(), request.user)
 
-            if 'start' in request.POST:
+            if "start" in request.POST:
                 page_perms = page.permissions_for_user(request.user)
                 if page_perms.can_publish():
                     ab_test.start()
 
-                    messages.success(request, _("The A/B test has been created and started."))
+                    messages.success(
+                        request, _("The A/B test has been created and started.")
+                    )
                 else:
-                    messages.error(request, _("The A/B test has been created but you do not have permission to publish so it couldn't be started."))
+                    messages.error(
+                        request,
+                        _(
+                            "The A/B test has been created but you do not have permission to publish so it couldn't be started."
+                        ),
+                    )
 
             else:
                 messages.success(request, _("The A/B test has been created."))
 
-            return redirect('wagtailadmin_pages:edit', page.id)
+            return redirect("wagtailadmin_pages:edit", page.id)
     else:
         form = CreateAbTestForm()
 
     event_types = get_event_types().items()
-    return render(request, 'wagtail_ab_testing/add_form.html', {
-        'page': page,
-        'form': form,
-        'goal_selector_props': json.dumps({
-            'goalTypesByPageType': {
-                f'{page_type._meta.app_label}.{page_type._meta.model_name}': [
-                    {
-                        'slug': slug,
-                        'name': event_type.name,
-                    }
-                    for slug, event_type in event_types
-                    if event_type.requires_page and event_type.can_be_triggered_on_page_type(page_type)
-                ]
-                for page_type in PAGE_MODEL_CLASSES
-            },
-            'globalGoalTypes': [
+
+    """
+    Template: wagtail_ab_testing/add_form.html is rendered here
+
+    Passing this to the template so we can test for it where an include is used because we can't use
+    "wagtailadmin/pages/_editor_css.html" as it's not available in Wagtail 4+
+    """
+
+    return render(
+        request,
+        "wagtail_ab_testing/add_form.html",
+        {
+            "page": page,
+            "form": form,
+            "goal_selector_props": json.dumps(
                 {
-                    'slug': slug,
-                    'name': event_type.name,
-                }
-                for slug, event_type in event_types
-                if not event_type.requires_page
-            ]
-        }, cls=DjangoJSONEncoder)
-    })
+                    "goalTypesByPageType": {
+                        f"{page_type._meta.app_label}.{page_type._meta.model_name}": [
+                            {
+                                "slug": slug,
+                                "name": event_type.name,
+                            }
+                            for slug, event_type in event_types
+                            if event_type.requires_page
+                            and event_type.can_be_triggered_on_page_type(page_type)
+                        ]
+                        for page_type in PAGE_MODEL_CLASSES
+                    },
+                    "globalGoalTypes": [
+                        {
+                            "slug": slug,
+                            "name": event_type.name,
+                        }
+                        for slug, event_type in event_types
+                        if not event_type.requires_page
+                    ],
+                },
+                cls=DjangoJSONEncoder,
+            ),
+        },
+    )
 
 
 class StartAbTestMenuItem(ActionMenuItem):
-    name = 'action-start-ab-test'
+    name = "action-start-ab-test"
     label = _("Start A/B test")
 
     def is_shown(self, request, context):
-        if not context['user_page_permissions'].for_page(context['ab_test'].page).can_publish():
+        if (
+            not context["user_page_permissions"]
+            .for_page(context["ab_test"].page)
+            .can_publish()
+        ):
             return False
 
-        return context['ab_test'].status == AbTest.STATUS_DRAFT
+        return context["ab_test"].status == AbTest.STATUS_DRAFT
 
 
 class RestartAbTestMenuItem(ActionMenuItem):
-    name = 'action-restart-ab-test'
+    name = "action-restart-ab-test"
     label = _("Restart A/B test")
 
     def is_shown(self, request, context):
-        if not context['user_page_permissions'].for_page(context['ab_test'].page).can_publish():
+        if (
+            not context["user_page_permissions"]
+            .for_page(context["ab_test"].page)
+            .can_publish()
+        ):
             return False
 
-        return context['ab_test'].status == AbTest.STATUS_PAUSED
+        return context["ab_test"].status == AbTest.STATUS_PAUSED
 
 
 class EndAbTestMenuItem(ActionMenuItem):
-    name = 'action-end-ab-test'
+    name = "action-end-ab-test"
     label = _("End A/B test")
 
     def is_shown(self, request, context):
-        if not context['user_page_permissions'].for_page(context['ab_test'].page).can_publish():
+        if (
+            not context["user_page_permissions"]
+            .for_page(context["ab_test"].page)
+            .can_publish()
+        ):
             return False
 
-        return context['ab_test'].status in [AbTest.STATUS_DRAFT, AbTest.STATUS_RUNNING, AbTest.STATUS_PAUSED]
+        return context["ab_test"].status in [
+            AbTest.STATUS_DRAFT,
+            AbTest.STATUS_RUNNING,
+            AbTest.STATUS_PAUSED,
+        ]
 
 
 class PauseAbTestMenuItem(ActionMenuItem):
-    name = 'action-pause-ab-test'
+    name = "action-pause-ab-test"
     label = _("Pause A/B test")
 
     def is_shown(self, request, context):
-        if not context['user_page_permissions'].for_page(context['ab_test'].page).can_publish():
+        if (
+            not context["user_page_permissions"]
+            .for_page(context["ab_test"].page)
+            .can_publish()
+        ):
             return False
 
-        return context['ab_test'].status == AbTest.STATUS_RUNNING
+        return context["ab_test"].status == AbTest.STATUS_RUNNING
 
 
 class AbTestActionMenu:
-    template = 'wagtailadmin/pages/action_menu/menu.html'
+    template = "wagtailadmin/pages/action_menu/menu.html"
 
     def __init__(self, request, **kwargs):
         self.request = request
         self.context = kwargs
-        self.context['user_page_permissions'] = UserPagePermissionsProxy(self.request.user)
+        self.context["user_page_permissions"] = UserPagePermissionsProxy(
+            self.request.user
+        )
         # The ActionMenuItem request object is available in the context dictionary as context['request'].
         # https://docs.wagtail.io/en/stable/releases/2.15.html#admin-homepage-panels-summary-items-and-action-menu-items-now-use-components
-        self.context['request'] = request
+        self.context["request"] = request
         self.menu_items = [
             StartAbTestMenuItem(order=0),
             RestartAbTestMenuItem(order=1),
@@ -226,14 +287,18 @@ def __init__(self, request, **kwargs):
             self.default_item = None
 
     def render_html(self):
-        return render_to_string(self.template, {
-            'default_menu_item': self.default_item.render_html(self.request, self.context),
-            'show_menu': bool(self.menu_items),
-            'rendered_menu_items': [
-                menu_item.render_html(self.request, self.context)
-                for menu_item in self.menu_items
-            ],
-        }, request=self.request)
+        return render_to_string(
+            self.template,
+            {
+                "default_menu_item": self.default_item.render_html(self.context),
+                "show_menu": bool(self.menu_items),
+                "rendered_menu_items": [
+                    menu_item.render_html(self.context)
+                    for menu_item in self.menu_items
+                ],
+            },
+            request=self.request,
+        )
 
     @cached_property
     def media(self):
@@ -246,15 +311,23 @@ def media(self):
 def get_progress_and_results_common_context(request, page, ab_test):
     # Fetch stats from database
     stats = ab_test.hourly_logs.aggregate(
-        control_participants=Sum('participants', filter=Q(version=AbTest.VERSION_CONTROL)),
-        control_conversions=Sum('conversions', filter=Q(version=AbTest.VERSION_CONTROL)),
-        variant_participants=Sum('participants', filter=Q(version=AbTest.VERSION_VARIANT)),
-        variant_conversions=Sum('conversions', filter=Q(version=AbTest.VERSION_VARIANT)),
+        control_participants=Sum(
+            "participants", filter=Q(version=AbTest.VERSION_CONTROL)
+        ),
+        control_conversions=Sum(
+            "conversions", filter=Q(version=AbTest.VERSION_CONTROL)
+        ),
+        variant_participants=Sum(
+            "participants", filter=Q(version=AbTest.VERSION_VARIANT)
+        ),
+        variant_conversions=Sum(
+            "conversions", filter=Q(version=AbTest.VERSION_VARIANT)
+        ),
     )
-    control_participants = stats['control_participants'] or 0
-    control_conversions = stats['control_conversions'] or 0
-    variant_participants = stats['variant_participants'] or 0
-    variant_conversions = stats['variant_conversions'] or 0
+    control_participants = stats["control_participants"] or 0
+    control_conversions = stats["control_conversions"] or 0
+    variant_participants = stats["variant_participants"] or 0
+    variant_conversions = stats["variant_conversions"] or 0
 
     current_sample_size = control_participants + variant_participants
 
@@ -263,16 +336,22 @@ def get_progress_and_results_common_context(request, page, ab_test):
         running_duration_days = ab_test.total_running_duration().days
 
         if running_duration_days > 0:
-            participants_per_day = current_sample_size / ab_test.total_running_duration().days
-            estimated_days_remaining = (ab_test.sample_size - current_sample_size) / participants_per_day
-            estimated_completion_date = timezone.now().date() + datetime.timedelta(days=estimated_days_remaining)
+            participants_per_day = (
+                current_sample_size / ab_test.total_running_duration().days
+            )
+            estimated_days_remaining = (
+                ab_test.sample_size - current_sample_size
+            ) / participants_per_day
+            estimated_completion_date = timezone.now().date() + datetime.timedelta(
+                days=estimated_days_remaining
+            )
 
     # Generate time series data for the chart
     time_series = []
     control = 0
     variant = 0
     date = None
-    for log in ab_test.hourly_logs.order_by('date', 'hour'):
+    for log in ab_test.hourly_logs.order_by("date", "hour"):
         # Accumulate the conversions
         if log.version == AbTest.VERSION_CONTROL:
             control += log.conversions
@@ -288,88 +367,145 @@ def get_progress_and_results_common_context(request, page, ab_test):
                 date += datetime.timedelta(days=1)
 
             # Generate a log for this time
-            time_series.append({
-                'date': date,
-                'control': control,
-                'variant': variant,
-            })
+            time_series.append(
+                {
+                    "date": date,
+                    "control": control,
+                    "variant": variant,
+                }
+            )
 
     return {
-        'page': page,
-        'ab_test': ab_test,
-        'current_sample_size': current_sample_size,
-        'current_sample_size_percent': int(current_sample_size / ab_test.sample_size * 100),
-        'control_conversions': control_conversions,
-        'control_participants': control_participants,
-        'control_conversions_percent': int(control_conversions / control_participants * 100) if control_participants else 0,
-        'variant_conversions': variant_conversions,
-        'variant_participants': variant_participants,
-        'variant_conversions_percent': int(variant_conversions / variant_participants * 100) if variant_participants else 0,
-        'control_is_winner': ab_test.winning_version == AbTest.VERSION_CONTROL,
-        'variant_is_winner': ab_test.winning_version == AbTest.VERSION_VARIANT,
-        'unclear_winner': ab_test.status in [AbTest.STATUS_FINISHED, ab_test.STATUS_COMPLETED] and ab_test.winning_version is None,
-        'estimated_completion_date': estimated_completion_date,
-        'chart_data': json.dumps({
-            'x': 'x',
-            'columns': [
-                ['x'] + [data_point['date'].isoformat() for data_point in time_series],
-                [_("Control")] + [data_point['control'] for data_point in time_series],
-                [_("Variant")] + [data_point['variant'] for data_point in time_series],
-            ],
-            'type': 'spline',
-        }),
+        "page": page,
+        "ab_test": ab_test,
+        "current_sample_size": current_sample_size,
+        "current_sample_size_percent": int(
+            current_sample_size / ab_test.sample_size * 100
+        ),
+        "control_conversions": control_conversions,
+        "control_participants": control_participants,
+        "control_conversions_percent": int(
+            control_conversions / control_participants * 100
+        )
+        if control_participants
+        else 0,
+        "variant_conversions": variant_conversions,
+        "variant_participants": variant_participants,
+        "variant_conversions_percent": int(
+            variant_conversions / variant_participants * 100
+        )
+        if variant_participants
+        else 0,
+        "control_is_winner": ab_test.winning_version == AbTest.VERSION_CONTROL,
+        "variant_is_winner": ab_test.winning_version == AbTest.VERSION_VARIANT,
+        "unclear_winner": ab_test.status
+        in [AbTest.STATUS_FINISHED, ab_test.STATUS_COMPLETED]
+        and ab_test.winning_version is None,
+        "estimated_completion_date": estimated_completion_date,
+        "chart_data": json.dumps(
+            {
+                "x": "x",
+                "columns": [
+                    ["x"]
+                    + [data_point["date"].isoformat() for data_point in time_series],
+                    [_("Control")]
+                    + [data_point["control"] for data_point in time_series],
+                    [_("Variant")]
+                    + [data_point["variant"] for data_point in time_series],
+                ],
+                "type": "spline",
+            }
+        ),
     }
 
 
 def progress(request, page, ab_test):
-    if request.method == 'POST':
+    if request.method == "POST":
         page_perms = page.permissions_for_user(request.user)
 
-        if 'action-start-ab-test' in request.POST or 'action-restart-ab-test' in request.POST:
+        if (
+            "action-start-ab-test" in request.POST
+            or "action-restart-ab-test" in request.POST
+        ):
             if page_perms.can_publish():
                 if ab_test.status in [AbTest.STATUS_DRAFT, AbTest.STATUS_PAUSED]:
                     ab_test.start()
 
                     messages.success(request, _("The A/B test has been started."))
                 else:
-                    messages.error(request, _("The A/B test must be in draft or paused in order to be started."))
+                    messages.error(
+                        request,
+                        _(
+                            "The A/B test must be in draft or paused in order to be started."
+                        ),
+                    )
             else:
-                messages.error(request, _("You must have permission to publish in order to start an A/B test."))
-
-        elif 'action-end-ab-test' in request.POST:
+                messages.error(
+                    request,
+                    _(
+                        "You must have permission to publish in order to start an A/B test."
+                    ),
+                )
+
+        elif "action-end-ab-test" in request.POST:
             if page_perms.can_publish():
-                if ab_test.status in [AbTest.STATUS_DRAFT, AbTest.STATUS_RUNNING, AbTest.STATUS_PAUSED]:
+                if ab_test.status in [
+                    AbTest.STATUS_DRAFT,
+                    AbTest.STATUS_RUNNING,
+                    AbTest.STATUS_PAUSED,
+                ]:
                     ab_test.cancel()
 
                     messages.success(request, _("The A/B test has been ended."))
                 elif ab_test.status == AbTest.STATUS_FINISHED:
-                    ab_test.complete(AbTest.COMPLETION_ACTION_DO_NOTHING, user=request.user)
+                    ab_test.complete(
+                        AbTest.COMPLETION_ACTION_DO_NOTHING, user=request.user
+                    )
                 else:
                     messages.error(request, _("The A/B test has already ended."))
             else:
-                messages.error(request, _("You must have permission to publish in order to end an A/B test."))
-
-        elif 'action-pause-ab-test' in request.POST:
+                messages.error(
+                    request,
+                    _(
+                        "You must have permission to publish in order to end an A/B test."
+                    ),
+                )
+
+        elif "action-pause-ab-test" in request.POST:
             if page_perms.can_publish():
                 if ab_test.status == AbTest.STATUS_RUNNING:
                     ab_test.pause()
 
                     messages.success(request, _("The A/B test has been paused."))
                 else:
-                    messages.error(request, _("The A/B test cannot be paused because it is not running."))
+                    messages.error(
+                        request,
+                        _("The A/B test cannot be paused because it is not running."),
+                    )
             else:
-                messages.error(request, _("You must have permission to publish in order to pause an A/B test."))
-
-        elif 'action-select-control' in request.POST:
+                messages.error(
+                    request,
+                    _(
+                        "You must have permission to publish in order to pause an A/B test."
+                    ),
+                )
+
+        elif "action-select-control" in request.POST:
             if ab_test.status == AbTest.STATUS_FINISHED:
                 ab_test.complete(AbTest.COMPLETION_ACTION_REVERT, user=request.user)
 
-                messages.success(request, _("The page has been reverted back to the control version."))
+                messages.success(
+                    request,
+                    _("The page has been reverted back to the control version."),
+                )
 
             else:
-                messages.error(request, _("The A/B test cannot be paused because it is not running."))
+                messages.error(
+                    request,
+                    _("The A/B test cannot be paused because it is not running."),
+                )
 
-        elif 'action-select-variant' in request.POST:
+        elif "action-select-variant" in request.POST:
             if ab_test.status == AbTest.STATUS_FINISHED:
                 # TODO Permission check?
                 ab_test.complete(AbTest.COMPLETION_ACTION_PUBLISH, user=request.user)
@@ -377,17 +513,22 @@ def progress(request, page, ab_test):
                 messages.success(request, _("The variant version has been published."))
 
             else:
-                messages.error(request, _("The A/B test cannot be paused because it is not running."))
+                messages.error(
+                    request,
+                    _("The A/B test cannot be paused because it is not running."),
+                )
 
         else:
             messages.error(request, _("Unknown action"))
 
         # Redirect back
-        return redirect('wagtailadmin_pages:edit', page.id)
+        return redirect("wagtailadmin_pages:edit", page.id)
 
     context = get_progress_and_results_common_context(request, page, ab_test)
-    context['action_menu'] = AbTestActionMenu(request, view='edit', page=page, ab_test=ab_test)
-    return render(request, 'wagtail_ab_testing/progress.html', context)
+    context["action_menu"] = AbTestActionMenu(
+        request, view="edit", page=page, ab_test=ab_test
+    )
+    return render(request, "wagtail_ab_testing/progress.html", context)
 
 
 def results(request, page_id, ab_test_id):
@@ -398,22 +539,32 @@ def results(request, page_id, ab_test_id):
     ab_test = get_object_or_404(AbTest, page=page, id=ab_test_id)
 
     context = get_progress_and_results_common_context(request, page, ab_test)
-    return render(request, 'wagtail_ab_testing/results.html', context)
+    return render(request, "wagtail_ab_testing/results.html", context)
 
 
 def compare_draft(request, page_id):
     page = get_object_or_404(Page, id=page_id).specific
 
-    latest_revision_as_page = page.get_latest_revision().as_page_object()
-    comparison = page.get_edit_handler().get_comparison()
+    latest_revision_as_page = page.get_latest_revision_as_object()
+
+    comparison = (
+        page.get_edit_handler()
+        .get_bound_panel(page, request=request, form=None)
+        .get_comparison()
+    )
+
     comparison = [comp(page, latest_revision_as_page) for comp in comparison]
     comparison = [comp for comp in comparison if comp.has_changed()]
 
-    return render(request, 'wagtail_ab_testing/compare.html', {
-        'page': page,
-        'latest_revision_as_page': latest_revision_as_page,
-        'comparison': comparison,
-    })
+    return render(
+        request,
+        "wagtail_ab_testing/compare.html",
+        {
+            "page": page,
+            "latest_revision_as_page": latest_revision_as_page,
+            "comparison": comparison,
+        },
+    )
 
 
 class SearchPageTitleFilter(django_filters.CharFilter):
@@ -426,30 +577,34 @@ def filter(self, qs, value):
 
 class AbTestingReportFilterSet(WagtailFilterSet):
     page = SearchPageTitleFilter()
-    first_started_at = django_filters.DateFromToRangeFilter(label=gettext_lazy("Started at"), widget=DateRangePickerWidget)
+    first_started_at = django_filters.DateFromToRangeFilter(
+        label=gettext_lazy("Started at"), widget=DateRangePickerWidget
+    )
 
     class Meta:
         model = AbTest
-        fields = ['status', 'page', 'first_started_at']
+        fields = ["status", "page", "first_started_at"]
 
 
 class AbTestingReportView(ReportView):
-    template_name = 'wagtail_ab_testing/report.html'
-    title = gettext_lazy('A/B testing')
-    header_icon = ''
+    template_name = "wagtail_ab_testing/report.html"
+    title = gettext_lazy("A/B testing")
+    header_icon = ""
 
     filterset_class = AbTestingReportFilterSet
 
     def get_queryset(self):
-        return AbTest.objects.all().order_by(F('first_started_at').desc(nulls_first=True))
+        return AbTest.objects.all().order_by(
+            F("first_started_at").desc(nulls_first=True)
+        )
 
 
 @csrf_exempt
-@api_view(['POST'])
+@api_view(["POST"])
 @authentication_classes([])
 @permission_classes([])
 def register_participant(request):
-    test_id = request.data.get('test_id', None)
+    test_id = request.data.get("test_id", None)
     if test_id is None:
         return Response("test_id not provided", status=status.HTTP_400_BAD_REQUEST)
 
@@ -458,18 +613,22 @@ def register_participant(request):
         if test_id < 1:
             raise ValueError
     except ValueError:
-        return Response("test_id must be a positive integer", status=status.HTTP_400_BAD_REQUEST)
+        return Response(
+            "test_id must be a positive integer", status=status.HTTP_400_BAD_REQUEST
+        )
 
     test = get_object_or_404(AbTest, id=test_id)
 
-    version = request.data.get('version', None)
+    version = request.data.get("version", None)
     if version is None:
         return Response("version not provided", status=status.HTTP_400_BAD_REQUEST)
 
     if version not in [AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT]:
         return Response(
-            "version must be either '{}' or '{}'".format(AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT),
-            status=status.HTTP_400_BAD_REQUEST
+            "version must be either '{}' or '{}'".format(
+                AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT
+            ),
+            status=status.HTTP_400_BAD_REQUEST,
         )
 
     # Add participant
@@ -479,24 +638,26 @@ def register_participant(request):
 
 
 @csrf_exempt
-@api_view(['POST'])
+@api_view(["POST"])
 @authentication_classes([])
 @permission_classes([])
 def goal_reached(request):
-    test_id = request.data.get('test_id', None)
+    test_id = request.data.get("test_id", None)
     if test_id is None:
         return Response("test_id not provided", status=status.HTTP_400_BAD_REQUEST)
 
     test = get_object_or_404(AbTest, id=test_id)
 
-    version = request.data.get('version', None)
+    version = request.data.get("version", None)
     if version is None:
         return Response("version not provided", status=status.HTTP_400_BAD_REQUEST)
 
     if version not in [AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT]:
         return Response(
-            "version must be either '{}' or '{}'".format(AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT),
-            status=status.HTTP_400_BAD_REQUEST
+            "version must be either '{}' or '{}'".format(
+                AbTest.VERSION_CONTROL, AbTest.VERSION_VARIANT
+            ),
+            status=status.HTTP_400_BAD_REQUEST,
         )
 
     # Log conversion
diff --git a/wagtail_ab_testing/wagtail_hooks.py b/wagtail_ab_testing/wagtail_hooks.py
index 74f04f4..cc2441d 100644
--- a/wagtail_ab_testing/wagtail_hooks.py
+++ b/wagtail_ab_testing/wagtail_hooks.py
@@ -10,10 +10,11 @@
 from django.utils.translation import gettext as _, gettext_lazy as __
 from django.views.i18n import JavaScriptCatalog
 
+
+from wagtail import hooks
 from wagtail.admin.action_menu import ActionMenuItem
 from wagtail.admin.menu import MenuItem
 from wagtail.admin.staticfiles import versioned_static
-from wagtail.core import hooks
 
 from . import views
 from .compat import DATE_FORMAT
@@ -48,16 +49,20 @@ class CreateAbTestActionMenuItem(ActionMenuItem):
     label = __("Save and create A/B Test")
     icon_name = 'people-arrows'
 
-    def is_shown(self, request, context):
+    def is_shown(self, context):
         if context['view'] != 'edit':
             return False
 
         # User must have permission to add A/B tests
-        if not request.user.has_perm('wagtail_ab_testing.add_abtest'):
+        if not self.check_user_permissions(context['request'].user):
             return False
 
         return True
 
+    @staticmethod
+    def check_user_permissions(user):
+        return user.has_perm('wagtail_ab_testing.add_abtest')
+
 
 @hooks.register('register_page_action_menu_item')
 def register_create_abtest_action_menu_item():
@@ -66,29 +71,34 @@ def register_create_abtest_action_menu_item():
 
 # This is the only way to inject custom JS into the editor with knowledge of the page being edited
 class AbTestingTabActionMenuItem(ActionMenuItem):
-    def render_html(self, request, context):
+
+    def render_html(self, context):
         if 'page' in context:
-            return format_html(
-                '<script src="{}"></script><script src="{}"></script><script>window.abTestingTabProps = JSON.parse("{}");</script>',
-                reverse('wagtail_ab_testing_admin:javascript_catalog'),
-                versioned_static('wagtail_ab_testing/js/wagtail-ab-testing.js'),
-                escapejs(json.dumps({
-                    'tests': [
-                        {
-                            'id': ab_test.id,
-                            'name': ab_test.name,
-                            'started_at': ab_test.first_started_at.strftime(DATE_FORMAT) if ab_test.first_started_at else _("Not started"),
-                            'status': ab_test.get_status_description(),
-                            'results_url': reverse('wagtail_ab_testing_admin:results', args=[ab_test.page_id, ab_test.id]),
-                        }
-                        for ab_test in AbTest.objects.filter(page=context['page']).order_by('-id')
-                    ],
-                    'can_create_abtest': request.user.has_perm('wagtail_ab_testing.add_abtest'),
-                }))
-            )
+            return self.format_html(context['request'].user, context)
 
         return ''
 
+    @staticmethod
+    def format_html(user, context):
+        return format_html(
+            '<script src="{}"></script><script src="{}"></script><script>window.abTestingTabProps = JSON.parse("{}");</script>',
+            reverse('wagtail_ab_testing_admin:javascript_catalog'),
+            versioned_static('wagtail_ab_testing/js/wagtail-ab-testing.js'),
+            escapejs(json.dumps({
+                'tests': [
+                    {
+                        'id': ab_test.id,
+                        'name': ab_test.name,
+                        'started_at': ab_test.first_started_at.strftime(DATE_FORMAT) if ab_test.first_started_at else _("Not started"),
+                        'status': ab_test.get_status_description(),
+                        'results_url': reverse('wagtail_ab_testing_admin:results', args=[ab_test.page_id, ab_test.id]),
+                    }
+                    for ab_test in AbTest.objects.filter(page=context['page']).order_by('-id')
+                ],
+                'can_create_abtest': user.has_perm('wagtail_ab_testing.add_abtest'),
+            }))
+        )
+
 
 @hooks.register('register_page_action_menu_item')
 def register_ab_testing_tab_action_menu_item():
@@ -137,7 +147,8 @@ def before_serve_page(page, request, serve_args, serve_kwargs):
 
         request.wagtail_ab_testing_serving_variant = True
 
-        variant_response = test.variant_revision.as_page_object().serve(request, *serve_args, **serve_kwargs)
+        variant_response = test.variant_revision.as_object().serve(request, *serve_args, **serve_kwargs)
+
         if hasattr(variant_response, "render"):
             variant_response.render()
 
@@ -162,7 +173,7 @@ def before_serve_page(page, request, serve_args, serve_kwargs):
     # If the user should be shown the variant, serve that from the revision. Otherwise return to keep the control
     if version == AbTest.VERSION_VARIANT:
         request.wagtail_ab_testing_serving_variant = True
-        return test.variant_revision.as_page_object().serve(request, *serve_args, **serve_kwargs)
+        return test.variant_revision.as_object().serve(request, *serve_args, **serve_kwargs)
 
 
 class AbTestingReportMenuItem(MenuItem):