Forráskód Böngészése

Format project with black (#511)

* Format with black, 80 char line length.
* Manually adjusted some line lengths of strings.
* Removed as many `# noqa` directives as possible.
* Manually fixed some flake8 errors.
* Flake8 the entire project, including project_template.
Vince Salvino 2 éve
szülő
commit
56a00d2f9f
68 módosított fájl, 3113 hozzáadás és 2461 törlés
  1. 4 1
      azure-pipelines.yml
  2. 0 47
      ci/run-flake8.ps1
  3. 14 5
      coderedcms/admin_urls.py
  4. 25 9
      coderedcms/api/mailchimp.py
  5. 3 3
      coderedcms/apps.py
  6. 57 43
      coderedcms/bin/coderedcms.py
  7. 93 57
      coderedcms/blocks/__init__.py
  8. 58 31
      coderedcms/blocks/base_blocks.py
  9. 104 75
      coderedcms/blocks/content_blocks.py
  10. 103 76
      coderedcms/blocks/html_blocks.py
  11. 28 28
      coderedcms/blocks/layout_blocks.py
  12. 55 24
      coderedcms/blocks/stream_form_blocks.py
  13. 12 7
      coderedcms/fields.py
  14. 78 46
      coderedcms/forms.py
  15. 14 13
      coderedcms/importexport.py
  16. 100 72
      coderedcms/models/integration_models.py
  17. 296 232
      coderedcms/models/page_models.py
  18. 161 147
      coderedcms/models/snippet_models.py
  19. 11 3
      coderedcms/models/tests/test_navbars_and_footers.py
  20. 19 17
      coderedcms/models/tests/test_page_models.py
  21. 152 122
      coderedcms/models/wagtailsettings_models.py
  22. 3 1
      coderedcms/project_template/basic/manage.py
  23. 72 82
      coderedcms/project_template/basic/project_name/settings/base.py
  24. 3 3
      coderedcms/project_template/basic/project_name/settings/dev.py
  25. 26 23
      coderedcms/project_template/basic/project_name/settings/prod.py
  26. 5 9
      coderedcms/project_template/basic/project_name/urls.py
  27. 3 1
      coderedcms/project_template/basic/project_name/wsgi.py
  28. 1 1
      coderedcms/project_template/basic/website/apps.py
  29. 23 17
      coderedcms/project_template/basic/website/models.py
  30. 3 1
      coderedcms/project_template/sass/manage.py
  31. 70 78
      coderedcms/project_template/sass/project_name/settings/base.py
  32. 8 6
      coderedcms/project_template/sass/project_name/settings/dev.py
  33. 29 31
      coderedcms/project_template/sass/project_name/settings/prod.py
  34. 5 9
      coderedcms/project_template/sass/project_name/urls.py
  35. 3 1
      coderedcms/project_template/sass/project_name/wsgi.py
  36. 1 1
      coderedcms/project_template/sass/website/apps.py
  37. 23 17
      coderedcms/project_template/sass/website/models.py
  38. 1 1
      coderedcms/search_urls.py
  39. 208 131
      coderedcms/settings.py
  40. 45 41
      coderedcms/templatetags/coderedcms_tags.py
  41. 77 84
      coderedcms/tests/settings.py
  42. 38 19
      coderedcms/tests/test_bin.py
  43. 7 6
      coderedcms/tests/test_templates.py
  44. 1 1
      coderedcms/tests/test_templatetags.py
  45. 99 85
      coderedcms/tests/test_urls.py
  46. 51 38
      coderedcms/tests/testapp/models.py
  47. 5 9
      coderedcms/tests/urls.py
  48. 30 18
      coderedcms/urls.py
  49. 2 3
      coderedcms/utils.py
  50. 94 64
      coderedcms/views.py
  51. 102 83
      coderedcms/wagtail_flexible_forms/blocks.py
  52. 14 8
      coderedcms/wagtail_flexible_forms/edit_handlers.py
  53. 255 169
      coderedcms/wagtail_flexible_forms/models.py
  54. 172 110
      coderedcms/wagtail_flexible_forms/wagtail_hooks.py
  55. 27 25
      coderedcms/wagtail_hooks.py
  56. 16 9
      coderedcms/widgets.py
  57. 2 2
      docs/conf.py
  58. 5 7
      docs/contributing/index.rst
  59. 10 13
      pyproject.toml
  60. 1 0
      requirements-ci.txt
  61. 1 1
      setup.cfg
  62. 41 41
      setup.py
  63. 72 82
      tutorial/mysite/mysite/settings/base.py
  64. 3 3
      tutorial/mysite/mysite/settings/dev.py
  65. 26 23
      tutorial/mysite/mysite/settings/prod.py
  66. 5 9
      tutorial/mysite/mysite/urls.py
  67. 2 2
      tutorial/mysite/website/apps.py
  68. 36 35
      tutorial/mysite/website/models.py

+ 4 - 1
azure-pipelines.yml

@@ -102,7 +102,10 @@ stages:
         python manage.py makemigrations --check
       displayName: 'CR-QC: Check migrations'
 
-    - pwsh: ./ci/run-flake8.ps1
+    - script: black --check .
+      displayName: 'CR-QC: Black'
+
+    - script: flake8 .
       displayName: 'CR-QC: Flake8'
 
   - job: codecov

+ 0 - 47
ci/run-flake8.ps1

@@ -1,47 +0,0 @@
-#!/usr/bin/env pwsh
-
-<#
-.SYNOPSIS
-Runs flake8 to analyze Python source code for errors and best practices.
-#>
-
-# Get path.
-$scriptDir = Split-Path $PSCommandPath -Parent
-$projectDir = (Get-Item $scriptDir).Parent
-
-# Set working directory to root of project.
-Push-Location $projectDir
-
-# Get the diff for the current branch.
-$ExitCode = 0
-$GitDiff = git diff origin/dev
-
-# If there is no diff between dev, then flake8 everything.
-if ( $null -eq $GitDiff ) {
-    flake8 .
-    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
-}
-# Else flake8 just the diff.
-else {
-    Write-Output $GitDiff | flake8 --diff
-    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
-    # If the project_template changed, then flake8 the testproject too.
-    $GitDiffTempl = Write-Output $GitDiff | Select-String -Pattern "^diff .*/project_template/.*"
-    if ( $null -ne $GitDiffTempl ) {
-        flake8 testproject
-        if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
-    }
-}
-
-# Write output for humans.
-if ($ExitCode -eq 0) {
-    Write-Host -ForegroundColor Green "[✔] Flake8 passed with no errors"
-}
-else {
-    # Write the error in a way that shows up as the failure reason in Azure Pipelines.
-    Write-Host "##vso[task.LogIssue type=error;]Flake8 exited with errors. Please resolve issues above."
-}
-
-# Unset working directory and exit with flake8's exit code.
-Pop-Location
-exit $ExitCode

+ 14 - 5
coderedcms/admin_urls.py

@@ -4,9 +4,18 @@ from coderedcms.views import import_index, import_pages_from_csv_file
 
 
 urlpatterns = [
-    path('codered/import-export/',
-         import_index, name="import_index"),
-    path('codered/import-export/import_from_csv/',
-         import_pages_from_csv_file, name="import_from_csv"),
-    path('', include(wagtailadmin_urls)),
+    path(
+        "codered/import-export/",
+        import_index,
+        name="import_index",
+    ),
+    path(
+        "codered/import-export/import_from_csv/",
+        import_pages_from_csv_file,
+        name="import_from_csv",
+    ),
+    path(
+        "",
+        include(wagtailadmin_urls),
+    ),
 ]

+ 25 - 9
coderedcms/api/mailchimp.py

@@ -13,7 +13,9 @@ class MailchimpApi:
 
     def set_access_token(self, site=None):
         site = site or Site.objects.get(is_default_site=True)
-        self.access_token = MailchimpApiSettings.for_site(site).mailchimp_api_key
+        self.access_token = MailchimpApiSettings.for_site(
+            site
+        ).mailchimp_api_key
         if self.access_token:
             self.set_base_url()
             self.is_active = True
@@ -24,7 +26,7 @@ class MailchimpApi:
         """
         The base url for the mailchimip api is dependent on the api key.
         """
-        key, datacenter = self.access_token.split('-')
+        key, datacenter = self.access_token.split("-")
         self.base_url = self.proto_base_url.format(datacenter)
 
     def default_headers(self):
@@ -41,18 +43,28 @@ class MailchimpApi:
         return json_response
 
     def get_merge_fields_for_list(self, list_id):
-        endpoint = "lists/{0}/merge-fields?fields=merge_fields.tag,merge_fields.merge_id,merge_fields.name".format(list_id)  # noqa
+        endpoint = (
+            f"lists/{list_id}/merge-fields"
+            "?fields=merge_fields.tag,merge_fields.merge_id,merge_fields.name"
+        )
         json_response = self._get(endpoint)
         return json_response
 
     def get_interest_categories_for_list(self, list_id):
-        endpoint = "lists/{0}/interest-categories?fields=categories.id,categories.title".format(
-            list_id)
+        endpoint = (
+            f"lists/{list_id}/interest-categories"
+            "?fields=categories.id,categories.title"
+        )
         json_response = self._get(endpoint)
         return json_response
 
-    def get_interests_for_interest_category(self, list_id, interest_category_id):
-        endpoint = "lists/{0}/interest-categories/{1}/interests?fields=interests.id,interests.name".format(list_id, interest_category_id)  # noqa
+    def get_interests_for_interest_category(
+        self, list_id, interest_category_id
+    ):
+        endpoint = (
+            f"lists/{list_id}/interest-categories/{interest_category_id}/interests"
+            "?fields=interests.id,interests.name"
+        )
         json_response = self._get(endpoint)
         return json_response
 
@@ -65,12 +77,16 @@ class MailchimpApi:
         auth = auth or self.default_auth()
         headers = headers or self.default_headers()
         full_url = "{0}{1}".format(self.base_url, endpoint)
-        r = requests.get(full_url, auth=auth, headers=headers, data=data, **kwargs)
+        r = requests.get(
+            full_url, auth=auth, headers=headers, data=data, **kwargs
+        )
         return r.json()
 
     def _post(self, endpoint, data={}, auth=None, headers=None, **kwargs):
         auth = auth or self.default_auth()
         headers = headers or self.default_headers()
         full_url = "{0}{1}".format(self.base_url, endpoint)
-        r = requests.post(full_url, auth=auth, headers=headers, data=data, **kwargs)
+        r = requests.post(
+            full_url, auth=auth, headers=headers, data=data, **kwargs
+        )
         return r.json()

+ 3 - 3
coderedcms/apps.py

@@ -2,8 +2,8 @@ from django.apps import AppConfig
 
 
 class CoderedcmsConfig(AppConfig):
-    name = 'coderedcms'
-    verbose_name = 'Wagtail CRX'
+    name = "coderedcms"
+    verbose_name = "Wagtail CRX"
     # TODO: At some point in the future, change this to BigAutoField and create
     # the corresponding migration for concrete models in coderedcms.
-    default_auto_field = 'django.db.models.AutoField'
+    default_auto_field = "django.db.models.AutoField"

+ 57 - 43
coderedcms/bin/coderedcms.py

@@ -11,7 +11,9 @@ REQUIRED_PYTHON = (3, 4)
 
 if CURRENT_PYTHON < REQUIRED_PYTHON:
     sys.stderr.write(
-        "This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format(*(REQUIRED_PYTHON + CURRENT_PYTHON))  # noqa
+        "This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format(
+            *(REQUIRED_PYTHON + CURRENT_PYTHON)
+        )
     )
     sys.exit(1)
 
@@ -20,24 +22,25 @@ class CreateProject(TemplateCommand):
     """
     Based on django.core.management.startproject
     """
+
     help = "Creates the directory structure for a new Wagtail CRX project."
     missing_args_message = "You must provide a project name."
 
     def add_arguments(self, parser):
         parser.add_argument(
-            '--sitename',
-            help='Human readable name of your website or brand, e.g. "Mega Corp Inc."'
+            "--sitename",
+            help='Human readable name of your website or brand, e.g. "Mega Corp Inc."',
         )
         parser.add_argument(
-            '--domain',
-            help='Domain that will be used for your website in production, e.g. "www.example.com"'
+            "--domain",
+            help='Domain that will be used for your website in production, e.g. "www.example.com"',
         )
         super().add_arguments(parser)
 
     def handle(self, **options):
         # pop standard args
-        project_name = options.pop('name')
-        target = options.pop('directory')
+        project_name = options.pop("name")
+        target = options.pop("directory")
 
         # Make sure given name is not already in use by another python package/module.
         try:
@@ -45,68 +48,76 @@ class CreateProject(TemplateCommand):
         except ImportError:
             pass
         else:
-            sys.exit("'%s' conflicts with the name of an existing "
-                     "Python module and cannot be used as a project "
-                     "name. Please try another name." % project_name)
+            sys.exit(
+                "'%s' conflicts with the name of an existing "
+                "Python module and cannot be used as a project "
+                "name. Please try another name." % project_name
+            )
 
         # Create a random SECRET_KEY to put it in the main settings.
-        options['secret_key'] = get_random_secret_key()
+        options["secret_key"] = get_random_secret_key()
 
         # Handle custom template logic
         import coderedcms
+
         codered_path = os.path.dirname(coderedcms.__file__)
-        if not options['template']:
-            options['template'] = 'basic'
+        if not options["template"]:
+            options["template"] = "basic"
         template_path = os.path.join(
-            os.path.join(codered_path, 'project_template'),
-            options['template']
+            os.path.join(codered_path, "project_template"), options["template"]
         )
 
         # Check if provided template is built-in to coderedcms,
         # otherwise, do not change it.
         if os.path.isdir(template_path):
-            options['template'] = template_path
+            options["template"] = template_path
 
         # Treat these files as Django templates to render the boilerplate.
-        options['extensions'] = ['py', 'md', 'txt']
-        options['files'] = ['Dockerfile']
+        options["extensions"] = ["py", "md", "txt"]
+        options["files"] = ["Dockerfile"]
 
         # Set options
         message = "Creating a Wagtail CRX project called %(project_name)s"
 
-        if options.get('sitename'):
+        if options.get("sitename"):
             message += " for %(sitename)s"
         else:
-            options['sitename'] = project_name
+            options["sitename"] = project_name
 
-        if options.get('domain'):
+        if options.get("domain"):
             message += " (%(domain)s)"
             # Stip protocol out of domain if it is present.
-            options['domain'] = options['domain'].split('://')[-1]
+            options["domain"] = options["domain"].split("://")[-1]
             # Figure out www logic.
-            if options['domain'].startswith('www.'):
-                options['domain_nowww'] = options['domain'].split('www.')[-1]
+            if options["domain"].startswith("www."):
+                options["domain_nowww"] = options["domain"].split("www.")[-1]
             else:
-                options['domain_nowww'] = options['domain']
+                options["domain_nowww"] = options["domain"]
         else:
-            options['domain'] = 'localhost'
-            options['domain_nowww'] = options['domain']
+            options["domain"] = "localhost"
+            options["domain_nowww"] = options["domain"]
 
         # Add additional custom options to the context.
-        options['coderedcms_release'] = coderedcms.release
+        options["coderedcms_release"] = coderedcms.release
 
         # Print a friendly message
-        print(message % {
-            'project_name': project_name,
-            'sitename': options.get('sitename'),
-            'domain': options.get('domain'),
-        })
+        print(
+            message
+            % {
+                "project_name": project_name,
+                "sitename": options.get("sitename"),
+                "domain": options.get("domain"),
+            }
+        )
 
         # Run command
-        super().handle('project', project_name, target, **options)
+        super().handle("project", project_name, target, **options)
 
         # Be a friend once again.
-        print("Success! %(project_name)s has been created" % {'project_name': project_name})
+        print(
+            "Success! %(project_name)s has been created"
+            % {"project_name": project_name}
+        )
 
         nextsteps = """
 Next steps:
@@ -116,11 +127,11 @@ Next steps:
     4. python manage.py runserver
     5. Go to http://localhost:8000/admin/ and start editing!
 """
-        print(nextsteps % {'directory': target if target else project_name})
+        print(nextsteps % {"directory": target if target else project_name})
 
 
 COMMANDS = {
-    'start': CreateProject(),
+    "start": CreateProject(),
 }
 
 
@@ -129,15 +140,18 @@ def prog_name():
 
 
 def help_index():
-    print("Type '%s help <subcommand>' for help on a specific subcommand.\n" % prog_name())  # NOQA
-    print("Available subcommands:\n")  # NOQA
+    print(
+        "Type '%s help <subcommand>' for help on a specific subcommand.\n"
+        % prog_name()
+    )
+    print("Available subcommands:\n")
     for name, cmd in sorted(COMMANDS.items()):
-        print("    %s%s" % (name.ljust(20), cmd.help))  # NOQA
+        print("    %s%s" % (name.ljust(20), cmd.help))
 
 
 def unknown_command(command):
-    print("Unknown command: '%s'" % command)  # NOQA
-    print("Type '%s help' for usage." % prog_name())  # NOQA
+    print("Unknown command: '%s'" % command)
+    print("Type '%s help' for usage." % prog_name())
     sys.exit(1)
 
 
@@ -148,7 +162,7 @@ def main():
         help_index()
         return
 
-    if command_name == 'help':
+    if command_name == "help":
         try:
             help_command_name = sys.argv[2]
         except IndexError:

+ 93 - 57
coderedcms/blocks/__init__.py

@@ -21,7 +21,7 @@ from .stream_form_blocks import (
     CoderedStreamFormRadioButtonsFieldBlock,
     CoderedStreamFormStepBlock,
     CoderedStreamFormTextFieldBlock,
-    CoderedStreamFormTimeFieldBlock
+    CoderedStreamFormTimeFieldBlock,
 )
 from .html_blocks import (
     ButtonBlock,
@@ -34,7 +34,7 @@ from .html_blocks import (
     PagePreviewBlock,
     QuoteBlock,
     RichTextBlock,
-    TableBlock
+    TableBlock,
 )
 from .content_blocks import (  # noqa
     AccordionBlock,
@@ -47,13 +47,9 @@ from .content_blocks import (  # noqa
     NavExternalLinkWithSubLinkBlock,
     NavPageLinkWithSubLinkBlock,
     PriceListBlock,
-    ReusableContentBlock
-)
-from .layout_blocks import (
-    CardGridBlock,
-    GridBlock,
-    HeroBlock
+    ReusableContentBlock,
 )
+from .layout_blocks import CardGridBlock, GridBlock, HeroBlock
 from .base_blocks import (  # noqa
     BaseBlock,
     BaseLayoutBlock,
@@ -68,71 +64,111 @@ from .base_blocks import (  # noqa
 # Collections of blocks commonly used together.
 
 HTML_STREAMBLOCKS = [
-    ('text', RichTextBlock(icon='cr-font')),
-    ('button', ButtonBlock()),
-    ('image', ImageBlock()),
-    ('image_link', ImageLinkBlock()),
-    ('html', blocks.RawHTMLBlock(icon='code', form_classname='monospace', label=_('HTML'), )),
-    ('download', DownloadBlock()),
-    ('embed_video', EmbedVideoBlock()),
-    ('quote', QuoteBlock()),
-    ('table', TableBlock()),
-    ('google_map', EmbedGoogleMapBlock()),
-    ('page_list', PageListBlock()),
-    ('page_preview', PagePreviewBlock()),
+    ("text", RichTextBlock(icon="cr-font")),
+    ("button", ButtonBlock()),
+    ("image", ImageBlock()),
+    ("image_link", ImageLinkBlock()),
+    (
+        "html",
+        blocks.RawHTMLBlock(
+            icon="code",
+            form_classname="monospace",
+            label=_("HTML"),
+        ),
+    ),
+    ("download", DownloadBlock()),
+    ("embed_video", EmbedVideoBlock()),
+    ("quote", QuoteBlock()),
+    ("table", TableBlock()),
+    ("google_map", EmbedGoogleMapBlock()),
+    ("page_list", PageListBlock()),
+    ("page_preview", PagePreviewBlock()),
 ]
 
 CONTENT_STREAMBLOCKS = HTML_STREAMBLOCKS + [
-    ('accordion', AccordionBlock()),
-    ('card', CardBlock()),
-    ('carousel', CarouselBlock()),
-    ('image_gallery', ImageGalleryBlock()),
-    ('modal', ModalBlock(HTML_STREAMBLOCKS)),
-    ('pricelist', PriceListBlock()),
-    ('reusable_content', ReusableContentBlock()),
+    ("accordion", AccordionBlock()),
+    ("card", CardBlock()),
+    ("carousel", CarouselBlock()),
+    ("image_gallery", ImageGalleryBlock()),
+    ("modal", ModalBlock(HTML_STREAMBLOCKS)),
+    ("pricelist", PriceListBlock()),
+    ("reusable_content", ReusableContentBlock()),
 ]
 
 NAVIGATION_STREAMBLOCKS = [
-    ('page_link', NavPageLinkWithSubLinkBlock()),
-    ('external_link', NavExternalLinkWithSubLinkBlock()),
-    ('document_link', NavDocumentLinkWithSubLinkBlock()),
+    ("page_link", NavPageLinkWithSubLinkBlock()),
+    ("external_link", NavExternalLinkWithSubLinkBlock()),
+    ("document_link", NavDocumentLinkWithSubLinkBlock()),
 ]
 
 BASIC_LAYOUT_STREAMBLOCKS = [
-    ('row', GridBlock(HTML_STREAMBLOCKS)),
-    ('html', blocks.RawHTMLBlock(icon='code', form_classname='monospace', label=_('HTML'))),
+    ("row", GridBlock(HTML_STREAMBLOCKS)),
+    (
+        "html",
+        blocks.RawHTMLBlock(
+            icon="code", form_classname="monospace", label=_("HTML")
+        ),
+    ),
 ]
 
 LAYOUT_STREAMBLOCKS = [
-    ('hero', HeroBlock([
-        ('row', GridBlock(CONTENT_STREAMBLOCKS)),
-        ('cardgrid', CardGridBlock([
-            ('card', CardBlock()),
-        ])),
-        ('html', blocks.RawHTMLBlock(icon='code', form_classname='monospace', label=_('HTML'))),
-    ])),
-    ('row', GridBlock(CONTENT_STREAMBLOCKS)),
-    ('cardgrid', CardGridBlock([
-        ('card', CardBlock()),
-    ])),
-    ('html', blocks.RawHTMLBlock(icon='code', form_classname='monospace', label=_('HTML'))),
+    (
+        "hero",
+        HeroBlock(
+            [
+                ("row", GridBlock(CONTENT_STREAMBLOCKS)),
+                (
+                    "cardgrid",
+                    CardGridBlock(
+                        [
+                            ("card", CardBlock()),
+                        ]
+                    ),
+                ),
+                (
+                    "html",
+                    blocks.RawHTMLBlock(
+                        icon="code", form_classname="monospace", label=_("HTML")
+                    ),
+                ),
+            ]
+        ),
+    ),
+    ("row", GridBlock(CONTENT_STREAMBLOCKS)),
+    (
+        "cardgrid",
+        CardGridBlock(
+            [
+                ("card", CardBlock()),
+            ]
+        ),
+    ),
+    (
+        "html",
+        blocks.RawHTMLBlock(
+            icon="code", form_classname="monospace", label=_("HTML")
+        ),
+    ),
 ]
 
 STREAMFORM_FIELDBLOCKS = [
-    ('sf_singleline', CoderedStreamFormCharFieldBlock(group=_('Fields'))),
-    ('sf_multiline', CoderedStreamFormTextFieldBlock(group=_('Fields'))),
-    ('sf_number', CoderedStreamFormNumberFieldBlock(group=_('Fields'))),
-    ('sf_checkboxes', CoderedStreamFormCheckboxesFieldBlock(group=_('Fields'))),
-    ('sf_radios', CoderedStreamFormRadioButtonsFieldBlock(group=_('Fields'))),
-    ('sf_dropdown', CoderedStreamFormDropdownFieldBlock(group=_('Fields'))),
-    ('sf_checkbox', CoderedStreamFormCheckboxFieldBlock(group=_('Fields'))),
-    ('sf_date', CoderedStreamFormDateFieldBlock(group=_('Fields'))),
-    ('sf_time', CoderedStreamFormTimeFieldBlock(group=_('Fields'))),
-    ('sf_datetime', CoderedStreamFormDateTimeFieldBlock(group=_('Fields'))),
-    ('sf_image', CoderedStreamFormImageFieldBlock(group=_('Fields'))),
-    ('sf_file', CoderedStreamFormFileFieldBlock(group=_('Fields'))),
+    ("sf_singleline", CoderedStreamFormCharFieldBlock(group=_("Fields"))),
+    ("sf_multiline", CoderedStreamFormTextFieldBlock(group=_("Fields"))),
+    ("sf_number", CoderedStreamFormNumberFieldBlock(group=_("Fields"))),
+    ("sf_checkboxes", CoderedStreamFormCheckboxesFieldBlock(group=_("Fields"))),
+    ("sf_radios", CoderedStreamFormRadioButtonsFieldBlock(group=_("Fields"))),
+    ("sf_dropdown", CoderedStreamFormDropdownFieldBlock(group=_("Fields"))),
+    ("sf_checkbox", CoderedStreamFormCheckboxFieldBlock(group=_("Fields"))),
+    ("sf_date", CoderedStreamFormDateFieldBlock(group=_("Fields"))),
+    ("sf_time", CoderedStreamFormTimeFieldBlock(group=_("Fields"))),
+    ("sf_datetime", CoderedStreamFormDateTimeFieldBlock(group=_("Fields"))),
+    ("sf_image", CoderedStreamFormImageFieldBlock(group=_("Fields"))),
+    ("sf_file", CoderedStreamFormFileFieldBlock(group=_("Fields"))),
 ]
 
 STREAMFORM_BLOCKS = [
-    ('step', CoderedStreamFormStepBlock(STREAMFORM_FIELDBLOCKS + HTML_STREAMBLOCKS)),
+    (
+        "step",
+        CoderedStreamFormStepBlock(STREAMFORM_FIELDBLOCKS + HTML_STREAMBLOCKS),
+    ),
 ]

+ 58 - 31
coderedcms/blocks/base_blocks.py

@@ -20,9 +20,12 @@ class ClassifierTermChooserBlock(blocks.FieldBlock):
     Enables choosing a ClassifierTerm in the streamfield.
     Lazy loads the target_model from the string to avoid recursive imports.
     """
+
     widget = forms.Select
 
-    def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
+    def __init__(
+        self, required=False, label=None, help_text=None, *args, **kwargs
+    ):
         self._required = required
         self._help_text = help_text
         self._label = label
@@ -30,12 +33,14 @@ class ClassifierTermChooserBlock(blocks.FieldBlock):
 
     @cached_property
     def target_model(self):
-        return resolve_model_string('coderedcms.ClassifierTerm')
+        return resolve_model_string("coderedcms.ClassifierTerm")
 
     @cached_property
     def field(self):
         return forms.ModelChoiceField(
-            queryset=self.target_model.objects.all().order_by('classifier__name', 'name'),
+            queryset=self.target_model.objects.all().order_by(
+                "classifier__name", "name"
+            ),
             widget=self.widget,
             required=self._required,
             label=self._label,
@@ -63,10 +68,13 @@ class CollectionChooserBlock(blocks.FieldBlock):
     """
     Enables choosing a wagtail Collection in the streamfield.
     """
+
     target_model = Collection
     widget = forms.Select
 
-    def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
+    def __init__(
+        self, required=False, label=None, help_text=None, *args, **kwargs
+    ):
         self._required = required
         self._help_text = help_text
         self._label = label
@@ -75,7 +83,7 @@ class CollectionChooserBlock(blocks.FieldBlock):
     @cached_property
     def field(self):
         return forms.ModelChoiceField(
-            queryset=self.target_model.objects.all().order_by('name'),
+            queryset=self.target_model.objects.all().order_by("name"),
             widget=self.widget,
             required=self._required,
             label=self._label,
@@ -103,22 +111,23 @@ class ButtonMixin(blocks.StructBlock):
     """
     Standard style and size options for buttons.
     """
+
     button_title = blocks.CharBlock(
         max_length=255,
         required=True,
-        label=_('Button Title'),
+        label=_("Button Title"),
     )
     button_style = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_BTN_STYLE_CHOICES,
         default=crx_settings.CRX_FRONTEND_BTN_STYLE_DEFAULT,
         required=False,
-        label=_('Button Style'),
+        label=_("Button Style"),
     )
     button_size = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_BTN_SIZE_CHOICES,
         default=crx_settings.CRX_FRONTEND_BTN_SIZE_DEFAULT,
         required=False,
-        label=_('Button Size'),
+        label=_("Button Size"),
     )
 
 
@@ -127,23 +136,26 @@ class CoderedAdvSettings(blocks.StructBlock):
     Common fields each block should have,
     which are hidden under the block's "Advanced Settings" dropdown.
     """
+
     # placeholder, real value get set in __init__()
     custom_template = blocks.Block()
 
     custom_css_class = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Custom CSS Class'),
+        label=_("Custom CSS Class"),
     )
     custom_id = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Custom ID'),
+        label=_("Custom ID"),
     )
 
     class Meta:
-        form_template = 'wagtailadmin/block_forms/base_block_settings_struct.html'
-        label = _('Advanced Settings')
+        form_template = (
+            "wagtailadmin/block_forms/base_block_settings_struct.html"
+        )
+        label = _("Advanced Settings")
 
     def __init__(self, local_blocks=None, template_choices=None, **kwargs):
         if not local_blocks:
@@ -151,12 +163,13 @@ class CoderedAdvSettings(blocks.StructBlock):
 
         local_blocks += (
             (
-                'custom_template',
+                "custom_template",
                 blocks.ChoiceBlock(
                     choices=template_choices,
                     default=None,
                     required=False,
-                    label=_('Template'))
+                    label=_("Template"),
+                ),
             ),
         )
 
@@ -167,15 +180,16 @@ class CoderedAdvTrackingSettings(CoderedAdvSettings):
     """
     CoderedAdvSettings plus additional tracking fields.
     """
+
     ga_tracking_event_category = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Tracking Event Category'),
+        label=_("Tracking Event Category"),
     )
     ga_tracking_event_label = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Tracking Event Label'),
+        label=_("Tracking Event Label"),
     )
 
 
@@ -183,12 +197,15 @@ class CoderedAdvColumnSettings(CoderedAdvSettings):
     """
     BaseBlockSettings plus additional column fields.
     """
+
     column_breakpoint = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_COL_BREAK_CHOICES,
         default=crx_settings.CRX_FRONTEND_COL_BREAK_DEFAULT,
         required=False,
-        verbose_name=_('Column Breakpoint'),
-        help_text=_('Screen size at which the column will expand horizontally or stack vertically.'),  # noqa
+        verbose_name=_("Column Breakpoint"),
+        help_text=_(
+            "Screen size at which the column will expand horizontally or stack vertically."
+        ),
     )
 
 
@@ -196,6 +213,7 @@ class BaseBlock(blocks.StructBlock):
     """
     Common attributes for all blocks used in Wagtail CRX.
     """
+
     # subclasses can override this to determine the advanced settings class
     advsettings_class = CoderedAdvSettings
 
@@ -207,20 +225,21 @@ class BaseBlock(blocks.StructBlock):
         Construct and inject settings block, then initialize normally.
         """
         klassname = self.__class__.__name__.lower()
-        choices = (
-            crx_settings.CRX_FRONTEND_TEMPLATES_BLOCKS.get('*', []) +
-            crx_settings.CRX_FRONTEND_TEMPLATES_BLOCKS.get(klassname, [])
-        )
+        choices = crx_settings.CRX_FRONTEND_TEMPLATES_BLOCKS.get(
+            "*", []
+        ) + crx_settings.CRX_FRONTEND_TEMPLATES_BLOCKS.get(klassname, [])
 
         if not local_blocks:
             local_blocks = ()
 
-        local_blocks += (('settings', self.advsettings_class(template_choices=choices)),)
+        local_blocks += (
+            ("settings", self.advsettings_class(template_choices=choices)),
+        )
 
         super().__init__(local_blocks, **kwargs)
 
     def render(self, value, context=None):
-        template = value['settings']['custom_template']
+        template = value["settings"]["custom_template"]
 
         if not template:
             template = self.get_template(context=context)
@@ -239,6 +258,7 @@ class BaseLayoutBlock(BaseBlock):
     """
     Common attributes for all blocks used in Wagtail CRX.
     """
+
     # Subclasses can override this to provide a default list of blocks for the content.
     content_streamblocks = []
 
@@ -247,7 +267,12 @@ class BaseLayoutBlock(BaseBlock):
             local_blocks = self.content_streamblocks
 
         if local_blocks:
-            local_blocks = (('content', blocks.StreamBlock(local_blocks, label=_('Content'))),)
+            local_blocks = (
+                (
+                    "content",
+                    blocks.StreamBlock(local_blocks, label=_("Content")),
+                ),
+            )
 
         super().__init__(local_blocks, **kwargs)
 
@@ -256,11 +281,12 @@ class LinkStructValue(blocks.StructValue):
     """
     Generates a URL for blocks with multiple link choices.
     """
+
     @property
     def url(self):
-        page = self.get('page_link')
-        doc = self.get('doc_link')
-        ext = self.get('other_link')
+        page = self.get("page_link")
+        doc = self.get("doc_link")
+        ext = self.get("other_link")
         if page and ext:
             return "{0}{1}".format(page.url, ext)
         elif page:
@@ -275,18 +301,19 @@ class BaseLinkBlock(BaseBlock):
     """
     Common attributes for creating a link within the CMS.
     """
+
     page_link = blocks.PageChooserBlock(
         required=False,
-        label=_('Page link'),
+        label=_("Page link"),
     )
     doc_link = DocumentChooserBlock(
         required=False,
-        label=_('Document link'),
+        label=_("Document link"),
     )
     other_link = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Other link'),
+        label=_("Other link"),
     )
 
     advsettings_class = CoderedAdvTrackingSettings

+ 104 - 75
coderedcms/blocks/content_blocks.py

@@ -8,7 +8,12 @@ from wagtail.documents.blocks import DocumentChooserBlock
 from wagtail.images.blocks import ImageChooserBlock
 from wagtail.snippets.blocks import SnippetChooserBlock
 
-from .base_blocks import BaseBlock, BaseLayoutBlock, ButtonMixin, CollectionChooserBlock
+from .base_blocks import (
+    BaseBlock,
+    BaseLayoutBlock,
+    ButtonMixin,
+    CollectionChooserBlock,
+)
 from .html_blocks import ButtonBlock
 
 
@@ -17,60 +22,62 @@ class AccordionBlock(BaseBlock):
     Allows selecting an accordion snippet
     """
 
-    accordion = SnippetChooserBlock('coderedcms.Accordion')
+    accordion = SnippetChooserBlock("coderedcms.Accordion")
 
     class Meta:
-        template = 'coderedcms/blocks/accordion_block.html'
-        icon = 'bars'
-        label = 'Accordion'
+        template = "coderedcms/blocks/accordion_block.html"
+        icon = "bars"
+        label = "Accordion"
 
 
 class CardBlock(BaseBlock):
     """
     A component of information with image, text, and buttons.
     """
