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

+
+

You're {% if self.instance.id %}editing{% else %}adding{% endif %} a page {% if self.instance.id %}of{% else %}to{% endif %} the parent page {{ title }}.

+
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.title }}

+

{{ 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 %} + + + {% 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, + }, + )