diff --git a/.gitignore b/.gitignore
index b6e4761..58ac19a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,7 +58,7 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
-db.sqlite3
+# db.sqlite3
db.sqlite3-journal
# Flask stuff:
diff --git a/README.md b/README.md
index 05219b8..ab462a6 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,8 @@ Admin and frontend page response checker
## `Live` testing scenario
+*If you run check_responses command on this branch it will not show any errors*
+
Wagtail 3 has had some extensive changes over previous verions: [Documentation](https://docs.wagtail.org/en/stable/releases/3.0.html)
Mostly, if you miss renaming an import it can be seen quite quickly when running the site. But some other changes can be hard to spot. E.G.
diff --git a/db.sqlite3 b/db.sqlite3
new file mode 100644
index 0000000..da42e00
Binary files /dev/null and b/db.sqlite3 differ
diff --git a/home/__init__.py b/home/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/edit_handlers.py b/home/edit_handlers.py
new file mode 100644
index 0000000..9c11ad1
--- /dev/null
+++ b/home/edit_handlers.py
@@ -0,0 +1,14 @@
+from django.template.loader import render_to_string
+from django.utils.safestring import mark_safe
+from wagtail.admin.edit_handlers import EditHandler
+
+
+class CustomHelpPanel(EditHandler):
+ template = "custom/help_panel.html"
+
+ def render(self):
+ return mark_safe(
+ render_to_string(
+ self.template, {"self": self, "title": self.form.parent_page.title}
+ )
+ )
diff --git a/home/management/__init__.py b/home/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/management/commands/__init__.py b/home/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/management/commands/check_responses.py b/home/management/commands/check_responses.py
new file mode 100644
index 0000000..9819b23
--- /dev/null
+++ b/home/management/commands/check_responses.py
@@ -0,0 +1,163 @@
+import requests
+from django.core.management.base import BaseCommand
+from wagtail.core.models import Site
+
+
+class Command(BaseCommand):
+ """A command to run against a development site that is running and ready to accept requests.
+
+ Requests are mode to one single page of each page model for both
+ the admin site (edit) and front end (view) and it reports the response codes.
+
+ What out for page paths highlighted in `red` and investigate the error 500.
+
+ Params:
+
+ --host: the hostname and port on your local machine
+ --site_id: the id of the site you want run to tests against
+ --username: the username of an admin account
+ --password: the password of the admin account
+
+ Run: python manage.py check_responses
+ """
+
+ help = "Check if admin response is 200 for each page content type when entering edit mode."
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--host",
+ default="http://127.0.0.1:8000",
+ help="The URL to check",
+ )
+ parser.add_argument(
+ "--site_id",
+ default=None,
+ help="The ID of the site to check",
+ )
+ parser.add_argument(
+ "--username",
+ default="admin",
+ help="The username to use",
+ )
+ parser.add_argument(
+ "--password",
+ default="password",
+ help="The password to use",
+ )
+
+ def handle(self, *args, **options):
+ with requests.Session() as session:
+ # Login
+ url = f"{options['host']}/admin/login/"
+
+ try:
+ session.get(url)
+ except requests.exceptions.ConnectionError:
+ self.stderr.write(
+ self.style.ERROR(
+ f"Could not connect to {options['host']}. Is the server running?"
+ )
+ )
+ return
+ except requests.exceptions.InvalidSchema:
+ self.stderr.write(
+ self.style.ERROR(
+ f"Could not connect to {options['host']}. Invalid schema"
+ )
+ )
+ return
+ except requests.exceptions.MissingSchema:
+ self.stderr.write(
+ self.style.ERROR(
+ f"Could not connect to {options['host']}. Missing schema"
+ )
+ )
+ return
+
+ csrftoken = session.cookies["csrftoken"]
+ login_data = dict(
+ username=options["username"],
+ password=options["password"],
+ csrfmiddlewaretoken=csrftoken,
+ next="/admin/",
+ )
+
+ logged_in = session.post(url, data=login_data).content
+
+ if "Forgotten password?" in logged_in.decode("utf-8"):
+ self.stderr.write(
+ self.style.ERROR(
+ f"Could not log in to {options['host']}. Is the username and password correct?"
+ )
+ )
+ exit()
+
+ try:
+ if options["site_id"]:
+ site = Site.objects.get(id=options["site_id"])
+ else:
+ site = Site.objects.get(is_default_site=True)
+ except Site.DoesNotExist:
+ self.stderr.write(
+ self.style.ERROR(
+ f"Could not find a site with id {options['site_id']}."
+ )
+ )
+ return
+ items = (
+ site.root_page.get_descendants(inclusive=True)
+ .defer_streamfields()
+ .specific()
+ )
+ track_class_names = []
+ result = []
+
+ for item in items:
+ class_name = item.__class__.__name__
+ if not class_name in track_class_names:
+ track_class_names.append(class_name)
+ result.append(
+ {
+ "title": item.title,
+ "url": f"{options['host']}{item.url}",
+ "id": item.id,
+ "editor_url": f"{options['host']}/admin/pages/{item.id}/edit/",
+ "class_name": class_name,
+ }
+ )
+
+ self.stdout.write(f"Checking {len(result)} content types...")
+ self.stdout.write("============================")
+ for count, content_type in enumerate(sorted(track_class_names)):
+ if count <= 8:
+ self.stdout.write(f" {count + 1}. {content_type}")
+ else:
+ self.stdout.write(f"{count + 1}. {content_type}")
+ self.stdout.write("============================\n")
+ # Check the admin url's
+ for page in result:
+ self.stdout.write(f"{page['title']} ( {page['class_name']} ) ↓\n")
+ response = session.get(page["editor_url"])
+ if response.status_code != 200:
+ self.stderr.write(
+ self.style.ERROR(
+ f"{page['editor_url']} ← {response.status_code}"
+ )
+ )
+ else:
+ self.stdout.write(self.style.SUCCESS(f"{page['editor_url']} ← 200"))
+ response = session.get(page["url"])
+ if response.status_code != 200:
+ if response.status_code == 404:
+ self.stderr.write(
+ self.style.WARNING(
+ f"{page['url']} ← {response.status_code} probably a draft page"
+ )
+ )
+ else:
+ self.stderr.write(
+ self.style.ERROR(f"{page['url']} ← {response.status_code}")
+ )
+ else:
+ self.stdout.write(self.style.SUCCESS(f"{page['url']} ← 200"))
+ self.stdout.write("-\n")
diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py
new file mode 100644
index 0000000..77b74b9
--- /dev/null
+++ b/home/migrations/0001_initial.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wagtailcore", "0040_page_draft_title"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="HomePage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ on_delete=models.CASCADE,
+ parent_link=True,
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.Page",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ ]
diff --git a/home/migrations/0002_create_homepage.py b/home/migrations/0002_create_homepage.py
new file mode 100644
index 0000000..ca328fa
--- /dev/null
+++ b/home/migrations/0002_create_homepage.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from django.db import migrations
+
+
+def create_homepage(apps, schema_editor):
+ # Get models
+ ContentType = apps.get_model("contenttypes.ContentType")
+ Page = apps.get_model("wagtailcore.Page")
+ Site = apps.get_model("wagtailcore.Site")
+ HomePage = apps.get_model("home.HomePage")
+
+ # Delete the default homepage
+ # If migration is run multiple times, it may have already been deleted
+ Page.objects.filter(id=2).delete()
+
+ # Create content type for homepage model
+ homepage_content_type, __ = ContentType.objects.get_or_create(
+ model="homepage", app_label="home"
+ )
+
+ # Create a new homepage
+ homepage = HomePage.objects.create(
+ title="Home",
+ draft_title="Home",
+ slug="home",
+ content_type=homepage_content_type,
+ path="00010001",
+ depth=2,
+ numchild=0,
+ url_path="/home/",
+ )
+
+ # Create a site with the new homepage set as the root
+ Site.objects.create(hostname="localhost", root_page=homepage, is_default_site=True)
+
+
+def remove_homepage(apps, schema_editor):
+ # Get models
+ ContentType = apps.get_model("contenttypes.ContentType")
+ HomePage = apps.get_model("home.HomePage")
+
+ # Delete the default homepage
+ # Page and Site objects CASCADE
+ HomePage.objects.filter(slug="home", depth=2).delete()
+
+ # Delete content type for homepage model
+ ContentType.objects.filter(model="homepage", app_label="home").delete()
+
+
+class Migration(migrations.Migration):
+
+ run_before = [
+ ("wagtailcore", "0053_locale_model"),
+ ]
+
+ dependencies = [
+ ("home", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(create_homepage, remove_homepage),
+ ]
diff --git a/home/migrations/0003_standardpage_homepage_body.py b/home/migrations/0003_standardpage_homepage_body.py
new file mode 100644
index 0000000..54fc0d1
--- /dev/null
+++ b/home/migrations/0003_standardpage_homepage_body.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.0.7 on 2022-10-03 20:39
+
+import django.db.models.deletion
+import wagtail.core.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wagtailcore", "0066_collection_management_permissions"),
+ ("home", "0002_create_homepage"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="StandardPage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.page",
+ ),
+ ),
+ ("body", wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ migrations.AddField(
+ model_name="homepage",
+ name="body",
+ field=wagtail.core.fields.RichTextField(blank=True),
+ ),
+ ]
diff --git a/home/migrations/0004_standardpage_story.py b/home/migrations/0004_standardpage_story.py
new file mode 100644
index 0000000..4a5e2b9
--- /dev/null
+++ b/home/migrations/0004_standardpage_story.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.0.7 on 2022-10-03 20:46
+
+import wagtail.core.blocks
+import wagtail.core.fields
+import wagtail.images.blocks
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("home", "0003_standardpage_homepage_body"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="standardpage",
+ name="story",
+ field=wagtail.core.fields.StreamField(
+ [
+ (
+ "heading",
+ wagtail.core.blocks.CharBlock(form_classname="full title"),
+ ),
+ ("paragraph", wagtail.core.blocks.RichTextBlock()),
+ ("image", wagtail.images.blocks.ImageChooserBlock()),
+ ],
+ blank=True,
+ null=True,
+ ),
+ ),
+ ]
diff --git a/home/migrations/0005_blogindexpage_blogpage.py b/home/migrations/0005_blogindexpage_blogpage.py
new file mode 100644
index 0000000..a2fa47d
--- /dev/null
+++ b/home/migrations/0005_blogindexpage_blogpage.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.0.7 on 2022-10-03 21:01
+
+import django.db.models.deletion
+import wagtail.core.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wagtailcore", "0066_collection_management_permissions"),
+ ("home", "0004_standardpage_story"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="BlogIndexPage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.page",
+ ),
+ ),
+ ("intro", wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ migrations.CreateModel(
+ name="BlogPage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ auto_created=True,
+ on_delete=django.db.models.deletion.CASCADE,
+ parent_link=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.page",
+ ),
+ ),
+ ("date", models.DateField(verbose_name="Post date")),
+ ("intro", models.CharField(max_length=250)),
+ ("body", wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ ]
diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/models.py b/home/models.py
new file mode 100644
index 0000000..38956ab
--- /dev/null
+++ b/home/models.py
@@ -0,0 +1,60 @@
+from django.db import models
+from wagtail.admin.edit_handlers import (FieldPanel, RichTextFieldPanel,
+ StreamFieldPanel)
+from wagtail.core import blocks
+from wagtail.core.fields import RichTextField, StreamField
+from wagtail.core.models import Page
+from wagtail.images.blocks import ImageChooserBlock
+
+from home.edit_handlers import CustomHelpPanel
+
+
+class HomePage(Page):
+ body = RichTextField(blank=True)
+
+ content_panels = Page.content_panels + [
+ RichTextFieldPanel("body"),
+ ]
+
+
+class StandardPage(Page):
+ body = RichTextField(blank=True)
+ story = StreamField(
+ [
+ ("heading", blocks.CharBlock(classname="full title")),
+ ("paragraph", blocks.RichTextBlock()),
+ ("image", ImageChooserBlock()),
+ ],
+ blank=True,
+ null=True,
+ )
+
+ content_panels = Page.content_panels + [
+ CustomHelpPanel(),
+ RichTextFieldPanel("body"),
+ StreamFieldPanel("story"),
+ ]
+
+
+class BlogIndexPage(Page):
+ intro = RichTextField(blank=True)
+
+ content_panels = Page.content_panels + [
+ RichTextFieldPanel("intro"),
+ ]
+
+ subpage_types = ["BlogPage"]
+
+
+class BlogPage(Page):
+ date = models.DateField("Post date")
+ intro = models.CharField(max_length=250)
+ body = RichTextField(blank=True)
+
+ content_panels = Page.content_panels + [
+ FieldPanel("date"),
+ FieldPanel("intro"),
+ RichTextFieldPanel("body"),
+ ]
+
+ parent_page_types = ["BlogIndexPage"]
diff --git a/home/static/css/welcome_page.css b/home/static/css/welcome_page.css
new file mode 100644
index 0000000..ce8b149
--- /dev/null
+++ b/home/static/css/welcome_page.css
@@ -0,0 +1,204 @@
+html {
+ box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+body {
+ max-width: 960px;
+ min-height: 100vh;
+ margin: 0 auto;
+ padding: 0 15px;
+ color: #231f20;
+ font-family: 'Helvetica Neue', 'Segoe UI', Arial, sans-serif;
+ line-height: 1.25;
+}
+
+a {
+ background-color: transparent;
+ color: #308282;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #ea1b10;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+p,
+ul {
+ padding: 0;
+ margin: 0;
+ font-weight: 400;
+}
+
+main {
+ display: block; /* For IE11 support */
+}
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e6e6e6;
+}
+
+.logo {
+ width: 150px;
+ margin-right: 20px;
+}
+
+.logo a {
+ display: block;
+}
+
+.figure-logo {
+ max-width: 150px;
+ max-height: 55.1px;
+}
+
+.release-notes {
+ font-size: 14px;
+}
+
+.main {
+ padding: 40px 0;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.figure-space {
+ max-width: 265px;
+}
+
+@-webkit-keyframes pos {
+ 0%, 100% {
+ -webkit-transform: rotate(-6deg);
+ transform: rotate(-6deg);
+ }
+ 50% {
+ -webkit-transform: rotate(6deg);
+ transform: rotate(6deg);
+ }
+}
+
+@keyframes pos {
+ 0%, 100% {
+ -webkit-transform: rotate(-6deg);
+ transform: rotate(-6deg);
+ }
+ 50% {
+ -webkit-transform: rotate(6deg);
+ transform: rotate(6deg);
+ }
+}
+
+.egg {
+ fill: #43b1b0;
+ -webkit-animation: pos 3s ease infinite;
+ animation: pos 3s ease infinite;
+ -webkit-transform: translateY(50px);
+ transform: translateY(50px);
+ -webkit-transform-origin: 50% 80%;
+ transform-origin: 50% 80%;
+}
+
+.main-text {
+ max-width: 400px;
+ margin: 5px auto;
+}
+
+.main-text h1 {
+ font-size: 22px;
+}
+
+.main-text p {
+ margin: 15px auto 0;
+}
+
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ border-top: 1px solid #e6e6e6;
+ padding: 10px;
+}
+
+.option {
+ display: block;
+ padding: 10px 10px 10px 34px;
+ position: relative;
+ text-decoration: none;
+}
+
+.option svg {
+ width: 24px;
+ height: 24px;
+ fill: gray;
+ border: 1px solid #d9d9d9;
+ padding: 5px;
+ border-radius: 100%;
+ top: 10px;
+ left: 0;
+ position: absolute;
+}
+
+.option h4 {
+ font-size: 19px;
+ text-decoration: underline;
+}
+
+.option p {
+ padding-top: 3px;
+ color: #231f20;
+ font-size: 15px;
+ font-weight: 300;
+}
+
+@media (max-width: 996px) {
+ body {
+ max-width: 780px;
+ }
+}
+
+@media (max-width: 767px) {
+ .option {
+ flex: 0 0 50%;
+ }
+}
+
+@media (max-width: 599px) {
+ .main {
+ padding: 20px 0;
+ }
+
+ .figure-space {
+ max-width: 200px;
+ }
+
+ .footer {
+ display: block;
+ width: 300px;
+ margin: 0 auto;
+ }
+}
+
+@media (max-width: 360px) {
+ .header-link {
+ max-width: 100px;
+ }
+}
diff --git a/home/templates/custom/help_panel.html b/home/templates/custom/help_panel.html
new file mode 100644
index 0000000..dc55cbe
--- /dev/null
+++ b/home/templates/custom/help_panel.html
@@ -0,0 +1,4 @@
+
Report Information
+
diff --git a/home/templates/home/blog_index_page.html b/home/templates/home/blog_index_page.html
new file mode 100644
index 0000000..fdf6ba4
--- /dev/null
+++ b/home/templates/home/blog_index_page.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-blogindexpage{% endblock %}
+
+{% block content %}
+
+{{ page.title }}
+
+{{ page.intro|richtext }}
+
+{% for post in page.get_children.all %}
+
+ {{ post.date }}
+ {{ post.intro|richtext }}
+{% endfor %}
+
+{% endblock content %}
diff --git a/home/templates/home/blog_page.html b/home/templates/home/blog_page.html
new file mode 100644
index 0000000..6ff46b4
--- /dev/null
+++ b/home/templates/home/blog_page.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-homepage{% endblock %}
+
+{% block content %}
+
+{{ page.title }}
+
+Date: {{ page.date }}
+
+{{ page.intro }}
+
+{{ page.body|richtext }}
+
+Blog Index
+{% endblock content %}
diff --git a/home/templates/home/home_page.html b/home/templates/home/home_page.html
new file mode 100644
index 0000000..b22d2e4
--- /dev/null
+++ b/home/templates/home/home_page.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-homepage{% endblock %}
+
+{% block content %}
+
+{{ page.title }}
+
+{{ page.body|richtext }}
+
+{% endblock content %}
diff --git a/home/templates/home/standard_page.html b/home/templates/home/standard_page.html
new file mode 100644
index 0000000..ac174e4
--- /dev/null
+++ b/home/templates/home/standard_page.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-standardpage{% endblock %}
+
+{% block content %}
+
+{{ page.title }}
+
+{{ page.body|richtext }}
+
+{% for block in page.story %}
+
+{% include_block block %}
+
+{% endfor %}
+
+{% endblock content %}
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..da8c567
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings.dev")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.max-165x165.jpg b/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.max-165x165.jpg
new file mode 100644
index 0000000..60a3bd7
Binary files /dev/null and b/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.max-165x165.jpg differ
diff --git a/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.original.jpg b/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.original.jpg
new file mode 100644
index 0000000..b649d15
Binary files /dev/null and b/media/images/alesia-kazantceva-VWcPlbHglYc-unsplash.original.jpg differ
diff --git a/media/original_images/alesia-kazantceva-VWcPlbHglYc-unsplash.jpg b/media/original_images/alesia-kazantceva-VWcPlbHglYc-unsplash.jpg
new file mode 100644
index 0000000..e8e832c
Binary files /dev/null and b/media/original_images/alesia-kazantceva-VWcPlbHglYc-unsplash.jpg differ
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..4e02bb8
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,549 @@
+[[package]]
+name = "anyascii"
+version = "0.3.1"
+description = "Unicode to ASCII transliteration"
+category = "main"
+optional = false
+python-versions = ">=3.3"
+
+[[package]]
+name = "asgiref"
+version = "3.5.2"
+description = "ASGI specs, helper code, and adapters"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
+
+[[package]]
+name = "backports.zoneinfo"
+version = "0.2.1"
+description = "Backport of the standard library zoneinfo module"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+tzdata = ["tzdata"]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.9.3"
+description = "Screen-scraping library"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+soupsieve = {version = ">1.2", markers = "python_version >= \"3.0\""}
+
+[package.extras]
+html5lib = ["html5lib"]
+lxml = ["lxml"]
+
+[[package]]
+name = "certifi"
+version = "2022.9.24"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.1.1"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.6.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "django"
+version = "4.0.7"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[package.dependencies]
+asgiref = ">=3.4.1,<4"
+"backports.zoneinfo" = {version = "*", markers = "python_version < \"3.9\""}
+sqlparse = ">=0.2.2"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+argon2 = ["argon2-cffi (>=19.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "django-filter"
+version = "21.1"
+description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "django-modelcluster"
+version = "5.3"
+description = "Django extension to allow working with 'clusters' of models as a single unit, independently of the database"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.dependencies]
+pytz = ">=2015.2"
+
+[package.extras]
+taggit = ["django-taggit (>=0.20)"]
+
+[[package]]
+name = "django-taggit"
+version = "2.1.0"
+description = "django-taggit is a reusable Django application for simple tagging."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "django-treebeard"
+version = "4.5.1"
+description = "Efficient tree implementations for Django"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+Django = ">=2.2"
+
+[[package]]
+name = "djangorestframework"
+version = "3.14.0"
+description = "Web APIs for Django, made easy."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+django = ">=3.0"
+pytz = "*"
+
+[[package]]
+name = "draftjs-exporter"
+version = "2.1.7"
+description = "Library to convert rich text from Draft.js raw ContentState to HTML"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+html5lib = ["beautifulsoup4 (>=4.4.1,<5)", "html5lib (>=0.999,<=1.0.1)"]
+lxml = ["lxml (>=4.2.0,<5)"]
+testing = ["beautifulsoup4 (>=4.4.1,<5)", "coverage (>=4.1.0)", "flake8 (>=3.2.0)", "html5lib (>=0.999,<=1.0.1)", "isort (==4.2.5)", "lxml (>=4.2.0,<5)", "markov-draftjs (==0.1.1)", "memory-profiler (==0.47)", "psutil (==5.4.1)", "tox (>=2.3.1)"]
+
+[[package]]
+name = "et-xmlfile"
+version = "1.1.0"
+description = "An implementation of lxml.xmlfile for the standard library"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "html5lib"
+version = "1.1"
+description = "HTML parser based on the WHATWG HTML specification"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+six = ">=1.9"
+webencodings = "*"
+
+[package.extras]
+all = ["chardet (>=2.2)", "genshi", "lxml"]
+chardet = ["chardet (>=2.2)"]
+genshi = ["genshi"]
+lxml = ["lxml"]
+
+[[package]]
+name = "idna"
+version = "3.4"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "l18n"
+version = "2021.3"
+description = "Internationalization for pytz timezones and territories"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pytz = ">=2020.1"
+six = "*"
+
+[[package]]
+name = "openpyxl"
+version = "3.0.10"
+description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+et-xmlfile = "*"
+
+[[package]]
+name = "pillow"
+version = "9.2.0"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "pytz"
+version = "2022.4"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "requests"
+version = "2.28.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2,<3"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "soupsieve"
+version = "2.3.2.post1"
+description = "A modern CSS selector implementation for Beautiful Soup."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "sqlparse"
+version = "0.4.3"
+description = "A non-validating SQL parser."
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "tablib"
+version = "3.2.1"
+description = "Format agnostic tabular data library (XLS, JSON, YAML, CSV)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+openpyxl = {version = ">=2.6.0", optional = true, markers = "extra == \"xlsx\""}
+xlrd = {version = "*", optional = true, markers = "extra == \"xls\""}
+xlwt = {version = "*", optional = true, markers = "extra == \"xls\""}
+
+[package.extras]
+all = ["markuppy", "odfpy", "openpyxl (>=2.6.0)", "pandas", "pyyaml", "tabulate", "xlrd", "xlwt"]
+cli = ["tabulate"]
+html = ["markuppy"]
+ods = ["odfpy"]
+pandas = ["pandas"]
+xls = ["xlrd", "xlwt"]
+xlsx = ["openpyxl (>=2.6.0)"]
+yaml = ["pyyaml"]
+
+[[package]]
+name = "telepath"
+version = "0.3"
+description = "A library for exchanging data between Python and JavaScript"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[package.extras]
+docs = ["mkdocs (>=1.1,<1.2)", "mkdocs-material (>=6.2,<6.3)"]
+
+[[package]]
+name = "tzdata"
+version = "2022.4"
+description = "Provider of IANA time zone data"
+category = "main"
+optional = false
+python-versions = ">=2"
+
+[[package]]
+name = "urllib3"
+version = "1.26.12"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
+
+[package.extras]
+brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
+secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "wagtail"
+version = "2.16.2"
+description = "A Django content management system."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+anyascii = ">=0.1.5"
+beautifulsoup4 = ">=4.8,<4.10"
+Django = ">=3.2,<4.1"
+django-filter = ">=2.2,<22"
+django-modelcluster = ">=5.2,<6.0"
+django-taggit = ">=2.0,<3.0"
+django-treebeard = ">=4.5.1,<5.0"
+djangorestframework = ">=3.11.1,<4.0"
+draftjs-exporter = ">=2.1.5,<3.0"
+html5lib = ">=0.999,<2"
+l18n = ">=2018.5"
+Pillow = ">=4.0.0,<10.0.0"
+requests = ">=2.11.1,<3.0"
+tablib = {version = ">=0.14.0", extras = ["xls", "xlsx"]}
+telepath = ">=0.1.1,<1"
+Willow = ">=1.4,<1.5"
+xlsxwriter = ">=1.2.8,<4.0"
+
+[package.extras]
+docs = ["Sphinx (>=1.5.2)", "pyenchant (>=3.1.1,<4)", "recommonmark (>=0.7.1)", "sphinx-autobuild (>=0.6.0)", "sphinx-wagtail-theme (==5.0.4)", "sphinxcontrib-spelling (>=5.4.0,<6)"]
+testing = ["Jinja2 (>=3.0,<3.1)", "Unidecode (>=0.04.14,<2.0)", "azure-mgmt-cdn (>=5.1,<6.0)", "azure-mgmt-frontdoor (>=0.3,<0.4)", "boto3 (>=1.16,<1.17)", "coverage (>=3.7.0)", "doc8 (==0.8.1)", "docutils (==0.15)", "elasticsearch (>=5.0,<6.0)", "flake8 (>=3.6.0)", "flake8-blind-except (==0.1.1)", "flake8-print (==2.0.2)", "freezegun (>=0.3.8)", "isort (==5.6.4)", "jinjalint (>=0.5)", "openpyxl (>=2.6.4)", "polib (>=1.1,<2.0)", "python-dateutil (>=2.7)", "pytz (>=2014.7)"]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "willow"
+version = "1.4.1"
+description = "A Python image library that sits on top of Pillow, Wand and OpenCV"
+category = "main"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
+
+[package.extras]
+testing = ["Pillow (>=6.0.0,<10.0.0)", "Wand (>=0.6,<1.0)", "mock (>=3.0,<4.0)"]
+
+[[package]]
+name = "xlrd"
+version = "2.0.1"
+description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.extras]
+build = ["twine", "wheel"]
+docs = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "xlsxwriter"
+version = "3.0.3"
+description = "A Python module for creating Excel XLSX files."
+category = "main"
+optional = false
+python-versions = ">=3.4"
+
+[[package]]
+name = "xlwt"
+version = "1.3.0"
+description = "Library to create spreadsheet files compatible with MS Excel 97/2000/XP/2003 XLS files, on any platform, with Python 2.6, 2.7, 3.3+"
+category = "main"
+optional = false
+python-versions = "*"
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.8"
+content-hash = "7fecbe8ae6b3afc859d145e85db6ca7718ea774daa877e822ed4d0853797adff"
+
+[metadata.files]
+anyascii = [
+ {file = "anyascii-0.3.1-py3-none-any.whl", hash = "sha256:8707d3185017435933360462a65e2c70a4818490745804f38a5ca55e59eb56a0"},
+ {file = "anyascii-0.3.1.tar.gz", hash = "sha256:dedf57728206e286c91eed7c759505a5e45c8cd01367dd40c2f7248bb15c11f6"},
+]
+asgiref = [
+ {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
+ {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
+]
+"backports.zoneinfo" = [
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da6013fd84a690242c310d77ddb8441a559e9cb3d3d59ebac9aca1a57b2e18bc"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:89a48c0d158a3cc3f654da4c2de1ceba85263fafb861b98b59040a5086259722"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1c5742112073a563c81f786e77514969acb58649bcdf6cdf0b4ed31a348d4546"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win32.whl", hash = "sha256:e8236383a20872c0cdf5a62b554b27538db7fa1bbec52429d8d106effbaeca08"},
+ {file = "backports.zoneinfo-0.2.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8439c030a11780786a2002261569bdf362264f605dfa4d65090b64b05c9f79a7"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:17746bd546106fa389c51dbea67c8b7c8f0d14b5526a579ca6ccf5ed72c526cf"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5c144945a7752ca544b4b78c8c41544cdfaf9786f25fe5ffb10e838e19a27570"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:e55b384612d93be96506932a786bbcde5a2db7a9e6a4bb4bffe8b733f5b9036b"},
+ {file = "backports.zoneinfo-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a76b38c52400b762e48131494ba26be363491ac4f9a04c1b7e92483d169f6582"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:8961c0f32cd0336fb8e8ead11a1f8cd99ec07145ec2931122faaac1c8f7fd987"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e81b76cace8eda1fca50e345242ba977f9be6ae3945af8d46326d776b4cf78d1"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7b0a64cda4145548fed9efc10322770f929b944ce5cee6c0dfe0c87bf4c0c8c9"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win32.whl", hash = "sha256:1b13e654a55cd45672cb54ed12148cd33628f672548f373963b0bff67b217328"},
+ {file = "backports.zoneinfo-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:4a0f800587060bf8880f954dbef70de6c11bbe59c673c3d818921f042f9954a6"},
+ {file = "backports.zoneinfo-0.2.1.tar.gz", hash = "sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"},
+]
+beautifulsoup4 = [
+ {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"},
+ {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"},
+ {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"},
+]
+certifi = [
+ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
+ {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
+]
+charset-normalizer = []
+django = []
+django-filter = [
+ {file = "django-filter-21.1.tar.gz", hash = "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e"},
+ {file = "django_filter-21.1-py3-none-any.whl", hash = "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"},
+]
+django-modelcluster = [
+ {file = "django-modelcluster-5.3.tar.gz", hash = "sha256:0347cdcacb19a1078ee56cc3e6d5413ba27b8a5900710c53bb92b5d8ff3819cd"},
+ {file = "django_modelcluster-5.3-py2.py3-none-any.whl", hash = "sha256:f9f4250e706ee5e57503a30fbf37058b73e9fdc3190d75031338b81913cc9b4e"},
+]
+django-taggit = [
+ {file = "django-taggit-2.1.0.tar.gz", hash = "sha256:a9f41e4ad58efe4b28d86f274728ee87eb98eeae90c9eb4b4efad39e5068184e"},
+ {file = "django_taggit-2.1.0-py3-none-any.whl", hash = "sha256:61547a23fc99967c9304107414a09e662b459f4163dbbae32e60b8ba40c34d05"},
+]
+django-treebeard = [
+ {file = "django-treebeard-4.5.1.tar.gz", hash = "sha256:80150017725239702054e5fa64dc66e383dc13ac262c8d47ee5a82cb005969da"},
+ {file = "django_treebeard-4.5.1-py3-none-any.whl", hash = "sha256:7c2b1cdb1e9b46d595825186064a1228bc4d00dbbc186db5b0b9412357fba91c"},
+]
+djangorestframework = [
+ {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
+ {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
+]
+draftjs-exporter = [
+ {file = "draftjs_exporter-2.1.7-py3-none-any.whl", hash = "sha256:d415a9964690a2cddb66a31ef32dd46c277e9b80434b94e39e3043188ed83e33"},
+ {file = "draftjs_exporter-2.1.7.tar.gz", hash = "sha256:5839cbc29d7bce2fb99837a404ca40c3a07313f2a20e2700de7ad6aa9a9a18fb"},
+]
+et-xmlfile = [
+ {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"},
+ {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
+]
+html5lib = [
+ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
+ {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
+]
+idna = [
+ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
+ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+]
+l18n = [
+ {file = "l18n-2021.3-py3-none-any.whl", hash = "sha256:78495d1df95b6f7dcc694d1ba8994df709c463a1cbac1bf016e1b9a5ce7280b9"},
+ {file = "l18n-2021.3.tar.gz", hash = "sha256:1956e890d673d17135cc20913253c154f6bc1c00266c22b7d503cc1a5a42d848"},
+]
+openpyxl = [
+ {file = "openpyxl-3.0.10-py2.py3-none-any.whl", hash = "sha256:0ab6d25d01799f97a9464630abacbb34aafecdcaa0ef3cba6d6b3499867d0355"},
+ {file = "openpyxl-3.0.10.tar.gz", hash = "sha256:e47805627aebcf860edb4edf7987b1309c1b3632f3750538ed962bbcc3bd7449"},
+]
+pillow = []
+pytz = [
+ {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"},
+ {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"},
+]
+requests = []
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+soupsieve = [
+ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"},
+ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"},
+]
+sqlparse = [
+ {file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
+ {file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
+]
+tablib = [
+ {file = "tablib-3.2.1-py3-none-any.whl", hash = "sha256:870d7e688f738531a14937a055e8bba404fbc388e77d4d500b2c904075d1019c"},
+ {file = "tablib-3.2.1.tar.gz", hash = "sha256:a57f2770b8c225febec1cb1e65012a69cf30dd28be810e0ff98d024768c7d0f1"},
+]
+telepath = [
+ {file = "telepath-0.3-py35-none-any.whl", hash = "sha256:fbdbc6bdd9a4a5c2b230caae242fa6ba4994c9dbcfd780df9be63f2966feb034"},
+ {file = "telepath-0.3.tar.gz", hash = "sha256:54f4b57232461bc67c54f63f437cb6d26843c189bae6b7ba8e4457f98d78e515"},
+]
+tzdata = [
+ {file = "tzdata-2022.4-py2.py3-none-any.whl", hash = "sha256:74da81ecf2b3887c94e53fc1d466d4362aaf8b26fc87cda18f22004544694583"},
+ {file = "tzdata-2022.4.tar.gz", hash = "sha256:ada9133fbd561e6ec3d1674d3fba50251636e918aa97bd59d63735bef5a513bb"},
+]
+urllib3 = []
+wagtail = [
+ {file = "wagtail-2.16.2-py3-none-any.whl", hash = "sha256:d48ea4d45d09f8482a1b4632e0dca1aad99489cc26386f2af32b306e014832ba"},
+ {file = "wagtail-2.16.2.tar.gz", hash = "sha256:ee23dc1ba39d4785d20acc95274b29dbcf83f5d9fd670555f541f2a88eec9daa"},
+]
+webencodings = [
+ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+willow = [
+ {file = "Willow-1.4.1-py2.py3-none-any.whl", hash = "sha256:fc4042696d090e75aef922fa1ed26d483c764f005b36cf523cf7c34e69d5dd7a"},
+ {file = "Willow-1.4.1.tar.gz", hash = "sha256:0df8ff528531e00b48d40bf72ed81beac1dc82f2d42e5bbed4aff0218bef8c0d"},
+]
+xlrd = [
+ {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"},
+ {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"},
+]
+xlsxwriter = [
+ {file = "XlsxWriter-3.0.3-py3-none-any.whl", hash = "sha256:df0aefe5137478d206847eccf9f114715e42aaea077e6a48d0e8a2152e983010"},
+ {file = "XlsxWriter-3.0.3.tar.gz", hash = "sha256:e89f4a1d2fa2c9ea15cde77de95cd3fd8b0345d0efb3964623f395c8c4988b7f"},
+]
+xlwt = [
+ {file = "xlwt-1.3.0-py2.py3-none-any.whl", hash = "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e"},
+ {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..425df65
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,16 @@
+[tool.poetry]
+name = "wagtail-response-checker"
+version = "0.1.0"
+description = "Check wagtail admin and frontend responses"
+authors = ["Nick Moreton "]
+license = "MIT"
+
+[tool.poetry.dependencies]
+python = "^3.8"
+wagtail = "2.16.2"
+
+[tool.poetry.dev-dependencies]
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/sandbox/__init__.py b/sandbox/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/settings/__init__.py b/sandbox/settings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/settings/base.py b/sandbox/settings/base.py
new file mode 100644
index 0000000..e548fef
--- /dev/null
+++ b/sandbox/settings/base.py
@@ -0,0 +1,166 @@
+"""
+Django settings for sandbox project.
+
+Generated by 'django-admin startproject' using Django 4.0.7.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/4.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/4.0/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "home",
+ "search",
+ "wagtail.contrib.forms",
+ "wagtail.contrib.redirects",
+ "wagtail.embeds",
+ "wagtail.sites",
+ "wagtail.users",
+ "wagtail.snippets",
+ "wagtail.documents",
+ "wagtail.images",
+ "wagtail.search",
+ "wagtail.admin",
+ "wagtail.core",
+ "modelcluster",
+ "taggit",
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "django.middleware.security.SecurityMiddleware",
+ "wagtail.contrib.redirects.middleware.RedirectMiddleware",
+]
+
+ROOT_URLCONF = "sandbox.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [
+ os.path.join(PROJECT_DIR, "templates"),
+ ],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "sandbox.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/4.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/4.0/howto/static-files/
+
+STATICFILES_FINDERS = [
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+]
+
+STATICFILES_DIRS = [
+ os.path.join(PROJECT_DIR, "static"),
+]
+
+# ManifestStaticFilesStorage is recommended in production, to prevent outdated
+# JavaScript / CSS assets being served from cache (e.g. after a Wagtail upgrade).
+# See https://docs.djangoproject.com/en/4.0/ref/contrib/staticfiles/#manifeststaticfilesstorage
+STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
+
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_URL = "/static/"
+
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_URL = "/media/"
+
+
+# Wagtail settings
+
+WAGTAIL_SITE_NAME = "sandbox"
+
+# Search
+# https://docs.wagtail.org/en/stable/topics/search/backends.html
+WAGTAILSEARCH_BACKENDS = {
+ "default": {
+ "BACKEND": "wagtail.search.backends.database",
+ }
+}
+
+# Base URL to use when referring to full URLs within the Wagtail admin backend -
+# e.g. in notification emails. Don't include '/admin' or a trailing slash
+BASE_URL = "http://example.com"
diff --git a/sandbox/settings/dev.py b/sandbox/settings/dev.py
new file mode 100644
index 0000000..0cda525
--- /dev/null
+++ b/sandbox/settings/dev.py
@@ -0,0 +1,18 @@
+from .base import *
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-vijx1n94bb8z4(_5ble!ad4%cn^qhv2mappmjbtqz#9e7i0hwk"
+
+# SECURITY WARNING: define the correct hosts in production!
+ALLOWED_HOSTS = ["*"]
+
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+
+
+try:
+ from .local import *
+except ImportError:
+ pass
diff --git a/sandbox/settings/production.py b/sandbox/settings/production.py
new file mode 100644
index 0000000..9ca4ed7
--- /dev/null
+++ b/sandbox/settings/production.py
@@ -0,0 +1,8 @@
+from .base import *
+
+DEBUG = False
+
+try:
+ from .local import *
+except ImportError:
+ pass
diff --git a/sandbox/static/css/sandbox.css b/sandbox/static/css/sandbox.css
new file mode 100644
index 0000000..5f7c9f4
--- /dev/null
+++ b/sandbox/static/css/sandbox.css
@@ -0,0 +1,5 @@
+img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
diff --git a/sandbox/static/js/sandbox.js b/sandbox/static/js/sandbox.js
new file mode 100644
index 0000000..e69de29
diff --git a/sandbox/templates/404.html b/sandbox/templates/404.html
new file mode 100644
index 0000000..c9e215f
--- /dev/null
+++ b/sandbox/templates/404.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block title %}Page not found{% endblock %}
+
+{% block body_class %}template-404{% endblock %}
+
+{% block content %}
+ Page not found
+
+ Sorry, this page could not be found.
+{% endblock %}
diff --git a/sandbox/templates/500.html b/sandbox/templates/500.html
new file mode 100644
index 0000000..72b6406
--- /dev/null
+++ b/sandbox/templates/500.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Internal server error
+
+
+
+ Internal server error
+
+ Sorry, there seems to be an error. Please try again soon.
+
+
diff --git a/sandbox/templates/base.html b/sandbox/templates/base.html
new file mode 100644
index 0000000..7f1ebe0
--- /dev/null
+++ b/sandbox/templates/base.html
@@ -0,0 +1,39 @@
+{% load static wagtailcore_tags wagtailuserbar %}
+
+
+
+
+
+
+ {% block title %}
+ {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %}
+ {% endblock %}
+ {% block title_suffix %}
+ {% wagtail_site as current_site %}
+ {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %}
+ {% endblock %}
+
+
+
+
+ {# Global stylesheets #}
+
+
+ {% block extra_css %}
+ {# Override this in templates to add extra stylesheets #}
+ {% endblock %}
+
+
+
+ {% wagtailuserbar %}
+
+ {% block content %}{% endblock %}
+
+ {# Global javascript #}
+
+
+ {% block extra_js %}
+ {# Override this in templates to add extra javascript #}
+ {% endblock %}
+
+
diff --git a/sandbox/urls.py b/sandbox/urls.py
new file mode 100644
index 0000000..a586c2f
--- /dev/null
+++ b/sandbox/urls.py
@@ -0,0 +1,34 @@
+from django.conf import settings
+from django.contrib import admin
+from django.urls import include, path
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail.core import urls as wagtail_urls
+from wagtail.documents import urls as wagtaildocs_urls
+
+from search import views as search_views
+
+urlpatterns = [
+ path("django-admin/", admin.site.urls),
+ path("admin/", include(wagtailadmin_urls)),
+ path("documents/", include(wagtaildocs_urls)),
+ path("search/", search_views.search, name="search"),
+]
+
+
+if settings.DEBUG:
+ from django.conf.urls.static import static
+ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+ # Serve static and media files from development server
+ urlpatterns += staticfiles_urlpatterns()
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+
+urlpatterns = urlpatterns + [
+ # For anything not caught by a more specific rule above, hand over to
+ # Wagtail's page serving mechanism. This should be the last pattern in
+ # the list:
+ path("", include(wagtail_urls)),
+ # Alternatively, if you want Wagtail pages to be served from a subpath
+ # of your site, rather than the site root:
+ # path("pages/", include(wagtail_urls)),
+]
diff --git a/sandbox/wsgi.py b/sandbox/wsgi.py
new file mode 100644
index 0000000..a3763c7
--- /dev/null
+++ b/sandbox/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for sandbox project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings.dev")
+
+application = get_wsgi_application()
diff --git a/search/__init__.py b/search/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/search/templates/search/search.html b/search/templates/search/search.html
new file mode 100644
index 0000000..5f222e5
--- /dev/null
+++ b/search/templates/search/search.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-searchresults{% endblock %}
+
+{% block title %}Search{% endblock %}
+
+{% block content %}
+ Search
+
+
+
+ {% if search_results %}
+
+ {% for result in search_results %}
+ -
+
+ {% if result.search_description %}
+ {{ result.search_description }}
+ {% endif %}
+
+ {% endfor %}
+
+
+ {% if search_results.has_previous %}
+ Previous
+ {% endif %}
+
+ {% if search_results.has_next %}
+ Next
+ {% endif %}
+ {% elif search_query %}
+ No results found
+ {% endif %}
+{% endblock %}
diff --git a/search/views.py b/search/views.py
new file mode 100644
index 0000000..13b2f36
--- /dev/null
+++ b/search/views.py
@@ -0,0 +1,37 @@
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
+from django.template.response import TemplateResponse
+from wagtail.core.models import Page
+from wagtail.search.models import Query
+
+
+def search(request):
+ search_query = request.GET.get("query", None)
+ page = request.GET.get("page", 1)
+
+ # Search
+ if search_query:
+ search_results = Page.objects.live().search(search_query)
+ query = Query.get(search_query)
+
+ # Record hit
+ query.add_hit()
+ else:
+ search_results = Page.objects.none()
+
+ # Pagination
+ paginator = Paginator(search_results, 10)
+ try:
+ search_results = paginator.page(page)
+ except PageNotAnInteger:
+ search_results = paginator.page(1)
+ except EmptyPage:
+ search_results = paginator.page(paginator.num_pages)
+
+ return TemplateResponse(
+ request,
+ "search/search.html",
+ {
+ "search_query": search_query,
+ "search_results": search_results,
+ },
+ )