+
     image = ImageChooserBlock(
         required=False,
         max_length=255,
-        label=_('Image'),
+        label=_("Image"),
     )
     title = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Title'),
+        label=_("Title"),
     )
     subtitle = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Subtitle'),
+        label=_("Subtitle"),
     )
     description = blocks.RichTextBlock(
-        features=['bold', 'italic', 'ol', 'ul', 'hr', 'link', 'document-link'],
-        label=_('Body'),
+        features=["bold", "italic", "ol", "ul", "hr", "link", "document-link"],
+        label=_("Body"),
     )
     links = blocks.StreamBlock(
-        [('Links', ButtonBlock())],
+        [("Links", ButtonBlock())],
         blank=True,
         required=False,
-        label=_('Links'),
+        label=_("Links"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/card_foot.html'
-        icon = 'cr-list-alt'
-        label = _('Card')
+        template = "coderedcms/blocks/card_foot.html"
+        icon = "cr-list-alt"
+        label = _("Card")
 
 
 class CarouselBlock(BaseBlock):
     """
     Enables choosing a Carousel snippet.
     """
-    carousel = SnippetChooserBlock('coderedcms.Carousel')
+
+    carousel = SnippetChooserBlock("coderedcms.Carousel")
 
     class Meta:
-        icon = 'image'
-        label = _('Carousel')
-        template = 'coderedcms/blocks/carousel_block.html'
+        icon = "image"
+        label = _("Carousel")
+        template = "coderedcms/blocks/carousel_block.html"
 
 
 class ImageGalleryBlock(BaseBlock):
@@ -78,54 +85,61 @@ class ImageGalleryBlock(BaseBlock):
     Show a collection of images with interactive previews that expand to
     full size images in a modal.
     """
+
     collection = CollectionChooserBlock(
         required=True,
-        label=_('Image Collection'),
+        label=_("Image Collection"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/image_gallery_block.html'
-        icon = 'image'
-        label = _('Image Gallery')
+        template = "coderedcms/blocks/image_gallery_block.html"
+        icon = "image"
+        label = _("Image Gallery")
 
 
 class ModalBlock(ButtonMixin, BaseLayoutBlock):
     """
     Renders a button that then opens a popup/modal with content.
     """
+
     header = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Modal heading'),
+        label=_("Modal heading"),
     )
     content = blocks.StreamBlock(
         [],
-        label=_('Modal content'),
+        label=_("Modal content"),
     )
     footer = blocks.StreamBlock(
         [
-            ('text', blocks.CharBlock(icon='cr-font', max_length=255, label=_('Simple Text'))),  # noqa
-            ('button', ButtonBlock()),
+            (
+                "text",
+                blocks.CharBlock(
+                    icon="cr-font", max_length=255, label=_("Simple Text")
+                ),
+            ),
+            ("button", ButtonBlock()),
         ],
         required=False,
-        label=_('Modal footer'),
+        label=_("Modal footer"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/modal_block.html'
-        icon = 'cr-window-maximize'
-        label = _('Modal')
+        template = "coderedcms/blocks/modal_block.html"
+        icon = "cr-window-maximize"
+        label = _("Modal")
 
 
 class NavBaseLinkBlock(BaseBlock):
     display_text = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Display text'),
+        label=_("Display text"),
     )
     image = ImageChooserBlock(
         required=False,
-        label=_('Image'),
+        label=_("Image"),
     )
 
 
@@ -133,54 +147,58 @@ class NavExternalLinkBlock(NavBaseLinkBlock):
     """
     External link.
     """
+
     link = blocks.CharBlock(
         required=False,
-        label=_('URL'),
+        label=_("URL"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/external_link_block.html'
-        label = _('External Link')
+        template = "coderedcms/blocks/external_link_block.html"
+        label = _("External Link")
 
 
 class NavPageLinkBlock(NavBaseLinkBlock):
     """
     Page link.
     """
+
     page = blocks.PageChooserBlock(
-        label=_('Page'),
+        label=_("Page"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/page_link_block.html'
-        label = _('Page Link')
+        template = "coderedcms/blocks/page_link_block.html"
+        label = _("Page Link")
 
 
 class NavDocumentLinkBlock(NavBaseLinkBlock):
     """
     Document link.
     """
+
     document = DocumentChooserBlock(
-        label=_('Document'),
+        label=_("Document"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/document_link_block.html'
-        label = _('Document Link')
+        template = "coderedcms/blocks/document_link_block.html"
+        label = _("Document Link")
 
 
 class NavSubLinkBlock(BaseBlock):
     """
     Streamblock for rendering nested sub-links.
     """
+
     sub_links = blocks.StreamBlock(
         [
-            ('page_link', NavPageLinkBlock()),
-            ('external_link', NavExternalLinkBlock()),
-            ('document_link', NavDocumentLinkBlock()),
+            ("page_link", NavPageLinkBlock()),
+            ("external_link", NavExternalLinkBlock()),
+            ("document_link", NavDocumentLinkBlock()),
         ],
         required=False,
-        label=_('Sub-links'),
+        label=_("Sub-links"),
     )
 
 
@@ -188,111 +206,122 @@ class NavExternalLinkWithSubLinkBlock(NavSubLinkBlock, NavExternalLinkBlock):
     """
     Extermal link with option for sub-links.
     """
+
     class Meta:
-        label = _('External link with sub-links')
+        label = _("External link with sub-links")
 
 
 class NavPageLinkWithSubLinkBlock(NavSubLinkBlock, NavPageLinkBlock):
     """
     Page link with option for sub-links or showing child pages.
     """
+
     show_child_links = blocks.BooleanBlock(
         required=False,
         default=False,
-        label=_('Show child pages'),
-        help_text=_('Automatically show a link to the Page’s child pages as a dropdown menu.'),
+        label=_("Show child pages"),
+        help_text=_(
+            "Automatically show a link to the Page’s child pages as a dropdown menu."
+        ),
     )
 
     class Meta:
-        label = _('Page link with sub-links')
+        label = _("Page link with sub-links")
 
 
 class NavDocumentLinkWithSubLinkBlock(NavSubLinkBlock, NavDocumentLinkBlock):
     """
     Document link with option for sub-links.
     """
+
     class Meta:
-        label = _('Document link with sub-links')
+        label = _("Document link with sub-links")
 
 
 class PriceListItemBlock(BaseBlock):
     """
     Represents one item in a PriceListBlock, such as an entree in a restaurant menu.
     """
+
     image = ImageChooserBlock(
         required=False,
-        label=_('Image'),
+        label=_("Image"),
     )
     name = blocks.CharBlock(
         required=True,
         max_length=255,
-        label=_('Name'),
+        label=_("Name"),
     )
     description = blocks.TextBlock(
         required=False,
         rows=4,
-        label=_('Description'),
+        label=_("Description"),
     )
     price = blocks.CharBlock(
         required=True,
-        label=_('Price'),
-        help_text=_('Any text here. Include currency sign if desired.'),
+        label=_("Price"),
+        help_text=_("Any text here. Include currency sign if desired."),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/pricelistitem_block.html'
-        icon = 'cr-usd'
-        label = _('Price List Item')
+        template = "coderedcms/blocks/pricelistitem_block.html"
+        icon = "cr-usd"
+        label = _("Price List Item")
 
 
 class PriceListBlock(BaseBlock):
     """
     A price list, such as a menu for a restaurant.
     """
+
     heading = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Heading'),
+        label=_("Heading"),
     )
     items = blocks.StreamBlock(
         [
-            ('item', PriceListItemBlock()),
+            ("item", PriceListItemBlock()),
         ],
-        label=_('Items'),
+        label=_("Items"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/pricelist_block.html'
-        icon = 'cr-usd'
-        label = _('Price List')
+        template = "coderedcms/blocks/pricelist_block.html"
+        icon = "cr-usd"
+        label = _("Price List")
 
 
 class ContentWallBlock(BaseBlock):
     """
     Enables choosing a ContentWall snippet.
     """
-    content_wall = SnippetChooserBlock('coderedcms.ContentWall')
+
+    content_wall = SnippetChooserBlock("coderedcms.ContentWall")
     show_content_wall_on_children = blocks.BooleanBlock(
         required=False,
         default=False,
-        verbose_name=_('Show content walls on children pages?'),
+        verbose_name=_("Show content walls on children pages?"),
         help_text=_(
-            'If this is checked, the content walls will be displayed on all children pages of this page.')  # noqa
+            "If this is checked, the content walls will be displayed on all "
+            "children pages of this page."
+        ),
     )
 
     class Meta:
-        icon = 'cr-stop'
-        label = _('Content Wall')
-        template = 'coderedcms/blocks/content_wall_block.html'
+        icon = "cr-stop"
+        label = _("Content Wall")
+        template = "coderedcms/blocks/content_wall_block.html"
 
 
 class ReusableContentBlock(BaseBlock):
     """
     Enables choosing a ResusableContent snippet.
     """
-    content = SnippetChooserBlock('coderedcms.ReusableContent')
+
+    content = SnippetChooserBlock("coderedcms.ReusableContent")
 
     class Meta:
-        icon = 'cr-recycle'
-        label = _('Reusable Content')
-        template = 'coderedcms/blocks/reusable_content_block.html'
+        icon = "cr-recycle"
+        label = _("Reusable Content")
+        template = "coderedcms/blocks/reusable_content_block.html"

+ 103 - 76
coderedcms/blocks/html_blocks.py

@@ -24,17 +24,18 @@ from .base_blocks import (
 )
 
 
-logger = logging.getLogger('coderedcms')
+logger = logging.getLogger("coderedcms")
 
 
 class ButtonBlock(ButtonMixin, BaseLinkBlock):
     """
     A link styled as a button.
     """
+
     class Meta:
-        template = 'coderedcms/blocks/button_block.html'
-        icon = 'cr-hand-pointer-o'
-        label = _('Button Link')
+        template = "coderedcms/blocks/button_block.html"
+        icon = "cr-hand-pointer-o"
+        label = _("Button Link")
         value_class = LinkStructValue
 
 
@@ -42,157 +43,170 @@ class DownloadBlock(ButtonMixin, BaseBlock):
     """
     Link to a file that can be downloaded.
     """
+
     downloadable_file = DocumentChooserBlock(
         required=False,
-        label=_('Document link'),
+        label=_("Document link"),
     )
 
     advsettings_class = CoderedAdvTrackingSettings
 
     class Meta:
-        template = 'coderedcms/blocks/download_block.html'
-        icon = 'download'
-        label = _('Download')
+        template = "coderedcms/blocks/download_block.html"
+        icon = "download"
+        label = _("Download")
 
 
 class EmbedGoogleMapBlock(BaseBlock):
     """
     An embedded Google map in an <iframe>.
     """
+
     search = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Search query'),
-        help_text=_('Address or search term used to find your location on the map.'),
+        label=_("Search query"),
+        help_text=_(
+            "Address or search term used to find your location on the map."
+        ),
     )
     map_title = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Map title'),
-        help_text=_('Map title for screen readers, ex: "Map to Goodale Park"')
+        label=_("Map title"),
+        help_text=_('Map title for screen readers, ex: "Map to Goodale Park"'),
     )
     place_id = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Google place ID'),
-        help_text=_('Requires API key to use place ID.')
+        label=_("Google place ID"),
+        help_text=_("Requires API key to use place ID."),
     )
     map_zoom_level = blocks.IntegerBlock(
         required=False,
         default=14,
-        label=_('Map zoom level'),
+        label=_("Map zoom level"),
         help_text=_(
-            "Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings"  # noqa
-        )
+            "Requires API key to use zoom. "
+            "1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings"
+        ),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/google_map.html'
-        icon = 'cr-map'
-        label = _('Google Map')
+        template = "coderedcms/blocks/google_map.html"
+        icon = "cr-map"
+        label = _("Google Map")
 
 
 class EmbedVideoBlock(BaseBlock):
     """
     Embedded media using stock wagtail functionality.
     """
+
     url = EmbedBlock(
         required=True,
-        label=_('URL'),
-        help_text=_('Link to a YouTube/Vimeo video, tweet, facebook post, etc.')
+        label=_("URL"),
+        help_text=_(
+            "Link to a YouTube/Vimeo video, tweet, facebook post, etc."
+        ),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/embed_video_block.html'
-        icon = 'media'
-        label = _('Embed Media')
+        template = "coderedcms/blocks/embed_video_block.html"
+        icon = "media"
+        label = _("Embed Media")
 
 
 class H1Block(BaseBlock):
     """
     An <h1> heading.
     """
+
     text = blocks.CharBlock(
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/h1_block.html'
-        icon = 'cr-header'
-        label = _('Heading 1')
+        template = "coderedcms/blocks/h1_block.html"
+        icon = "cr-header"
+        label = _("Heading 1")
 
 
 class H2Block(BaseBlock):
     """
     An <h2> heading.
     """
+
     text = blocks.CharBlock(
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/h2_block.html'
-        icon = 'cr-header'
-        label = _('Heading 2')
+        template = "coderedcms/blocks/h2_block.html"
+        icon = "cr-header"
+        label = _("Heading 2")
 
 
 class H3Block(BaseBlock):
     """
     An <h3> heading.
     """
+
     text = blocks.CharBlock(
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/h3_block.html'
-        icon = 'cr-header'
-        label = _('Heading 3')
+        template = "coderedcms/blocks/h3_block.html"
+        icon = "cr-header"
+        label = _("Heading 3")
 
 
 class TableBlock(BaseBlock):
     table = WagtailTableBlock()
 
     class Meta:
-        template = 'coderedcms/blocks/table_block.html'
-        icon = 'table'
-        label = 'Table'
+        template = "coderedcms/blocks/table_block.html"
+        icon = "table"
+        label = "Table"
 
 
 class ImageBlock(BaseBlock):
     """
     An <img>, by default styled responsively to fill its container.
     """
+
     image = ImageChooserBlock(
-        label=_('Image'),
+        label=_("Image"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/image_block.html'
-        icon = 'image'
-        label = _('Image')
+        template = "coderedcms/blocks/image_block.html"
+        icon = "image"
+        label = _("Image")
 
 
 class ImageLinkBlock(BaseLinkBlock):
     """
     An <a> with an image inside it, instead of text.
     """
+
     image = ImageChooserBlock(
-        label=_('Image'),
+        label=_("Image"),
     )
     alt_text = blocks.CharBlock(
         max_length=255,
         required=True,
-        help_text=_('Alternate text to show if the image doesn’t load'),
+        help_text=_("Alternate text to show if the image doesn’t load"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/image_link_block.html'
-        icon = 'image'
-        label = _('Image Link')
+        template = "coderedcms/blocks/image_link_block.html"
+        icon = "image"
+        label = _("Image Link")
         value_class = LinkStructValue
 
 
@@ -200,55 +214,66 @@ class PageListBlock(BaseBlock):
     """
     Renders a preview of selected pages.
     """
+
     indexed_by = blocks.PageChooserBlock(
         required=True,
-        label=_('Parent page'),
+        label=_("Parent page"),
         help_text=_(
-            "Show a preview of pages that are children of the selected page. Uses ordering specified in the page’s LAYOUT tab."  # noqa
+            "Show a preview of pages that are children of the selected page. "
+            "Uses ordering specified in the page’s LAYOUT tab."
         ),
     )
     classified_by = ClassifierTermChooserBlock(
         required=False,
-        label=_('Classified as'),
-        help_text=_('Only show pages that are classified with this term.')
+        label=_("Classified as"),
+        help_text=_("Only show pages that are classified with this term."),
     )
     show_preview = blocks.BooleanBlock(
         required=False,
         default=False,
-        label=_('Show body preview'),
+        label=_("Show body preview"),
     )
     num_posts = blocks.IntegerBlock(
         default=3,
-        label=_('Number of pages to show'),
+        label=_("Number of pages to show"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/pagelist_block.html'
-        icon = 'list-ul'
-        label = _('Latest Pages')
+        template = "coderedcms/blocks/pagelist_block.html"
+        icon = "list-ul"
+        label = _("Latest Pages")
 
     def get_context(self, value, parent_context=None):
 
         context = super().get_context(value, parent_context=parent_context)
 
-        indexer = value['indexed_by'].specific
+        indexer = value["indexed_by"].specific
         # try to use the CoderedPage `get_index_children()`,
         # but fall back to get_children if this is a non-CoderedPage
-        if hasattr(indexer, 'get_index_children'):
+        if hasattr(indexer, "get_index_children"):
             pages = indexer.get_index_children()
-            if value['classified_by']:
+            if value["classified_by"]:
                 try:
-                    pages = pages.filter(classifier_terms=value['classified_by'])
+                    pages = pages.filter(
+                        classifier_terms=value["classified_by"]
+                    )
                 except AttributeError:
                     # `pages` is not a queryset, or is not a queryset of CoderedPage.
                     logger.warning(
-                        "Tried to filter by ClassifierTerm in PageListBlock, but <%s.%s ('%s')>.get_index_children()  # noqadid not return a queryset or is not a queryset of CoderedPage models.",  # noqa
-                        indexer._meta.app_label, indexer.__class__.__name__, indexer.title
+                        (
+                            "Tried to filter by ClassifierTerm in PageListBlock, "
+                            "but <%s.%s ('%s')>.get_index_children() "
+                            "did not return a queryset or is not a queryset of "
+                            "CoderedPage models."
+                        ),
+                        indexer._meta.app_label,
+                        indexer.__class__.__name__,
+                        indexer.title,
                     )
         else:
             pages = indexer.get_children().live()
 
-        context['pages'] = pages[:value['num_posts']]
+        context["pages"] = pages[: value["num_posts"]]
         return context
 
 
@@ -256,39 +281,41 @@ class PagePreviewBlock(BaseBlock):
     """
     Renders a preview of a specific page.
     """
+
     page = blocks.PageChooserBlock(
         required=True,
-        label=_('Page to preview'),
-        help_text=_('Show a mini preview of the selected page.'),
+        label=_("Page to preview"),
+        help_text=_("Show a mini preview of the selected page."),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/pagepreview_block.html'
-        icon = 'doc-empty-inverse'
-        label = _('Page Preview')
+        template = "coderedcms/blocks/pagepreview_block.html"
+        icon = "doc-empty-inverse"
+        label = _("Page Preview")
 
 
 class QuoteBlock(BaseBlock):
     """
     A <blockquote>.
     """
+
     text = blocks.TextBlock(
         required=True,
         rows=4,
-        label=_('Quote Text'),
+        label=_("Quote Text"),
     )
     author = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Author'),
+        label=_("Author"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/quote_block.html'
-        icon = 'openquote'
-        label = _('Quote')
+        template = "coderedcms/blocks/quote_block.html"
+        icon = "openquote"
+        label = _("Quote")
 
 
 class RichTextBlock(blocks.RichTextBlock):
     class Meta:
-        template = 'coderedcms/blocks/rich_text_block.html'
+        template = "coderedcms/blocks/rich_text_block.html"

+ 28 - 28
coderedcms/blocks/layout_blocks.py

@@ -19,56 +19,55 @@ class ColumnBlock(BaseLayoutBlock):
     """
     Renders content in a column.
     """
+
     column_size = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_COL_SIZE_CHOICES,
         default=crx_settings.CRX_FRONTEND_COL_SIZE_DEFAULT,
         required=False,
-        label=_('Column size'),
+        label=_("Column size"),
     )
 
     advsettings_class = CoderedAdvColumnSettings
 
     class Meta:
-        template = 'coderedcms/blocks/column_block.html'
-        icon = 'placeholder'
-        label = 'Column'
+        template = "coderedcms/blocks/column_block.html"
+        icon = "placeholder"
+        label = "Column"
 
 
 class GridBlock(BaseLayoutBlock):
     """
     Renders a row of columns.
     """
+
     fluid = blocks.BooleanBlock(
         required=False,
-        label=_('Full width'),
+        label=_("Full width"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/grid_block.html'
-        icon = 'cr-columns'
-        label = _('Responsive Grid Row')
+        template = "coderedcms/blocks/grid_block.html"
+        icon = "cr-columns"
+        label = _("Responsive Grid Row")
 
     def __init__(self, local_blocks=None, **kwargs):
-        super().__init__(
-            local_blocks=[
-                ('content', ColumnBlock(local_blocks))
-            ]
-        )
+        super().__init__(local_blocks=[("content", ColumnBlock(local_blocks))])
 
 
 class CardGridBlock(BaseLayoutBlock):
     """
     Renders a row of cards.
     """
+
     fluid = blocks.BooleanBlock(
         required=False,
-        label=_('Full width'),
+        label=_("Full width"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/cardgrid_deck.html'
-        icon = 'cr-th-large'
-        label = _('Card Grid')
+        template = "coderedcms/blocks/cardgrid_deck.html"
+        icon = "cr-th-large"
+        label = _("Card Grid")
 
 
 class HeroBlock(BaseLayoutBlock):
@@ -79,34 +78,35 @@ class HeroBlock(BaseLayoutBlock):
     fluid = blocks.BooleanBlock(
         required=False,
         default=True,
-        label=_('Full width'),
+        label=_("Full width"),
     )
     is_parallax = blocks.BooleanBlock(
         required=False,
-        label=_('Parallax Effect'),
+        label=_("Parallax Effect"),
         help_text=_(
-            'Background images scroll slower than foreground images, creating an illusion of depth.'),  # noqa
+            "Background images scroll slower than foreground images, creating an illusion of depth."
+        ),
     )
     background_image = ImageChooserBlock(required=False)
     tile_image = blocks.BooleanBlock(
         required=False,
         default=False,
-        label=_('Tile background image'),
+        label=_("Tile background image"),
     )
     background_color = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Background color'),
-        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+        label=_("Background color"),
+        help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
     )
     foreground_color = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Text color'),
-        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+        label=_("Text color"),
+        help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
     )
 
     class Meta:
-        template = 'coderedcms/blocks/hero_block.html'
-        icon = 'cr-newspaper-o'
-        label = 'Hero Unit'
+        template = "coderedcms/blocks/hero_block.html"
+        icon = "cr-newspaper-o"
+        label = "Hero Unit"

+ 55 - 24
coderedcms/blocks/stream_form_blocks.py

@@ -4,10 +4,13 @@ from wagtail import blocks
 from coderedcms.wagtail_flexible_forms import blocks as form_blocks
 from coderedcms.blocks.base_blocks import BaseBlock, CoderedAdvSettings
 from coderedcms.forms import (
-    CoderedDateField, CoderedDateInput,
-    CoderedDateTimeField, CoderedDateTimeInput,
-    CoderedTimeField, CoderedTimeInput,
-    SecureFileField
+    CoderedDateField,
+    CoderedDateInput,
+    CoderedDateTimeField,
+    CoderedDateTimeInput,
+    CoderedTimeField,
+    CoderedTimeInput,
+    SecureFileField,
 )
 
 
@@ -16,16 +19,20 @@ class CoderedFormAdvSettings(CoderedAdvSettings):
     condition_trigger_id = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Condition Trigger ID'),
+        label=_("Condition Trigger ID"),
         help_text=_(
-            'The "Custom ID" of another field that that will trigger this field to be shown/hidden.')  # noqa
+            'The "Custom ID" of another field that that will trigger this '
+            "field to be shown/hidden."
+        ),
     )
     condition_trigger_value = blocks.CharBlock(
         required=False,
         max_length=255,
-        label=_('Condition Trigger Value'),
+        label=_("Condition Trigger Value"),
         help_text=_(
-            'The value of the field in "Condition Trigger ID" that will trigger this field to be shown.')  # noqa
+            'The value of the field in "Condition Trigger ID" that will '
+            "trigger this field to be shown."
+        ),
     )
 
 
@@ -36,53 +43,71 @@ class FormBlockMixin(BaseBlock):
     advsettings_class = CoderedFormAdvSettings
 
 
-class CoderedStreamFormFieldBlock(form_blocks.OptionalFormFieldBlock, FormBlockMixin):
+class CoderedStreamFormFieldBlock(
+    form_blocks.OptionalFormFieldBlock, FormBlockMixin
+):
     pass
 
 
-class CoderedStreamFormCharFieldBlock(form_blocks.CharFieldBlock, FormBlockMixin):
+class CoderedStreamFormCharFieldBlock(
+    form_blocks.CharFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Text or Email input")
         icon = "cr-window-minimize"
 
 
-class CoderedStreamFormTextFieldBlock(form_blocks.TextFieldBlock, FormBlockMixin):
+class CoderedStreamFormTextFieldBlock(
+    form_blocks.TextFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Multi-line text")
         icon = "cr-align-left"
 
 
-class CoderedStreamFormNumberFieldBlock(form_blocks.NumberFieldBlock, FormBlockMixin):
+class CoderedStreamFormNumberFieldBlock(
+    form_blocks.NumberFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Numbers only")
         icon = "cr-hashtag"
 
 
-class CoderedStreamFormCheckboxFieldBlock(form_blocks.CheckboxFieldBlock, FormBlockMixin):
+class CoderedStreamFormCheckboxFieldBlock(
+    form_blocks.CheckboxFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Single Checkbox")
         icon = "cr-check-square-o"
 
 
-class CoderedStreamFormRadioButtonsFieldBlock(form_blocks.RadioButtonsFieldBlock, FormBlockMixin):
+class CoderedStreamFormRadioButtonsFieldBlock(
+    form_blocks.RadioButtonsFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Radios")
         icon = "list-ul"
 
 
-class CoderedStreamFormDropdownFieldBlock(form_blocks.DropdownFieldBlock, FormBlockMixin):
+class CoderedStreamFormDropdownFieldBlock(
+    form_blocks.DropdownFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Dropdown")
         icon = "cr-list-alt"
 
 
-class CoderedStreamFormCheckboxesFieldBlock(form_blocks.CheckboxesFieldBlock, FormBlockMixin):
+class CoderedStreamFormCheckboxesFieldBlock(
+    form_blocks.CheckboxesFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Checkboxes")
         icon = "list-ul"
 
 
-class CoderedStreamFormDateFieldBlock(form_blocks.DateFieldBlock, FormBlockMixin):
+class CoderedStreamFormDateFieldBlock(
+    form_blocks.DateFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Date")
         icon = "date"
@@ -91,7 +116,9 @@ class CoderedStreamFormDateFieldBlock(form_blocks.DateFieldBlock, FormBlockMixin
     widget = CoderedDateInput
 
 
-class CoderedStreamFormTimeFieldBlock(form_blocks.TimeFieldBlock, FormBlockMixin):
+class CoderedStreamFormTimeFieldBlock(
+    form_blocks.TimeFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Time")
         icon = "time"
@@ -100,7 +127,9 @@ class CoderedStreamFormTimeFieldBlock(form_blocks.TimeFieldBlock, FormBlockMixin
     widget = CoderedTimeInput
 
 
-class CoderedStreamFormDateTimeFieldBlock(form_blocks.DateTimeFieldBlock, FormBlockMixin):
+class CoderedStreamFormDateTimeFieldBlock(
+    form_blocks.DateTimeFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Date and Time")
         icon = "date"
@@ -109,13 +138,17 @@ class CoderedStreamFormDateTimeFieldBlock(form_blocks.DateTimeFieldBlock, FormBl
     widget = CoderedDateTimeInput
 
 
-class CoderedStreamFormImageFieldBlock(form_blocks.ImageFieldBlock, FormBlockMixin):
+class CoderedStreamFormImageFieldBlock(
+    form_blocks.ImageFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Image Upload")
         icon = "image"
 
 
-class CoderedStreamFormFileFieldBlock(form_blocks.FileFieldBlock, FormBlockMixin):
+class CoderedStreamFormFileFieldBlock(
+    form_blocks.FileFieldBlock, FormBlockMixin
+):
     class Meta:
         label = _("Secure File Upload")
         icon = "upload"
@@ -128,7 +161,5 @@ class CoderedStreamFormStepBlock(form_blocks.FormStepBlock):
 
     def __init__(self, local_blocks=None, **kwargs):
         super().__init__(
-            local_blocks=[
-                ('form_fields', blocks.StreamBlock(local_blocks))
-            ]
+            local_blocks=[("form_fields", blocks.StreamBlock(local_blocks))]
         )

+ 12 - 7
coderedcms/fields.py

@@ -35,6 +35,7 @@ class CoderedStreamField(StreamField):
     Inspired by:
     https://cynthiakiser.com/blog/2022/01/06/trimming-wagtail-migration-cruft.html
     """
+
     def __init__(self, *args, **kwargs):
         """
         Patch init to work around django reconstruct not sending empty args.
@@ -64,12 +65,13 @@ class ColorField(models.CharField):
     """
     A CharField which uses the HTML5 color picker widget.
     """
+
     def __init__(self, *args, **kwargs):
-        kwargs['max_length'] = 255
+        kwargs["max_length"] = 255
         super().__init__(*args, **kwargs)
 
     def formfield(self, **kwargs):
-        kwargs['widget'] = ColorPickerWidget
+        kwargs["widget"] = ColorPickerWidget
         return super().formfield(**kwargs)
 
 
@@ -77,10 +79,13 @@ class MonospaceField(models.TextField):
     """
     A TextField which renders as a large HTML textarea with monospace font.
     """
+
     def formfield(self, **kwargs):
-        kwargs["widget"] = Textarea(attrs={
-            "rows": 12,
-            "class": "monospace",
-            "spellcheck": "false",
-        })
+        kwargs["widget"] = Textarea(
+            attrs={
+                "rows": 12,
+                "class": "monospace",
+                "spellcheck": "false",
+            }
+        )
         return super().formfield(**kwargs)

+ 78 - 46
coderedcms/forms.py

@@ -9,7 +9,9 @@ from django.core.exceptions import ValidationError
 from django.db import models
 from django.http import HttpResponse
 from django.utils.translation import gettext_lazy as _
-from wagtail.contrib.forms.views import SubmissionsListView as WagtailSubmissionsListView
+from wagtail.contrib.forms.views import (
+    SubmissionsListView as WagtailSubmissionsListView,
+)
 from wagtail.contrib.forms.forms import FormBuilder
 from wagtail.contrib.forms.models import AbstractFormField
 
@@ -17,40 +19,52 @@ from coderedcms.settings import crx_settings
 from coderedcms.utils import attempt_protected_media_value_conversion
 
 FORM_FIELD_CHOICES = (
-    (_("Text"), (
-        ("singleline", _("Single line text")),
-        ("multiline", _("Multi-line text")),
-        ("email", _("Email")),
-        ("number", _("Number - only allows integers")),
-        ("url", _("URL")),
-    ),),
-    (_("Choice"), (
-        ("checkboxes", _("Checkboxes")),
-        ("dropdown", _("Drop down")),
-        ("radio", _("Radio buttons")),
-        ("multiselect", _("Multiple select")),
-        ("checkbox", _("Single checkbox")),
-    ),),
-    (_("Date & Time"), (
-        ("date", _("Date")),
-        ("time", _("Time")),
-        ("datetime", _("Date and time")),
-    ),),
-    (_("File Upload"), (
-        ("file", _("Secure File - login required to access uploaded files")),
-    ),),
-    (_("Other"), (
-        ("hidden", _("Hidden field")),
-    ),),
+    (
+        _("Text"),
+        (
+            ("singleline", _("Single line text")),
+            ("multiline", _("Multi-line text")),
+            ("email", _("Email")),
+            ("number", _("Number - only allows integers")),
+            ("url", _("URL")),
+        ),
+    ),
+    (
+        _("Choice"),
+        (
+            ("checkboxes", _("Checkboxes")),
+            ("dropdown", _("Drop down")),
+            ("radio", _("Radio buttons")),
+            ("multiselect", _("Multiple select")),
+            ("checkbox", _("Single checkbox")),
+        ),
+    ),
+    (
+        _("Date & Time"),
+        (
+            ("date", _("Date")),
+            ("time", _("Time")),
+            ("datetime", _("Date and time")),
+        ),
+    ),
+    (
+        _("File Upload"),
+        (("file", _("Secure File - login required to access uploaded files")),),
+    ),
+    (
+        _("Other"),
+        (("hidden", _("Hidden field")),),
+    ),
 )
 
 
 # Files
 
+
 class SecureFileField(forms.FileField):
     custom_error_messages = {
-        'blacklist_file': _('Submitted file is not allowed.'),
-        'whitelist_file': _('Submitted file is not allowed.')
+        "blacklist_file": _("Submitted file is not allowed."),
+        "whitelist_file": _("Submitted file is not allowed."),
     }
 
     def __init__(self, **kwargs):
@@ -65,19 +79,26 @@ class SecureFileField(forms.FileField):
 
     def _check_whitelist(self, value):
         if crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_WHITELIST:
-            if os.path.splitext(value.name)[1].lower() not in crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_WHITELIST:  # noqa
-                raise ValidationError(self.error_messages['whitelist_file'])
+            if (
+                os.path.splitext(value.name)[1].lower()
+                not in crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_WHITELIST
+            ):
+                raise ValidationError(self.error_messages["whitelist_file"])
 
     def _check_blacklist(self, value):
         if crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST:
-            if os.path.splitext(value.name)[1].lower() in crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST:  # noqa
-                raise ValidationError(self.error_messages['blacklist_file'])
+            if (
+                os.path.splitext(value.name)[1].lower()
+                in crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST
+            ):
+                raise ValidationError(self.error_messages["blacklist_file"])
 
 
 # Date
 
+
 class CoderedDateInput(forms.DateInput):
-    template_name = 'coderedcms/formfields/date.html'
+    template_name = "coderedcms/formfields/date.html"
 
 
 class CoderedDateField(forms.DateField):
@@ -86,24 +107,31 @@ class CoderedDateField(forms.DateField):
 
 # Datetime
 
+
 class CoderedDateTimeInput(forms.DateTimeInput):
-    template_name = 'coderedcms/formfields/datetime.html'
+    template_name = "coderedcms/formfields/datetime.html"
 
 
 class CoderedDateTimeField(forms.DateTimeField):
     widget = CoderedDateTimeInput()
-    input_formats = ['%Y-%m-%dT%H:%M', '%m/%d/%Y %I:%M %p', '%m/%d/%Y %I:%M%p', '%m/%d/%Y %H:%M']
+    input_formats = [
+        "%Y-%m-%dT%H:%M",
+        "%m/%d/%Y %I:%M %p",
+        "%m/%d/%Y %I:%M%p",
+        "%m/%d/%Y %H:%M",
+    ]
 
 
 # Time
 
+
 class CoderedTimeInput(forms.TimeInput):
-    template_name = 'coderedcms/formfields/time.html'
+    template_name = "coderedcms/formfields/time.html"
 
 
 class CoderedTimeField(forms.TimeField):
     widget = CoderedTimeInput()
-    input_formats = ['%H:%M', '%I:%M %p', '%I:%M%p']
+    input_formats = ["%H:%M", "%I:%M %p", "%I:%M%p"]
 
 
 class CoderedFormBuilder(FormBuilder):
@@ -127,15 +155,19 @@ class CoderedFormBuilder(FormBuilder):
 class CoderedSubmissionsListView(WagtailSubmissionsListView):
     def get_csv_response(self, context):
         filename = self.get_csv_filename()
-        response = HttpResponse(content_type='text/csv; charset=utf-8')
-        response['Content-Disposition'] = 'attachment;filename={}'.format(filename)
+        response = HttpResponse(content_type="text/csv; charset=utf-8")
+        response["Content-Disposition"] = "attachment;filename={}".format(
+            filename
+        )
 
         writer = csv.writer(response)
-        writer.writerow(context['data_headings'])
-        for data_row in context['data_rows']:
+        writer.writerow(context["data_headings"])
+        for data_row in context["data_rows"]:
             modified_data_row = []
             for cell in data_row:
-                modified_cell = attempt_protected_media_value_conversion(self.request, cell)
+                modified_cell = attempt_protected_media_value_conversion(
+                    self.request, cell
+                )
                 modified_data_row.append(modified_cell)
 
             writer.writerow(modified_data_row)
@@ -147,11 +179,11 @@ class CoderedFormField(AbstractFormField):
         abstract = True
 
     field_type = models.CharField(
-        verbose_name=_('field type'),
+        verbose_name=_("field type"),
         max_length=16,
         choices=FORM_FIELD_CHOICES,
         blank=False,
-        default='Single line text'
+        default="Single line text",
     )
 
 
@@ -159,13 +191,13 @@ class SearchForm(forms.Form):
     s = forms.CharField(
         max_length=255,
         required=False,
-        label=_('Search'),
+        label=_("Search"),
     )
     t = forms.CharField(
         widget=forms.HiddenInput,
         max_length=255,
         required=False,
-        label=_('Page type'),
+        label=_("Page type"),
     )
 
 

+ 14 - 13
coderedcms/importexport.py

@@ -28,6 +28,7 @@ class ImportPagesFromCSVFileForm(forms.Form):
     https://github.com/torchbox/wagtail-import-export/blob/master/wagtailimportexport/forms.py#L29
     with addition of ``page_type``.
     """
+
     page_type = forms.ChoiceField(choices=get_page_model_choices)
 
     file = forms.FileField(label=_("File to import"))
@@ -36,7 +37,7 @@ class ImportPagesFromCSVFileForm(forms.Form):
         queryset=Page.objects.all(),
         widget=AdminPageChooser(can_choose_root=True, show_edit_link=False),
         label=_("Destination parent page"),
-        help_text=_("Imported pages will be created as children of this page.")
+        help_text=_("Imported pages will be created as children of this page."),
     )
 
 
@@ -46,7 +47,9 @@ def update_page_references(model, pages_by_original_id):
     https://github.com/torchbox/wagtail-import-export/blob/master/wagtailimportexport/importing.py#L67
     """
     for field in model._meta.get_fields():
-        if isinstance(field, models.ForeignKey) and issubclass(field.related_model, Page):
+        if isinstance(field, models.ForeignKey) and issubclass(
+            field.related_model, Page
+        ):
             linked_page_id = getattr(model, field.attname)
             try:
                 # see if the linked page is one of the ones we're importing
@@ -86,10 +89,10 @@ def import_pages(import_data, parent_page):
     # text / streamfields.
     page_content_type = ContentType.objects.get_for_model(Page)
 
-    for page_record in import_data['pages']:
+    for page_record in import_data["pages"]:
         # build a base Page instance from the exported content
         # (so that we pick up its title and other core attributes)
-        page = Page.from_serializable_data(page_record['content'])
+        page = Page.from_serializable_data(page_record["content"])
 
         # clear id and treebeard-related fields so that
         # they get reassigned when we save via add_child
@@ -102,22 +105,20 @@ def import_pages(import_data, parent_page):
         parent_page.add_child(instance=page)
 
         # Custom Code to add the new pk back into the original page record.
-        page_record['content']['pk'] = page.pk
+        page_record["content"]["pk"] = page.pk
 
         pages_by_original_id[page.id] = page
 
-    for page_record in import_data['pages']:
+    for page_record in import_data["pages"]:
         # Get the page model of the source page by app_label and model name
         # The content type ID of the source page is not in general the same
         # between the source and destination sites but the page model needs
         # to exist on both.
         # Raises LookupError exception if there is no matching model
-        model = apps.get_model(page_record['app_label'], page_record['model'])
+        model = apps.get_model(page_record["app_label"], page_record["model"])
 
         specific_page = model.from_serializable_data(
-            page_record['content'],
-            check_fks=False,
-            strict_fks=False
+            page_record["content"], check_fks=False, strict_fks=False
         )
         base_page = pages_by_original_id[specific_page.id]
         specific_page.base_page_ptr = base_page
@@ -126,7 +127,7 @@ def import_pages(import_data, parent_page):
         update_page_references(specific_page, pages_by_original_id)
         specific_page.save()
 
-    return len(import_data['pages'])
+    return len(import_data["pages"])
 
 
 def convert_csv_to_json(csv_file, page_type):
@@ -141,6 +142,6 @@ def convert_csv_to_json(csv_file, page_type):
     pages_csv_dict = csv.DictReader(csv_file)
     for row in pages_csv_dict:
         page_dict = copy.deepcopy(default_page_data)
-        page_dict['content'].update(row)
-        pages_json['pages'].append(page_dict)
+        page_dict["content"].update(row)
+        pages_json["pages"].append(page_dict)
     return pages_json

+ 100 - 72
coderedcms/models/integration_models.py

@@ -13,29 +13,40 @@ import json
 
 
 class MailchimpSubscriberIntegrationWidget(Input):
-    template_name = 'coderedcms/formfields/mailchimp/subscriber_integration_widget.html'
-    js_template_name = 'coderedcms/formfields/mailchimp/subscriber_integration_js.html'
+    template_name = (
+        "coderedcms/formfields/mailchimp/subscriber_integration_widget.html"
+    )
+    js_template_name = (
+        "coderedcms/formfields/mailchimp/subscriber_integration_js.html"
+    )
 
     def get_context(self, name, value, attrs):
-        ctx = super(MailchimpSubscriberIntegrationWidget, self).get_context(name, value, attrs)
+        ctx = super(MailchimpSubscriberIntegrationWidget, self).get_context(
+            name, value, attrs
+        )
 
         json_value = self.get_json_value(value)
         list_library = self.build_list_library()
-        ctx['widget']['value'] = json.dumps(json_value)
-        ctx['widget']['extra_js'] = self.render_js(name, list_library, json_value)
-        ctx['widget']['selectable_mailchimp_lists'] = self.get_selectable_mailchimp_lists(
-            list_library)
-        ctx['widget']['stored_mailchimp_list'] = self.get_stored_mailchimp_list(json_value)
+        ctx["widget"]["value"] = json.dumps(json_value)
+        ctx["widget"]["extra_js"] = self.render_js(
+            name, list_library, json_value
+        )
+        ctx["widget"][
+            "selectable_mailchimp_lists"
+        ] = self.get_selectable_mailchimp_lists(list_library)
+        ctx["widget"]["stored_mailchimp_list"] = self.get_stored_mailchimp_list(
+            json_value
+        )
 
         return ctx
 
     def render_js(self, name, list_library, json_value):
         ctx = {
-            'widget_name': name,
-            'widget_js_name': name.replace('-', '_'),
-            'list_library': list_library,
-            'stored_mailchimp_list': self.get_stored_mailchimp_list(json_value),
-            'stored_merge_fields': self.get_stored_merge_fields(json_value),
+            "widget_name": name,
+            "widget_js_name": name.replace("-", "_"),
+            "list_library": list_library,
+            "stored_mailchimp_list": self.get_stored_mailchimp_list(json_value),
+            "stored_merge_fields": self.get_stored_merge_fields(json_value),
         }
 
         return render_to_string(self.js_template_name, ctx)
@@ -44,30 +55,30 @@ class MailchimpSubscriberIntegrationWidget(Input):
         if value:
             json_value = json.loads(value)
         else:
-            json_value = json.loads('{}')
-        if 'list_id' not in json_value:
-            json_value['list_id'] = ""
-        if 'merge_fields' not in json_value:
-            json_value['merge_fields'] = {}
-        if 'email_field' not in json_value:
-            json_value['email_field'] = ""
-        if 'interest_categories' not in json_value:
-            json_value['interest_categories'] = {}
+            json_value = json.loads("{}")
+        if "list_id" not in json_value:
+            json_value["list_id"] = ""
+        if "merge_fields" not in json_value:
+            json_value["merge_fields"] = {}
+        if "email_field" not in json_value:
+            json_value["email_field"] = ""
+        if "interest_categories" not in json_value:
+            json_value["interest_categories"] = {}
         return json_value
 
     def get_stored_mailchimp_list(self, value):
-        if 'list_id' in value:
-            return str(value['list_id'])
+        if "list_id" in value:
+            return str(value["list_id"])
 
     def get_stored_merge_fields(self, value):
-        if 'merge_fields' in value:
-            return json.dumps(value['merge_fields'])
+        if "merge_fields" in value:
+            return json.dumps(value["merge_fields"])
         return json.dumps({})
 
     def get_selectable_mailchimp_lists(self, library):
-        selectable_lists = [('', '--- Select a Mailchimp List ---')]
+        selectable_lists = [("", "--- Select a Mailchimp List ---")]
         for k, v in library.items():
-            selectable_lists.append((k, v['name']))
+            selectable_lists.append((k, v["name"]))
 
         return selectable_lists
 
@@ -76,23 +87,34 @@ class MailchimpSubscriberIntegrationWidget(Input):
         list_library = {}
         if mailchimp.is_active:
             lists = mailchimp.get_lists()
-            for mlist in lists['lists']:
-                list_library[mlist['id']] = {
-                    'name': mlist['name'],
-                    'merge_fields': {},
-                    'interest_categories': {}
+            for mlist in lists["lists"]:
+                list_library[mlist["id"]] = {
+                    "name": mlist["name"],
+                    "merge_fields": {},
+                    "interest_categories": {},
                 }
 
-                list_library[mlist['id']]['merge_fields'] = \
-                    mailchimp.get_merge_fields_for_list(mlist['id'])['merge_fields']
-                list_library[mlist['id']]['interest_categories'] = \
-                    mailchimp.get_interest_categories_for_list(mlist['id'])['categories']
-
-                for category in list_library[mlist['id']]['interest_categories']:
-                    category['interests'] = mailchimp.get_interests_for_interest_category(
-                        mlist['id'],
-                        category['id']
-                    )['interests']
+                list_library[mlist["id"]][
+                    "merge_fields"
+                ] = mailchimp.get_merge_fields_for_list(mlist["id"])[
+                    "merge_fields"
+                ]
+                list_library[mlist["id"]][
+                    "interest_categories"
+                ] = mailchimp.get_interest_categories_for_list(mlist["id"])[
+                    "categories"
+                ]
+
+                for category in list_library[mlist["id"]][
+                    "interest_categories"
+                ]:
+                    category[
+                        "interests"
+                    ] = mailchimp.get_interests_for_interest_category(
+                        mlist["id"], category["id"]
+                    )[
+                        "interests"
+                    ]
 
         return list_library
 
@@ -101,62 +123,68 @@ class MailchimpSubscriberIntegration(models.Model):
     class Meta:
         abstract = True
 
-    subscriber_json_data = models.TextField(
-        blank=True,
-        verbose_name=_("List")
-    )
+    subscriber_json_data = models.TextField(blank=True, verbose_name=_("List"))
 
     def integration_operation(self, instance, **kwargs):
         mailchimp = MailchimpApi()
         if mailchimp.is_active:
-            submission_dict = kwargs['form_submission'].get_data()
+            submission_dict = kwargs["form_submission"].get_data()
             rendered_dictionary = self.render_dictionary(submission_dict)
-            mailchimp.add_user_to_list(list_id=self.get_list_id(), data=rendered_dictionary)
+            mailchimp.add_user_to_list(
+                list_id=self.get_list_id(), data=rendered_dictionary
+            )
 
     def get_data(self):
         return json.loads(self.subscriber_json_data)
 
     def get_merge_fields(self):
-        if 'merge_fields' in self.get_data():
-            return self.get_data()['merge_fields']
+        if "merge_fields" in self.get_data():
+            return self.get_data()["merge_fields"]
         return {}
 
     def get_list_id(self):
-        if 'list_id' in self.get_data():
-            return self.get_data()['list_id']
+        if "list_id" in self.get_data():
+            return self.get_data()["list_id"]
 
     def combine_interest_categories(self):
         interest_dict = {}
-        for category_id, value in self.get_data()['interest_categories'].items():
-            interest_dict.update(value['interests'])
+        for category_id, value in self.get_data()[
+            "interest_categories"
+        ].items():
+            interest_dict.update(value["interests"])
 
         return interest_dict
 
     def render_dictionary(self, form_submission):
-        rendered_dictionary_template = json.dumps({
-            'members': [
-                {
-                    'email_address': self.get_data()['email_field'],
-                    'merge_fields': self.get_data()['merge_fields'],
-                    'interests': self.combine_interest_categories(),
-                    'status': 'subscribed',
-                }
-            ],
-            'update_existing': True
-        })
-
-        rendered_dictionary = Template(
-            rendered_dictionary_template).render(Context(form_submission))
+        rendered_dictionary_template = json.dumps(
+            {
+                "members": [
+                    {
+                        "email_address": self.get_data()["email_field"],
+                        "merge_fields": self.get_data()["merge_fields"],
+                        "interests": self.combine_interest_categories(),
+                        "status": "subscribed",
+                    }
+                ],
+                "update_existing": True,
+            }
+        )
+
+        rendered_dictionary = Template(rendered_dictionary_template).render(
+            Context(form_submission)
+        )
         return rendered_dictionary
 
     panels = [
-        FieldPanel('subscriber_json_data', widget=MailchimpSubscriberIntegrationWidget)
+        FieldPanel(
+            "subscriber_json_data", widget=MailchimpSubscriberIntegrationWidget
+        )
     ]
 
 
-@hooks.register('form_page_submit')
+@hooks.register("form_page_submit")
 def run_mailchimp_subscriber_integrations(instance, **kwargs):
-    if hasattr(instance, 'integration_panels'):
+    if hasattr(instance, "integration_panels"):
         for panel in instance.integration_panels:
             for integration in getattr(instance, panel.relation_name).all():
                 integration.integration_operation(instance, **kwargs)

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 296 - 232
coderedcms/models/page_models.py


+ 161 - 147
coderedcms/models/snippet_models.py

@@ -16,9 +16,12 @@ from wagtail.models import Orderable
 from wagtail.snippets.models import register_snippet
 from wagtail.images import get_image_model_string
 
-from coderedcms.blocks import HTML_STREAMBLOCKS, LAYOUT_STREAMBLOCKS, NAVIGATION_STREAMBLOCKS
+from coderedcms.blocks import (
+    HTML_STREAMBLOCKS,
+    LAYOUT_STREAMBLOCKS,
+    NAVIGATION_STREAMBLOCKS,
+)
 from coderedcms.fields import CoderedStreamField
-from coderedcms.settings import crx_settings
 
 
 @register_snippet
@@ -28,37 +31,42 @@ class Carousel(ClusterableModel):
     Selected through Page StreamField bodies by the CarouselSnippetChooser in
     snippet_choosers.py
     """
+
     class Meta:
-        verbose_name = _('Carousel')
+        verbose_name = _("Carousel")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     show_controls = models.BooleanField(
         default=True,
-        verbose_name=_('Show controls'),
-        help_text=_('Shows arrows on the left and right of the carousel to advance next or previous slides.'),  # noqa
+        verbose_name=_("Show controls"),
+        help_text=_(
+            "Shows arrows on the left and right of the carousel to advance "
+            "next or previous slides."
+        ),
     )
     show_indicators = models.BooleanField(
         default=True,
-        verbose_name=_('Show indicators'),
-        help_text=_('Shows small indicators at the bottom of the carousel based on the number of slides.'),  # noqa
+        verbose_name=_("Show indicators"),
+        help_text=_(
+            "Shows small indicators at the bottom of the carousel based on the "
+            "number of slides."
+        ),
     )
 
-    panels = (
-        [
-            MultiFieldPanel(
-                heading=_('Slider'),
-                children=[
-                    FieldPanel('name'),
-                    FieldPanel('show_controls'),
-                    FieldPanel('show_indicators'),
-                ]
-            ),
-            InlinePanel('carousel_slides', label=_('Slides'))
-        ]
-    )
+    panels = [
+        MultiFieldPanel(
+            heading=_("Slider"),
+            children=[
+                FieldPanel("name"),
+                FieldPanel("show_controls"),
+                FieldPanel("show_indicators"),
+            ],
+        ),
+        InlinePanel("carousel_slides", label=_("Slides")),
+    ]
 
     def __str__(self):
         return self.name
@@ -69,37 +77,38 @@ class CarouselSlide(Orderable, models.Model):
     Represents a slide for the Carousel model. Can be modified through the
     snippets UI.
     """
+
     class Meta(Orderable.Meta):
-        verbose_name = _('Carousel Slide')
+        verbose_name = _("Carousel Slide")
 
     carousel = ParentalKey(
         Carousel,
-        related_name='carousel_slides',
-        verbose_name=_('Carousel'),
+        related_name="carousel_slides",
+        verbose_name=_("Carousel"),
     )
     image = models.ForeignKey(
         get_image_model_string(),
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
-        related_name='+',
-        verbose_name=_('Image'),
+        related_name="+",
+        verbose_name=_("Image"),
     )
     background_color = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Background color'),
-        help_text=_('Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)'),
+        verbose_name=_("Background color"),
+        help_text=_("Hexadecimal, rgba, or CSS color notation (e.g. #ff0011)"),
     )
     custom_css_class = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom CSS class'),
+        verbose_name=_("Custom CSS class"),
     )
     custom_id = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
 
     content = CoderedStreamField(
@@ -108,15 +117,13 @@ class CarouselSlide(Orderable, models.Model):
         use_json_field=True,
     )
 
-    panels = (
-        [
-            FieldPanel('image'),
-            FieldPanel('background_color'),
-            FieldPanel('custom_css_class'),
-            FieldPanel('custom_id'),
-            FieldPanel('content'),
-        ]
-    )
+    panels = [
+        FieldPanel("image"),
+        FieldPanel("background_color"),
+        FieldPanel("custom_css_class"),
+        FieldPanel("custom_id"),
+        FieldPanel("content"),
+    ]
 
 
 @register_snippet
@@ -124,24 +131,25 @@ class Classifier(ClusterableModel):
     """
     Simple and generic model to organize/categorize/group pages.
     """
+
     class Meta:
-        verbose_name = _('Classifier')
-        verbose_name_plural = _('Classifiers')
-        ordering = ['name']
+        verbose_name = _("Classifier")
+        verbose_name_plural = _("Classifiers")
+        ordering = ["name"]
 
     slug = models.SlugField(
         allow_unicode=True,
         unique=True,
-        verbose_name=_('Slug'),
+        verbose_name=_("Slug"),
     )
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
 
     panels = [
-        FieldPanel('name'),
-        InlinePanel('terms', label=_('Classifier Terms'))
+        FieldPanel("name"),
+        InlinePanel("terms", label=_("Classifier Terms")),
     ]
 
     def save(self, *args, **kwargs):
@@ -166,27 +174,28 @@ class ClassifierTerm(Orderable, models.Model):
     """
     Term used to categorize a page.
     """
+
     class Meta(Orderable.Meta):
-        verbose_name = _('Classifier Term')
-        verbose_name_plural = _('Classifier Terms')
+        verbose_name = _("Classifier Term")
+        verbose_name_plural = _("Classifier Terms")
 
     classifier = ParentalKey(
         Classifier,
-        related_name='terms',
-        verbose_name=_('Classifier'),
+        related_name="terms",
+        verbose_name=_("Classifier"),
     )
     slug = models.SlugField(
         allow_unicode=True,
         unique=True,
-        verbose_name=_('Slug'),
+        verbose_name=_("Slug"),
     )
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
 
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
     ]
 
     def save(self, *args, **kwargs):
@@ -212,40 +221,41 @@ class Navbar(models.Model):
     """
     Snippet for site navigation bars (header, main menu, etc.)
     """
+
     class Meta:
-        verbose_name = _('Navigation Bar')
+        verbose_name = _("Navigation Bar")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     custom_css_class = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom CSS Class'),
+        verbose_name=_("Custom CSS Class"),
     )
     custom_id = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
     menu_items = CoderedStreamField(
         NAVIGATION_STREAMBLOCKS,
-        verbose_name=_('Navigation links'),
+        verbose_name=_("Navigation links"),
         blank=True,
         use_json_field=True,
     )
 
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
         MultiFieldPanel(
             [
-                FieldPanel('custom_css_class'),
-                FieldPanel('custom_id'),
+                FieldPanel("custom_css_class"),
+                FieldPanel("custom_id"),
             ],
-            heading=_('Attributes')
+            heading=_("Attributes"),
         ),
-        FieldPanel('menu_items')
+        FieldPanel("menu_items"),
     ]
 
     def __str__(self):
@@ -257,40 +267,41 @@ class Footer(models.Model):
     """
     Snippet for website footer content.
     """
+
     class Meta:
-        verbose_name = _('Footer')
+        verbose_name = _("Footer")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     custom_css_class = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom CSS Class'),
+        verbose_name=_("Custom CSS Class"),
     )
     custom_id = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('Content'),
+        verbose_name=_("Content"),
         blank=True,
         use_json_field=True,
     )
 
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
         MultiFieldPanel(
             [
-                FieldPanel('custom_css_class'),
-                FieldPanel('custom_id'),
+                FieldPanel("custom_css_class"),
+                FieldPanel("custom_id"),
             ],
-            heading=_('Attributes')
+            heading=_("Attributes"),
         ),
-        FieldPanel('content')
+        FieldPanel("content"),
     ]
 
     def __str__(self):
@@ -302,25 +313,23 @@ class ReusableContent(models.Model):
     """
     Snippet for resusable content in streamfields.
     """
+
     class Meta:
-        verbose_name = _('Reusable Content')
-        verbose_name_plural = _('Reusable Content')
+        verbose_name = _("Reusable Content")
+        verbose_name_plural = _("Reusable Content")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('content'),
+        verbose_name=_("content"),
         blank=True,
         use_json_field=True,
     )
 
-    panels = [
-        FieldPanel('name'),
-        FieldPanel('content')
-    ]
+    panels = [FieldPanel("name"), FieldPanel("content")]
 
     def __str__(self):
         return self.name
@@ -329,26 +338,25 @@ class ReusableContent(models.Model):
 @register_snippet
 class Accordion(ClusterableModel):
     """Class for reusable content in a collapsible block."""
+
     class Meta:
-        verbose_name = _('Accordion')
+        verbose_name = _("Accordion")
         verbose_name_plural = _("Accordions")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
 
-    panels = (
-        [
-            MultiFieldPanel(
-                heading=_('Accordion'),
-                children=[
-                    FieldPanel('name'),
-                ]
-            ),
-            InlinePanel('accordion_panels', label=_('Panels'))
-        ]
-    )
+    panels = [
+        MultiFieldPanel(
+            heading=_("Accordion"),
+            children=[
+                FieldPanel("name"),
+            ],
+        ),
+        InlinePanel("accordion_panels", label=_("Panels")),
+    ]
 
     def __str__(self):
         return self.name
@@ -359,13 +367,13 @@ class AccordionPanel(Orderable, models.Model):
 
     accordion = ParentalKey(
         Accordion,
-        related_name='accordion_panels',
-        verbose_name=_('Accordion'),
+        related_name="accordion_panels",
+        verbose_name=_("Accordion"),
     )
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
 
     content = CoderedStreamField(
@@ -377,22 +385,20 @@ class AccordionPanel(Orderable, models.Model):
     custom_css_class = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom CSS class'),
+        verbose_name=_("Custom CSS class"),
     )
     custom_id = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
 
-    panels = (
-        [
-            FieldPanel('custom_css_class'),
-            FieldPanel('custom_id'),
-            FieldPanel("name"),
-            FieldPanel('content'),
-        ]
-    )
+    panels = [
+        FieldPanel("custom_css_class"),
+        FieldPanel("custom_id"),
+        FieldPanel("name"),
+        FieldPanel("content"),
+    ]
 
 
 @register_snippet
@@ -400,39 +406,42 @@ class ContentWall(models.Model):
     """
     Snippet that restricts access to a page with a modal.
     """
+
     class Meta:
-        verbose_name = _('Content Wall')
+        verbose_name = _("Content Wall")
 
     name = models.CharField(
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('Content'),
+        verbose_name=_("Content"),
         blank=True,
         use_json_field=True,
     )
     is_dismissible = models.BooleanField(
         default=True,
-        verbose_name=_('Dismissible'),
+        verbose_name=_("Dismissible"),
     )
     show_once = models.BooleanField(
         default=True,
-        verbose_name=_('Show once'),
-        help_text=_('Do not show the content wall to the same user again after it has been closed.')
+        verbose_name=_("Show once"),
+        help_text=_(
+            "Do not show the content wall to the same user again after it has been closed."
+        ),
     )
 
     panels = [
         MultiFieldPanel(
             [
-                FieldPanel('name'),
-                FieldPanel('is_dismissible'),
-                FieldPanel('show_once'),
+                FieldPanel("name"),
+                FieldPanel("is_dismissible"),
+                FieldPanel("show_once"),
             ],
-            heading=_('Content Wall')
+            heading=_("Content Wall"),
         ),
-        FieldPanel('content'),
+        FieldPanel("content"),
     ]
 
     def __str__(self):
@@ -444,57 +453,62 @@ class CoderedEmail(ClusterableModel):
     General purpose abstract clusterable model used for holding email information.
     Most likely this should be subclassed with addition of a ParentalKey.
     """
+
     class Meta:
         abstract = True
-        verbose_name = _('CodeRed Email')
+        verbose_name = _("CodeRed Email")
 
     to_address = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('To Addresses'),
-        help_text=_('Separate multiple email addresses with commas.')
+        verbose_name=_("To Addresses"),
+        help_text=_("Separate multiple email addresses with commas."),
     )
     from_address = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('From Address'),
-        help_text=_('For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes).')  # noqa
+        verbose_name=_("From Address"),
+        help_text=_(
+            'For example: "sender@example.com" or '
+            '"Sender Name <sender@example.com>" (without quotes).'
+        ),
     )
     reply_address = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('Reply-To Address'),
-        help_text=_('Separate multiple email addresses with commas.')
+        verbose_name=_("Reply-To Address"),
+        help_text=_("Separate multiple email addresses with commas."),
     )
     cc_address = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('CC'),
-        help_text=_('Separate multiple email addresses with commas.')
+        verbose_name=_("CC"),
+        help_text=_("Separate multiple email addresses with commas."),
     )
     bcc_address = models.CharField(
         max_length=255,
         blank=True,
-        verbose_name=_('BCC'),
-        help_text=_('Separate multiple email addresses with commas.')
-    )
-    subject = models.CharField(max_length=255, blank=True, verbose_name=_('Subject'))
-    body = models.TextField(blank=True, verbose_name=_('Body'))
-
-    panels = (
-        [
-            MultiFieldPanel(
-                [
-                    FieldPanel('to_address'),
-                    FieldPanel('from_address'),
-                    FieldPanel('cc_address'),
-                    FieldPanel('bcc_address'),
-                    FieldPanel('subject'),
-                    FieldPanel('body'),
-                ],
-                _('Email Message')
-            ),
-        ])
+        verbose_name=_("BCC"),
+        help_text=_("Separate multiple email addresses with commas."),
+    )
+    subject = models.CharField(
+        max_length=255, blank=True, verbose_name=_("Subject")
+    )
+    body = models.TextField(blank=True, verbose_name=_("Body"))
+
+    panels = [
+        MultiFieldPanel(
+            [
+                FieldPanel("to_address"),
+                FieldPanel("from_address"),
+                FieldPanel("cc_address"),
+                FieldPanel("bcc_address"),
+                FieldPanel("subject"),
+                FieldPanel("body"),
+            ],
+            _("Email Message"),
+        ),
+    ]
 
     def __str__(self):
         return self.subject

+ 11 - 3
coderedcms/models/tests/test_navbars_and_footers.py

@@ -30,7 +30,9 @@ class NavbarFooterTestCase(WagtailPageTests):
         self.navbar = Navbar.objects.create(name="Nav1", custom_id="Nav1")
         self.navbar2 = Navbar.objects.create(name="Nav2", custom_id="Nav2")
         self.footer = Footer.objects.create(name="Footer1", custom_id="Footer1")
-        self.footer2 = Footer.objects.create(name="Footer2", custom_id="Footer2")
+        self.footer2 = Footer.objects.create(
+            name="Footer2", custom_id="Footer2"
+        )
 
         # Populate settings.
         self.settings = LayoutSettings.for_site(self.site)
@@ -103,10 +105,16 @@ class NavbarFooterTestCase(WagtailPageTests):
         response = self.client.get(self.homepage.url, follow=True)
 
         self.assertContains(
-            response, text=f'<div id="{self.footer.custom_id}">', status_code=200, html=True
+            response,
+            text=f'<div id="{self.footer.custom_id}">',
+            status_code=200,
+            html=True,
         )
         self.assertNotContains(
-            response, text=f'<div id="{self.footer2.custom_id}">', status_code=200, html=True
+            response,
+            text=f'<div id="{self.footer2.custom_id}">',
+            status_code=200,
+            html=True,
         )
 
     def test_multi_footers(self):

+ 19 - 17
coderedcms/models/tests/test_page_models.py

@@ -12,7 +12,7 @@ from coderedcms.models.page_models import (
     CoderedPage,
     CoderedStreamFormPage,
     CoderedWebPage,
-    get_page_models
+    get_page_models,
 )
 from coderedcms.models.snippet_models import Classifier, ClassifierTerm
 from coderedcms.tests.testapp.models import (
@@ -25,23 +25,22 @@ from coderedcms.tests.testapp.models import (
     LocationIndexPage,
     LocationPage,
     StreamFormPage,
-    WebPage
+    WebPage,
 )
 
 
-class BasicPageTestCase():
+class BasicPageTestCase:
     """
     This is a testing mixin used to run common tests for basic versions of page types.
     """
+
     class Meta:
         abstract = True
 
     def setUp(self):
         self.client = Client()
-        self.basic_page = self.model(
-            title=str(self.model._meta.verbose_name)
-        )
-        self.homepage = WebPage.objects.get(url_path='/home/')
+        self.basic_page = self.model(title=str(self.model._meta.verbose_name))
+        self.homepage = WebPage.objects.get(url_path="/home/")
         self.homepage.add_child(instance=self.basic_page)
 
     def tearDown(self):
@@ -55,10 +54,11 @@ class BasicPageTestCase():
         self.assertEqual(response.status_code, 200)
 
 
-class AbstractPageTestCase():
+class AbstractPageTestCase:
     """
     This is a testing mixin used to run common tests for abstract page types.
     """
+
     class Meta:
         abstract = True
 
@@ -71,10 +71,11 @@ class AbstractPageTestCase():
         self.assertFalse(self.model in get_page_models())
 
 
-class ConcretePageTestCase():
+class ConcretePageTestCase:
     """
     This is a testing mixin used to run common tests for concrete page types.
     """
+
     class Meta:
         abstract = True
 
@@ -103,11 +104,7 @@ class ConcreteFormPageTestCase(ConcreteBasicPageTestCase):
         """
         # TODO: add form field via streamfield.
         response = self.client.post(
-            self.basic_page.url,
-            {
-                'name': 'Monty Python'
-            },
-            follow=True
+            self.basic_page.url, {"name": "Monty Python"}, follow=True
         )
         self.assertEqual(response.status_code, 200)
         # TODO: log in as superuser and get wagtail admin form submission page.
@@ -116,8 +113,12 @@ class ConcreteFormPageTestCase(ConcreteBasicPageTestCase):
         """
         Test to check if the default spam catching works.
         """
-        response = self.client.post(self.basic_page.url, {'cr-decoy-comments': 'This is Spam'}, follow=True)  # noqa
-        messages = list(response.context['messages'])
+        response = self.client.post(
+            self.basic_page.url,
+            {"cr-decoy-comments": "This is Spam"},
+            follow=True,
+        )
+        messages = list(response.context["messages"])
         self.assertEqual(len(messages), 1)
         self.assertEqual(str(messages[0]), self.basic_page.get_spam_message())
 
@@ -126,7 +127,7 @@ class ConcreteFormPageTestCase(ConcreteBasicPageTestCase):
         Test to check if the default spam catching won't mark correct posts as spam.
         """
         response = self.client.post(self.basic_page.url)
-        self.assertFalse(hasattr(response, 'is_spam'))
+        self.assertFalse(hasattr(response, "is_spam"))
 
 
 class CoderedArticleIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
@@ -219,6 +220,7 @@ class IndexTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
     """
     Tests indexing features (show/sort/filter child pages).
     """
+
     model = IndexTestPage
 
     def setUp(self):

+ 152 - 122
coderedcms/models/wagtailsettings_models.py

@@ -8,7 +8,12 @@ from django.db import models
 from django.utils.translation import gettext_lazy as _
 from modelcluster.fields import ParentalKey
 from modelcluster.models import ClusterableModel
-from wagtail.admin.panels import FieldPanel, InlinePanel, HelpPanel, MultiFieldPanel
+from wagtail.admin.panels import (
+    FieldPanel,
+    InlinePanel,
+    HelpPanel,
+    MultiFieldPanel,
+)
 from wagtail.models import Orderable
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.images import get_image_model_string
@@ -17,119 +22,129 @@ from coderedcms.settings import crx_settings
 from coderedcms.models.snippet_models import Navbar, Footer
 
 
-@register_setting(icon='cr-desktop')
+@register_setting(icon="cr-desktop")
 class LayoutSettings(ClusterableModel, BaseSetting):
     """
     Branding, navbar, and theme settings.
     """
+
     class Meta:
-        verbose_name = _('Layout')
+        verbose_name = _("Layout")
 
     logo = models.ForeignKey(
         get_image_model_string(),
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
-        related_name='+',
-        verbose_name=_('Logo'),
-        help_text=_('Brand logo used in the navbar and throughout the site')
+        related_name="+",
+        verbose_name=_("Logo"),
+        help_text=_("Brand logo used in the navbar and throughout the site"),
     )
     favicon = models.ForeignKey(
         get_image_model_string(),
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
-        related_name='favicon',
-        verbose_name=_('Favicon'),
+        related_name="favicon",
+        verbose_name=_("Favicon"),
     )
     navbar_color_scheme = models.CharField(
         blank=True,
         max_length=50,
         choices=None,
-        default='',
-        verbose_name=_('Navbar color scheme'),
-        help_text=_('Optimizes text and other navbar elements for use with light or dark backgrounds.'),  # noqa
+        default="",
+        verbose_name=_("Navbar color scheme"),
+        help_text=_(
+            "Optimizes text and other navbar elements for use with light or "
+            "dark backgrounds."
+        ),
     )
     navbar_class = models.CharField(
         blank=True,
         max_length=255,
-        default='',
-        verbose_name=_('Navbar CSS class'),
-        help_text=_('Custom classes applied to navbar e.g. "bg-light", "bg-dark", "bg-primary".'),
+        default="",
+        verbose_name=_("Navbar CSS class"),
+        help_text=_(
+            'Custom classes applied to navbar e.g. "bg-light", "bg-dark", "bg-primary".'
+        ),
     )
     navbar_fixed = models.BooleanField(
         default=False,
-        verbose_name=_('Fixed navbar'),
-        help_text=_('Fixed navbar will remain at the top of the page when scrolling.'),
+        verbose_name=_("Fixed navbar"),
+        help_text=_(
+            "Fixed navbar will remain at the top of the page when scrolling."
+        ),
     )
     navbar_content_fluid = models.BooleanField(
         default=False,
-        verbose_name=_('Full width navbar contents'),
-        help_text=_('Content within the navbar will fill edge to edge.'),
+        verbose_name=_("Full width navbar contents"),
+        help_text=_("Content within the navbar will fill edge to edge."),
     )
     navbar_collapse_mode = models.CharField(
         blank=True,
         max_length=50,
         choices=None,
-        default='',
-        verbose_name=_('Collapse navbar menu'),
-        help_text=_('Control on what screen sizes to show and collapse the navbar menu links.'),
+        default="",
+        verbose_name=_("Collapse navbar menu"),
+        help_text=_(
+            "Control on what screen sizes to show and collapse the navbar menu links."
+        ),
     )
     navbar_format = models.CharField(
         blank=True,
         max_length=50,
         choices=None,
-        default='',
-        verbose_name=_('Navbar format'),
+        default="",
+        verbose_name=_("Navbar format"),
     )
     navbar_search = models.BooleanField(
         default=True,
-        verbose_name=_('Search box'),
-        help_text=_('Show search box in navbar')
+        verbose_name=_("Search box"),
+        help_text=_("Show search box in navbar"),
     )
     frontend_theme = models.CharField(
         blank=True,
         max_length=50,
         choices=None,
-        default='',
-        verbose_name=_('Theme variant'),
+        default="",
+        verbose_name=_("Theme variant"),
     )
 
     panels = [
         MultiFieldPanel(
             [
-                FieldPanel('logo'),
-                FieldPanel('favicon'),
+                FieldPanel("logo"),
+                FieldPanel("favicon"),
             ],
-            heading=_('Branding')
+            heading=_("Branding"),
         ),
         InlinePanel(
-            'site_navbar',
-            help_text=_('Choose one or more navbars for your site.'),
-            heading=_('Site Navbars')
+            "site_navbar",
+            help_text=_("Choose one or more navbars for your site."),
+            heading=_("Site Navbars"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('navbar_color_scheme'),
-                FieldPanel('navbar_class'),
-                FieldPanel('navbar_fixed'),
-                FieldPanel('navbar_content_fluid'),
-                FieldPanel('navbar_collapse_mode'),
-                FieldPanel('navbar_format'),
-                FieldPanel('navbar_search'),
+                FieldPanel("navbar_color_scheme"),
+                FieldPanel("navbar_class"),
+                FieldPanel("navbar_fixed"),
+                FieldPanel("navbar_content_fluid"),
+                FieldPanel("navbar_collapse_mode"),
+                FieldPanel("navbar_format"),
+                FieldPanel("navbar_search"),
             ],
-            heading=_('Site Navbar Layout')
+            heading=_("Site Navbar Layout"),
         ),
         InlinePanel(
-            'site_footer',
-            help_text=_('Choose one or more footers for your site.'),
-            heading=_('Site Footers')
+            "site_footer",
+            help_text=_("Choose one or more footers for your site."),
+            heading=_("Site Footers"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('frontend_theme'),
+                FieldPanel("frontend_theme"),
             ],
-            heading=_('Theming')
+            heading=_("Theming"),
         ),
     ]
 
@@ -140,24 +155,28 @@ class LayoutSettings(ClusterableModel, BaseSetting):
         """
         super().__init__(*args, **kwargs)
         # Set choices dynamically.
-        self._meta.get_field('frontend_theme').choices = (
-            crx_settings.CRX_FRONTEND_THEME_CHOICES
-        )
-        self._meta.get_field('navbar_collapse_mode').choices = (
-            crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES
-        )
-        self._meta.get_field('navbar_color_scheme').choices = (
-            crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES
-        )
-        self._meta.get_field('navbar_format').choices = (
-            crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_CHOICES
-        )
+        self._meta.get_field(
+            "frontend_theme"
+        ).choices = crx_settings.CRX_FRONTEND_THEME_CHOICES
+        self._meta.get_field(
+            "navbar_collapse_mode"
+        ).choices = crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES
+        self._meta.get_field(
+            "navbar_color_scheme"
+        ).choices = crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES
+        self._meta.get_field(
+            "navbar_format"
+        ).choices = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_CHOICES
         # Set default dynamically.
         if not self.id:
             self.frontend_theme = crx_settings.CRX_FRONTEND_THEME_DEFAULT
             self.navbar_class = crx_settings.CRX_FRONTEND_NAVBAR_CLASS_DEFAULT
-            self.navbar_collapse_mode = crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT
-            self.navbar_color_scheme = crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT
+            self.navbar_collapse_mode = (
+                crx_settings.CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT
+            )
+            self.navbar_color_scheme = (
+                crx_settings.CRX_FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT
+            )
             self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT
 
 
@@ -165,7 +184,7 @@ class NavbarOrderable(Orderable, models.Model):
     navbar_chooser = ParentalKey(
         LayoutSettings,
         related_name="site_navbar",
-        verbose_name=_('Site Navbars')
+        verbose_name=_("Site Navbars"),
     )
     navbar = models.ForeignKey(
         Navbar,
@@ -174,16 +193,14 @@ class NavbarOrderable(Orderable, models.Model):
         on_delete=models.CASCADE,
     )
 
-    panels = [
-        FieldPanel("navbar")
-    ]
+    panels = [FieldPanel("navbar")]
 
 
 class FooterOrderable(Orderable, models.Model):
     footer_chooser = ParentalKey(
         LayoutSettings,
         related_name="site_footer",
-        verbose_name=_('Site Footers')
+        verbose_name=_("Site Footers"),
     )
     footer = models.ForeignKey(
         Footer,
@@ -192,115 +209,124 @@ class FooterOrderable(Orderable, models.Model):
         on_delete=models.CASCADE,
     )
 
-    panels = [
-        FieldPanel("footer")
-    ]
+    panels = [FieldPanel("footer")]
 
 
-@register_setting(icon='cr-google')
+@register_setting(icon="cr-google")
 class AnalyticsSettings(BaseSetting):
     """
     Tracking and Google Analytics.
     """
+
     class Meta:
-        verbose_name = _('Tracking')
+        verbose_name = _("Tracking")
 
     ga_tracking_id = models.CharField(
         blank=True,
         max_length=255,
-        verbose_name=_('UA Tracking ID'),
-        help_text=_('Your Google "Universal Analytics" tracking ID (begins with "UA-")'),
+        verbose_name=_("UA Tracking ID"),
+        help_text=_(
+            'Your Google "Universal Analytics" tracking ID (begins with "UA-")'
+        ),
     )
     ga_g_tracking_id = models.CharField(
         blank=True,
         max_length=255,
-        verbose_name=_('G Tracking ID'),
+        verbose_name=_("G Tracking ID"),
         help_text=_('Your Google Analytics 4 tracking ID (begins with "G-")'),
     )
     ga_track_button_clicks = models.BooleanField(
         default=False,
-        verbose_name=_('Track button clicks'),
-        help_text=_('Track all button clicks using Google Analytics event tracking. Event tracking details can be specified in each button’s advanced settings options.'),  # noqa
+        verbose_name=_("Track button clicks"),
+        help_text=_(
+            "Track all button clicks using Google Analytics event tracking. "
+            "Event tracking details can be specified in each button’s advanced "
+            "settings options."
+        ),
     )
     gtm_id = models.CharField(
         blank=True,
         max_length=255,
-        verbose_name=_('Google Tag Manager ID'),
+        verbose_name=_("Google Tag Manager ID"),
         help_text=_('Begins with "GTM-"'),
     )
     head_scripts = MonospaceField(
         blank=True,
         null=True,
-        verbose_name=_('<head> tracking scripts'),
-        help_text=_('Add tracking scripts between the <head> tags.'),
+        verbose_name=_("<head> tracking scripts"),
+        help_text=_("Add tracking scripts between the <head> tags."),
     )
     body_scripts = MonospaceField(
         blank=True,
         null=True,
-        verbose_name=_('<body> tracking scripts'),
-        help_text=_('Add tracking scripts toward closing <body> tag.'),
+        verbose_name=_("<body> tracking scripts"),
+        help_text=_("Add tracking scripts toward closing <body> tag."),
     )
 
     panels = [
         HelpPanel(
-            heading=_('Know your tracking'),
+            heading=_("Know your tracking"),
             content=_(
-                '<h3><b>Which tracking IDs do I need?</b></h3>'
-                '<p>Before adding tracking to your site, '
+                "<h3><b>Which tracking IDs do I need?</b></h3>"
+                "<p>Before adding tracking to your site, "
                 '<a href="https://docs.coderedcorp.com/wagtail-crx/how_to/add_tracking_scripts.html" '  # noqa
                 'target="_blank">read about the difference between UA, G, GTM, '
-                'and other tracking IDs</a>.</p>'
+                "and other tracking IDs</a>.</p>"
             ),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('ga_tracking_id'),
-                FieldPanel('ga_g_tracking_id'),
-                FieldPanel('ga_track_button_clicks'),
+                FieldPanel("ga_tracking_id"),
+                FieldPanel("ga_g_tracking_id"),
+                FieldPanel("ga_track_button_clicks"),
             ],
-            heading=_('Google Analytics'),
+            heading=_("Google Analytics"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('gtm_id'),
+                FieldPanel("gtm_id"),
             ],
-            heading=_('Google Tag Manager'),
+            heading=_("Google Tag Manager"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('head_scripts'),
-                FieldPanel('body_scripts'),
+                FieldPanel("head_scripts"),
+                FieldPanel("body_scripts"),
             ],
-            heading=_('Other Tracking Scripts')
-        )
+            heading=_("Other Tracking Scripts"),
+        ),
     ]
 
 
-@register_setting(icon='cr-universal-access')
+@register_setting(icon="cr-universal-access")
 class ADASettings(BaseSetting):
     """
     Accessibility related options.
     """
+
     class Meta:
-        verbose_name = 'Accessibility'
+        verbose_name = "Accessibility"
 
     skip_navigation = models.BooleanField(
         default=False,
-        verbose_name=_('Show skip navigation link'),
-        help_text=_('Shows a "Skip Navigation" link above the navbar that takes you directly to the main content.'),  # noqa
+        verbose_name=_("Show skip navigation link"),
+        help_text=_(
+            'Shows a "Skip Navigation" link above the navbar that takes you '
+            "directly to the main content."
+        ),
     )
 
     panels = [
         MultiFieldPanel(
             [
-                FieldPanel('skip_navigation'),
+                FieldPanel("skip_navigation"),
             ],
-            heading=_('Accessibility')
+            heading=_("Accessibility"),
         )
     ]
 
 
-@register_setting(icon='cog')
+@register_setting(icon="cog")
 class GeneralSettings(BaseSetting):
     """
     Various site-wide settings. A good place to put
@@ -308,73 +334,77 @@ class GeneralSettings(BaseSetting):
     """
 
     from_email_address = models.CharField(
-
         blank=True,
         max_length=255,
-        verbose_name=_('From email address'),
-        help_text=_('The default email address this site appears to send from. For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes)'),  # noqa
+        verbose_name=_("From email address"),
+        help_text=_(
+            "The default email address this site appears to send from. "
+            'For example: "sender@example.com" or '
+            '"Sender Name <sender@example.com>" (without quotes)'
+        ),
     )
     search_num_results = models.PositiveIntegerField(
         default=10,
-        verbose_name=_('Number of results per page'),
+        verbose_name=_("Number of results per page"),
     )
     external_new_tab = models.BooleanField(
-        default=False,
-        verbose_name=_('Open all external links in new tab')
+        default=False, verbose_name=_("Open all external links in new tab")
     )
 
     panels = [
         MultiFieldPanel(
             [
-                FieldPanel('from_email_address'),
+                FieldPanel("from_email_address"),
             ],
-            _('Email')
+            _("Email"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('search_num_results'),
+                FieldPanel("search_num_results"),
             ],
-            _('Search Settings')
+            _("Search Settings"),
         ),
         MultiFieldPanel(
             [
-                FieldPanel('external_new_tab'),
+                FieldPanel("external_new_tab"),
             ],
-            _('Links')
+            _("Links"),
         ),
     ]
 
     class Meta:
-        verbose_name = _('General')
+        verbose_name = _("General")
 
 
-@register_setting(icon='cr-puzzle-piece')
+@register_setting(icon="cr-puzzle-piece")
 class GoogleApiSettings(BaseSetting):
     """
     Settings for Google API services.
     """
+
     class Meta:
-        verbose_name = _('Google API')
+        verbose_name = _("Google API")
 
     google_maps_api_key = models.CharField(
         blank=True,
         max_length=255,
-        verbose_name=_('Google Maps API Key'),
-        help_text=_('The API Key used for Google Maps.')
+        verbose_name=_("Google Maps API Key"),
+        help_text=_("The API Key used for Google Maps."),
     )
 
 
-@register_setting(icon='cr-puzzle-piece')
+@register_setting(icon="cr-puzzle-piece")
 class MailchimpApiSettings(BaseSetting):
     """
     Settings for Mailchimp API services.
     """
+
     class Meta:
-        verbose_name = _('Mailchimp API')
+        verbose_name = _("Mailchimp API")
 
     mailchimp_api_key = models.CharField(
         blank=True,
         max_length=255,
-        verbose_name=_('Mailchimp API Key'),
-        help_text=_('The API Key used for Mailchimp.')
+        verbose_name=_("Mailchimp API Key"),
+        help_text=_("The API Key used for Mailchimp."),
     )

+ 3 - 1
coderedcms/project_template/basic/manage.py

@@ -3,7 +3,9 @@ import os
 import sys
 
 if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+    os.environ.setdefault(
+        "DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev"
+    )
 
     from django.core.management import execute_from_command_line
 

+ 72 - 82
coderedcms/project_template/basic/project_name/settings/base.py

@@ -26,95 +26,87 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 
 INSTALLED_APPS = [
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
-    'wagtail.contrib.forms',
-    'wagtail.contrib.redirects',
-    'wagtail.embeds',
-    'wagtail.sites',
-    'wagtail.users',
-    'wagtail.snippets',
-    'wagtail.documents',
-    'wagtail.images',
-    'wagtail.search',
-    'wagtail',
-    'wagtail.contrib.settings',
-    'wagtail.contrib.modeladmin',
-    'wagtail.contrib.table_block',
-    'wagtail.admin',
-
+    "wagtail.contrib.forms",
+    "wagtail.contrib.redirects",
+    "wagtail.embeds",
+    "wagtail.sites",
+    "wagtail.users",
+    "wagtail.snippets",
+    "wagtail.documents",
+    "wagtail.images",
+    "wagtail.search",
+    "wagtail",
+    "wagtail.contrib.settings",
+    "wagtail.contrib.modeladmin",
+    "wagtail.contrib.table_block",
+    "wagtail.admin",
     # Django
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'django.contrib.sitemaps',
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "django.contrib.sitemaps",
 ]
 
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.common.CommonMiddleware',
-
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.common.CommonMiddleware",
     # Security
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "django.middleware.security.SecurityMiddleware",
     #  Error reporting. Uncomment this to receive emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 
-ROOT_URLCONF = '{{ project_name }}.urls'
+ROOT_URLCONF = "{{ project_name }}.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
+WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
     }
 }
 
@@ -124,16 +116,16 @@ DATABASES = {
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -141,13 +133,11 @@ AUTH_PASSWORD_VALIDATORS = [
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 
 # To add or change language of the project, modify the list below.
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-LANGUAGES = [
-    ('en-us', _('English'))
-]
+LANGUAGES = [("en-us", _("English"))]
 
-TIME_ZONE = 'America/New_York'
+TIME_ZONE = "America/New_York"
 
 USE_I18N = True
 
@@ -158,38 +148,38 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 STATICFILES_FINDERS = [
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
-STATIC_URL = '/static/'
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_URL = "/static/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_URL = "/media/"
 
 
 # Login
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 # Wagtail settings
 
-WAGTAIL_SITE_NAME = '{{ sitename }}'
+WAGTAIL_SITE_NAME = "{{ sitename }}"
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 WAGTAILSEARCH_BACKENDS = {
-    'default': {
-        'BACKEND': 'wagtail.search.backends.database',
+    "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
-WAGTAILADMIN_BASE_URL = 'http://{{ domain }}'
+WAGTAILADMIN_BASE_URL = "http://{{ domain }}"
 
 
 # Tags
@@ -199,4 +189,4 @@ TAGGIT_CASE_INSENSITIVE = True
 
 # Sets default for primary key IDs
 # See https://docs.djangoproject.com/en/{{ docs_version }}/ref/models/fields/#bigautofield
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

+ 3 - 3
coderedcms/project_template/basic/project_name/settings/dev.py

@@ -4,11 +4,11 @@ from .base import *  # noqa
 DEBUG = True
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '{{ secret_key }}'
+SECRET_KEY = "{{ secret_key }}"
 
-ALLOWED_HOSTS = ['*']
+ALLOWED_HOSTS = ["*"]
 
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 
 WAGTAIL_CACHE = False
 

+ 26 - 23
coderedcms/project_template/basic/project_name/settings/prod.py

@@ -4,22 +4,22 @@ from .base import *  # noqa
 DEBUG = False
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '{{ secret_key }}'
+SECRET_KEY = "{{ secret_key }}"
 
 # Add your site's domain name(s) here.
-ALLOWED_HOSTS = ['{{ domain }}']
+ALLOWED_HOSTS = ["{{ domain }}"]
 
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-backend
-EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend'
+EMAIL_BACKEND = "django_sendmail_backend.backends.EmailBackend"
 
 # Default email address used to send messages from the website.
-DEFAULT_FROM_EMAIL = '{{ sitename }} <info@{{ domain_nowww }}>'
+DEFAULT_FROM_EMAIL = "{{ sitename }} <info@{{ domain_nowww }}>"
 
 # A list of people who get error notifications.
 ADMINS = [
-    ('Administrator', 'admin@{{ domain_nowww }}'),
+    ("Administrator", "admin@{{ domain_nowww }}"),
 ]
 
 # A list in the same format as ADMINS that specifies who should get broken link
@@ -43,31 +43,34 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
-            'loaders': [
-                ('django.template.loaders.cached.Loader', [
-                    'django.template.loaders.filesystem.Loader',
-                    'django.template.loaders.app_directories.Loader',
-                ]),
+            "loaders": [
+                (
+                    "django.template.loaders.cached.Loader",
+                    [
+                        "django.template.loaders.filesystem.Loader",
+                        "django.template.loaders.app_directories.Loader",
+                    ],
+                ),
             ],
         },
     },
 ]
 
 CACHES = {
-    'default': {
-        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
-        'LOCATION': os.path.join(BASE_DIR, 'cache'),  # noqa
-        'KEY_PREFIX': 'coderedcms',
-        'TIMEOUT': 14400,  # in seconds
+    "default": {
+        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
+        "LOCATION": os.path.join(BASE_DIR, "cache"),  # noqa
+        "KEY_PREFIX": "coderedcms",
+        "TIMEOUT": 14400,  # in seconds
     }
 }
 

+ 5 - 9
coderedcms/project_template/basic/project_name/urls.py

@@ -8,20 +8,16 @@ from coderedcms import urls as codered_urls
 
 urlpatterns = [
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # the page serving mechanism. This should be the last pattern in
     # the list:
-    path('', include(codered_urls)),
-
+    path("", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     #    path("pages/", include(codered_urls)),

+ 3 - 1
coderedcms/project_template/basic/project_name/wsgi.py

@@ -11,6 +11,8 @@ import os
 
 from django.core.wsgi import get_wsgi_application
 
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+os.environ.setdefault(
+    "DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev"
+)
 
 application = get_wsgi_application()

+ 1 - 1
coderedcms/project_template/basic/website/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class WebsiteConfig(AppConfig):
-    name = 'website'
+    name = "website"

+ 23 - 17
coderedcms/project_template/basic/website/models.py

@@ -8,7 +8,7 @@ from coderedcms.models import (
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 
 
@@ -16,65 +16,71 @@ class ArticlePage(CoderedArticlePage):
     """
     Article, suitable for news or blog content.
     """
+
     class Meta:
-        verbose_name = 'Article'
-        ordering = ['-first_published_at']
+        verbose_name = "Article"
+        ordering = ["-first_published_at"]
 
     # Only allow this page to be created beneath an ArticleIndexPage.
-    parent_page_types = ['website.ArticleIndexPage']
+    parent_page_types = ["website.ArticleIndexPage"]
 
-    template = 'coderedcms/pages/article_page.html'
-    search_template = 'coderedcms/pages/article_page.search.html'
+    template = "coderedcms/pages/article_page.html"
+    search_template = "coderedcms/pages/article_page.search.html"
 
 
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     Shows a list of article sub-pages.
     """
+
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
     # Only allow ArticlePages beneath this page.
-    subpage_types = ['website.ArticlePage']
+    subpage_types = ["website.ArticlePage"]
 
-    template = 'coderedcms/pages/article_index_page.html'
+    template = "coderedcms/pages/article_index_page.html"
 
 
 class FormPage(CoderedFormPage):
     """
     A page with an html <form>.
     """
+
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 class FormPageField(CoderedFormField):
     """
     A field that links to a FormPage.
     """
+
     class Meta:
-        ordering = ['sort_order']
+        ordering = ["sort_order"]
 
-    page = ParentalKey('FormPage', related_name='form_fields')
+    page = ParentalKey("FormPage", related_name="form_fields")
 
 
 class FormConfirmEmail(CoderedEmail):
     """
     Sends a confirmation email after submitting a FormPage.
     """
-    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+    page = ParentalKey("FormPage", related_name="confirmation_emails")
 
 
 class WebPage(CoderedWebPage):
     """
     General use page with featureful streamfield and SEO attributes.
     """
+
     class Meta:
-        verbose_name = 'Web Page'
+        verbose_name = "Web Page"
 
-    template = 'coderedcms/pages/web_page.html'
+    template = "coderedcms/pages/web_page.html"

+ 3 - 1
coderedcms/project_template/sass/manage.py

@@ -3,7 +3,9 @@ import os
 import sys
 
 if __name__ == "__main__":
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+    os.environ.setdefault(
+        "DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev"
+    )
 
     from django.core.management import execute_from_command_line
 

+ 70 - 78
coderedcms/project_template/sass/project_name/settings/base.py

@@ -25,95 +25,87 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 
 INSTALLED_APPS = [
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
-    'wagtail.contrib.forms',
-    'wagtail.contrib.redirects',
-    'wagtail.embeds',
-    'wagtail.sites',
-    'wagtail.users',
-    'wagtail.snippets',
-    'wagtail.documents',
-    'wagtail.images',
-    'wagtail.search',
-    'wagtail',
-    'wagtail.contrib.settings',
-    'wagtail.contrib.modeladmin',
-    'wagtail.contrib.table_block',
-    'wagtail.admin',
-
+    "wagtail.contrib.forms",
+    "wagtail.contrib.redirects",
+    "wagtail.embeds",
+    "wagtail.sites",
+    "wagtail.users",
+    "wagtail.snippets",
+    "wagtail.documents",
+    "wagtail.images",
+    "wagtail.search",
+    "wagtail",
+    "wagtail.contrib.settings",
+    "wagtail.contrib.modeladmin",
+    "wagtail.contrib.table_block",
+    "wagtail.admin",
     # Django
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
     "django.contrib.sitemaps",
 ]
 
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.common.CommonMiddleware',
-
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.common.CommonMiddleware",
     # Security
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "django.middleware.security.SecurityMiddleware",
     # Error reporting. Uncomment this to receive emails when a 404 is triggered.
-    #'django.middleware.common.BrokenLinkEmailsMiddleware',
-
+    # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 
-ROOT_URLCONF = '{{ project_name }}.urls'
+ROOT_URLCONF = "{{ project_name }}.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
+WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
     }
 }
 
@@ -123,16 +115,16 @@ DATABASES = {
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -140,9 +132,9 @@ AUTH_PASSWORD_VALIDATORS = [
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-TIME_ZONE = 'America/New_York'
+TIME_ZONE = "America/New_York"
 
 USE_I18N = False
 
@@ -153,21 +145,21 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 STATICFILES_FINDERS = [
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
-STATIC_URL = '/static/'
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_URL = "/static/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_URL = "/media/"
 
 
 # Login
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 # Wagtail settings
@@ -177,14 +169,14 @@ WAGTAIL_SITE_NAME = "{{ sitename }}"
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 WAGTAILSEARCH_BACKENDS = {
-    'default': {
-        'BACKEND': 'wagtail.search.backends.database',
+    "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
-WAGTAILADMIN_BASE_URL = 'http://{{ domain }}'
+WAGTAILADMIN_BASE_URL = "http://{{ domain }}"
 
 
 # Tags
@@ -194,4 +186,4 @@ TAGGIT_CASE_INSENSITIVE = True
 
 # Sets default for primary key IDs
 # See https://docs.djangoproject.com/en/{{ docs_version }}/ref/models/fields/#bigautofield
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

+ 8 - 6
coderedcms/project_template/sass/project_name/settings/dev.py

@@ -1,20 +1,22 @@
-from .base import *
+from .base import *  # noqa
 
 # 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 = '{{ secret_key }}'
+SECRET_KEY = "{{ secret_key }}"
 
-ALLOWED_HOSTS = ['*']
+ALLOWED_HOSTS = ["*"]
 
-INSTALLED_APPS += ['django_sass',]
+INSTALLED_APPS += [  # noqa
+    "django_sass",
+]
 
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 
 WAGTAIL_CACHE = False
 
 try:
-    from .local_settings import *
+    from .local import *  # noqa
 except ImportError:
     pass

+ 29 - 31
coderedcms/project_template/sass/project_name/settings/prod.py

@@ -1,25 +1,25 @@
-from .base import *
+from .base import *  # noqa
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = '{{ secret_key }}'
+SECRET_KEY = "{{ secret_key }}"
 
 # Add your site's domain name(s) here.
-ALLOWED_HOSTS = ['{{ domain }}']
+ALLOWED_HOSTS = ["{{ domain }}"]
 
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-backend
-EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend'
+EMAIL_BACKEND = "django_sendmail_backend.backends.EmailBackend"
 
 # Default email address used to send messages from the website.
-DEFAULT_FROM_EMAIL = '{{ sitename }} <info@{{ domain_nowww }}>'
+DEFAULT_FROM_EMAIL = "{{ sitename }} <info@{{ domain_nowww }}>"
 
 # A list of people who get error notifications.
 ADMINS = [
-    ('Administrator', 'admin@{{ domain_nowww }}'),
+    ("Administrator", "admin@{{ domain_nowww }}"),
 ]
 
 # A list in the same format as ADMINS that specifies who should get broken link
@@ -29,7 +29,7 @@ MANAGERS = ADMINS
 # Email address used to send error messages to ADMINS.
 SERVER_EMAIL = DEFAULT_FROM_EMAIL
 
-#DATABASES = {
+# DATABASES = {
 #    'default': {
 #        'ENGINE': 'django.db.backends.mysql',
 #        'HOST': 'localhost',
@@ -37,41 +37,39 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 #        'USER': '{{ project_name }}',
 #        'PASSWORD': '',
 #    }
-#}
+# }
 
 # Use template caching to speed up wagtail admin and front-end.
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
-            'loaders': [
-                ('django.template.loaders.cached.Loader', [
-                    'django.template.loaders.filesystem.Loader',
-                    'django.template.loaders.app_directories.Loader',
-                ]),
+            "loaders": [
+                (
+                    "django.template.loaders.cached.Loader",
+                    [
+                        "django.template.loaders.filesystem.Loader",
+                        "django.template.loaders.app_directories.Loader",
+                    ],
+                ),
             ],
         },
     },
 ]
 
 CACHES = {
-    'default': {
-        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
-        'LOCATION': os.path.join(BASE_DIR, 'cache'),
-        'KEY_PREFIX': 'coderedcms',
-        'TIMEOUT': 14400, # in seconds
+    "default": {
+        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
+        "LOCATION": os.path.join(BASE_DIR, "cache"),  # noqa
+        "KEY_PREFIX": "coderedcms",
+        "TIMEOUT": 14400,  # in seconds
     }
 }
-
-try:
-    from .local_settings import *
-except ImportError:
-    pass

+ 5 - 9
coderedcms/project_template/sass/project_name/urls.py

@@ -8,20 +8,16 @@ from coderedcms import urls as codered_urls
 
 urlpatterns = [
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # the page serving mechanism. This should be the last pattern in
     # the list:
-    path('', include(codered_urls)),
-
+    path("", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     #    path("pages/", include(codered_urls)),

+ 3 - 1
coderedcms/project_template/sass/project_name/wsgi.py

@@ -11,6 +11,8 @@ import os
 
 from django.core.wsgi import get_wsgi_application
 
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev")
+os.environ.setdefault(
+    "DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings.dev"
+)
 
 application = get_wsgi_application()

+ 1 - 1
coderedcms/project_template/sass/website/apps.py

@@ -2,4 +2,4 @@ from django.apps import AppConfig
 
 
 class WebsiteConfig(AppConfig):
-    name = 'website'
+    name = "website"

+ 23 - 17
coderedcms/project_template/sass/website/models.py

@@ -8,7 +8,7 @@ from coderedcms.models import (
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 
 
@@ -16,65 +16,71 @@ class ArticlePage(CoderedArticlePage):
     """
     Article, suitable for news or blog content.
     """
+
     class Meta:
-        verbose_name = 'Article'
-        ordering = ['-first_published_at']
+        verbose_name = "Article"
+        ordering = ["-first_published_at"]
 
     # Only allow this page to be created beneath an ArticleIndexPage.
-    parent_page_types = ['website.ArticleIndexPage']
+    parent_page_types = ["website.ArticleIndexPage"]
 
-    template = 'coderedcms/pages/article_page.html'
-    search_template = 'coderedcms/pages/article_page.search.html'
+    template = "coderedcms/pages/article_page.html"
+    search_template = "coderedcms/pages/article_page.search.html"
 
 
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     Shows a list of article sub-pages.
     """
+
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
     # Only allow ArticlePages beneath this page.
-    subpage_types = ['website.ArticlePage']
+    subpage_types = ["website.ArticlePage"]
 
-    template = 'coderedcms/pages/article_index_page.html'
+    template = "coderedcms/pages/article_index_page.html"
 
 
 class FormPage(CoderedFormPage):
     """
     A page with an html <form>.
     """
+
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 class FormPageField(CoderedFormField):
     """
     A field that links to a FormPage.
     """
+
     class Meta:
-        ordering = ['sort_order']
+        ordering = ["sort_order"]
 
-    page = ParentalKey('FormPage', related_name='form_fields')
+    page = ParentalKey("FormPage", related_name="form_fields")
 
 
 class FormConfirmEmail(CoderedEmail):
     """
     Sends a confirmation email after submitting a FormPage.
     """
-    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+    page = ParentalKey("FormPage", related_name="confirmation_emails")
 
 
 class WebPage(CoderedWebPage):
     """
     General use page with featureful streamfield and SEO attributes.
     """
+
     class Meta:
-        verbose_name = 'Web Page'
+        verbose_name = "Web Page"
 
-    template = 'coderedcms/pages/web_page.html'
+    template = "coderedcms/pages/web_page.html"

+ 1 - 1
coderedcms/search_urls.py

@@ -2,5 +2,5 @@ from django.urls import path
 from coderedcms.views import search
 
 urlpatterns = [
-    path('', search, name='codered_search'),
+    path("", search, name="codered_search"),
 ]

+ 208 - 131
coderedcms/settings.py

@@ -5,178 +5,256 @@ from django.conf import settings
 
 class _DefaultSettings:
 
-    CRX_PROTECTED_MEDIA_URL = '/protected/'
-    CRX_PROTECTED_MEDIA_ROOT = os.path.join(settings.BASE_DIR, 'protected')
+    CRX_PROTECTED_MEDIA_URL = "/protected/"
+    CRX_PROTECTED_MEDIA_ROOT = os.path.join(settings.BASE_DIR, "protected")
     CRX_PROTECTED_MEDIA_UPLOAD_WHITELIST = []
     CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST = [
-        '.app',
-        '.bat',
-        '.exe',
-        '.jar',
-        '.php',
-        '.pl',
-        '.ps1',
-        '.py',
-        '.rb',
-        '.sh',
+        ".app",
+        ".bat",
+        ".exe",
+        ".jar",
+        ".php",
+        ".pl",
+        ".ps1",
+        ".py",
+        ".rb",
+        ".sh",
     ]
 
-    CRX_FRONTEND_BTN_SIZE_DEFAULT = ''
+    CRX_FRONTEND_BTN_SIZE_DEFAULT = ""
     CRX_FRONTEND_BTN_SIZE_CHOICES = [
-        ('btn-sm', 'Small'),
-        ('', 'Default'),
-        ('btn-lg', 'Large'),
+        ("btn-sm", "Small"),
+        ("", "Default"),
+        ("btn-lg", "Large"),
     ]
 
-    CRX_FRONTEND_BTN_STYLE_DEFAULT = 'btn-primary'
+    CRX_FRONTEND_BTN_STYLE_DEFAULT = "btn-primary"
     CRX_FRONTEND_BTN_STYLE_CHOICES = [
-        ('btn-primary', 'Primary'),
-        ('btn-secondary', 'Secondary'),
-        ('btn-success', 'Success'),
-        ('btn-danger', 'Danger'),
-        ('btn-warning', 'Warning'),
-        ('btn-info', 'Info'),
-        ('btn-link', 'Link'),
-        ('btn-light', 'Light'),
-        ('btn-dark', 'Dark'),
-        ('btn-outline-primary', 'Outline Primary'),
-        ('btn-outline-secondary', 'Outline Secondary'),
-        ('btn-outline-success', 'Outline Success'),
-        ('btn-outline-danger', 'Outline Danger'),
-        ('btn-outline-warning', 'Outline Warning'),
-        ('btn-outline-info', 'Outline Info'),
-        ('btn-outline-light', 'Outline Light'),
-        ('btn-outline-dark', 'Outline Dark'),
+        ("btn-primary", "Primary"),
+        ("btn-secondary", "Secondary"),
+        ("btn-success", "Success"),
+        ("btn-danger", "Danger"),
+        ("btn-warning", "Warning"),
+        ("btn-info", "Info"),
+        ("btn-link", "Link"),
+        ("btn-light", "Light"),
+        ("btn-dark", "Dark"),
+        ("btn-outline-primary", "Outline Primary"),
+        ("btn-outline-secondary", "Outline Secondary"),
+        ("btn-outline-success", "Outline Success"),
+        ("btn-outline-danger", "Outline Danger"),
+        ("btn-outline-warning", "Outline Warning"),
+        ("btn-outline-info", "Outline Info"),
+        ("btn-outline-light", "Outline Light"),
+        ("btn-outline-dark", "Outline Dark"),
     ]
 
-    CRX_FRONTEND_COL_SIZE_DEFAULT = ''
+    CRX_FRONTEND_COL_SIZE_DEFAULT = ""
     CRX_FRONTEND_COL_SIZE_CHOICES = [
-        ('', 'Automatically size'),
-        ('12', 'Full row'),
-        ('6', 'Half - 1/2 column'),
-        ('4', 'Thirds - 1/3 column'),
-        ('8', 'Thirds - 2/3 column'),
-        ('3', 'Quarters - 1/4 column'),
-        ('9', 'Quarters - 3/4 column'),
-        ('2', 'Sixths - 1/6 column'),
-        ('10', 'Sixths - 5/6 column'),
-        ('1', 'Twelfths - 1/12 column'),
-        ('5', 'Twelfths - 5/12 column'),
-        ('7', 'Twelfths - 7/12 column'),
-        ('11', 'Twelfths - 11/12 column'),
+        ("", "Automatically size"),
+        ("12", "Full row"),
+        ("6", "Half - 1/2 column"),
+        ("4", "Thirds - 1/3 column"),
+        ("8", "Thirds - 2/3 column"),
+        ("3", "Quarters - 1/4 column"),
+        ("9", "Quarters - 3/4 column"),
+        ("2", "Sixths - 1/6 column"),
+        ("10", "Sixths - 5/6 column"),
+        ("1", "Twelfths - 1/12 column"),
+        ("5", "Twelfths - 5/12 column"),
+        ("7", "Twelfths - 7/12 column"),
+        ("11", "Twelfths - 11/12 column"),
     ]
 
-    CRX_FRONTEND_COL_BREAK_DEFAULT = 'md'
+    CRX_FRONTEND_COL_BREAK_DEFAULT = "md"
     CRX_FRONTEND_COL_BREAK_CHOICES = [
-        ('', 'Always expanded'),
-        ('sm', 'sm - Expand on small screens (phone, 576px) and larger'),
-        ('md', 'md - Expand on medium screens (tablet, 768px) and larger'),
-        ('lg', 'lg - Expand on large screens (laptop, 992px) and larger'),
-        ('xl', 'xl - Expand on extra large screens (wide monitor, 1200px)'),
+        ("", "Always expanded"),
+        ("sm", "sm - Expand on small screens (phone, 576px) and larger"),
+        ("md", "md - Expand on medium screens (tablet, 768px) and larger"),
+        ("lg", "lg - Expand on large screens (laptop, 992px) and larger"),
+        ("xl", "xl - Expand on extra large screens (wide monitor, 1200px)"),
     ]
 
-    CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT = ''
+    CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT = ""
     CRX_FRONTEND_NAVBAR_FORMAT_CHOICES = [
-        ('', 'Default Bootstrap Navbar'),
-        ('crx-navbar-center', 'Centered logo at top'),
+        ("", "Default Bootstrap Navbar"),
+        ("crx-navbar-center", "Centered logo at top"),
     ]
 
-    CRX_FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT = 'navbar-light'
+    CRX_FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT = "navbar-light"
     CRX_FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES = [
-        ('navbar-light', 'Light - for use with a light-colored navbar'),
-        ('navbar-dark', 'Dark - for use with a dark-colored navbar'),
+        ("navbar-light", "Light - for use with a light-colored navbar"),
+        ("navbar-dark", "Dark - for use with a dark-colored navbar"),
     ]
 
-    CRX_FRONTEND_NAVBAR_CLASS_DEFAULT = 'bg-light'
+    CRX_FRONTEND_NAVBAR_CLASS_DEFAULT = "bg-light"
 
-    CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT = 'navbar-expand-lg'
+    CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_DEFAULT = "navbar-expand-lg"
     CRX_FRONTEND_NAVBAR_COLLAPSE_MODE_CHOICES = [
-        ('', 'Never show menu - Always collapse menu behind a button'),
-        ('navbar-expand-sm', 'sm - Show on small screens (phone size) and larger'),
-        ('navbar-expand-md', 'md - Show on medium screens (tablet size) and larger'),
-        ('navbar-expand-lg', 'lg - Show on large screens (laptop size) and larger'),
-        ('navbar-expand-xl', 'xl - Show on extra large screens (desktop, wide monitor)'),
+        (
+            "",
+            "Never show menu - Always collapse menu behind a button",
+        ),
+        (
+            "navbar-expand-sm",
+            "sm - Show on small screens (phone size) and larger",
+        ),
+        (
+            "navbar-expand-md",
+            "md - Show on medium screens (tablet size) and larger",
+        ),
+        (
+            "navbar-expand-lg",
+            "lg - Show on large screens (laptop size) and larger",
+        ),
+        (
+            "navbar-expand-xl",
+            "xl - Show on extra large screens (desktop, wide monitor)",
+        ),
     ]
 
-    CRX_FRONTEND_THEME_DEFAULT = ''
+    CRX_FRONTEND_THEME_DEFAULT = ""
     CRX_FRONTEND_THEME_CHOICES = [
-        ('', 'Default - Classic Bootstrap'),
-        ('cerulean', 'Cerulean - A calm blue sky'),
-        ('cosmo', 'Cosmo - An ode to Metro'),
-        ('cyborg', 'Cyborg - Jet black and electric blue'),
-        ('darkly', 'Darkly - Flatly in night mode'),
-        ('flatly', 'Flatly - Flat and modern'),
-        ('journal', 'Journal - Crisp like a new sheet of paper'),
-        ('litera', 'Litera - The medium is the message'),
-        ('lumen', 'Lumen - Light and shadow'),
-        ('lux', 'Lux - A touch of class'),
-        ('materia', 'Materia - Material is the metaphor'),
-        ('minty', 'Minty - A fresh feel'),
-        ('morph', 'Morph - A neumorphic layer'),
-        ('pulse', 'Pulse - A trace of purple'),
-        ('quartz', 'A glassmorphic layer'),
-        ('sandstone', 'Sandstone - A touch of warmth'),
-        ('simplex', 'Simplex - Mini and minimalist'),
-        ('sketchy', 'Sketchy - A hand-drawn look for mockups and mirth'),
-        ('slate', 'Slate - Shades of gunmetal gray'),
-        ('solar', 'Solar - A dark spin on Solarized'),
-        ('spacelab', 'Spacelab - Silvery and sleek'),
-        ('superhero', 'Superhero - The brave and the blue'),
-        ('united', 'United - Ubuntu orange and unique font'),
-        ('vapor', 'A cyberpunk aesthetic'),
-        ('yeti', 'Yeti - A friendly foundation'),
-        ('zephyr', 'Breezy and beautiful'),
+        ("", "Default - Classic Bootstrap"),
+        ("cerulean", "Cerulean - A calm blue sky"),
+        ("cosmo", "Cosmo - An ode to Metro"),
+        ("cyborg", "Cyborg - Jet black and electric blue"),
+        ("darkly", "Darkly - Flatly in night mode"),
+        ("flatly", "Flatly - Flat and modern"),
+        ("journal", "Journal - Crisp like a new sheet of paper"),
+        ("litera", "Litera - The medium is the message"),
+        ("lumen", "Lumen - Light and shadow"),
+        ("lux", "Lux - A touch of class"),
+        ("materia", "Materia - Material is the metaphor"),
+        ("minty", "Minty - A fresh feel"),
+        ("morph", "Morph - A neumorphic layer"),
+        ("pulse", "Pulse - A trace of purple"),
+        ("quartz", "A glassmorphic layer"),
+        ("sandstone", "Sandstone - A touch of warmth"),
+        ("simplex", "Simplex - Mini and minimalist"),
+        ("sketchy", "Sketchy - A hand-drawn look for mockups and mirth"),
+        ("slate", "Slate - Shades of gunmetal gray"),
+        ("solar", "Solar - A dark spin on Solarized"),
+        ("spacelab", "Spacelab - Silvery and sleek"),
+        ("superhero", "Superhero - The brave and the blue"),
+        ("united", "United - Ubuntu orange and unique font"),
+        ("vapor", "A cyberpunk aesthetic"),
+        ("yeti", "Yeti - A friendly foundation"),
+        ("zephyr", "Breezy and beautiful"),
     ]
 
     CRX_FRONTEND_TEMPLATES_BLOCKS = {
-        'cardblock': [
-            ('coderedcms/blocks/card_block.html', 'Card'),
-            ('coderedcms/blocks/card_head.html', 'Card with header'),
-            ('coderedcms/blocks/card_foot.html', 'Card with footer'),
-            ('coderedcms/blocks/card_head_foot.html', 'Card with header and footer'),
-            ('coderedcms/blocks/card_blurb.html', 'Blurb - rounded image and no border'),
-            ('coderedcms/blocks/card_img.html', 'Cover image - use image as background'),
+        "cardblock": [
+            (
+                "coderedcms/blocks/card_block.html",
+                "Card",
+            ),
+            (
+                "coderedcms/blocks/card_head.html",
+                "Card with header",
+            ),
+            (
+                "coderedcms/blocks/card_foot.html",
+                "Card with footer",
+            ),
+            (
+                "coderedcms/blocks/card_head_foot.html",
+                "Card with header and footer",
+            ),
+            (
+                "coderedcms/blocks/card_blurb.html",
+                "Blurb - rounded image and no border",
+            ),
+            (
+                "coderedcms/blocks/card_img.html",
+                "Cover image - use image as background",
+            ),
         ],
-        'cardgridblock': [
-            ('coderedcms/blocks/cardgrid_group.html', 'Card group - attached cards of equal size'),
-            ('coderedcms/blocks/cardgrid_deck.html', 'Card deck - separate cards of equal size'),
-            ('coderedcms/blocks/cardgrid_columns.html', 'Card masonry - fluid brick pattern'),
+        "cardgridblock": [
+            (
+                "coderedcms/blocks/cardgrid_group.html",
+                "Card group - attached cards of equal size",
+            ),
+            (
+                "coderedcms/blocks/cardgrid_deck.html",
+                "Card deck - separate cards of equal size",
+            ),
+            (
+                "coderedcms/blocks/cardgrid_columns.html",
+                "Card masonry - fluid brick pattern",
+            ),
         ],
-        'pagelistblock': [
-            ('coderedcms/blocks/pagelist_block.html', 'General, simple list'),
-            ('coderedcms/blocks/pagelist_list_group.html', 'General, list group navigation panel'),
-            ('coderedcms/blocks/pagelist_article_media.html', 'Article, media format'),
-            ('coderedcms/blocks/pagelist_article_card_group.html',
-                'Article, card group - attached cards of equal size'),
-            ('coderedcms/blocks/pagelist_article_card_deck.html',
-             'Article, card deck - separate cards of equal size'),
-            ('coderedcms/blocks/pagelist_article_card_columns.html',
-             'Article, card masonry - fluid brick pattern'),
+        "pagelistblock": [
+            (
+                "coderedcms/blocks/pagelist_block.html",
+                "General, simple list",
+            ),
+            (
+                "coderedcms/blocks/pagelist_list_group.html",
+                "General, list group navigation panel",
+            ),
+            (
+                "coderedcms/blocks/pagelist_article_media.html",
+                "Article, media format",
+            ),
+            (
+                "coderedcms/blocks/pagelist_article_card_group.html",
+                "Article, card group - attached cards of equal size",
+            ),
+            (
+                "coderedcms/blocks/pagelist_article_card_deck.html",
+                "Article, card deck - separate cards of equal size",
+            ),
+            (
+                "coderedcms/blocks/pagelist_article_card_columns.html",
+                "Article, card masonry - fluid brick pattern",
+            ),
         ],
-        'pagepreviewblock': [
-            ('coderedcms/blocks/pagepreview_card.html', 'Card'),
-            ('coderedcms/blocks/pagepreview_form.html', 'Form inputs'),
+        "pagepreviewblock": [
+            (
+                "coderedcms/blocks/pagepreview_card.html",
+                "Card",
+            ),
+            (
+                "coderedcms/blocks/pagepreview_form.html",
+                "Form inputs",
+            ),
         ],
         # templates that are available for all block types
-        '*': [
-            ('', 'Default'),
+        "*": [
+            ("", "Default"),
         ],
     }
 
     CRX_FRONTEND_TEMPLATES_PAGES = {
         # templates that are available for all page types
-        '*': [
-            ('', 'Default'),
-            ('coderedcms/pages/web_page.html', 'Web page showing title and cover image'),
-            ('coderedcms/pages/web_page_notitle.html', 'Web page without title and cover image'),
-            ('coderedcms/pages/home_page.html', 'Home page without title and cover image'),
-            ('coderedcms/pages/base.html', 'Blank page - no navbar or footer'),
+        "*": [
+            (
+                "",
+                "Default",
+            ),
+            (
+                "coderedcms/pages/web_page.html",
+                "Web page showing title and cover image",
+            ),
+            (
+                "coderedcms/pages/web_page_notitle.html",
+                "Web page without title and cover image",
+            ),
+            (
+                "coderedcms/pages/home_page.html",
+                "Home page without title and cover image",
+            ),
+            (
+                "coderedcms/pages/base.html",
+                "Blank page - no navbar or footer",
+            ),
         ],
     }
 
     CRX_BANNER = None
-    CRX_BANNER_BACKGROUND = '#f00'
-    CRX_BANNER_TEXT_COLOR = '#fff'
+    CRX_BANNER_BACKGROUND = "#f00"
+    CRX_BANNER_TEXT_COLOR = "#fff"
 
     def __getattribute__(self, attr: str):
         # First load from Django settings.
@@ -193,9 +271,8 @@ crx_settings = _DefaultSettings()
 # If the older django-bootstrap4 is the only version listed in INSTALLED_APPS,
 # use it for compatibility. Otherwise use django-bootstrap5 which is a
 # dependency of coderedcms.
-if (
-    apps.is_installed("bootstrap4")
-    and not apps.is_installed("django_bootstrap5")
+if apps.is_installed("bootstrap4") and not apps.is_installed(
+    "django_bootstrap5"
 ):
     import bootstrap4.bootstrap as bootstrap
 else:

+ 45 - 41
coderedcms/templatetags/coderedcms_tags.py

@@ -27,7 +27,9 @@ def is_advanced_setting(obj):
 
 @register.filter
 def is_file_form(form):
-    return any([isinstance(field.field.widget, ClearableFileInput) for field in form])
+    return any(
+        [isinstance(field.field.widget, ClearableFileInput) for field in form]
+    )
 
 
 @register.simple_tag
@@ -37,25 +39,25 @@ def coderedcms_version():
 
 @register.simple_tag
 def generate_random_id():
-    value = ''.join(random.choice(string.ascii_letters + string.digits) for n in range(20))
+    value = "".join(
+        random.choice(string.ascii_letters + string.digits) for n in range(20)
+    )
     return "cr-{}".format(value)
 
 
 @register.simple_tag
 def is_menu_item_dropdown(value):
-    return \
-        len(value.get('sub_links', [])) > 0 or \
-        (
-            value.get('show_child_links', False) and
-            len(value.get('page', []).get_children().live()) > 0
-        )
+    return len(value.get("sub_links", [])) > 0 or (
+        value.get("show_child_links", False)
+        and len(value.get("page", []).get_children().live()) > 0
+    )
 
 
 @register.simple_tag(takes_context=True)
 def is_active_page(context, curr_page, other_page):
-    if hasattr(curr_page, 'get_url') and hasattr(other_page, 'get_url'):
-        curr_url = curr_page.get_url(context['request'])
-        other_url = other_page.get_url(context['request'])
+    if hasattr(curr_page, "get_url") and hasattr(other_page, "get_url"):
+        curr_url = curr_page.get_url(context["request"])
+        other_url = other_page.get_url(context["request"])
         return curr_url == other_url
     return False
 
@@ -68,34 +70,36 @@ def get_pictures(collection_id):
 
 @register.simple_tag(takes_context=True)
 def get_navbar_css(context):
-    layout = LayoutSettings.for_request(context['request'])
+    layout = LayoutSettings.for_request(context["request"])
     fixed = "fixed-top" if layout.navbar_fixed else ""
-    return " ".join([
-        fixed,
-        layout.navbar_collapse_mode,
-        layout.navbar_color_scheme,
-        layout.navbar_format,
-        layout.navbar_class
-    ])
+    return " ".join(
+        [
+            fixed,
+            layout.navbar_collapse_mode,
+            layout.navbar_color_scheme,
+            layout.navbar_format,
+            layout.navbar_class,
+        ]
+    )
 
 
 @register.simple_tag(takes_context=True)
-def get_navbars(context) -> 'QuerySet[Navbar]':
-    layout = LayoutSettings.for_request(context['request'])
+def get_navbars(context) -> "QuerySet[Navbar]":
+    layout = LayoutSettings.for_request(context["request"])
     navbarorderables = layout.site_navbar.all()
     navbars = Navbar.objects.filter(
         navbarorderable__in=navbarorderables
-        ).order_by('navbarorderable__sort_order')
+    ).order_by("navbarorderable__sort_order")
     return navbars
 
 
 @register.simple_tag(takes_context=True)
-def get_footers(context) -> 'QuerySet[Footer]':
-    layout = LayoutSettings.for_request(context['request'])
+def get_footers(context) -> "QuerySet[Footer]":
+    layout = LayoutSettings.for_request(context["request"])
     footerorderables = layout.site_footer.all()
     footers = Footer.objects.filter(
         footerorderable__in=footerorderables
-        ).order_by('footerorderable__sort_order')
+    ).order_by("footerorderable__sort_order")
     return footers
 
 
@@ -113,7 +117,9 @@ def get_pageform(page, request):
 
 @register.simple_tag
 def process_form_cell(request, cell):
-    if isinstance(cell, str) and cell.startswith(crx_settings_obj.CRX_PROTECTED_MEDIA_URL):
+    if isinstance(cell, str) and cell.startswith(
+        crx_settings_obj.CRX_PROTECTED_MEDIA_URL
+    ):
         return utils.get_protected_media_link(request, cell, render_link=True)
     if utils.uri_validator(str(cell)):
         return mark_safe("<a href='{0}'>{1}</a>".format(cell, cell))
@@ -146,7 +152,7 @@ def query_update(querydict, key=None, value=None):
             get[key] = value
         else:
             try:
-                del(get[key])
+                del get[key]
             except KeyError:
                 pass
     return get
@@ -156,8 +162,8 @@ def query_update(querydict, key=None, value=None):
 def render_iframe_from_embed(embed):
     soup = BeautifulSoup(embed.html, "html.parser")
     try:
-        iframe_tags = soup.find('iframe')
-        iframe_tags['title'] = embed.title
+        iframe_tags = soup.find("iframe")
+        iframe_tags["title"] = embed.title
         return mark_safe(soup.prettify())
     except AttributeError:
         pass
@@ -173,26 +179,25 @@ def map_to_bootstrap_alert(message_tag):
     Converts a message level to a bootstrap 4 alert class
     """
     message_to_alert_dict = {
-        'debug': 'primary',
-        'info': 'info',
-        'success': 'success',
-        'warning': 'warning',
-        'error': 'danger'
+        "debug": "primary",
+        "info": "info",
+        "success": "success",
+        "warning": "warning",
+        "error": "danger",
     }
 
     try:
         return message_to_alert_dict[message_tag]
     except KeyError:
-        return ''
+        return ""
 
 
 @register.filter
 def get_name_of_class(class_type):
     if hasattr(class_type.__class__, "search_name"):
         return class_type.__class__.search_name
-    elif (
-            hasattr(class_type.__class__, "_meta") and
-            hasattr(class_type.__class__._meta, "verbose_name")
+    elif hasattr(class_type.__class__, "_meta") and hasattr(
+        class_type.__class__._meta, "verbose_name"
     ):
         return class_type.__class__._meta.verbose_name
     else:
@@ -203,9 +208,8 @@ def get_name_of_class(class_type):
 def get_plural_name_of_class(class_type):
     if hasattr(class_type.__class__, "search_name_plural"):
         return class_type.__class__.search_name_plural
-    elif (
-            hasattr(class_type.__class__, "_meta") and
-            hasattr(class_type.__class__._meta, "verbose_name_plural")
+    elif hasattr(class_type.__class__, "_meta") and hasattr(
+        class_type.__class__._meta, "verbose_name_plural"
     ):
         return class_type.__class__._meta.verbose_name_plural
     else:

+ 77 - 84
coderedcms/tests/settings.py

@@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 import os
-from django.utils.translation import gettext_lazy as _  # noqa
 
 
 PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -27,80 +26,72 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 
 INSTALLED_APPS = [
     # Test
-    'coderedcms.tests.testapp',
-
+    "coderedcms.tests.testapp",
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
-    'wagtail.contrib.forms',
-    'wagtail.contrib.redirects',
-    'wagtail.embeds',
-    'wagtail.sites',
-    'wagtail.users',
-    'wagtail.snippets',
-    'wagtail.documents',
-    'wagtail.images',
-    'wagtail.search',
-    'wagtail',
-    'wagtail.contrib.settings',
-    'wagtail.contrib.modeladmin',
-    'wagtail.contrib.table_block',
-    'wagtail.admin',
-
+    "wagtail.contrib.forms",
+    "wagtail.contrib.redirects",
+    "wagtail.embeds",
+    "wagtail.sites",
+    "wagtail.users",
+    "wagtail.snippets",
+    "wagtail.documents",
+    "wagtail.images",
+    "wagtail.search",
+    "wagtail",
+    "wagtail.contrib.settings",
+    "wagtail.contrib.modeladmin",
+    "wagtail.contrib.table_block",
+    "wagtail.admin",
     # Django
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
     "django.contrib.sitemaps",
 ]
 
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.common.CommonMiddleware',
-
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.common.CommonMiddleware",
     # Security
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "django.middleware.security.SecurityMiddleware",
     # Error reporting. Uncomment this to receive emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 
-ROOT_URLCONF = 'coderedcms.tests.urls'
+ROOT_URLCONF = "coderedcms.tests.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
         },
     },
@@ -111,12 +102,12 @@ TEMPLATES = [
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
-        'TEST': {
-            'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
-        }
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+        "TEST": {
+            "NAME": os.path.join(BASE_DIR, "test_db.sqlite3"),
+        },
     }
 }
 
@@ -126,16 +117,16 @@ DATABASES = {
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -143,9 +134,9 @@ AUTH_PASSWORD_VALIDATORS = [
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-TIME_ZONE = 'America/New_York'
+TIME_ZONE = "America/New_York"
 
 USE_I18N = True
 
@@ -156,21 +147,21 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 STATICFILES_FINDERS = [
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
-STATIC_URL = '/teststatic/'
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_URL = "/teststatic/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-MEDIA_URL = '/testmedia/'
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_URL = "/testmedia/"
 
 
 # Login
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 # Wagtail settings
@@ -180,27 +171,29 @@ WAGTAIL_SITE_NAME = ""
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 WAGTAILSEARCH_BACKENDS = {
-    'default': {
-        'BACKEND': 'wagtail.search.backends.database',
+    "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
-WAGTAILADMIN_BASE_URL = ''
+WAGTAILADMIN_BASE_URL = ""
 
 
 # Tags
 
 TAGGIT_CASE_INSENSITIVE = True
 
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 
 WAGTAIL_CACHE = False
 
-SECRET_KEY = 'not needed'
+SECRET_KEY = "not needed"
 
-DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
 
-NOSE_ARGS = ['--nocapture',
-             '--nologcapture', ]
+NOSE_ARGS = [
+    "--nocapture",
+    "--nologcapture",
+]

+ 38 - 19
coderedcms/tests/test_bin.py

@@ -7,7 +7,7 @@ from coderedcms.bin.coderedcms import main as coderedcms_main
 
 class TestCoderedcmsStart(unittest.TestCase):
     CURR_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-    TEST_DIR = os.path.join(CURR_DIR, 'testproject-unittest')
+    TEST_DIR = os.path.join(CURR_DIR, "testproject-unittest")
 
     def setup(self):
         # Clean/create directory to start into
@@ -22,14 +22,14 @@ class TestCoderedcmsStart(unittest.TestCase):
 
     def test_help(self):
         # Set args
-        sys.argv = ['coderedcms', 'help']
+        sys.argv = ["coderedcms", "help"]
         # Run
         coderedcms_main()
         # Nothing to assert here... just make sure it doesn't error out.
 
     def test_help_start(self):
         # Set args
-        sys.argv = ['coderedcms', 'help', 'start']
+        sys.argv = ["coderedcms", "help", "start"]
         # Run
         coderedcms_main()
         # Nothing to assert here... just make sure it doesn't error out.
@@ -37,53 +37,72 @@ class TestCoderedcmsStart(unittest.TestCase):
     def test_default(self):
         self.setup()
         # Set args
-        sys.argv = ['coderedcms', 'start', 'myproject', self.TEST_DIR]
+        sys.argv = ["coderedcms", "start", "myproject", self.TEST_DIR]
         # Run
         coderedcms_main()
         # Assert files exist
-        self.assertTrue(os.path.exists(os.path.join(self.TEST_DIR, 'README.md')))
+        self.assertTrue(
+            os.path.exists(os.path.join(self.TEST_DIR, "README.md"))
+        )
         self.cleanup()
 
     def test_allopts(self):
         self.setup()
         # Set args
         sys.argv = [
-            'coderedcms',
-            'start',
-            'myproject',
+            "coderedcms",
+            "start",
+            "myproject",
             self.TEST_DIR,
-            '--template', 'basic',
-            '--sitename', 'MegaCorp, Inc.',
-            '--domain', 'example.com'
+            "--template",
+            "basic",
+            "--sitename",
+            "MegaCorp, Inc.",
+            "--domain",
+            "example.com",
         ]
         # Run
         coderedcms_main()
         # Assert files exist
-        self.assertTrue(os.path.exists(os.path.join(self.TEST_DIR, 'README.md')))
+        self.assertTrue(
+            os.path.exists(os.path.join(self.TEST_DIR, "README.md"))
+        )
         self.cleanup()
 
     def test_domain_www(self):
         self.setup()
         # Set args
         sys.argv = [
-            'coderedcms',
-            'start',
-            'myproject',
+            "coderedcms",
+            "start",
+            "myproject",
             self.TEST_DIR,
-            '--domain', 'www.example.com'
+            "--domain",
+            "www.example.com",
         ]
         # Run
         coderedcms_main()
         # Assert files exist
-        self.assertTrue(os.path.exists(os.path.join(self.TEST_DIR, 'README.md')))
+        self.assertTrue(
+            os.path.exists(os.path.join(self.TEST_DIR, "README.md"))
+        )
         self.cleanup()
 
     def test_template_sass(self):
         self.setup()
         # Set args
-        sys.argv = ['coderedcms', 'start', 'myproject', self.TEST_DIR, '--template', 'sass']
+        sys.argv = [
+            "coderedcms",
+            "start",
+            "myproject",
+            self.TEST_DIR,
+            "--template",
+            "sass",
+        ]
         # Run
         coderedcms_main()
         # Assert files exist
-        self.assertTrue(os.path.exists(os.path.join(self.TEST_DIR, 'README.md')))
+        self.assertTrue(
+            os.path.exists(os.path.join(self.TEST_DIR, "README.md"))
+        )
         self.cleanup()

+ 7 - 6
coderedcms/tests/test_templates.py

@@ -12,12 +12,13 @@ EXPECTED_BANNER_HTML = """
 
 @pytest.mark.django_db
 class TestSiteBanner(TestCase):
-
     @override_settings(CRX_BANNER="Test")
     def test_with_banner(self):
         response = self.client.get("/")
         self.assertEqual(response.status_code, 200)
-        self.assertInHTML(EXPECTED_BANNER_HTML, response.content.decode("utf-8"))
+        self.assertInHTML(
+            EXPECTED_BANNER_HTML, response.content.decode("utf-8")
+        )
 
     def test_without_banner(self):
         response = self.client.get("/")
@@ -29,9 +30,7 @@ class TestSiteBanner(TestCase):
 class TestWagtailAdminBanner(TestCase):
     def setUp(self):
         admin = get_user_model().objects.create_superuser(
-            "admin",
-            email="admin@example.com",
-            password="admin"
+            "admin", email="admin@example.com", password="admin"
         )
         self.client.force_login(admin)
 
@@ -42,7 +41,9 @@ class TestWagtailAdminBanner(TestCase):
     def test_with_banner(self):
         response = self.client.get("/admin/")
         self.assertEqual(response.status_code, 200)
-        self.assertInHTML(EXPECTED_BANNER_HTML, response.content.decode("utf-8"))
+        self.assertInHTML(
+            EXPECTED_BANNER_HTML, response.content.decode("utf-8")
+        )
 
     def test_without_banner(self):
         response = self.client.get("/admin/")

+ 1 - 1
coderedcms/tests/test_templatetags.py

@@ -4,7 +4,7 @@ from django.template import engines
 from django.test import TestCase
 
 
-django_engine = engines['django']
+django_engine = engines["django"]
 html_id_re = re.compile(r"^[A-Za-z][A-Za-z0-9_:\.-]*$")
 
 

+ 99 - 85
coderedcms/tests/test_urls.py

@@ -13,7 +13,11 @@ from wagtail.models import Site, Page
 from wagtail.images.tests.utils import Image, get_test_image_file
 
 from coderedcms.models import LayoutSettings
-from coderedcms.tests.testapp.models import EventPage, EventIndexPage, EventOccurrence
+from coderedcms.tests.testapp.models import (
+    EventPage,
+    EventIndexPage,
+    EventOccurrence,
+)
 
 
 @pytest.mark.django_db
@@ -31,34 +35,32 @@ class TestSiteURLs(unittest.TestCase):
         response = self.client.get("/sitemap.xml")
 
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-type'], 'application/xml')
+        self.assertEqual(response["content-type"], "application/xml")
 
     def test_robots(self):
         response = self.client.get("/robots.txt")
 
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-type'], 'text/plain')
+        self.assertEqual(response["content-type"], "text/plain")
 
     def test_search(self):
-        response = self.client.get(reverse(
-            'codered_search'),
-            {'s': 'Test Search Query'},
-            follow=True
+        response = self.client.get(
+            reverse("codered_search"), {"s": "Test Search Query"}, follow=True
         )
 
         self.assertEqual(response.status_code, 200)
-        self.assertNotEqual(response.context['results'], None)
+        self.assertNotEqual(response.context["results"], None)
 
-        response = self.client.get(reverse(
-            'codered_search'),
+        response = self.client.get(
+            reverse("codered_search"),
             {
-                's': 'keyword',
-                't': 't',
+                "s": "keyword",
+                "t": "t",
             },
-            follow=False
+            follow=False,
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response.context['results'], None)
+        self.assertEqual(response.context["results"], None)
 
 
 @pytest.mark.django_db
@@ -69,10 +71,10 @@ class TestEventURLs(unittest.TestCase):
 
     def test_generate_single_event(self):
         event_page = EventPage(
-            path='/single-event/',
+            path="/single-event/",
             depth=1,
-            title='Single Event',
-            slug='single-event'
+            title="Single Event",
+            slug="single-event",
         )
         self.root_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
@@ -87,34 +89,42 @@ class TestEventURLs(unittest.TestCase):
         response = self.client.post(
             ajax_url,
             {
-                'event_pk': event_page.pk,
-                'datetime_start': occurrence.start.strftime("%Y-%m-%dT%H:%M:%S%z"),
-                'datetime_end': occurrence.end.strftime("%Y-%m-%dT%H:%M:%S%z"),
+                "event_pk": event_page.pk,
+                "datetime_start": occurrence.start.strftime(
+                    "%Y-%m-%dT%H:%M:%S%z"
+                ),
+                "datetime_end": occurrence.end.strftime("%Y-%m-%dT%H:%M:%S%z"),
             },
-            follow=True
+            follow=True,
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['Filename'], "{0}.ics".format(event_page.slug))
         self.assertEqual(
-            response['Content-Disposition'],
-            'attachment; filename={0}.ics'.format(event_page.slug)
+            response["Filename"], "{0}.ics".format(event_page.slug)
+        )
+        self.assertEqual(
+            response["Content-Disposition"],
+            "attachment; filename={0}.ics".format(event_page.slug),
         )
-        self.assertEqual(response['content-type'], 'text/calendar')
+        self.assertEqual(response["content-type"], "text/calendar")
 
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
-        split_content = str(response._container[0]).split('VALUE=DATE-TIME:')
-        start = split_content[1].split('\\')[0]
-        end = split_content[2].split('\\')[0]
+        split_content = str(response._container[0]).split("VALUE=DATE-TIME:")
+        start = split_content[1].split("\\")[0]
+        end = split_content[2].split("\\")[0]
         self.assertTrue(
             start.startswith(
-                EventOccurrence.objects.get(event=event_page).start.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).start.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
         self.assertTrue(
             end.startswith(
-                EventOccurrence.objects.get(event=event_page).end.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).end.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
 
@@ -129,7 +139,7 @@ class TestEventURLs(unittest.TestCase):
                 "event_pk": "junk",
                 "datetime_start": "junk",
                 "datetime_end": "junk",
-            }
+            },
         )
         self.assertEqual(response.status_code, 400)
         response = self.client.post(
@@ -138,54 +148,58 @@ class TestEventURLs(unittest.TestCase):
                 "event_pk": "junk",
                 "datetime_start": "2022-07-14T10:00:00+0000",
                 "datetime_end": "2022-07-14T10:00:00+0000",
-            }
+            },
         )
         self.assertEqual(response.status_code, 404)
 
     def test_generate_recurring_event(self):
         event_page = EventPage(
-            path='/recurring-event/',
+            path="/recurring-event/",
             depth=1,
-            title='Recurring Event',
-            slug='recurring-event'
+            title="Recurring Event",
+            slug="recurring-event",
         )
         self.root_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
             event=event_page,
-            start='2019-01-01T10:00:00+0000',
-            end='2019-01-01T11:00:00+0000'
+            start="2019-01-01T10:00:00+0000",
+            end="2019-01-01T11:00:00+0000",
         )
         occurrence.save()
 
         ajax_url = reverse("event_generate_recurring_ical")
 
         response = self.client.post(
-            ajax_url,
-            {'event_pk': event_page.pk},
-            follow=True
+            ajax_url, {"event_pk": event_page.pk}, follow=True
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['Filename'], "{0}.ics".format(event_page.slug))
         self.assertEqual(
-            response['Content-Disposition'],
-            'attachment; filename={0}.ics'.format(event_page.slug)
+            response["Filename"], "{0}.ics".format(event_page.slug)
         )
-        self.assertEqual(response['content-type'], 'text/calendar')
+        self.assertEqual(
+            response["Content-Disposition"],
+            "attachment; filename={0}.ics".format(event_page.slug),
+        )
+        self.assertEqual(response["content-type"], "text/calendar")
 
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
-        split_content = str(response._container[0]).split('VALUE=DATE-TIME:')
-        start = split_content[1].split('\\')[0]
-        end = split_content[2].split('\\')[0]
+        split_content = str(response._container[0]).split("VALUE=DATE-TIME:")
+        start = split_content[1].split("\\")[0]
+        end = split_content[2].split("\\")[0]
         self.assertTrue(
             start.startswith(
-                EventOccurrence.objects.get(event=event_page).start.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).start.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
         self.assertTrue(
             end.startswith(
-                EventOccurrence.objects.get(event=event_page).end.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).end.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
 
@@ -197,53 +211,57 @@ class TestEventURLs(unittest.TestCase):
 
     def test_generate_calendar(self):
         calendar_page = EventIndexPage(
-            path='/event-index-page/',
+            path="/event-index-page/",
             depth=1,
-            title='Event Index Page',
-            slug='event-index-page'
+            title="Event Index Page",
+            slug="event-index-page",
         )
         self.root_page.add_child(instance=calendar_page)
 
         event_page = EventPage(
-            path='/eventpage/1/',
+            path="/eventpage/1/",
             depth=2,
-            title='Event Page 1',
-            slug='eventpage1'
+            title="Event Page 1",
+            slug="eventpage1",
         )
         calendar_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
             event=event_page,
-            start='2019-01-01T10:00:00+0000',
-            end='2019-01-01T11:00:00+0000'
+            start="2019-01-01T10:00:00+0000",
+            end="2019-01-01T11:00:00+0000",
         )
         occurrence.save()
 
         ajax_url = reverse("event_generate_ical_for_calendar")
 
         response = self.client.post(
-            ajax_url,
-            {'page_id': calendar_page.pk},
-            follow=True
+            ajax_url, {"page_id": calendar_page.pk}, follow=True
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['Filename'], 'calendar.ics')
-        self.assertEqual(response['Content-Disposition'], 'attachment; filename=calendar.ics')
-        self.assertEqual(response['content-type'], 'text/calendar')
+        self.assertEqual(response["Filename"], "calendar.ics")
+        self.assertEqual(
+            response["Content-Disposition"], "attachment; filename=calendar.ics"
+        )
+        self.assertEqual(response["content-type"], "text/calendar")
 
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
-        split_content = str(response._container[0]).split('VALUE=DATE-TIME:')
-        start = split_content[1].split('\\')[0]
-        end = split_content[2].split('\\')[0]
+        split_content = str(response._container[0]).split("VALUE=DATE-TIME:")
+        start = split_content[1].split("\\")[0]
+        end = split_content[2].split("\\")[0]
         self.assertTrue(
             start.startswith(
-                EventOccurrence.objects.get(event=event_page).start.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).start.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
         self.assertTrue(
             end.startswith(
-                EventOccurrence.objects.get(event=event_page).end.strftime("%Y%m%dT%H%M%S")
+                EventOccurrence.objects.get(event=event_page).end.strftime(
+                    "%Y%m%dT%H%M%S"
+                )
             )
         )
 
@@ -255,24 +273,24 @@ class TestEventURLs(unittest.TestCase):
 
     def test_ajax_calendar(self):
         calendar_page = EventIndexPage(
-            path='/event-index-page/',
+            path="/event-index-page/",
             depth=1,
-            title='Event Index Page',
-            slug='event-index-page'
+            title="Event Index Page",
+            slug="event-index-page",
         )
         self.root_page.add_child(instance=calendar_page)
 
         event_page = EventPage(
-            path='/eventpage/1/',
+            path="/eventpage/1/",
             depth=2,
-            title='Event Page 1',
-            slug='eventpage1'
+            title="Event Page 1",
+            slug="eventpage1",
         )
         calendar_page.add_child(instance=event_page)
         occurrence_one = EventOccurrence(
             event=event_page,
-            start='2019-01-01T10:00:00+0000',
-            end='2019-01-01T11:00:00+0000'
+            start="2019-01-01T10:00:00+0000",
+            end="2019-01-01T11:00:00+0000",
         )
         occurrence_one.save()
 
@@ -281,13 +299,13 @@ class TestEventURLs(unittest.TestCase):
         response = self.client.post(
             f"{ajax_url}?pid={calendar_page.pk}",
             follow=True,
-            **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+            **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"},
         )
         self.assertEqual(response.status_code, 200)
 
         # Get datetimes from response and compare them to datetimes on page
-        start = literal_eval(response._container[0].decode()[1:-1])['start']
-        end = literal_eval(response._container[0].decode()[1:-1])['end']
+        start = literal_eval(response._container[0].decode()[1:-1])["start"]
+        end = literal_eval(response._container[0].decode()[1:-1])["end"]
         event_local_start = timezone.localtime(
             EventOccurrence.objects.get(event=event_page).start
         )
@@ -295,13 +313,9 @@ class TestEventURLs(unittest.TestCase):
             EventOccurrence.objects.get(event=event_page).end
         )
         self.assertEqual(
-            start,
-            event_local_start.strftime("%Y-%m-%dT%H:%M:%S%z")
-        )
-        self.assertEqual(
-            end,
-            event_local_end.strftime("%Y-%m-%dT%H:%M:%S%z")
+            start, event_local_start.strftime("%Y-%m-%dT%H:%M:%S%z")
         )
+        self.assertEqual(end, event_local_end.strftime("%Y-%m-%dT%H:%M:%S%z"))
 
         # Test that garbage requests are handled appropriately.
         response = self.client.post(ajax_url)

+ 51 - 38
coderedcms/tests/testapp/models.py

@@ -12,7 +12,7 @@ from coderedcms.models import (
     CoderedLocationIndexPage,
     CoderedLocationPage,
     CoderedStreamFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 
 
@@ -20,111 +20,122 @@ class ArticlePage(CoderedArticlePage):
     """
     Article, suitable for news or blog content.
     """
+
     class Meta:
-        verbose_name = 'Article'
-        ordering = ['-first_published_at', ]
+        verbose_name = "Article"
+        ordering = [
+            "-first_published_at",
+        ]
 
     # Only allow this page to be created beneath an ArticleIndexPage.
-    parent_page_types = ['testapp.ArticleIndexPage']
+    parent_page_types = ["testapp.ArticleIndexPage"]
 
-    template = 'coderedcms/pages/article_page.html'
-    search_template = 'coderedcms/pages/article_page.search.html'
+    template = "coderedcms/pages/article_page.html"
+    search_template = "coderedcms/pages/article_page.search.html"
 
 
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     Shows a list of article sub-pages.
     """
+
     class Meta:
-        verbose_name = 'Article Landing Page'
-    index_order_by_default = ''
+        verbose_name = "Article Landing Page"
+
+    index_order_by_default = ""
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'testapp.ArticlePage'
+    index_query_pagemodel = "testapp.ArticlePage"
 
     # Only allow ArticlePages beneath this page.
-    subpage_types = ['testapp.ArticlePage']
+    subpage_types = ["testapp.ArticlePage"]
 
-    template = 'coderedcms/pages/article_index_page.html'
+    template = "coderedcms/pages/article_index_page.html"
 
 
 class FormPage(CoderedFormPage):
     """
     A page with an html <form>.
     """
+
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 class FormPageField(CoderedFormField):
     """
     A field that links to a FormPage.
     """
+
     class Meta:
-        ordering = ['sort_order']
+        ordering = ["sort_order"]
 
-    page = ParentalKey('FormPage', related_name='form_fields')
+    page = ParentalKey("FormPage", related_name="form_fields")
 
 
 class FormConfirmEmail(CoderedEmail):
     """
     Sends a confirmation email after submitting a FormPage.
     """
-    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+    page = ParentalKey("FormPage", related_name="confirmation_emails")
 
 
 class WebPage(CoderedWebPage):
     """
     General use page with featureful streamfield and SEO attributes.
     """
+
     class Meta:
-        verbose_name = 'Web Page'
+        verbose_name = "Web Page"
 
-    template = 'coderedcms/pages/web_page.html'
+    template = "coderedcms/pages/web_page.html"
 
 
 class EventPage(CoderedEventPage):
     class Meta:
-        verbose_name = 'Event Page'
+        verbose_name = "Event Page"
 
-    parent_page_types = ['testapp.EventIndexPage']
+    parent_page_types = ["testapp.EventIndexPage"]
     subpage_types = []
-    template = 'coderedcms/pages/event_page.html'
+    template = "coderedcms/pages/event_page.html"
 
 
 class EventIndexPage(CoderedEventIndexPage):
     """
     Shows a list of event sub-pages.
     """
+
     class Meta:
-        verbose_name = 'Events Landing Page'
+        verbose_name = "Events Landing Page"
 
-    index_query_pagemodel = 'testapp.EventPage'
-    index_order_by_default = ''
+    index_query_pagemodel = "testapp.EventPage"
+    index_order_by_default = ""
 
     # Only allow EventPages beneath this page.
-    subpage_types = ['testapp.EventPage']
+    subpage_types = ["testapp.EventPage"]
 
-    template = 'coderedcms/pages/event_index_page.html'
+    template = "coderedcms/pages/event_index_page.html"
 
 
 class EventOccurrence(CoderedEventOccurrence):
-    event = ParentalKey(EventPage, related_name='occurrences')
+    event = ParentalKey(EventPage, related_name="occurrences")
 
 
 class LocationPage(CoderedLocationPage):
     """
     A page that holds a location.  This could be a store, a restaurant, etc.
     """
+
     class Meta:
-        verbose_name = 'Location Page'
+        verbose_name = "Location Page"
 
-    template = 'coderedcms/pages/location_page.html'
+    template = "coderedcms/pages/location_page.html"
 
     # Only allow LocationIndexPages above this page.
-    parent_page_types = ['testapp.LocationIndexPage']
+    parent_page_types = ["testapp.LocationIndexPage"]
 
 
 class LocationIndexPage(CoderedLocationIndexPage):
@@ -133,27 +144,28 @@ class LocationIndexPage(CoderedLocationIndexPage):
     This does require a Google Maps API Key that can be defined in Settings >
     Google API Settings
     """
+
     class Meta:
-        verbose_name = 'Location Landing Page'
+        verbose_name = "Location Landing Page"
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'testapp.LocationPage'
+    index_query_pagemodel = "testapp.LocationPage"
 
     # Only allow LocationPages beneath this page.
-    subpage_types = ['testapp.LocationPage']
+    subpage_types = ["testapp.LocationPage"]
 
-    template = 'coderedcms/pages/location_index_page.html'
+    template = "coderedcms/pages/location_index_page.html"
 
 
 class StreamFormPage(CoderedStreamFormPage):
     class Meta:
-        verbose_name = 'Stream Form'
+        verbose_name = "Stream Form"
 
-    template = 'coderedcms/pages/stream_form_page.html'
+    template = "coderedcms/pages/stream_form_page.html"
 
 
 class StreamFormConfirmEmail(CoderedEmail):
-    page = ParentalKey('StreamFormPage', related_name='confirmation_emails')
+    page = ParentalKey("StreamFormPage", related_name="confirmation_emails")
 
 
 """
@@ -169,9 +181,10 @@ class IndexTestPage(CoderedPage):
     """
     Tests indexing features (show/sort/filter child pages).
     """
+
     class Meta:
         verbose_name = "Index Test Page"
 
     index_query_pagemodel = "testapp.WebPage"
 
-    template = 'coderedcms/pages/base.html'
+    template = "coderedcms/pages/base.html"

+ 5 - 9
coderedcms/tests/urls.py

@@ -7,20 +7,16 @@ from coderedcms import urls as codered_urls
 
 urlpatterns = [
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # the page serving mechanism. This should be the last pattern in
     # the list:
-    re_path(r'', include(codered_urls)),
-
+    re_path(r"", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     #    re_path(r'^pages/', include(codered_urls)),

+ 30 - 18
coderedcms/urls.py

@@ -9,31 +9,43 @@ from coderedcms.views import (
     event_get_calendar_events,
     favicon,
     robots,
-    serve_protected_file
+    serve_protected_file,
 )
 
 
 urlpatterns = [
     # CodeRed custom URLs
-    path(r'favicon.ico', favicon, name='codered_favicon'),
-    path(r'robots.txt', robots, name='codered_robots'),
-    path(r'sitemap.xml', sitemap, name='codered_sitemap'),
-    re_path(r'^{0}(?P<path>.*)$'.format(
-        crx_settings.CRX_PROTECTED_MEDIA_URL.lstrip('/')),
+    path(r"favicon.ico", favicon, name="codered_favicon"),
+    path(r"robots.txt", robots, name="codered_robots"),
+    path(r"sitemap.xml", sitemap, name="codered_sitemap"),
+    re_path(
+        r"^{0}(?P<path>.*)$".format(
+            crx_settings.CRX_PROTECTED_MEDIA_URL.lstrip("/")
+        ),
         serve_protected_file,
-        name="serve_protected_file"
+        name="serve_protected_file",
     ),
-
     # Event/Calendar URLs
-    path('ical/generate/single/', event_generate_single_ical_for_event,
-         name='event_generate_single_ical'),
-    path('ical/generate/recurring/', event_generate_recurring_ical_for_event,
-         name='event_generate_recurring_ical'),
-    path('ical/generate/calendar/', event_generate_ical_for_calendar,
-         name='event_generate_ical_for_calendar'),
-    path('ajax/calendar/events/', event_get_calendar_events,
-         name='event_get_calendar_events'),
-
+    path(
+        "ical/generate/single/",
+        event_generate_single_ical_for_event,
+        name="event_generate_single_ical",
+    ),
+    path(
+        "ical/generate/recurring/",
+        event_generate_recurring_ical_for_event,
+        name="event_generate_recurring_ical",
+    ),
+    path(
+        "ical/generate/calendar/",
+        event_generate_ical_for_calendar,
+        name="event_generate_ical_for_calendar",
+    ),
+    path(
+        "ajax/calendar/events/",
+        event_get_calendar_events,
+        name="event_get_calendar_events",
+    ),
     # Wagtail
-    path('', include(wagtailcore_urls)),
+    path("", include(wagtailcore_urls)),
 ]

+ 2 - 3
coderedcms/utils.py

@@ -9,11 +9,10 @@ def get_protected_media_link(request, path, render_link=False):
     if render_link:
         return mark_safe(
             "<a href='{0}{1}'>{0}{1}</a>".format(
-                request.build_absolute_uri('/')[:-1],
-                path
+                request.build_absolute_uri("/")[:-1], path
             )
         )
-    return "{0}{1}".format(request.build_absolute_uri('/')[:-1], path)
+    return "{0}{1}".format(request.build_absolute_uri("/")[:-1], path)
 
 
 def uri_validator(possible_uri):

+ 94 - 64
coderedcms/views.py

@@ -1,10 +1,20 @@
 import mimetypes
 import os
 from datetime import datetime
-from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect, JsonResponse
+from django.http import (
+    Http404,
+    HttpResponse,
+    HttpResponsePermanentRedirect,
+    JsonResponse,
+)
 from django.contrib.auth.decorators import login_required
 from django.contrib.contenttypes.models import ContentType
-from django.core.paginator import Paginator, InvalidPage, EmptyPage, PageNotAnInteger
+from django.core.paginator import (
+    Paginator,
+    InvalidPage,
+    EmptyPage,
+    PageNotAnInteger,
+)
 from django.shortcuts import redirect, render
 from django.utils import timezone
 from django.utils.translation import ngettext, gettext_lazy as _
@@ -14,12 +24,12 @@ from wagtail.admin import messages
 from wagtail.models import Page, get_page_models
 from coderedcms import utils
 from coderedcms.forms import SearchForm
-from coderedcms.models import (
-    CoderedPage,
-    GeneralSettings,
-    LayoutSettings
+from coderedcms.models import CoderedPage, GeneralSettings, LayoutSettings
+from coderedcms.importexport import (
+    convert_csv_to_json,
+    import_pages,
+    ImportPagesFromCSVFileForm,
 )
-from coderedcms.importexport import convert_csv_to_json, import_pages, ImportPagesFromCSVFileForm
 from coderedcms.settings import crx_settings
 from coderedcms.templatetags.coderedcms_tags import get_name_of_class
 
@@ -34,8 +44,8 @@ def search(request):
     results_paginated = None
 
     if search_form.is_valid():
-        search_query = search_form.cleaned_data['s']
-        search_model = search_form.cleaned_data['t']
+        search_query = search_form.cleaned_data["s"]
+        search_model = search_form.cleaned_data["t"]
 
         # get all page models
         pagemodels = sorted(get_page_models(), key=get_name_of_class)
@@ -48,7 +58,9 @@ def search(request):
         if search_model:
             try:
                 # If provided a model name, try to get it
-                model = ContentType.objects.get(model=search_model).model_class()
+                model = ContentType.objects.get(
+                    model=search_model
+                ).model_class()
                 results = results.type(model)
             except ContentType.DoesNotExist:
                 # Maintain existing behavior of only returning objects if the page type is real
@@ -57,8 +69,10 @@ def search(request):
         # get and paginate results
         if results:
             results = results.search(search_query)
-            paginator = Paginator(results, GeneralSettings.for_request(request).search_num_results)
-            page = request.GET.get('p', 1)
+            paginator = Paginator(
+                results, GeneralSettings.for_request(request).search_num_results
+            )
+            page = request.GET.get("p", 1)
             try:
                 results_paginated = paginator.page(page)
             except PageNotAnInteger:
@@ -69,13 +83,17 @@ def search(request):
                 results_paginated = paginator.page(1)
 
     # Render template
-    return render(request, 'coderedcms/pages/search.html', {
-        'request': request,
-        'pagetypes': pagetypes,
-        'form': search_form,
-        'results': results,
-        'results_paginated': results_paginated
-    })
+    return render(
+        request,
+        "coderedcms/pages/search.html",
+        {
+            "request": request,
+            "pagetypes": pagetypes,
+            "form": search_form,
+            "results": results,
+            "results_paginated": results_paginated,
+        },
+    )
 
 
 @login_required
@@ -90,7 +108,7 @@ def serve_protected_file(request, path):
     # Path must be a sub-path of the PROTECTED_MEDIA_ROOT, and exist.
     if fullpath.startswith(mediapath) and os.path.isfile(fullpath):
         mimetype, encoding = mimetypes.guess_type(fullpath)
-        with open(fullpath, 'rb') as f:
+        with open(fullpath, "rb") as f:
             response = HttpResponse(f.read(), content_type=mimetype)
         if encoding:
             response["Content-Encoding"] = encoding
@@ -102,29 +120,29 @@ def serve_protected_file(request, path):
 def favicon(request):
     icon = LayoutSettings.for_request(request).favicon
     if icon:
-        return HttpResponsePermanentRedirect(icon.get_rendition('original').url)
+        return HttpResponsePermanentRedirect(icon.get_rendition("original").url)
     raise Http404()
 
 
 def robots(request):
-    return render(
-        request,
-        'robots.txt',
-        content_type='text/plain'
-    )
+    return render(request, "robots.txt", content_type="text/plain")
 
 
 @require_POST
 def event_generate_single_ical_for_event(request):
     # Parse input.
     try:
-        event_pk = request.POST['event_pk']
+        event_pk = request.POST["event_pk"]
     except KeyError:
         return HttpResponse("event_pk required", status=400)
 
     try:
-        dt_start_str = utils.fix_ical_datetime_format(request.POST['datetime_start'])
-        dt_end_str = utils.fix_ical_datetime_format(request.POST['datetime_end'])
+        dt_start_str = utils.fix_ical_datetime_format(
+            request.POST["datetime_start"]
+        )
+        dt_end_str = utils.fix_ical_datetime_format(
+            request.POST["datetime_end"]
+        )
         dt_start = None
         dt_end = None
         if dt_start_str:
@@ -150,12 +168,16 @@ def event_generate_single_ical_for_event(request):
 
     # Generate the ical file.
     ical = Calendar()
-    ical.add('prodid', '-//Wagtail CRX//')
-    ical.add('version', '2.0')
-    ical.add_component(event.create_single_ical(dt_start=dt_start, dt_end=dt_end))
+    ical.add("prodid", "-//Wagtail CRX//")
+    ical.add("version", "2.0")
+    ical.add_component(
+        event.create_single_ical(dt_start=dt_start, dt_end=dt_end)
+    )
     response = HttpResponse(ical.to_ical(), content_type="text/calendar")
-    response['Filename'] = "{0}.ics".format(event.slug)
-    response['Content-Disposition'] = 'attachment; filename={0}.ics'.format(event.slug)
+    response["Filename"] = "{0}.ics".format(event.slug)
+    response["Content-Disposition"] = "attachment; filename={0}.ics".format(
+        event.slug
+    )
     return response
 
 
@@ -163,7 +185,7 @@ def event_generate_single_ical_for_event(request):
 def event_generate_recurring_ical_for_event(request):
     # Parse input.
     try:
-        event_pk = request.POST['event_pk']
+        event_pk = request.POST["event_pk"]
     except KeyError:
         return HttpResponse("event_pk required", status=400)
 
@@ -175,13 +197,15 @@ def event_generate_recurring_ical_for_event(request):
 
     # Generate the ical file.
     ical = Calendar()
-    ical.add('prodid', '-//Wagtail CRX//')
-    ical.add('version', '2.0')
+    ical.add("prodid", "-//Wagtail CRX//")
+    ical.add("version", "2.0")
     for e in event.create_recurring_ical():
         ical.add_component(e)
     response = HttpResponse(ical.to_ical(), content_type="text/calendar")
-    response['Filename'] = "{0}.ics".format(event.slug)
-    response['Content-Disposition'] = 'attachment; filename={0}.ics'.format(event.slug)
+    response["Filename"] = "{0}.ics".format(event.slug)
+    response["Content-Disposition"] = "attachment; filename={0}.ics".format(
+        event.slug
+    )
     return response
 
 
@@ -201,14 +225,14 @@ def event_generate_ical_for_calendar(request):
 
     # Generate the ical file.
     ical = Calendar()
-    ical.add('prodid', '-//Wagtail CRX//')
-    ical.add('version', '2.0')
+    ical.add("prodid", "-//Wagtail CRX//")
+    ical.add("version", "2.0")
     for event_page in page.get_index_children():
         for e in event_page.specific.create_recurring_ical():
             ical.add_component(e)
     response = HttpResponse(ical.to_ical(), content_type="text/calendar")
-    response['Filename'] = "calendar.ics"
-    response['Content-Disposition'] = 'attachment; filename=calendar.ics'
+    response["Filename"] = "calendar.ics"
+    response["Content-Disposition"] = "attachment; filename=calendar.ics"
     return response
 
 
@@ -224,8 +248,8 @@ def event_get_calendar_events(request):
 
     start = None
     end = None
-    start_str = request.GET.get('start', None)
-    end_str = request.GET.get('end', None)
+    start_str = request.GET.get("start", None)
+    end_str = request.GET.get("end", None)
     try:
         if start_str:
             start = timezone.make_aware(
@@ -237,8 +261,7 @@ def event_get_calendar_events(request):
             )
     except ValueError:
         return HttpResponse(
-            "start and end must be valid datetimes.",
-            status=400
+            "start and end must be valid datetimes.", status=400
         )
 
     # Get the page.
@@ -248,8 +271,7 @@ def event_get_calendar_events(request):
         raise Http404("Page does not exist")
 
     return JsonResponse(
-        page.get_calendar_events(start=start, end=end),
-        safe=False
+        page.get_calendar_events(start=start, end=end), safe=False
     )
 
 
@@ -258,7 +280,7 @@ def import_index(request):
     """
     Landing page to replace wagtailimportexport.
     """
-    return render(request, 'wagtailimportexport/index.html')
+    return render(request, "wagtailimportexport/index.html")
 
 
 @login_required
@@ -269,30 +291,38 @@ def import_pages_from_csv_file(request):
     format that the importer expects.
     """
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = ImportPagesFromCSVFileForm(request.POST, request.FILES)
         if form.is_valid():
             import_data = convert_csv_to_json(
-                form.cleaned_data['file'].read().decode('utf-8').splitlines(),
-                form.cleaned_data['page_type']
+                form.cleaned_data["file"].read().decode("utf-8").splitlines(),
+                form.cleaned_data["page_type"],
             )
-            parent_page = form.cleaned_data['parent_page']
+            parent_page = form.cleaned_data["parent_page"]
             try:
                 page_count = import_pages(import_data, parent_page)
             except LookupError as e:
-                messages.error(request, _(
-                    "Import failed: %(reason)s") % {'reason': e}
+                messages.error(
+                    request, _("Import failed: %(reason)s") % {"reason": e}
                 )
             else:
-                messages.success(request, ngettext(
-                    "%(count)s page imported.",
-                    "%(count)s pages imported.",
-                    page_count) % {'count': page_count}
+                messages.success(
+                    request,
+                    ngettext(
+                        "%(count)s page imported.",
+                        "%(count)s pages imported.",
+                        page_count,
+                    )
+                    % {"count": page_count},
                 )
-            return redirect('wagtailadmin_explore', parent_page.pk)
+            return redirect("wagtailadmin_explore", parent_page.pk)
     else:
         form = ImportPagesFromCSVFileForm()
 
-    return render(request, 'wagtailimportexport/import_from_csv.html', {
-        'form': form,
-    })
+    return render(
+        request,
+        "wagtailimportexport/import_from_csv.html",
+        {
+            "form": form,
+        },
+    )

+ 102 - 83
coderedcms/wagtail_flexible_forms/blocks.py

@@ -5,20 +5,29 @@ from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 from anyascii import anyascii
 from wagtail.blocks import (
-    StructBlock, TextBlock, CharBlock, BooleanBlock, ListBlock, StreamBlock,
-    DateBlock, TimeBlock, DateTimeBlock, ChoiceBlock, RichTextBlock,
+    StructBlock,
+    TextBlock,
+    CharBlock,
+    BooleanBlock,
+    ListBlock,
+    StreamBlock,
+    DateBlock,
+    TimeBlock,
+    DateTimeBlock,
+    ChoiceBlock,
+    RichTextBlock,
 )
 
 
 class FormFieldBlock(StructBlock):
-    field_label = CharBlock(label=_('Label'))
-    help_text = TextBlock(required=False, label=_('Help text'))
+    field_label = CharBlock(label=_("Label"))
+    help_text = TextBlock(required=False, label=_("Help text"))
 
     field_class = forms.CharField
     widget = None
 
     def get_slug(self, struct_value):
-        return slugify(anyascii(struct_value['field_label']))
+        return slugify(anyascii(struct_value["field_label"]))
 
     def get_field_class(self, struct_value):
         return self.field_class
@@ -27,71 +36,76 @@ class FormFieldBlock(StructBlock):
         return self.widget
 
     def get_field_kwargs(self, struct_value):
-        kwargs = {'label': struct_value['field_label'],
-                  'help_text': struct_value['help_text'],
-                  'required': struct_value.get('required', False)}
-        if 'default_value' in struct_value:
-            kwargs['initial'] = struct_value['default_value']
+        kwargs = {
+            "label": struct_value["field_label"],
+            "help_text": struct_value["help_text"],
+            "required": struct_value.get("required", False),
+        }
+        if "default_value" in struct_value:
+            kwargs["initial"] = struct_value["default_value"]
         form_widget = self.get_widget(struct_value)
         if form_widget is not None:
-            kwargs['widget'] = form_widget
+            kwargs["widget"] = form_widget
         return kwargs
 
     def get_field(self, struct_value):
         return self.get_field_class(struct_value)(
-            **self.get_field_kwargs(struct_value))
+            **self.get_field_kwargs(struct_value)
+        )
 
 
 class OptionalFormFieldBlock(FormFieldBlock):
-    required = BooleanBlock(label=_('Required'), required=False)
+    required = BooleanBlock(label=_("Required"), required=False)
 
 
 CHARFIELD_FORMATS = [
-    ('email', _('Email')),
-    ('url', _('URL')),
+    ("email", _("Email")),
+    ("url", _("URL")),
 ]
 try:
     from phonenumber_field.formfields import PhoneNumberField
 except ImportError:
     pass
 else:
-    CHARFIELD_FORMATS.append(('phone', _('Phone')))
+    CHARFIELD_FORMATS.append(("phone", _("Phone")))
 
 
 class CharFieldBlock(OptionalFormFieldBlock):
-    format = ChoiceBlock(choices=CHARFIELD_FORMATS, required=False, label=_('Format'))
-    default_value = CharBlock(required=False, label=_('Default value'))
+    format = ChoiceBlock(
+        choices=CHARFIELD_FORMATS, required=False, label=_("Format")
+    )
+    default_value = CharBlock(required=False, label=_("Default value"))
 
     class Meta:
-        label = _('Text field (single line)')
+        label = _("Text field (single line)")
 
     def get_field_class(self, struct_value):
-        text_format = struct_value['format']
-        if text_format == 'url':
+        text_format = struct_value["format"]
+        if text_format == "url":
             return forms.URLField
-        if text_format == 'email':
+        if text_format == "email":
             return forms.EmailField
-        if text_format == 'phone':
+        if text_format == "phone":
             return PhoneNumberField
         return super().get_field_class(struct_value)
 
 
 class TextFieldBlock(OptionalFormFieldBlock):
-    default_value = TextBlock(required=False, label=_('Default value'))
+    default_value = TextBlock(required=False, label=_("Default value"))
 
-    widget = forms.Textarea(attrs={'rows': 5})
+    widget = forms.Textarea(attrs={"rows": 5})
 
     class Meta:
-        label = _('Text field (multi line)')
+        label = _("Text field (multi line)")
 
 
 class NumberFieldBlock(OptionalFormFieldBlock):
-    default_value = CharBlock(required=False, label=_('Default value'))
+    default_value = CharBlock(required=False, label=_("Default value"))
 
     widget = forms.NumberInput
 
     class Meta:
-        label = _('Number field')
+        label = _("Number field")
 
 
 class CheckboxFieldBlock(FormFieldBlock):
@@ -100,24 +114,25 @@ class CheckboxFieldBlock(FormFieldBlock):
     field_class = forms.BooleanField
 
     class Meta:
-        label = _('Checkbox field')
-        icon = 'tick-inverse'
+        label = _("Checkbox field")
+        icon = "tick-inverse"
 
 
 class RadioButtonsFieldBlock(OptionalFormFieldBlock):
-    choices = ListBlock(CharBlock(label=_('Choice')))
+    choices = ListBlock(CharBlock(label=_("Choice")))
 
     field_class = forms.ChoiceField
     widget = forms.RadioSelect
 
     class Meta:
-        label = _('Radio buttons')
-        icon = 'radio-empty'
+        label = _("Radio buttons")
+        icon = "radio-empty"
 
     def get_field_kwargs(self, struct_value):
         kwargs = super().get_field_kwargs(struct_value)
-        kwargs['choices'] = [(choice, choice)
-                             for choice in struct_value['choices']]
+        kwargs["choices"] = [
+            (choice, choice) for choice in struct_value["choices"]
+        ]
         return kwargs
 
 
@@ -125,44 +140,47 @@ class DropdownFieldBlock(RadioButtonsFieldBlock):
     widget = forms.Select
 
     class Meta:
-        label = _('Dropdown field')
-        icon = 'arrow-down-big'
+        label = _("Dropdown field")
+        icon = "arrow-down-big"
 
     def get_field_kwargs(self, struct_value):
-        kwargs = super(DropdownFieldBlock,
-                       self).get_field_kwargs(struct_value)
-        kwargs['choices'].insert(0, BLANK_CHOICE_DASH[0])
+        kwargs = super(DropdownFieldBlock, self).get_field_kwargs(struct_value)
+        kwargs["choices"].insert(0, BLANK_CHOICE_DASH[0])
         return kwargs
 
 
 class CheckboxesFieldBlock(OptionalFormFieldBlock):
-    checkboxes = ListBlock(CharBlock(label=_('Checkbox')))
+    checkboxes = ListBlock(CharBlock(label=_("Checkbox")))
 
     field_class = forms.MultipleChoiceField
     widget = forms.CheckboxSelectMultiple
 
     class Meta:
-        label = _('Multiple checkboxes field')
-        icon = 'list-ul'
+        label = _("Multiple checkboxes field")
+        icon = "list-ul"
 
     def get_field_kwargs(self, struct_value):
-        kwargs = super(CheckboxesFieldBlock,
-                       self).get_field_kwargs(struct_value)
-        kwargs['choices'] = [(choice, choice)
-                             for choice in struct_value['checkboxes']]
+        kwargs = super(CheckboxesFieldBlock, self).get_field_kwargs(
+            struct_value
+        )
+        kwargs["choices"] = [
+            (choice, choice) for choice in struct_value["checkboxes"]
+        ]
         return kwargs
 
 
 class DatePickerInput(forms.DateInput):
     def __init__(self, *args, **kwargs):
-        attrs = kwargs.get('attrs')
+        attrs = kwargs.get("attrs")
         if attrs is None:
             attrs = {}
-        attrs.update({
-            'data-provide': 'datepicker',
-            'data-date-format': 'yyyy-mm-dd',
-        })
-        kwargs['attrs'] = attrs
+        attrs.update(
+            {
+                "data-provide": "datepicker",
+                "data-date-format": "yyyy-mm-dd",
+            }
+        )
+        kwargs["attrs"] = attrs
         super().__init__(*args, **kwargs)
 
 
@@ -173,12 +191,12 @@ class DateFieldBlock(OptionalFormFieldBlock):
     widget = DatePickerInput
 
     class Meta:
-        label = _('Date field')
-        icon = 'date'
+        label = _("Date field")
+        icon = "date"
 
 
 class HTML5TimeInput(forms.TimeInput):
-    input_type = 'time'
+    input_type = "time"
 
 
 class TimeFieldBlock(OptionalFormFieldBlock):
@@ -188,14 +206,15 @@ class TimeFieldBlock(OptionalFormFieldBlock):
     widget = HTML5TimeInput
 
     class Meta:
-        label = _('Time field')
-        icon = 'time'
+        label = _("Time field")
+        icon = "time"
 
 
 class DateTimePickerInput(forms.SplitDateTimeWidget):
     def __init__(self, attrs=None, date_format=None, time_format=None):
-        super().__init__(attrs=attrs,
-                         date_format=date_format, time_format=time_format)
+        super().__init__(
+            attrs=attrs, date_format=date_format, time_format=time_format
+        )
         self.widgets = (
             DatePickerInput(attrs=attrs, format=date_format),
             HTML5TimeInput(attrs=attrs, format=time_format),
@@ -214,55 +233,55 @@ class DateTimeFieldBlock(OptionalFormFieldBlock):
     widget = DateTimePickerInput
 
     class Meta:
-        label = _('Date+time field')
-        icon = 'date'
+        label = _("Date+time field")
+        icon = "date"
 
 
 class ImageFieldBlock(OptionalFormFieldBlock):
     field_class = forms.ImageField
 
     class Meta:
-        label = _('Image field')
-        icon = 'image'
+        label = _("Image field")
+        icon = "image"
 
 
 class FileFieldBlock(OptionalFormFieldBlock):
     field_class = forms.FileField
 
     class Meta:
-        label = _('File field')
-        icon = 'download'
+        label = _("File field")
+        icon = "download"
 
 
 class FormFieldsBlock(StreamBlock):
-    char = CharFieldBlock(group=_('Fields'))
-    text = TextFieldBlock(group=_('Fields'))
-    number = NumberFieldBlock(group=_('Fields'))
-    checkbox = CheckboxFieldBlock(group=_('Fields'))
-    radios = RadioButtonsFieldBlock(group=_('Fields'))
-    dropdown = DropdownFieldBlock(group=_('Fields'))
-    checkboxes = CheckboxesFieldBlock(group=_('Fields'))
-    date = DateFieldBlock(group=_('Fields'))
-    time = TimeFieldBlock(group=_('Fields'))
-    datetime = DateTimeFieldBlock(group=_('Fields'))
-    image = ImageFieldBlock(group=_('Fields'))
-    file = FileFieldBlock(group=_('Fields'))
-    text_markup = RichTextBlock(group=_('Other'))
+    char = CharFieldBlock(group=_("Fields"))
+    text = TextFieldBlock(group=_("Fields"))
+    number = NumberFieldBlock(group=_("Fields"))
+    checkbox = CheckboxFieldBlock(group=_("Fields"))
+    radios = RadioButtonsFieldBlock(group=_("Fields"))
+    dropdown = DropdownFieldBlock(group=_("Fields"))
+    checkboxes = CheckboxesFieldBlock(group=_("Fields"))
+    date = DateFieldBlock(group=_("Fields"))
+    time = TimeFieldBlock(group=_("Fields"))
+    datetime = DateTimeFieldBlock(group=_("Fields"))
+    image = ImageFieldBlock(group=_("Fields"))
+    file = FileFieldBlock(group=_("Fields"))
+    text_markup = RichTextBlock(group=_("Other"))
 
     class Meta:
-        label = _('Form fields')
+        label = _("Form fields")
 
 
 class FormStepBlock(StructBlock):
-    name = CharBlock(label=_('Name'), required=False)
+    name = CharBlock(label=_("Name"), required=False)
     form_fields = FormFieldsBlock()
 
     class Meta:
-        label = _('Form step')
+        label = _("Form step")
 
 
 class FormStepsBlock(StreamBlock):
     step = FormStepBlock()
 
     class Meta:
-        label = _('Form steps')
+        label = _("Form steps")

+ 14 - 8
coderedcms/wagtail_flexible_forms/edit_handlers.py

@@ -11,7 +11,7 @@ class FormSubmissionsPanel(EditHandler):
     def bind_to(self, model=None, instance=None, request=None, form=None):
         new = super().bind_to(model=model)
         if self.heading is None:
-            new.heading = _('{} submissions').format(model.get_verbose_name())
+            new.heading = _("{} submissions").format(model.get_verbose_name())
         return new
 
     def render(self):
@@ -20,11 +20,17 @@ class FormSubmissionsPanel(EditHandler):
         submission_count = submissions.count()
 
         if not submission_count:
-            return ''
+            return ""
 
-        return mark_safe(render_to_string(self.template, {
-            'self': self,
-            'submission_count': submission_count,
-            'last_submit_time': (submissions.order_by('submit_time')
-                                 .last().submit_time),
-        }))
+        return mark_safe(
+            render_to_string(
+                self.template,
+                {
+                    "self": self,
+                    "submission_count": submission_count,
+                    "last_submit_time": (
+                        submissions.order_by("submit_time").last().submit_time
+                    ),
+                },
+            )
+        )

+ 255 - 169
coderedcms/wagtail_flexible_forms/models.py

@@ -15,7 +15,13 @@ from django.contrib.contenttypes.models import ContentType
 from django.core.files.storage import default_storage
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db.models import (
-    CharField, TextField, DateTimeField, Model, ForeignKey, PROTECT, CASCADE,
+    CharField,
+    TextField,
+    DateTimeField,
+    Model,
+    ForeignKey,
+    PROTECT,
+    CASCADE,
     QuerySet,
 )
 from django.db.models.fields.files import FieldFile
@@ -29,7 +35,10 @@ from django.utils.timezone import now
 from django.utils.translation import gettext_lazy as _
 from wagtail.models import Page
 from wagtail.contrib.forms.models import (
-    AbstractForm, AbstractEmailForm, AbstractFormSubmission)
+    AbstractForm,
+    AbstractEmailForm,
+    AbstractFormSubmission,
+)
 
 from .blocks import FormStepBlock, FormFieldBlock
 
@@ -38,14 +47,14 @@ class Step:
     def __init__(self, steps, index, struct_child):
         self.steps = steps
         self.index = index
-        block = getattr(struct_child, 'block', None)
+        block = getattr(struct_child, "block", None)
         if block is None:
             struct_child = []
         if isinstance(block, FormStepBlock):
-            self.name = struct_child.value['name']
-            self.form_fields = struct_child.value['form_fields']
+            self.name = struct_child.value["name"]
+            self.form_fields = struct_child.value["form_fields"]
         else:
-            self.name = ''
+            self.name = ""
             self.form_fields = struct_child
 
     @property
@@ -54,7 +63,7 @@ class Step:
 
     @property
     def url(self):
-        return '%s?step=%s' % (self.steps.page.url, self.index1)
+        return "%s?step=%s" % (self.steps.page.url, self.index1)
 
     def get_form_fields(self):
         form_fields = OrderedDict()
@@ -68,8 +77,11 @@ class Step:
         return form_fields
 
     def get_form_class(self):
-        return type('WagtailForm', self.steps.page.get_form_class_bases(),
-                    self.get_form_fields())
+        return type(
+            "WagtailForm",
+            self.steps.page.get_form_class_bases(),
+            self.get_form_fields(),
+        )
 
     def get_markups_and_bound_fields(self, form):
         for struct_child in self.form_fields:
@@ -77,22 +89,24 @@ class Step:
             if isinstance(block, FormFieldBlock):
                 struct_value = struct_child.value
                 field_name = block.get_slug(struct_value)
-                yield form[field_name], 'field'
+                yield form[field_name], "field"
             else:
-                yield mark_safe(struct_child), 'markup'
+                yield mark_safe(struct_child), "markup"
 
     def __str__(self):
         if self.name:
             return self.name
-        return _('Step %s') % self.index1
+        return _("Step %s") % self.index1
 
     @property
     def badge(self):
-        return (mark_safe('<span class="badge">%s/%s</span>')
-                % (self.index1, len(self.steps)))
+        return mark_safe('<span class="badge">%s/%s</span>') % (
+            self.index1,
+            len(self.steps),
+        )
 
     def __html__(self):
-        return '%s %s' % (self, self.badge)
+        return "%s %s" % (self, self.badge)
 
     @property
     def is_active(self):
@@ -113,23 +127,25 @@ class Step:
     @property
     def prev(self):
         if self.has_prev:
-            return self.steps[self.index-1]
+            return self.steps[self.index - 1]
 
     @property
     def next(self):
         if self.has_next:
-            return self.steps[self.index+1]
+            return self.steps[self.index + 1]
 
     def get_existing_data(self, raw=False):
         data = self.steps.get_existing_data()[self.index]
         fields = self.get_form_fields()
         if not raw:
+
             class FakeField:
                 storage = self.steps.get_storage()
 
             for field_name, value in data.items():
-                if field_name in fields and isinstance(fields[field_name],
-                                                       FileField):
+                if field_name in fields and isinstance(
+                    fields[field_name], FileField
+                ):
                     data[field_name] = FieldFile(None, FakeField, value)
         return data
 
@@ -157,11 +173,15 @@ class Steps(list):
         # TODO: Make it possible to change the `form_fields` attribute.
         self.form_fields = page.form_fields
         self.request = request
-        has_steps = any(isinstance(struct_child.block, FormStepBlock)
-                        for struct_child in self.form_fields)
+        has_steps = any(
+            isinstance(struct_child.block, FormStepBlock)
+            for struct_child in self.form_fields
+        )
         if has_steps:
-            steps = [Step(self, i, form_field)
-                     for i, form_field in enumerate(self.form_fields)]
+            steps = [
+                Step(self, i, form_field)
+                for i, form_field in enumerate(self.form_fields)
+            ]
         else:
             steps = [Step(self, 0, self.form_fields)]
         super().__init__(steps)
@@ -186,9 +206,10 @@ class Steps(list):
     @current.setter
     def current(self, new_index: int):
         if not isinstance(new_index, int):
-            raise TypeError('Use an integer to set the new current step.')
-        self.request.session[self.page.current_step_session_key] = \
-            self.clamp_index(new_index)
+            raise TypeError("Use an integer to set the new current step.")
+        self.request.session[
+            self.page.current_step_session_key
+        ] = self.clamp_index(new_index)
 
     def forward(self, increment: int = 1):
         self.current = self.current_index + increment
@@ -209,16 +230,19 @@ class Steps(list):
 
     def get_current_form(self):
         request = self.request
-        if request.method == 'POST':
-            step_value = request.POST.get('step', 'next')
-            if step_value == 'prev':
+        if request.method == "POST":
+            step_value = request.POST.get("step", "next")
+            if step_value == "prev":
                 self.backward()
             else:
                 return self.current.get_form_class()(
-                    request.POST, request.FILES,
-                    initial=self.current.get_existing_data())
+                    request.POST,
+                    request.FILES,
+                    initial=self.current.get_existing_data(),
+                )
         return self.current.get_form_class()(
-            initial=self.current.get_existing_data())
+            initial=self.current.get_existing_data()
+        )
 
     def get_storage(self):
         return self.page.get_storage()
@@ -228,21 +252,21 @@ class Steps(list):
         for name, field in form.fields.items():
             if isinstance(field, FileField):
                 file = form.cleaned_data[name]
-                if file == form.initial.get(name, ''):  # Nothing submitted.
+                if file == form.initial.get(name, ""):  # Nothing submitted.
                     form.cleaned_data[name] = file.name
                     continue
                 if submission is not None:
                     submission.delete_file(name)
                 if not file:  # 'Clear' was checked.
-                    form.cleaned_data[name] = ''
+                    form.cleaned_data[name] = ""
                     continue
                 directory = self.request.session.session_key
                 storage = self.get_storage()
-                Path(storage.path(directory)).mkdir(parents=True,
-                                                    exist_ok=True)
+                Path(storage.path(directory)).mkdir(parents=True, exist_ok=True)
                 path = storage.get_available_name(
-                    str(Path(directory) / file.name))
-                with storage.open(path, 'wb+') as destination:
+                    str(Path(directory) / file.name)
+                )
+                with storage.open(path, "wb+") as destination:
                     for chunk in file.chunks():
                         destination.write(chunk)
                 form.cleaned_data[name] = path
@@ -271,29 +295,33 @@ class Steps(list):
 class SessionFormSubmission(AbstractFormSubmission):
 
     session_key = CharField(max_length=40, null=True, default=None)
-    user = ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
-                      related_name='+', on_delete=PROTECT)
+    user = ForeignKey(
+        settings.AUTH_USER_MODEL,
+        null=True,
+        blank=True,
+        related_name="+",
+        on_delete=PROTECT,
+    )
     thumbnails_by_path = TextField(default=json.dumps({}))
-    last_modification = DateTimeField(_('last modification'), auto_now=True)
-    INCOMPLETE = 'incomplete'
-    COMPLETE = 'complete'
-    REVIEWED = 'reviewed'
-    APPROVED = 'approved'
-    REJECTED = 'rejected'
+    last_modification = DateTimeField(_("last modification"), auto_now=True)
+    INCOMPLETE = "incomplete"
+    COMPLETE = "complete"
+    REVIEWED = "reviewed"
+    APPROVED = "approved"
+    REJECTED = "rejected"
     STATUSES = (
-        (INCOMPLETE, _('Not submitted')),
-        (COMPLETE, _('In progress')),
-        (REVIEWED, _('Under consideration')),
-        (APPROVED, _('Approved')),
-        (REJECTED, _('Rejected')),
+        (INCOMPLETE, _("Not submitted")),
+        (COMPLETE, _("In progress")),
+        (REVIEWED, _("Under consideration")),
+        (APPROVED, _("Approved")),
+        (REJECTED, _("Rejected")),
     )
     status = CharField(max_length=10, choices=STATUSES, default=INCOMPLETE)
 
     class Meta:
-        verbose_name = _('form submission')
-        verbose_name_plural = _('form submissions')
-        unique_together = (('page', 'session_key'),
-                           ('page', 'user'))
+        verbose_name = _("form submission")
+        verbose_name_plural = _("form submissions")
+        unique_together = (("page", "session_key"), ("page", "user"))
         abstract = True
 
     @property
@@ -306,7 +334,8 @@ class SessionFormSubmission(AbstractFormSubmission):
 
     def get_session(self):
         return import_module(settings.SESSION_ENGINE).SessionStore(
-            session_key=self.session_key)
+            session_key=self.session_key
+        )
 
     def reset_step(self):
         session = self.get_session()
@@ -322,8 +351,8 @@ class SessionFormSubmission(AbstractFormSubmission):
 
     def get_thumbnail_path(self, path, width=64, height=64):
         if not path:
-            return ''
-        variant = '%s×%s' % (width, height)
+            return ""
+        variant = "%s×%s" % (width, height)
         thumbnails_by_path = json.loads(self.thumbnails_by_path)
         thumbnails_paths = thumbnails_by_path.get(path)
         if thumbnails_paths is None:
@@ -334,8 +363,7 @@ class SessionFormSubmission(AbstractFormSubmission):
                 return thumbnail_path
 
         path = Path(path)
-        thumbnail_path = str(path.with_suffix('.%s%s'
-                                              % (variant, path.suffix)))
+        thumbnail_path = str(path.with_suffix(".%s%s" % (variant, path.suffix)))
         storage = self.get_storage()
         thumbnail_path = storage.get_available_name(thumbnail_path)
 
@@ -344,8 +372,9 @@ class SessionFormSubmission(AbstractFormSubmission):
         thumbnail.save(storage.path(thumbnail_path))
 
         thumbnails_by_path[str(path)][variant] = thumbnail_path
-        self.thumbnails_by_path = json.dumps(thumbnails_by_path,
-                                             cls=StreamFormJSONEncoder)
+        self.thumbnails_by_path = json.dumps(
+            thumbnails_by_path, cls=StreamFormJSONEncoder
+        )
         self.save()
         return thumbnail_path
 
@@ -365,7 +394,8 @@ class SessionFormSubmission(AbstractFormSubmission):
                 path = data.get(name)
                 if path:
                     files[name] = [path] + list(
-                        self.get_existing_thumbnails(path))
+                        self.get_existing_thumbnails(path)
+                    )
         return files
 
     def get_all_files(self):
@@ -379,43 +409,44 @@ class SessionFormSubmission(AbstractFormSubmission):
             self.get_storage().delete(path)
             if path in thumbnails_by_path:
                 del thumbnails_by_path[path]
-        self.thumbnails_by_path = json.dumps(thumbnails_by_path,
-                                             cls=StreamFormJSONEncoder)
+        self.thumbnails_by_path = json.dumps(
+            thumbnails_by_path, cls=StreamFormJSONEncoder
+        )
         self.save()
 
     def render_email(self, value):
-        return (mark_safe('<a href="mailto:%s" target="_blank">%s</a>')
-                % (value, value))
+        return mark_safe('<a href="mailto:%s" target="_blank">%s</a>') % (
+            value,
+            value,
+        )
 
     def render_link(self, value):
-        return (mark_safe('<a href="%s" target="_blank">%s</a>')
-                % (value, value))
+        return mark_safe('<a href="%s" target="_blank">%s</a>') % (value, value)
 
     def render_image(self, value):
         storage = self.get_storage()
-        return (mark_safe('<a href="%s" target="_blank"><img src="%s" /></a>')
-                % (storage.url(value),
-                   storage.url(self.get_thumbnail_path(value))))
+        return mark_safe(
+            '<a href="%s" target="_blank"><img src="%s" /></a>'
+        ) % (storage.url(value), storage.url(self.get_thumbnail_path(value)))
 
     def render_file(self, value):
         return mark_safe('<a href="%s" target="_blank">%s</a>') % (
             self.get_storage().url(value),
-            Path(value).name
+            Path(value).name,
         )
 
     def format_value(self, field, value):
-        if value is None or value == '':
-            return '-'
+        if value is None or value == "":
+            return "-"
         new_value = self.form_page.format_value(field, value)
         if new_value != value:
             return new_value
         if value is True:
-            return 'Yes'
+            return "Yes"
         if value is False:
-            return 'No'
+            return "No"
         if isinstance(value, (list, tuple)):
-            return ', '.join([self.format_value(field, item)
-                              for item in value])
+            return ", ".join([self.format_value(field, item) for item in value])
         if isinstance(value, datetime.date):
             return value
         if isinstance(field, EmailField):
@@ -426,30 +457,37 @@ class SessionFormSubmission(AbstractFormSubmission):
             return self.render_image(value)
         if isinstance(field, FileField):
             return self.render_file(value)
-        if isinstance(value, SafeData) or hasattr(value, '__html__'):
+        if isinstance(value, SafeData) or hasattr(value, "__html__"):
             return value
         return str(value)
 
     def format_db_field(self, field_name, raw=False):
-        method = getattr(self, 'get_%s_display' % field_name, None)
+        method = getattr(self, "get_%s_display" % field_name, None)
         if method is not None:
             return method()
         value = getattr(self, field_name)
         if raw:
             return value
-        return self.format_value(self._meta.get_field(field_name).formfield(),
-                                 value)
+        return self.format_value(
+            self._meta.get_field(field_name).formfield(), value
+        )
 
     def get_steps_data(self, raw=False):
         steps_data = json.loads(self.form_data)
         if raw:
             return steps_data
-        fields_and_data_iterator = zip_longest(self.get_fields(by_step=True),
-                                               steps_data, fillvalue={})
+        fields_and_data_iterator = zip_longest(
+            self.get_fields(by_step=True), steps_data, fillvalue={}
+        )
         return [
-            OrderedDict([(name, self.format_value(field, step_data.get(name)))
-                         for name, field in step_fields.items()])
-            for step_fields, step_data in fields_and_data_iterator]
+            OrderedDict(
+                [
+                    (name, self.format_value(field, step_data.get(name)))
+                    for name, field in step_fields.items()
+                ]
+            )
+            for step_fields, step_data in fields_and_data_iterator
+        ]
 
     def get_extra_data(self, raw=False):
         return self.form_page.get_extra_data(self, raw=raw)
@@ -462,26 +500,30 @@ class SessionFormSubmission(AbstractFormSubmission):
             form_data.update(step_data)
         if add_metadata:
             form_data.update(
-                status=self.format_db_field('status', raw=raw),
-                user=self.format_db_field('user', raw=raw),
-                submit_time=self.format_db_field('submit_time', raw=raw),
-                last_modification=self.format_db_field('last_modification',
-                                                       raw=raw),
+                status=self.format_db_field("status", raw=raw),
+                user=self.format_db_field("user", raw=raw),
+                submit_time=self.format_db_field("submit_time", raw=raw),
+                last_modification=self.format_db_field(
+                    "last_modification", raw=raw
+                ),
             )
         return form_data
 
     def steps_with_data_iterator(self, raw=False):
         for step, step_data_fields, step_data in zip(
-                self.form_page.get_steps(),
-                self.form_page.get_data_fields(by_step=True),
-                self.get_steps_data(raw=raw)):
-            yield step, [(field_name, field_label, step_data[field_name])
-                         for field_name, field_label in step_data_fields]
+            self.form_page.get_steps(),
+            self.form_page.get_data_fields(by_step=True),
+            self.get_steps_data(raw=raw),
+        ):
+            yield step, [
+                (field_name, field_label, step_data[field_name])
+                for field_name, field_label in step_data_fields
+            ]
 
 
 @receiver(post_delete, sender=SessionFormSubmission)
 def delete_files(sender, **kwargs):
-    instance = kwargs['instance']
+    instance = kwargs["instance"]
     instance.reset_step()
     storage = instance.get_storage()
     for path in instance.get_all_files():
@@ -514,34 +556,35 @@ class SubmissionRevisionQuerySet(QuerySet):
 
 
 class SubmissionRevision(Model):
-    CREATED = 'created'
-    CHANGED = 'changed'
-    DELETED = 'deleted'
+    CREATED = "created"
+    CHANGED = "changed"
+    DELETED = "deleted"
     TYPES = (
-        (CREATED, _('Created')),
-        (CHANGED, _('Changed')),
-        (DELETED, _('Deleted')),
+        (CREATED, _("Created")),
+        (CHANGED, _("Changed")),
+        (DELETED, _("Deleted")),
     )
     type = CharField(max_length=7, choices=TYPES)
     created_at = DateTimeField(auto_now_add=True)
-    submission_ct = ForeignKey('contenttypes.ContentType', on_delete=CASCADE)
+    submission_ct = ForeignKey("contenttypes.ContentType", on_delete=CASCADE)
     submission_id = TextField()
-    submission = GenericForeignKey('submission_ct', 'submission_id')
+    submission = GenericForeignKey("submission_ct", "submission_id")
     data = TextField()
     summary = TextField()
 
     objects = SubmissionRevisionQuerySet.as_manager()
 
     class Meta:
-        ordering = ('-created_at',)
+        ordering = ("-created_at",)
         abstract = True
 
     @staticmethod
     def get_filters_for(submission):
         return {
-            'submission_ct':
-                ContentType.objects.get_for_model(submission._meta.model),
-            'submission_id': str(submission.pk),
+            "submission_ct": ContentType.objects.get_for_model(
+                submission._meta.model
+            ),
+            "submission_id": str(submission.pk),
         }
 
     @classmethod
@@ -554,56 +597,72 @@ class SubmissionRevision(Model):
             value2 = data2.get(k)
             if value2 == value1 or not value1 and not value2:
                 continue
-            is_hidden = (isinstance(value1, hidden_types)
-                         or isinstance(value2, hidden_types))
+            is_hidden = isinstance(value1, hidden_types) or isinstance(
+                value2, hidden_types
+            )
 
             # Escapes newlines as they are used as separator inside summaries.
             if isinstance(value1, str):
-                value1 = value1.replace('\n', r'\n')
+                value1 = value1.replace("\n", r"\n")
             if isinstance(value2, str):
-                value2 = value2.replace('\n', r'\n')
+                value2 = value2.replace("\n", r"\n")
 
             if value2 and not value1:
                 diff.append(
-                    ((_('“%s” set.') % label) if is_hidden
-                     else (_('“%s” set to “%s”.')) % (label, value2)))
+                    (
+                        (_("“%s” set.") % label)
+                        if is_hidden
+                        else (_("“%s” set to “%s”.")) % (label, value2)
+                    )
+                )
             elif value1 and not value2:
-                diff.append(_('“%s” unset.') % label)
+                diff.append(_("“%s” unset.") % label)
             else:
-                diff.append(((_('“%s” changed.') % label) if is_hidden
-                             else (_('“%s” changed from “%s” to “%s”.')
-                                   % (label, value1, value2))))
-        return '\n'.join(diff)
+                diff.append(
+                    (
+                        (_("“%s” changed.") % label)
+                        if is_hidden
+                        else (
+                            _("“%s” changed from “%s” to “%s”.")
+                            % (label, value1, value2)
+                        )
+                    )
+                )
+        return "\n".join(diff)
 
     @classmethod
     def create_from_submission(cls, submission, revision_type):
         page = submission.form_page
         try:
-            previous = cls.objects.for_submission(
-                submission).latest('created_at')
+            previous = cls.objects.for_submission(submission).latest(
+                "created_at"
+            )
         except cls.DoesNotExist:
             previous_data = {}
         else:
             previous_data = previous.get_data()
         filters = cls.get_filters_for(submission)
         data = submission.get_data(raw=True, add_metadata=False)
-        data['status'] = submission.status
+        data["status"] = submission.status
         if revision_type == cls.CREATED:
-            summary = _('Submission created.')
+            summary = _("Submission created.")
         elif revision_type == cls.DELETED:
-            summary = _('Submission deleted.')
+            summary = _("Submission deleted.")
         else:
             summary = cls.diff_summary(page, previous_data, data)
         if not summary:  # Nothing changed.
             return
         filters.update(
-            type=revision_type, data=json.dumps(data, cls=StreamFormJSONEncoder), summary=summary
+            type=revision_type,
+            data=json.dumps(data, cls=StreamFormJSONEncoder),
+            summary=summary,
         )
         return cls.objects.create(**filters)
 
     def get_data(self):
         return json.loads(self.data)
 
+
 # ORIGINAL NORIPYT CODE.
 # We don't want these receivers triggering.
 
@@ -632,10 +691,10 @@ class StreamFormMixin:
 
     @property
     def current_step_session_key(self):
-        return '%s:step' % self.pk
+        return "%s:step" % self.pk
 
     def get_steps(self, request=None):
-        if not hasattr(self, 'steps'):
+        if not hasattr(self, "steps"):
             steps = Steps(self, request=request)
             if request is None:
                 return steps
@@ -653,7 +712,7 @@ class StreamFormMixin:
     def get_context(self, request, *args, **kwargs):
         context = super().get_context(request, *args, **kwargs)
         self.steps = self.get_steps(request)
-        step_value = request.GET.get('step')
+        step_value = request.GET.get("step")
         if step_value is not None and step_value.isdigit():
             self.steps.current = int(step_value) - 1
         form = self.steps.get_current_form()
@@ -662,7 +721,8 @@ class StreamFormMixin:
             step=self.steps.current,
             form=form,
             markups_and_bound_fields=list(
-                self.steps.current.get_markups_and_bound_fields(form)),
+                self.steps.current.get_markups_and_bound_fields(form)
+            ),
         )
         return context
 
@@ -671,7 +731,7 @@ class StreamFormMixin:
 
     @staticmethod
     def get_form_class_bases():
-        return Form,
+        return (Form,)
 
     @staticmethod
     def get_submission_class():
@@ -680,25 +740,37 @@ class StreamFormMixin:
     def get_submission(self, request):
         Submission = self.get_submission_class()
         if request.user.is_authenticated:
-            user_submission = Submission.objects.filter(
-                user=request.user, page=self).order_by('-pk').first()
+            user_submission = (
+                Submission.objects.filter(user=request.user, page=self)
+                .order_by("-pk")
+                .first()
+            )
             if user_submission is None:
-                return Submission(user=request.user, page=self, form_data='[]')
+                return Submission(user=request.user, page=self, form_data="[]")
             return user_submission
 
-        user_submission = Submission.objects.filter(
-            session_key=request.session.session_key, page=self
-        ).order_by('-pk').first()
+        user_submission = (
+            Submission.objects.filter(
+                session_key=request.session.session_key, page=self
+            )
+            .order_by("-pk")
+            .first()
+        )
         if user_submission is None:
-            return Submission(session_key=request.session.session_key,
-                              page=self, form_data='[]')
+            return Submission(
+                session_key=request.session.session_key,
+                page=self,
+                form_data="[]",
+            )
         return user_submission
 
     def get_success_url(self):
-        form_complete_models = [model for model in apps.get_models()
-                                if issubclass(model, FormCompleteMixin)]
-        cts = (ContentType.objects
-               .get_for_models(*form_complete_models).values())
+        form_complete_models = [
+            model
+            for model in apps.get_models()
+            if issubclass(model, FormCompleteMixin)
+        ]
+        cts = ContentType.objects.get_for_models(*form_complete_models).values()
         first_child = self.get_children().filter(content_type__in=cts).first()
         if first_child is None:
             return self.url
@@ -707,14 +779,13 @@ class StreamFormMixin:
     def serve_success(self, request, *args, **kwargs):
         url = self.get_success_url()
         if url == self.url:
-            messages.success(request,
-                             _('Successfully submitted the form.'))
+            messages.success(request, _("Successfully submitted the form."))
         return HttpResponseRedirect(url)
 
     def serve(self, request, *args, **kwargs):
         context = self.get_context(request)
-        form = context['form']
-        if request.method == 'POST' and form.is_valid():
+        form = context["form"]
+        if request.method == "POST" and form.is_valid():
             is_complete = self.steps.update_data()
             if is_complete:
                 return self.serve_success(request, *args, **kwargs)
@@ -723,22 +794,32 @@ class StreamFormMixin:
 
     def get_data_fields(self, by_step=False, add_metadata=True):
         if by_step:
-            return [[(field_name, field.label)
-                     for field_name, field in step_fields.items()]
-                    for step_fields in self.get_form_fields(by_step=True)]
+            return [
+                [
+                    (field_name, field.label)
+                    for field_name, field in step_fields.items()
+                ]
+                for step_fields in self.get_form_fields(by_step=True)
+            ]
 
         data_fields = []
         data_fields.extend(self.get_extra_data_fields())
         if add_metadata:
-            data_fields.extend((
-                ('status', _('Status')),
-                ('user', _('User')),
-                ('submit_time', _('First modification')),
-                ('last_modification', _('Last modification'))))
-        data_fields.extend([
-            (field_name, field_label)
-            for step_data_fields in self.get_data_fields(by_step=True)
-            for field_name, field_label in step_data_fields])
+            data_fields.extend(
+                (
+                    ("status", _("Status")),
+                    ("user", _("User")),
+                    ("submit_time", _("First modification")),
+                    ("last_modification", _("Last modification")),
+                )
+            )
+        data_fields.extend(
+            [
+                (field_name, field_label)
+                for step_data_fields in self.get_data_fields(by_step=True)
+                for field_name, field_label in step_data_fields
+            ]
+        )
         return data_fields
 
     def get_extra_data_fields(self):
@@ -767,7 +848,7 @@ class ClosingFormMixin(Model):
         if self.closed_template is None:
             template = self.get_template(request, *args, **kwargs)
             base, ext = os.path.splitext(template)
-            return '%s_closed%s' % (base, ext)
+            return "%s_closed%s" % (base, ext)
         return self.closed_template
 
     def serve_closed(self, request, *args, **kwargs):
@@ -789,19 +870,24 @@ class FormCompleteMixin:
 
     def serve(self, request, *args, **kwargs):
         form_page = self.get_form_page()
-        if isinstance(form_page, LoginRequiredMixin) \
-                and not request.user.is_authenticated():
+        if (
+            isinstance(form_page, LoginRequiredMixin)
+            and not request.user.is_authenticated()
+        ):
             return HttpResponseRedirect(form_page.url)
         self.submission = form_page.get_submission(request)
-        if self.submission is not None and self.submission.is_complete \
-                or getattr(request, 'is_preview', False):
+        if (
+            self.submission is not None
+            and self.submission.is_complete
+            or getattr(request, "is_preview", False)
+        ):
             return super().serve(request, *args, **kwargs)
         return HttpResponseRedirect(form_page.url)
 
     def get_context(self, *args, **kwargs):
         context = super().get_context(*args, **kwargs)
-        if hasattr(self, 'submission'):
-            context['submission'] = self.submission
+        if hasattr(self, "submission"):
+            context["submission"] = self.submission
         return context
 
 
@@ -812,7 +898,7 @@ class LoginRequiredMixin:
         if self.login_required_template is None:
             template = self.get_template(request, *args, **kwargs)
             base, ext = os.path.splitext(template)
-            return '%s_login_required%s' % (base, ext)
+            return "%s_login_required%s" % (base, ext)
         return self.login_required_template
 
     def serve_login_required(self, request, *args, **kwargs):

+ 172 - 110
coderedcms/wagtail_flexible_forms/wagtail_hooks.py

@@ -4,8 +4,12 @@ from django.shortcuts import redirect
 from django.urls import path, reverse
 from django.utils.translation import gettext_lazy as _
 from wagtail.contrib.modeladmin.helpers import (
-    PermissionHelper, PagePermissionHelper, PageAdminURLHelper, AdminURLHelper,
-    ButtonHelper)
+    PermissionHelper,
+    PagePermissionHelper,
+    PageAdminURLHelper,
+    AdminURLHelper,
+    ButtonHelper,
+)
 from wagtail.contrib.modeladmin.options import ModelAdmin
 from wagtail.contrib.modeladmin.views import IndexView, InstanceSpecificView
 from wagtail.admin import messages
@@ -17,7 +21,7 @@ from .models import SessionFormSubmission
 
 
 class FormIndexView(IndexView):
-    page_title = _('Forms')
+    page_title = _("Forms")
 
 
 class FormPermissionHelper(PagePermissionHelper):
@@ -48,17 +52,21 @@ class FormPermissionHelper(PagePermissionHelper):
 
 class FormURLHelper(PageAdminURLHelper):
     def _get_action_url_pattern(self, action):
-        if action == 'index':
-            return r'^stream_forms/$'
-        return r'^stream_forms/%s/$' % action
+        if action == "index":
+            return r"^stream_forms/$"
+        return r"^stream_forms/%s/$" % action
 
 
 class FormAdmin(ModelAdmin):
     model = Page
-    menu_label = _('Forms')
-    menu_icon = 'form'
-    list_display = ('title', 'unprocessed_submissions_link',
-                    'all_submissions_link', 'edit_link')
+    menu_label = _("Forms")
+    menu_icon = "form"
+    list_display = (
+        "title",
+        "unprocessed_submissions_link",
+        "all_submissions_link",
+        "edit_link",
+    )
     index_view_class = FormIndexView
     permission_helper_class = FormPermissionHelper
     url_helper_class = FormURLHelper
@@ -66,37 +74,48 @@ class FormAdmin(ModelAdmin):
     def get_queryset(self, request):
         return get_forms_for_user(request.user)
 
-    def all_submissions_link(self, obj, label=_('See all submissions'),
-                             url_suffix=''):
+    def all_submissions_link(
+        self, obj, label=_("See all submissions"), url_suffix=""
+    ):
         return '<a href="%s?page_id=%s%s">%s</a>' % (
-            reverse(SubmissionAdmin().url_helper.get_action_url_name('index')),
-            obj.pk, url_suffix, label)
-    all_submissions_link.short_description = ''
+            reverse(SubmissionAdmin().url_helper.get_action_url_name("index")),
+            obj.pk,
+            url_suffix,
+            label,
+        )
+
+    all_submissions_link.short_description = ""
     all_submissions_link.allow_tags = True
 
     def unprocessed_submissions_link(self, obj):
         return self.all_submissions_link(
-            obj, _('See unprocessed submissions'),
-            '&status=%s' % SubmissionStatusFilter.unprocessed_status)
-    unprocessed_submissions_link.short_description = ''
+            obj,
+            _("See unprocessed submissions"),
+            "&status=%s" % SubmissionStatusFilter.unprocessed_status,
+        )
+
+    unprocessed_submissions_link.short_description = ""
     unprocessed_submissions_link.allow_tags = True
 
     def edit_link(self, obj):
         return '<a href="%s">%s</a>' % (
-            reverse('wagtailadmin_pages:edit', args=(obj.pk,)),
-            _('Edit this form page'))
-    edit_link.short_description = ''
+            reverse("wagtailadmin_pages:edit", args=(obj.pk,)),
+            _("Edit this form page"),
+        )
+
+    edit_link.short_description = ""
     edit_link.allow_tags = True
 
 
 class SubmissionStatusFilter(SimpleListFilter):
-    title = _('status')
-    parameter_name = 'status'
-    unprocessed_status = ','.join((SessionFormSubmission.COMPLETE,
-                                   SessionFormSubmission.REVIEWED))
+    title = _("status")
+    parameter_name = "status"
+    unprocessed_status = ",".join(
+        (SessionFormSubmission.COMPLETE, SessionFormSubmission.REVIEWED)
+    )
 
     def lookups(self, request, model_admin):
-        yield (self.unprocessed_status, _('Complete or reviewed'))
+        yield (self.unprocessed_status, _("Complete or reviewed"))
         for status, verbose_status in SessionFormSubmission.STATUSES:
             if status != SessionFormSubmission.INCOMPLETE:
                 yield status, verbose_status
@@ -105,8 +124,8 @@ class SubmissionStatusFilter(SimpleListFilter):
         status = self.value()
         if not status:
             return queryset
-        if ',' in status:
-            return queryset.filter(status__in=status.split(','))
+        if "," in status:
+            return queryset.filter(status__in=status.split(","))
         return queryset.filter(status=status)
 
 
@@ -129,149 +148,192 @@ class SubmissionPermissionHelper(PermissionHelper):
 
 class SubmissionURLHelper(AdminURLHelper):
     def _get_action_url_pattern(self, action):
-        if action == 'index':
-            return r'^%s/%s/$' % (self.opts.app_label, 'submissions')
-        return r'^%s/%s/%s/$' % (self.opts.app_label, 'submissions', action)
+        if action == "index":
+            return r"^%s/%s/$" % (self.opts.app_label, "submissions")
+        return r"^%s/%s/%s/$" % (self.opts.app_label, "submissions", action)
 
     def _get_object_specific_action_url_pattern(self, action):
-        return r'^%s/%s/%s/(?P<instance_pk>[-\w]+)/$' % (
-            self.opts.app_label, 'submissions', action)
+        return r"^%s/%s/%s/(?P<instance_pk>[-\w]+)/$" % (
+            self.opts.app_label,
+            "submissions",
+            action,
+        )
 
 
 class SubmissionButtonHelper(ButtonHelper):
-    def set_status_button(self, pk, status, label, title, classnames_add=None,
-                          classnames_exclude=None):
+    def set_status_button(
+        self,
+        pk,
+        status,
+        label,
+        title,
+        classnames_add=None,
+        classnames_exclude=None,
+    ):
         if classnames_add is None:
             classnames_add = []
         if classnames_exclude is None:
             classnames_exclude = []
-        classnames = self.finalise_classname(classnames_add,
-                                             classnames_exclude)
-        url = self.url_helper.get_action_url('set_status', quote(pk))
-        url += '?status=' + status
+        classnames = self.finalise_classname(classnames_add, classnames_exclude)
+        url = self.url_helper.get_action_url("set_status", quote(pk))
+        url += "?status=" + status
         return {
-            'url': url,
-            'label': label,
-            'classname': classnames,
-            'title': title,
+            "url": url,
+            "label": label,
+            "classname": classnames,
+            "title": title,
         }
 
-    def reviewed_button(self, pk, classnames_add=None,
-                        classnames_exclude=None):
+    def reviewed_button(self, pk, classnames_add=None, classnames_exclude=None):
         if classnames_add is None:
             classnames_add = []
-        return self.set_status_button(pk, self.model.REVIEWED,
-                                      _('mark as reviewed'),
-                                      _('Mark this submission as reviewed'),
-                                      classnames_add=classnames_add,
-                                      classnames_exclude=classnames_exclude)
-
-    def approve_button(self, pk, classnames_add=None,
-                       classnames_exclude=None):
+        return self.set_status_button(
+            pk,
+            self.model.REVIEWED,
+            _("mark as reviewed"),
+            _("Mark this submission as reviewed"),
+            classnames_add=classnames_add,
+            classnames_exclude=classnames_exclude,
+        )
+
+    def approve_button(self, pk, classnames_add=None, classnames_exclude=None):
         if classnames_add is None:
             classnames_add = []
-        if 'button-secondary' in classnames_add:
-            classnames_add.remove('button-secondary')
-        classnames_add = ['yes'] + classnames_add
-        return self.set_status_button(pk, self.model.APPROVED, _('approve'),
-                                      _('Approve this submission'),
-                                      classnames_add=classnames_add,
-                                      classnames_exclude=classnames_exclude)
-
-    def reject_button(self, pk, classnames_add=None,
-                      classnames_exclude=None):
+        if "button-secondary" in classnames_add:
+            classnames_add.remove("button-secondary")
+        classnames_add = ["yes"] + classnames_add
+        return self.set_status_button(
+            pk,
+            self.model.APPROVED,
+            _("approve"),
+            _("Approve this submission"),
+            classnames_add=classnames_add,
+            classnames_exclude=classnames_exclude,
+        )
+
+    def reject_button(self, pk, classnames_add=None, classnames_exclude=None):
         if classnames_add is None:
             classnames_add = []
-        if 'button-secondary' in classnames_add:
-            classnames_add.remove('button-secondary')
-        classnames_add = ['no'] + classnames_add
-        return self.set_status_button(pk, self.model.REJECTED, _('reject'),
-                                      _('Reject this submission'),
-                                      classnames_add=classnames_add,
-                                      classnames_exclude=classnames_exclude)
-
-    def get_buttons_for_obj(self, obj, exclude=None, classnames_add=None,
-                            classnames_exclude=None):
+        if "button-secondary" in classnames_add:
+            classnames_add.remove("button-secondary")
+        classnames_add = ["no"] + classnames_add
+        return self.set_status_button(
+            pk,
+            self.model.REJECTED,
+            _("reject"),
+            _("Reject this submission"),
+            classnames_add=classnames_add,
+            classnames_exclude=classnames_exclude,
+        )
+
+    def get_buttons_for_obj(
+        self, obj, exclude=None, classnames_add=None, classnames_exclude=None
+    ):
         buttons = super().get_buttons_for_obj(
-            obj, exclude=exclude, classnames_add=classnames_add,
-            classnames_exclude=classnames_exclude)
+            obj,
+            exclude=exclude,
+            classnames_add=classnames_add,
+            classnames_exclude=classnames_exclude,
+        )
         pk = getattr(obj, self.opts.pk.attname)
         status_buttons = []
         if obj.status != obj.REVIEWED:
-            status_buttons.append(self.reviewed_button(
-                pk, classnames_add=classnames_add,
-                classnames_exclude=classnames_exclude))
+            status_buttons.append(
+                self.reviewed_button(
+                    pk,
+                    classnames_add=classnames_add,
+                    classnames_exclude=classnames_exclude,
+                )
+            )
         if obj.status != obj.APPROVED:
-            status_buttons.append(self.approve_button(
-                pk, classnames_add=classnames_add,
-                classnames_exclude=classnames_exclude))
+            status_buttons.append(
+                self.approve_button(
+                    pk,
+                    classnames_add=classnames_add,
+                    classnames_exclude=classnames_exclude,
+                )
+            )
         if obj.status != obj.REJECTED:
-            status_buttons.append(self.reject_button(
-                pk, classnames_add=classnames_add,
-                classnames_exclude=classnames_exclude))
+            status_buttons.append(
+                self.reject_button(
+                    pk,
+                    classnames_add=classnames_add,
+                    classnames_exclude=classnames_exclude,
+                )
+            )
         return status_buttons + buttons
 
 
 class SetStatusView(InstanceSpecificView):
     def check_action_permitted(self, user):
-        return self.permission_helper.user_can_set_status_obj(user,
-                                                              self.instance)
+        return self.permission_helper.user_can_set_status_obj(
+            user, self.instance
+        )
 
     def get(self, request, *args, **kwargs):
-        status = request.GET.get('status')
+        status = request.GET.get("status")
         if status in dict(self.model.STATUSES):
             previous_status = self.instance.status
             self.instance.status = status
             self.instance.save()
             verbose_label = self.instance.get_status_display()
-            if 'revert' in request.GET:
-                messages.success(request, 'Reverted to the “%s” status.'
-                                 % verbose_label)
+            if "revert" in request.GET:
+                messages.success(
+                    request, "Reverted to the “%s” status." % verbose_label
+                )
             else:
-                revert_url = (self.url_helper.get_action_url('set_status',
-                                                             self.instance_pk)
-                              + '?revert&status=' + previous_status)
+                revert_url = (
+                    self.url_helper.get_action_url(
+                        "set_status", self.instance_pk
+                    )
+                    + "?revert&status="
+                    + previous_status
+                )
                 messages.success(
                     request,
-                    'Successfully changed the status to “%s”.' % verbose_label,
-                    buttons=[messages.button(revert_url, _('Revert'))])
-        url = request.META.get('HTTP_REFERER')
+                    "Successfully changed the status to “%s”." % verbose_label,
+                    buttons=[messages.button(revert_url, _("Revert"))],
+                )
+        url = request.META.get("HTTP_REFERER")
         if url is None:
-            url = (self.url_helper.get_action_url('index')
-                   + '?page_id=%s' % self.instance.page_id)
+            url = (
+                self.url_helper.get_action_url("index")
+                + "?page_id=%s" % self.instance.page_id
+            )
         return redirect(url)
 
 
 class SubmissionAdmin(ModelAdmin):
     model = SessionFormSubmission
-    menu_icon = 'form'
+    menu_icon = "form"
     permission_helper_class = SubmissionPermissionHelper
     url_helper_class = SubmissionURLHelper
     button_helper_class = SubmissionButtonHelper
     set_status_view_class = SetStatusView
-    list_display = ('status', 'user', 'submit_time', 'last_modification')
-    list_filter = (SubmissionStatusFilter, 'submit_time', 'last_modification')
-    search_fields = ('user__first_name', 'user__last_name')
+    list_display = ("status", "user", "submit_time", "last_modification")
+    list_filter = (SubmissionStatusFilter, "submit_time", "last_modification")
+    search_fields = ("user__first_name", "user__last_name")
 
     def register_with_wagtail(self):
-        @hooks.register('register_permissions')
+        @hooks.register("register_permissions")
         def register_permissions():
             return self.get_permissions_for_registration()
 
-        @hooks.register('register_admin_urls')
+        @hooks.register("register_admin_urls")
         def register_admin_urls():
             return self.get_admin_urls_for_registration()
 
     def get_queryset(self, request):
         qs = super().get_queryset(request)
         form_pages = get_forms_for_user(request.user)
-        return (qs.filter(page__in=form_pages)
-                .exclude(status=self.model.INCOMPLETE))
+        return qs.filter(page__in=form_pages).exclude(
+            status=self.model.INCOMPLETE
+        )
 
     def get_form_page(self, request):
         form_pages = get_forms_for_user(request.user)
         try:
-            return form_pages.get(pk=int(request.GET['page_id'])).specific
+            return form_pages.get(pk=int(request.GET["page_id"])).specific
         except (KeyError, TypeError, ValueError, Page.DoesNotExist):
             pass
 
@@ -294,7 +356,7 @@ class SubmissionAdmin(ModelAdmin):
         return fields
 
     def set_status_view(self, request, instance_pk):
-        kwargs = {'model_admin': self, 'instance_pk': instance_pk}
+        kwargs = {"model_admin": self, "instance_pk": instance_pk}
         view_class = self.set_status_view_class
         return view_class.as_view(**kwargs)(request)
 
@@ -302,9 +364,9 @@ class SubmissionAdmin(ModelAdmin):
         urls = super().get_admin_urls_for_registration()
         urls += (
             path(
-                self.url_helper.get_action_url_pattern('set_status'),
+                self.url_helper.get_action_url_pattern("set_status"),
                 self.set_status_view,
-                name=self.url_helper.get_action_url_name('set_status')
+                name=self.url_helper.get_action_url_name("set_status"),
             ),
         )
         return urls

+ 27 - 25
coderedcms/wagtail_hooks.py

@@ -14,29 +14,29 @@ from wagtailcache.cache import clear_cache
 from coderedcms import __version__
 
 
-@hooks.register('insert_global_admin_css')
+@hooks.register("insert_global_admin_css")
 def global_admin_css():
     return format_html(
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
-        static('coderedcms/css/crx-admin.css'),
+        static("coderedcms/css/crx-admin.css"),
         __version__,
     )
 
 
-@hooks.register('insert_editor_css')
+@hooks.register("insert_editor_css")
 def editor_css():
     return format_html(
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
-        static('coderedcms/css/crx-editor.css'),
+        static("coderedcms/css/crx-editor.css"),
         __version__,
     )
 
 
-@hooks.register('insert_editor_js')
+@hooks.register("insert_editor_js")
 def collapsible_js():
     return format_html(
         '<script src="{}?v={}"></script>',
-        static('coderedcms/js/crx-editor.js'),
+        static("coderedcms/js/crx-editor.js"),
         __version__,
     )
 
@@ -79,16 +79,16 @@ def clear_wagtailcache(*args, **kwargs):
 # Clear cache whenever pages/snippets are changed. Err on the side of clearing
 # the cache vs not clearing the cache, as this usually leads to support requests
 # when staff members make edits but do not see the changes.
-hooks.register('after_delete_page', clear_wagtailcache)
-hooks.register('after_move_page', clear_wagtailcache)
-hooks.register('after_publish_page', clear_wagtailcache)
-hooks.register('after_unpublish_page', clear_wagtailcache)
-hooks.register('after_create_snippet', clear_wagtailcache)
-hooks.register('after_edit_snippet', clear_wagtailcache)
-hooks.register('after_delete_snippet', clear_wagtailcache)
+hooks.register("after_delete_page", clear_wagtailcache)
+hooks.register("after_move_page", clear_wagtailcache)
+hooks.register("after_publish_page", clear_wagtailcache)
+hooks.register("after_unpublish_page", clear_wagtailcache)
+hooks.register("after_create_snippet", clear_wagtailcache)
+hooks.register("after_edit_snippet", clear_wagtailcache)
+hooks.register("after_delete_snippet", clear_wagtailcache)
 
 
-@hooks.register('filter_form_submissions_for_user')
+@hooks.register("filter_form_submissions_for_user")
 def codered_forms(user, editable_forms):
     """
     Add our own CoderedFormPage to editable_forms, since wagtail is unaware
@@ -96,13 +96,13 @@ def codered_forms(user, editable_forms):
     and wagtail.contrib.forms.get_form_types()
     """
     from coderedcms.models import CoderedFormMixin
+
     form_models = [
-        model for model in get_page_models()
+        model
+        for model in get_page_models()
         if issubclass(model, CoderedFormMixin)
     ]
-    form_types = list(
-        ContentType.objects.get_for_models(*form_models).values()
-    )
+    form_types = list(ContentType.objects.get_for_models(*form_models).values())
 
     editable_forms = UserPagePermissionsProxy(user).editable_pages()
     editable_forms = editable_forms.filter(content_type__in=form_types)
@@ -110,7 +110,7 @@ def codered_forms(user, editable_forms):
     return editable_forms
 
 
-@hooks.register('before_serve_document')
+@hooks.register("before_serve_document")
 def serve_document_directly(document, request):
     """
     This hook prevents documents from being downloaded unless
@@ -118,8 +118,10 @@ def serve_document_directly(document, request):
     """
     content_type, content_encoding = mimetypes.guess_type(document.filename)
     response = HttpResponse(document.file.read(), content_type=content_type)
-    response['Content-Disposition'] = 'inline;filename="{0}"'.format(document.filename)
-    response['Content-Encoding'] = content_encoding
+    response["Content-Disposition"] = 'inline;filename="{0}"'.format(
+        document.filename
+    )
+    response["Content-Encoding"] = content_encoding
     return response
 
 
@@ -128,10 +130,10 @@ class ImportExportMenuItem(MenuItem):
         return request.user.is_superuser
 
 
-@hooks.register('register_settings_menu_item')
+@hooks.register("register_settings_menu_item")
 def register_import_export_menu_item():
     return ImportExportMenuItem(
-        _('Import'),
-        reverse('import_index'),
-        classnames='icon icon-download',
+        _("Import"),
+        reverse("import_index"),
+        classnames="icon icon-download",
     )

+ 16 - 9
coderedcms/widgets.py

@@ -2,14 +2,15 @@ from django import forms
 
 
 class ColorPickerWidget(forms.TextInput):
-    input_type = 'color'
+    input_type = "color"
 
 
 class ClassifierSelectWidget(forms.CheckboxSelectMultiple):
-    template_name = 'coderedcms/widgets/checkbox_classifiers.html'
+    template_name = "coderedcms/widgets/checkbox_classifiers.html"
 
     def optgroups(self, name, value, attrs=None):
         from coderedcms.models.snippet_models import Classifier
+
         classifiers = Classifier.objects.all().select_related()
 
         groups = []
@@ -27,15 +28,21 @@ class ClassifierSelectWidget(forms.CheckboxSelectMultiple):
             groups.append((group_name, subgroup, index))
 
             for subvalue, sublabel in choices:
-                selected = (
-                    str(subvalue) in value and
-                    (not has_selected or self.allow_multiple_selected)
+                selected = str(subvalue) in value and (
+                    not has_selected or self.allow_multiple_selected
                 )
                 has_selected |= selected
-                subgroup.append(self.create_option(
-                    name, subvalue, sublabel, selected, index,
-                    subindex=subindex, attrs=attrs,
-                ))
+                subgroup.append(
+                    self.create_option(
+                        name,
+                        subvalue,
+                        sublabel,
+                        selected,
+                        index,
+                        subindex=subindex,
+                        attrs=attrs,
+                    )
+                )
                 if subindex is not None:
                     subindex += 1
         return groups

+ 2 - 2
docs/conf.py

@@ -34,7 +34,7 @@ author = "CodeRed LLC"
 extensions = ["sphinx_wagtail_theme"]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
@@ -86,6 +86,6 @@ html_static_path = ["_static"]
 # defined by theme itself.  Builtin themes are using these templates by
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # 'searchbox.html']``.
-html_sidebars = {'**': ['searchbox.html', 'globaltoc.html', 'sponsor.html']}
+html_sidebars = {"**": ["searchbox.html", "globaltoc.html", "sponsor.html"]}
 
 html_css_files = ["custom.css"]

+ 5 - 7
docs/contributing/index.rst

@@ -205,20 +205,18 @@ For example, here is how you would add tests for a new abstract page type,
 Static Analysis
 ---------------
 
-Flake8 is used to check for syntax and style errors. To analyze the entire
-codebase, run:
+All code should be formatted with ``black`` before committing:
 
 .. code-block:: console
 
-    $ flake8 .
+    $ black .
 
-Alternatively, our continuous integration only analyzes the diff between your
-changes and the dev branch. To analyze just the diff of your current changes,
-run the PowerShell script:
+Flake8 is used to check for syntax and style errors. To analyze the entire
+codebase, run:
 
 .. code-block:: console
 
-    $ ./ci/run-flake8.ps1
+    $ flake8 .
 
 
 Contributor Guidelines

+ 10 - 13
pyproject.toml

@@ -1,26 +1,23 @@
 [tool.black]
-line-length = 100
-target-version = ['py37']
-include = '\.pyi?$'
+line-length = 80
+target-version = ['py37', 'py38', 'py39', 'py310']
 exclude = '''
-
 (
   /(
-      \.eggs         # exclude a few common directories in the
-    | \.git          # root of the project
+      \.eggs
+    | \.git
+    | \.github
     | \.hg
     | \.mypy_cache
     | \.tox
     | \.venv
     | _build
-    | buck-out
     | build
-    | dist
-    | coderedcms/project_template
-    | coderedcms/tests/settings.py
-    | .*/migrations
-    | \.github
     | ci
+    | dist
+    | migrations
+    | testproject
+    | venv
   )/
 )
-'''
+'''

+ 1 - 0
requirements-ci.txt

@@ -2,6 +2,7 @@
 -e .
 
 # Requirements, in addition to coderedcms, needed for CI runner.
+black
 codespell
 flake8
 pytest-cov

+ 1 - 1
setup.cfg

@@ -1,5 +1,5 @@
 [flake8]
-exclude = .venv/*,build/*,coderedcms/project_template/*,*/migrations/*,schema.py
+exclude = .venv/*,build/*,*/migrations/*,schema.py
 max-line-length = 100
 
 [tool:pytest]

+ 41 - 41
setup.py

@@ -2,61 +2,61 @@ import os
 from setuptools import setup
 from coderedcms import __version__
 
-with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8") as readme:
+with open(
+    os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8"
+) as readme:
     README = readme.read()
 
 # allow setup.py to be run from any path
 os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
 
 setup(
-    name='coderedcms',
+    name="coderedcms",
     version=__version__,
-    packages=['coderedcms'],
+    packages=["coderedcms"],
     include_package_data=True,
-    license='BSD License',
-    description='Wagtail-based CMS by CodeRed for building marketing websites.',
+    license="BSD License",
+    description="Wagtail-based CMS by CodeRed for building marketing websites.",
     long_description=README,
-    long_description_content_type='text/markdown',
-    url='https://github.com/coderedcorp/coderedcms',
-    author='CodeRed LLC',
-    author_email='info@coderedcorp.com',
+    long_description_content_type="text/markdown",
+    url="https://github.com/coderedcorp/coderedcms",
+    author="CodeRed LLC",
+    author_email="info@coderedcorp.com",
     classifiers=[
-        'Environment :: Web Environment',
-        'Framework :: Django',
-        'Intended Audience :: Developers',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3 :: Only',
-        'Framework :: Django',
-        'Framework :: Django :: 3.2',
-        'Framework :: Django :: 4.0',
-        'Framework :: Wagtail',
-        'Framework :: Wagtail :: 2',
-        'Topic :: Internet :: WWW/HTTP',
-        'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
-        'Topic :: Internet :: WWW/HTTP :: Site Management',
+        "Environment :: Web Environment",
+        "Framework :: Django",
+        "Intended Audience :: Developers",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3 :: Only",
+        "Framework :: Django",
+        "Framework :: Django :: 3.2",
+        "Framework :: Django :: 4.0",
+        "Framework :: Wagtail",
+        "Framework :: Wagtail :: 2",
+        "Topic :: Internet :: WWW/HTTP",
+        "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
+        "Topic :: Internet :: WWW/HTTP :: Site Management",
     ],
-    python_requires='>=3.7',
+    python_requires=">=3.7",
     install_requires=[
-        'beautifulsoup4>=4.8,<4.10',    # should be the same as wagtail
-        'django-eventtools==1.0.*',
-        'django-bootstrap5==21.3',
-        'Django>=3.2,<4.1',             # should be the same as wagtail
-        'geocoder==1.38.*',
-        'icalendar==4.1.*',
-        'wagtail==3.*',
-        'wagtail-cache==2.*',
-        'wagtail-seo>=2.2,<3',
+        "beautifulsoup4>=4.8,<4.10",  # should be the same as wagtail
+        "django-eventtools==1.0.*",
+        "django-bootstrap5==21.3",
+        "Django>=3.2,<4.1",  # should be the same as wagtail
+        "geocoder==1.38.*",
+        "icalendar==4.1.*",
+        "wagtail==3.*",
+        "wagtail-cache==2.*",
+        "wagtail-seo>=2.2,<3",
     ],
     entry_points={
-        "console_scripts": [
-            "coderedcms=coderedcms.bin.coderedcms:main"
-        ]
+        "console_scripts": ["coderedcms=coderedcms.bin.coderedcms:main"]
     },
     zip_safe=False,
 )

+ 72 - 82
tutorial/mysite/mysite/settings/base.py

@@ -26,95 +26,87 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 
 INSTALLED_APPS = [
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
-    'wagtail.contrib.forms',
-    'wagtail.contrib.redirects',
-    'wagtail.embeds',
-    'wagtail.sites',
-    'wagtail.users',
-    'wagtail.snippets',
-    'wagtail.documents',
-    'wagtail.images',
-    'wagtail.search',
-    'wagtail.core',
-    'wagtail.contrib.settings',
-    'wagtail.contrib.modeladmin',
-    'wagtail.contrib.table_block',
-    'wagtail.admin',
-
+    "wagtail.contrib.forms",
+    "wagtail.contrib.redirects",
+    "wagtail.embeds",
+    "wagtail.sites",
+    "wagtail.users",
+    "wagtail.snippets",
+    "wagtail.documents",
+    "wagtail.images",
+    "wagtail.search",
+    "wagtail.core",
+    "wagtail.contrib.settings",
+    "wagtail.contrib.modeladmin",
+    "wagtail.contrib.table_block",
+    "wagtail.admin",
     # Django
-    'django.contrib.admin',
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.messages',
-    'django.contrib.staticfiles',
-    'django.contrib.sitemaps',
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+    "django.contrib.sitemaps",
 ]
 
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
-    'django.contrib.sessions.middleware.SessionMiddleware',
-    'django.contrib.messages.middleware.MessageMiddleware',
-    'django.middleware.common.CommonMiddleware',
-
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.common.CommonMiddleware",
     # Security
-    'django.middleware.csrf.CsrfViewMiddleware',
-    'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.middleware.clickjacking.XFrameOptionsMiddleware',
-    'django.middleware.security.SecurityMiddleware',
-
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "django.middleware.security.SecurityMiddleware",
     #  Error reporting. Uncomment this to recieve emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 
-ROOT_URLCONF = 'mysite.urls'
+ROOT_URLCONF = "mysite.urls"
 
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
         },
     },
 ]
 
-WSGI_APPLICATION = 'mysite.wsgi.application'
+WSGI_APPLICATION = "mysite.wsgi.application"
 
 
 # Database
 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
 
 DATABASES = {
-    'default': {
-        'ENGINE': 'django.db.backends.sqlite3',
-        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
     }
 }
 
@@ -123,16 +115,16 @@ DATABASES = {
 
 AUTH_PASSWORD_VALIDATORS = [
     {
-        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
     },
     {
-        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
     },
 ]
 
@@ -140,13 +132,11 @@ AUTH_PASSWORD_VALIDATORS = [
 # https://docs.djangoproject.com/en/3.0/topics/i18n/
 
 # To add or change language of the project, modify the list below.
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
 
-LANGUAGES = [
-    ('en-us', _('English'))
-]
+LANGUAGES = [("en-us", _("English"))]
 
-TIME_ZONE = 'America/New_York'
+TIME_ZONE = "America/New_York"
 
 USE_I18N = True
 
@@ -157,38 +147,38 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/3.0/howto/static-files/
 
 STATICFILES_FINDERS = [
-    'django.contrib.staticfiles.finders.FileSystemFinder',
-    'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, 'static')
-STATIC_URL = '/static/'
+STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_URL = "/static/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
-MEDIA_URL = '/media/'
+MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_URL = "/media/"
 
 
 # Login
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 # Wagtail settings
 
-WAGTAIL_SITE_NAME = 'Simple Sweet Desserts Ltd.'
+WAGTAIL_SITE_NAME = "Simple Sweet Desserts Ltd."
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 WAGTAILSEARCH_BACKENDS = {
-    'default': {
-        'BACKEND': 'wagtail.search.backends.database',
+    "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
-WAGTAILADMIN_BASE_URL = 'http://localhost'
+WAGTAILADMIN_BASE_URL = "http://localhost"
 
 
 # Tags
@@ -196,4 +186,4 @@ WAGTAILADMIN_BASE_URL = 'http://localhost'
 TAGGIT_CASE_INSENSITIVE = True
 
 
-DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

+ 3 - 3
tutorial/mysite/mysite/settings/dev.py

@@ -4,11 +4,11 @@ from .base import *  # noqa
 DEBUG = True
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'abn^vwh^_m31u=sxw)+7ztc^ov&rpi2zc=1o54&m0r+(0m5s*i'
+SECRET_KEY = "abn^vwh^_m31u=sxw)+7ztc^ov&rpi2zc=1o54&m0r+(0m5s*i"
 
-ALLOWED_HOSTS = ['*']
+ALLOWED_HOSTS = ["*"]
 
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 
 WAGTAIL_CACHE = False
 

+ 26 - 23
tutorial/mysite/mysite/settings/prod.py

@@ -4,22 +4,22 @@ from .base import *  # noqa
 DEBUG = False
 
 # SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'abn^vwh^_m31u=sxw)+7ztc^ov&rpi2zc=1o54&m0r+(0m5s*i'
+SECRET_KEY = "abn^vwh^_m31u=sxw)+7ztc^ov&rpi2zc=1o54&m0r+(0m5s*i"
 
 # Add your site's domain name(s) here.
-ALLOWED_HOSTS = ['localhost']
+ALLOWED_HOSTS = ["localhost"]
 
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/3.0/ref/settings/#email-backend
-EMAIL_BACKEND = 'django_sendmail_backend.backends.EmailBackend'
+EMAIL_BACKEND = "django_sendmail_backend.backends.EmailBackend"
 
 # Default email address used to send messages from the website.
-DEFAULT_FROM_EMAIL = 'My Company Inc. <info@localhost>'
+DEFAULT_FROM_EMAIL = "My Company Inc. <info@localhost>"
 
 # A list of people who get error notifications.
 ADMINS = [
-    ('Administrator', 'admin@localhost'),
+    ("Administrator", "admin@localhost"),
 ]
 
 # A list in the same format as ADMINS that specifies who should get broken link
@@ -45,31 +45,34 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
     {
-        'BACKEND': 'django.template.backends.django.DjangoTemplates',
-        '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',
-                'wagtail.contrib.settings.context_processors.settings',
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "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",
+                "wagtail.contrib.settings.context_processors.settings",
             ],
-            'loaders': [
-                ('django.template.loaders.cached.Loader', [
-                    'django.template.loaders.filesystem.Loader',
-                    'django.template.loaders.app_directories.Loader',
-                ]),
+            "loaders": [
+                (
+                    "django.template.loaders.cached.Loader",
+                    [
+                        "django.template.loaders.filesystem.Loader",
+                        "django.template.loaders.app_directories.Loader",
+                    ],
+                ),
             ],
         },
     },
 ]
 
 CACHES = {
-    'default': {
-        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
-        'LOCATION': os.path.join(BASE_DIR, 'cache'),  # noqa
-        'KEY_PREFIX': 'coderedcms',
-        'TIMEOUT': 14400,  # in seconds
+    "default": {
+        "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
+        "LOCATION": os.path.join(BASE_DIR, "cache"),  # noqa
+        "KEY_PREFIX": "coderedcms",
+        "TIMEOUT": 14400,  # in seconds
     }
 }
 

+ 5 - 9
tutorial/mysite/mysite/urls.py

@@ -8,20 +8,16 @@ from coderedcms import urls as codered_urls
 
 urlpatterns = [
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # the page serving mechanism. This should be the last pattern in
     # the list:
-    re_path(r'', include(codered_urls)),
-
+    re_path(r"", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     #    re_path(r"^pages/", include(codered_urls)),

+ 2 - 2
tutorial/mysite/website/apps.py

@@ -2,5 +2,5 @@ from django.apps import AppConfig
 
 
 class WebsiteConfig(AppConfig):
-    default_auto_field = 'django.db.models.AutoField'
-    name = 'website'
+    default_auto_field = "django.db.models.AutoField"
+    name = "website"

+ 36 - 35
tutorial/mysite/website/models.py

@@ -8,71 +8,75 @@ from coderedcms.models import (
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 from django.db import models
 from wagtail.admin.edit_handlers import FieldPanel
 from wagtail.core.fields import RichTextField
 from wagtail.images import get_image_model_string
-from wagtail.images.edit_handlers import ImageChooserPanel
 
 
 class ArticlePage(CoderedArticlePage):
     """
     Article, suitable for news or blog content.
     """
+
     class Meta:
-        verbose_name = 'Article'
-        ordering = ['-first_published_at']
+        verbose_name = "Article"
+        ordering = ["-first_published_at"]
 
     # Only allow this page to be created beneath an ArticleIndexPage.
-    parent_page_types = ['website.ArticleIndexPage']
+    parent_page_types = ["website.ArticleIndexPage"]
 
-    template = 'coderedcms/pages/article_page.html'
-    search_template = 'coderedcms/pages/article_page.search.html'
+    template = "coderedcms/pages/article_page.html"
+    search_template = "coderedcms/pages/article_page.search.html"
 
 
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     Shows a list of article sub-pages.
     """
+
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
     # Only allow ArticlePages beneath this page.
-    subpage_types = ['website.ArticlePage']
+    subpage_types = ["website.ArticlePage"]
 
-    template = 'coderedcms/pages/article_index_page.html'
+    template = "coderedcms/pages/article_index_page.html"
 
 
 class FormPage(CoderedFormPage):
     """
     A page with an html <form>.
     """
+
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 class FormPageField(CoderedFormField):
     """
     A field that links to a FormPage.
     """
+
     class Meta:
-        ordering = ['sort_order']
+        ordering = ["sort_order"]
 
-    page = ParentalKey('FormPage', related_name='form_fields')
+    page = ParentalKey("FormPage", related_name="form_fields")
 
 
 class FormConfirmEmail(CoderedEmail):
     """
     Sends a confirmation email after submitting a FormPage.
     """
-    page = ParentalKey('FormPage', related_name='confirmation_emails')
+
+    page = ParentalKey("FormPage", related_name="confirmation_emails")
 
 
 class WebPage(CoderedWebPage):
@@ -80,26 +84,28 @@ class WebPage(CoderedWebPage):
     General use page with featureful streamfield and SEO attributes.
     Template renders all Navbar and Footer snippets in existence.
     """
+
     class Meta:
-        verbose_name = 'Web Page'
+        verbose_name = "Web Page"
 
-    template = 'coderedcms/pages/web_page.html'
+    template = "coderedcms/pages/web_page.html"
 
 
 class CupcakesIndexPage(CoderedWebPage):
     """
     Landing page for Cupcakes
     """
+
     class Meta:
         verbose_name = "Cupcakes Landing Page"
 
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.CupcakesPage'
+    index_query_pagemodel = "website.CupcakesPage"
 
     # Only allow CupcakesPages beneath this page.
-    subpage_types = ['website.CupcakesPage']
+    subpage_types = ["website.CupcakesPage"]
 
-    template = 'website/pages/cupcakes_index_page.html'
+    template = "website/pages/cupcakes_index_page.html"
 
 
 class CupcakesPage(CoderedWebPage):
@@ -111,35 +117,30 @@ class CupcakesPage(CoderedWebPage):
         verbose_name = "Cupcakes Page"
 
     # Only allow this page to be created beneath an CupcakesIndexPage.
-    parent_page_types = ['website.CupcakesIndexPage']
+    parent_page_types = ["website.CupcakesIndexPage"]
 
     template = "website/pages/cupcakes_page.html"
 
     # The name of the cucpake will be in the page title
     description = RichTextField(
-        verbose_name="Cupcake Description",
-        null=True,
-        blank=True,
-        default=""
+        verbose_name="Cupcake Description", null=True, blank=True, default=""
     )
     photo = models.ForeignKey(
         get_image_model_string(),
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
-        related_name='+',
-        verbose_name='Cupcake Photo',
+        related_name="+",
+        verbose_name="Cupcake Photo",
     )
     DAYS_CHOICES = (
-       ("Weekends Only", "Weekends Only"),
-       ("Monday-Friday", "Monday-Friday"),
-       ("Tuesday/Thursday", "Tuesday/Thursday"),
-       ("Seasonal", "Seasonal"),
+        ("Weekends Only", "Weekends Only"),
+        ("Monday-Friday", "Monday-Friday"),
+        ("Tuesday/Thursday", "Tuesday/Thursday"),
+        ("Seasonal", "Seasonal"),
     )
     days_available = models.CharField(
-        choices=DAYS_CHOICES,
-        max_length=20,
-        default=""
+        choices=DAYS_CHOICES, max_length=20, default=""
     )
 
     # Add custom fields to the body

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott